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 6d45f7e48a510e54bb7df5c998fe16e690b3b8a9 Author: Evan Rusackas <[email protected]> AuthorDate: Fri Feb 7 15:19:13 2025 -0700 more fixes... --- superset-frontend/package.json | 2 +- .../Chart/DrillDetail/DrillDetailPane.test.tsx | 68 +- .../DatabaseSelector/DatabaseSelector.test.tsx | 173 +++-- .../src/components/Select/AsyncSelect.test.tsx | 24 +- .../FilterScope/FilterScope.test.tsx | 95 ++- .../AnnotationLayer.test.tsx | 2 +- .../databases/DatabaseModal/index.test.tsx | 701 ++++++++++----------- 7 files changed, 485 insertions(+), 580 deletions(-) diff --git a/superset-frontend/package.json b/superset-frontend/package.json index a3dbc78926..1d074fdaab 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -69,7 +69,7 @@ "prune": "rm -rf ./{packages,plugins}/*/{node_modules,lib,esm,tsconfig.tsbuildinfo,package-lock.json} ./.temp_cache", "storybook": "cross-env NODE_ENV=development BABEL_ENV=development storybook dev -p 6006", "tdd": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=4096\" jest --watch", - "test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=4096\" jest --max-workers=90% --silent", + "test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=4096\" jest --max-workers=30% --silent", "type": "tsc --noEmit", "update-maps": "jupyter nbconvert --to notebook --execute --inplace 'plugins/legacy-plugin-chart-country-map/scripts/Country Map GeoJSON Generator.ipynb' -Xfrozen_modules=off", "validate-release": "../RELEASING/validate_this_release.sh" diff --git a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.test.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.test.tsx index 9f6c7f6d3b..6f1a628646 100644 --- a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.test.tsx +++ b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.test.tsx @@ -16,26 +16,14 @@ * specific language governing permissions and limitations * under the License. */ -import { cleanup } from '@testing-library/react'; import fetchMock from 'fetch-mock'; -import type { QueryFormData } from '@superset-ui/core'; -import { SupersetClient } from '@superset-ui/core'; +import { QueryFormData, SupersetClient } from '@superset-ui/core'; import { render, screen, waitFor } from 'spec/helpers/testing-library'; import { getMockStoreWithNativeFilters } from 'spec/fixtures/mockStore'; import chartQueries, { sliceId } from 'spec/fixtures/mockChartQueries'; import { supersetGetCache } from 'src/utils/cachedSupersetGet'; import DrillDetailPane from './DrillDetailPane'; -// Add cleanup after each test -afterEach(async () => { - cleanup(); - fetchMock.restore(); - supersetGetCache.clear(); - jest.resetAllMocks(); - // Wait for any pending effects to complete - await new Promise(resolve => setTimeout(resolve, 0)); -}); - const chart = chartQueries[sliceId]; const setup = (overrides: Record<string, any> = {}) => { const store = getMockStoreWithNativeFilters(); @@ -126,52 +114,44 @@ const fetchWithData = () => { }); }; -// Add cleanup function -afterEach(async () => { +afterEach(() => { fetchMock.restore(); supersetGetCache.clear(); - // Wait for any pending effects to complete - await waitFor(() => {}, { timeout: 100 }).catch(() => {}); }); -// Modify test cases to properly await all operations test('should render', async () => { fetchWithNoData(); const { container } = await waitForRender(); - await waitFor(() => { - expect(container).toBeInTheDocument(); - }); + expect(container).toBeInTheDocument(); }); test('should render loading indicator', async () => { fetchWithData(); setup(); - await waitFor(() => { - expect(screen.getByLabelText('Loading')).toBeInTheDocument(); - }); + await waitFor(() => + expect(screen.getByLabelText('Loading')).toBeInTheDocument(), + ); }); test('should render the table with results', async () => { fetchWithData(); await waitForRender(); - await waitFor(() => { - expect(screen.getByRole('table')).toBeInTheDocument(); - expect(screen.getByText('1996')).toBeInTheDocument(); - expect(screen.getByText('11.27')).toBeInTheDocument(); - expect(screen.getByText('1989')).toBeInTheDocument(); - expect(screen.getByText('23.2')).toBeInTheDocument(); - expect(screen.getByText('1999')).toBeInTheDocument(); - expect(screen.getByText('9')).toBeInTheDocument(); - expect( - screen.getByRole('columnheader', { name: 'year' }), - ).toBeInTheDocument(); - expect( - screen.getByRole('columnheader', { name: 'na_sales' }), - ).toBeInTheDocument(); - expect( - screen.getByRole('columnheader', { name: 'eu_sales' }), - ).toBeInTheDocument(); - }); + expect(screen.getByRole('table')).toBeInTheDocument(); + expect(screen.getByText('1996')).toBeInTheDocument(); + expect(screen.getByText('11.27')).toBeInTheDocument(); + expect(screen.getByText('1989')).toBeInTheDocument(); + expect(screen.getByText('23.2')).toBeInTheDocument(); + expect(screen.getByText('1999')).toBeInTheDocument(); + expect(screen.getByText('9')).toBeInTheDocument(); + expect( + screen.getByRole('columnheader', { name: 'year' }), + ).toBeInTheDocument(); + expect( + screen.getByRole('columnheader', { name: 'na_sales' }), + ).toBeInTheDocument(); + expect( + screen.getByRole('columnheader', { name: 'eu_sales' }), + ).toBeInTheDocument(); }); test('should render the "No results" components', async () => { @@ -215,7 +195,5 @@ test('should render the error', async () => { .spyOn(SupersetClient, 'post') .mockRejectedValue(new Error('Something went wrong')); await waitForRender(); - await waitFor(() => { - expect(screen.getByText('Error: Something went wrong')).toBeInTheDocument(); - }); + expect(screen.getByText('Error: Something went wrong')).toBeInTheDocument(); }); diff --git a/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx b/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx index 44c23a0756..ee17f2d801 100644 --- a/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx +++ b/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx @@ -203,111 +203,98 @@ test('Should render', async () => { expect(await screen.findByTestId('DatabaseSelector')).toBeInTheDocument(); }); -describe('DatabaseSelector', () => { - beforeAll(() => { - jest.setTimeout(30000); - }); - - afterEach(async () => { - fetchMock.reset(); - act(() => { - store.dispatch(api.util.resetApiState()); - }); - await new Promise(resolve => setTimeout(resolve, 0)); - }); - - test('Refresh should work', async () => { - const props = createProps(); - const { unmount } = render(<DatabaseSelector {...props} />, { - useRedux: true, - store, - }); - - expect(fetchMock.calls(schemaApiRoute).length).toBe(0); - - const select = screen.getByRole('combobox', { - name: 'Select schema or type to search schemas', - }); - - await act(async () => { - userEvent.click(select); - }); +test('Refresh should work', async () => { + const props = createProps(); - 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 }, - ); + render(<DatabaseSelector {...props} />, { useRedux: true, store }); - // click schema reload - await act(async () => { - userEvent.click(screen.getByRole('button', { name: 'refresh' })); - }); + expect(fetchMock.calls(schemaApiRoute).length).toBe(0); - 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 }, - ); + const select = screen.getByRole('combobox', { + name: 'Select schema or type to search schemas', + }); - unmount(); - }, 15000); + userEvent.click(select); - test('Should fetch the search keyword when total count exceeds initial options', async () => { - fetchMock.get( - databaseApiRoute, - { - ...fakeDatabaseApiResult, - count: fakeDatabaseApiResult.result.length + 1, - }, - { overwriteRoutes: true }, - ); + 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); + }); - const props = createProps(); - const { unmount } = render(<DatabaseSelector {...props} />, { - useRedux: true, - store, - }); + // click schema reload + userEvent.click(screen.getByRole('button', { name: 'refresh' })); - const select = screen.getByRole('combobox', { - name: 'Select database or type to search databases', - }); + 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); + }); +}); - await waitFor( - () => { - expect(fetchMock.calls(databaseApiRoute)).toHaveLength(1); - }, - { timeout: 10000 }, - ); +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(); +}); - expect(select).toBeInTheDocument(); +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'); - await act(async () => { - userEvent.type(select, 'keywordtest'); - }); + expect(options[0]).toHaveTextContent( + `${fakeDatabaseApiResultInReverseOrder.result[0].id}`, + ); + expect(options[1]).toHaveTextContent( + `${fakeDatabaseApiResultInReverseOrder.result[1].id}`, + ); +}); - await waitFor( - () => { - expect(fetchMock.calls(databaseApiRoute)).toHaveLength(2); - expect(fetchMock.calls(databaseApiRoute)[1][0]).toContain( - 'keywordtest', - ); - }, - { timeout: 10000 }, - ); +test('Should fetch the search keyword when total count exceeds initial options', async () => { + fetchMock.get( + databaseApiRoute, + { + ...fakeDatabaseApiResult, + count: fakeDatabaseApiResult.result.length + 1, + }, + { overwriteRoutes: true }, + ); - unmount(); - }, 15000); + 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'); }); test('should show empty state if there are no options', async () => { diff --git a/superset-frontend/src/components/Select/AsyncSelect.test.tsx b/superset-frontend/src/components/Select/AsyncSelect.test.tsx index be46dbbe45..2e9efcbf0b 100644 --- a/superset-frontend/src/components/Select/AsyncSelect.test.tsx +++ b/superset-frontend/src/components/Select/AsyncSelect.test.tsx @@ -26,7 +26,6 @@ import { } from 'spec/helpers/testing-library'; import userEvent from '@testing-library/user-event'; import { AsyncSelect } from 'src/components'; -import { cleanup } from '@testing-library/react'; const ARIA_LABEL = 'Test'; const NEW_OPTION = 'Kyle'; @@ -105,13 +104,10 @@ const getSelect = () => screen.getByRole('combobox', { name: ARIA_LABEL }); const getAllSelectOptions = () => getElementsByClassName('.ant-select-item-option-content'); -const findSelectOption = async (text: string) => { - await waitFor(() => { - const virtualList = getElementByClassName('.rc-virtual-list'); - expect(virtualList).not.toBeInTheDocument(); - }); - return within(getElementByClassName('.rc-virtual-list')).findByText(text); -}; +const findSelectOption = (text: string) => + waitFor(() => + within(getElementByClassName('.rc-virtual-list')).getByText(text), + ); const querySelectOption = (text: string) => waitFor(() => @@ -149,13 +145,6 @@ const type = (text: string) => { const open = () => waitFor(() => userEvent.click(getSelect())); -// Add cleanup after each test -afterEach(async () => { - cleanup(); - // Wait for any pending effects to complete - await new Promise(resolve => setTimeout(resolve, 0)); -}); - test('displays a header', async () => { const headerText = 'Header'; render(<AsyncSelect {...defaultProps} header={headerText} />); @@ -308,10 +297,7 @@ test('searches for label or value', async () => { render(<AsyncSelect {...defaultProps} />); const search = option.value; await type(search.toString()); - - const optionElement = await findSelectOption(option.label); - expect(optionElement).toBeInTheDocument(); - + expect(await findSelectOption(option.label)).toBeInTheDocument(); const options = await findAllSelectOptions(); expect(options.length).toBe(1); expect(options[0]).toHaveTextContent(option.label); 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 7ada1ccaa0..c37153993e 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 @@ -76,69 +76,46 @@ describe('FilterScope', () => { document.querySelectorAll('.ant-tree-switcher')[order]; beforeAll(() => { - jest.setTimeout(30000); + // Reduce the global timeout + jest.setTimeout(10000); }); - afterEach(async () => { - cleanup(); - // Wait for any pending effects to complete - await new Promise(resolve => setTimeout(resolve, 0)); + beforeEach(() => { + // Add this to speed up animations/transitions + jest.useFakeTimers(); }); - it('renders "apply to all" filter scope', async () => { - const { unmount } = render(<MockModal />); - expect(screen.queryByRole('tree')).not.toBeInTheDocument(); - unmount(); + afterEach(() => { + cleanup(); + jest.runOnlyPendingTimers(); + jest.useRealTimers(); }); 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(() => { - expect(screen.getByText('Scoping')).toBeInTheDocument(); - }); - fireEvent.click(screen.getByText('Scoping')); + // First, wait for and click the Scoping tab + const scopingTab = await screen.findByRole('tab', { name: 'Scoping' }); + fireEvent.click(scopingTab); - // Wait for the tree to be rendered + // Wait for tree to be rendered after tab switch 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)); - }); - - // 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")', - ), + { timeout: 3000 }, ); - if (!chartNode) { - throw new Error('Chart node not found in tree'); - } + // Continue with tree interactions + fireEvent.click(getTreeSwitcher(2)); + const chartNode = await screen.findByText('CHART_ID2'); fireEvent.click(chartNode); - // Verify the form value + // Check form values await waitFor( () => expect( @@ -147,27 +124,37 @@ describe('FilterScope', () => { excluded: [20], rootPath: ['ROOT_ID'], }), - { timeout: 10000 }, + { timeout: 3000 }, ); unmount(); - }, 20000); + }, 5000); it('select 1 value only', async () => { const { unmount } = render(<MockModal />); - await waitFor(() => { - fireEvent.click(screen.getByText('Scoping')); - }); + // First, wait for and click the Scoping tab + const scopingTab = await screen.findByRole('tab', { name: 'Scoping' }); + fireEvent.click(scopingTab); + + // Wait for tree to be rendered + await waitFor( + () => { + expect(screen.getByRole('tree')).toBeInTheDocument(); + }, + { timeout: 3000 }, + ); - expect(screen.getByRole('tree')).toBeInTheDocument(); + // Perform tree interactions + fireEvent.click(getTreeSwitcher(2)); - await waitFor(() => { - fireEvent.click(getTreeSwitcher(2)); - fireEvent.click(screen.getByText('CHART_ID2')); - fireEvent.click(screen.getByText('tab1')); - }); + const chartNode = await screen.findByText('CHART_ID2'); + fireEvent.click(chartNode); + + const tabNode = await screen.findByText('tab1'); + fireEvent.click(tabNode); + // Update expected state to include both excluded IDs await waitFor( () => expect( @@ -176,11 +163,11 @@ describe('FilterScope', () => { excluded: [18, 20], rootPath: ['ROOT_ID'], }), - { timeout: 10000 }, + { timeout: 3000 }, ); unmount(); - }, 15000); + }, 5000); it('correct init tree with values', async () => { const { unmount } = render( diff --git a/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.test.tsx b/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.test.tsx index 3f3a592ea0..1b60cd7ae6 100644 --- a/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.test.tsx +++ b/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.test.tsx @@ -102,7 +102,7 @@ test('renders extra checkboxes when type is time series', async () => { await screen.findByRole('button', { name: 'Show Markers' }), ).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Hide Line' })).toBeInTheDocument(); -}); +}, 10000); test('enables apply and ok buttons', async () => { const { container } = render(<AnnotationLayer {...defaultProps} />); diff --git a/superset-frontend/src/features/databases/DatabaseModal/index.test.tsx b/superset-frontend/src/features/databases/DatabaseModal/index.test.tsx index b6b3e208bd..5c109a1c4e 100644 --- a/superset-frontend/src/features/databases/DatabaseModal/index.test.tsx +++ b/superset-frontend/src/features/databases/DatabaseModal/index.test.tsx @@ -21,14 +21,7 @@ import fetchMock from 'fetch-mock'; import userEvent from '@testing-library/user-event'; -import { - render, - screen, - within, - cleanup, - act, - waitFor, -} from 'spec/helpers/testing-library'; +import { render, screen, within, waitFor } from 'spec/helpers/testing-library'; import { getExtensionsRegistry } from '@superset-ui/core'; import setupExtensions from 'src/setup/setupExtensions'; import * as hooks from 'src/views/CRUD/hooks'; @@ -37,6 +30,7 @@ import DatabaseModal, { dbReducer, DBReducerActionType, ActionType, + DatabaseModalProps, } from './index'; jest.mock('@superset-ui/core', () => ({ @@ -64,279 +58,281 @@ const AVAILABLE_DB_ENDPOINT = 'glob:*/api/v1/database/available*'; const VALIDATE_PARAMS_ENDPOINT = 'glob:*/api/v1/database/validate_parameters*'; const DATABASE_CONNECT_ENDPOINT = 'glob:*/api/v1/database/'; -fetchMock.post(DATABASE_CONNECT_ENDPOINT, { - id: 10, - result: { - configuration_method: 'sqlalchemy_form', - database_name: 'Other2', - driver: 'apsw', - expose_in_sqllab: true, - extra: '{"allows_virtual_table_explore":true}', - sqlalchemy_uri: 'gsheets://', - }, - json: 'foo', -}); +const databaseFixture: DatabaseObject = { + id: 123, + backend: 'postgres', + configuration_method: ConfigurationMethod.DynamicForm, + database_name: 'Postgres', + name: 'PostgresDB', + is_managed_externally: false, + driver: 'psycopg2', +}; -fetchMock.config.overwriteRoutes = true; -fetchMock.get(DATABASE_FETCH_ENDPOINT, { - result: { - id: 10, - database_name: 'my database', - expose_in_sqllab: false, - allow_ctas: false, - allow_cvas: false, - configuration_method: 'sqlalchemy_form', - }, -}); -fetchMock.mock(AVAILABLE_DB_ENDPOINT, { - databases: [ - { - available_drivers: ['psycopg2'], - default_driver: 'psycopg2', - engine: 'postgresql', - name: 'PostgreSQL', - parameters: { - properties: { - database: { - description: 'Database name', - type: 'string', - }, - encryption: { - description: 'Use an encrypted connection to the database', - type: 'boolean', - }, - host: { - description: 'Hostname or IP address', - type: 'string', - }, - password: { - description: 'Password', - nullable: true, - type: 'string', - }, - port: { - description: 'Database port', - format: 'int32', - maximum: 65536, - minimum: 0, - type: 'integer', - }, - query: { - additionalProperties: {}, - description: 'Additional parameters', - type: 'object', - }, - ssh: { - description: 'Create SSH Tunnel', - type: 'boolean', - }, - username: { - description: 'Username', - nullable: true, - type: 'string', - }, - }, - required: ['database', 'host', 'port', 'username'], - type: 'object', - }, - preferred: true, - sqlalchemy_uri_placeholder: - 'postgresql://user:password@host:port/dbname[?key=value&key=value...]', - engine_information: { - supports_file_upload: true, - disable_ssh_tunneling: false, +describe('DatabaseModal', () => { + beforeEach(() => { + fetchMock.post(DATABASE_CONNECT_ENDPOINT, { + id: 10, + result: { + configuration_method: 'sqlalchemy_form', + database_name: 'Other2', + driver: 'apsw', + expose_in_sqllab: true, + extra: '{"allows_virtual_table_explore":true}', + sqlalchemy_uri: 'gsheets://', }, - }, - { - available_drivers: ['rest'], - engine: 'presto', - name: 'Presto', - preferred: true, - engine_information: { - supports_file_upload: true, - disable_ssh_tunneling: false, + json: 'foo', + }); + + fetchMock.get(DATABASE_FETCH_ENDPOINT, { + result: { + id: 10, + database_name: 'my database', + expose_in_sqllab: false, + allow_ctas: false, + allow_cvas: false, + configuration_method: 'sqlalchemy_form', }, - }, - { - available_drivers: ['mysqldb'], - default_driver: 'mysqldb', - engine: 'mysql', - name: 'MySQL', - parameters: { - properties: { - database: { - description: 'Database name', - type: 'string', - }, - encryption: { - description: 'Use an encrypted connection to the database', - type: 'boolean', - }, - host: { - description: 'Hostname or IP address', - type: 'string', + }); + fetchMock.mock(AVAILABLE_DB_ENDPOINT, { + databases: [ + { + available_drivers: ['psycopg2'], + default_driver: 'psycopg2', + engine: 'postgresql', + name: 'PostgreSQL', + parameters: { + properties: { + database: { + description: 'Database name', + type: 'string', + }, + encryption: { + description: 'Use an encrypted connection to the database', + type: 'boolean', + }, + host: { + description: 'Hostname or IP address', + type: 'string', + }, + password: { + description: 'Password', + nullable: true, + type: 'string', + }, + port: { + description: 'Database port', + format: 'int32', + maximum: 65536, + minimum: 0, + type: 'integer', + }, + query: { + additionalProperties: {}, + description: 'Additional parameters', + type: 'object', + }, + ssh: { + description: 'Create SSH Tunnel', + type: 'boolean', + }, + username: { + description: 'Username', + nullable: true, + type: 'string', + }, + }, + required: ['database', 'host', 'port', 'username'], + type: 'object', }, - password: { - description: 'Password', - nullable: true, - type: 'string', + preferred: true, + sqlalchemy_uri_placeholder: + 'postgresql://user:password@host:port/dbname[?key=value&key=value...]', + engine_information: { + supports_file_upload: true, + disable_ssh_tunneling: false, }, - port: { - description: 'Database port', - format: 'int32', - maximum: 65536, - minimum: 0, - type: 'integer', + }, + { + available_drivers: ['rest'], + engine: 'presto', + name: 'Presto', + preferred: true, + engine_information: { + supports_file_upload: true, + disable_ssh_tunneling: false, }, - query: { - additionalProperties: {}, - description: 'Additional parameters', + }, + { + available_drivers: ['mysqldb'], + default_driver: 'mysqldb', + engine: 'mysql', + name: 'MySQL', + parameters: { + properties: { + database: { + description: 'Database name', + type: 'string', + }, + encryption: { + description: 'Use an encrypted connection to the database', + type: 'boolean', + }, + host: { + description: 'Hostname or IP address', + type: 'string', + }, + password: { + description: 'Password', + nullable: true, + type: 'string', + }, + port: { + description: 'Database port', + format: 'int32', + maximum: 65536, + minimum: 0, + type: 'integer', + }, + query: { + additionalProperties: {}, + description: 'Additional parameters', + type: 'object', + }, + username: { + description: 'Username', + nullable: true, + type: 'string', + }, + }, + required: ['database', 'host', 'port', 'username'], type: 'object', }, - username: { - description: 'Username', - nullable: true, - type: 'string', + preferred: true, + sqlalchemy_uri_placeholder: + 'mysql://user:password@host:port/dbname[?key=value&key=value...]', + engine_information: { + supports_file_upload: true, + disable_ssh_tunneling: false, }, }, - required: ['database', 'host', 'port', 'username'], - type: 'object', - }, - preferred: true, - sqlalchemy_uri_placeholder: - 'mysql://user:password@host:port/dbname[?key=value&key=value...]', - engine_information: { - supports_file_upload: true, - disable_ssh_tunneling: false, - }, - }, - { - available_drivers: ['pysqlite'], - engine: 'sqlite', - name: 'SQLite', - preferred: true, - engine_information: { - supports_file_upload: true, - disable_ssh_tunneling: false, - }, - }, - { - available_drivers: ['rest'], - engine: 'druid', - name: 'Apache Druid', - preferred: false, - engine_information: { - supports_file_upload: true, - disable_ssh_tunneling: false, - }, - }, - { - available_drivers: ['bigquery'], - default_driver: 'bigquery', - engine: 'bigquery', - name: 'Google BigQuery', - parameters: { - properties: { - credentials_info: { - description: 'Contents of BigQuery JSON credentials.', - type: 'string', - 'x-encrypted-extra': true, - }, - query: { - type: 'object', + { + available_drivers: ['pysqlite'], + engine: 'sqlite', + name: 'SQLite', + preferred: true, + engine_information: { + supports_file_upload: true, + disable_ssh_tunneling: false, }, }, - type: 'object', - }, - preferred: false, - sqlalchemy_uri_placeholder: 'bigquery://{project_id}', - engine_information: { - supports_file_upload: true, - disable_ssh_tunneling: true, - }, - }, - { - available_drivers: ['rest'], - default_driver: 'apsw', - engine: 'gsheets', - name: 'Google Sheets', - preferred: false, - engine_information: { - supports_file_upload: false, - disable_ssh_tunneling: true, - }, - }, - { - available_drivers: ['connector'], - default_driver: 'connector', - engine: 'databricks', - name: 'Databricks', - parameters: { - properties: { - access_token: { - type: 'string', + { + available_drivers: ['rest'], + engine: 'druid', + name: 'Apache Druid', + preferred: false, + engine_information: { + supports_file_upload: true, + disable_ssh_tunneling: false, }, - database: { - type: 'string', + }, + { + available_drivers: ['bigquery'], + default_driver: 'bigquery', + engine: 'bigquery', + name: 'Google BigQuery', + parameters: { + properties: { + credentials_info: { + description: 'Contents of BigQuery JSON credentials.', + type: 'string', + 'x-encrypted-extra': true, + }, + query: { + type: 'object', + }, + }, + type: 'object', }, - host: { - type: 'string', + preferred: false, + sqlalchemy_uri_placeholder: 'bigquery://{project_id}', + engine_information: { + supports_file_upload: true, + disable_ssh_tunneling: true, }, - http_path: { - type: 'string', + }, + { + available_drivers: ['rest'], + default_driver: 'apsw', + engine: 'gsheets', + name: 'Google Sheets', + preferred: false, + engine_information: { + supports_file_upload: false, + disable_ssh_tunneling: true, }, - port: { - format: 'int32', - type: 'integer', + }, + { + available_drivers: ['connector'], + default_driver: 'connector', + engine: 'databricks', + name: 'Databricks', + parameters: { + properties: { + access_token: { + type: 'string', + }, + database: { + type: 'string', + }, + host: { + type: 'string', + }, + http_path: { + type: 'string', + }, + port: { + format: 'int32', + type: 'integer', + }, + }, + required: ['access_token', 'database', 'host', 'http_path', 'port'], + type: 'object', }, + preferred: true, + sqlalchemy_uri_placeholder: + 'databricks+connector://token:{access_token}@{host}:{port}/{database_name}', }, - required: ['access_token', 'database', 'host', 'http_path', 'port'], - type: 'object', - }, - preferred: true, - sqlalchemy_uri_placeholder: - 'databricks+connector://token:{access_token}@{host}:{port}/{database_name}', - }, - ], -}); -fetchMock.post(VALIDATE_PARAMS_ENDPOINT, { - message: 'OK', -}); - -const databaseFixture: DatabaseObject = { - id: 123, - backend: 'postgres', - configuration_method: ConfigurationMethod.DynamicForm, - database_name: 'Postgres', - name: 'PostgresDB', - is_managed_externally: false, - driver: 'psycopg2', -}; - -describe('DatabaseModal', () => { - const renderAndWait = async () => - waitFor(() => - render(<DatabaseModal {...dbProps} />, { - useRedux: true, - }), - ); + ], + }); + fetchMock.post(VALIDATE_PARAMS_ENDPOINT, { + message: 'OK', + }); + }); - beforeEach(async () => { - await renderAndWait(); + beforeEach(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + fetchMock.restore(); }); - afterEach(cleanup); + const setup = (propsOverwrite: Partial<DatabaseModalProps> = {}) => + render(<DatabaseModal {...dbProps} {...propsOverwrite} />, { + useRedux: true, + }); describe('Visual: New database connection', () => { - test('renders the initial load of Step 1 correctly', () => { + test('renders the initial load of Step 1 correctly', async () => { + setup(); + // ---------- Components ---------- // <TabHeader> - AntD header - const closeButton = screen.getByLabelText('Close'); + const closeButton = await screen.findByLabelText('Close'); const step1Header = screen.getByRole('heading', { name: /connect a database/i, }); // <ModalHeader> - Connection header - const step1Helper = screen.getByText(/step 1 of 3/i); + const step1Helper = await screen.findByText(/step 1 of 3/i); const selectDbHeader = screen.getByRole('heading', { name: /select a database to connect/i, }); @@ -376,7 +372,8 @@ describe('DatabaseModal', () => { hidden: true, }); - const footer = document.getElementsByClassName('ant-modal-footer'); + const modal = screen.getByRole('dialog'); + const footer = modal.querySelector('.ant-modal-footer'); // ---------- TODO (lyndsiWilliams): Selector options, can't seem to get these to render properly. // renderAvailableSelector() => <Alert> - Supported databases alert @@ -415,13 +412,15 @@ describe('DatabaseModal', () => { expect(component).toBeInTheDocument(); }); // there should be a footer but it should not have any buttons in it - expect(footer[0]).toBeEmptyDOMElement(); + expect(footer).toBeEmptyDOMElement(); }); test('renders the "Basic" tab of SQL Alchemy form (step 2 of 2) correctly', async () => { + setup(); + // On step 1, click dbButton to access SQL Alchemy form userEvent.click( - screen.getByRole('button', { + await screen.findByRole('button', { name: /sqlite/i, }), ); @@ -429,7 +428,7 @@ describe('DatabaseModal', () => { // ---------- Components ---------- // <TabHeader> - AntD header - const closeButton = screen.getByRole('button', { name: 'Close' }); + const closeButton = await screen.findByRole('button', { name: 'Close' }); const basicHeader = screen.getByRole('heading', { name: /connect a database/i, @@ -462,7 +461,7 @@ describe('DatabaseModal', () => { // <SSHTunnelForm> - Basic tab's SSH Tunnel Form const SSHTunnelingToggle = screen.getByTestId('ssh-tunnel-switch'); userEvent.click(SSHTunnelingToggle); - const SSHTunnelServerAddressInput = screen.getByTestId( + const SSHTunnelServerAddressInput = await screen.findByTestId( 'ssh-tunnel-server_address-input', ); const SSHTunnelServerPortInput = screen.getByTestId( @@ -527,9 +526,11 @@ describe('DatabaseModal', () => { }); test('renders the unexpanded "Advanced" tab correctly', async () => { + setup(); + // On step 1, click dbButton to access step 2 userEvent.click( - screen.getByRole('button', { + await screen.findByRole('button', { name: /sqlite/i, }), ); @@ -626,18 +627,20 @@ describe('DatabaseModal', () => { }); test('renders the "Advanced" - SQL LAB tab correctly (unexpanded)', async () => { + setup(); + // ---------- Components ---------- // On step 1, click dbButton to access step 2 userEvent.click( - screen.getByRole('button', { + await screen.findByRole('button', { name: /sqlite/i, }), ); // Click the "Advanced" tab - userEvent.click(screen.getByRole('tab', { name: /advanced/i })); + userEvent.click(await screen.findByRole('tab', { name: /advanced/i })); // Click the "SQL Lab" tab userEvent.click( - screen.getByRole('tab', { + await screen.findByRole('tab', { name: /right sql lab adjust how this database will interact with sql lab\./i, }), ); @@ -645,7 +648,7 @@ describe('DatabaseModal', () => { // ----- BEGIN STEP 2 (ADVANCED - SQL LAB) // <TabHeader> - AntD header - const closeButton = screen.getByRole('button', { name: /close/i }); + const closeButton = await screen.findByRole('button', { name: /close/i }); const advancedHeader = screen.getByRole('heading', { name: /connect a database/i, }); @@ -788,10 +791,12 @@ describe('DatabaseModal', () => { }); test('renders the "Advanced" - PERFORMANCE tab correctly', async () => { + setup(); + // ---------- Components ---------- // On step 1, click dbButton to access step 2 userEvent.click( - screen.getByRole('button', { + await screen.findByRole('button', { name: /sqlite/i, }), ); @@ -851,10 +856,12 @@ describe('DatabaseModal', () => { }); test('renders the "Advanced" - SECURITY tab correctly', async () => { + setup(); + // ---------- Components ---------- // On step 1, click dbButton to access step 2 userEvent.click( - screen.getByRole('button', { + await screen.findByRole('button', { name: /sqlite/i, }), ); @@ -935,10 +942,12 @@ describe('DatabaseModal', () => { }); it('renders the "Advanced" - SECURITY tab correctly after selecting Allow file uploads', async () => { + setup(); + // ---------- Components ---------- // On step 1, click dbButton to access step 2 userEvent.click( - screen.getByRole('button', { + await screen.findByRole('button', { name: /sqlite/i, }), ); @@ -1021,10 +1030,12 @@ describe('DatabaseModal', () => { }); test('renders the "Advanced" - OTHER tab correctly', async () => { + setup(); + // ---------- Components ---------- // On step 1, click dbButton to access step 2 userEvent.click( - screen.getByRole('button', { + await screen.findByRole('button', { name: /sqlite/i, }), ); @@ -1040,7 +1051,7 @@ describe('DatabaseModal', () => { // ----- BEGIN STEP 2 (ADVANCED - OTHER) // <TabHeader> - AntD header - const closeButton = screen.getByRole('button', { name: /close/i }); + const closeButton = await screen.findByRole('button', { name: /close/i }); const advancedHeader = screen.getByRole('heading', { name: /connect a database/i, }); @@ -1092,10 +1103,12 @@ describe('DatabaseModal', () => { }); test('Dynamic form', async () => { + setup(); + // ---------- Components ---------- // On step 1, click dbButton to access step 2 userEvent.click( - screen.getByRole('button', { + await screen.findByRole('button', { name: /postgresql/i, }), ); @@ -1107,9 +1120,11 @@ describe('DatabaseModal', () => { describe('Functional: Create new database', () => { test('directs databases to the appropriate form (dynamic vs. SQL Alchemy)', async () => { + setup(); + // ---------- Dynamic example (3-step form) // Click the PostgreSQL button to enter the dynamic form - const postgreSQLButton = screen.getByRole('button', { + const postgreSQLButton = await screen.findByRole('button', { name: /postgresql/i, }); userEvent.click(postgreSQLButton); @@ -1139,8 +1154,10 @@ describe('DatabaseModal', () => { describe('SQL Alchemy form flow', () => { test('enters step 2 of 2 when proper database is selected', async () => { + setup(); + userEvent.click( - screen.getByRole('button', { + await screen.findByRole('button', { name: /sqlite/i, }), ); @@ -1167,8 +1184,10 @@ describe('DatabaseModal', () => { describe('step 2 component interaction', () => { test('properly interacts with textboxes', async () => { + setup(); + userEvent.click( - screen.getByRole('button', { + await screen.findByRole('button', { name: /sqlite/i, }), ); @@ -1211,15 +1230,17 @@ describe('DatabaseModal', () => { describe('SSH Tunnel Form interaction', () => { test('properly interacts with SSH Tunnel form textboxes for dynamic form', async () => { + setup(); + userEvent.click( - screen.getByRole('button', { + await screen.findByRole('button', { name: /postgresql/i, }), ); expect(await screen.findByText(/step 2 of 3/i)).toBeInTheDocument(); const SSHTunnelingToggle = screen.getByTestId('ssh-tunnel-switch'); userEvent.click(SSHTunnelingToggle); - const SSHTunnelServerAddressInput = screen.getByTestId( + const SSHTunnelServerAddressInput = await screen.findByTestId( 'ssh-tunnel-server_address-input', ); expect(SSHTunnelServerAddressInput).toHaveValue(''); @@ -1246,8 +1267,10 @@ describe('DatabaseModal', () => { }); test('properly interacts with SSH Tunnel form textboxes', async () => { + setup(); + userEvent.click( - screen.getByRole('button', { + await screen.findByRole('button', { name: /sqlite/i, }), ); @@ -1255,7 +1278,7 @@ describe('DatabaseModal', () => { expect(await screen.findByText(/step 2 of 2/i)).toBeInTheDocument(); const SSHTunnelingToggle = screen.getByTestId('ssh-tunnel-switch'); userEvent.click(SSHTunnelingToggle); - const SSHTunnelServerAddressInput = screen.getByTestId( + const SSHTunnelServerAddressInput = await screen.findByTestId( 'ssh-tunnel-server_address-input', ); expect(SSHTunnelServerAddressInput).toHaveValue(''); @@ -1282,8 +1305,10 @@ describe('DatabaseModal', () => { }); test('if the SSH Tunneling toggle is not true, no inputs are displayed', async () => { + setup(); + userEvent.click( - screen.getByRole('button', { + await screen.findByRole('button', { name: /sqlite/i, }), ); @@ -1310,8 +1335,10 @@ describe('DatabaseModal', () => { }); test('If user changes the login method, the inputs change', async () => { + setup(); + userEvent.click( - screen.getByRole('button', { + await screen.findByRole('button', { name: /sqlite/i, }), ); @@ -1319,7 +1346,7 @@ describe('DatabaseModal', () => { expect(await screen.findByText(/step 2 of 2/i)).toBeInTheDocument(); const SSHTunnelingToggle = screen.getByTestId('ssh-tunnel-switch'); userEvent.click(SSHTunnelingToggle); - const SSHTunnelUsePasswordInput = screen.getByTestId( + const SSHTunnelUsePasswordInput = await screen.findByTestId( 'ssh-tunnel-use_password-radio', ); expect(SSHTunnelUsePasswordInput).toBeInTheDocument(); @@ -1348,6 +1375,8 @@ describe('DatabaseModal', () => { describe('Dynamic form flow', () => { test('enters step 2 of 3 when proper database is selected', async () => { + setup(); + expect(await screen.findByText(/step 1 of 3/i)).toBeInTheDocument(); userEvent.click( screen.getByRole('button', { @@ -1355,14 +1384,13 @@ describe('DatabaseModal', () => { }), ); expect(await screen.findByText(/step 2 of 3/i)).toBeInTheDocument(); - - const step2of3text = screen.getByText(/step 2 of 3/i); - expect(step2of3text).toBeVisible(); }); test('enters form credentials and runs fetchResource when "Connect" is clicked', async () => { + setup(); + userEvent.click( - screen.getByRole('button', { + await screen.findByRole('button', { name: /postgresql/i, }), ); @@ -1403,9 +1431,11 @@ describe('DatabaseModal', () => { describe('Import database flow', () => { test('imports a file', async () => { - const importDbButton = screen.getByTestId( + setup(); + + const importDbButton = (await screen.findByTestId( 'import-database-btn', - ) as HTMLInputElement; + )) as HTMLInputElement; importDbButton.type = 'file'; importDbButton.files = {} as FileList; expect(importDbButton).toBeInTheDocument(); @@ -1423,54 +1453,25 @@ describe('DatabaseModal', () => { }); describe('DatabaseModal w/ Deeplinking Engine', () => { - const renderAndWait = async () => { - const mounted = act(async () => { - render(<DatabaseModal {...dbProps} dbEngine="PostgreSQL" />, { - useRedux: true, - }); - }); - - return mounted; - }; - - beforeEach(async () => { - await renderAndWait(); - }); - - test('enters step 2 of 3 when proper database is selected', () => { - const step2of3text = screen.getByText(/step 2 of 3/i); + test('enters step 2 of 3 when proper database is selected', async () => { + setup({ dbEngine: 'PostgreSQL' }); + const step2of3text = await screen.findByText(/step 2 of 3/i); expect(step2of3text).toBeInTheDocument(); }); }); describe('DatabaseModal w/ GSheet Engine', () => { - const renderAndWait = async () => { - const dbProps = { - show: true, - database_name: 'my database', - sqlalchemy_uri: 'gsheets://', - }; - const mounted = act(async () => { - render(<DatabaseModal {...dbProps} dbEngine="Google Sheets" />, { - useRedux: true, - }); - }); - - return mounted; - }; - - beforeEach(async () => { - await renderAndWait(); - }); - - it('enters step 2 of 2 when proper database is selected', () => { - const step2of2text = screen.getByText(/step 2 of 2/i); + it('enters step 2 of 2 when proper database is selected', async () => { + setup({ dbEngine: 'Google Sheets' }); + const step2of2text = await screen.findByText(/step 2 of 2/i); expect(step2of2text).toBeInTheDocument(); }); it('renders the "Advanced" - SECURITY tab without Allow File Upload Checkbox', async () => { + setup({ dbEngine: 'Google Sheets' }); + // Click the "Advanced" tab - userEvent.click(screen.getByRole('tab', { name: /advanced/i })); + userEvent.click(await screen.findByRole('tab', { name: /advanced/i })); // Click the "Security" tab userEvent.click( screen.getByRole('tab', { @@ -1509,6 +1510,8 @@ describe('DatabaseModal', () => { }); it('if the SSH Tunneling toggle is not displayed, nothing should get displayed', async () => { + setup({ dbEngine: 'Google Sheets' }); + const SSHTunnelingToggle = screen.queryByTestId('ssh-tunnel-switch'); expect(SSHTunnelingToggle).not.toBeInTheDocument(); const SSHTunnelServerAddressInput = screen.queryByTestId( @@ -1536,22 +1539,9 @@ describe('DatabaseModal', () => { useSingleViewResource: jest.fn(), })); - const renderAndWait = async () => { - const mounted = act(async () => { - render(<DatabaseModal {...dbProps} dbEngine="PostgreSQL" />, { - useRedux: true, - }); - }); - - return mounted; - }; - - beforeEach(async () => { - await renderAndWait(); - }); - test('Error displays when it is an object', async () => { - const step2of3text = screen.getByText(/step 2 of 3/i); + setup({ dbEngine: 'PostgreSQL' }); + const step2of3text = await screen.findByText(/step 2 of 3/i); const errorSection = screen.getByText(/Database Creation Error/i); expect(step2of3text).toBeInTheDocument(); expect(errorSection).toBeInTheDocument(); @@ -1581,22 +1571,10 @@ describe('DatabaseModal', () => { setResource: jest.fn(), }); - const renderAndWait = async () => { - const mounted = act(async () => { - render(<DatabaseModal {...dbProps} dbEngine="PostgreSQL" />, { - useRedux: true, - }); - }); - - return mounted; - }; - - beforeEach(async () => { - await renderAndWait(); - }); - test('Error displays when it is a string', async () => { - const step2of3text = screen.getByText(/step 2 of 3/i); + setup({ dbEngine: 'PostgreSQL' }); + + const step2of3text = await screen.findByText(/step 2 of 3/i); const errorTitleMessage = screen.getByText(/Database Creation Error/i); const button = screen.getByText('See more'); userEvent.click(button); @@ -1608,7 +1586,7 @@ describe('DatabaseModal', () => { }); describe('DatabaseModal w Extensions', () => { - const renderAndWait = async () => { + beforeAll(() => { const extensionsRegistry = getExtensionsRegistry(); extensionsRegistry.set('ssh_tunnel.form.switch', () => ( @@ -1616,23 +1594,12 @@ describe('DatabaseModal', () => { )); setupExtensions(); - - const mounted = act(async () => { - render(<DatabaseModal {...dbProps} dbEngine="SQLite" />, { - useRedux: true, - }); - }); - - return mounted; - }; - - beforeEach(async () => { - await renderAndWait(); }); - test('should render an extension component if one is supplied', () => { + test('should render an extension component if one is supplied', async () => { + setup({ dbEngine: 'SQLite' }); expect( - screen.getByText('ssh_tunnel.form.switch extension component'), + await screen.findByText('ssh_tunnel.form.switch extension component'), ).toBeInTheDocument(); }); });
