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];

Reply via email to