mirror of
https://github.com/apache/superset.git
synced 2026-05-30 04:39:20 +00:00
782 lines
25 KiB
TypeScript
782 lines
25 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 URI from 'urijs';
|
|
import fetchMock from 'fetch-mock';
|
|
|
|
import {
|
|
FeatureFlag,
|
|
SupersetClient,
|
|
getChartMetadataRegistry,
|
|
getChartBuildQueryRegistry,
|
|
QueryFormData,
|
|
JsonObject,
|
|
AnnotationLayer,
|
|
AnnotationType,
|
|
AnnotationSourceType,
|
|
AnnotationStyle,
|
|
} from '@superset-ui/core';
|
|
import { LOG_EVENT } from 'src/logger/actions';
|
|
import * as exploreUtils from 'src/explore/exploreUtils';
|
|
import * as actions from 'src/components/Chart/chartAction';
|
|
import * as asyncEvent from 'src/middleware/asyncEvent';
|
|
import { handleChartDataResponse } from 'src/components/Chart/chartAction';
|
|
import * as dataMaskActions from 'src/dataMask/actions';
|
|
|
|
import configureMockStore from 'redux-mock-store';
|
|
import thunk from 'redux-thunk';
|
|
import { initialState } from 'src/SqlLab/fixtures';
|
|
|
|
interface MockState {
|
|
charts: {
|
|
[key: string]: {
|
|
latestQueryFormData?: {
|
|
time_grain_sqla?: string;
|
|
granularity_sqla?: string;
|
|
};
|
|
queryController?: AbortController;
|
|
};
|
|
};
|
|
common: {
|
|
conf: {
|
|
SUPERSET_WEBSERVER_TIMEOUT?: number;
|
|
};
|
|
};
|
|
}
|
|
|
|
const middlewares = [thunk];
|
|
const mockStore = configureMockStore(middlewares);
|
|
|
|
const mockGetState = (): MockState => ({
|
|
charts: {
|
|
chartKey: {
|
|
latestQueryFormData: {
|
|
time_grain_sqla: 'P1D',
|
|
granularity_sqla: 'Date',
|
|
},
|
|
},
|
|
},
|
|
common: {
|
|
conf: {},
|
|
},
|
|
});
|
|
|
|
jest.mock('@superset-ui/core', () => ({
|
|
...jest.requireActual('@superset-ui/core'),
|
|
getChartMetadataRegistry: jest.fn(),
|
|
getChartBuildQueryRegistry: jest.fn(),
|
|
}));
|
|
|
|
const mockedGetChartMetadataRegistry =
|
|
getChartMetadataRegistry as jest.MockedFunction<
|
|
typeof getChartMetadataRegistry
|
|
>;
|
|
const mockedGetChartBuildQueryRegistry =
|
|
getChartBuildQueryRegistry as jest.MockedFunction<
|
|
typeof getChartBuildQueryRegistry
|
|
>;
|
|
|
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
|
describe('chart actions', () => {
|
|
const MOCK_URL = '/mockURL';
|
|
let dispatch: jest.Mock;
|
|
let getExploreUrlStub: jest.SpyInstance;
|
|
let getChartDataUriStub: jest.SpyInstance;
|
|
let buildV1ChartDataPayloadStub: jest.SpyInstance;
|
|
let waitForAsyncDataStub: jest.SpyInstance;
|
|
let fakeMetadata: { useLegacyApi?: boolean; viz_type?: string };
|
|
|
|
beforeAll(() => {
|
|
fetchMock.get('glob:*api/v1/security/csrf_token/*', { result: '1234' });
|
|
});
|
|
|
|
const setupDefaultFetchMock = (): void => {
|
|
fetchMock.post(`glob:*${MOCK_URL}*`, { json: {} }, { name: MOCK_URL });
|
|
};
|
|
|
|
beforeEach(() => {
|
|
setupDefaultFetchMock();
|
|
});
|
|
|
|
afterEach(() => fetchMock.clearHistory().removeRoutes());
|
|
|
|
beforeEach(() => {
|
|
dispatch = jest.fn();
|
|
getExploreUrlStub = jest
|
|
.spyOn(exploreUtils, 'getExploreUrl')
|
|
.mockImplementation(() => MOCK_URL);
|
|
getChartDataUriStub = jest
|
|
.spyOn(exploreUtils, 'getChartDataUri')
|
|
.mockImplementation(({ qs }: { qs?: Record<string, unknown> }) =>
|
|
URI(MOCK_URL).query(qs || {}),
|
|
);
|
|
buildV1ChartDataPayloadStub = jest
|
|
.spyOn(exploreUtils, 'buildV1ChartDataPayload')
|
|
.mockResolvedValue({
|
|
some_param: 'fake query!',
|
|
result_type: 'full',
|
|
result_format: 'json',
|
|
} as unknown as Awaited<
|
|
ReturnType<typeof exploreUtils.buildV1ChartDataPayload>
|
|
>);
|
|
fakeMetadata = { useLegacyApi: true };
|
|
mockedGetChartMetadataRegistry.mockImplementation(
|
|
() =>
|
|
({
|
|
get: () => fakeMetadata,
|
|
}) as unknown as ReturnType<typeof getChartMetadataRegistry>,
|
|
);
|
|
mockedGetChartBuildQueryRegistry.mockImplementation(
|
|
() =>
|
|
({
|
|
get: () => () => ({
|
|
some_param: 'fake query!',
|
|
result_type: 'full',
|
|
result_format: 'json',
|
|
}),
|
|
}) as unknown as ReturnType<typeof getChartBuildQueryRegistry>,
|
|
);
|
|
waitForAsyncDataStub = jest
|
|
.spyOn(asyncEvent, 'waitForAsyncData')
|
|
.mockImplementation((data: unknown) => Promise.resolve(data));
|
|
});
|
|
|
|
test('should drop stale success dispatches when a newer controller has replaced ours in state', async () => {
|
|
const chartKey = 'stale_success_test';
|
|
const formData: Partial<QueryFormData> = {
|
|
slice_id: 456,
|
|
datasource: 'table__1',
|
|
viz_type: 'table',
|
|
};
|
|
// A controller belonging to a *newer* in-flight request, already stored
|
|
// in state by the time this thunk's response resolves.
|
|
const newerController = new AbortController();
|
|
const state: MockState = {
|
|
charts: {
|
|
[chartKey]: {
|
|
queryController: newerController,
|
|
},
|
|
},
|
|
common: {
|
|
conf: {
|
|
SUPERSET_WEBSERVER_TIMEOUT: 60,
|
|
},
|
|
},
|
|
};
|
|
const getState = jest.fn(() => state);
|
|
const dispatchMock = jest.fn();
|
|
const getChartDataRequestSpy = jest
|
|
.spyOn(actions, 'getChartDataRequest')
|
|
.mockResolvedValue({
|
|
response: { status: 200 } as Response,
|
|
json: { result: [{ data: [{ stale: true }] }] },
|
|
});
|
|
const handleChartDataResponseSpy = jest
|
|
.spyOn(actions, 'handleChartDataResponse')
|
|
.mockResolvedValue([{ data: [{ stale: true }] }]);
|
|
const updateDataMaskSpy = jest
|
|
.spyOn(dataMaskActions, 'updateDataMask')
|
|
.mockReturnValue({ type: 'UPDATE_DATA_MASK' } as ReturnType<
|
|
typeof dataMaskActions.updateDataMask
|
|
>);
|
|
const getQuerySettingsStub = jest
|
|
.spyOn(exploreUtils, 'getQuerySettings')
|
|
.mockReturnValue([false, () => {}] as unknown as ReturnType<
|
|
typeof exploreUtils.getQuerySettings
|
|
>);
|
|
|
|
try {
|
|
const thunkAction = actions.exploreJSON(
|
|
formData as QueryFormData,
|
|
false,
|
|
undefined,
|
|
chartKey,
|
|
);
|
|
await thunkAction(
|
|
dispatchMock as unknown as actions.ChartThunkDispatch,
|
|
getState as unknown as () => actions.RootState,
|
|
undefined,
|
|
);
|
|
|
|
// CHART_UPDATE_STARTED is fine (it ran before the gate),
|
|
// but CHART_UPDATE_SUCCEEDED must NOT have fired with the stale data.
|
|
const dispatchedTypes = dispatchMock.mock.calls.map(
|
|
([action]) => action?.type,
|
|
);
|
|
expect(dispatchedTypes).toContain(actions.CHART_UPDATE_STARTED);
|
|
expect(dispatchedTypes).not.toContain(actions.CHART_UPDATE_SUCCEEDED);
|
|
} finally {
|
|
getChartDataRequestSpy.mockRestore();
|
|
handleChartDataResponseSpy.mockRestore();
|
|
updateDataMaskSpy.mockRestore();
|
|
getQuerySettingsStub.mockRestore();
|
|
}
|
|
});
|
|
|
|
test('should defer abort of previous controller to avoid Redux state mutation', async () => {
|
|
jest.useFakeTimers();
|
|
const chartKey = 'defer_abort_test';
|
|
const formData: Partial<QueryFormData> = {
|
|
slice_id: 123,
|
|
datasource: 'table__1',
|
|
viz_type: 'table',
|
|
};
|
|
const oldController = new AbortController();
|
|
const abortSpy = jest.spyOn(oldController, 'abort');
|
|
const state: MockState = {
|
|
charts: {
|
|
[chartKey]: {
|
|
queryController: oldController,
|
|
},
|
|
},
|
|
common: {
|
|
conf: {
|
|
SUPERSET_WEBSERVER_TIMEOUT: 60,
|
|
},
|
|
},
|
|
};
|
|
const getState = jest.fn(() => state);
|
|
const dispatchMock = jest.fn();
|
|
const getChartDataRequestSpy = jest
|
|
.spyOn(actions, 'getChartDataRequest')
|
|
.mockResolvedValue({
|
|
response: { status: 200 } as Response,
|
|
json: { result: [] },
|
|
});
|
|
const handleChartDataResponseSpy = jest
|
|
.spyOn(actions, 'handleChartDataResponse')
|
|
.mockResolvedValue([]);
|
|
const updateDataMaskSpy = jest
|
|
.spyOn(dataMaskActions, 'updateDataMask')
|
|
.mockReturnValue({ type: 'UPDATE_DATA_MASK' } as ReturnType<
|
|
typeof dataMaskActions.updateDataMask
|
|
>);
|
|
const getQuerySettingsStub = jest
|
|
.spyOn(exploreUtils, 'getQuerySettings')
|
|
.mockReturnValue([false, () => {}] as unknown as ReturnType<
|
|
typeof exploreUtils.getQuerySettings
|
|
>);
|
|
|
|
try {
|
|
const thunkAction = actions.exploreJSON(
|
|
formData as QueryFormData,
|
|
false,
|
|
undefined,
|
|
chartKey,
|
|
);
|
|
const promise = thunkAction(
|
|
dispatchMock as unknown as actions.ChartThunkDispatch,
|
|
getState as unknown as () => actions.RootState,
|
|
undefined,
|
|
);
|
|
|
|
expect(abortSpy).not.toHaveBeenCalled();
|
|
expect(oldController.signal.aborted).toBe(false);
|
|
|
|
jest.runOnlyPendingTimers();
|
|
|
|
expect(abortSpy).toHaveBeenCalledTimes(1);
|
|
expect(oldController.signal.aborted).toBe(true);
|
|
|
|
await promise;
|
|
} finally {
|
|
getChartDataRequestSpy.mockRestore();
|
|
handleChartDataResponseSpy.mockRestore();
|
|
updateDataMaskSpy.mockRestore();
|
|
getQuerySettingsStub.mockRestore();
|
|
abortSpy.mockRestore();
|
|
jest.useRealTimers();
|
|
}
|
|
});
|
|
|
|
afterEach(() => {
|
|
getExploreUrlStub.mockRestore();
|
|
getChartDataUriStub.mockRestore();
|
|
buildV1ChartDataPayloadStub.mockRestore();
|
|
fetchMock.clearHistory();
|
|
waitForAsyncDataStub.mockRestore();
|
|
|
|
(
|
|
global as unknown as { featureFlags: Record<string, boolean> }
|
|
).featureFlags = {
|
|
[FeatureFlag.GlobalAsyncQueries]: false,
|
|
};
|
|
});
|
|
|
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
|
describe('v1 API', () => {
|
|
beforeEach(() => {
|
|
fakeMetadata = { viz_type: 'my_viz', useLegacyApi: false };
|
|
});
|
|
|
|
test('should query with the built query', async () => {
|
|
const actionThunk = actions.postChartFormData(
|
|
{} as QueryFormData,
|
|
false,
|
|
undefined,
|
|
undefined,
|
|
);
|
|
await actionThunk(
|
|
dispatch as unknown as actions.ChartThunkDispatch,
|
|
mockGetState as unknown as () => actions.RootState,
|
|
undefined,
|
|
);
|
|
|
|
expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1);
|
|
expect(fetchMock.callHistory.calls(MOCK_URL)[0].options.body).toBe(
|
|
JSON.stringify({
|
|
some_param: 'fake query!',
|
|
result_type: 'full',
|
|
result_format: 'json',
|
|
}),
|
|
);
|
|
expect(dispatch.mock.calls[0][0].type).toBe(actions.CHART_UPDATE_STARTED);
|
|
});
|
|
|
|
test('should handle the bigint without regression', async () => {
|
|
getChartDataUriStub.mockRestore();
|
|
const mockBigIntUrl = '/mock/chart/data/bigint';
|
|
const expectedBigNumber = '9223372036854775807';
|
|
fetchMock.post(mockBigIntUrl, `{ "value": ${expectedBigNumber} }`, {
|
|
name: mockBigIntUrl,
|
|
});
|
|
getChartDataUriStub = jest
|
|
.spyOn(exploreUtils, 'getChartDataUri')
|
|
.mockImplementation(() => URI(mockBigIntUrl));
|
|
|
|
const { json } = await actions.getChartDataRequest({
|
|
formData: fakeMetadata as QueryFormData,
|
|
});
|
|
|
|
expect(fetchMock.callHistory.calls(mockBigIntUrl)).toHaveLength(1);
|
|
expect((json as JsonObject).value.toString()).toEqual(expectedBigNumber);
|
|
});
|
|
|
|
test('handleChartDataResponse should return result if GlobalAsyncQueries flag is disabled', async () => {
|
|
const result = await handleChartDataResponse(
|
|
{ status: 200 } as Response,
|
|
{
|
|
result: [
|
|
1, 2, 3,
|
|
] as unknown as actions.ChartDataRequestResponse['json']['result'],
|
|
},
|
|
);
|
|
expect(result).toEqual([1, 2, 3]);
|
|
});
|
|
|
|
test('handleChartDataResponse should handle responses when GlobalAsyncQueries flag is enabled and results are returned synchronously', async () => {
|
|
(
|
|
global as unknown as { featureFlags: Record<string, boolean> }
|
|
).featureFlags = {
|
|
[FeatureFlag.GlobalAsyncQueries]: true,
|
|
};
|
|
const result = await handleChartDataResponse(
|
|
{ status: 200 } as Response,
|
|
{
|
|
result: [
|
|
1, 2, 3,
|
|
] as unknown as actions.ChartDataRequestResponse['json']['result'],
|
|
},
|
|
);
|
|
expect(result).toEqual([1, 2, 3]);
|
|
});
|
|
|
|
test('handleChartDataResponse should handle responses when GlobalAsyncQueries flag is enabled and query is running asynchronously', async () => {
|
|
(
|
|
global as unknown as { featureFlags: Record<string, boolean> }
|
|
).featureFlags = {
|
|
[FeatureFlag.GlobalAsyncQueries]: true,
|
|
};
|
|
const result = await handleChartDataResponse(
|
|
{ status: 202 } as Response,
|
|
{
|
|
result: [
|
|
1, 2, 3,
|
|
] as unknown as actions.ChartDataRequestResponse['json']['result'],
|
|
},
|
|
);
|
|
expect(result).toEqual([1, 2, 3]);
|
|
});
|
|
});
|
|
|
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
|
describe('legacy API', () => {
|
|
beforeEach(() => {
|
|
fakeMetadata = { useLegacyApi: true };
|
|
});
|
|
|
|
test('should dispatch CHART_UPDATE_STARTED action before the query', () => {
|
|
const actionThunk = actions.postChartFormData({
|
|
viz_type: 'table',
|
|
} as QueryFormData);
|
|
|
|
return actionThunk(
|
|
dispatch as unknown as actions.ChartThunkDispatch,
|
|
mockGetState as unknown as () => actions.RootState,
|
|
undefined,
|
|
).then(() => {
|
|
// chart update, trigger query, update form data, success
|
|
expect(dispatch.mock.calls.length).toBe(5);
|
|
expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1);
|
|
expect(dispatch.mock.calls[0][0].type).toBe(
|
|
actions.CHART_UPDATE_STARTED,
|
|
);
|
|
});
|
|
});
|
|
|
|
test('should dispatch TRIGGER_QUERY action with the query', () => {
|
|
const actionThunk = actions.postChartFormData({
|
|
viz_type: 'table',
|
|
} as QueryFormData);
|
|
return actionThunk(
|
|
dispatch as unknown as actions.ChartThunkDispatch,
|
|
mockGetState as unknown as () => actions.RootState,
|
|
undefined,
|
|
).then(() => {
|
|
// chart update, trigger query, update form data, success
|
|
expect(dispatch.mock.calls.length).toBe(5);
|
|
expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1);
|
|
expect(dispatch.mock.calls[1][0].type).toBe(actions.TRIGGER_QUERY);
|
|
});
|
|
});
|
|
|
|
test('should dispatch UPDATE_QUERY_FORM_DATA action with the query', () => {
|
|
const actionThunk = actions.postChartFormData({
|
|
viz_type: 'table',
|
|
} as QueryFormData);
|
|
return actionThunk(
|
|
dispatch as unknown as actions.ChartThunkDispatch,
|
|
mockGetState as unknown as () => actions.RootState,
|
|
undefined,
|
|
).then(() => {
|
|
// chart update, trigger query, update form data, success
|
|
expect(dispatch.mock.calls.length).toBe(5);
|
|
expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1);
|
|
expect(dispatch.mock.calls[2][0].type).toBe(
|
|
actions.UPDATE_QUERY_FORM_DATA,
|
|
);
|
|
});
|
|
});
|
|
|
|
test('should dispatch logEvent async action', () => {
|
|
const actionThunk = actions.postChartFormData({
|
|
viz_type: 'table',
|
|
} as QueryFormData);
|
|
return actionThunk(
|
|
dispatch as unknown as actions.ChartThunkDispatch,
|
|
mockGetState as unknown as () => actions.RootState,
|
|
undefined,
|
|
).then(() => {
|
|
// chart update, trigger query, update form data, success
|
|
expect(dispatch.mock.calls.length).toBe(5);
|
|
expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1);
|
|
expect(typeof dispatch.mock.calls[3][0]).toBe('function');
|
|
|
|
dispatch.mock.calls[3][0](dispatch);
|
|
expect(dispatch.mock.calls.length).toBe(6);
|
|
expect(dispatch.mock.calls[5][0].type).toBe(LOG_EVENT);
|
|
});
|
|
});
|
|
|
|
test('should dispatch CHART_UPDATE_SUCCEEDED action upon success', () => {
|
|
// Pass a viz_type so getQuerySettings returns useLegacyApi from the mocked registry
|
|
const actionThunk = actions.postChartFormData({
|
|
viz_type: 'table',
|
|
} as QueryFormData);
|
|
return actionThunk(
|
|
dispatch as unknown as actions.ChartThunkDispatch,
|
|
mockGetState as unknown as () => actions.RootState,
|
|
undefined,
|
|
).then(() => {
|
|
// chart update, trigger query, update form data, success
|
|
expect(dispatch.mock.calls.length).toBe(5);
|
|
expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1);
|
|
expect(dispatch.mock.calls[4][0].type).toBe(
|
|
actions.CHART_UPDATE_SUCCEEDED,
|
|
);
|
|
});
|
|
});
|
|
|
|
test('should dispatch CHART_UPDATE_FAILED action upon query timeout', () => {
|
|
const unresolvingPromise = new Promise(() => {});
|
|
fetchMock.removeRoute(MOCK_URL);
|
|
fetchMock.post(MOCK_URL, () => unresolvingPromise, {
|
|
name: MOCK_URL,
|
|
});
|
|
|
|
const timeoutInSec = 1 / 1000;
|
|
const actionThunk = actions.postChartFormData(
|
|
{} as QueryFormData,
|
|
false,
|
|
timeoutInSec,
|
|
);
|
|
|
|
return actionThunk(
|
|
dispatch as unknown as actions.ChartThunkDispatch,
|
|
mockGetState as unknown as () => actions.RootState,
|
|
undefined,
|
|
).then(() => {
|
|
// chart update, trigger query, update form data, fail
|
|
expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1);
|
|
expect(dispatch.mock.calls.length).toBe(5);
|
|
expect(dispatch.mock.calls[4][0].type).toBe(
|
|
actions.CHART_UPDATE_FAILED,
|
|
);
|
|
|
|
fetchMock.removeRoute(MOCK_URL);
|
|
setupDefaultFetchMock();
|
|
});
|
|
});
|
|
|
|
test('should dispatch CHART_UPDATE_FAILED action upon non-timeout non-abort failure', () => {
|
|
fetchMock.removeRoute(MOCK_URL);
|
|
fetchMock.post(
|
|
MOCK_URL,
|
|
{ throws: { statusText: 'misc error' } },
|
|
{ name: MOCK_URL },
|
|
);
|
|
|
|
const timeoutInSec = 100; // Set to a time that is longer than the time this will take to fail
|
|
const actionThunk = actions.postChartFormData(
|
|
{} as QueryFormData,
|
|
false,
|
|
timeoutInSec,
|
|
);
|
|
|
|
return actionThunk(
|
|
dispatch as unknown as actions.ChartThunkDispatch,
|
|
mockGetState as unknown as () => actions.RootState,
|
|
undefined,
|
|
).then(() => {
|
|
// chart update, trigger query, update form data, fail
|
|
expect(dispatch.mock.calls.length).toBe(5);
|
|
const updateFailedAction = dispatch.mock.calls[4][0];
|
|
expect(updateFailedAction.type).toBe(actions.CHART_UPDATE_FAILED);
|
|
expect(updateFailedAction.queriesResponse[0].error).toBe('misc error');
|
|
|
|
fetchMock.removeRoute(MOCK_URL);
|
|
setupDefaultFetchMock();
|
|
});
|
|
});
|
|
|
|
test('should dispatch CHART_UPDATE_STOPPED action upon abort', () => {
|
|
fetchMock.removeRoute(MOCK_URL);
|
|
fetchMock.post(
|
|
MOCK_URL,
|
|
{ throws: { name: 'AbortError' } },
|
|
{ name: MOCK_URL },
|
|
);
|
|
|
|
const timeoutInSec = 100;
|
|
const actionThunk = actions.postChartFormData(
|
|
{} as QueryFormData,
|
|
false,
|
|
timeoutInSec,
|
|
);
|
|
|
|
return actionThunk(
|
|
dispatch as unknown as actions.ChartThunkDispatch,
|
|
mockGetState as unknown as () => actions.RootState,
|
|
undefined,
|
|
).then(() => {
|
|
const types = dispatch.mock.calls
|
|
.map((call: [{ type?: string }]) => call[0] && call[0].type)
|
|
.filter(Boolean);
|
|
|
|
expect(types).toContain(actions.CHART_UPDATE_STOPPED);
|
|
expect(types).not.toContain(actions.CHART_UPDATE_FAILED);
|
|
|
|
fetchMock.removeRoutes();
|
|
setupDefaultFetchMock();
|
|
});
|
|
});
|
|
|
|
test('should handle the bigint without regression', async () => {
|
|
getExploreUrlStub.mockRestore();
|
|
const mockBigIntUrl = '/mock/chart/data/bigint';
|
|
const expectedBigNumber = '9223372036854775807';
|
|
fetchMock.post(mockBigIntUrl, `{ "value": ${expectedBigNumber} }`, {
|
|
name: mockBigIntUrl,
|
|
});
|
|
getExploreUrlStub = jest
|
|
.spyOn(exploreUtils, 'getExploreUrl')
|
|
.mockImplementation(() => mockBigIntUrl);
|
|
|
|
// Need viz_type to trigger the mocked getChartMetadataRegistry for legacy API
|
|
const { json } = await actions.getChartDataRequest({
|
|
formData: { ...fakeMetadata, viz_type: 'table' } as QueryFormData,
|
|
});
|
|
|
|
expect(fetchMock.callHistory.calls(mockBigIntUrl)).toHaveLength(1);
|
|
expect((json.result[0] as JsonObject).value.toString()).toEqual(
|
|
expectedBigNumber,
|
|
);
|
|
});
|
|
});
|
|
|
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
|
describe('runAnnotationQuery', () => {
|
|
const mockDispatch = jest.fn();
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
test('should dispatch annotationQueryStarted and annotationQuerySuccess on successful query', async () => {
|
|
const annotation = {
|
|
name: 'Holidays',
|
|
annotationType: AnnotationType.Event,
|
|
sourceType: AnnotationSourceType.Native,
|
|
color: null,
|
|
opacity: undefined,
|
|
style: AnnotationStyle.Solid,
|
|
width: 1,
|
|
showMarkers: false,
|
|
hideLine: false,
|
|
value: 1,
|
|
overrides: {
|
|
time_range: null,
|
|
},
|
|
show: true,
|
|
showLabel: false,
|
|
titleColumn: '',
|
|
descriptionColumns: [],
|
|
timeColumn: '',
|
|
intervalEndColumn: '',
|
|
} as AnnotationLayer;
|
|
const key = undefined;
|
|
|
|
const postSpy = jest.spyOn(SupersetClient, 'post');
|
|
postSpy.mockImplementation(
|
|
() =>
|
|
Promise.resolve({ json: { result: [] } }) as unknown as ReturnType<
|
|
typeof SupersetClient.post
|
|
>,
|
|
);
|
|
const buildV1ChartDataPayloadSpy = jest.spyOn(
|
|
exploreUtils,
|
|
'buildV1ChartDataPayload',
|
|
);
|
|
|
|
const queryFunc = actions.runAnnotationQuery({ annotation, key });
|
|
await queryFunc(
|
|
mockDispatch as unknown as actions.ChartThunkDispatch,
|
|
mockGetState as unknown as () => actions.RootState,
|
|
undefined,
|
|
);
|
|
|
|
expect(buildV1ChartDataPayloadSpy).toHaveBeenCalledWith({
|
|
formData: {
|
|
granularity: 'Date',
|
|
granularity_sqla: 'Date',
|
|
time_grain_sqla: 'P1D',
|
|
},
|
|
force: false,
|
|
resultFormat: 'json',
|
|
resultType: 'full',
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
|
describe('chart actions timeout', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
test('should use the timeout from arguments when given', async () => {
|
|
const postSpy = jest.spyOn(SupersetClient, 'post');
|
|
postSpy.mockImplementation(
|
|
() =>
|
|
Promise.resolve({ json: { result: [] } }) as unknown as ReturnType<
|
|
typeof SupersetClient.post
|
|
>,
|
|
);
|
|
const timeout = 10; // Set the timeout value here
|
|
const formData: Partial<QueryFormData> = { datasource: 'table__1' }; // Set the formData here
|
|
const key = 'chartKey'; // Set the chart key here
|
|
|
|
const store = mockStore(initialState);
|
|
await store.dispatch(
|
|
actions.runAnnotationQuery({
|
|
annotation: {
|
|
value: 'annotationValue',
|
|
sourceType: AnnotationSourceType.Native,
|
|
overrides: {},
|
|
} as unknown as AnnotationLayer,
|
|
timeout,
|
|
formData: formData as QueryFormData,
|
|
key,
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
}) as any,
|
|
);
|
|
|
|
const expectedPayload = {
|
|
url: expect.any(String) as string,
|
|
signal: expect.any(AbortSignal) as AbortSignal,
|
|
timeout: timeout * 1000,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
jsonPayload: expect.any(Object) as JsonObject,
|
|
};
|
|
|
|
expect(postSpy).toHaveBeenCalledWith(expectedPayload);
|
|
});
|
|
|
|
test('should use the timeout from common.conf when not passed as an argument', async () => {
|
|
const postSpy = jest.spyOn(SupersetClient, 'post');
|
|
postSpy.mockImplementation(
|
|
() =>
|
|
Promise.resolve({ json: { result: [] } }) as unknown as ReturnType<
|
|
typeof SupersetClient.post
|
|
>,
|
|
);
|
|
const formData: Partial<QueryFormData> = { datasource: 'table__1' }; // Set the formData here
|
|
const key = 'chartKey'; // Set the chart key here
|
|
|
|
const store = mockStore(initialState);
|
|
await store.dispatch(
|
|
actions.runAnnotationQuery({
|
|
annotation: {
|
|
value: 'annotationValue',
|
|
sourceType: AnnotationSourceType.Native,
|
|
overrides: {},
|
|
} as unknown as AnnotationLayer,
|
|
timeout: undefined,
|
|
formData: formData as QueryFormData,
|
|
key,
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
}) as any,
|
|
);
|
|
|
|
const expectedPayload = {
|
|
url: expect.any(String) as string,
|
|
signal: expect.any(AbortSignal) as AbortSignal,
|
|
timeout:
|
|
(
|
|
initialState.common.conf as unknown as {
|
|
SUPERSET_WEBSERVER_TIMEOUT: number;
|
|
}
|
|
).SUPERSET_WEBSERVER_TIMEOUT * 1000,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
jsonPayload: expect.any(Object) as JsonObject,
|
|
};
|
|
|
|
expect(postSpy).toHaveBeenCalledWith(expectedPayload);
|
|
});
|
|
});
|