Files
superset2/superset-frontend/src/SqlLab/actions/sqlLab.test.ts
Jean Massucatto fe7d9b4724 fix(sqllab): reflect query history deletion without page refresh (#41019)
Co-authored-by: Evan Rusackas <evan@rusackas.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 09:40:33 -07:00

2002 lines
61 KiB
TypeScript

/**
* 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 { api } from 'src/hooks/apiResources/queryApi';
import {
queryHistoryApi,
type QueryResult,
} from 'src/hooks/apiResources/queries';
import { defaultStore } from 'spec/helpers/testing-library';
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: 'A saved query 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',
description: 'A saved query description',
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);
});
});
test('removeQuery removes the deleted query from the editorQueries cache', async () => {
isFeatureEnabledMock.mockReturnValue(false);
const editorId = 'editor1';
const queryToRemove = {
...query,
id: 'queryToRemove',
sqlEditorId: editorId,
};
await defaultStore.dispatch(
queryHistoryApi.util.upsertQueryData('editorQueries', { editorId }, {
count: 2,
ids: [],
result: [{ id: 'queryToRemove' }, { id: 'keep' }],
} as unknown as QueryResult),
);
await defaultStore.dispatch(
actions.removeQuery(queryToRemove) as unknown as AnyAction,
);
const { data } = queryHistoryApi.endpoints.editorQueries.select({
editorId,
})(defaultStore.getState());
expect(data).toEqual({
count: 1,
ids: [],
result: [{ id: 'keep' }],
});
defaultStore.dispatch(api.util.resetApiState());
});
test('removeQuery removes duplicate cache entries for the deleted query', async () => {
isFeatureEnabledMock.mockReturnValue(false);
const editorId = 'editor1';
const queryToRemove = {
...query,
id: 'queryToRemove',
sqlEditorId: editorId,
};
// The infinite-scroll merge can append the same query twice when
// offsets shift between page fetches; deletion must drop every copy.
await defaultStore.dispatch(
queryHistoryApi.util.upsertQueryData('editorQueries', { editorId }, {
count: 3,
ids: [],
result: [
{ id: 'queryToRemove' },
{ id: 'keep' },
{ id: 'queryToRemove' },
],
} as unknown as QueryResult),
);
await defaultStore.dispatch(
actions.removeQuery(queryToRemove) as unknown as AnyAction,
);
const { data } = queryHistoryApi.endpoints.editorQueries.select({
editorId,
})(defaultStore.getState());
expect(data).toEqual({
count: 2,
ids: [],
result: [{ id: 'keep' }],
});
defaultStore.dispatch(api.util.resetApiState());
});
// 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: null,
isDataPreview: true,
},
{
...query,
id: 'previewTwo',
sqlEditorId: null,
isDataPreview: true,
},
{
...query,
id: 'runningQuery',
sqlEditorId: oldQueryEditor.id,
state: 'running',
},
{
...query,
id: 'unrelatedQuery',
sqlEditorId: 'other-editor',
state: 'running',
},
];
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: 'runningQuery',
queryEditorId: '1',
},
];
return store
.dispatch(actions.syncQueryEditor(oldQueryEditor))
.then(() => {
expect(store.getActions()).toEqual(expectedActions);
expect(
fetchMock.callHistory.calls(updateTabStateEndpoint),
).toHaveLength(2);
// query editor has 2 tables loaded in the schema viewer
expect(
fetchMock.callHistory.calls(updateTableSchemaEndpoint),
).toHaveLength(2);
});
});
});
});
});