mirror of
https://github.com/apache/superset.git
synced 2026-07-09 08:15:49 +00:00
Co-authored-by: Evan Rusackas <evan@rusackas.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2002 lines
61 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|