| /** |
| * 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 configureMockStore from 'redux-mock-store'; |
| import thunk from 'redux-thunk'; |
| import type { ThunkDispatch } from 'redux-thunk'; |
| import type { AnyAction } from 'redux'; |
| import { waitFor } from 'spec/helpers/testing-library'; |
| import * as actions from 'src/SqlLab/actions/sqlLab'; |
| import type { QueryEditor, Table, SqlLabRootState } from 'src/SqlLab/types'; |
| import { LOG_EVENT } from 'src/logger/actions'; |
| import { |
| defaultQueryEditor, |
| query as queryFixture, |
| initialState, |
| queryId, |
| } from 'src/SqlLab/fixtures'; |
| import { SupersetClient, isFeatureEnabled } from '@superset-ui/core'; |
| import { ADD_TOAST } from 'src/components/MessageToasts/actions'; |
| import { EMPTY_STATE_QE_ID } from 'src/SqlLab/hooks/useQueryEditor'; |
| import { ToastType } from '../../components/MessageToasts/types'; |
| |
| const isFeatureEnabledMock = isFeatureEnabled as unknown as jest.Mock; |
| const query = { ...queryFixture, id: queryId } as any; |
| // Cast fixture to satisfy SqlLabRootState for getState callbacks in thunk tests |
| const typedInitialState = initialState as unknown as SqlLabRootState; |
| |
| type DispatchExts = ThunkDispatch<SqlLabRootState, undefined, AnyAction>; |
| |
| const middlewares = [thunk]; |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| const mockStore = configureMockStore<any, DispatchExts>(middlewares); |
| |
| jest.mock('nanoid', () => ({ |
| nanoid: () => 'abcd', |
| })); |
| |
| afterAll(() => { |
| jest.resetAllMocks(); |
| }); |
| |
| jest.mock('@superset-ui/core', () => ({ |
| ...jest.requireActual('@superset-ui/core'), |
| isFeatureEnabled: jest.fn(), |
| })); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('getUpToDateQuery', () => { |
| test('should return the up to date query editor state', () => { |
| const outOfUpdatedQueryEditor = { |
| ...defaultQueryEditor, |
| schema: null as unknown as string, |
| sql: 'SELECT ...', |
| }; |
| const queryEditor = { |
| ...defaultQueryEditor, |
| sql: 'SELECT * FROM table', |
| }; |
| const state = { |
| sqlLab: { |
| queryEditors: [queryEditor], |
| unsavedQueryEditor: {}, |
| }, |
| } as unknown as SqlLabRootState; |
| expect(actions.getUpToDateQuery(state, outOfUpdatedQueryEditor)).toEqual( |
| queryEditor, |
| ); |
| }); |
| }); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('async actions', () => { |
| const mockBigNumber = '9223372036854775807'; |
| const queryEditor = { |
| ...defaultQueryEditor, |
| id: 'abcd', |
| autorun: false, |
| latestQueryId: null, |
| sql: 'SELECT *\nFROM\nWHERE', |
| name: 'Untitled Query 1', |
| }; |
| |
| let dispatch: jest.Mock; |
| const fetchQueryEndpoint = 'glob:*/api/v1/sqllab/results/*'; |
| const runQueryEndpoint = 'glob:*/api/v1/sqllab/execute/'; |
| |
| beforeEach(() => { |
| dispatch = jest.fn(); |
| fetchMock.removeRoute(fetchQueryEndpoint); |
| fetchMock.get( |
| fetchQueryEndpoint, |
| JSON.stringify({ |
| data: mockBigNumber, |
| query: { sqlEditorId: 'dfsadfs' }, |
| }), |
| { name: fetchQueryEndpoint }, |
| ); |
| |
| fetchMock.removeRoute(runQueryEndpoint); |
| fetchMock.post(runQueryEndpoint, `{ "data": ${mockBigNumber} }`, { |
| name: runQueryEndpoint, |
| }); |
| }); |
| |
| afterEach(() => { |
| fetchMock.clearHistory(); |
| }); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('saveQuery', () => { |
| const saveQueryEndpoint = 'glob:*/api/v1/saved_query/'; |
| fetchMock.post(saveQueryEndpoint, { results: { json: {} } }); |
| |
| const makeRequest = () => { |
| const request = actions.saveQuery(query, queryId); |
| return request(dispatch, () => typedInitialState, undefined); |
| }; |
| |
| test('posts to the correct url', () => { |
| expect.assertions(1); |
| |
| const store = mockStore(initialState); |
| return store.dispatch(actions.saveQuery(query, queryId)).then(() => { |
| expect(fetchMock.callHistory.calls(saveQueryEndpoint)).toHaveLength(1); |
| }); |
| }); |
| |
| test('posts the correct query object', () => { |
| const store = mockStore(initialState); |
| return store.dispatch(actions.saveQuery(query, queryId)).then(() => { |
| const call = fetchMock.callHistory.calls(saveQueryEndpoint)[0]; |
| const formData = JSON.parse(call.options.body as string); |
| const mappedQueryToServer = actions.convertQueryToServer(query); |
| |
| // The 'id' field is excluded from the POST payload since it's for new queries |
| expect(formData.id).toBeUndefined(); |
| Object.keys(mappedQueryToServer).forEach(key => { |
| if (key !== 'id') { |
| expect(formData[key]).toBeDefined(); |
| } |
| }); |
| }); |
| }); |
| |
| test('calls 3 dispatch actions', () => { |
| expect.assertions(1); |
| |
| return makeRequest().then(() => { |
| expect(dispatch.mock.calls.length).toBe(2); |
| }); |
| }); |
| |
| test('calls QUERY_EDITOR_SAVED after making a request', () => { |
| expect.assertions(1); |
| |
| return makeRequest().then(() => { |
| expect(dispatch.mock.calls[0][0].type).toBe(actions.QUERY_EDITOR_SAVED); |
| }); |
| }); |
| |
| test('onSave calls QUERY_EDITOR_SAVED and QUERY_EDITOR_SET_TITLE', () => { |
| expect.assertions(1); |
| |
| const store = mockStore(initialState); |
| const expectedActionTypes = [ |
| actions.QUERY_EDITOR_SAVED, |
| actions.QUERY_EDITOR_SET_TITLE, |
| ]; |
| return store.dispatch(actions.saveQuery(query, queryId)).then(() => { |
| expect(store.getActions().map(a => a.type)).toEqual( |
| expectedActionTypes, |
| ); |
| }); |
| }); |
| }); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('formatQuery', () => { |
| const formatQueryEndpoint = 'glob:*/api/v1/sqllab/format_sql/'; |
| const expectedSql = 'SELECT 1'; |
| |
| beforeEach(() => { |
| fetchMock.removeRoute(formatQueryEndpoint); |
| fetchMock.post( |
| formatQueryEndpoint, |
| { result: expectedSql }, |
| { |
| name: formatQueryEndpoint, |
| }, |
| ); |
| }); |
| |
| test('posts to the correct url', async () => { |
| const store = mockStore(initialState); |
| store.dispatch(actions.formatQuery(query as unknown as QueryEditor)); |
| await waitFor(() => |
| expect(fetchMock.callHistory.calls(formatQueryEndpoint)).toHaveLength( |
| 1, |
| ), |
| ); |
| expect(store.getActions()[0].type).toBe(actions.QUERY_EDITOR_SET_SQL); |
| expect(store.getActions()[0].sql).toBe(expectedSql); |
| }); |
| |
| test('sends only sql in request body when no dbId or templateParams', async () => { |
| const queryEditorWithoutExtras = { |
| ...defaultQueryEditor, |
| sql: 'SELECT * FROM table', |
| dbId: null, |
| templateParams: null, |
| }; |
| const state = { |
| sqlLab: { |
| queryEditors: [queryEditorWithoutExtras], |
| unsavedQueryEditor: {}, |
| }, |
| }; |
| const store = mockStore(state); |
| |
| store.dispatch( |
| actions.formatQuery(queryEditorWithoutExtras as unknown as QueryEditor), |
| ); |
| |
| await waitFor(() => |
| expect(fetchMock.callHistory.calls(formatQueryEndpoint)).toHaveLength( |
| 1, |
| ), |
| ); |
| |
| const call = fetchMock.callHistory.calls(formatQueryEndpoint)[0]; |
| const body = JSON.parse(call.options.body as string); |
| |
| expect(body).toEqual({ sql: 'SELECT * FROM table' }); |
| expect(body.database_id).toBeUndefined(); |
| expect(body.template_params).toBeUndefined(); |
| }); |
| |
| test('includes database_id in request when dbId is provided', async () => { |
| const queryEditorWithDb = { |
| ...defaultQueryEditor, |
| sql: 'SELECT * FROM table', |
| dbId: 5, |
| templateParams: null, |
| }; |
| const state = { |
| sqlLab: { |
| queryEditors: [queryEditorWithDb], |
| unsavedQueryEditor: {}, |
| }, |
| }; |
| const store = mockStore(state); |
| |
| store.dispatch( |
| actions.formatQuery(queryEditorWithDb as unknown as QueryEditor), |
| ); |
| |
| await waitFor(() => |
| expect(fetchMock.callHistory.calls(formatQueryEndpoint)).toHaveLength( |
| 1, |
| ), |
| ); |
| |
| const call = fetchMock.callHistory.calls(formatQueryEndpoint)[0]; |
| const body = JSON.parse(call.options.body as string); |
| |
| expect(body).toEqual({ |
| sql: 'SELECT * FROM table', |
| database_id: 5, |
| }); |
| }); |
| |
| test('includes template_params as string when provided as string', async () => { |
| const queryEditorWithTemplateString = { |
| ...defaultQueryEditor, |
| sql: 'SELECT * FROM table WHERE id = {{ user_id }}', |
| dbId: 5, |
| templateParams: '{"user_id": 123}', |
| }; |
| const state = { |
| sqlLab: { |
| queryEditors: [queryEditorWithTemplateString], |
| unsavedQueryEditor: {}, |
| }, |
| }; |
| const store = mockStore(state); |
| |
| store.dispatch( |
| actions.formatQuery( |
| queryEditorWithTemplateString as unknown as QueryEditor, |
| ), |
| ); |
| |
| await waitFor(() => |
| expect(fetchMock.callHistory.calls(formatQueryEndpoint)).toHaveLength( |
| 1, |
| ), |
| ); |
| |
| const call = fetchMock.callHistory.calls(formatQueryEndpoint)[0]; |
| const body = JSON.parse(call.options.body as string); |
| |
| expect(body).toEqual({ |
| sql: 'SELECT * FROM table WHERE id = {{ user_id }}', |
| database_id: 5, |
| template_params: '{"user_id": 123}', |
| }); |
| }); |
| |
| test('stringifies template_params when provided as object', async () => { |
| const queryEditorWithTemplateObject = { |
| ...defaultQueryEditor, |
| sql: 'SELECT * FROM table WHERE id = {{ user_id }}', |
| dbId: 5, |
| templateParams: { user_id: 123, status: 'active' }, |
| }; |
| const state = { |
| sqlLab: { |
| queryEditors: [queryEditorWithTemplateObject], |
| unsavedQueryEditor: {}, |
| }, |
| }; |
| const store = mockStore(state); |
| |
| store.dispatch( |
| actions.formatQuery( |
| queryEditorWithTemplateObject as unknown as QueryEditor, |
| ), |
| ); |
| |
| await waitFor(() => |
| expect(fetchMock.callHistory.calls(formatQueryEndpoint)).toHaveLength( |
| 1, |
| ), |
| ); |
| |
| const call = fetchMock.callHistory.calls(formatQueryEndpoint)[0]; |
| const body = JSON.parse(call.options.body as string); |
| |
| expect(body).toEqual({ |
| sql: 'SELECT * FROM table WHERE id = {{ user_id }}', |
| database_id: 5, |
| template_params: '{"user_id":123,"status":"active"}', |
| }); |
| }); |
| |
| test('dispatches QUERY_EDITOR_SET_SQL with formatted result', async () => { |
| const formattedSql = 'SELECT\n *\nFROM\n table'; |
| fetchMock.removeRoute(formatQueryEndpoint); |
| fetchMock.route( |
| formatQueryEndpoint, |
| { result: formattedSql }, |
| { name: formatQueryEndpoint }, |
| ); |
| |
| const queryEditorToFormat = { |
| ...defaultQueryEditor, |
| sql: 'SELECT * FROM table', |
| }; |
| const state = { |
| sqlLab: { |
| queryEditors: [queryEditorToFormat], |
| unsavedQueryEditor: {}, |
| }, |
| }; |
| const store = mockStore(state); |
| |
| await store.dispatch(actions.formatQuery(queryEditorToFormat)); |
| |
| const dispatchedActions = store.getActions(); |
| expect(dispatchedActions).toHaveLength(1); |
| expect(dispatchedActions[0].type).toBe(actions.QUERY_EDITOR_SET_SQL); |
| expect(dispatchedActions[0].sql).toBe(formattedSql); |
| }); |
| |
| test('uses up-to-date query editor state from store', async () => { |
| const outdatedQueryEditor = { |
| ...defaultQueryEditor, |
| sql: 'OLD SQL', |
| dbId: 1, |
| }; |
| const upToDateQueryEditor = { |
| ...defaultQueryEditor, |
| sql: 'SELECT * FROM updated_table', |
| dbId: 10, |
| }; |
| const state = { |
| sqlLab: { |
| queryEditors: [upToDateQueryEditor], |
| unsavedQueryEditor: {}, |
| }, |
| }; |
| const store = mockStore(state); |
| |
| // Pass outdated query editor, but expect the function to use the up-to-date one from store |
| store.dispatch(actions.formatQuery(outdatedQueryEditor)); |
| |
| await waitFor(() => |
| expect(fetchMock.callHistory.calls(formatQueryEndpoint)).toHaveLength( |
| 1, |
| ), |
| ); |
| |
| const call = fetchMock.callHistory.calls(formatQueryEndpoint)[0]; |
| const body = JSON.parse(call.options.body as string); |
| |
| expect(body.sql).toBe('SELECT * FROM updated_table'); |
| expect(body.database_id).toBe(10); |
| }); |
| }); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('fetchQueryResults', () => { |
| const makeRequest = () => { |
| const store = mockStore(initialState); |
| const request = actions.fetchQueryResults(query); |
| return request(dispatch, store.getState, undefined); |
| }; |
| |
| test('makes the fetch request', () => { |
| expect.assertions(1); |
| |
| return makeRequest().then(() => { |
| expect(fetchMock.callHistory.calls(fetchQueryEndpoint)).toHaveLength(1); |
| }); |
| }); |
| |
| test('calls requestQueryResults', () => { |
| expect.assertions(1); |
| |
| return makeRequest().then(() => { |
| expect(dispatch.mock.calls[0][0].type).toBe( |
| actions.REQUEST_QUERY_RESULTS, |
| ); |
| }); |
| }); |
| |
| /* oxlint-disable-next-line jest/no-disabled-tests */ |
| test.skip('parses large number result without losing precision', () => |
| makeRequest().then(() => { |
| expect(fetchMock.callHistory.calls(fetchQueryEndpoint)).toHaveLength(1); |
| expect(dispatch.mock.calls.length).toBe(2); |
| expect( |
| dispatch.mock.calls[1][ |
| dispatch.mock.calls[1].length - 1 |
| ].results.data.toString(), |
| ).toBe(mockBigNumber); |
| })); |
| |
| test('calls querySuccess on fetch success', () => { |
| expect.assertions(1); |
| |
| const store = mockStore({}); |
| const expectedActionTypes = [ |
| actions.REQUEST_QUERY_RESULTS, |
| actions.QUERY_SUCCESS, |
| ]; |
| return store.dispatch(actions.fetchQueryResults(query)).then(() => { |
| expect(store.getActions().map(a => a.type)).toEqual( |
| expectedActionTypes, |
| ); |
| }); |
| }); |
| |
| test('calls queryFailed on fetch error', () => { |
| expect.assertions(1); |
| |
| fetchMock.removeRoute(fetchQueryEndpoint); |
| fetchMock.get( |
| fetchQueryEndpoint, |
| { throws: { message: 'error text' } }, |
| { name: fetchQueryEndpoint }, |
| ); |
| |
| const store = mockStore({}); |
| const expectedActionTypes = [ |
| actions.REQUEST_QUERY_RESULTS, |
| actions.QUERY_FAILED, |
| ]; |
| return store.dispatch(actions.fetchQueryResults(query)).then(() => { |
| expect(store.getActions().map(a => a.type)).toEqual( |
| expectedActionTypes, |
| ); |
| }); |
| }); |
| }); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('runQuery without query params', () => { |
| const makeRequest = () => { |
| const request = actions.runQuery(query); |
| return request(dispatch, () => typedInitialState, undefined); |
| }; |
| |
| test('makes the fetch request', () => { |
| expect.assertions(1); |
| |
| return makeRequest().then(() => { |
| expect(fetchMock.callHistory.calls(runQueryEndpoint)).toHaveLength(1); |
| }); |
| }); |
| |
| test('calls startQuery', () => { |
| expect.assertions(1); |
| |
| return makeRequest().then(() => { |
| expect(dispatch.mock.calls[0][0].type).toBe(actions.START_QUERY); |
| }); |
| }); |
| |
| test('parses large number result without losing precision', () => |
| makeRequest().then(() => { |
| expect(fetchMock.callHistory.calls(runQueryEndpoint)).toHaveLength(1); |
| expect(dispatch.mock.calls.length).toBe(2); |
| expect( |
| dispatch.mock.calls[1][ |
| dispatch.mock.calls[1].length - 1 |
| ].results.data.toString(), |
| ).toBe(mockBigNumber); |
| })); |
| |
| test('calls querySuccess on fetch success', () => { |
| expect.assertions(1); |
| |
| const store = mockStore({}); |
| const expectedActionTypes = [actions.START_QUERY, actions.QUERY_SUCCESS]; |
| const { dispatch } = store; |
| const request = actions.runQuery(query); |
| return request(dispatch, () => typedInitialState, undefined).then(() => { |
| expect(store.getActions().map(a => a.type)).toEqual( |
| expectedActionTypes, |
| ); |
| }); |
| }); |
| |
| test('calls queryFailed on fetch error and logs the error details', () => { |
| expect.assertions(2); |
| |
| fetchMock.removeRoute(runQueryEndpoint); |
| fetchMock.post( |
| runQueryEndpoint, |
| { |
| throws: { |
| message: 'error text', |
| timeout: true, |
| statusText: 'timeout', |
| }, |
| }, |
| { name: runQueryEndpoint }, |
| ); |
| |
| const store = mockStore({}); |
| const expectedActionTypes = [ |
| actions.START_QUERY, |
| LOG_EVENT, |
| actions.QUERY_FAILED, |
| ]; |
| const { dispatch } = store; |
| const request = actions.runQuery(query); |
| return request(dispatch, () => typedInitialState, undefined).then(() => { |
| const actions = store.getActions(); |
| expect(actions.map(a => a.type)).toEqual(expectedActionTypes); |
| expect(actions[1].payload.eventData.issue_codes).toEqual([1000, 1001]); |
| }); |
| }); |
| }); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('runQuery with query params', () => { |
| let locationSpy: jest.SpyInstance; |
| |
| beforeAll(() => { |
| const u = new URL('http://localhost/sqllab/?foo=bar'); |
| locationSpy = jest.spyOn(window, 'location', 'get').mockReturnValue({ |
| href: u.href, |
| pathname: u.pathname, |
| search: u.search, |
| hash: u.hash, |
| origin: u.origin, |
| host: u.host, |
| hostname: u.hostname, |
| port: u.port, |
| protocol: u.protocol, |
| } as Location); |
| }); |
| |
| afterAll(() => { |
| locationSpy.mockRestore(); |
| }); |
| |
| const makeRequest = () => { |
| const request = actions.runQuery(query); |
| return request(dispatch, () => typedInitialState, undefined); |
| }; |
| |
| test('makes the fetch request', async () => { |
| const runQueryEndpointWithParams = |
| 'glob:*/api/v1/sqllab/execute/?foo=bar'; |
| fetchMock.post( |
| runQueryEndpointWithParams, |
| `{ "data": ${mockBigNumber} }`, |
| ); |
| await makeRequest().then(() => { |
| expect( |
| fetchMock.callHistory.calls(runQueryEndpointWithParams), |
| ).toHaveLength(1); |
| }); |
| }); |
| }); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('reRunQuery', () => { |
| test('creates new query with a new id', () => { |
| const id = 'id'; |
| const state = { |
| sqlLab: { |
| tabHistory: [id], |
| queryEditors: [{ id, name: 'Dummy query editor' }], |
| unsavedQueryEditor: {}, |
| }, |
| }; |
| const store = mockStore(state); |
| const request = actions.reRunQuery(query); |
| request(store.dispatch, store.getState, undefined); |
| expect(store.getActions()[0].query.id).toEqual('abcd'); |
| }); |
| }); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('postStopQuery', () => { |
| const stopQueryEndpoint = 'glob:*/api/v1/query/stop'; |
| fetchMock.post(stopQueryEndpoint, {}); |
| const baseQuery = { |
| ...query, |
| id: 'test_foo', |
| }; |
| |
| const makeRequest = () => { |
| const request = actions.postStopQuery(baseQuery); |
| return request(dispatch, () => typedInitialState, undefined); |
| }; |
| |
| test('makes the fetch request', () => { |
| expect.assertions(1); |
| |
| return makeRequest().then(() => { |
| expect(fetchMock.callHistory.calls(stopQueryEndpoint)).toHaveLength(1); |
| }); |
| }); |
| |
| test('calls stopQuery', () => { |
| expect.assertions(1); |
| |
| return makeRequest().then(() => { |
| expect(dispatch.mock.calls[0][0].type).toBe(actions.STOP_QUERY); |
| }); |
| }); |
| |
| test('sends the correct data', () => { |
| expect.assertions(1); |
| |
| return makeRequest().then(() => { |
| const call = fetchMock.callHistory.calls(stopQueryEndpoint)[0]; |
| const body = JSON.parse(call.options.body as string); |
| expect(body.client_id).toBe(baseQuery.id); |
| }); |
| }); |
| }); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('cloneQueryToNewTab', () => { |
| test('creates new query editor', () => { |
| expect.assertions(1); |
| |
| const id = 'id'; |
| const state = { |
| sqlLab: { |
| tabHistory: [id], |
| queryEditors: [{ id, name: 'out of updated title' }], |
| unsavedQueryEditor: { |
| id, |
| name: 'Dummy query editor', |
| }, |
| }, |
| }; |
| const store = mockStore(state); |
| const expectedActions = [ |
| { |
| type: actions.ADD_QUERY_EDITOR, |
| queryEditor: { |
| name: 'Copy of Dummy query editor', |
| dbId: 1, |
| catalog: query.catalog, |
| schema: query.schema, |
| autorun: true, |
| sql: 'SELECT * FROM something', |
| queryLimit: undefined, |
| maxRow: undefined, |
| id: 'abcd', |
| immutableId: 'abcd', |
| templateParams: undefined, |
| inLocalStorage: true, |
| loaded: true, |
| }, |
| }, |
| ]; |
| const request = actions.cloneQueryToNewTab(query, true); |
| request(store.dispatch, store.getState, undefined); |
| |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| }); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('popSavedQuery', () => { |
| const supersetClientGetSpy = jest.spyOn(SupersetClient, 'get'); |
| const store = mockStore({}); |
| |
| const mockSavedQueryApiResponse = { |
| catalog: null, |
| changed_by: { |
| first_name: 'Superset', |
| id: 1, |
| last_name: 'Admin', |
| }, |
| changed_on: '2024-12-28T20:06:14.246743', |
| changed_on_delta_humanized: '8 days ago', |
| created_by: { |
| first_name: 'Superset', |
| id: 1, |
| last_name: 'Admin', |
| }, |
| database: { |
| database_name: 'examples', |
| id: 2, |
| }, |
| description: '', |
| id: 1, |
| label: 'Query 1', |
| schema: 'public', |
| sql: 'SELECT * FROM channels', |
| sql_tables: [ |
| { |
| catalog: null, |
| schema: null, |
| table: 'channels', |
| }, |
| ], |
| template_parameters: null, |
| }; |
| |
| const makeRequest = (id: string | number) => { |
| const request = actions.popSavedQuery(String(id)); |
| const { dispatch } = store; |
| |
| return request(dispatch, () => typedInitialState, undefined); |
| }; |
| |
| beforeEach(() => { |
| supersetClientGetSpy.mockClear(); |
| store.clearActions(); |
| }); |
| |
| afterAll(() => { |
| supersetClientGetSpy.mockRestore(); |
| }); |
| |
| test('calls API endpint with correct params', async () => { |
| supersetClientGetSpy.mockResolvedValue({ |
| json: { result: mockSavedQueryApiResponse }, |
| } as any); |
| |
| await makeRequest(123); |
| |
| expect(supersetClientGetSpy).toHaveBeenCalledWith({ |
| endpoint: '/api/v1/saved_query/123', |
| }); |
| }); |
| |
| test('dispatches addQueryEditor with correct params on successful API call', async () => { |
| supersetClientGetSpy.mockResolvedValue({ |
| json: { result: mockSavedQueryApiResponse }, |
| } as any); |
| |
| const expectedParams = { |
| name: 'Query 1', |
| dbId: 2, |
| catalog: null, |
| schema: 'public', |
| sql: 'SELECT * FROM channels', |
| templateParams: null, |
| remoteId: 1, |
| }; |
| |
| await makeRequest(1); |
| |
| const addQueryEditorAction = store |
| .getActions() |
| .find(action => action.type === actions.ADD_QUERY_EDITOR); |
| |
| expect(addQueryEditorAction).toBeTruthy(); |
| expect(addQueryEditorAction?.queryEditor).toEqual( |
| expect.objectContaining(expectedParams), |
| ); |
| }); |
| |
| test('should dispatch addDangerToast on API error', async () => { |
| supersetClientGetSpy.mockResolvedValue(new Error() as any); |
| |
| await makeRequest(1); |
| |
| const addToastAction = store |
| .getActions() |
| .find(action => action.type === ADD_TOAST); |
| |
| expect(addToastAction).toBeTruthy(); |
| expect(addToastAction?.payload?.toastType).toBe(ToastType.Danger); |
| }); |
| }); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('addQueryEditor', () => { |
| test('creates new query editor', () => { |
| expect.assertions(1); |
| |
| const store = mockStore(initialState); |
| const expectedActions = [ |
| { |
| type: actions.ADD_QUERY_EDITOR, |
| queryEditor: { |
| ...queryEditor, |
| immutableId: 'abcd', |
| inLocalStorage: true, |
| loaded: true, |
| }, |
| }, |
| ]; |
| store.dispatch(actions.addQueryEditor(defaultQueryEditor)); |
| |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('addNewQueryEditor', () => { |
| test('creates new query editor with new tab name', () => { |
| const store = mockStore({ |
| ...initialState, |
| sqlLab: { |
| ...initialState.sqlLab, |
| unsavedQueryEditor: { |
| id: defaultQueryEditor.id, |
| name: 'Untitled Query 6', |
| }, |
| }, |
| }); |
| const expectedActions = [ |
| { |
| type: actions.ADD_QUERY_EDITOR, |
| queryEditor: { |
| id: 'abcd', |
| immutableId: 'abcd', |
| sql: expect.stringContaining('SELECT ...'), |
| name: `Untitled Query 7`, |
| dbId: defaultQueryEditor.dbId, |
| catalog: defaultQueryEditor.catalog, |
| schema: defaultQueryEditor.schema, |
| autorun: false, |
| queryLimit: |
| (defaultQueryEditor as any).queryLimit || |
| initialState.common.conf.DEFAULT_SQLLAB_LIMIT, |
| inLocalStorage: true, |
| loaded: true, |
| }, |
| }, |
| ]; |
| const request = actions.addNewQueryEditor(); |
| request(store.dispatch, store.getState, undefined); |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| |
| test('creates a new query editor from the saved state in the empty tab', () => { |
| const unsavedEmptyTabState = { |
| id: EMPTY_STATE_QE_ID, |
| dbId: 2, |
| catalog: 'test_catalog', |
| schema: 'test_schema', |
| }; |
| const store = mockStore({ |
| ...initialState, |
| sqlLab: { |
| ...initialState.sqlLab, |
| tabHistory: [EMPTY_STATE_QE_ID], |
| unsavedQueryEditor: unsavedEmptyTabState, |
| }, |
| }); |
| const expectedActions = [ |
| { |
| type: actions.ADD_QUERY_EDITOR, |
| queryEditor: { |
| id: 'abcd', |
| immutableId: 'abcd', |
| sql: expect.stringContaining('SELECT ...'), |
| name: 'Untitled Query 4', |
| dbId: unsavedEmptyTabState.dbId, |
| catalog: unsavedEmptyTabState.catalog, |
| schema: unsavedEmptyTabState.schema, |
| inLocalStorage: true, |
| autorun: false, |
| queryLimit: initialState.common.conf.DEFAULT_SQLLAB_LIMIT, |
| loaded: true, |
| }, |
| }, |
| ]; |
| const request = actions.addNewQueryEditor(); |
| request(store.dispatch, store.getState, undefined); |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| }); |
| }); |
| |
| test('set current query editor', () => { |
| expect.assertions(1); |
| |
| const store = mockStore(initialState); |
| const expectedActions = [ |
| { |
| type: actions.SET_ACTIVE_QUERY_EDITOR, |
| queryEditor: defaultQueryEditor, |
| }, |
| ]; |
| store.dispatch(actions.setActiveQueryEditor(defaultQueryEditor)); |
| |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('swithQueryEditor', () => { |
| test('switch to the next tab editor', () => { |
| const store = mockStore(initialState); |
| const expectedActions = [ |
| { |
| type: actions.SET_ACTIVE_QUERY_EDITOR, |
| queryEditor: initialState.sqlLab.queryEditors[1], |
| }, |
| ]; |
| store.dispatch(actions.switchQueryEditor()); |
| |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| |
| test('switch to the first tab editor once it reaches the rightmost tab', () => { |
| const store = mockStore({ |
| ...initialState, |
| sqlLab: { |
| ...initialState.sqlLab, |
| tabHistory: [ |
| initialState.sqlLab.queryEditors[ |
| initialState.sqlLab.queryEditors.length - 1 |
| ].id, |
| ], |
| }, |
| }); |
| const expectedActions = [ |
| { |
| type: actions.SET_ACTIVE_QUERY_EDITOR, |
| queryEditor: initialState.sqlLab.queryEditors[0], |
| }, |
| ]; |
| store.dispatch(actions.switchQueryEditor()); |
| |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| |
| test('switch to the previous tab editor', () => { |
| const store = mockStore({ |
| ...initialState, |
| sqlLab: { |
| ...initialState.sqlLab, |
| tabHistory: [initialState.sqlLab.queryEditors[1].id], |
| }, |
| }); |
| const expectedActions = [ |
| { |
| type: actions.SET_ACTIVE_QUERY_EDITOR, |
| queryEditor: initialState.sqlLab.queryEditors[0], |
| }, |
| ]; |
| store.dispatch(actions.switchQueryEditor(true)); |
| |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| |
| test('switch to the last tab editor once it reaches the leftmost tab', () => { |
| const store = mockStore({ |
| ...initialState, |
| sqlLab: { |
| ...initialState.sqlLab, |
| tabHistory: [initialState.sqlLab.queryEditors[0].id], |
| }, |
| }); |
| const expectedActions = [ |
| { |
| type: actions.SET_ACTIVE_QUERY_EDITOR, |
| queryEditor: |
| initialState.sqlLab.queryEditors[ |
| initialState.sqlLab.queryEditors.length - 1 |
| ], |
| }, |
| ]; |
| store.dispatch(actions.switchQueryEditor(true)); |
| |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| }); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('backend sync', () => { |
| const updateTabStateEndpoint = 'glob:*/tabstateview/*'; |
| fetchMock.put(updateTabStateEndpoint, {}); |
| fetchMock.delete(updateTabStateEndpoint, {}); |
| fetchMock.post(updateTabStateEndpoint, JSON.stringify({ id: 1 })); |
| |
| const updateTableSchemaEndpoint = 'glob:*/tableschemaview/*'; |
| fetchMock.put(updateTableSchemaEndpoint, {}); |
| fetchMock.delete(updateTableSchemaEndpoint, {}); |
| fetchMock.post(updateTableSchemaEndpoint, JSON.stringify({ id: 1 })); |
| |
| const updateTableSchemaExpandedEndpoint = |
| 'glob:**/tableschemaview/*/expanded'; |
| fetchMock.post(updateTableSchemaExpandedEndpoint, {}); |
| |
| const getTableMetadataEndpoint = |
| 'glob:**/api/v1/database/*/table_metadata/*'; |
| fetchMock.get(getTableMetadataEndpoint, {}); |
| const getExtraTableMetadataEndpoint = |
| 'glob:**/api/v1/database/*/table_metadata/extra/*'; |
| fetchMock.get(getExtraTableMetadataEndpoint, {}); |
| |
| beforeEach(() => { |
| isFeatureEnabledMock.mockImplementation( |
| (feature: string) => feature === 'SQLLAB_BACKEND_PERSISTENCE', |
| ); |
| }); |
| |
| afterEach(() => { |
| isFeatureEnabledMock.mockRestore(); |
| }); |
| |
| afterEach(() => fetchMock.clearHistory()); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('addQueryEditor', () => { |
| test('creates the tab state in the local storage', () => { |
| expect.assertions(2); |
| |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.ADD_QUERY_EDITOR, |
| queryEditor: { |
| ...queryEditor, |
| id: 'abcd', |
| immutableId: 'abcd', |
| loaded: true, |
| inLocalStorage: true, |
| }, |
| }, |
| ]; |
| store.dispatch(actions.addQueryEditor(queryEditor)); |
| |
| expect(store.getActions()).toEqual(expectedActions); |
| expect( |
| fetchMock.callHistory.calls(updateTabStateEndpoint), |
| ).toHaveLength(0); |
| }); |
| }); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('removeQueryEditor', () => { |
| test('updates the tab state in the backend', () => { |
| expect.assertions(1); |
| |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.REMOVE_QUERY_EDITOR, |
| queryEditor, |
| }, |
| ]; |
| store.dispatch(actions.removeQueryEditor(queryEditor)); |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| }); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('queryEditorSetDb', () => { |
| test('updates the tab state in the backend', () => { |
| expect.assertions(1); |
| |
| const dbId = 42; |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.QUERY_EDITOR_SETDB, |
| queryEditor, |
| dbId, |
| }, |
| ]; |
| store.dispatch(actions.queryEditorSetDb(queryEditor, dbId)); |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| }); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('queryEditorSetCatalog', () => { |
| test('updates the tab state in the backend', () => { |
| expect.assertions(1); |
| |
| const catalog = 'public'; |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.QUERY_EDITOR_SET_CATALOG, |
| queryEditor, |
| catalog, |
| }, |
| ]; |
| store.dispatch(actions.queryEditorSetCatalog(queryEditor, catalog)); |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| }); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('queryEditorSetSchema', () => { |
| test('updates the tab state in the backend', () => { |
| expect.assertions(1); |
| |
| const schema = 'schema'; |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.QUERY_EDITOR_SET_SCHEMA, |
| queryEditor, |
| schema, |
| }, |
| ]; |
| store.dispatch(actions.queryEditorSetSchema(queryEditor, schema)); |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| }); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('queryEditorSetAutorun', () => { |
| test('updates the tab state in the backend', () => { |
| expect.assertions(1); |
| |
| const autorun = true; |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.QUERY_EDITOR_SET_AUTORUN, |
| queryEditor, |
| autorun, |
| }, |
| ]; |
| store.dispatch(actions.queryEditorSetAutorun(queryEditor, autorun)); |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| }); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('queryEditorSetTitle', () => { |
| test('updates the tab state in the backend', () => { |
| expect.assertions(1); |
| |
| const name = 'name'; |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.QUERY_EDITOR_SET_TITLE, |
| queryEditor, |
| name, |
| }, |
| ]; |
| store.dispatch( |
| actions.queryEditorSetTitle(queryEditor, name, queryEditor.id), |
| ); |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| }); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('queryEditorSetAndSaveSql', () => { |
| const sql = 'SELECT * '; |
| const expectedActions = [ |
| { |
| type: actions.QUERY_EDITOR_SET_SQL, |
| queryEditor, |
| sql, |
| }, |
| ]; |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('with backend persistence flag on', () => { |
| test('updates the tab state in the backend', () => { |
| expect.assertions(2); |
| |
| const store = mockStore({ |
| ...initialState, |
| sqlLab: { |
| ...initialState.sqlLab, |
| queryEditors: [queryEditor], |
| }, |
| }); |
| const request = actions.queryEditorSetAndSaveSql(queryEditor, sql); |
| return request(store.dispatch, store.getState, undefined).then(() => { |
| expect(store.getActions()).toEqual(expectedActions); |
| expect( |
| fetchMock.callHistory.calls(updateTabStateEndpoint), |
| ).toHaveLength(1); |
| }); |
| }); |
| }); |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('with backend persistence flag off', () => { |
| test('does not update the tab state in the backend', () => { |
| isFeatureEnabledMock.mockImplementation( |
| (feature: string) => !(feature === 'SQLLAB_BACKEND_PERSISTENCE'), |
| ); |
| |
| const store = mockStore({ |
| ...initialState, |
| sqlLab: { |
| ...initialState.sqlLab, |
| queryEditors: [queryEditor], |
| }, |
| }); |
| const request = actions.queryEditorSetAndSaveSql(queryEditor, sql); |
| request(store.dispatch, store.getState, undefined); |
| |
| expect(store.getActions()).toEqual(expectedActions); |
| expect( |
| fetchMock.callHistory.calls(updateTabStateEndpoint), |
| ).toHaveLength(0); |
| isFeatureEnabledMock.mockRestore(); |
| }); |
| }); |
| }); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('queryEditorSetQueryLimit', () => { |
| test('updates the tab state in the backend', () => { |
| expect.assertions(1); |
| |
| const queryLimit = 10; |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.QUERY_EDITOR_SET_QUERY_LIMIT, |
| queryEditor, |
| queryLimit, |
| }, |
| ]; |
| store.dispatch( |
| actions.queryEditorSetQueryLimit(queryEditor, queryLimit), |
| ); |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| }); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('queryEditorSetTemplateParams', () => { |
| test('updates the tab state in the backend', () => { |
| expect.assertions(1); |
| |
| const templateParams = '{"foo": "bar"}'; |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.QUERY_EDITOR_SET_TEMPLATE_PARAMS, |
| queryEditor, |
| templateParams, |
| }, |
| ]; |
| store.dispatch( |
| actions.queryEditorSetTemplateParams(queryEditor, templateParams), |
| ); |
| |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| }); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('addTable', () => { |
| test('dispatches table state from unsaved change', () => { |
| const tableName = 'table'; |
| const catalogName = null; |
| const schemaName = 'schema'; |
| const expectedDbId = 473892; |
| const store = mockStore({ |
| ...initialState, |
| sqlLab: { |
| ...initialState.sqlLab, |
| unsavedQueryEditor: { |
| id: query.id, |
| dbId: expectedDbId, |
| }, |
| }, |
| }); |
| const request = actions.addTable( |
| query, |
| tableName, |
| catalogName, |
| schemaName, |
| ); |
| request(store.dispatch, store.getState, undefined); |
| expect(store.getActions()[0]).toEqual( |
| expect.objectContaining({ |
| table: expect.objectContaining({ |
| name: tableName, |
| catalog: catalogName, |
| schema: schemaName, |
| dbId: expectedDbId, |
| }), |
| }), |
| ); |
| }); |
| |
| test('uses tabViewId when available', () => { |
| const tableName = 'table'; |
| const catalogName = null; |
| const schemaName = 'schema'; |
| const expectedDbId = 473892; |
| const tabViewId = '123'; |
| const queryWithTabViewId = { ...query, tabViewId }; |
| |
| const store = mockStore({ |
| ...initialState, |
| sqlLab: { |
| ...initialState.sqlLab, |
| unsavedQueryEditor: { |
| id: query.id, |
| dbId: expectedDbId, |
| }, |
| }, |
| }); |
| |
| const request = actions.addTable( |
| queryWithTabViewId, |
| tableName, |
| catalogName, |
| schemaName, |
| ); |
| request(store.dispatch, store.getState, undefined); |
| |
| expect(store.getActions()[0]).toEqual( |
| expect.objectContaining({ |
| table: expect.objectContaining({ |
| name: tableName, |
| catalog: catalogName, |
| schema: schemaName, |
| dbId: expectedDbId, |
| queryEditorId: tabViewId, // Should use tabViewId, not id |
| }), |
| }), |
| ); |
| }); |
| |
| test('falls back to id when tabViewId is not available', () => { |
| const tableName = 'table'; |
| const catalogName = null; |
| const schemaName = 'schema'; |
| const expectedDbId = 473892; |
| const queryWithoutTabViewId = { ...query, tabViewId: undefined }; |
| |
| const store = mockStore({ |
| ...initialState, |
| sqlLab: { |
| ...initialState.sqlLab, |
| unsavedQueryEditor: { |
| id: query.id, |
| dbId: expectedDbId, |
| }, |
| }, |
| }); |
| |
| const request = actions.addTable( |
| queryWithoutTabViewId, |
| tableName, |
| catalogName, |
| schemaName, |
| ); |
| request(store.dispatch, store.getState, undefined); |
| |
| expect(store.getActions()[0]).toEqual( |
| expect.objectContaining({ |
| table: expect.objectContaining({ |
| name: tableName, |
| catalog: catalogName, |
| schema: schemaName, |
| dbId: expectedDbId, |
| queryEditorId: query.id, // Should use id when tabViewId is not available |
| }), |
| }), |
| ); |
| }); |
| }); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('runTablePreviewQuery', () => { |
| const results = { |
| data: mockBigNumber, |
| query: { sqlEditorId: 'null', dbId: 1 }, |
| query_id: 'efgh', |
| }; |
| const tableName = 'table'; |
| const catalogName = null; |
| const schemaName = 'schema'; |
| const store = mockStore({ |
| ...initialState, |
| sqlLab: { |
| ...initialState.sqlLab, |
| databases: { |
| 1: { disable_data_preview: false }, |
| }, |
| }, |
| }); |
| |
| beforeEach(() => { |
| fetchMock.removeRoute(runQueryEndpoint); |
| fetchMock.post(runQueryEndpoint, JSON.stringify(results), { |
| name: runQueryEndpoint, |
| }); |
| }); |
| |
| afterEach(() => { |
| store.clearActions(); |
| fetchMock.clearHistory(); |
| }); |
| |
| test('updates and runs data preview query when configured', () => { |
| expect.assertions(3); |
| |
| const expectedActionTypes = [ |
| actions.MERGE_TABLE, // addTable (data preview) |
| actions.START_QUERY, // runQuery (data preview) |
| actions.QUERY_SUCCESS, // querySuccess |
| ]; |
| const request = actions.runTablePreviewQuery({ |
| dbId: 1, |
| name: tableName, |
| catalog: catalogName, |
| schema: schemaName, |
| }); |
| return request(store.dispatch, store.getState, undefined).then(() => { |
| expect(store.getActions().map(a => a.type)).toEqual( |
| expectedActionTypes, |
| ); |
| expect(fetchMock.callHistory.calls(runQueryEndpoint)).toHaveLength(1); |
| // tab state is not updated, since the query is a data preview |
| expect( |
| fetchMock.callHistory.calls(updateTabStateEndpoint), |
| ).toHaveLength(0); |
| }); |
| }); |
| |
| test('runs data preview query only', () => { |
| const expectedActionTypes = [ |
| actions.START_QUERY, // runQuery (data preview) |
| actions.QUERY_SUCCESS, // querySuccess |
| ]; |
| const request = actions.runTablePreviewQuery( |
| { |
| dbId: 1, |
| name: tableName, |
| catalog: catalogName, |
| schema: schemaName, |
| }, |
| true, |
| ); |
| return request(store.dispatch, store.getState, undefined).then(() => { |
| expect(store.getActions().map(a => a.type)).toEqual( |
| expectedActionTypes, |
| ); |
| expect(fetchMock.callHistory.calls(runQueryEndpoint)).toHaveLength(1); |
| // tab state is not updated, since the query is a data preview |
| expect( |
| fetchMock.callHistory.calls(updateTabStateEndpoint), |
| ).toHaveLength(0); |
| }); |
| }); |
| }); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('expandTable', () => { |
| test('updates the table schema state in the backend when initialized', () => { |
| expect.assertions(2); |
| |
| const table = { id: 1, initialized: true }; |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.EXPAND_TABLE, |
| table, |
| }, |
| ]; |
| return store |
| .dispatch(actions.expandTable(table as unknown as Table)) |
| .then(() => { |
| expect(store.getActions()).toEqual(expectedActions); |
| const expandedCalls = fetchMock.callHistory |
| .calls() |
| .filter( |
| call => |
| call.url && |
| call.url.includes('/tableschemaview/') && |
| call.url.includes('/expanded'), |
| ); |
| expect(expandedCalls).toHaveLength(1); |
| }); |
| }); |
| |
| test('does not call backend when table is not initialized', () => { |
| expect.assertions(2); |
| |
| const table = { id: 'yVJPtuSackF', initialized: false }; |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.EXPAND_TABLE, |
| table, |
| }, |
| ]; |
| return store |
| .dispatch(actions.expandTable(table as unknown as Table)) |
| .then(() => { |
| expect(store.getActions()).toEqual(expectedActions); |
| // Check all POST calls to find the expanded endpoint |
| const expandedCalls = fetchMock.callHistory |
| .calls() |
| .filter( |
| call => |
| call.url && |
| call.url.includes('/tableschemaview/') && |
| call.url.includes('/expanded'), |
| ); |
| expect(expandedCalls).toHaveLength(0); |
| }); |
| }); |
| |
| test('does not call backend when initialized is undefined', () => { |
| expect.assertions(2); |
| |
| const table = { id: 'yVJPtuSackF' }; |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.EXPAND_TABLE, |
| table, |
| }, |
| ]; |
| return store |
| .dispatch(actions.expandTable(table as unknown as Table)) |
| .then(() => { |
| expect(store.getActions()).toEqual(expectedActions); |
| // Check all POST calls to find the expanded endpoint |
| const expandedCalls = fetchMock.callHistory |
| .calls() |
| .filter( |
| call => |
| call.url && |
| call.url.includes('/tableschemaview/') && |
| call.url.includes('/expanded'), |
| ); |
| expect(expandedCalls).toHaveLength(0); |
| }); |
| }); |
| |
| test('does not call backend when feature flag is off', () => { |
| expect.assertions(2); |
| |
| isFeatureEnabledMock.mockImplementation( |
| (feature: string) => !(feature === 'SQLLAB_BACKEND_PERSISTENCE'), |
| ); |
| |
| const table = { id: 1, initialized: true }; |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.EXPAND_TABLE, |
| table, |
| }, |
| ]; |
| return store |
| .dispatch(actions.expandTable(table as unknown as Table)) |
| .then(() => { |
| expect(store.getActions()).toEqual(expectedActions); |
| // Check all POST calls to find the expanded endpoint |
| const expandedCalls = fetchMock.callHistory |
| .calls() |
| .filter( |
| call => |
| call.url && |
| call.url.includes('/tableschemaview/') && |
| call.url.includes('/expanded'), |
| ); |
| expect(expandedCalls).toHaveLength(0); |
| isFeatureEnabledMock.mockRestore(); |
| }); |
| }); |
| }); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('collapseTable', () => { |
| test('updates the table schema state in the backend when initialized', () => { |
| expect.assertions(2); |
| |
| const table = { id: 1, initialized: true }; |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.COLLAPSE_TABLE, |
| table, |
| }, |
| ]; |
| return store |
| .dispatch(actions.collapseTable(table as unknown as Table)) |
| .then(() => { |
| expect(store.getActions()).toEqual(expectedActions); |
| const expandedCalls = fetchMock.callHistory |
| .calls() |
| .filter( |
| call => |
| call.url && |
| call.url.includes('/tableschemaview/') && |
| call.url.includes('/expanded'), |
| ); |
| expect(expandedCalls).toHaveLength(1); |
| }); |
| }); |
| |
| test('does not call backend when table is not initialized', () => { |
| expect.assertions(2); |
| |
| const table = { id: 'yVJPtuSackF', initialized: false }; |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.COLLAPSE_TABLE, |
| table, |
| }, |
| ]; |
| return store |
| .dispatch(actions.collapseTable(table as unknown as Table)) |
| .then(() => { |
| expect(store.getActions()).toEqual(expectedActions); |
| const expandedCalls = fetchMock.callHistory |
| .calls() |
| .filter( |
| call => |
| call.url && |
| call.url.includes('/tableschemaview/') && |
| call.url.includes('/expanded'), |
| ); |
| expect(expandedCalls).toHaveLength(0); |
| }); |
| }); |
| |
| test('does not call backend when initialized is undefined', () => { |
| expect.assertions(2); |
| |
| const table = { id: 'yVJPtuSackF' }; |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.COLLAPSE_TABLE, |
| table, |
| }, |
| ]; |
| return store |
| .dispatch(actions.collapseTable(table as unknown as Table)) |
| .then(() => { |
| expect(store.getActions()).toEqual(expectedActions); |
| const expandedCalls = fetchMock.callHistory |
| .calls() |
| .filter( |
| call => |
| call.url && |
| call.url.includes('/tableschemaview/') && |
| call.url.includes('/expanded'), |
| ); |
| expect(expandedCalls).toHaveLength(0); |
| }); |
| }); |
| |
| test('does not call backend when feature flag is off', () => { |
| expect.assertions(2); |
| |
| isFeatureEnabledMock.mockImplementation( |
| (feature: string) => !(feature === 'SQLLAB_BACKEND_PERSISTENCE'), |
| ); |
| |
| const table = { id: 1, initialized: true }; |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.COLLAPSE_TABLE, |
| table, |
| }, |
| ]; |
| return store |
| .dispatch(actions.collapseTable(table as unknown as Table)) |
| .then(() => { |
| expect(store.getActions()).toEqual(expectedActions); |
| const expandedCalls = fetchMock.callHistory |
| .calls() |
| .filter( |
| call => |
| call.url && |
| call.url.includes('/tableschemaview/') && |
| call.url.includes('/expanded'), |
| ); |
| expect(expandedCalls).toHaveLength(0); |
| isFeatureEnabledMock.mockRestore(); |
| }); |
| }); |
| }); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('removeTables', () => { |
| test('updates the table schema state in the backend', () => { |
| expect.assertions(2); |
| |
| const table = { id: 1, initialized: true }; |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.REMOVE_TABLES, |
| tables: [table], |
| }, |
| ]; |
| return store |
| .dispatch(actions.removeTables([table] as unknown as Table[])) |
| .then(() => { |
| expect(store.getActions()).toEqual(expectedActions); |
| expect( |
| fetchMock.callHistory.calls(updateTableSchemaEndpoint), |
| ).toHaveLength(1); |
| }); |
| }); |
| |
| test('deletes multiple tables and updates the table schema state in the backend', () => { |
| expect.assertions(2); |
| |
| const tables = [ |
| { id: 1, initialized: true }, |
| { id: 2, initialized: true }, |
| ]; |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.REMOVE_TABLES, |
| tables, |
| }, |
| ]; |
| return store |
| .dispatch(actions.removeTables(tables as unknown as Table[])) |
| .then(() => { |
| expect(store.getActions()).toEqual(expectedActions); |
| expect( |
| fetchMock.callHistory.calls(updateTableSchemaEndpoint), |
| ).toHaveLength(2); |
| }); |
| }); |
| |
| test('only updates the initialized table schema state in the backend', () => { |
| expect.assertions(2); |
| |
| const tables = [{ id: 1 }, { id: 2, initialized: true }]; |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.REMOVE_TABLES, |
| tables, |
| }, |
| ]; |
| return store |
| .dispatch(actions.removeTables(tables as unknown as Table[])) |
| .then(() => { |
| expect(store.getActions()).toEqual(expectedActions); |
| expect( |
| fetchMock.callHistory.calls(updateTableSchemaEndpoint), |
| ).toHaveLength(1); |
| }); |
| }); |
| }); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('syncQueryEditor', () => { |
| test('updates the tab state in the backend', () => { |
| expect.assertions(3); |
| |
| const results = { |
| data: mockBigNumber, |
| query: { sqlEditorId: 'null' }, |
| query_id: 'efgh', |
| }; |
| fetchMock.removeRoute(runQueryEndpoint); |
| fetchMock.post(runQueryEndpoint, JSON.stringify(results), { |
| name: runQueryEndpoint, |
| }); |
| |
| const oldQueryEditor = { ...queryEditor, inLocalStorage: true }; |
| const tables = [ |
| { |
| id: 'one', |
| dataPreviewQueryId: 'previewOne', |
| queryEditorId: oldQueryEditor.id, |
| inLocalStorage: true, |
| }, |
| { |
| id: 'two', |
| dataPreviewQueryId: 'previewTwo', |
| queryEditorId: oldQueryEditor.id, |
| inLocalStorage: true, |
| }, |
| ]; |
| const queries = [ |
| { |
| ...query, |
| id: 'previewOne', |
| sqlEditorId: oldQueryEditor.id, |
| inLocalStorage: true, |
| }, |
| { |
| ...query, |
| id: 'previewTwo', |
| sqlEditorId: oldQueryEditor.id, |
| inLocalStorage: true, |
| }, |
| ]; |
| const store = mockStore({ |
| sqlLab: { |
| queries, |
| tables, |
| }, |
| }); |
| const expectedActions = [ |
| { |
| type: actions.MIGRATE_QUERY_EDITOR, |
| oldQueryEditor, |
| // new qe has a different id |
| newQueryEditor: { |
| ...oldQueryEditor, |
| tabViewId: '1', |
| inLocalStorage: false, |
| loaded: true, |
| }, |
| }, |
| { |
| type: actions.MIGRATE_TABLE, |
| oldTable: tables[0], |
| // new table has a different id and points to new query editor |
| newTable: { ...tables[0], id: 1, queryEditorId: '1' }, |
| }, |
| { |
| type: actions.MIGRATE_TABLE, |
| oldTable: tables[1], |
| // new table has a different id and points to new query editor |
| newTable: { ...tables[1], id: 1, queryEditorId: '1' }, |
| }, |
| { |
| type: actions.MIGRATE_QUERY, |
| queryId: 'previewOne', |
| queryEditorId: '1', |
| }, |
| { |
| type: actions.MIGRATE_QUERY, |
| queryId: 'previewTwo', |
| queryEditorId: '1', |
| }, |
| ]; |
| return store |
| .dispatch(actions.syncQueryEditor(oldQueryEditor)) |
| .then(() => { |
| expect(store.getActions()).toEqual(expectedActions); |
| expect( |
| fetchMock.callHistory.calls(updateTabStateEndpoint), |
| ).toHaveLength(3); |
| |
| // query editor has 2 tables loaded in the schema viewer |
| expect( |
| fetchMock.callHistory.calls(updateTableSchemaEndpoint), |
| ).toHaveLength(2); |
| }); |
| }); |
| }); |
| }); |
| }); |