This is an automated email from the ASF dual-hosted git repository. rusackas pushed a commit to branch chore/ts-migration-dashboard in repository https://gitbox.apache.org/repos/asf/superset.git
commit d54833bc9c916a670c8df078f63ad3209725a921 Author: Evan Rusackas <[email protected]> AuthorDate: Tue Feb 3 05:01:18 2026 +0100 refactor(frontend): migrate sinon to jest and remove sinon dependency Replace all sinon spies, stubs, and sandboxes with jest equivalents across 17 test files and 1 fixture file: - sinon.spy() → jest.fn() - sinon.spy(obj, 'method') → jest.spyOn(obj, 'method') - sinon.stub(obj, 'method').returns(val) → jest.spyOn(...).mockReturnValue(val) - sinon.createSandbox({ useFakeTimers }) → jest.useFakeTimers() - spy.callCount → spy.mock.calls.length - spy.getCall(n).args[0] → spy.mock.calls[n][0] Remove sinon and @types/sinon from devDependencies and jest.config.js. Co-Authored-By: Claude Opus 4.5 <[email protected]> --- superset-frontend/jest.config.js | 2 +- superset-frontend/package.json | 2 - .../src/SqlLab/actions/sqlLab.test.ts | 37 ++++--- superset-frontend/src/SqlLab/fixtures.ts | 5 +- .../src/components/Chart/chartActions.test.ts | 115 +++++++++++---------- .../ChangeDatasourceModal.test.tsx | 3 +- .../DatasourceModal/DatasourceModal.test.tsx | 3 +- .../src/dashboard/actions/dashboardLayout.test.ts | 108 +++++++++---------- .../src/dashboard/actions/dashboardState.test.ts | 61 ++++++----- .../ChartHolder/ChartHolder.test.tsx | 5 +- .../gridComponents/Divider/Divider.test.tsx | 6 +- .../gridComponents/Header/Header.test.tsx | 24 ++--- .../src/explore/actions/datasourcesActions.test.ts | 13 ++- .../src/explore/actions/saveModalActions.test.ts | 105 +++++++++---------- .../ExploreChartHeader/ExploreChartHeader.test.tsx | 89 ++++++++-------- ...AdhocFilterEditPopoverSimpleTabContent.test.tsx | 108 ++++++++++++------- .../src/explore/exploreUtils/exploreUtils.test.tsx | 12 +-- .../reports/ReportModal/ReportModal.test.tsx | 5 +- superset-frontend/src/middleware/logger.test.ts | 50 ++++----- 19 files changed, 391 insertions(+), 362 deletions(-) diff --git a/superset-frontend/jest.config.js b/superset-frontend/jest.config.js index 75634bd9158..615323b9cb9 100644 --- a/superset-frontend/jest.config.js +++ b/superset-frontend/jest.config.js @@ -65,7 +65,7 @@ module.exports = { ], coverageReporters: ['lcov', 'json-summary', 'html', 'text'], transformIgnorePatterns: [ - 'node_modules/(?!d3-(array|interpolate|color|time|scale|time-format)|internmap|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|@rjsf/*.|sinon|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast [...] + 'node_modules/(?!d3-(array|interpolate|color|time|scale|time-format)|internmap|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|@rjsf/*.|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|re [...] ], preset: 'ts-jest', transform: { diff --git a/superset-frontend/package.json b/superset-frontend/package.json index c651fccdfb9..61113fe9d81 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -285,7 +285,6 @@ "@types/redux-localstorage": "^1.0.8", "@types/redux-mock-store": "^1.0.6", "@types/rison": "0.1.0", - "@types/sinon": "^17.0.3", "@types/tinycolor2": "^1.4.3", "@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/parser": "^7.18.0", @@ -346,7 +345,6 @@ "react-refresh": "^0.18.0", "react-resizable": "^3.1.3", "redux-mock-store": "^1.5.4", - "sinon": "^18.0.0", "source-map": "^0.7.6", "source-map-support": "^0.5.21", "speed-measure-webpack-plugin": "^1.5.0", diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.test.ts b/superset-frontend/src/SqlLab/actions/sqlLab.test.ts index e9fab1d19ff..40de79a0cad 100644 --- a/superset-frontend/src/SqlLab/actions/sqlLab.test.ts +++ b/superset-frontend/src/SqlLab/actions/sqlLab.test.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import sinon from 'sinon'; import fetchMock from 'fetch-mock'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; @@ -96,12 +95,12 @@ describe('async actions', () => { name: 'Untitled Query 1', }; - let dispatch: sinon.SinonSpy; + let dispatch: jest.Mock; const fetchQueryEndpoint = 'glob:*/api/v1/sqllab/results/*'; const runQueryEndpoint = 'glob:*/api/v1/sqllab/execute/'; beforeEach(() => { - dispatch = sinon.spy(); + dispatch = jest.fn(); fetchMock.removeRoute(fetchQueryEndpoint); fetchMock.get( fetchQueryEndpoint, @@ -158,7 +157,7 @@ describe('async actions', () => { expect.assertions(1); return makeRequest().then(() => { - expect(dispatch.callCount).toBe(2); + expect(dispatch.mock.calls.length).toBe(2); }); }); @@ -166,7 +165,7 @@ describe('async actions', () => { expect.assertions(1); return makeRequest().then(() => { - expect(dispatch.args[0][0].type).toBe(actions.QUERY_EDITOR_SAVED); + expect(dispatch.mock.calls[0][0].type).toBe(actions.QUERY_EDITOR_SAVED); }); }); @@ -440,17 +439,21 @@ describe('async actions', () => { expect.assertions(1); return makeRequest().then(() => { - expect(dispatch.args[0][0].type).toBe(actions.REQUEST_QUERY_RESULTS); + expect(dispatch.mock.calls[0][0].type).toBe( + actions.REQUEST_QUERY_RESULTS, + ); }); }); test.skip('parses large number result without losing precision', () => makeRequest().then(() => { expect(fetchMock.callHistory.calls(fetchQueryEndpoint)).toHaveLength(1); - expect(dispatch.callCount).toBe(2); - expect(dispatch.getCall(1).lastArg.results.data.toString()).toBe( - mockBigNumber, - ); + 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', () => { @@ -510,17 +513,19 @@ describe('async actions', () => { expect.assertions(1); return makeRequest().then(() => { - expect(dispatch.args[0][0].type).toBe(actions.START_QUERY); + 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.callCount).toBe(2); - expect(dispatch.getCall(1).lastArg.results.data.toString()).toBe( - mockBigNumber, - ); + 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', () => { @@ -647,7 +652,7 @@ describe('async actions', () => { expect.assertions(1); return makeRequest().then(() => { - expect(dispatch.getCall(0).args[0].type).toBe(actions.STOP_QUERY); + expect(dispatch.mock.calls[0][0].type).toBe(actions.STOP_QUERY); }); }); diff --git a/superset-frontend/src/SqlLab/fixtures.ts b/superset-frontend/src/SqlLab/fixtures.ts index 77dd482fb8b..e03ecebb162 100644 --- a/superset-frontend/src/SqlLab/fixtures.ts +++ b/superset-frontend/src/SqlLab/fixtures.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import sinon from 'sinon'; import * as actions from 'src/SqlLab/actions/sqlLab'; import { ColumnKeyTypeType } from 'src/SqlLab/components/ColumnElement'; import { @@ -30,7 +29,9 @@ import { GenericDataType } from '@apache-superset/core/api/core'; import { LatestQueryEditorVersion } from 'src/SqlLab/types'; import { ISaveableDatasource } from 'src/SqlLab/components/SaveDatasetModal'; -export const mockedActions = sinon.stub({ ...actions }); +export const mockedActions = Object.fromEntries( + Object.keys(actions).map(key => [key, jest.fn()]), +); export const alert = { bsStyle: 'danger', msg: 'Oops', id: 'lksvmcx32' }; export const table = { diff --git a/superset-frontend/src/components/Chart/chartActions.test.ts b/superset-frontend/src/components/Chart/chartActions.test.ts index 57967b1d46b..f82c7d03efe 100644 --- a/superset-frontend/src/components/Chart/chartActions.test.ts +++ b/superset-frontend/src/components/Chart/chartActions.test.ts @@ -18,7 +18,6 @@ */ import URI from 'urijs'; import fetchMock from 'fetch-mock'; -import sinon, { SinonSpy, SinonStub } from 'sinon'; import { FeatureFlag, @@ -95,11 +94,11 @@ const mockedGetChartBuildQueryRegistry = // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('chart actions', () => { const MOCK_URL = '/mockURL'; - let dispatch: SinonSpy; - let getExploreUrlStub: SinonStub; - let getChartDataUriStub: SinonStub; - let buildV1ChartDataPayloadStub: SinonStub; - let waitForAsyncDataStub: SinonStub; + 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(() => { @@ -117,18 +116,18 @@ describe('chart actions', () => { afterEach(() => fetchMock.clearHistory().removeRoutes()); beforeEach(() => { - dispatch = sinon.spy(); - getExploreUrlStub = sinon - .stub(exploreUtils, 'getExploreUrl') - .callsFake(() => MOCK_URL); - getChartDataUriStub = sinon - .stub(exploreUtils, 'getChartDataUri') - .callsFake(({ qs }: { qs?: Record<string, unknown> }) => + 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 = sinon - .stub(exploreUtils, 'buildV1ChartDataPayload') - .resolves({ + buildV1ChartDataPayloadStub = jest + .spyOn(exploreUtils, 'buildV1ChartDataPayload') + .mockResolvedValue({ some_param: 'fake query!', result_type: 'full', result_format: 'json', @@ -152,9 +151,9 @@ describe('chart actions', () => { }), }) as unknown as ReturnType<typeof getChartBuildQueryRegistry>, ); - waitForAsyncDataStub = sinon - .stub(asyncEvent, 'waitForAsyncData') - .callsFake((data: unknown) => Promise.resolve(data)); + waitForAsyncDataStub = jest + .spyOn(asyncEvent, 'waitForAsyncData') + .mockImplementation((data: unknown) => Promise.resolve(data)); }); test.only('should defer abort of previous controller to avoid Redux state mutation', async () => { @@ -195,9 +194,9 @@ describe('chart actions', () => { .mockReturnValue({ type: 'UPDATE_DATA_MASK' } as ReturnType< typeof dataMaskActions.updateDataMask >); - const getQuerySettingsStub = sinon - .stub(exploreUtils, 'getQuerySettings') - .returns([false, () => {}] as unknown as ReturnType< + const getQuerySettingsStub = jest + .spyOn(exploreUtils, 'getQuerySettings') + .mockReturnValue([false, () => {}] as unknown as ReturnType< typeof exploreUtils.getQuerySettings >); @@ -227,18 +226,18 @@ describe('chart actions', () => { getChartDataRequestSpy.mockRestore(); handleChartDataResponseSpy.mockRestore(); updateDataMaskSpy.mockRestore(); - getQuerySettingsStub.restore(); + getQuerySettingsStub.mockRestore(); abortSpy.mockRestore(); jest.useRealTimers(); } }); afterEach(() => { - getExploreUrlStub.restore(); - getChartDataUriStub.restore(); - buildV1ChartDataPayloadStub.restore(); + getExploreUrlStub.mockRestore(); + getChartDataUriStub.mockRestore(); + buildV1ChartDataPayloadStub.mockRestore(); fetchMock.clearHistory(); - waitForAsyncDataStub.restore(); + waitForAsyncDataStub.mockRestore(); ( global as unknown as { featureFlags: Record<string, boolean> } @@ -274,19 +273,19 @@ describe('chart actions', () => { result_format: 'json', }), ); - expect(dispatch.args[0][0].type).toBe(actions.CHART_UPDATE_STARTED); + expect(dispatch.mock.calls[0][0].type).toBe(actions.CHART_UPDATE_STARTED); }); test('should handle the bigint without regression', async () => { - getChartDataUriStub.restore(); + getChartDataUriStub.mockRestore(); const mockBigIntUrl = '/mock/chart/data/bigint'; const expectedBigNumber = '9223372036854775807'; fetchMock.post(mockBigIntUrl, `{ "value": ${expectedBigNumber} }`, { name: mockBigIntUrl, }); - getChartDataUriStub = sinon - .stub(exploreUtils, 'getChartDataUri') - .callsFake(() => URI(mockBigIntUrl)); + getChartDataUriStub = jest + .spyOn(exploreUtils, 'getChartDataUri') + .mockImplementation(() => URI(mockBigIntUrl)); const { json } = await actions.getChartDataRequest({ formData: fakeMetadata as QueryFormData, @@ -360,9 +359,11 @@ describe('chart actions', () => { undefined, ).then(() => { // chart update, trigger query, update form data, success - expect(dispatch.callCount).toBe(5); + expect(dispatch.mock.calls.length).toBe(5); expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1); - expect(dispatch.args[0][0].type).toBe(actions.CHART_UPDATE_STARTED); + expect(dispatch.mock.calls[0][0].type).toBe( + actions.CHART_UPDATE_STARTED, + ); }); }); @@ -376,9 +377,9 @@ describe('chart actions', () => { undefined, ).then(() => { // chart update, trigger query, update form data, success - expect(dispatch.callCount).toBe(5); + expect(dispatch.mock.calls.length).toBe(5); expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1); - expect(dispatch.args[1][0].type).toBe(actions.TRIGGER_QUERY); + expect(dispatch.mock.calls[1][0].type).toBe(actions.TRIGGER_QUERY); }); }); @@ -392,9 +393,11 @@ describe('chart actions', () => { undefined, ).then(() => { // chart update, trigger query, update form data, success - expect(dispatch.callCount).toBe(5); + expect(dispatch.mock.calls.length).toBe(5); expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1); - expect(dispatch.args[2][0].type).toBe(actions.UPDATE_QUERY_FORM_DATA); + expect(dispatch.mock.calls[2][0].type).toBe( + actions.UPDATE_QUERY_FORM_DATA, + ); }); }); @@ -408,13 +411,13 @@ describe('chart actions', () => { undefined, ).then(() => { // chart update, trigger query, update form data, success - expect(dispatch.callCount).toBe(5); + expect(dispatch.mock.calls.length).toBe(5); expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1); - expect(typeof dispatch.args[3][0]).toBe('function'); + expect(typeof dispatch.mock.calls[3][0]).toBe('function'); - dispatch.args[3][0](dispatch); - expect(dispatch.callCount).toBe(6); - expect(dispatch.args[5][0].type).toBe(LOG_EVENT); + dispatch.mock.calls[3][0](dispatch); + expect(dispatch.mock.calls.length).toBe(6); + expect(dispatch.mock.calls[5][0].type).toBe(LOG_EVENT); }); }); @@ -429,9 +432,11 @@ describe('chart actions', () => { undefined, ).then(() => { // chart update, trigger query, update form data, success - expect(dispatch.callCount).toBe(5); + expect(dispatch.mock.calls.length).toBe(5); expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1); - expect(dispatch.args[4][0].type).toBe(actions.CHART_UPDATE_SUCCEEDED); + expect(dispatch.mock.calls[4][0].type).toBe( + actions.CHART_UPDATE_SUCCEEDED, + ); }); }); @@ -456,8 +461,10 @@ describe('chart actions', () => { ).then(() => { // chart update, trigger query, update form data, fail expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1); - expect(dispatch.callCount).toBe(5); - expect(dispatch.args[4][0].type).toBe(actions.CHART_UPDATE_FAILED); + expect(dispatch.mock.calls.length).toBe(5); + expect(dispatch.mock.calls[4][0].type).toBe( + actions.CHART_UPDATE_FAILED, + ); fetchMock.removeRoute(MOCK_URL); setupDefaultFetchMock(); @@ -485,8 +492,8 @@ describe('chart actions', () => { undefined, ).then(() => { // chart update, trigger query, update form data, fail - expect(dispatch.callCount).toBe(5); - const updateFailedAction = dispatch.args[4][0]; + 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'); @@ -515,7 +522,7 @@ describe('chart actions', () => { mockGetState as unknown as () => actions.RootState, undefined, ).then(() => { - const types = dispatch.args + const types = dispatch.mock.calls .map((call: [{ type?: string }]) => call[0] && call[0].type) .filter(Boolean); @@ -528,15 +535,15 @@ describe('chart actions', () => { }); test('should handle the bigint without regression', async () => { - getExploreUrlStub.restore(); + getExploreUrlStub.mockRestore(); const mockBigIntUrl = '/mock/chart/data/bigint'; const expectedBigNumber = '9223372036854775807'; fetchMock.post(mockBigIntUrl, `{ "value": ${expectedBigNumber} }`, { name: mockBigIntUrl, }); - getExploreUrlStub = sinon - .stub(exploreUtils, 'getExploreUrl') - .callsFake(() => mockBigIntUrl); + getExploreUrlStub = jest + .spyOn(exploreUtils, 'getExploreUrl') + .mockImplementation(() => mockBigIntUrl); // Need viz_type to trigger the mocked getChartMetadataRegistry for legacy API const { json } = await actions.getChartDataRequest({ diff --git a/superset-frontend/src/components/Datasource/ChangeDatasourceModal/ChangeDatasourceModal.test.tsx b/superset-frontend/src/components/Datasource/ChangeDatasourceModal/ChangeDatasourceModal.test.tsx index 2feac5ab8ff..adcff761be9 100644 --- a/superset-frontend/src/components/Datasource/ChangeDatasourceModal/ChangeDatasourceModal.test.tsx +++ b/superset-frontend/src/components/Datasource/ChangeDatasourceModal/ChangeDatasourceModal.test.tsx @@ -20,7 +20,6 @@ import { waitFor, render, fireEvent } from 'spec/helpers/testing-library'; import configureStore from 'redux-mock-store'; import fetchMock from 'fetch-mock'; import thunk from 'redux-thunk'; -import sinon from 'sinon'; import mockDatasource from 'spec/fixtures/mockDatasource'; import ChangeDatasourceModal from '.'; @@ -29,7 +28,7 @@ const store = mockStore({}); const mockedProps = { addDangerToast: () => {}, - onDatasourceSave: sinon.spy(), + onDatasourceSave: jest.fn(), onChange: () => {}, onHide: () => {}, show: true, diff --git a/superset-frontend/src/components/Datasource/DatasourceModal/DatasourceModal.test.tsx b/superset-frontend/src/components/Datasource/DatasourceModal/DatasourceModal.test.tsx index e19b728a3ab..816da881b91 100644 --- a/superset-frontend/src/components/Datasource/DatasourceModal/DatasourceModal.test.tsx +++ b/superset-frontend/src/components/Datasource/DatasourceModal/DatasourceModal.test.tsx @@ -26,7 +26,6 @@ import { defaultStore as store, } from 'spec/helpers/testing-library'; import fetchMock from 'fetch-mock'; -import sinon from 'sinon'; import { SupersetClient } from '@superset-ui/core'; import mockDatasource from 'spec/fixtures/mockDatasource'; import React from 'react'; @@ -51,7 +50,7 @@ const mockedProps = { onChange: () => {}, onHide: () => {}, show: true, - onDatasourceSave: sinon.spy(), + onDatasourceSave: jest.fn(), }; let container: HTMLElement; diff --git a/superset-frontend/src/dashboard/actions/dashboardLayout.test.ts b/superset-frontend/src/dashboard/actions/dashboardLayout.test.ts index 25696d511da..a61c1fb54f5 100644 --- a/superset-frontend/src/dashboard/actions/dashboardLayout.test.ts +++ b/superset-frontend/src/dashboard/actions/dashboardLayout.test.ts @@ -16,8 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import sinon from 'sinon'; - import { ActionCreators as UndoActionCreators } from 'redux-undo'; import { @@ -77,18 +75,20 @@ describe('dashboardLayout actions', () => { }, }; + let updateLayoutComponentsSpy: jest.SpyInstance; + function setup(stateOverrides: Record<string, unknown> = {}) { const state = { ...mockState, ...stateOverrides }; - const getState = sinon.spy(() => state) as unknown as GetState; - const dispatch = sinon.spy(); + const getState = jest.fn(() => state) as unknown as GetState; + const dispatch = jest.fn(); return { getState, dispatch, state }; } beforeEach(() => { - sinon.spy(dashboardFilters, 'updateLayoutComponents'); + updateLayoutComponentsSpy = jest.spyOn(dashboardFilters, 'updateLayoutComponents'); }); afterEach(() => { - (dashboardFilters.updateLayoutComponents as sinon.SinonSpy).restore(); + updateLayoutComponentsSpy.mockRestore(); }); // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks @@ -98,15 +98,15 @@ describe('dashboardLayout actions', () => { const nextComponents = { 1: {} }; const thunk = updateComponents(nextComponents); thunk(dispatch, getState); - expect(dispatch.callCount).toBe(1); - expect(dispatch.getCall(0).args[0]).toEqual({ + expect(dispatch.mock.calls.length).toBe(1); + expect(dispatch.mock.calls[0][0]).toEqual({ type: UPDATE_COMPONENTS, payload: { nextComponents }, }); // update component should not trigger action for dashboardFilters expect( - (dashboardFilters.updateLayoutComponents as sinon.SinonSpy).callCount, + updateLayoutComponentsSpy.mock.calls.length, ).toEqual(0); }); @@ -117,11 +117,11 @@ describe('dashboardLayout actions', () => { const nextComponents = { 1: {} }; const thunk = updateComponents(nextComponents); thunk(dispatch, getState); - expect(dispatch.getCall(1).args[0]).toEqual(setUnsavedChanges(true)); + expect(dispatch.mock.calls[1][0]).toEqual(setUnsavedChanges(true)); // update component should not trigger action for dashboardFilters expect( - (dashboardFilters.updateLayoutComponents as sinon.SinonSpy).callCount, + updateLayoutComponentsSpy.mock.calls.length, ).toEqual(0); }); }); @@ -132,14 +132,14 @@ describe('dashboardLayout actions', () => { const { getState, dispatch } = setup(); const thunk = deleteComponent('id', 'parentId'); thunk(dispatch, getState); - expect(dispatch.getCall(0).args[0]).toEqual({ + expect(dispatch.mock.calls[0][0]).toEqual({ type: DELETE_COMPONENT, payload: { id: 'id', parentId: 'parentId' }, }); // delete components should trigger action for dashboardFilters expect( - (dashboardFilters.updateLayoutComponents as sinon.SinonSpy).callCount, + updateLayoutComponentsSpy.mock.calls.length, ).toEqual(1); }); @@ -149,11 +149,11 @@ describe('dashboardLayout actions', () => { }); const thunk = deleteComponent('id', 'parentId'); thunk(dispatch, getState); - expect(dispatch.getCall(2).args[0]).toEqual(setUnsavedChanges(true)); + expect(dispatch.mock.calls[2][0]).toEqual(setUnsavedChanges(true)); // delete components should trigger action for dashboardFilters expect( - (dashboardFilters.updateLayoutComponents as sinon.SinonSpy).callCount, + updateLayoutComponentsSpy.mock.calls.length, ).toEqual(1); }); }); @@ -165,10 +165,10 @@ describe('dashboardLayout actions', () => { const thunk1 = updateDashboardTitle('new text'); thunk1(dispatch, getState); - const thunk2 = dispatch.getCall(0).args[0]; + const thunk2 = dispatch.mock.calls[0][0]; thunk2(dispatch, getState); - expect(dispatch.getCall(1).args[0]).toEqual({ + expect(dispatch.mock.calls[1][0]).toEqual({ type: UPDATE_COMPONENTS, payload: { nextComponents: { @@ -181,7 +181,7 @@ describe('dashboardLayout actions', () => { // update dashboard title should not trigger action for dashboardFilters expect( - (dashboardFilters.updateLayoutComponents as sinon.SinonSpy).callCount, + updateLayoutComponentsSpy.mock.calls.length, ).toEqual(0); }); }); @@ -193,14 +193,14 @@ describe('dashboardLayout actions', () => { const dropResult = {} as DropResult; const thunk = createTopLevelTabs(dropResult); thunk(dispatch, getState); - expect(dispatch.getCall(0).args[0]).toEqual({ + expect(dispatch.mock.calls[0][0]).toEqual({ type: CREATE_TOP_LEVEL_TABS, payload: { dropResult }, }); // create top level tabs should trigger action for dashboardFilters expect( - (dashboardFilters.updateLayoutComponents as sinon.SinonSpy).callCount, + updateLayoutComponentsSpy.mock.calls.length, ).toEqual(1); }); @@ -211,11 +211,11 @@ describe('dashboardLayout actions', () => { const dropResult = {} as DropResult; const thunk = createTopLevelTabs(dropResult); thunk(dispatch, getState); - expect(dispatch.getCall(2).args[0]).toEqual(setUnsavedChanges(true)); + expect(dispatch.mock.calls[2][0]).toEqual(setUnsavedChanges(true)); // create top level tabs should trigger action for dashboardFilters expect( - (dashboardFilters.updateLayoutComponents as sinon.SinonSpy).callCount, + updateLayoutComponentsSpy.mock.calls.length, ).toEqual(1); }); }); @@ -226,14 +226,14 @@ describe('dashboardLayout actions', () => { const { getState, dispatch } = setup(); const thunk = deleteTopLevelTabs(); thunk(dispatch, getState); - expect(dispatch.getCall(0).args[0]).toEqual({ + expect(dispatch.mock.calls[0][0]).toEqual({ type: DELETE_TOP_LEVEL_TABS, payload: {}, }); // delete top level tabs should trigger action for dashboardFilters expect( - (dashboardFilters.updateLayoutComponents as sinon.SinonSpy).callCount, + updateLayoutComponentsSpy.mock.calls.length, ).toEqual(1); }); @@ -243,11 +243,11 @@ describe('dashboardLayout actions', () => { }); const thunk = deleteTopLevelTabs(); thunk(dispatch, getState); - expect(dispatch.getCall(2).args[0]).toEqual(setUnsavedChanges(true)); + expect(dispatch.mock.calls[2][0]).toEqual(setUnsavedChanges(true)); // delete top level tabs should trigger action for dashboardFilters expect( - (dashboardFilters.updateLayoutComponents as sinon.SinonSpy).callCount, + updateLayoutComponentsSpy.mock.calls.length, ).toEqual(1); }); }); @@ -276,11 +276,11 @@ describe('dashboardLayout actions', () => { const thunk1 = resizeComponent({ id: '1', width: 10, height: 3 }); thunk1(dispatch, getState); - const thunk2 = dispatch.getCall(0).args[0]; + const thunk2 = dispatch.mock.calls[0][0]; thunk2(dispatch, getState); - expect(dispatch.callCount).toBe(2); - expect(dispatch.getCall(1).args[0]).toEqual({ + expect(dispatch.mock.calls.length).toBe(2); + expect(dispatch.mock.calls[1][0]).toEqual({ type: UPDATE_COMPONENTS, payload: { nextComponents: { @@ -296,7 +296,7 @@ describe('dashboardLayout actions', () => { }, }); - expect(dispatch.callCount).toBe(2); + expect(dispatch.mock.calls.length).toBe(2); }); test('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => { @@ -307,14 +307,14 @@ describe('dashboardLayout actions', () => { const thunk1 = resizeComponent({ id: '1', width: 10, height: 3 }); thunk1(dispatch, getState); - const thunk2 = dispatch.getCall(0).args[0]; + const thunk2 = dispatch.mock.calls[0][0]; thunk2(dispatch, getState); - expect(dispatch.callCount).toBe(3); + expect(dispatch.mock.calls.length).toBe(3); // resize components should not trigger action for dashboardFilters expect( - (dashboardFilters.updateLayoutComponents as sinon.SinonSpy).callCount, + updateLayoutComponentsSpy.mock.calls.length, ).toEqual(0); }); }); @@ -332,10 +332,10 @@ describe('dashboardLayout actions', () => { const handleComponentDropThunk = handleComponentDrop(dropResult); handleComponentDropThunk(dispatch, getState); - const createComponentThunk = dispatch.getCall(0).args[0]; + const createComponentThunk = dispatch.mock.calls[0][0]; createComponentThunk(dispatch, getState); - expect(dispatch.getCall(1).args[0]).toEqual({ + expect(dispatch.mock.calls[1][0]).toEqual({ type: CREATE_COMPONENT, payload: { dropResult, @@ -344,7 +344,7 @@ describe('dashboardLayout actions', () => { // create components should trigger action for dashboardFilters expect( - (dashboardFilters.updateLayoutComponents as sinon.SinonSpy).callCount, + updateLayoutComponentsSpy.mock.calls.length, ).toEqual(1); }); @@ -364,10 +364,10 @@ describe('dashboardLayout actions', () => { const handleComponentDropThunk = handleComponentDrop(dropResult); handleComponentDropThunk(dispatch, getState); - const moveComponentThunk = dispatch.getCall(0).args[0]; + const moveComponentThunk = dispatch.mock.calls[0][0]; moveComponentThunk(dispatch, getState); - expect(dispatch.getCall(1).args[0]).toEqual({ + expect(dispatch.mock.calls[1][0]).toEqual({ type: MOVE_COMPONENT, payload: { dropResult, @@ -376,7 +376,7 @@ describe('dashboardLayout actions', () => { // create components should trigger action for dashboardFilters expect( - (dashboardFilters.updateLayoutComponents as sinon.SinonSpy).callCount, + updateLayoutComponentsSpy.mock.calls.length, ).toEqual(1); }); @@ -404,9 +404,9 @@ describe('dashboardLayout actions', () => { const thunk = handleComponentDrop(dropResult); thunk(dispatch, getState); - expect(dispatch.getCall(0).args[0].type).toEqual(ADD_TOAST); + expect(dispatch.mock.calls[0][0].type).toEqual(ADD_TOAST); - expect(dispatch.callCount).toBe(1); + expect(dispatch.mock.calls.length).toBe(1); }); test('should delete a parent Row or Tabs if the moved child was the only child', () => { @@ -434,10 +434,10 @@ describe('dashboardLayout actions', () => { moveThunk(dispatch, getState); // first call is move action which is not a thunk - const deleteThunk = dispatch.getCall(1).args[0]; + const deleteThunk = dispatch.mock.calls[1][0]; deleteThunk(dispatch, getState); - expect(dispatch.getCall(2).args[0]).toEqual({ + expect(dispatch.mock.calls[2][0]).toEqual({ type: DELETE_COMPONENT, payload: { id: 'tabsId', @@ -457,10 +457,10 @@ describe('dashboardLayout actions', () => { const thunk1 = handleComponentDrop(dropResult); thunk1(dispatch, getState); - const thunk2 = dispatch.getCall(0).args[0]; + const thunk2 = dispatch.mock.calls[0][0]; thunk2(dispatch, getState); - expect(dispatch.getCall(1).args[0]).toEqual({ + expect(dispatch.mock.calls[1][0]).toEqual({ type: CREATE_TOP_LEVEL_TABS, payload: { dropResult, @@ -519,7 +519,7 @@ describe('dashboardLayout actions', () => { handleComponentDrop(dropResult)(dispatch, getState); - expect(dispatch.getCall(0).args[0].type).toEqual(ADD_TOAST); + expect(dispatch.mock.calls[0][0].type).toEqual(ADD_TOAST); }); }); @@ -532,8 +532,8 @@ describe('dashboardLayout actions', () => { const thunk = undoLayoutAction(); thunk(dispatch, getState); - expect(dispatch.callCount).toBe(1); - expect(dispatch.getCall(0).args[0]).toEqual(UndoActionCreators.undo()); + expect(dispatch.mock.calls.length).toBe(1); + expect(dispatch.mock.calls[0][0]).toEqual(UndoActionCreators.undo()); }); test('should dispatch a setUnsavedChanges(false) action history length is zero', () => { @@ -543,8 +543,8 @@ describe('dashboardLayout actions', () => { const thunk = undoLayoutAction(); thunk(dispatch, getState); - expect(dispatch.callCount).toBe(2); - expect(dispatch.getCall(1).args[0]).toEqual(setUnsavedChanges(false)); + expect(dispatch.mock.calls.length).toBe(2); + expect(dispatch.mock.calls[1][0]).toEqual(setUnsavedChanges(false)); }); }); @@ -555,11 +555,11 @@ describe('dashboardLayout actions', () => { const thunk = redoLayoutAction(); thunk(dispatch, getState); - expect(dispatch.getCall(0).args[0]).toEqual(UndoActionCreators.redo()); + expect(dispatch.mock.calls[0][0]).toEqual(UndoActionCreators.redo()); // redo/undo should trigger action for dashboardFilters expect( - (dashboardFilters.updateLayoutComponents as sinon.SinonSpy).callCount, + updateLayoutComponentsSpy.mock.calls.length, ).toEqual(1); }); @@ -569,11 +569,11 @@ describe('dashboardLayout actions', () => { }); const thunk = redoLayoutAction(); thunk(dispatch, getState); - expect(dispatch.getCall(2).args[0]).toEqual(setUnsavedChanges(true)); + expect(dispatch.mock.calls[2][0]).toEqual(setUnsavedChanges(true)); // redo/undo should trigger action for dashboardFilters expect( - (dashboardFilters.updateLayoutComponents as sinon.SinonSpy).callCount, + updateLayoutComponentsSpy.mock.calls.length, ).toEqual(1); }); }); diff --git a/superset-frontend/src/dashboard/actions/dashboardState.test.ts b/superset-frontend/src/dashboard/actions/dashboardState.test.ts index 853df56263a..7f8b3b53d4a 100644 --- a/superset-frontend/src/dashboard/actions/dashboardState.test.ts +++ b/superset-frontend/src/dashboard/actions/dashboardState.test.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import sinon from 'sinon'; import { SupersetClient, isFeatureEnabled } from '@superset-ui/core'; import { waitFor } from 'spec/helpers/testing-library'; @@ -76,16 +75,16 @@ describe('dashboardState actions', () => { }; const newDashboardData = mockDashboardData; - let postStub: sinon.SinonStub; - let getStub: sinon.SinonStub; - let putStub: sinon.SinonStub; + let postStub: jest.SpyInstance; + let getStub: jest.SpyInstance; + let putStub: jest.SpyInstance; const updatedCss = '.updated_css_value {\n color: black;\n}'; beforeEach(() => { - postStub = sinon - .stub(SupersetClient, 'post') - .resolves('the value you want to return' as any); - getStub = sinon.stub(SupersetClient, 'get').resolves({ + postStub = jest + .spyOn(SupersetClient, 'post') + .mockResolvedValue('the value you want to return' as any); + getStub = jest.spyOn(SupersetClient, 'get').mockResolvedValue({ json: { result: { ...mockDashboardData, @@ -93,22 +92,22 @@ describe('dashboardState actions', () => { }, }, } as any); - putStub = sinon.stub(SupersetClient, 'put').resolves({ + putStub = jest.spyOn(SupersetClient, 'put').mockResolvedValue({ json: { result: mockDashboardData, }, } as any); }); afterEach(() => { - postStub.restore(); - getStub.restore(); - putStub.restore(); + postStub.mockRestore(); + getStub.mockRestore(); + putStub.mockRestore(); }); function setup(stateOverrides: Record<string, unknown> = {}) { const state = { ...mockState, ...stateOverrides }; - const getState = sinon.spy(() => state) as unknown as () => any; - const dispatch = sinon.stub(); + const getState = jest.fn(() => state) as unknown as () => any; + const dispatch = jest.fn(); return { getState, dispatch, state }; } @@ -120,11 +119,11 @@ describe('dashboardState actions', () => { }); const thunk = saveDashboardRequest(newDashboardData, 1, 'save_dash'); thunk(dispatch, getState); - expect(dispatch.callCount).toBe(2); - expect(dispatch.getCall(0).args[0].type).toBe( + expect(dispatch.mock.calls.length).toBe(2); + expect(dispatch.mock.calls[0][0].type).toBe( UPDATE_COMPONENTS_PARENTS_LIST, ); - expect(dispatch.getCall(1).args[0].type).toBe(SAVE_DASHBOARD_STARTED); + expect(dispatch.mock.calls[1][0].type).toBe(SAVE_DASHBOARD_STARTED); }); test('should post dashboard data with updated redux state', () => { @@ -139,7 +138,7 @@ describe('dashboardState actions', () => { // mock redux work: dispatch an event, cause modify redux state const mockParentsList = ['ROOT_ID']; - dispatch.callsFake(() => { + dispatch.mockImplementation(() => { (mockState.dashboardLayout.present[DASHBOARD_GRID_ID] as any).parents = mockParentsList; }); @@ -148,8 +147,8 @@ describe('dashboardState actions', () => { // layout object (with parents attribute) const thunk = saveDashboardRequest(newDashboardData, 1, 'save_dash'); thunk(dispatch, getState); - expect(postStub.callCount).toBe(1); - const { jsonPayload } = postStub.getCall(0).args[0]; + expect(postStub.mock.calls.length).toBe(1); + const { jsonPayload } = postStub.mock.calls[0][0]; const parsedJsonMetadata = JSON.parse(jsonPayload.json_metadata); expect( parsedJsonMetadata.positions[DASHBOARD_GRID_ID].parents, @@ -177,13 +176,13 @@ describe('dashboardState actions', () => { SAVE_TYPE_OVERWRITE, ); thunk(dispatch, getState); - expect(getStub.callCount).toBe(1); - expect(postStub.callCount).toBe(0); + expect(getStub.mock.calls.length).toBe(1); + expect(postStub.mock.calls.length).toBe(0); await waitFor(() => - expect(dispatch.getCall(2).args[0].type).toBe(SET_OVERRIDE_CONFIRM), + expect(dispatch.mock.calls[2][0].type).toBe(SET_OVERRIDE_CONFIRM), ); expect( - dispatch.getCall(2).args[0].overwriteConfirmMetadata.dashboardId, + dispatch.mock.calls[2][0].overwriteConfirmMetadata.dashboardId, ).toBe(id); }); @@ -200,10 +199,10 @@ describe('dashboardState actions', () => { SAVE_TYPE_OVERWRITE_CONFIRMED, ); thunk(dispatch, getState); - expect(getStub.callCount).toBe(0); - expect(postStub.callCount).toBe(0); - await waitFor(() => expect(putStub.callCount).toBe(1)); - const { body } = putStub.getCall(0).args[0]; + expect(getStub.mock.calls.length).toBe(0); + expect(postStub.mock.calls.length).toBe(0); + await waitFor(() => expect(putStub.mock.calls.length).toBe(1)); + const { body } = putStub.mock.calls[0][0]; expect(body).toBe(JSON.stringify(confirmedDashboardData)); }); }); @@ -214,8 +213,8 @@ describe('dashboardState actions', () => { dashboardState: { hasUnsavedChanges: true }, }); - postStub.restore(); - postStub = sinon.stub(SupersetClient, 'post').resolves({ + postStub.mockRestore(); + postStub = jest.spyOn(SupersetClient, 'post').mockResolvedValue({ json: { result: { ...mockDashboardData, @@ -231,7 +230,7 @@ describe('dashboardState actions', () => { ); await thunk(dispatch, getState); - await waitFor(() => expect(postStub.callCount).toBe(1)); + await waitFor(() => expect(postStub.mock.calls.length).toBe(1)); expect(mockNavigateTo).toHaveBeenCalledWith( `/superset/dashboard/${newDashboardId}/`, ); diff --git a/superset-frontend/src/dashboard/components/gridComponents/ChartHolder/ChartHolder.test.tsx b/superset-frontend/src/dashboard/components/gridComponents/ChartHolder/ChartHolder.test.tsx index f9787affad7..20a6d2622f2 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/ChartHolder/ChartHolder.test.tsx +++ b/superset-frontend/src/dashboard/components/gridComponents/ChartHolder/ChartHolder.test.tsx @@ -20,7 +20,6 @@ import { combineReducers, createStore, applyMiddleware, compose } from 'redux'; import { Provider } from 'react-redux'; import thunk from 'redux-thunk'; -import sinon from 'sinon'; import mockState from 'spec/fixtures/mockState'; import reducerIndex from 'spec/helpers/reducerIndex'; import { sliceId as chartId } from 'spec/fixtures/mockChartQueries'; @@ -399,7 +398,7 @@ describe('ChartHolder', () => { }); test('should call deleteComponent when deleted', async () => { - const deleteComponent = sinon.spy(); + const deleteComponent = jest.fn(); const store = createMockStore(); const { rerender } = renderWrapper(store, { editMode: false, @@ -433,6 +432,6 @@ describe('ChartHolder', () => { screen.getByTestId('dashboard-delete-component-button') .firstElementChild!, ); - expect(deleteComponent.callCount).toBe(1); + expect(deleteComponent).toHaveBeenCalledTimes(1); }); }); diff --git a/superset-frontend/src/dashboard/components/gridComponents/Divider/Divider.test.tsx b/superset-frontend/src/dashboard/components/gridComponents/Divider/Divider.test.tsx index 7657d8ea9bc..356f8077d3c 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Divider/Divider.test.tsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Divider/Divider.test.tsx @@ -16,8 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import sinon from 'sinon'; - import newComponentFactory from 'src/dashboard/util/newComponentFactory'; import { DIVIDER_TYPE, @@ -71,9 +69,9 @@ describe('Divider', () => { }); test('should call deleteComponent when deleted', () => { - const deleteComponent = sinon.spy(); + const deleteComponent = jest.fn(); setup({ editMode: true, deleteComponent }); userEvent.click(screen.getByRole('button')); - expect(deleteComponent.callCount).toBe(1); + expect(deleteComponent).toHaveBeenCalledTimes(1); }); }); diff --git a/superset-frontend/src/dashboard/components/gridComponents/Header/Header.test.tsx b/superset-frontend/src/dashboard/components/gridComponents/Header/Header.test.tsx index 21aafeada07..64cf8aba18f 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Header/Header.test.tsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Header/Header.test.tsx @@ -19,8 +19,6 @@ import { Provider } from 'react-redux'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; -import sinon from 'sinon'; - import { render, screen, fireEvent } from 'spec/helpers/testing-library'; import newComponentFactory from 'src/dashboard/util/newComponentFactory'; import { @@ -45,8 +43,8 @@ describe('Header', () => { embeddedMode: boolean; filters: Record<string, any>; handleComponentDrop: () => void; - deleteComponent: sinon.SinonSpy; - updateComponents: sinon.SinonSpy; + deleteComponent: jest.Mock; + updateComponents: jest.Mock; } const baseComponent = newComponentFactory(HEADER_TYPE); @@ -69,8 +67,8 @@ describe('Header', () => { embeddedMode: false, filters: {}, handleComponentDrop: () => {}, - deleteComponent: sinon.spy(), - updateComponents: sinon.spy(), + deleteComponent: jest.fn(), + updateComponents: jest.fn(), }; function setup(overrideProps: Partial<HeaderTestProps> = {}) { @@ -84,8 +82,8 @@ describe('Header', () => { } beforeEach(() => { - if (props.deleteComponent) props.deleteComponent.resetHistory(); - if (props.updateComponents) props.updateComponents.resetHistory(); + if (props.deleteComponent) props.deleteComponent.mockClear(); + if (props.updateComponents) props.updateComponents.mockClear(); }); test('should render a Draggable', () => { @@ -115,7 +113,7 @@ describe('Header', () => { }); test('should call updateComponents when EditableTitle changes', () => { - const updateComponents = sinon.spy(); + const updateComponents = jest.fn(); setup({ editMode: true, updateComponents }); // First click to enter edit mode @@ -128,8 +126,8 @@ describe('Header', () => { fireEvent.blur(titleInput); const headerId = props.id; - expect(updateComponents.callCount).toBe(1); - const componentUpdates = updateComponents.getCall(0).args[0] as Record< + expect(updateComponents).toHaveBeenCalledTimes(1); + const componentUpdates = updateComponents.mock.calls[0][0] as Record< string, any >; @@ -143,13 +141,13 @@ describe('Header', () => { }); test('should call deleteComponent when deleted', () => { - const deleteComponent = sinon.spy(); + const deleteComponent = jest.fn(); setup({ editMode: true, deleteComponent }); const trashButton = screen.getByRole('button', { name: 'delete' }); fireEvent.click(trashButton); - expect(deleteComponent.callCount).toBe(1); + expect(deleteComponent).toHaveBeenCalledTimes(1); }); test('should render the AnchorLink in view mode', () => { diff --git a/superset-frontend/src/explore/actions/datasourcesActions.test.ts b/superset-frontend/src/explore/actions/datasourcesActions.test.ts index 4f2c29322a6..c82b6e758c7 100644 --- a/superset-frontend/src/explore/actions/datasourcesActions.test.ts +++ b/superset-frontend/src/explore/actions/datasourcesActions.test.ts @@ -23,7 +23,6 @@ import { changeDatasource, saveDataset, } from 'src/explore/actions/datasourcesActions'; -import sinon from 'sinon'; import datasourcesReducer from '../reducers/datasourcesReducer'; import { updateFormDataByDatasource } from './exploreActions'; @@ -112,15 +111,15 @@ test('saveDataset handles success', async () => { }; fetchMock.clearHistory().removeRoutes(); fetchMock.post(saveDatasetEndpoint, saveDatasetResponse); - const dispatch = sinon.spy(); - const getState = sinon.spy(() => ({ explore: { datasource } })); + const dispatch = jest.fn(); + const getState = jest.fn(() => ({ explore: { datasource } })); const dataset = await saveDataset(SAVE_DATASET_POST_ARGS)(dispatch); expect(fetchMock.callHistory.calls(saveDatasetEndpoint)).toHaveLength(1); - expect(dispatch.callCount).toBe(1); - const thunk = dispatch.getCall(0).args[0]; + expect(dispatch.mock.calls.length).toBe(1); + const thunk = dispatch.mock.calls[0][0]; thunk(dispatch, getState); - expect(dispatch.getCall(1).args[0].type).toEqual('SET_DATASOURCE'); + expect(dispatch.mock.calls[1][0].type).toEqual('SET_DATASOURCE'); expect(dataset).toEqual(datasource); }); @@ -132,7 +131,7 @@ test('updateSlice with add to existing dashboard handles failure', async () => { Promise.resolve(sampleError), ); fetchMock.post(saveDatasetEndpoint, { throws: sampleError }); - const dispatch = sinon.spy(); + const dispatch = jest.fn(); let caughtError; try { diff --git a/superset-frontend/src/explore/actions/saveModalActions.test.ts b/superset-frontend/src/explore/actions/saveModalActions.test.ts index 253afdddc10..9a6b667079c 100644 --- a/superset-frontend/src/explore/actions/saveModalActions.test.ts +++ b/superset-frontend/src/explore/actions/saveModalActions.test.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import sinon from 'sinon'; import fetchMock from 'fetch-mock'; import { Dispatch } from 'redux'; import { ADD_TOAST } from 'src/components/MessageToasts/actions'; @@ -108,7 +107,7 @@ test('updateSlice handles success', async () => { fetchMock.put(updateSliceEndpoint, sliceResponsePayload, { name: updateSliceEndpoint, }); - const dispatchSpy = sinon.spy(); + const dispatchSpy = jest.fn(); const dispatch = (action: any) => { dispatchSpy(action); }; @@ -140,13 +139,11 @@ test('updateSlice handles success', async () => { [], )(dispatch as Dispatch<any>, getState); expect(fetchMock.callHistory.calls(updateSliceEndpoint)).toHaveLength(1); - expect(dispatchSpy.callCount).toBe(2); - expect(dispatchSpy.getCall(0).args[0].type).toBe(SAVE_SLICE_SUCCESS); - expect(dispatchSpy.getCall(1).args[0].type).toBe('ADD_TOAST'); - expect(dispatchSpy.getCall(1).args[0].payload.toastType).toBe( - 'SUCCESS_TOAST', - ); - expect(dispatchSpy.getCall(1).args[0].payload.text).toBe( + expect(dispatchSpy.mock.calls.length).toBe(2); + expect(dispatchSpy.mock.calls[0][0].type).toBe(SAVE_SLICE_SUCCESS); + expect(dispatchSpy.mock.calls[1][0].type).toBe('ADD_TOAST'); + expect(dispatchSpy.mock.calls[1][0].payload.toastType).toBe('SUCCESS_TOAST'); + expect(dispatchSpy.mock.calls[1][0].payload.text).toBe( 'Chart [New chart] has been overwritten', ); expect(slice).toEqual(sliceResponsePayload); @@ -159,7 +156,7 @@ test('updateSlice handles failure', async () => { { name: updateSliceEndpoint }, ); - const dispatchSpy = sinon.spy(); + const dispatchSpy = jest.fn(); const dispatch = (action: any) => { dispatchSpy(action); }; @@ -199,8 +196,8 @@ test('updateSlice handles failure', async () => { expect(caughtError).toEqual(sampleError); expect(fetchMock.callHistory.calls(updateSliceEndpoint)).toHaveLength(4); - expect(dispatchSpy.callCount).toBe(1); - expect(dispatchSpy.getCall(0).args[0].type).toBe(SAVE_SLICE_FAILED); + expect(dispatchSpy.mock.calls.length).toBe(1); + expect(dispatchSpy.mock.calls[0][0].type).toBe(SAVE_SLICE_FAILED); }); /** @@ -211,7 +208,7 @@ test('createSlice handles success', async () => { fetchMock.post(createSliceEndpoint, sliceResponsePayload, { name: createSliceEndpoint, }); - const dispatchSpy = sinon.spy(); + const dispatchSpy = jest.fn(); const dispatch = (action: any) => dispatchSpy(action); const getState = () => mockExploreState; const slice: Partial<PayloadSlice> = await createSlice(sliceName, [])( @@ -219,13 +216,11 @@ test('createSlice handles success', async () => { getState, ); expect(fetchMock.callHistory.calls(createSliceEndpoint)).toHaveLength(1); - expect(dispatchSpy.callCount).toBe(2); - expect(dispatchSpy.getCall(0).args[0].type).toBe(SAVE_SLICE_SUCCESS); - expect(dispatchSpy.getCall(1).args[0].type).toBe(ADD_TOAST); - expect(dispatchSpy.getCall(1).args[0].payload.toastType).toBe( - 'SUCCESS_TOAST', - ); - expect(dispatchSpy.getCall(1).args[0].payload.text).toBe( + expect(dispatchSpy.mock.calls.length).toBe(2); + expect(dispatchSpy.mock.calls[0][0].type).toBe(SAVE_SLICE_SUCCESS); + expect(dispatchSpy.mock.calls[1][0].type).toBe(ADD_TOAST); + expect(dispatchSpy.mock.calls[1][0].payload.toastType).toBe('SUCCESS_TOAST'); + expect(dispatchSpy.mock.calls[1][0].payload.text).toBe( 'Chart [New chart] has been saved', ); @@ -235,7 +230,7 @@ test('createSlice handles success', async () => { test('createSlice handles failure', async () => { fetchMock.post(createSliceEndpoint, { throws: sampleError }); - const dispatchSpy = sinon.spy(); + const dispatchSpy = jest.fn(); const dispatch = (action: any) => dispatchSpy(action); const getState = () => mockExploreState; @@ -248,8 +243,8 @@ test('createSlice handles failure', async () => { expect(caughtError).toEqual(sampleError); expect(fetchMock.callHistory.calls(createSliceEndpoint)).toHaveLength(4); - expect(dispatchSpy.callCount).toBe(1); - expect(dispatchSpy.getCall(0).args[0].type).toBe(SAVE_SLICE_FAILED); + expect(dispatchSpy.mock.calls.length).toBe(1); + expect(dispatchSpy.mock.calls[0][0].type).toBe(SAVE_SLICE_FAILED); }); const dashboardName = 'New dashboard'; @@ -266,12 +261,12 @@ test('createDashboard handles success', async () => { fetchMock.post(createDashboardEndpoint, dashboardResponsePayload, { name: createDashboardEndpoint, }); - const dispatch = sinon.spy(); + const dispatch = jest.fn(); const dashboard = await createDashboard(dashboardName)( dispatch as Dispatch<any>, ); expect(fetchMock.callHistory.calls(createDashboardEndpoint)).toHaveLength(1); - expect(dispatch.callCount).toBe(0); + expect(dispatch.mock.calls.length).toBe(0); expect(dashboard).toEqual(dashboardResponsePayload); }); @@ -281,7 +276,7 @@ test('createDashboard handles failure', async () => { { throws: sampleError }, { name: createDashboardEndpoint }, ); - const dispatch = sinon.spy(); + const dispatch = jest.fn(); let caughtError; try { await createDashboard(dashboardName)(dispatch as Dispatch<any>); @@ -291,15 +286,15 @@ test('createDashboard handles failure', async () => { expect(caughtError).toEqual(sampleError); expect(fetchMock.callHistory.calls(createDashboardEndpoint)).toHaveLength(4); - expect(dispatch.callCount).toBe(1); - expect(dispatch.getCall(0).args[0].type).toBe(SAVE_SLICE_FAILED); + expect(dispatch.mock.calls.length).toBe(1); + expect(dispatch.mock.calls[0][0].type).toBe(SAVE_SLICE_FAILED); }); test('updateSlice with add to new dashboard handles success', async () => { fetchMock.put(updateSliceEndpoint, sliceResponsePayload, { name: updateSliceEndpoint, }); - const dispatchSpy = sinon.spy(); + const dispatchSpy = jest.fn(); const dispatch = (action: any) => dispatchSpy(action); const getState = () => mockExploreState; @@ -339,20 +334,16 @@ test('updateSlice with add to new dashboard handles success', async () => { )(dispatch as Dispatch<any>, getState); expect(fetchMock.callHistory.calls(updateSliceEndpoint)).toHaveLength(1); - expect(dispatchSpy.callCount).toBe(3); - expect(dispatchSpy.getCall(0).args[0].type).toBe(SAVE_SLICE_SUCCESS); - expect(dispatchSpy.getCall(1).args[0].type).toBe(ADD_TOAST); - expect(dispatchSpy.getCall(1).args[0].payload.toastType).toBe( - 'SUCCESS_TOAST', - ); - expect(dispatchSpy.getCall(1).args[0].payload.text).toBe( + expect(dispatchSpy.mock.calls.length).toBe(3); + expect(dispatchSpy.mock.calls[0][0].type).toBe(SAVE_SLICE_SUCCESS); + expect(dispatchSpy.mock.calls[1][0].type).toBe(ADD_TOAST); + expect(dispatchSpy.mock.calls[1][0].payload.toastType).toBe('SUCCESS_TOAST'); + expect(dispatchSpy.mock.calls[1][0].payload.text).toBe( 'Chart [New chart] has been overwritten', ); - expect(dispatchSpy.getCall(2).args[0].type).toBe(ADD_TOAST); - expect(dispatchSpy.getCall(2).args[0].payload.toastType).toBe( - 'SUCCESS_TOAST', - ); - expect(dispatchSpy.getCall(2).args[0].payload.text).toBe( + expect(dispatchSpy.mock.calls[2][0].type).toBe(ADD_TOAST); + expect(dispatchSpy.mock.calls[2][0].payload.toastType).toBe('SUCCESS_TOAST'); + expect(dispatchSpy.mock.calls[2][0].payload.text).toBe( 'Dashboard [New dashboard] just got created and chart [New chart] was added to it', ); @@ -363,7 +354,7 @@ test('updateSlice with add to existing dashboard handles success', async () => { fetchMock.put(updateSliceEndpoint, sliceResponsePayload, { name: updateSliceEndpoint, }); - const dispatchSpy = sinon.spy(); + const dispatchSpy = jest.fn(); const dispatch = (action: any) => dispatchSpy(action); const getState = () => mockExploreState; const slice = await updateSlice( @@ -402,20 +393,16 @@ test('updateSlice with add to existing dashboard handles success', async () => { )(dispatch as Dispatch<any>, getState); expect(fetchMock.callHistory.calls(updateSliceEndpoint)).toHaveLength(1); - expect(dispatchSpy.callCount).toBe(3); - expect(dispatchSpy.getCall(0).args[0].type).toBe(SAVE_SLICE_SUCCESS); - expect(dispatchSpy.getCall(1).args[0].type).toBe(ADD_TOAST); - expect(dispatchSpy.getCall(1).args[0].payload.toastType).toBe( - 'SUCCESS_TOAST', - ); - expect(dispatchSpy.getCall(1).args[0].payload.text).toBe( + expect(dispatchSpy.mock.calls.length).toBe(3); + expect(dispatchSpy.mock.calls[0][0].type).toBe(SAVE_SLICE_SUCCESS); + expect(dispatchSpy.mock.calls[1][0].type).toBe(ADD_TOAST); + expect(dispatchSpy.mock.calls[1][0].payload.toastType).toBe('SUCCESS_TOAST'); + expect(dispatchSpy.mock.calls[1][0].payload.text).toBe( 'Chart [New chart] has been overwritten', ); - expect(dispatchSpy.getCall(2).args[0].type).toBe(ADD_TOAST); - expect(dispatchSpy.getCall(2).args[0].payload.toastType).toBe( - 'SUCCESS_TOAST', - ); - expect(dispatchSpy.getCall(2).args[0].payload.text).toBe( + expect(dispatchSpy.mock.calls[2][0].type).toBe(ADD_TOAST); + expect(dispatchSpy.mock.calls[2][0].payload.toastType).toBe('SUCCESS_TOAST'); + expect(dispatchSpy.mock.calls[2][0].payload.text).toBe( 'Chart [New chart] was added to dashboard [New dashboard]', ); @@ -437,7 +424,7 @@ test('getSliceDashboards with slice handles success', async () => { fetchMock.get(getSliceDashboardsEndpoint, dashboardSlicesResponsePayload, { name: getSliceDashboardsEndpoint, }); - const dispatchSpy = sinon.spy(); + const dispatchSpy = jest.fn(); const dispatch = (action: any) => dispatchSpy(action); const sliceDashboards = await getSliceDashboards({ slice_id: 10, @@ -452,7 +439,7 @@ test('getSliceDashboards with slice handles success', async () => { expect(fetchMock.callHistory.calls(getSliceDashboardsEndpoint)).toHaveLength( 1, ); - expect(dispatchSpy.callCount).toBe(0); + expect(dispatchSpy.mock.calls.length).toBe(0); expect(sliceDashboards).toEqual(getDashboardSlicesReturnValue); }); @@ -462,7 +449,7 @@ test('getSliceDashboards with slice handles failure', async () => { { throws: sampleError }, { name: getSliceDashboardsEndpoint }, ); - const dispatch = sinon.spy(); + const dispatch = jest.fn(); let caughtError; try { await getSliceDashboards({ @@ -483,8 +470,8 @@ test('getSliceDashboards with slice handles failure', async () => { expect(fetchMock.callHistory.calls(getSliceDashboardsEndpoint)).toHaveLength( 4, ); - expect(dispatch.callCount).toBe(1); - expect(dispatch.getCall(0).args[0].type).toBe(SAVE_SLICE_FAILED); + expect(dispatch.mock.calls.length).toBe(1); + expect(dispatch.mock.calls[0][0].type).toBe(SAVE_SLICE_FAILED); }); // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks diff --git a/superset-frontend/src/explore/components/ExploreChartHeader/ExploreChartHeader.test.tsx b/superset-frontend/src/explore/components/ExploreChartHeader/ExploreChartHeader.test.tsx index c1911bf901a..487241fe3d6 100644 --- a/superset-frontend/src/explore/components/ExploreChartHeader/ExploreChartHeader.test.tsx +++ b/superset-frontend/src/explore/components/ExploreChartHeader/ExploreChartHeader.test.tsx @@ -17,7 +17,6 @@ * under the License. */ -import sinon from 'sinon'; import { render, screen, @@ -597,12 +596,12 @@ describe('Additional actions tests', () => { // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('Export All Data', () => { - let spyDownloadAsImage = sinon.spy(); - let spyExportChart = sinon.spy(); + let spyDownloadAsImage: jest.SpyInstance; + let spyExportChart: jest.SpyInstance; beforeEach(() => { - spyDownloadAsImage = sinon.spy(downloadAsImage, 'default'); - spyExportChart = sinon.spy(exploreUtils, 'exportChart'); + spyDownloadAsImage = jest.spyOn(downloadAsImage, 'default'); + spyExportChart = jest.spyOn(exploreUtils, 'exportChart'); (useUnsavedChangesPrompt as jest.Mock).mockReturnValue({ showModal: false, @@ -614,8 +613,8 @@ describe('Additional actions tests', () => { }); afterEach(async () => { - spyDownloadAsImage.restore(); - spyExportChart.restore(); + spyDownloadAsImage.mockRestore(); + spyExportChart.mockRestore(); // Wait for any pending effects to complete await new Promise(resolve => setTimeout(resolve, 0)); }); @@ -636,7 +635,7 @@ describe('Additional actions tests', () => { userEvent.click(downloadAsImageElement); await waitFor(() => { - expect(spyDownloadAsImage.callCount).toBe(1); + expect(spyDownloadAsImage.mock.calls.length).toBe(1); }); }); @@ -650,8 +649,8 @@ describe('Additional actions tests', () => { userEvent.hover(await screen.findByText('Export All Data')); const exportCSVElement = await screen.findByText('Export to .CSV'); userEvent.click(exportCSVElement); - expect(spyExportChart.callCount).toBe(0); - spyExportChart.restore(); + expect(spyExportChart.mock.calls.length).toBe(0); + spyExportChart.mockRestore(); }); test('Should export to CSV if canDownload=true', async () => { @@ -666,8 +665,8 @@ describe('Additional actions tests', () => { userEvent.hover(await screen.findByText('Export All Data')); const exportCSVElement = await screen.findByText('Export to .CSV'); userEvent.click(exportCSVElement); - expect(spyExportChart.callCount).toBe(1); - spyExportChart.restore(); + expect(spyExportChart.mock.calls.length).toBe(1); + spyExportChart.mockRestore(); }); test('Should not export to JSON if canDownload=false', async () => { @@ -680,8 +679,8 @@ describe('Additional actions tests', () => { userEvent.hover(await screen.findByText('Export All Data')); const exportJsonElement = await screen.findByText('Export to .JSON'); userEvent.click(exportJsonElement); - expect(spyExportChart.callCount).toBe(0); - spyExportChart.restore(); + expect(spyExportChart.mock.calls.length).toBe(0); + spyExportChart.mockRestore(); }); test('Should export to JSON if canDownload=true', async () => { @@ -696,7 +695,7 @@ describe('Additional actions tests', () => { userEvent.hover(await screen.findByText('Export All Data')); const exportJsonElement = await screen.findByText('Export to .JSON'); userEvent.click(exportJsonElement); - expect(spyExportChart.callCount).toBe(1); + expect(spyExportChart.mock.calls.length).toBe(1); }); test('Should not export to pivoted CSV if canDownloadCSV=false and viz_type=pivot_table_v2', async () => { @@ -713,7 +712,7 @@ describe('Additional actions tests', () => { 'Export to pivoted .CSV', ); userEvent.click(exportCSVElement); - expect(spyExportChart.callCount).toBe(0); + expect(spyExportChart.mock.calls.length).toBe(0); }); test('Should export to pivoted CSV if canDownloadCSV=true and viz_type=pivot_table_v2', async () => { @@ -731,7 +730,7 @@ describe('Additional actions tests', () => { 'Export to pivoted .CSV', ); userEvent.click(exportCSVElement); - expect(spyExportChart.callCount).toBe(1); + expect(spyExportChart.mock.calls.length).toBe(1); }); test('Should not export to Excel if canDownload=false', async () => { @@ -744,8 +743,8 @@ describe('Additional actions tests', () => { userEvent.hover(await screen.findByText('Export All Data')); const exportExcelElement = await screen.findByText('Export to Excel'); userEvent.click(exportExcelElement); - expect(spyExportChart.callCount).toBe(0); - spyExportChart.restore(); + expect(spyExportChart.mock.calls.length).toBe(0); + spyExportChart.mockRestore(); }); test('Should export to Excel if canDownload=true', async () => { @@ -759,13 +758,13 @@ describe('Additional actions tests', () => { userEvent.hover(await screen.findByText('Export All Data')); const exportExcelElement = await screen.findByText('Export to Excel'); userEvent.click(exportExcelElement); - expect(spyExportChart.callCount).toBe(1); + expect(spyExportChart.mock.calls.length).toBe(1); }); }); describe('Current View', () => { - let spyDownloadAsImage = sinon.spy(); - let spyExportChart = sinon.spy(); + let spyDownloadAsImage: jest.SpyInstance; + let spyExportChart: jest.SpyInstance; let originalURL: typeof URL; let anchorClickSpy: jest.SpyInstance; @@ -801,8 +800,8 @@ describe('Additional actions tests', () => { }); beforeEach(() => { - spyDownloadAsImage = sinon.spy(downloadAsImage, 'default'); - spyExportChart = sinon.spy(exploreUtils, 'exportChart'); + spyDownloadAsImage = jest.spyOn(downloadAsImage, 'default'); + spyExportChart = jest.spyOn(exploreUtils, 'exportChart'); (useUnsavedChangesPrompt as jest.Mock).mockReturnValue({ showModal: false, @@ -814,8 +813,8 @@ describe('Additional actions tests', () => { }); afterEach(async () => { - spyDownloadAsImage.restore(); - spyExportChart.restore(); + spyDownloadAsImage.mockRestore(); + spyExportChart.mockRestore(); await new Promise(r => setTimeout(r, 0)); }); @@ -831,14 +830,14 @@ describe('Additional actions tests', () => { userEvent.hover(await screen.findByText('Data Export Options')); userEvent.hover(await screen.findByText('Export Current View')); - // clear previous calls on the sinon spy you created in beforeEach - spyDownloadAsImage.resetHistory(); + // clear previous calls on the jest spy created in beforeEach + spyDownloadAsImage.mockClear(); const item = await screen.findByText('Export screenshot (jpeg)'); userEvent.click(item); await waitFor(() => { - expect(spyDownloadAsImage.called).toBe(true); + expect(spyDownloadAsImage).toHaveBeenCalled(); }); getSpy.mockRestore(); @@ -871,11 +870,11 @@ describe('Additional actions tests', () => { userEvent.hover(await screen.findByText('Data Export Options')); userEvent.hover(await screen.findByText('Export Current View')); - spyExportChart.resetHistory(); + spyExportChart.mockClear(); userEvent.click(await screen.findByText('Export to .CSV')); - expect(spyExportChart.called).toBe(false); // or: expect(spyExportChart.callCount).toBe(0) + expect(spyExportChart).not.toHaveBeenCalled(); getSpy.mockRestore(); }); @@ -901,10 +900,10 @@ describe('Additional actions tests', () => { userEvent.hover(await screen.findByText('Data Export Options')); userEvent.hover(await screen.findByText('Export Current View')); - spyExportChart.resetHistory(); + spyExportChart.mockClear(); userEvent.click(await screen.findByText('Export to .JSON')); - expect(spyExportChart.called).toBe(false); + expect(spyExportChart).not.toHaveBeenCalled(); getSpy.mockRestore(); }); @@ -923,11 +922,11 @@ describe('Additional actions tests', () => { userEvent.hover(await screen.findByText('Data Export Options')); userEvent.hover(await screen.findByText('Export Current View')); - spyExportChart.resetHistory(); + spyExportChart.mockClear(); userEvent.click(await screen.findByText('Export to .CSV')); - expect(spyExportChart.callCount).toBe(1); - const args = spyExportChart.getCall(0).args[0]; + expect(spyExportChart.mock.calls.length).toBe(1); + const args = spyExportChart.mock.calls[0][0]; expect(args.resultType).toBe('results'); expect(args.resultFormat).toBe('csv'); @@ -954,10 +953,10 @@ describe('Additional actions tests', () => { userEvent.hover(await screen.findByText('Data Export Options')); userEvent.hover(await screen.findByText('Export Current View')); - spyExportChart.resetHistory(); + spyExportChart.mockClear(); userEvent.click(await screen.findByText(/Export to (Excel|\.XLSX)/i)); - expect(spyExportChart.called).toBe(false); + expect(spyExportChart).not.toHaveBeenCalled(); getSpy.mockRestore(); }); @@ -974,11 +973,11 @@ describe('Additional actions tests', () => { userEvent.hover(await screen.findByText('Data Export Options')); userEvent.hover(await screen.findByText('Export Current View')); - spyExportChart.resetHistory(); + spyExportChart.mockClear(); userEvent.click(await screen.findByText(/Export to (Excel|\.XLSX)/i)); - expect(spyExportChart.callCount).toBe(1); - const args = spyExportChart.getCall(0).args[0]; + expect(spyExportChart.mock.calls.length).toBe(1); + const args = spyExportChart.mock.calls[0][0]; expect(args.resultType).toBe('results'); expect(args.resultFormat).toBe('xlsx'); getSpy.mockRestore(); @@ -1006,17 +1005,17 @@ describe('Additional actions tests', () => { userEvent.hover(await screen.findByText('Data Export Options')); userEvent.hover(await screen.findByText('Export Current View')); - // server path expected → use the sinon spy and inspect call args - spyExportChart.resetHistory(); + // server path expected - use the jest spy and inspect call args + spyExportChart.mockClear(); const jsonItem = await screen.findByText('Export to .JSON'); userEvent.click(jsonItem); await waitFor(() => { - expect(spyExportChart.callCount).toBe(1); + expect(spyExportChart.mock.calls.length).toBe(1); }); - const args = spyExportChart.getCall(0).args[0]; + const args = spyExportChart.mock.calls[0][0]; expect(args.resultType).toBe('results'); expect(args.resultFormat).toBe('json'); diff --git a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSimpleTabContent/AdhocFilterEditPopoverSimpleTabContent.test.tsx b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSimpleTabContent/AdhocFilterEditPopoverSimpleTabContent.test.tsx index f9b3e13a20b..1c0cb00f7e6 100644 --- a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSimpleTabContent/AdhocFilterEditPopoverSimpleTabContent.test.tsx +++ b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSimpleTabContent/AdhocFilterEditPopoverSimpleTabContent.test.tsx @@ -17,7 +17,6 @@ * under the License. */ import * as redux from 'react-redux'; -import sinon from 'sinon'; import { act, render, @@ -95,8 +94,8 @@ const options = [ ]; const getAdvancedDataTypeTestProps = (overrides?: Record<string, unknown>) => { - const onChange = sinon.spy(); - const validHandler = sinon.spy(); + const onChange = jest.fn(); + const validHandler = jest.fn(); const props = { adhocFilter: advancedTypeTestAdhocFilterTest, onChange, @@ -115,8 +114,8 @@ const getAdvancedDataTypeTestProps = (overrides?: Record<string, unknown>) => { }; function setup(overrides?: Record<string, unknown>) { - const onChange = sinon.spy(); - const validHandler = sinon.spy(); + const onChange = jest.fn(); + const validHandler = jest.fn(); const spy = jest.spyOn(redux, 'useSelector'); spy.mockReturnValue({}); const props = { @@ -259,9 +258,15 @@ test('will convert from individual comparator to array if the operator changes t props as unknown as Props, ); onOperatorChange(Operators.In); - expect(props.onChange.calledOnce).toBe(true); - expect(props.onChange.lastCall.args[0].comparator).toEqual(['10']); - expect(props.onChange.lastCall.args[0].operatorId).toEqual(Operators.In); + expect(props.onChange.mock.calls.length === 1).toBe(true); + expect( + props.onChange.mock.calls[props.onChange.mock.calls.length - 1][0] + .comparator, + ).toEqual(['10']); + expect( + props.onChange.mock.calls[props.onChange.mock.calls.length - 1][0] + .operatorId, + ).toEqual(Operators.In); }); test('will convert from array to individual comparators if the operator changes from multi', () => { @@ -272,8 +277,10 @@ test('will convert from array to individual comparators if the operator changes props as unknown as Props, ); onOperatorChange(Operators.LessThan); - expect(props.onChange.calledOnce).toBe(true); - expect(props.onChange.lastCall.args[0]).toEqual( + expect(props.onChange.mock.calls.length === 1).toBe(true); + expect( + props.onChange.mock.calls[props.onChange.mock.calls.length - 1][0], + ).toEqual( simpleMultiAdhocFilter.duplicateWith({ operatorId: Operators.LessThan, operator: '<', @@ -288,10 +295,10 @@ test('passes the new adhocFilter to onChange after onComparatorChange', () => { props as unknown as Props, ); onComparatorChange('20'); - expect(props.onChange.calledOnce).toBe(true); - expect(props.onChange.lastCall.args[0]).toEqual( - simpleAdhocFilter.duplicateWith({ comparator: '20' }), - ); + expect(props.onChange.mock.calls.length === 1).toBe(true); + expect( + props.onChange.mock.calls[props.onChange.mock.calls.length - 1][0], + ).toEqual(simpleAdhocFilter.duplicateWith({ comparator: '20' })); }); test('will filter operators for table datasources', () => { @@ -337,8 +344,10 @@ test('will generate custom sqlExpression for LATEST PARTITION operator', () => { props as unknown as Props, ); onOperatorChange(Operators.LatestPartition); - expect(props.onChange.calledOnce).toBe(true); - expect(props.onChange.lastCall.args[0]).toEqual( + expect(props.onChange.mock.calls.length === 1).toBe(true); + expect( + props.onChange.mock.calls[props.onChange.mock.calls.length - 1][0], + ).toEqual( testAdhocFilter.duplicateWith({ subject: 'ds', operator: 'LATEST PARTITION', @@ -400,10 +409,18 @@ test('sets comparator to undefined when operator is IS_TRUE', () => { props as unknown as Props, ); onOperatorChange(Operators.IsTrue); - expect(props.onChange.calledOnce).toBe(true); - expect(props.onChange.lastCall.args[0].operatorId).toBe(Operators.IsTrue); - expect(props.onChange.lastCall.args[0].operator).toBe('IS TRUE'); - expect(props.onChange.lastCall.args[0].comparator).toBe(undefined); + expect(props.onChange.mock.calls.length === 1).toBe(true); + expect( + props.onChange.mock.calls[props.onChange.mock.calls.length - 1][0] + .operatorId, + ).toBe(Operators.IsTrue); + expect( + props.onChange.mock.calls[props.onChange.mock.calls.length - 1][0].operator, + ).toBe('IS TRUE'); + expect( + props.onChange.mock.calls[props.onChange.mock.calls.length - 1][0] + .comparator, + ).toBe(undefined); }); test('sets comparator to undefined when operator is IS_FALSE', () => { @@ -412,10 +429,18 @@ test('sets comparator to undefined when operator is IS_FALSE', () => { props as unknown as Props, ); onOperatorChange(Operators.IsFalse); - expect(props.onChange.calledOnce).toBe(true); - expect(props.onChange.lastCall.args[0].operatorId).toBe(Operators.IsFalse); - expect(props.onChange.lastCall.args[0].operator).toBe('IS FALSE'); - expect(props.onChange.lastCall.args[0].comparator).toBe(undefined); + expect(props.onChange.mock.calls.length === 1).toBe(true); + expect( + props.onChange.mock.calls[props.onChange.mock.calls.length - 1][0] + .operatorId, + ).toBe(Operators.IsFalse); + expect( + props.onChange.mock.calls[props.onChange.mock.calls.length - 1][0].operator, + ).toBe('IS FALSE'); + expect( + props.onChange.mock.calls[props.onChange.mock.calls.length - 1][0] + .comparator, + ).toBe(undefined); }); test('sets comparator to undefined when operator is IS_NULL or IS_NOT_NULL', () => { @@ -425,12 +450,19 @@ test('sets comparator to undefined when operator is IS_NULL or IS_NOT_NULL', () ); [Operators.IsNull, Operators.IsNotNull].forEach(op => { onOperatorChange(op); - expect(props.onChange.called).toBe(true); - expect(props.onChange.lastCall.args[0].operatorId).toBe(op); - expect(props.onChange.lastCall.args[0].operator).toBe( - OPERATOR_ENUM_TO_OPERATOR_TYPE[op].operation, - ); - expect(props.onChange.lastCall.args[0].comparator).toBe(undefined); + expect(props.onChange.mock.calls.length > 0).toBe(true); + expect( + props.onChange.mock.calls[props.onChange.mock.calls.length - 1][0] + .operatorId, + ).toBe(op); + expect( + props.onChange.mock.calls[props.onChange.mock.calls.length - 1][0] + .operator, + ).toBe(OPERATOR_ENUM_TO_OPERATOR_TYPE[op].operation); + expect( + props.onChange.mock.calls[props.onChange.mock.calls.length - 1][0] + .comparator, + ).toBe(undefined); }); }); @@ -505,7 +537,9 @@ test('should call API when column has advanced data type', async () => { fetchMock.callHistory.calls(ADVANCED_DATA_TYPE_ENDPOINT_VALID), ).toHaveLength(1), ); - expect(props.validHandler.lastCall.args[0]).toBe(true); + expect( + props.validHandler.mock.calls[props.validHandler.mock.calls.length - 1][0], + ).toBe(true); }); test('save button should be disabled if error message from API is returned', async () => { @@ -547,7 +581,9 @@ test('save button should be disabled if error message from API is returned', asy fetchMock.callHistory.calls(ADVANCED_DATA_TYPE_ENDPOINT_INVALID), ).toHaveLength(1), ); - expect(props.validHandler.lastCall.args[0]).toBe(false); + expect( + props.validHandler.mock.calls[props.validHandler.mock.calls.length - 1][0], + ).toBe(false); }); test('advanced data type operator list should update after API response', async () => { @@ -589,7 +625,9 @@ test('advanced data type operator list should update after API response', async fetchMock.callHistory.calls(ADVANCED_DATA_TYPE_ENDPOINT_VALID), ).toHaveLength(1), ); - expect(props.validHandler.lastCall.args[0]).toBe(true); + expect( + props.validHandler.mock.calls[props.validHandler.mock.calls.length - 1][0], + ).toBe(true); const operatorValueField = screen.getByRole('combobox', { name: 'Select operator', @@ -609,8 +647,8 @@ test('advanced data type operator list should update after API response', async }); test('dropdown should remain open when clicked after filter is configured', async () => { - const onChange = sinon.spy(); - const validHandler = sinon.spy(); + const onChange = jest.fn(); + const validHandler = jest.fn(); const spy = jest.spyOn(redux, 'useSelector'); spy.mockReturnValue({}); diff --git a/superset-frontend/src/explore/exploreUtils/exploreUtils.test.tsx b/superset-frontend/src/explore/exploreUtils/exploreUtils.test.tsx index 5e3d9cfca3a..9b8069faf62 100644 --- a/superset-frontend/src/explore/exploreUtils/exploreUtils.test.tsx +++ b/superset-frontend/src/explore/exploreUtils/exploreUtils.test.tsx @@ -16,8 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import sinon from 'sinon'; - import URI from 'urijs'; import { buildV1ChartDataPayload, @@ -136,7 +134,7 @@ describe('exploreUtils', () => { // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('domain sharding', () => { - let stub: sinon.SinonStub; + let stub: jest.ReplaceProperty<typeof hostNamesConfig.availableDomains>; const availableDomains = [ 'http://localhost/', 'domain1.com', @@ -144,9 +142,11 @@ describe('exploreUtils', () => { 'domain3.com', ]; beforeEach(() => { - stub = sinon - .stub(hostNamesConfig, 'availableDomains') - .value(availableDomains); + stub = jest.replaceProperty( + hostNamesConfig, + 'availableDomains', + availableDomains, + ); }); afterEach(() => { stub.restore(); diff --git a/superset-frontend/src/features/reports/ReportModal/ReportModal.test.tsx b/superset-frontend/src/features/reports/ReportModal/ReportModal.test.tsx index c0c121528f9..c07381817b7 100644 --- a/superset-frontend/src/features/reports/ReportModal/ReportModal.test.tsx +++ b/superset-frontend/src/features/reports/ReportModal/ReportModal.test.tsx @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import sinon from 'sinon'; import fetchMock from 'fetch-mock'; import { render, @@ -115,7 +114,7 @@ describe('Email Report Modal', () => { let dispatch: any; beforeEach(async () => { - dispatch = sinon.spy(); + dispatch = jest.fn(); }); test('creates a new email report', async () => { @@ -163,7 +162,7 @@ describe('Email Report Modal', () => { expect(fetchMock.callHistory.lastCall()?.options?.body).toEqual( stringyReportValues, ); - expect(dispatch.callCount).toBe(2); + expect(dispatch).toHaveBeenCalledTimes(2); const reportCalls = fetchMock.callHistory.calls(REPORT_ENDPOINT); expect(reportCalls).toHaveLength(2); }); diff --git a/superset-frontend/src/middleware/logger.test.ts b/superset-frontend/src/middleware/logger.test.ts index 39cbfb396ea..5b989e21713 100644 --- a/superset-frontend/src/middleware/logger.test.ts +++ b/superset-frontend/src/middleware/logger.test.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import sinon, { SinonSpy, SinonStub } from 'sinon'; import { SupersetClient } from '@superset-ui/core'; import logger from 'src/middleware/loggerMiddleware'; import { LOG_EVENT } from 'src/logger/actions'; @@ -37,7 +36,7 @@ interface LogEventAction { // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('logger middleware', () => { const dashboardId = 123; - const next: SinonSpy = sinon.spy(); + const next: jest.Mock = jest.fn(); // Mock store with minimal state needed for tests const mockStore = { getState: () => ({ @@ -59,18 +58,23 @@ describe('logger middleware', () => { }, }; - const timeSandbox = sinon.createSandbox({ - useFakeTimers: true, + beforeAll(() => { + jest.useFakeTimers(); + }); + afterAll(() => { + jest.useRealTimers(); }); - let postStub: SinonStub; + let postStub: jest.SpyInstance; beforeEach(() => { - postStub = sinon.stub(SupersetClient, 'post'); + postStub = jest + .spyOn(SupersetClient, 'post') + .mockImplementation(() => undefined as any); }); afterEach(() => { - next.resetHistory(); - postStub.restore(); - timeSandbox.clock.reset(); + next.mockClear(); + postStub.mockRestore(); + jest.setSystemTime(0); }); test('should listen to LOG_EVENT action type', () => { @@ -81,16 +85,16 @@ describe('logger middleware', () => { }, }; (logger as Function)(mockStore)(next)(action1); - expect(next.callCount).toBe(1); + expect(next.mock.calls.length).toBe(1); }); test('should POST an event to /superset/log/ when called', () => { (logger as Function)(mockStore)(next)(action); - expect(next.callCount).toBe(0); + expect(next.mock.calls.length).toBe(0); - timeSandbox.clock.tick(2000); - expect(postStub.callCount).toBe(1); - expect(postStub.getCall(0).args[0].endpoint).toMatch('/superset/log/'); + jest.advanceTimersByTime(2000); + expect(postStub.mock.calls.length).toBe(1); + expect(postStub.mock.calls[0][0].endpoint).toMatch('/superset/log/'); }); test('should include ts, start_offset, event_name, impression_id, source, and source_id in every event', () => { @@ -110,11 +114,11 @@ describe('logger middleware', () => { eventData: { path: `/dashboard/${dashboardId}/` }, }, }); - timeSandbox.clock.tick(2000); + jest.advanceTimersByTime(2000); fetchLog(action); - timeSandbox.clock.tick(2000); - expect(postStub.callCount).toBe(2); - const { events } = postStub.getCall(1).args[0].postPayload; + jest.advanceTimersByTime(2000); + expect(postStub.mock.calls.length).toBe(2); + const { events } = postStub.mock.calls[1][0].postPayload; const mockEventdata = action.payload.eventData; const mockEventname = action.payload.eventName; expect(events[0]).toMatchObject({ @@ -142,10 +146,10 @@ describe('logger middleware', () => { (logger as Function)(mockStore)(next)(action); (logger as Function)(mockStore)(next)(action); (logger as Function)(mockStore)(next)(action); - timeSandbox.clock.tick(2000); + jest.advanceTimersByTime(2000); - expect(postStub.callCount).toBe(1); - expect(postStub.getCall(0).args[0].postPayload.events).toHaveLength(3); + expect(postStub.mock.calls.length).toBe(1); + expect(postStub.mock.calls[0][0].postPayload.events).toHaveLength(3); }); test('should use navigator.sendBeacon if it exists', () => { @@ -157,7 +161,7 @@ describe('logger middleware', () => { (logger as Function)(mockStore)(next)(action); expect(beaconMock.mock.calls.length).toBe(0); - timeSandbox.clock.tick(2000); + jest.advanceTimersByTime(2000); expect(beaconMock.mock.calls.length).toBe(1); const endpoint = beaconMock.mock.calls[0][0]; @@ -174,7 +178,7 @@ describe('logger middleware', () => { (logger as Function)(mockStore)(next)(action); expect(beaconMock.mock.calls.length).toBe(0); - timeSandbox.clock.tick(2000); + jest.advanceTimersByTime(2000); expect(beaconMock.mock.calls.length).toBe(1); const formData = beaconMock.mock.calls[0][1];
