diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index ac95ca561..b30c07afb 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -1,7 +1,7 @@ import { assertAbsolutePath } from '@h5web/shared'; -import { useState, Suspense } from 'react'; +import { Suspense, useContext, useState } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; -import { ReflexContainer, ReflexSplitter, ReflexElement } from 'react-reflex'; +import { ReflexContainer, ReflexElement, ReflexSplitter } from 'react-reflex'; import styles from './App.module.css'; import ErrorFallback from './ErrorFallback'; @@ -10,6 +10,7 @@ import VisConfigProvider from './VisConfigProvider'; import BreadcrumbsBar from './breadcrumbs/BreadcrumbsBar'; import Explorer from './explorer/Explorer'; import MetadataViewer from './metadata-viewer/MetadataViewer'; +import { ProviderContext } from './providers/context'; import Visualizer from './visualizer/Visualizer'; const DEFAULT_PATH = process.env.REACT_APP_DEFAULT_PATH || '/'; @@ -25,6 +26,13 @@ function App(props: Props) { const [isExplorerOpen, setExplorerOpen] = useState(!startFullscreen); const [isInspecting, setInspecting] = useState(false); + const { valuesStore } = useContext(ProviderContext); + function onSelectPath(path: string) { + setSelectedPath(path); + valuesStore.cancelOngoing(); + valuesStore.evictCancelled(); + } + return ( @@ -34,7 +42,7 @@ function App(props: Props) { flex={25} minSize={150} > - + setExplorerOpen(!isExplorerOpen)} onChangeInspecting={setInspecting} - onSelectPath={setSelectedPath} + onSelectPath={onSelectPath} /> ) : ( diff --git a/packages/app/src/__tests__/Visualizer.test.tsx b/packages/app/src/__tests__/Visualizer.test.tsx index e50969295..fb68d7e20 100644 --- a/packages/app/src/__tests__/Visualizer.test.tsx +++ b/packages/app/src/__tests__/Visualizer.test.tsx @@ -195,3 +195,60 @@ test('retry fetching dataset slice automatically when re-selecting slice', async jest.runOnlyPendingTimers(); jest.useRealTimers(); }); + +test('cancel fetching dataset slice when changing entity', async () => { + jest.useFakeTimers('modern'); + await renderApp(); + + // Select dataset and start fetching first slice + await selectExplorerNode('resilience/slow_slicing'); + expect(await screen.findByText(/Loading current slice/)).toBeVisible(); + + // Switch to another entity to cancel the fetch + await selectExplorerNode('resilience/slow_value'); + expect(await screen.findByText(/Loading data/)).toBeVisible(); + + // Let pending requests succeed + jest.runAllTimers(); + + // Reselect dataset and check that it refetches the first slice + await selectExplorerNode('resilience/slow_slicing'); + // The slice request was cancelled so it should be pending once again + expect(await screen.findByText(/Loading current slice/)).toBeVisible(); + + // Let fetch of first slice succeed + jest.runAllTimers(); + expect(await screen.findByRole('figure')).toBeVisible(); + + jest.runOnlyPendingTimers(); + jest.useRealTimers(); +}); + +test('cancel fetching dataset slice when changing vis', async () => { + jest.useFakeTimers('modern'); + await renderApp(); + + // Select dataset and start fetching the slice + await selectExplorerNode('resilience/slow_slicing'); + expect(await screen.findByText(/Loading current slice/)).toBeVisible(); + + // Switch to the Line vis to cancel the fetch + userEvent.click(screen.getByRole('tab', { name: 'Line' })); + expect(await screen.findByText(/Loading current slice/)).toBeVisible(); + + // Let pending requests succeed + jest.runAllTimers(); + expect(await screen.findByRole('figure')).toBeVisible(); + + // Switch back to Heatmap and check that it refetches the slice + userEvent.click(screen.getByRole('tab', { name: 'Heatmap' })); + // The slice request was cancelled so it should be pending once again + expect(await screen.findByText(/Loading current slice/)).toBeVisible(); + + // Let fetch of the slice succeed + jest.runAllTimers(); + expect(await screen.findByRole('figure')).toBeVisible(); + + jest.runOnlyPendingTimers(); + jest.useRealTimers(); +}); diff --git a/packages/app/src/visualizer/VisManager.tsx b/packages/app/src/visualizer/VisManager.tsx index 418933c36..f60ad59d3 100644 --- a/packages/app/src/visualizer/VisManager.tsx +++ b/packages/app/src/visualizer/VisManager.tsx @@ -1,8 +1,9 @@ import type { Entity } from '@h5web/shared'; import { assertDefined } from '@h5web/shared'; -import { useState } from 'react'; +import { useContext, useState } from 'react'; import Profiler from '../Profiler'; +import { ProviderContext } from '../providers/context'; import type { VisDef } from '../vis-packs/models'; import VisSelector from './VisSelector'; import styles from './Visualizer.module.css'; @@ -28,6 +29,13 @@ function VisManager(props: Props) { const [visBarElem, setVisBarElem] = useState(); + const { valuesStore } = useContext(ProviderContext); + function onVisChange(vis: VisDef) { + setActiveVis(vis); + valuesStore.cancelOngoing(); + valuesStore.evictCancelled(); + } + return (