| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import fetchMock from 'fetch-mock'; |
| import { |
| act, |
| defaultStore as store, |
| render, |
| screen, |
| userEvent, |
| waitFor, |
| } from 'spec/helpers/testing-library'; |
| import { api } from 'src/hooks/apiResources/queryApi'; |
| import { EmptyState } from '@superset-ui/core/components'; |
| import { DatabaseSelector } from '.'; |
| import type { DatabaseSelectorProps } from './types'; |
| |
| const createProps = (): DatabaseSelectorProps => ({ |
| db: { |
| id: 1, |
| database_name: 'test', |
| backend: 'test-postgresql', |
| }, |
| formMode: false, |
| isDatabaseSelectEnabled: true, |
| readOnly: false, |
| catalog: null, |
| schema: 'public', |
| sqlLabMode: true, |
| getDbList: jest.fn(), |
| handleError: jest.fn(), |
| onDbChange: jest.fn(), |
| onSchemaChange: jest.fn(), |
| }); |
| |
| const fakeDatabaseApiResult = { |
| count: 2, |
| description_columns: {}, |
| ids: [1, 2], |
| label_columns: { |
| allow_file_upload: 'Allow Csv Upload', |
| allow_ctas: 'Allow Ctas', |
| allow_cvas: 'Allow Cvas', |
| allow_dml: 'Allow DDL and DML', |
| allow_run_async: 'Allow Run Async', |
| allows_cost_estimate: 'Allows Cost Estimate', |
| allows_subquery: 'Allows Subquery', |
| allows_virtual_table_explore: 'Allows Virtual Table Explore', |
| disable_data_preview: 'Disables SQL Lab Data Preview', |
| disable_drill_to_detail: 'Disable Drill To Detail', |
| backend: 'Backend', |
| changed_on: 'Changed On', |
| changed_on_delta_humanized: 'Changed On Delta Humanized', |
| 'created_by.first_name': 'Created By First Name', |
| 'created_by.last_name': 'Created By Last Name', |
| database_name: 'Database Name', |
| explore_database_id: 'Explore Database Id', |
| expose_in_sqllab: 'Expose In Sqllab', |
| force_ctas_schema: 'Force Ctas Schema', |
| id: 'Id', |
| }, |
| list_columns: [ |
| 'allow_file_upload', |
| 'allow_ctas', |
| 'allow_cvas', |
| 'allow_dml', |
| 'allow_run_async', |
| 'allows_cost_estimate', |
| 'allows_subquery', |
| 'allows_virtual_table_explore', |
| 'disable_data_preview', |
| 'disable_drill_to_detail', |
| 'backend', |
| 'changed_on', |
| 'changed_on_delta_humanized', |
| 'created_by.first_name', |
| 'created_by.last_name', |
| 'database_name', |
| 'explore_database_id', |
| 'expose_in_sqllab', |
| 'force_ctas_schema', |
| 'id', |
| ], |
| list_title: 'List Database', |
| order_columns: [ |
| 'allow_file_upload', |
| 'allow_dml', |
| 'allow_run_async', |
| 'changed_on', |
| 'changed_on_delta_humanized', |
| 'created_by.first_name', |
| 'database_name', |
| 'expose_in_sqllab', |
| ], |
| result: [ |
| { |
| allow_file_upload: false, |
| allow_ctas: false, |
| allow_cvas: false, |
| allow_dml: false, |
| allow_run_async: false, |
| allows_cost_estimate: null, |
| allows_subquery: true, |
| allows_virtual_table_explore: true, |
| disable_data_preview: false, |
| disable_drill_to_detail: false, |
| backend: 'postgresql', |
| changed_on: '2021-03-09T19:02:07.141095', |
| changed_on_delta_humanized: 'a day ago', |
| created_by: null, |
| database_name: 'test-postgres', |
| explore_database_id: 1, |
| expose_in_sqllab: true, |
| force_ctas_schema: null, |
| id: 1, |
| }, |
| { |
| allow_csv_upload: false, |
| allow_ctas: false, |
| allow_cvas: false, |
| allow_dml: false, |
| allow_run_async: false, |
| allows_cost_estimate: null, |
| allows_subquery: true, |
| allows_virtual_table_explore: true, |
| disable_data_preview: false, |
| disable_drill_to_detail: false, |
| backend: 'mysql', |
| changed_on: '2021-03-09T19:02:07.141095', |
| changed_on_delta_humanized: 'a day ago', |
| created_by: null, |
| database_name: 'test-mysql', |
| explore_database_id: 1, |
| expose_in_sqllab: true, |
| force_ctas_schema: null, |
| id: 2, |
| }, |
| ], |
| }; |
| |
| const fakeDatabaseApiResultInReverseOrder = { |
| ...fakeDatabaseApiResult, |
| ids: [2, 1], |
| result: [...fakeDatabaseApiResult.result].reverse(), |
| }; |
| |
| const fakeSchemaApiResult = { |
| count: 2, |
| result: ['information_schema', 'public'], |
| }; |
| |
| const fakeCatalogApiResult = { |
| count: 0, |
| result: [], |
| }; |
| |
| const fakeFunctionNamesApiResult = { |
| function_names: [], |
| }; |
| |
| const databaseApiRoute = |
| 'glob:*/api/v1/database/?*order_column:database_name,order_direction*'; |
| const catalogApiRoute = 'glob:*/api/v1/database/*/catalogs/?*'; |
| const schemaApiRoute = 'glob:*/api/v1/database/*/schemas/?*'; |
| const tablesApiRoute = 'glob:*/api/v1/database/*/tables/*'; |
| |
| function setupFetchMock() { |
| fetchMock.get(databaseApiRoute, fakeDatabaseApiResult); |
| fetchMock.get(catalogApiRoute, fakeCatalogApiResult); |
| fetchMock.get(schemaApiRoute, fakeSchemaApiResult); |
| fetchMock.get(tablesApiRoute, fakeFunctionNamesApiResult); |
| } |
| |
| beforeEach(() => { |
| setupFetchMock(); |
| }); |
| |
| afterEach(() => { |
| fetchMock.reset(); |
| act(() => { |
| store.dispatch(api.util.resetApiState()); |
| }); |
| }); |
| |
| test('Should render', async () => { |
| const props = createProps(); |
| render(<DatabaseSelector {...props} />, { useRedux: true, store }); |
| expect(await screen.findByTestId('DatabaseSelector')).toBeInTheDocument(); |
| }); |
| |
| test('Refresh should work', async () => { |
| const props = createProps(); |
| |
| 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: public', |
| }); |
| |
| await userEvent.click(select); |
| |
| 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); |
| }); |
| |
| // click schema reload |
| await userEvent.click(screen.getByRole('button', { name: 'sync' })); |
| |
| 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); |
| }); |
| }); |
| |
| 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(); |
| await userEvent.click(select); |
| expect(await screen.findByText('test-mysql')).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(); |
| await userEvent.click(select); |
| const options = await screen.findAllByRole('option'); |
| |
| 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 }, |
| ); |
| |
| 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(); |
| await 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 () => { |
| fetchMock.reset(); |
| fetchMock.get(databaseApiRoute, { result: [] }); |
| fetchMock.get(schemaApiRoute, { result: [] }); |
| fetchMock.get(tablesApiRoute, { result: [] }); |
| const props = createProps(); |
| render( |
| <DatabaseSelector |
| {...props} |
| db={undefined} |
| emptyState={<EmptyState size="small" title="empty" />} |
| />, |
| { useRedux: true, store }, |
| ); |
| const select = screen.getByRole('combobox', { |
| name: 'Select database or type to search databases', |
| }); |
| await userEvent.click(select); |
| const emptystate = await screen.findByText('empty'); |
| expect(emptystate).toBeInTheDocument(); |
| expect(screen.queryByText('test-mysql')).not.toBeInTheDocument(); |
| }); |
| |
| test('Should schema select display options', async () => { |
| const props = createProps(); |
| render(<DatabaseSelector {...props} />, { useRedux: true, store }); |
| const select = screen.getByRole('combobox', { |
| name: 'Select schema or type to search schemas: public', |
| }); |
| expect(select).toBeInTheDocument(); |
| await userEvent.click(select); |
| await waitFor(() => { |
| expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); |
| }); |
| const publicOption = await screen.findByRole('option', { name: 'public' }); |
| expect(publicOption).toBeInTheDocument(); |
| |
| const infoSchemaOption = await screen.findByRole('option', { |
| name: 'information_schema', |
| }); |
| expect(infoSchemaOption).toBeInTheDocument(); |
| }); |
| |
| test('Sends the correct db when changing the database', 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(); |
| await userEvent.click(select); |
| await userEvent.click(await screen.findByText('test-mysql')); |
| await waitFor(() => |
| expect(props.onDbChange).toHaveBeenCalledWith( |
| expect.objectContaining({ |
| value: 2, |
| database_name: 'test-mysql', |
| backend: 'mysql', |
| }), |
| ), |
| ); |
| }); |
| |
| test('Sends the correct schema when changing the schema', async () => { |
| const props = createProps(); |
| const { rerender } = render(<DatabaseSelector {...props} db={null} />, { |
| useRedux: true, |
| store, |
| }); |
| await waitFor(() => expect(fetchMock.calls(databaseApiRoute).length).toBe(1)); |
| rerender(<DatabaseSelector {...props} />); |
| expect(props.onSchemaChange).toHaveBeenCalledTimes(0); |
| const select = screen.getByRole('combobox', { |
| name: 'Select schema or type to search schemas: public', |
| }); |
| expect(select).toBeInTheDocument(); |
| await userEvent.click(select); |
| const schemaOption = await screen.findByText('information_schema'); |
| await userEvent.click(schemaOption); |
| await waitFor(() => |
| expect(props.onSchemaChange).toHaveBeenCalledWith('information_schema'), |
| ); |
| expect(props.onSchemaChange).toHaveBeenCalledTimes(1); |
| }); |