| /** |
| * 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 * as toastActions from 'src/components/MessageToasts/actions'; |
| 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]); |
| }); |
| |
| test('dispatches addWarningToast when a query response includes a warning', async () => { |
| const warningMessage = |
| 'Results truncated to 1,000 rows due to memory constraints.'; |
| fetchMock.removeRoute(MOCK_URL); |
| fetchMock.post( |
| `glob:*${MOCK_URL}*`, |
| { result: [{ warning: warningMessage }] }, |
| { name: MOCK_URL }, |
| ); |
| const addWarningToastSpy = jest.spyOn(toastActions, 'addWarningToast'); |
| |
| const actionThunk = actions.postChartFormData( |
| { viz_type: 'my_viz' } as QueryFormData, |
| false, |
| undefined, |
| undefined, |
| ); |
| await actionThunk( |
| dispatch as unknown as actions.ChartThunkDispatch, |
| mockGetState as unknown as () => actions.RootState, |
| undefined, |
| ); |
| |
| expect(addWarningToastSpy).toHaveBeenCalledWith(warningMessage, { |
| noDuplicate: true, |
| }); |
| addWarningToastSpy.mockRestore(); |
| fetchMock.removeRoute(MOCK_URL); |
| setupDefaultFetchMock(); |
| }); |
| |
| test('does not dispatch addWarningToast when no query response has a warning', async () => { |
| const addWarningToastSpy = jest.spyOn(toastActions, 'addWarningToast'); |
| |
| const actionThunk = actions.postChartFormData( |
| { viz_type: 'my_viz' } as QueryFormData, |
| false, |
| undefined, |
| undefined, |
| ); |
| await actionThunk( |
| dispatch as unknown as actions.ChartThunkDispatch, |
| mockGetState as unknown as () => actions.RootState, |
| undefined, |
| ); |
| |
| expect(addWarningToastSpy).not.toHaveBeenCalled(); |
| addWarningToastSpy.mockRestore(); |
| }); |
| }); |
| |
| // 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); |
| }); |
| }); |