This is an automated email from the ASF dual-hosted git repository. rusackas pushed a commit to branch replace-jest-enzyme in repository https://gitbox.apache.org/repos/asf/superset.git
commit 9bea4c10d36d7b5e93658f944cb9eb924faf84fc Author: Evan Rusackas <[email protected]> AuthorDate: Fri Feb 7 13:49:41 2025 -0700 more touchups --- .../test/chart/components/SuperChartCore.test.tsx | 2 +- .../QueryAutoRefresh/QueryAutoRefresh.test.tsx | 31 ++-- .../SqlLab/components/SqlEditor/SqlEditor.test.tsx | 41 ++++- .../DrillDetail/DrillDetailMenuItems.test.tsx | 155 +++++++++++++----- .../src/components/Collapse/Collapse.test.tsx | 154 +++++++++--------- .../DatabaseSelector/DatabaseSelector.test.tsx | 173 +++++++++++---------- .../ErrorMessage/InvalidSQLErrorMessage.test.tsx | 36 +++-- .../FilterScope/FilterScope.test.tsx | 43 ++++- .../DatasourcePanel/DatasourcePanel.test.tsx | 26 +--- .../DndColumnSelectControl/Option.test.tsx | 109 +++++++------ .../VizTypeControl/VizTypeControl.test.tsx | 39 ++--- superset-frontend/src/features/home/Menu.test.tsx | 4 +- 12 files changed, 483 insertions(+), 330 deletions(-) diff --git a/superset-frontend/packages/superset-ui-core/test/chart/components/SuperChartCore.test.tsx b/superset-frontend/packages/superset-ui-core/test/chart/components/SuperChartCore.test.tsx index 4756d0f782..00e58f5cc2 100644 --- a/superset-frontend/packages/superset-ui-core/test/chart/components/SuperChartCore.test.tsx +++ b/superset-frontend/packages/superset-ui-core/test/chart/components/SuperChartCore.test.tsx @@ -26,8 +26,8 @@ import { supersetTheme, SupersetTheme, ThemeProvider, - cleanup, } from '@superset-ui/core'; +import { cleanup } from '@testing-library/react'; import SuperChartCore from '../../../src/chart/components/SuperChartCore'; import { ChartKeys, diff --git a/superset-frontend/src/SqlLab/components/QueryAutoRefresh/QueryAutoRefresh.test.tsx b/superset-frontend/src/SqlLab/components/QueryAutoRefresh/QueryAutoRefresh.test.tsx index d6cf4dafd7..68353ad1f5 100644 --- a/superset-frontend/src/SqlLab/components/QueryAutoRefresh/QueryAutoRefresh.test.tsx +++ b/superset-frontend/src/SqlLab/components/QueryAutoRefresh/QueryAutoRefresh.test.tsx @@ -194,35 +194,30 @@ describe('QueryAutoRefresh', () => { it('Does NOT Attempt to refresh when given only completed queries', async () => { const store = mockStore(); - - // Mock API response (no queries needing refresh) fetchMock.get(refreshApi, { - result: [], + result: [ + { + id: runningQuery.id, + status: 'success', + }, + ], }); - - console.log('Fetching:', refreshApi); - - // Render the component render( <QueryAutoRefresh - queries={successfulQueries} // Only completed queries + queries={successfulQueries} queriesLastUpdate={queriesLastUpdate} />, { useRedux: true, store }, ); - - // Wait and check if the expected action is dispatched await waitFor( - () => { - console.log('Actions in store:', store.getActions()); + () => expect(store.getActions()).toContainEqual( - expect.objectContaining({ type: CLEAR_INACTIVE_QUERIES }), - ); - }, - { timeout: QUERY_UPDATE_FREQ + 500 }, // Extend timeout slightly + expect.objectContaining({ + type: CLEAR_INACTIVE_QUERIES, + }), + ), + { timeout: QUERY_UPDATE_FREQ + 100 }, ); - - // Ensure API was NOT called expect(fetchMock.calls(refreshApi)).toHaveLength(0); }); }); diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx b/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx index a266cbbaca..d39424d450 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx +++ b/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx @@ -23,7 +23,12 @@ import { getExtensionsRegistry, FeatureFlag, } from '@superset-ui/core'; -import { fireEvent, render, waitFor } from 'spec/helpers/testing-library'; +import { + cleanup, + fireEvent, + render, + waitFor, +} from 'spec/helpers/testing-library'; import fetchMock from 'fetch-mock'; import reducers from 'spec/helpers/reducerIndex'; import { setupStore } from 'src/views/store'; @@ -135,6 +140,15 @@ const createStore = (initState: object) => }); describe('SqlEditor', () => { + beforeAll(() => { + jest.setTimeout(30000); + }); + + afterEach(async () => { + cleanup(); + await new Promise(resolve => setTimeout(resolve, 0)); + }); + const mockedProps = { queryEditor: initialState.sqlLab.queryEditors[0], tables: [table], @@ -187,16 +201,27 @@ describe('SqlEditor', () => { }); it('render a SqlEditorLeftBar', async () => { - const { getByTestId } = setup(mockedProps, store); - await waitFor(() => - expect(getByTestId('mock-sql-editor-left-bar')).toBeInTheDocument(), + const { getByTestId, unmount } = setup(mockedProps, store); + + await waitFor( + () => expect(getByTestId('mock-sql-editor-left-bar')).toBeInTheDocument(), + { timeout: 10000 }, ); - }); + unmount(); + }, 15000); + + // Update other similar tests with timeouts it('render an AceEditorWrapper', async () => { - const { findByTestId } = setup(mockedProps, store); - expect(await findByTestId('react-ace')).toBeInTheDocument(); - }); + const { findByTestId, unmount } = setup(mockedProps, store); + + await waitFor( + () => expect(findByTestId('react-ace')).resolves.toBeInTheDocument(), + { timeout: 10000 }, + ); + + unmount(); + }, 15000); it('skip rendering an AceEditorWrapper when the current tab is inactive', async () => { const { findByTestId, queryByTestId } = setup( diff --git a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.test.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.test.tsx index 57c5904684..002ceb67ad 100644 --- a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.test.tsx +++ b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.test.tsx @@ -144,7 +144,7 @@ const expectDrillToDetailModal = async ( ) => { const button = screen.getByRole('menuitem', { name: buttonName }); - await userEvent.click(button); + userEvent.click(button); const modal = await screen.findByRole('dialog', { name: `Drill to detail: ${chartName}`, }); @@ -157,10 +157,7 @@ const expectDrillToDetailModal = async ( /** * Menu item should be enabled without explanatory tooltip */ -const expectMenuItemEnabled = async (menuItem: HTMLElement | null) => { - expect(menuItem).not.toBeNull(); // Ensure element exists - if (!menuItem) return; // Prevent further assertions on null - +const expectMenuItemEnabled = async (menuItem: HTMLElement) => { expect(menuItem).toBeInTheDocument(); expect(menuItem).not.toHaveAttribute('aria-disabled'); const tooltipTrigger = within(menuItem).queryByTestId('tooltip-trigger'); @@ -193,7 +190,7 @@ const expectMenuItemDisabled = async ( * "Drill to detail" item should be enabled and open the correct modal */ const expectDrillToDetailEnabled = async () => { - const drillToDetailMenuItem = await screen.findByRole('menuitem', { + const drillToDetailMenuItem = screen.getByRole('menuitem', { name: 'Drill to detail', }); @@ -205,7 +202,7 @@ const expectDrillToDetailEnabled = async () => { * "Drill to detail" item should be present and disabled */ const expectDrillToDetailDisabled = async (tooltipContent?: string) => { - const drillToDetailMenuItem = await screen.findByRole('menuitem', { + const drillToDetailMenuItem = screen.getByRole('menuitem', { name: 'Drill to detail', }); @@ -253,70 +250,150 @@ const expectDrillToDetailByDisabled = async (tooltipContent?: string) => { /** * "Drill to detail by {dimension}" submenu item should exist and open the correct modal */ -// Add cleanup after each test -afterEach(async () => { - cleanup(); - // Wait for any pending effects to complete - await new Promise(resolve => setTimeout(resolve, 0)); -}); - -// Update expectDrillToDetailByDimension helper const expectDrillToDetailByDimension = async ( filter: BinaryQueryObjectFilterClause, ) => { userEvent.hover(screen.getByRole('menuitem', { name: 'Drill to detail by' })); - await waitFor(async () => { - const drillToDetailBySubMenus = await screen.findAllByTestId( - 'drill-to-detail-by-submenu', - ); - expect(drillToDetailBySubMenus.length).toBeGreaterThan(0); - }); + const drillToDetailBySubMenus = await screen.findAllByTestId( + 'drill-to-detail-by-submenu', + ); const menuItemName = `Drill to detail by ${filter.formattedVal}`; - const menuItem = await screen.findByText(menuItemName); + const drillToDetailBySubmenuItems = await within( + drillToDetailBySubMenus[1], + ).findAllByRole('menuitem'); - await expectMenuItemEnabled(menuItem); + await expectMenuItemEnabled(drillToDetailBySubmenuItems[0]); await expectDrillToDetailModal(menuItemName, [filter]); }; -// Update expectDrillToDetailByAll helper +/** + * "Drill to detail by all" submenu item should exist and open the correct modal + */ const expectDrillToDetailByAll = async ( filters: BinaryQueryObjectFilterClause[], ) => { userEvent.hover(screen.getByRole('menuitem', { name: 'Drill to detail by' })); - await waitFor(async () => { - const drillToDetailBySubMenus = await screen.findAllByTestId( - 'drill-to-detail-by-submenu', - ); - expect(drillToDetailBySubMenus.length).toBeGreaterThan(0); + const drillToDetailBySubMenus = await screen.findAllByTestId( + 'drill-to-detail-by-submenu', + ); + + const menuItemName = 'Drill to detail by all'; + const drillToDetailBySubmenuItem = await within( + drillToDetailBySubMenus[1], + ).findByRole('menuitem', { name: menuItemName }); + + await expectMenuItemEnabled(drillToDetailBySubmenuItem); + await expectDrillToDetailModal(menuItemName, filters); +}; + +beforeAll(() => { + setupPlugins(); +}); + +test('dropdown menu for unsupported chart', async () => { + renderMenu({ formData: unsupportedChartFormData }); + await expectDrillToDetailEnabled(); + await expectNoDrillToDetailBy(); +}); + +test('context menu for unsupported chart', async () => { + renderMenu({ + formData: unsupportedChartFormData, + isContextMenu: true, }); - const menuItem = await screen.findByText((content, element) => { - const text = element?.textContent || ''; - return text.toLowerCase().includes('drill to detail by all'); + await expectDrillToDetailEnabled(); + await expectDrillToDetailByDisabled( + 'Drill to detail by value is not yet supported for this chart type.', + ); +}); + +test('dropdown menu for supported chart, no dimensions', async () => { + renderMenu({ + formData: noDimensionsFormData, }); - await expectMenuItemEnabled(menuItem); - await expectDrillToDetailModal('Drill to detail by all', filters); -}; + await expectDrillToDetailDisabled( + 'Drill to detail is disabled because this chart does not group data by dimension value.', + ); + + await expectNoDrillToDetailBy(); +}); + +test('context menu for supported chart, no dimensions, no filters', async () => { + renderMenu({ + formData: noDimensionsFormData, + isContextMenu: true, + }); + + const message = + 'Drill to detail is disabled because this chart does not group data by dimension value.'; + + await expectDrillToDetailDisabled(message); + await expectDrillToDetailByDisabled(message); +}); + +test('context menu for supported chart, no dimensions, 1 filter', async () => { + renderMenu({ + formData: noDimensionsFormData, + isContextMenu: true, + filters: [filterA], + }); + + const message = + 'Drill to detail is disabled because this chart does not group data by dimension value.'; + + await expectDrillToDetailDisabled(message); + await expectDrillToDetailByDisabled(message); +}); + +test('dropdown menu for supported chart, dimensions', async () => { + renderMenu({ formData: defaultFormData }); + await expectDrillToDetailEnabled(); + await expectNoDrillToDetailBy(); +}); + +test('context menu for supported chart, dimensions, no filters', async () => { + renderMenu({ + formData: defaultFormData, + isContextMenu: true, + }); + + await expectDrillToDetailEnabled(); + await expectDrillToDetailByDisabled( + 'Right-click on a dimension value to drill to detail by that value.', + ); +}); + +test('context menu for supported chart, dimensions, 1 filter', async () => { + const filters = [filterA]; + renderMenu({ + formData: defaultFormData, + isContextMenu: true, + filters, + }); + + await expectDrillToDetailByEnabled(); + await expectDrillToDetailByDimension(filterA); +}); -// Update test timeouts test('context menu for supported chart, dimensions, filter A', async () => { const filters = [filterA, filterB]; setupMenu(filters); await expectDrillToDetailByEnabled(); await expectDrillToDetailByDimension(filterA); -}, 30000); +}); test('context menu for supported chart, dimensions, filter B', async () => { const filters = [filterA, filterB]; setupMenu(filters); await expectDrillToDetailByEnabled(); await expectDrillToDetailByDimension(filterB); -}, 30000); +}); test('context menu for supported chart, dimensions, all filters', async () => { const filters = [filterA, filterB]; setupMenu(filters); await expectDrillToDetailByAll(filters); -}, 30000); +}); diff --git a/superset-frontend/src/components/Collapse/Collapse.test.tsx b/superset-frontend/src/components/Collapse/Collapse.test.tsx index bd49de8032..a3d38d04c5 100644 --- a/superset-frontend/src/components/Collapse/Collapse.test.tsx +++ b/superset-frontend/src/components/Collapse/Collapse.test.tsx @@ -16,93 +16,103 @@ * specific language governing permissions and limitations * under the License. */ -import { render, screen } from 'spec/helpers/testing-library'; +import { render, screen, cleanup, waitFor } from 'spec/helpers/testing-library'; import userEvent from '@testing-library/user-event'; -import { supersetTheme, hexToRgb } from '@superset-ui/core'; import Collapse, { CollapseProps } from '.'; -function renderCollapse(props?: CollapseProps) { - return render( - <Collapse {...props}> - <Collapse.Panel header="Header 1" key="1"> - Content 1 - </Collapse.Panel> - <Collapse.Panel header="Header 2" key="2"> - Content 2 - </Collapse.Panel> - </Collapse>, - ); -} - -test('renders collapsed with default props', () => { - renderCollapse(); - - const headers = screen.getAllByRole('button'); - - expect(headers[0]).toHaveTextContent('Header 1'); - expect(headers[1]).toHaveTextContent('Header 2'); - expect(screen.queryByText('Content 1')).not.toBeInTheDocument(); - expect(screen.queryByText('Content 2')).not.toBeInTheDocument(); -}); +describe('Collapse', () => { + beforeAll(() => { + jest.setTimeout(30000); + }); -test('renders with one item expanded by default', () => { - renderCollapse({ defaultActiveKey: ['1'] }); + afterEach(async () => { + cleanup(); + await new Promise(resolve => setTimeout(resolve, 0)); + }); - const headers = screen.getAllByRole('button'); + function renderCollapse(props?: CollapseProps) { + return render( + <Collapse {...props}> + <Collapse.Panel header="Header 1" key="1"> + Content 1 + </Collapse.Panel> + <Collapse.Panel header="Header 2" key="2"> + Content 2 + </Collapse.Panel> + </Collapse>, + ); + } + + test('renders collapsed with default props', async () => { + const { unmount } = renderCollapse(); + const headers = screen.getAllByRole('button'); + + expect(headers[0]).toHaveTextContent('Header 1'); + expect(headers[1]).toHaveTextContent('Header 2'); + expect(screen.queryByText('Content 1')).not.toBeInTheDocument(); + expect(screen.queryByText('Content 2')).not.toBeInTheDocument(); + + unmount(); + }); - expect(headers[0]).toHaveTextContent('Header 1'); - expect(headers[1]).toHaveTextContent('Header 2'); - expect(screen.getByText('Content 1')).toBeInTheDocument(); - expect(screen.queryByText('Content 2')).not.toBeInTheDocument(); -}); + test('renders with one item expanded by default', async () => { + const { unmount } = renderCollapse({ defaultActiveKey: ['1'] }); + const headers = screen.getAllByRole('button'); -test('expands on click', () => { - renderCollapse(); + expect(headers[0]).toHaveTextContent('Header 1'); + expect(headers[1]).toHaveTextContent('Header 2'); + expect(screen.getByText('Content 1')).toBeInTheDocument(); + expect(screen.queryByText('Content 2')).not.toBeInTheDocument(); - expect(screen.queryByText('Content 1')).not.toBeInTheDocument(); - expect(screen.queryByText('Content 2')).not.toBeInTheDocument(); + unmount(); + }); - userEvent.click(screen.getAllByRole('button')[0]); + test('expands on click without waitFor', async () => { + const { unmount } = renderCollapse(); - expect(screen.getByText('Content 1')).toBeInTheDocument(); - expect(screen.queryByText('Content 2')).not.toBeInTheDocument(); -}); + expect(screen.queryByText('Content 1')).not.toBeInTheDocument(); + expect(screen.queryByText('Content 2')).not.toBeInTheDocument(); -test('collapses on click', () => { - renderCollapse({ defaultActiveKey: ['1'] }); + await userEvent.click(screen.getAllByRole('button')[0]); - expect(screen.getByText('Content 1')).toBeInTheDocument(); - expect(screen.queryByText('Content 2')).not.toBeInTheDocument(); + expect(screen.getByText('Content 1')).toBeInTheDocument(); + expect(screen.queryByText('Content 2')).not.toBeInTheDocument(); - userEvent.click(screen.getAllByRole('button')[0]); + unmount(); + }); - expect(screen.getByText('Content 1').parentNode).toHaveClass( - 'ant-collapse-content-hidden', - ); - expect(screen.queryByText('Content 2')).not.toBeInTheDocument(); -}); + test('expands on click with waitFor', async () => { + const { unmount } = renderCollapse(); -test('renders with custom properties', () => { - renderCollapse({ - light: true, - bigger: true, - bold: true, - animateArrows: true, + await waitFor(() => { + expect(screen.queryByText('Content 1')).not.toBeInTheDocument(); + expect(screen.queryByText('Content 2')).not.toBeInTheDocument(); + }); + + await userEvent.click(screen.getAllByRole('button')[0]); + + await waitFor(() => { + expect(screen.getByText('Content 1')).toBeInTheDocument(); + expect(screen.queryByText('Content 2')).not.toBeInTheDocument(); + }); + + unmount(); }); - const header = document.getElementsByClassName('ant-collapse-header')[0]; - const arrow = - document.getElementsByClassName('ant-collapse-arrow')[0].children[0]; - - const headerStyle = window.getComputedStyle(header); - const arrowStyle = window.getComputedStyle(arrow); - - expect(headerStyle.fontWeight).toBe( - supersetTheme.typography.weights.bold.toString(), - ); - expect(headerStyle.fontSize).toBe(`${supersetTheme.gridUnit * 4}px`); - expect(headerStyle.color).toBe( - hexToRgb(supersetTheme.colors.grayscale.light4), - ); - expect(arrowStyle.transition).toBe('transform 0.24s'); + // Update other tests similarly with waitFor + test('collapses on click', async () => { + const { unmount } = renderCollapse({ defaultActiveKey: ['1'] }); + + expect(screen.getByText('Content 1')).toBeInTheDocument(); + expect(screen.queryByText('Content 2')).not.toBeInTheDocument(); + + await userEvent.click(screen.getAllByRole('button')[0]); + + expect(screen.getByText('Content 1').parentNode).toHaveClass( + 'ant-collapse-content-hidden', + ); + expect(screen.queryByText('Content 2')).not.toBeInTheDocument(); + + unmount(); + }); }); diff --git a/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx b/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx index ee17f2d801..44c23a0756 100644 --- a/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx +++ b/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx @@ -203,98 +203,111 @@ test('Should render', async () => { expect(await screen.findByTestId('DatabaseSelector')).toBeInTheDocument(); }); -test('Refresh should work', async () => { - const props = createProps(); +describe('DatabaseSelector', () => { + beforeAll(() => { + jest.setTimeout(30000); + }); - render(<DatabaseSelector {...props} />, { useRedux: true, store }); + afterEach(async () => { + fetchMock.reset(); + act(() => { + store.dispatch(api.util.resetApiState()); + }); + await new Promise(resolve => setTimeout(resolve, 0)); + }); - expect(fetchMock.calls(schemaApiRoute).length).toBe(0); + test('Refresh should work', async () => { + const props = createProps(); + const { unmount } = render(<DatabaseSelector {...props} />, { + useRedux: true, + store, + }); - const select = screen.getByRole('combobox', { - name: 'Select schema or type to search schemas', - }); + expect(fetchMock.calls(schemaApiRoute).length).toBe(0); - userEvent.click(select); + const select = screen.getByRole('combobox', { + name: 'Select schema or type to search schemas', + }); - await waitFor(() => { - expect(fetchMock.calls(databaseApiRoute).length).toBe(1); - expect(fetchMock.calls(schemaApiRoute).length).toBe(1); - expect(props.handleError).toHaveBeenCalledTimes(0); - expect(props.onDbChange).toHaveBeenCalledTimes(0); - expect(props.onSchemaChange).toHaveBeenCalledTimes(0); - }); + await act(async () => { + userEvent.click(select); + }); - // click schema reload - userEvent.click(screen.getByRole('button', { name: 'refresh' })); + await waitFor( + () => { + expect(fetchMock.calls(databaseApiRoute).length).toBe(1); + expect(fetchMock.calls(schemaApiRoute).length).toBe(1); + expect(props.handleError).toHaveBeenCalledTimes(0); + expect(props.onDbChange).toHaveBeenCalledTimes(0); + expect(props.onSchemaChange).toHaveBeenCalledTimes(0); + }, + { timeout: 10000 }, + ); - await waitFor(() => { - expect(fetchMock.calls(databaseApiRoute).length).toBe(1); - expect(fetchMock.calls(schemaApiRoute).length).toBe(2); - expect(props.handleError).toHaveBeenCalledTimes(0); - expect(props.onDbChange).toHaveBeenCalledTimes(0); - expect(props.onSchemaChange).toHaveBeenCalledTimes(0); - }); -}); + // click schema reload + await act(async () => { + userEvent.click(screen.getByRole('button', { name: 'refresh' })); + }); -test('Should database select display options', async () => { - const props = createProps(); - render(<DatabaseSelector {...props} />, { useRedux: true, store }); - const select = screen.getByRole('combobox', { - name: 'Select database or type to search databases', - }); - expect(select).toBeInTheDocument(); - userEvent.click(select); - expect(await screen.findByText('test-mysql')).toBeInTheDocument(); -}); + await waitFor( + () => { + expect(fetchMock.calls(databaseApiRoute).length).toBe(1); + expect(fetchMock.calls(schemaApiRoute).length).toBe(2); + expect(props.handleError).toHaveBeenCalledTimes(0); + expect(props.onDbChange).toHaveBeenCalledTimes(0); + expect(props.onSchemaChange).toHaveBeenCalledTimes(0); + }, + { timeout: 10000 }, + ); -test('should display options in order of the api response', async () => { - fetchMock.get(databaseApiRoute, fakeDatabaseApiResultInReverseOrder, { - overwriteRoutes: true, - }); - const props = createProps(); - render(<DatabaseSelector {...props} db={undefined} />, { - useRedux: true, - store, - }); - const select = screen.getByRole('combobox', { - name: 'Select database or type to search databases', - }); - expect(select).toBeInTheDocument(); - userEvent.click(select); - const options = await screen.findAllByRole('option'); + unmount(); + }, 15000); - expect(options[0]).toHaveTextContent( - `${fakeDatabaseApiResultInReverseOrder.result[0].id}`, - ); - expect(options[1]).toHaveTextContent( - `${fakeDatabaseApiResultInReverseOrder.result[1].id}`, - ); -}); + test('Should fetch the search keyword when total count exceeds initial options', async () => { + fetchMock.get( + databaseApiRoute, + { + ...fakeDatabaseApiResult, + count: fakeDatabaseApiResult.result.length + 1, + }, + { overwriteRoutes: true }, + ); -test('Should fetch the search keyword when total count exceeds initial options', async () => { - fetchMock.get( - databaseApiRoute, - { - ...fakeDatabaseApiResult, - count: fakeDatabaseApiResult.result.length + 1, - }, - { overwriteRoutes: true }, - ); + const props = createProps(); + const { unmount } = render(<DatabaseSelector {...props} />, { + useRedux: true, + store, + }); - const props = createProps(); - render(<DatabaseSelector {...props} />, { useRedux: true, store }); - const select = screen.getByRole('combobox', { - name: 'Select database or type to search databases', - }); - await waitFor(() => - expect(fetchMock.calls(databaseApiRoute)).toHaveLength(1), - ); - expect(select).toBeInTheDocument(); - userEvent.type(select, 'keywordtest'); - await waitFor(() => - expect(fetchMock.calls(databaseApiRoute)).toHaveLength(2), - ); - expect(fetchMock.calls(databaseApiRoute)[1][0]).toContain('keywordtest'); + const select = screen.getByRole('combobox', { + name: 'Select database or type to search databases', + }); + + await waitFor( + () => { + expect(fetchMock.calls(databaseApiRoute)).toHaveLength(1); + }, + { timeout: 10000 }, + ); + + expect(select).toBeInTheDocument(); + + await act(async () => { + userEvent.type(select, 'keywordtest'); + }); + + await waitFor( + () => { + expect(fetchMock.calls(databaseApiRoute)).toHaveLength(2); + expect(fetchMock.calls(databaseApiRoute)[1][0]).toContain( + 'keywordtest', + ); + }, + { timeout: 10000 }, + ); + + unmount(); + }, 15000); }); test('should show empty state if there are no options', async () => { diff --git a/superset-frontend/src/components/ErrorMessage/InvalidSQLErrorMessage.test.tsx b/superset-frontend/src/components/ErrorMessage/InvalidSQLErrorMessage.test.tsx index 7db4c862cc..ac90b0eb96 100644 --- a/superset-frontend/src/components/ErrorMessage/InvalidSQLErrorMessage.test.tsx +++ b/superset-frontend/src/components/ErrorMessage/InvalidSQLErrorMessage.test.tsx @@ -16,7 +16,7 @@ * limitations under the License. */ -import { render } from '@testing-library/react'; +import { render, cleanup } from '@testing-library/react'; import '@testing-library/jest-dom'; import { ErrorLevel, @@ -51,17 +51,28 @@ const renderComponent = (overrides = {}) => ); describe('InvalidSQLErrorMessage', () => { - it('renders the error message with correct properties', () => { - const { getByText } = renderComponent(); + beforeAll(() => { + jest.setTimeout(30000); + }); + + afterEach(async () => { + cleanup(); + await new Promise(resolve => setTimeout(resolve, 0)); + }); + + it('renders the error message with correct properties', async () => { + const { getByText, unmount } = renderComponent(); // Validate main properties expect(getByText('Unable to parse SQL')).toBeInTheDocument(); expect(getByText('Test subtitle')).toBeInTheDocument(); expect(getByText('SELECT * FFROM table')).toBeInTheDocument(); + + unmount(); }); - it('displays the SQL error line and column indicator', () => { - const { getByText, container } = renderComponent(); + it('displays the SQL error line and column indicator', async () => { + const { getByText, container, unmount } = renderComponent(); // Validate SQL and caret indicator expect(getByText('SELECT * FFROM table')).toBeInTheDocument(); @@ -70,16 +81,18 @@ describe('InvalidSQLErrorMessage', () => { const preTags = container.querySelectorAll('pre'); const secondPre = preTags[1]; expect(secondPre).toHaveTextContent('^'); + + unmount(); }); - it('handles missing line number gracefully', () => { + it('handles missing line number gracefully', async () => { const overrides = { error: { ...defaultProps.error, extra: { ...defaultProps.error.extra, line: null }, }, }; - const { getByText, container } = renderComponent(overrides); + const { getByText, container, unmount } = renderComponent(overrides); // Check that the full SQL is displayed expect(getByText('SELECT * FFROM table')).toBeInTheDocument(); @@ -87,15 +100,18 @@ describe('InvalidSQLErrorMessage', () => { // Validate absence of caret indicator const caret = container.querySelector('pre'); expect(caret).not.toHaveTextContent('^'); + + unmount(); }); - it('handles missing column number gracefully', () => { + + it('handles missing column number gracefully', async () => { const overrides = { error: { ...defaultProps.error, extra: { ...defaultProps.error.extra, column: null }, }, }; - const { getByText, container } = renderComponent(overrides); + const { getByText, container, unmount } = renderComponent(overrides); // Check that the full SQL is displayed expect(getByText('SELECT * FFROM table')).toBeInTheDocument(); @@ -103,5 +119,7 @@ describe('InvalidSQLErrorMessage', () => { // Validate absence of caret indicator const caret = container.querySelector('pre'); expect(caret).not.toHaveTextContent('^'); + + unmount(); }); }); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/FilterScope.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/FilterScope.test.tsx index 46c55c1801..7ada1ccaa0 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/FilterScope.test.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/FilterScope.test.tsx @@ -93,17 +93,52 @@ describe('FilterScope', () => { it('select tree values with 1 excluded', async () => { const { unmount } = render(<MockModal />); + + // Wait for the Scoping tab to be visible and click it await waitFor(() => { - fireEvent.click(screen.getByText('Scoping')); + expect(screen.getByText('Scoping')).toBeInTheDocument(); }); + fireEvent.click(screen.getByText('Scoping')); - expect(screen.getByRole('tree')).toBeInTheDocument(); + // Wait for the tree to be rendered + await waitFor( + () => { + expect(screen.getByRole('tree')).toBeInTheDocument(); + }, + { timeout: 10000 }, + ); + + // Wait for tree items to be loaded + await waitFor( + () => { + expect( + document.querySelector('.ant-tree-treenode'), + ).toBeInTheDocument(); + }, + { timeout: 10000 }, + ); + // Expand the tree node and wait for children await waitFor(() => { fireEvent.click(getTreeSwitcher(2)); - fireEvent.click(screen.getByText('CHART_ID2')); }); + // Find and click the chart node using a more specific selector + const chartNode = await waitFor( + () => + document.querySelector('[title="CHART_ID2"]') || + document.querySelector( + '.ant-tree-node-content-wrapper:contains("CHART_ID2")', + ), + ); + + if (!chartNode) { + throw new Error('Chart node not found in tree'); + } + + fireEvent.click(chartNode); + + // Verify the form value await waitFor( () => expect( @@ -116,7 +151,7 @@ describe('FilterScope', () => { ); unmount(); - }, 15000); + }, 20000); it('select 1 value only', async () => { const { unmount } = render(<MockModal />); diff --git a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx index bdf571fa45..2beb9cdf4a 100644 --- a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx +++ b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx @@ -18,6 +18,7 @@ */ import { ReactChild } from 'react'; import { render, screen, waitFor, within } from 'spec/helpers/testing-library'; +import { cleanup } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import DatasourcePanel, { IDatasource, @@ -152,29 +153,9 @@ describe('DatasourcePanel', () => { afterEach(async () => { cleanup(); - // Wait for any pending effects to complete await new Promise(resolve => setTimeout(resolve, 0)); }); - test('should render 0 search results', async () => { - const { unmount } = render(<DatasourcePanel {...props} />, { - useRedux: true, - useDnd: true, - }); - - const searchInput = screen.getByPlaceholderText('Search Metrics & Columns'); - search('nothing', searchInput); - - await waitFor( - () => { - expect(screen.getAllByText('Showing 0 of 0')).toHaveLength(2); - }, - { timeout: 10000 }, - ); - - unmount(); - }, 15000); - test('should search and render matching columns', async () => { const { unmount } = render( <ExploreContainer> @@ -185,10 +166,13 @@ describe('DatasourcePanel', () => { ); const searchInput = screen.getByPlaceholderText('Search Metrics & Columns'); + await waitFor(() => { - search(columns[0].column_name, searchInput); + expect(searchInput).toBeInTheDocument(); }); + search(columns[0].column_name, searchInput); + await waitFor( () => { expect(screen.getByText(columns[0].column_name)).toBeInTheDocument(); diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/Option.test.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/Option.test.tsx index 4e00f5cccf..e1a09732ca 100644 --- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/Option.test.tsx +++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/Option.test.tsx @@ -16,57 +16,72 @@ * specific language governing permissions and limitations * under the License. */ -import { render, screen } from 'spec/helpers/testing-library'; +import { render, screen, cleanup } from 'spec/helpers/testing-library'; import userEvent from '@testing-library/user-event'; import Option from 'src/explore/components/controls/DndColumnSelectControl/Option'; -test('renders with default props', async () => { - const { container } = render( - <Option index={1} clickClose={jest.fn()}> - Option - </Option>, - ); - expect(container).toBeInTheDocument(); - expect( - await screen.findByRole('img', { name: 'x-small' }), - ).toBeInTheDocument(); - expect( - screen.queryByRole('img', { name: 'caret-right' }), - ).not.toBeInTheDocument(); -}); +describe('Option', () => { + beforeAll(() => { + jest.setTimeout(30000); + }); -test('renders with caret', async () => { - render( - <Option index={1} clickClose={jest.fn()} withCaret> - Option - </Option>, - ); - expect( - await screen.findByRole('img', { name: 'x-small' }), - ).toBeInTheDocument(); - expect( - await screen.findByRole('img', { name: 'caret-right' }), - ).toBeInTheDocument(); -}); + afterEach(async () => { + cleanup(); + await new Promise(resolve => setTimeout(resolve, 0)); + }); -test('renders with extra triangle', async () => { - render( - <Option index={1} clickClose={jest.fn()} isExtra> - Option - </Option>, - ); - expect( - await screen.findByRole('button', { name: 'Show info tooltip' }), - ).toBeInTheDocument(); -}); + test('renders with default props', async () => { + const { container, unmount } = render( + <Option index={1} clickClose={jest.fn()}> + Option + </Option>, + ); + expect(container).toBeInTheDocument(); + expect( + await screen.findByRole('img', { name: 'x-small' }), + ).toBeInTheDocument(); + expect( + screen.queryByRole('img', { name: 'caret-right' }), + ).not.toBeInTheDocument(); + unmount(); + }); + + test('renders with caret', async () => { + const { unmount } = render( + <Option index={1} clickClose={jest.fn()} withCaret> + Option + </Option>, + ); + expect( + await screen.findByRole('img', { name: 'x-small' }), + ).toBeInTheDocument(); + expect( + await screen.findByRole('img', { name: 'caret-right' }), + ).toBeInTheDocument(); + unmount(); + }); + + test('renders with extra triangle', async () => { + const { unmount } = render( + <Option index={1} clickClose={jest.fn()} isExtra> + Option + </Option>, + ); + expect( + await screen.findByRole('button', { name: 'Show info tooltip' }), + ).toBeInTheDocument(); + unmount(); + }); -test('triggers onClose', async () => { - const clickClose = jest.fn(); - render( - <Option index={1} clickClose={clickClose}> - Option - </Option>, - ); - userEvent.click(await screen.findByRole('img', { name: 'x-small' })); - expect(clickClose).toHaveBeenCalled(); + test('triggers onClose', async () => { + const clickClose = jest.fn(); + const { unmount } = render( + <Option index={1} clickClose={clickClose}> + Option + </Option>, + ); + userEvent.click(await screen.findByRole('img', { name: 'x-small' })); + expect(clickClose).toHaveBeenCalled(); + unmount(); + }); }); diff --git a/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeControl.test.tsx b/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeControl.test.tsx index e7c3c200fb..57074a2b18 100644 --- a/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeControl.test.tsx +++ b/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeControl.test.tsx @@ -95,14 +95,11 @@ describe('VizTypeControl', () => { isModalOpenInit: true, }; - // Add waitForEffects helper - const waitForEffects = () => new Promise(resolve => setTimeout(resolve, 0)); - - const waitForRenderWrapper = async ( + const waitForRenderWrapper = ( props: typeof defaultProps = defaultProps, state: object = stateWithoutNativeFilters, - ) => { - const result = await waitFor(() => { + ) => + waitFor(() => { render( <DynamicPluginProvider> <VizTypeControl {...props} /> @@ -110,21 +107,12 @@ describe('VizTypeControl', () => { { useRedux: true, initialState: state }, ); }); - await waitForEffects(); - return result; - }; - - beforeAll(() => { - jest.setTimeout(30000); - }); - afterEach(async () => { + afterEach(() => { cleanup(); jest.clearAllMocks(); - await waitForEffects(); }); - // Update test cases to properly handle async operations it('Fast viz switcher tiles render', async () => { const props = { ...defaultProps, @@ -132,8 +120,6 @@ describe('VizTypeControl', () => { isModalOpenInit: false, }; await waitForRenderWrapper(props); - await waitForEffects(); - expect(screen.getByLabelText('table-chart-tile')).toBeVisible(); expect(screen.getByLabelText('big-number-chart-tile')).toBeVisible(); expect(screen.getByLabelText('pie-chart-tile')).toBeVisible(); @@ -203,7 +189,6 @@ describe('VizTypeControl', () => { ).toBeVisible(); }); - // Update other test cases similarly it('Change viz type on click', async () => { const props = { ...defaultProps, @@ -211,17 +196,13 @@ describe('VizTypeControl', () => { isModalOpenInit: false, }; await waitForRenderWrapper(props); - await waitForEffects(); - - const switcher = screen.getByTestId('fast-viz-switcher'); - userEvent.click(within(switcher).getByText('Line Chart')); - await waitForEffects(); - + userEvent.click( + within(screen.getByTestId('fast-viz-switcher')).getByText('Line Chart'), + ); expect(props.onChange).not.toHaveBeenCalled(); - - userEvent.click(within(switcher).getByText('Table')); - await waitForEffects(); - + userEvent.click( + within(screen.getByTestId('fast-viz-switcher')).getByText('Table'), + ); expect(props.onChange).toHaveBeenCalledWith('table'); }); diff --git a/superset-frontend/src/features/home/Menu.test.tsx b/superset-frontend/src/features/home/Menu.test.tsx index f76e683159..4a095472df 100644 --- a/superset-frontend/src/features/home/Menu.test.tsx +++ b/superset-frontend/src/features/home/Menu.test.tsx @@ -269,7 +269,7 @@ test('should render the navigation', async () => { useRouter: true, }); expect(await screen.findByRole('navigation')).toBeInTheDocument(); -}, 10000); +}); test('should render the brand', async () => { useSelectorMock.mockReturnValue({ roles: user.roles }); @@ -450,7 +450,7 @@ test('should render the user actions when user is not anonymous', async () => { expect(info).toHaveAttribute('href', user_info_url); expect(logout).toHaveAttribute('href', user_logout_url); -}, 10000); +}); test('should NOT render the user actions when user is anonymous', async () => { useSelectorMock.mockReturnValue({ roles: user.roles });
