diff --git a/cypress/e2e/app.cy.ts b/cypress/e2e/app.cy.ts index b9d03f07a..9bcf012e9 100644 --- a/cypress/e2e/app.cy.ts +++ b/cypress/e2e/app.cy.ts @@ -162,7 +162,7 @@ describe('/mock', () => { .and('have.attr', 'aria-valuemax', 8); // Move slider up by three marks and check value - cy.get('@d1Slider').type('{uparrow}{uparrow}{uparrow}'); + cy.get('@d1Slider').type('{upArrow}{upArrow}{upArrow}'); cy.waitForStableDOM(); cy.get('@d1Slider').should('have.attr', 'aria-valuenow', 3); diff --git a/packages/app/src/EntityLoader.tsx b/packages/app/src/EntityLoader.tsx index a5c433a6a..2ac013552 100644 --- a/packages/app/src/EntityLoader.tsx +++ b/packages/app/src/EntityLoader.tsx @@ -19,6 +19,7 @@ function EntityLoader(props: Props) {
{isReady &&{message}...
} > diff --git a/packages/app/src/__tests__/CorePack.test.tsx b/packages/app/src/__tests__/CorePack.test.tsx index 7e5cb2167..7a2446c70 100644 --- a/packages/app/src/__tests__/CorePack.test.tsx +++ b/packages/app/src/__tests__/CorePack.test.tsx @@ -1,68 +1,59 @@ import { screen, within } from '@testing-library/react'; -import { findSelectedVisTab, findVisTabs, renderApp } from '../test-utils'; +import { SLOW_TIMEOUT } from '../providers/mock/mock-api'; +import { getSelectedVisTab, getVisTabs, renderApp } from '../test-utils'; import { Vis } from '../vis-packs/core/visualizations'; test('visualize raw dataset', async () => { const { selectExplorerNode } = await renderApp('/entities/raw'); - await expect(findVisTabs()).resolves.toEqual([Vis.Raw]); - await expect(findSelectedVisTab()).resolves.toBe(Vis.Raw); - await expect(screen.findByText(/"int": 42/)).resolves.toBeVisible(); + expect(getVisTabs()).toEqual([Vis.Raw]); + expect(getSelectedVisTab()).toBe(Vis.Raw); + expect(screen.getByText(/"int": 42/)).toBeVisible(); await selectExplorerNode('raw_large'); - await expect(screen.findByText(/Too big to display/)).resolves.toBeVisible(); + expect(screen.getByText(/Too big to display/)).toBeVisible(); }); test('visualize scalar dataset', async () => { // Integer scalar const { selectExplorerNode } = await renderApp('/entities/scalar_int'); - await expect(findVisTabs()).resolves.toEqual([Vis.Scalar]); - await expect(findSelectedVisTab()).resolves.toBe(Vis.Scalar); - await expect(screen.findByText('0')).resolves.toBeVisible(); + expect(getVisTabs()).toEqual([Vis.Scalar]); + expect(getSelectedVisTab()).toBe(Vis.Scalar); + expect(screen.getByText('0')).toBeVisible(); // String scalar await selectExplorerNode('scalar_str'); - await expect(findVisTabs()).resolves.toEqual([Vis.Scalar]); - await expect(findSelectedVisTab()).resolves.toBe(Vis.Scalar); - await expect(screen.findByText('foo')).resolves.toBeVisible(); + expect(getVisTabs()).toEqual([Vis.Scalar]); + expect(getSelectedVisTab()).toBe(Vis.Scalar); + expect(screen.getByText('foo')).toBeVisible(); }); test('visualize 1D dataset', async () => { await renderApp('/nD_datasets/oneD'); - await expect(findVisTabs()).resolves.toEqual([Vis.Matrix, Vis.Line]); - await expect(findSelectedVisTab()).resolves.toBe(Vis.Line); - - await expect( - screen.findByRole('figure', { name: 'oneD' }), - ).resolves.toBeVisible(); + expect(getVisTabs()).toEqual([Vis.Matrix, Vis.Line]); + expect(getSelectedVisTab()).toBe(Vis.Line); + expect(screen.getByRole('figure', { name: 'oneD' })).toBeVisible(); }); test('visualize 1D complex dataset', async () => { await renderApp('/nD_datasets/oneD_cplx'); - await expect(findVisTabs()).resolves.toEqual([Vis.Matrix, Vis.Line]); - await expect(findSelectedVisTab()).resolves.toBe(Vis.Line); - - await expect( - screen.findByRole('figure', { name: 'oneD_cplx' }), - ).resolves.toBeVisible(); + expect(getVisTabs()).toEqual([Vis.Matrix, Vis.Line]); + expect(getSelectedVisTab()).toBe(Vis.Line); + expect(screen.getByRole('figure', { name: 'oneD_cplx' })).toBeVisible(); }); -test('visualize 2D datasets', async () => { +test('visualize 2D dataset', async () => { await renderApp('/nD_datasets/twoD'); - await expect(findVisTabs()).resolves.toEqual([ - Vis.Matrix, - Vis.Line, - Vis.Heatmap, - ]); - await expect(findSelectedVisTab()).resolves.toBe(Vis.Heatmap); + expect(getVisTabs()).toEqual([Vis.Matrix, Vis.Line, Vis.Heatmap]); + expect(getSelectedVisTab()).toBe(Vis.Heatmap); - const figure = await screen.findByRole('figure', { name: 'twoD' }); + const figure = screen.getByRole('figure', { name: 'twoD' }); expect(figure).toBeVisible(); expect(within(figure).getByText('4e+2')).toBeVisible(); // color bar limit }); @@ -70,16 +61,10 @@ test('visualize 2D datasets', async () => { test('visualize 2D complex dataset', async () => { const { user } = await renderApp('/nD_datasets/twoD_cplx'); - await expect(findVisTabs()).resolves.toEqual([ - Vis.Matrix, - Vis.Line, - Vis.Heatmap, - ]); - await expect(findSelectedVisTab()).resolves.toBe(Vis.Heatmap); + expect(getVisTabs()).toEqual([Vis.Matrix, Vis.Line, Vis.Heatmap]); + expect(getSelectedVisTab()).toBe(Vis.Heatmap); - const figure = await screen.findByRole('figure', { - name: 'twoD_cplx (amplitude)', - }); + const figure = screen.getByRole('figure', { name: 'twoD_cplx (amplitude)' }); expect(figure).toBeVisible(); expect(within(figure).getByText('5e+0')).toBeVisible(); // color bar limit @@ -106,8 +91,9 @@ test('visualize 1D slice of 3D dataset as Line with and without autoscale', asyn ).resolves.toBeVisible(); // Wait for fetch of first slice to succeed - jest.runAllTimers(); - await expect(screen.findByRole('figure')).resolves.toBeVisible(); + await expect( + screen.findByRole('figure', undefined, { timeout: SLOW_TIMEOUT }), + ).resolves.toBeVisible(); // Confirm that autoscale is indeed on const autoScaleBtn = screen.getByRole('button', { name: 'Auto-scale' }); @@ -123,8 +109,9 @@ test('visualize 1D slice of 3D dataset as Line with and without autoscale', asyn ).resolves.toBeVisible(); // Wait for fetch of second slice to succeed - jest.runAllTimers(); - await expect(screen.findByRole('figure')).resolves.toBeVisible(); + await expect( + screen.findByRole('figure', undefined, { timeout: SLOW_TIMEOUT }), + ).resolves.toBeVisible(); // Activate autoscale await user.click(autoScaleBtn); @@ -135,17 +122,15 @@ test('visualize 1D slice of 3D dataset as Line with and without autoscale', asyn ).resolves.toBeVisible(); // Wait for fetch of entire dataset to succeed - jest.runAllTimers(); - await expect(screen.findByRole('figure')).resolves.toBeVisible(); + await expect( + screen.findByRole('figure', undefined, { timeout: SLOW_TIMEOUT }), + ).resolves.toBeVisible(); // Move to third slice await user.type(d0Slider, '{ArrowUp}'); // Wait for new slicing to apply to Line visualization to confirm that no more slow fetching is performed await expect(screen.findByTestId('2,0,x', undefined)).resolves.toBeVisible(); - - d0Slider.blur(); // remove focus to avoid state update after unmount - jest.runAllTimers(); }); test('show interactions help for heatmap according to "keep ratio"', async () => { diff --git a/packages/app/src/__tests__/DomainWidget.test.tsx b/packages/app/src/__tests__/DomainWidget.test.tsx index d335d3437..7dfdfae6c 100644 --- a/packages/app/src/__tests__/DomainWidget.test.tsx +++ b/packages/app/src/__tests__/DomainWidget.test.tsx @@ -5,12 +5,12 @@ import { renderApp } from '../test-utils'; test('show slider with two thumbs and reveal popup on hover', async () => { const { user } = await renderApp('/nexus_entry/nx_process/nx_data'); - const thumbs = await screen.findAllByRole('slider'); + const thumbs = screen.getAllByRole('slider'); expect(thumbs).toHaveLength(2); expect(thumbs[0]).toHaveAttribute('aria-valuenow', '20'); expect(thumbs[1]).toHaveAttribute('aria-valuenow', '81'); - const editBtn = await screen.findByRole('button', { name: 'Edit domain' }); + const editBtn = screen.getByRole('button', { name: 'Edit domain' }); const popup = screen.getByRole('dialog', { hidden: true }); expect(editBtn).toHaveAttribute('aria-expanded', 'false'); @@ -35,8 +35,8 @@ test('show slider with two thumbs and reveal popup on hover', async () => { test('show min/max and data range in popup', async () => { const { user } = await renderApp('/nexus_entry/nx_process/nx_data'); - // Hover edit button to reveal popup - const editBtn = await screen.findByRole('button', { name: 'Edit domain' }); + // Hover edit button to reveal tooltip + const editBtn = screen.getByRole('button', { name: 'Edit domain' }); await user.hover(editBtn); const minInput = screen.getByLabelText('min'); @@ -55,11 +55,11 @@ test('show min/max and data range in popup', async () => { test('move thumbs with keyboard to update domain', async () => { const { user } = await renderApp('/nexus_entry/nx_process/nx_data'); - // Hover min thumb to reveal popup - const minThumb = await screen.findByRole('slider', { name: /min/ }); + // Hover min thumb to reveal tooltip + const minThumb = screen.getByRole('slider', { name: /min/ }); await user.hover(minThumb); - const visArea = await screen.findByRole('figure'); + const visArea = screen.getByRole('figure'); const minInput = screen.getByLabelText('min'); const maxInput = screen.getByLabelText('max'); @@ -77,7 +77,7 @@ test('move thumbs with keyboard to update domain', async () => { expect(maxInput).toHaveValue('4e+2'); // unaffected // Move max thumb ten steps to the left - const maxThumb = await screen.findByRole('slider', { name: /max/ }); + const maxThumb = screen.getByRole('slider', { name: /max/ }); await user.type(maxThumb, '{PageDown}'); expect(maxInput).toHaveValue('5.72182e+1'); expect(within(visArea).getByText('5.722e+1')).toBeVisible(); @@ -85,26 +85,22 @@ test('move thumbs with keyboard to update domain', async () => { // Move max thumb ten steps to the right await user.type(maxThumb, '{PageUp}'); expect(maxInput).toHaveValue('3.68841e+2'); // not back to 4e+2 because 4e+2 is not exactly on a slider division - expect(within(visArea).getByText('3.688e+2')).toBeVisible(); + expect(within(visArea).getByText('3.688e+2')).toBeInTheDocument(); expect(maxThumb).toHaveAttribute('aria-valuenow', '81'); // still at original position expect(minInput).toHaveValue('−2.30818e+2'); // unaffected - - // Remove focus now to avoid state update after unmount - maxThumb.blur(); }); test('edit bounds manually', async () => { const { user } = await renderApp('/nexus_entry/nx_process/nx_data'); - const editBtn = await screen.findByRole('button', { name: 'Edit domain' }); + const editBtn = screen.getByRole('button', { name: 'Edit domain' }); expect(editBtn).toHaveAttribute('aria-pressed', 'false'); // Click on edit button to open popup with both min and max in edit mode await user.click(editBtn); expect(editBtn).toHaveAttribute('aria-pressed', 'true'); expect(editBtn).toHaveAttribute('aria-expanded', 'true'); - const minInput = screen.getByLabelText('min'); const maxInput = screen.getByLabelText('max'); const applyMinBtn = screen.getByRole('button', { name: 'Apply min' }); @@ -117,7 +113,7 @@ test('edit bounds manually', async () => { await user.type(minInput, '1'); expect(minInput).toHaveValue('−9.5e+11'); - const visArea = await screen.findByRole('figure'); + const visArea = screen.getByRole('figure'); expect(within(visArea).getByText('−9.5e+1')).toBeVisible(); // not applied yet // Cancel min edit @@ -132,9 +128,13 @@ test('edit bounds manually', async () => { expect(within(visArea).getByText('−9.5e+11')).toBeVisible(); // applied expect(editBtn).toHaveAttribute('aria-pressed', 'true'); // because max still in edit mode - // Replace value of max input field and apply new max with Enter + // Replace value of max input field and apply new max await user.clear(maxInput); - await user.type(maxInput, '100000{enter}'); + // Submiting `BoundEditor` form with Enter key leads to `act` warning + // https://github.com/testing-library/user-event/discussions/964 + // await user.type(maxInput, '100000{Enter}'); + await user.type(maxInput, '100000'); + await user.click(screen.getByRole('button', { name: 'Apply max' })); expect(maxInput).toHaveValue('1e+5'); // auto-format expect(within(visArea).getByText('1e+5')).toBeVisible(); @@ -144,34 +144,33 @@ test('edit bounds manually', async () => { test('clamp domain in symlog scale', async () => { const { user } = await renderApp('/nexus_entry/nx_process/nx_data'); - await user.click(await screen.findByRole('button', { name: 'Edit domain' })); + await user.click(screen.getByRole('button', { name: 'Edit domain' })); const minThumb = screen.getByRole('slider', { name: /min/ }); const maxThumb = screen.getByRole('slider', { name: /max/ }); const minInput = screen.getByLabelText('min'); const maxInput = screen.getByLabelText('max'); await user.clear(minInput); - await user.type(minInput, '-1e+1000{enter}'); + await user.type(minInput, '-1e+1000'); + await user.click(screen.getByRole('button', { name: 'Apply min' })); expect(minInput).toHaveValue('−8.98847e+307'); expect(minThumb).toHaveAttribute('aria-valuenow', '1'); await user.clear(maxInput); - await user.type(maxInput, '1e+1000{enter}'); + await user.type(maxInput, '1e+1000'); + await user.click(screen.getByRole('button', { name: 'Apply max' })); expect(maxInput).toHaveValue('8.98847e+307'); expect(maxThumb).toHaveAttribute('aria-valuenow', '100'); await user.type(maxThumb, '{ArrowLeft}'); expect(maxInput).toHaveValue('5.40006e+301'); expect(maxThumb).toHaveAttribute('aria-valuenow', '99'); // does not jump back to 81 - - // Remove focus now to avoid state update after unmount - maxThumb.blur(); }); test('control min/max autoscale behaviour', async () => { const { user } = await renderApp('/nexus_entry/nx_process/nx_data'); - const minThumb = await screen.findByRole('slider', { name: /min/ }); + const minThumb = screen.getByRole('slider', { name: /min/ }); await user.hover(minThumb); const minBtn = screen.getByRole('button', { name: 'Min' }); @@ -188,7 +187,8 @@ test('control min/max autoscale behaviour', async () => { expect(maxBtn).toHaveAttribute('aria-pressed', 'true'); // unaffected // Editing max disables max autoscale - await user.type(maxInput, '0{enter}'); + await user.type(maxInput, '0'); + await user.click(screen.getByRole('button', { name: 'Apply max' })); expect(maxInput).toHaveValue('4e+20'); expect(maxBtn).toHaveAttribute('aria-pressed', 'false'); @@ -202,7 +202,7 @@ test('control min/max autoscale behaviour', async () => { test('handle empty domain', async () => { const { user } = await renderApp('/nexus_entry/nx_process/nx_data'); - await user.click(await screen.findByRole('button', { name: 'Edit domain' })); + await user.click(screen.getByRole('button', { name: 'Edit domain' })); const minInput = screen.getByLabelText('min'); const maxInput = screen.getByLabelText('max'); const minThumb = screen.getByRole('slider', { name: /min/ }); @@ -210,11 +210,12 @@ test('handle empty domain', async () => { // Give min the same value as max await user.clear(minInput); - await user.type(minInput, '400{enter}'); + await user.type(minInput, '400'); + await user.click(screen.getByRole('button', { name: 'Apply min' })); expect(minThumb).toHaveAttribute('aria-valuenow', '81'); expect(maxThumb).toHaveAttribute('aria-valuenow', '81'); - const visArea = await screen.findByRole('figure'); + const visArea = screen.getByRole('figure'); expect(within(visArea).getByText('−∞')).toBeVisible(); expect(within(visArea).getByText('+∞')).toBeVisible(); @@ -230,27 +231,25 @@ test('handle empty domain', async () => { expect(maxInput).toHaveValue('3.71154e+2'); expect(minThumb).toHaveAttribute('aria-valuenow', '80'); expect(maxThumb).toHaveAttribute('aria-valuenow', '81'); - - // Remove focus now to avoid state update after unmount - maxThumb.blur(); }); test('handle min > max', async () => { const { user } = await renderApp('/nexus_entry/nx_process/nx_data'); - await user.click(await screen.findByRole('button', { name: 'Edit domain' })); + await user.click(screen.getByRole('button', { name: 'Edit domain' })); const minInput = screen.getByLabelText('min'); const maxInput = screen.getByLabelText('max'); await user.clear(minInput); - await user.type(minInput, '500{enter}'); + await user.type(minInput, '500'); + await user.click(screen.getByRole('button', { name: 'Apply min' })); expect(minInput).toHaveValue('5e+2'); expect(maxInput).toHaveValue('4e+2'); expect(screen.getByText(/Min greater than max/)).toHaveTextContent( /falling back to data range/, ); - const visArea = await screen.findByRole('figure'); + const visArea = screen.getByRole('figure'); expect(within(visArea).getByText('−9.5e+1')).toBeVisible(); expect(within(visArea).getByText('4e+2')).toBeVisible(); @@ -263,9 +262,7 @@ test('handle min or max <= 0 in log scale', async () => { const { user } = await renderApp('/nexus_entry/image'); // Ensure the scale type is log - await expect( - screen.findByRole('button', { name: 'Log' }), - ).resolves.toBeVisible(); + expect(screen.getByRole('button', { name: 'Log' })).toBeVisible(); const editBtn = screen.getByRole('button', { name: 'Edit domain' }); await user.click(editBtn); @@ -273,18 +270,20 @@ test('handle min or max <= 0 in log scale', async () => { const maxInput = screen.getByLabelText('max'); await user.clear(minInput); - await user.type(minInput, '-5{enter}'); + await user.type(minInput, '-5'); + await user.click(screen.getByRole('button', { name: 'Apply min' })); expect(minInput).toHaveValue('−5e+0'); expect(screen.getByText(/Custom min invalid/)).toHaveTextContent( /falling back to data min/, ); - const visArea = await screen.findByRole('figure'); + const visArea = screen.getByRole('figure'); expect(within(visArea).getByText('9.996e-1')).toBeVisible(); // data max // If min and max are negative and min > max, min > max error and fallback take over await user.clear(maxInput); - await user.type(maxInput, '-10{enter}'); + await user.type(maxInput, '-10'); + await user.click(screen.getByRole('button', { name: 'Apply max' })); expect(screen.queryByText(/Custom min invalid/)).not.toBeInTheDocument(); expect(screen.queryByText(/Custom max invalid/)).not.toBeInTheDocument(); expect(screen.getByText(/Min greater than max/)).toHaveTextContent( @@ -296,26 +295,26 @@ test('handle min <= 0 with custom max fallback in log scale', async () => { const { user } = await renderApp('/nexus_entry/image'); // Ensure the scale type is log - await expect( - screen.findByRole('button', { name: 'Log' }), - ).resolves.toBeVisible(); + expect(screen.getByRole('button', { name: 'Log' })).toBeVisible(); await user.click(screen.getByRole('button', { name: 'Edit domain' })); const minInput = screen.getByLabelText('min'); const maxInput = screen.getByLabelText('max'); await user.clear(minInput); - await user.type(minInput, '-5{enter}'); + await user.type(minInput, '-5'); + await user.click(screen.getByRole('button', { name: 'Apply min' })); await user.clear(maxInput); - await user.type(maxInput, '1e-4{enter}'); // lower than data min + await user.type(maxInput, '1e-4'); // lower than data min + await user.click(screen.getByRole('button', { name: 'Apply max' })); expect(screen.getByText(/Custom min invalid/)).toHaveTextContent( /falling back to custom max/, ); // Min fallback = custom max, so domain is empty - const visArea = await screen.findByRole('figure'); + const visArea = screen.getByRole('figure'); expect(within(visArea).getByText('−∞')).toBeVisible(); expect(within(visArea).getByText('+∞')).toBeVisible(); }); diff --git a/packages/app/src/__tests__/Explorer.test.tsx b/packages/app/src/__tests__/Explorer.test.tsx index 7de7436b0..bbc39298c 100644 --- a/packages/app/src/__tests__/Explorer.test.tsx +++ b/packages/app/src/__tests__/Explorer.test.tsx @@ -1,12 +1,13 @@ import { mockFilepath } from '@h5web/shared'; -import { screen, waitFor } from '@testing-library/react'; +import { screen } from '@testing-library/react'; +import { SLOW_TIMEOUT } from '../providers/mock/mock-api'; import { renderApp } from '../test-utils'; test('select root group by default', async () => { await renderApp(); - const title = await screen.findByRole('heading', { name: mockFilepath }); + const title = screen.getByRole('heading', { name: mockFilepath }); expect(title).toBeVisible(); const fileBtn = screen.getByRole('treeitem', { name: mockFilepath }); @@ -17,9 +18,7 @@ test('select root group by default', async () => { test('toggle sidebar', async () => { const { user } = await renderApp(); - const fileBtn = await screen.findByRole('treeitem', { - name: mockFilepath, - }); + const fileBtn = screen.getByRole('treeitem', { name: mockFilepath }); const sidebarBtn = screen.getByRole('button', { name: 'Toggle sidebar', }); @@ -39,25 +38,21 @@ test('toggle sidebar', async () => { test('navigate groups in explorer', async () => { const { selectExplorerNode } = await renderApp(); - const groupBtn = await screen.findByRole('treeitem', { name: 'entities' }); + const groupBtn = screen.getByRole('treeitem', { name: 'entities' }); expect(groupBtn).toHaveAttribute('aria-selected', 'false'); expect(groupBtn).toHaveAttribute('aria-expanded', 'false'); // Expand `entities` group await selectExplorerNode('entities'); - expect(groupBtn).toHaveAttribute('aria-selected', 'true'); expect(groupBtn).toHaveAttribute('aria-expanded', 'true'); - const childGroupBtn = await screen.findByRole('treeitem', { - name: 'empty_group', - }); + const childGroupBtn = screen.getByRole('treeitem', { name: 'empty_group' }); expect(childGroupBtn).toHaveAttribute('aria-selected', 'false'); expect(childGroupBtn).toHaveAttribute('aria-expanded', 'false'); // Expand `empty_group` child group await selectExplorerNode('empty_group'); - expect(groupBtn).toHaveAttribute('aria-selected', 'false'); expect(groupBtn).toHaveAttribute('aria-expanded', 'true'); expect(childGroupBtn).toHaveAttribute('aria-selected', 'true'); @@ -65,20 +60,17 @@ test('navigate groups in explorer', async () => { // Collapse `empty_group` child group await selectExplorerNode('empty_group'); - expect(childGroupBtn).toHaveAttribute('aria-selected', 'true'); expect(childGroupBtn).toHaveAttribute('aria-expanded', 'false'); // Select `entities` group await selectExplorerNode('entities'); - expect(groupBtn).toHaveAttribute('aria-selected', 'true'); expect(groupBtn).toHaveAttribute('aria-expanded', 'true'); // remains expanded as it wasn't previously selected expect(childGroupBtn).toHaveAttribute('aria-selected', 'false'); // Collapse `entities` group await selectExplorerNode('entities'); - expect( screen.queryByRole('treeitem', { name: 'empty_group' }), ).not.toBeInTheDocument(); @@ -92,13 +84,21 @@ test('show spinner when group metadata is slow to fetch', async () => { withFakeTimers: true, }); - await expect(screen.findByText(/Loading/)).resolves.toBeVisible(); - expect(screen.getByLabelText(/Loading group metadata/)).toBeVisible(); + // Wait for `slow_metadata` group to appear in explorer (i.e. for root and `resilience` groups to finish loading) + await expect( + screen.findByRole('treeitem', { name: 'slow_metadata' }), + ).resolves.toBeVisible(); - jest.runAllTimers(); // resolve slow fetch right away - await waitFor(() => { - expect( - screen.queryByLabelText(/Loading group metadata/), - ).not.toBeInTheDocument(); - }); + // Ensure explorer now shows loading spinner (i.e. for `slow_metadata` group) + expect(screen.getByLabelText(/Loading group/)).toBeVisible(); + + // Wait for fetch of group metadata to succeed + await expect( + screen.findByText(/No visualization available/, undefined, { + timeout: SLOW_TIMEOUT, + }), + ).resolves.toBeVisible(); + + // Spinner has been removed + expect(screen.queryByLabelText(/Loading group/)).not.toBeInTheDocument(); }); diff --git a/packages/app/src/__tests__/MetadataViewer.test.tsx b/packages/app/src/__tests__/MetadataViewer.test.tsx index 697c8b5b2..e6458ab69 100644 --- a/packages/app/src/__tests__/MetadataViewer.test.tsx +++ b/packages/app/src/__tests__/MetadataViewer.test.tsx @@ -8,21 +8,17 @@ test('switch between "display" and "inspect" modes', async () => { // Switch to "inspect" mode await user.click(screen.getByRole('tab', { name: 'Inspect' })); expect(screen.getByRole('row', { name: /^Path/ })).toBeVisible(); - expect( - screen.queryByRole('tablist', { name: 'Visualization' }), - ).not.toBeInTheDocument(); // Switch back to "display" mode await user.click(screen.getByRole('tab', { name: 'Display' })); expect(screen.queryByRole('row', { name: /^Path/ })).not.toBeInTheDocument(); - expect(screen.getByRole('tablist', { name: 'Visualization' })).toBeVisible(); }); test('inspect group', async () => { const { user } = await renderApp('/entities'); - await user.click(await screen.findByRole('tab', { name: 'Inspect' })); + await user.click(screen.getByRole('tab', { name: 'Inspect' })); - const column = await screen.findByRole('columnheader', { name: /^Group/ }); + const column = screen.getByRole('columnheader', { name: /^Group/ }); const nameRow = screen.getByRole('row', { name: /^Name/ }); const pathRow = screen.getByRole('row', { name: /^Path/ }); @@ -33,11 +29,9 @@ test('inspect group', async () => { test('inspect scalar dataset', async () => { const { user } = await renderApp('/entities/scalar_int'); - await user.click(await screen.findByRole('tab', { name: 'Inspect' })); + await user.click(screen.getByRole('tab', { name: 'Inspect' })); - const column = await screen.findByRole('columnheader', { - name: /Dataset/, - }); + const column = screen.getByRole('columnheader', { name: /Dataset/ }); const nameRow = screen.getByRole('row', { name: /^Name/ }); const pathRow = screen.getByRole('row', { name: /^Path/ }); const shapeRow = screen.getByRole('row', { name: /^Shape/ }); @@ -52,30 +46,27 @@ test('inspect scalar dataset', async () => { test('inspect array dataset', async () => { const { user } = await renderApp('/nD_datasets/threeD'); - await user.click(await screen.findByRole('tab', { name: 'Inspect' })); + await user.click(screen.getByRole('tab', { name: 'Inspect' })); - const shapeRow = await screen.findByRole('row', { name: /^Shape/ }); + const shapeRow = screen.getByRole('row', { name: /^Shape/ }); expect(shapeRow).toHaveTextContent(/9 x 20 x 41 = 7380/); }); test('inspect empty dataset', async () => { const { user } = await renderApp('/entities/empty_dataset'); - await user.click(await screen.findByRole('tab', { name: 'Inspect' })); + await user.click(screen.getByRole('tab', { name: 'Inspect' })); - const shapeRow = await screen.findByRole('row', { name: /^Shape/ }); + const shapeRow = screen.getByRole('row', { name: /^Shape/ }); const typeRow = screen.getByRole('row', { name: /^Type/ }); - expect(shapeRow).toHaveTextContent(/None/); expect(typeRow).toHaveTextContent(/Integer, 32-bit, little-endian/); }); test('inspect datatype', async () => { const { user } = await renderApp('/entities/datatype'); - await user.click(await screen.findByRole('tab', { name: 'Inspect' })); + await user.click(screen.getByRole('tab', { name: 'Inspect' })); - const column = await screen.findByRole('columnheader', { - name: /Datatype/, - }); + const column = screen.getByRole('columnheader', { name: /Datatype/ }); const nameRow = screen.getByRole('row', { name: /^Name/ }); const pathRow = screen.getByRole('row', { name: /^Path/ }); const typeRow = screen.getByRole('row', { name: /^Type/ }); @@ -88,9 +79,9 @@ test('inspect datatype', async () => { test('inspect unresolved soft link', async () => { const { user } = await renderApp('/entities/unresolved_soft_link'); - await user.click(await screen.findByRole('tab', { name: 'Inspect' })); + await user.click(screen.getByRole('tab', { name: 'Inspect' })); - const column = await screen.findByRole('columnheader', { name: /Entity/ }); + const column = screen.getByRole('columnheader', { name: /Entity/ }); const nameRow = screen.getByRole('row', { name: /^Name/ }); const pathRow = screen.getByRole('row', { name: /^Path/ }); const linkRow = screen.getByRole('row', { name: /^Soft link/ }); @@ -103,27 +94,22 @@ test('inspect unresolved soft link', async () => { test('inspect unresolved external link', async () => { const { user } = await renderApp('/entities/unresolved_external_link'); - await user.click(await screen.findByRole('tab', { name: 'Inspect' })); + await user.click(screen.getByRole('tab', { name: 'Inspect' })); - const column = await screen.findByRole('columnheader', { name: /Entity/ }); + const column = screen.getByRole('columnheader', { name: /Entity/ }); const linkRow = screen.getByRole('row', { name: /^External link/ }); - expect(column).toBeVisible(); expect(linkRow).toHaveTextContent(/my_file.h5:entry_000\/dataset/); }); test('follow path attributes', async () => { const { user, selectExplorerNode } = await renderApp(); - await user.click(await screen.findByRole('tab', { name: 'Inspect' })); + await user.click(screen.getByRole('tab', { name: 'Inspect' })); // Follow relative `default` attribute - await user.click( - await screen.findByRole('button', { name: 'Inspect nexus_entry' }), - ); + await user.click(screen.getByRole('button', { name: 'Inspect nexus_entry' })); - const nxEntry = await screen.findByRole('treeitem', { - name: /^nexus_entry /, - }); + const nxEntry = screen.getByRole('treeitem', { name: /^nexus_entry / }); expect(nxEntry).toHaveAttribute('aria-selected', 'true'); expect(nxEntry).toHaveAttribute('aria-expanded', 'true'); @@ -137,7 +123,7 @@ test('follow path attributes', async () => { }), ); - const nxData = await screen.findByRole('treeitem', { name: /nx_data/ }); + const nxData = screen.getByRole('treeitem', { name: /nx_data/ }); expect(nxData).toHaveAttribute('aria-selected', 'true'); expect(nxData).toHaveAttribute('aria-expanded', 'true'); }); diff --git a/packages/app/src/__tests__/NexusPack.test.tsx b/packages/app/src/__tests__/NexusPack.test.tsx index b5177fb74..f9ad1a20f 100644 --- a/packages/app/src/__tests__/NexusPack.test.tsx +++ b/packages/app/src/__tests__/NexusPack.test.tsx @@ -1,8 +1,9 @@ import { screen } from '@testing-library/react'; +import { SLOW_TIMEOUT } from '../providers/mock/mock-api'; import { - findSelectedVisTab, - findVisTabs, + getSelectedVisTab, + getVisTabs, mockConsoleMethod, renderApp, } from '../test-utils'; @@ -11,31 +12,31 @@ import { NexusVis } from '../vis-packs/nexus/visualizations'; test('visualize NXdata group with explicit signal interpretation', async () => { // Signal with "spectrum" interpretation const { selectExplorerNode } = await renderApp('/nexus_entry/spectrum'); - await expect(findVisTabs()).resolves.toEqual([NexusVis.NxSpectrum]); - await expect( - screen.findByRole('figure', { name: 'twoD_spectrum (arb. units)' }), // signal name + `units` attribute - ).resolves.toBeVisible(); + expect(getVisTabs()).toEqual([NexusVis.NxSpectrum]); + expect( + screen.getByRole('figure', { name: 'twoD_spectrum (arb. units)' }), // signal name + `units` attribute + ).toBeVisible(); // Signal with "image" interpretation await selectExplorerNode('image'); - await expect(findVisTabs()).resolves.toEqual([NexusVis.NxImage]); - await expect( - screen.findByRole('figure', { name: 'Interference fringes' }), // `long_name` attribute - ).resolves.toBeVisible(); + expect(getVisTabs()).toEqual([NexusVis.NxImage]); + expect( + screen.getByRole('figure', { name: 'Interference fringes' }), // `long_name` attribute + ).toBeVisible(); // 2D complex signal with "spectrum" interpretation await selectExplorerNode('complex_spectrum'); - await expect(findVisTabs()).resolves.toEqual([NexusVis.NxSpectrum]); - await expect( - screen.findByRole('figure', { name: 'twoD_complex' }), // signal name (complex vis type is displayed as ordinate label) - ).resolves.toBeVisible(); + expect(getVisTabs()).toEqual([NexusVis.NxSpectrum]); + expect( + screen.getByRole('figure', { name: 'twoD_complex' }), // signal name (complex vis type is displayed as ordinate label) + ).toBeVisible(); // Signal with "rgb-image" interpretation await selectExplorerNode('rgb-image'); - await expect(findVisTabs()).resolves.toEqual([NexusVis.NxRGB]); - await expect( - screen.findByRole('figure', { name: 'RGB CMY DGW' }), // `long_name` attribute - ).resolves.toBeVisible(); + expect(getVisTabs()).toEqual([NexusVis.NxRGB]); + expect( + screen.getByRole('figure', { name: 'RGB CMY DGW' }), // `long_name` attribute + ).toBeVisible(); }); test('visualize NXdata group without explicit signal interpretation', async () => { @@ -43,107 +44,74 @@ test('visualize NXdata group without explicit signal interpretation', async () = const { selectExplorerNode } = await renderApp( '/nexus_entry/nx_process/nx_data', ); - await expect(findVisTabs()).resolves.toEqual([ - NexusVis.NxSpectrum, - NexusVis.NxImage, - ]); - await expect(findSelectedVisTab()).resolves.toBe(NexusVis.NxImage); - await expect( - screen.findByRole('figure', { name: 'NeXus 2D' }), // `title` dataset - ).resolves.toBeVisible(); + expect(getVisTabs()).toEqual([NexusVis.NxSpectrum, NexusVis.NxImage]); + expect(getSelectedVisTab()).toBe(NexusVis.NxImage); + expect(screen.getByRole('figure', { name: 'NeXus 2D' })).toBeVisible(); // `title` dataset // 1D signal (no interpretation) await selectExplorerNode('log_spectrum'); - await expect(findSelectedVisTab()).resolves.toBe(NexusVis.NxSpectrum); - await expect( - screen.findByRole('figure', { name: 'oneD' }), - ).resolves.toBeVisible(); // signal name + expect(getVisTabs()).toEqual([NexusVis.NxSpectrum]); + expect(screen.getByRole('figure', { name: 'oneD' })).toBeVisible(); // signal name // 2D complex signal (no interpretation) await selectExplorerNode('complex'); - await expect(findVisTabs()).resolves.toEqual([ - NexusVis.NxSpectrum, - NexusVis.NxImage, - ]); - await expect(findSelectedVisTab()).resolves.toBe(NexusVis.NxImage); - await expect( - screen.findByRole('figure', { name: 'twoD_complex (amplitude)' }), // signal name + complex visualization type - ).resolves.toBeVisible(); + expect(getVisTabs()).toEqual([NexusVis.NxSpectrum, NexusVis.NxImage]); + expect(getSelectedVisTab()).toBe(NexusVis.NxImage); + expect( + screen.getByRole('figure', { name: 'twoD_complex (amplitude)' }), // signal name + complex visualization type + ).toBeVisible(); // 2D signal and two 1D axes of same length (implicit scatter interpretation) await selectExplorerNode('scatter'); - await expect(findVisTabs()).resolves.toEqual([NexusVis.NxScatter]); - await expect( - screen.findByRole('figure', { name: 'scatter_data' }), // signal name - ).resolves.toBeVisible(); + expect(getVisTabs()).toEqual([NexusVis.NxScatter]); + expect(screen.getByRole('figure', { name: 'scatter_data' })).toBeVisible(); // signal name }); test('visualize NXdata group with old-style signal', async () => { await renderApp('/nexus_entry/old-style'); - - await expect(findVisTabs()).resolves.toEqual([ - NexusVis.NxSpectrum, - NexusVis.NxImage, - ]); - await expect(findSelectedVisTab()).resolves.toBe(NexusVis.NxImage); - - await expect( - screen.findByRole('figure', { name: 'twoD' }), // name of dataset with `signal` attribute - ).resolves.toBeVisible(); + expect(getVisTabs()).toEqual([NexusVis.NxSpectrum, NexusVis.NxImage]); + expect( + screen.getByRole('figure', { name: 'twoD' }), // name of dataset with `signal` attribute + ).toBeVisible(); }); test('visualize group with `default` attribute', async () => { // NXroot with relative path to NXentry group with relative path to NXdata group with 2D signal const { selectExplorerNode } = await renderApp(); - - await expect(findVisTabs()).resolves.toEqual([ - NexusVis.NxSpectrum, - NexusVis.NxImage, - ]); - await expect(findSelectedVisTab()).resolves.toBe(NexusVis.NxImage); - await expect( - screen.findByRole('figure', { name: 'NeXus 2D' }), // `title` dataset - ).resolves.toBeVisible(); + expect(getVisTabs()).toEqual([NexusVis.NxSpectrum, NexusVis.NxImage]); + expect( + screen.getByRole('figure', { name: 'NeXus 2D' }), // `title` dataset + ).toBeVisible(); // NXentry with relative path to NXdata group with 2D signal await selectExplorerNode('nexus_entry'); - await expect(findVisTabs()).resolves.toEqual([ - NexusVis.NxSpectrum, - NexusVis.NxImage, - ]); - await expect(findSelectedVisTab()).resolves.toBe(NexusVis.NxImage); - await expect( - screen.findByRole('figure', { name: 'NeXus 2D' }), // `title` dataset - ).resolves.toBeVisible(); + expect(getVisTabs()).toEqual([NexusVis.NxSpectrum, NexusVis.NxImage]); + expect( + screen.getByRole('figure', { name: 'NeXus 2D' }), // `title` dataset + ).toBeVisible(); // NXentry with absolute path to NXdata group with 2D signal await selectExplorerNode('nx_process'); await selectExplorerNode('absolute_default_path'); - await expect(findVisTabs()).resolves.toEqual([ - NexusVis.NxSpectrum, - NexusVis.NxImage, - ]); - await expect(findSelectedVisTab()).resolves.toBe(NexusVis.NxImage); - await expect( - screen.findByRole('figure', { name: 'NeXus 2D' }), // `title` dataset - ).resolves.toBeVisible(); + expect(getVisTabs()).toEqual([NexusVis.NxSpectrum, NexusVis.NxImage]); + expect( + screen.getByRole('figure', { name: 'NeXus 2D' }), // `title` dataset + ).toBeVisible(); }); test('visualize NXentry group with implicit default child NXdata group', async () => { await renderApp('/nexus_no_default'); - - await expect(findVisTabs()).resolves.toEqual([NexusVis.NxSpectrum]); - - await expect( - screen.findByRole('figure', { name: 'oneD' }), // signal name of NXdata group "spectrum" - ).resolves.toBeVisible(); + expect(getVisTabs()).toEqual([NexusVis.NxSpectrum]); + expect( + screen.getByRole('figure', { name: 'oneD' }), // signal name of NXdata group "spectrum" + ).toBeVisible(); }); test('follow SILX styles on NXdata group', async () => { await renderApp('/nexus_entry/log_spectrum'); - await expect( - screen.findAllByRole('button', { name: 'Log' }), - ).resolves.toHaveLength(2); // log for both axes + + const logSelectors = screen.getAllByRole('button', { name: 'Log' }); + expect(logSelectors).toHaveLength(2); // log for both axes }); test('handle unknown/incompatible interpretation gracefully', async () => { @@ -151,76 +119,65 @@ test('handle unknown/incompatible interpretation gracefully', async () => { // Signal with unknown interpretation await selectExplorerNode('interpretation_unknown'); - await expect(findVisTabs()).resolves.toEqual([ - NexusVis.NxSpectrum, - NexusVis.NxImage, - ]); // fallback based on number of dimensions - await expect(findSelectedVisTab()).resolves.toBe(NexusVis.NxImage); - await expect( - screen.findByRole('figure', { name: 'fourD' }), // signal name - ).resolves.toBeVisible(); + expect(getVisTabs()).toEqual([NexusVis.NxSpectrum, NexusVis.NxImage]); // fallback based on number of dimensions + expect(screen.getByRole('figure', { name: 'fourD' })).toBeVisible(); // signal name // Signal with too few dimensions for "rgb-image" interpretation await selectExplorerNode('rgb-image_incompatible'); - await expect(findVisTabs()).resolves.toEqual([NexusVis.NxSpectrum]); // fallback based on number of dimensions - await expect( - screen.findByRole('figure', { name: 'oneD' }), // signal name - ).resolves.toBeVisible(); + expect(getVisTabs()).toEqual([NexusVis.NxSpectrum]); // fallback based on number of dimensions + expect(screen.getByRole('figure', { name: 'oneD' })).toBeVisible(); // signal name }); test('show error/fallback for malformed NeXus entity', async () => { const errorSpy = mockConsoleMethod('error'); - const { selectExplorerNode } = await renderApp('/nexus_malformed'); // `default` attribute points to non-existant entity await selectExplorerNode('default_not_found'); - await expect( - screen.findByText('No entity found at /test'), - ).resolves.toBeVisible(); + expect(screen.getByText('No entity found at /test')).toBeVisible(); errorSpy.mockClear(); // No `signal` attribute await selectExplorerNode('no_signal'); - await expect( - screen.findByText('No visualization available for this entity.'), - ).resolves.toBeInTheDocument(); + expect( + screen.getByText('No visualization available for this entity.'), + ).toBeInTheDocument(); expect(errorSpy).not.toHaveBeenCalled(); errorSpy.mockClear(); // `signal` attribute points to non-existant dataset await selectExplorerNode('signal_not_found'); - await expect( - screen.findByText('Expected "unknown" signal entity to exist'), - ).resolves.toBeVisible(); + expect( + screen.getByText('Expected "unknown" signal entity to exist'), + ).toBeVisible(); errorSpy.mockClear(); // Signal entity is not a dataset await selectExplorerNode('signal_not_dataset'); - await expect( - screen.findByText('Expected "some_group" signal to be a dataset'), - ).resolves.toBeVisible(); + expect( + screen.getByText('Expected "some_group" signal to be a dataset'), + ).toBeVisible(); errorSpy.mockClear(); // Old-style signal entity is not a dataset await selectExplorerNode('signal_old-style_not_dataset'); - await expect( - screen.findByText('Expected old-style "some_group" signal to be a dataset'), - ).resolves.toBeVisible(); + expect( + screen.getByText('Expected old-style "some_group" signal to be a dataset'), + ).toBeVisible(); errorSpy.mockClear(); // Shape of signal dataset is not array await selectExplorerNode('signal_not_array'); - await expect( - screen.findByText('Expected dataset to have array shape'), - ).resolves.toBeVisible(); + expect( + screen.getByText('Expected dataset to have array shape'), + ).toBeVisible(); errorSpy.mockClear(); // Type of signal dataset is not numeric await selectExplorerNode('signal_not_numeric'); - await expect( - screen.findByText('Expected dataset to have numeric or complex type'), - ).resolves.toBeVisible(); + expect( + screen.getByText('Expected dataset to have numeric or complex type'), + ).toBeVisible(); errorSpy.mockClear(); errorSpy.mockRestore(); @@ -234,16 +191,15 @@ test('ignore malformed `SILX_style` attribute', async () => { const { selectExplorerNode } = await renderApp( '/nexus_malformed/silx_style_unknown', ); - await expect(findVisTabs()).resolves.toEqual([NexusVis.NxSpectrum]); - await expect( - screen.findAllByRole('button', { - name: 'Linear', // scales remain unchanged - }), - ).resolves.toHaveLength(2); + + expect(getVisTabs()).toEqual([NexusVis.NxSpectrum]); + const scaleSelectors = screen.getAllByRole('button', { name: 'Linear' }); + expect(scaleSelectors).toHaveLength(2); // scales remain unchanged // Invalid JSON await selectExplorerNode('silx_style_malformed'); - await expect(findVisTabs()).resolves.toEqual([NexusVis.NxSpectrum]); + + expect(getVisTabs()).toEqual([NexusVis.NxSpectrum]); expect(warningSpy).toHaveBeenCalledWith( "Malformed 'SILX_style' attribute: {", // warn in console ); @@ -263,20 +219,18 @@ test('cancel and retry slow fetch of NxSpectrum', async () => { // Cancel all fetches at once const errorSpy = mockConsoleMethod('error'); - await user.click(await screen.findByRole('button', { name: /Cancel/ })); - + await user.click(screen.getByRole('button', { name: /Cancel/ })); await expect(screen.findByText('Request cancelled')).resolves.toBeVisible(); - expect(errorSpy).toHaveBeenCalledTimes(2); // React logs two stack traces errorSpy.mockRestore(); // Retry all fetches at once - await user.click(await screen.findByRole('button', { name: /Retry/ })); + await user.click(screen.getByRole('button', { name: /Retry/ })); await expect(screen.findByText(/Loading data/)).resolves.toBeVisible(); // Let fetches succeed - jest.runAllTimers(); - - await expect(screen.findByRole('figure')).resolves.toBeVisible(); + await expect( + screen.findByRole('figure', undefined, { timeout: SLOW_TIMEOUT }), + ).resolves.toBeVisible(); }); test('cancel and retry slow fetch of NxImage', async () => { @@ -289,18 +243,18 @@ test('cancel and retry slow fetch of NxImage', async () => { // Cancel all fetches at once const errorSpy = mockConsoleMethod('error'); - await user.click(await screen.findByRole('button', { name: /Cancel/ })); + await user.click(screen.getByRole('button', { name: /Cancel/ })); await expect(screen.findByText('Request cancelled')).resolves.toBeVisible(); - expect(errorSpy).toHaveBeenCalledTimes(2); // React logs two stack traces errorSpy.mockRestore(); // Retry all fetches at once - await user.click(await screen.findByRole('button', { name: /Retry/ })); + await user.click(screen.getByRole('button', { name: /Retry/ })); await expect(screen.findByText(/Loading data/)).resolves.toBeVisible(); // Let fetches succeed - jest.runAllTimers(); - await expect(screen.findByRole('figure')).resolves.toBeVisible(); + await expect( + screen.findByRole('figure', undefined, { timeout: SLOW_TIMEOUT }), + ).resolves.toBeVisible(); }); test('retry fetching automatically when re-selecting NxSpectrum', async () => { @@ -313,9 +267,8 @@ test('retry fetching automatically when re-selecting NxSpectrum', async () => { // Cancel all fetches at once const errorSpy = mockConsoleMethod('error'); - await user.click(await screen.findByRole('button', { name: /Cancel/ })); + await user.click(screen.getByRole('button', { name: /Cancel/ })); await expect(screen.findByText('Request cancelled')).resolves.toBeVisible(); - expect(errorSpy).toHaveBeenCalledTimes(2); // React logs two stack traces errorSpy.mockRestore(); // Switch to other entity with no visualization @@ -327,8 +280,9 @@ test('retry fetching automatically when re-selecting NxSpectrum', async () => { await expect(screen.findByText(/Loading data/)).resolves.toBeVisible(); // Let fetches succeed - jest.runAllTimers(); - await expect(screen.findByRole('figure')).resolves.toBeVisible(); + await expect( + screen.findByRole('figure', undefined, { timeout: SLOW_TIMEOUT }), + ).resolves.toBeVisible(); }); test('retry fetching automatically when selecting other NxImage slice', async () => { @@ -341,9 +295,8 @@ test('retry fetching automatically when selecting other NxImage slice', async () // Cancel all fetches at once const errorSpy = mockConsoleMethod('error'); - await user.click(await screen.findByRole('button', { name: /Cancel/ })); + await user.click(screen.getByRole('button', { name: /Cancel/ })); await expect(screen.findByText('Request cancelled')).resolves.toBeVisible(); - expect(errorSpy).toHaveBeenCalledTimes(2); // React logs two stack traces errorSpy.mockRestore(); // Move to other slice to retry fetching automatically @@ -352,15 +305,16 @@ test('retry fetching automatically when selecting other NxImage slice', async () await expect(screen.findByText(/Loading data/)).resolves.toBeVisible(); // Let fetches succeed - jest.runAllTimers(); - await expect(screen.findByRole('figure')).resolves.toBeVisible(); + await expect( + screen.findByRole('figure', undefined, { timeout: SLOW_TIMEOUT }), + ).resolves.toBeVisible(); // Move back to first slice to retry fetching it automatically await user.type(d0Slider, '{PageDown}'); await expect(screen.findByText(/Loading data/)).resolves.toBeVisible(); // Let fetch of first slice succeed - jest.runAllTimers(); - await expect(screen.findByRole('figure')).resolves.toBeVisible(); - d0Slider.blur(); // remove focus to avoid state update after unmount + await expect( + screen.findByRole('figure', undefined, { timeout: SLOW_TIMEOUT }), + ).resolves.toBeVisible(); }); diff --git a/packages/app/src/__tests__/VisSelector.test.tsx b/packages/app/src/__tests__/VisSelector.test.tsx index 70563289d..1cfd69c88 100644 --- a/packages/app/src/__tests__/VisSelector.test.tsx +++ b/packages/app/src/__tests__/VisSelector.test.tsx @@ -1,12 +1,12 @@ import { screen } from '@testing-library/react'; -import { findSelectedVisTab, renderApp } from '../test-utils'; +import { getSelectedVisTab, renderApp } from '../test-utils'; import { Vis } from '../vis-packs/core/visualizations'; test('switch between visualizations', async () => { const { user } = await renderApp('/nD_datasets/oneD'); - const lineTab = await screen.findByRole('tab', { name: 'Line' }); + const lineTab = screen.getByRole('tab', { name: 'Line' }); expect(lineTab).toBeVisible(); expect(lineTab).toHaveAttribute('aria-selected', 'true'); @@ -16,15 +16,8 @@ test('switch between visualizations', async () => { // Switch to Matrix visualization await user.click(matrixTab); - - expect(screen.getByRole('tab', { name: 'Matrix' })).toHaveAttribute( - 'aria-selected', - 'true', - ); - expect(screen.getByRole('tab', { name: 'Line' })).toHaveAttribute( - 'aria-selected', - 'false', - ); + expect(matrixTab).toHaveAttribute('aria-selected', 'true'); + expect(lineTab).toHaveAttribute('aria-selected', 'false'); }); test('restore active visualization when switching to inspect mode and back', async () => { @@ -34,35 +27,26 @@ test('restore active visualization when switching to inspect mode and back', asy await selectVisTab(Vis.Line); // Switch to inspect mode and back - await user.click(await screen.findByRole('tab', { name: 'Inspect' })); - await user.click(await screen.findByRole('tab', { name: 'Display' })); + await user.click(screen.getByRole('tab', { name: 'Inspect' })); + await user.click(screen.getByRole('tab', { name: 'Display' })); - await expect( - screen.findByRole('tab', { name: 'Line' }), - ).resolves.toHaveAttribute('aria-selected', 'true'); + // Ensure Line visualization is active + expect(getSelectedVisTab()).toBe('Line'); }); test('choose most advanced visualization when switching between datasets', async () => { const { selectExplorerNode } = await renderApp('/nD_datasets/oneD'); - await expect( - screen.findByRole('tab', { name: 'Line' }), - ).resolves.toHaveAttribute('aria-selected', 'true'); + expect(getSelectedVisTab()).toBe('Line'); await selectExplorerNode('twoD'); - await expect( - screen.findByRole('tab', { name: 'Heatmap' }), - ).resolves.toHaveAttribute('aria-selected', 'true'); + expect(getSelectedVisTab()).toBe(Vis.Heatmap); await selectExplorerNode('threeD_rgb'); - await expect( - screen.findByRole('tab', { name: 'RGB' }), - ).resolves.toHaveAttribute('aria-selected', 'true'); + expect(getSelectedVisTab()).toBe(Vis.RGB); await selectExplorerNode('threeD_bool'); - await expect( - screen.findByRole('tab', { name: 'Matrix' }), - ).resolves.toHaveAttribute('aria-selected', 'true'); + expect(getSelectedVisTab()).toBe(Vis.Matrix); }); test('remember preferred visualization when switching between datasets', async () => { @@ -73,27 +57,21 @@ test('remember preferred visualization when switching between datasets', async ( /* Switch to Matrix vis. Since this is _not_ the most advanced visualization * for `twoD`, it becomes the preferred visualization. */ await selectVisTab(Vis.Matrix); - await expect(findSelectedVisTab()).resolves.toBe('Matrix'); + expect(getSelectedVisTab()).toBe('Matrix'); // Select another dataset for which the Matrix vis is not the most advanced visualization await selectExplorerNode('oneD'); // Check that the preferred visualization is restored - await expect( - screen.findByRole('tab', { name: 'Matrix' }), - ).resolves.toHaveAttribute('aria-selected', 'true'); - await expect(findSelectedVisTab()).resolves.toBe('Matrix'); + expect(getSelectedVisTab()).toBe(Vis.Matrix); /* Switch to Line vis. Since this _is_ the most advanced visualization for * `oneD`, the preferred visualization is cleared. */ - await user.click(await screen.findByRole('tab', { name: 'Line' })); // becomes preferred vis + await user.click(screen.getByRole('tab', { name: 'Line' })); // becomes preferred vis // Select another dataset with a more advanced visualization than Line await selectExplorerNode('threeD_rgb'); // Check that the most advanced visualization is selected - await expect( - screen.findByRole('tab', { name: 'RGB' }), - ).resolves.toHaveAttribute('aria-selected', 'true'); - await expect(findSelectedVisTab()).resolves.toBe('RGB'); + expect(getSelectedVisTab()).toBe(Vis.RGB); }); diff --git a/packages/app/src/__tests__/Visualizer.test.tsx b/packages/app/src/__tests__/Visualizer.test.tsx index 324917232..73b491db0 100644 --- a/packages/app/src/__tests__/Visualizer.test.tsx +++ b/packages/app/src/__tests__/Visualizer.test.tsx @@ -1,5 +1,6 @@ import { screen } from '@testing-library/react'; +import { SLOW_TIMEOUT } from '../providers/mock/mock-api'; import { mockConsoleMethod, renderApp } from '../test-utils'; import { Vis } from '../vis-packs/core/visualizations'; @@ -15,22 +16,21 @@ test('show loader while fetching dataset value', async () => { }); await expect(screen.findByText(/Loading data/)).resolves.toBeVisible(); - - jest.runAllTimers(); // resolve slow fetch right away - await expect(screen.findByText(/42/)).resolves.toBeVisible(); + await expect( + screen.findByText(/42/, undefined, { timeout: SLOW_TIMEOUT }), + ).resolves.toBeVisible(); }); test("show error when dataset value can't be fetched", async () => { const errorSpy = mockConsoleMethod('error'); const { selectExplorerNode } = await renderApp('/resilience/error_value'); - await expect(screen.findByText('error')).resolves.toBeVisible(); - expect(errorSpy).toHaveBeenCalledTimes(2); // React logs two stack traces + expect(screen.getByText('error')).toBeVisible(); errorSpy.mockRestore(); // Make sure error boundary resets when selecting another entity await selectExplorerNode('entities'); - await expect(screen.findByText(/No visualization/)).resolves.toBeVisible(); + expect(screen.getByText(/No visualization/)).toBeVisible(); }); test('cancel and retry slow fetch of dataset value', async () => { @@ -43,19 +43,19 @@ test('cancel and retry slow fetch of dataset value', async () => { // Cancel fetch const errorSpy = mockConsoleMethod('error'); - await user.click(await screen.findByRole('button', { name: /Cancel/ })); - + await user.click(screen.getByRole('button', { name: /Cancel/ })); await expect(screen.findByText('Request cancelled')).resolves.toBeVisible(); - expect(errorSpy).toHaveBeenCalledTimes(2); // React logs two stack traces + // expect(errorSpy).toHaveBeenCalledTimes(3); // React logs two stack traces + one extra for some reason errorSpy.mockRestore(); // Retry fetch - await user.click(await screen.findByRole('button', { name: /Retry/ })); + await user.click(screen.getByRole('button', { name: /Retry/ })); await expect(screen.findByText(/Loading data/)).resolves.toBeVisible(); // Let fetch succeed - jest.runAllTimers(); - await expect(screen.findByText(/42/)).resolves.toBeVisible(); + await expect( + screen.findByText(/42/, undefined, { timeout: SLOW_TIMEOUT }), + ).resolves.toBeVisible(); }); test('cancel and retry slow fetch of dataset slice', async () => { @@ -70,21 +70,20 @@ test('cancel and retry slow fetch of dataset slice', async () => { // Cancel fetch of first slice const errorSpy = mockConsoleMethod('error'); - await user.click(await screen.findByRole('button', { name: /Cancel/ })); - + await user.click(screen.getByRole('button', { name: /Cancel/ })); await expect(screen.findByText('Request cancelled')).resolves.toBeVisible(); - expect(errorSpy).toHaveBeenCalledTimes(2); // React logs two stack traces errorSpy.mockRestore(); // Retry fetch of first slice - await user.click(await screen.findByRole('button', { name: /Retry/ })); + await user.click(screen.getByRole('button', { name: /Retry/ })); await expect( screen.findByText(/Loading current slice/), ).resolves.toBeVisible(); // Let fetch of first slice succeed - jest.runAllTimers(); - await expect(screen.findByRole('figure')).resolves.toBeVisible(); + await expect( + screen.findByRole('figure', undefined, { timeout: SLOW_TIMEOUT }), + ).resolves.toBeVisible(); }); test('retry fetching automatically when re-selecting dataset', async () => { @@ -97,7 +96,7 @@ test('retry fetching automatically when re-selecting dataset', async () => { // Cancel fetch const errorSpy = mockConsoleMethod('error'); - await user.click(await screen.findByRole('button', { name: /Cancel/ })); + await user.click(screen.getByRole('button', { name: /Cancel/ })); await expect(screen.findByText('Request cancelled')).resolves.toBeVisible(); errorSpy.mockRestore(); @@ -110,8 +109,9 @@ test('retry fetching automatically when re-selecting dataset', async () => { await expect(screen.findByText(/Loading data/)).resolves.toBeVisible(); // Let fetch succeed - jest.runAllTimers(); - await expect(screen.findByText(/42/)).resolves.toBeVisible(); + await expect( + screen.findByText(/42/, undefined, { timeout: SLOW_TIMEOUT }), + ).resolves.toBeVisible(); }); test('retry fetching dataset slice automatically when re-selecting slice', async () => { @@ -126,32 +126,34 @@ test('retry fetching dataset slice automatically when re-selecting slice', async // Cancel fetch of first slice const errorSpy = mockConsoleMethod('error'); - await user.click(await screen.findByRole('button', { name: /Cancel/ })); + await user.click(screen.getByRole('button', { name: /Cancel/ })); await expect(screen.findByText('Request cancelled')).resolves.toBeVisible(); - expect(errorSpy).toHaveBeenCalledTimes(2); // React logs two stack traces errorSpy.mockRestore(); // Move to other slice and start fetching const d0Slider = screen.getByRole('slider', { name: 'D0' }); await user.type(d0Slider, '{ArrowUp}'); + await expect( screen.findByText(/Loading current slice/), ).resolves.toBeVisible(); // Let fetch of other slice succeed - jest.runAllTimers(); - await expect(screen.findByRole('figure')).resolves.toBeVisible(); + await expect( + screen.findByRole('figure', undefined, { timeout: SLOW_TIMEOUT }), + ).resolves.toBeVisible(); // Move back to first slice to retry fetching it automatically await user.type(d0Slider, '{ArrowDown}'); + await expect( screen.findByText(/Loading current slice/), ).resolves.toBeVisible(); - d0Slider.blur(); // remove focus to avoid state update after unmount // Let fetch of first slice succeed - jest.runAllTimers(); - await expect(screen.findByRole('figure')).resolves.toBeVisible(); + await expect( + screen.findByRole('figure', undefined, { timeout: SLOW_TIMEOUT }), + ).resolves.toBeVisible(); }); test('cancel fetching dataset slice when changing entity', async () => { @@ -165,11 +167,15 @@ test('cancel fetching dataset slice when changing entity', async () => { ).resolves.toBeVisible(); // Switch to another entity to cancel the fetch + const errorSpy = mockConsoleMethod('error'); // `act` warning due to previous slice getting cancelled await selectExplorerNode('slow_value'); await expect(screen.findByText(/Loading data/)).resolves.toBeVisible(); + errorSpy.mockRestore(); - // Let pending requests succeed - jest.runAllTimers(); + // Let fetch succeed + await expect( + screen.findByText(/42/, undefined, { timeout: SLOW_TIMEOUT }), + ).resolves.toBeVisible(); // Reselect initial dataset await selectExplorerNode('slow_slicing'); @@ -180,8 +186,9 @@ test('cancel fetching dataset slice when changing entity', async () => { ).resolves.toBeVisible(); // Let fetch of first slice succeed - jest.runAllTimers(); - await expect(screen.findByRole('figure')).resolves.toBeVisible(); + await expect( + screen.findByRole('figure', undefined, { timeout: SLOW_TIMEOUT }), + ).resolves.toBeVisible(); }); test('cancel fetching dataset slice when changing vis', async () => { @@ -190,20 +197,22 @@ test('cancel fetching dataset slice when changing vis', async () => { withFakeTimers: true, }); - // Select dataset and start fetching the slice await expect( screen.findByText(/Loading current slice/), ).resolves.toBeVisible(); // Switch to Line visualization to cancel fetch + const errorSpy = mockConsoleMethod('error'); // `act` warning due to previous slice getting cancelled await selectVisTab(Vis.Line); await expect( screen.findByText(/Loading current slice/), ).resolves.toBeVisible(); + errorSpy.mockRestore(); // Let pending requests succeed - jest.runAllTimers(); - await expect(screen.findByRole('figure')).resolves.toBeVisible(); + await expect( + screen.findByRole('figure', undefined, { timeout: SLOW_TIMEOUT }), + ).resolves.toBeVisible(); // Switch back to Heatmap visualization await selectVisTab(Vis.Heatmap); @@ -214,6 +223,7 @@ test('cancel fetching dataset slice when changing vis', async () => { ).resolves.toBeVisible(); // Let fetch of slice succeed - jest.runAllTimers(); - await expect(screen.findByRole('figure')).resolves.toBeVisible(); + await expect( + screen.findByRole('figure', undefined, { timeout: SLOW_TIMEOUT }), + ).resolves.toBeVisible(); }); diff --git a/packages/app/src/dimension-mapper/SlicingSlider.tsx b/packages/app/src/dimension-mapper/SlicingSlider.tsx index a2baafcce..6048c7726 100644 --- a/packages/app/src/dimension-mapper/SlicingSlider.tsx +++ b/packages/app/src/dimension-mapper/SlicingSlider.tsx @@ -53,6 +53,15 @@ function SlicingSlider(props: Props) { setValue(newValue); onDebouncedChange(newValue); }} + /* When slicing in E2E tests, `onChange` is called with the old value. + A `setState` callback in `ReactSlider` that is supposed to be called + after the state is updated, is in fact called before. + https://github.com/zillow/react-slider/blob/master/src/components/ReactSlider/ReactSlider.jsx#L890 + Adding `onAfterChange` fixes the issue for now. */ + onAfterChange={(newValue) => { + setValue(newValue); + onDebouncedChange(newValue); + }} renderThumb={(thumbProps, state) => ({message}...
++ +
+ > )} -{message}...
-- -