From 28f495fee34475cfd69bbe7f3d0c6ce731e3d55e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 03:13:20 +0000 Subject: [PATCH 1/5] chore(deps-dev): bump fetch-mock Bumps [fetch-mock](https://github.com/wheresrhys/fetch-mock/tree/HEAD/packages/fetch-mock) from 11.1.5 to 12.6.0. - [Release notes](https://github.com/wheresrhys/fetch-mock/releases) - [Changelog](https://github.com/wheresrhys/fetch-mock/blob/main/packages/fetch-mock/CHANGELOG.md) - [Commits](https://github.com/wheresrhys/fetch-mock/commits/fetch-mock-v12.6.0/packages/fetch-mock) --- updated-dependencies: - dependency-name: fetch-mock dependency-version: 12.6.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- superset-frontend/packages/superset-ui-core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset-frontend/packages/superset-ui-core/package.json b/superset-frontend/packages/superset-ui-core/package.json index b9a43b28186e..3df01054f482 100644 --- a/superset-frontend/packages/superset-ui-core/package.json +++ b/superset-frontend/packages/superset-ui-core/package.json @@ -82,7 +82,7 @@ "@types/prop-types": "^15.7.15", "@types/rison": "0.1.0", "@types/seedrandom": "^3.0.8", - "fetch-mock": "^11.1.4", + "fetch-mock": "^12.6.0", "jest-mock-console": "^2.0.0", "resize-observer-polyfill": "1.5.1", "timezone-mock": "1.3.6" From 25af9c4bcb03ee3c3e73945fe9122812b535a141 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Mon, 26 Jan 2026 16:05:19 -0800 Subject: [PATCH 2/5] chore(deps): update package-lock.json Regenerate lockfile after rebase on master to sync with fetch-mock upgrade. Co-Authored-By: Claude Opus 4.5 --- superset-frontend/package-lock.json | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 492016be4713..cfd73f6083be 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -63668,7 +63668,7 @@ "@types/react-table": "^7.7.20", "@types/rison": "0.1.0", "@types/seedrandom": "^3.0.8", - "fetch-mock": "^11.1.4", + "fetch-mock": "^12.6.0", "jest-mock-console": "^2.0.0", "resize-observer-polyfill": "1.5.1", "timezone-mock": "1.3.6" @@ -63822,6 +63822,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/superset-ui-core/node_modules/fetch-mock": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-12.6.0.tgz", + "integrity": "sha512-oAy0OqAvjAvduqCeWveBix7LLuDbARPqZZ8ERYtBcCURA3gy7EALA3XWq0tCNxsSg+RmmJqyaeeZlOCV9abv6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/glob-to-regexp": "^0.4.4", + "dequal": "^2.0.3", + "glob-to-regexp": "^0.4.1", + "regexparam": "^3.0.0" + }, + "engines": { + "node": ">=18.11.0" + } + }, "packages/superset-ui-core/node_modules/hast-util-parse-selector": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", From 6ccdf752b17e7089bb8a8d4191654039ae931cb4 Mon Sep 17 00:00:00 2001 From: hainenber Date: Tue, 27 Jan 2026 22:59:05 +0700 Subject: [PATCH 3/5] feat: refactor tests to work with `fetch-mock` v12 Signed-off-by: hainenber --- superset-frontend/package-lock.json | 20 +- superset-frontend/package.json | 2 +- .../ListViewCard/ImageLoader.test.tsx | 25 +- .../test/chart/clients/ChartClient.test.ts | 5 +- .../test/connection/SupersetClient.test.ts | 17 +- .../connection/SupersetClientClass.test.ts | 112 +++++---- .../test/connection/callApi/callApi.test.ts | 144 ++++++----- .../callApiAndParseWithTimeout.test.ts | 9 +- .../connection/callApi/parseResponse.test.ts | 17 +- .../api/legacy/getDatasourceMetadata.test.ts | 5 +- .../test/query/api/legacy/getFormData.test.ts | 5 +- .../test/query/api/v1/getChartData.test.ts | 6 +- .../test/query/api/v1/makeApi.test.ts | 20 +- .../time-comparison/fetchTimeRange.test.ts | 29 ++- .../spec/helpers/jsDomWithFetchAPI.ts | 1 + superset-frontend/spec/helpers/shim.tsx | 4 + .../src/SqlLab/actions/sqlLab.test.js | 229 +++++++++++------- .../AceEditorWrapper/useAnnotations.test.ts | 35 ++- .../AceEditorWrapper/useKeywords.test.ts | 6 +- .../EditorAutoSync/EditorAutoSync.test.tsx | 42 ++-- .../ExploreCtasResultsButton.test.tsx | 4 +- .../PopEditorTab/PopEditorTab.test.tsx | 14 +- .../QueryAutoRefresh.test.tsx | 6 +- .../QueryHistory/QueryHistory.test.tsx | 8 +- .../components/ResultSet/ResultSet.test.tsx | 14 +- .../ShareSqlLabQuery.test.tsx | 21 +- .../components/SqlEditor/SqlEditor.test.tsx | 18 +- .../SqlEditorLeftBar.test.tsx | 10 +- .../TabbedSqlEditors.test.tsx | 2 +- .../TableElement/TableElement.test.tsx | 58 +++-- .../TablePreview/TablePreview.test.tsx | 26 +- .../Chart/DrillBy/DrillByModal.test.tsx | 36 +-- .../Chart/DrillBy/DrillBySubmenu.test.tsx | 2 +- .../DrillDetail/DrillDetailPane.test.tsx | 2 +- .../src/components/Chart/chartActions.test.js | 51 ++-- .../DatabaseSelector.test.tsx | 41 ++-- .../ChangeDatasourceModal.test.jsx | 8 +- .../DatasourceModal/DatasourceModal.test.jsx | 26 +- .../DatasourceModal.useModal.test.tsx | 4 +- .../DatasetUsageTab/DatasetUsageTab.test.tsx | 4 +- .../tests/DatasourceEditor.test.tsx | 31 ++- .../tests/DatasourceEditor.test.utils.tsx | 29 ++- .../tests/DatasourceEditorCurrency.test.tsx | 9 +- .../ImportModal/ImportModal.test.tsx | 12 +- .../src/components/ListView/ListView.test.tsx | 3 - .../TableSelector/TableSelector.test.tsx | 2 +- .../src/components/Tag/utils.test.tsx | 24 +- .../OverwriteConfirmModal.test.tsx | 24 +- .../PropertiesModal/PropertiesModal.test.tsx | 2 +- .../URLShortLinkButton.test.tsx | 13 +- .../ShareMenuItems/ShareMenuItems.test.tsx | 12 +- .../ScopingModal/ScopingModal.test.tsx | 10 +- .../FilterBar/FilterBar.test.tsx | 25 +- .../FilterBarSettings.test.tsx | 4 +- .../FiltersConfigForm/ColumnSelect.test.tsx | 2 +- .../FiltersConfigForm/DatasetSelect.test.tsx | 6 +- .../actions/datasourcesActions.test.ts | 8 +- .../explore/actions/saveModalActions.test.ts | 81 ++++--- .../test/DataTablesPane.test.tsx | 14 +- .../test/ResultsPaneOnDashboard.test.tsx | 2 +- .../DataTablesPane/test/SamplesPane.test.tsx | 2 +- .../ExploreChartHeader.test.tsx | 3 - .../ExploreChartPanel.test.jsx | 6 +- .../ExploreViewContainer.test.tsx | 12 +- .../PropertiesModal/PropertiesModal.test.tsx | 2 +- .../src/explore/components/SaveModal.test.jsx | 22 +- .../AnnotationLayer.test.tsx | 6 +- .../DatasourceControl.test.tsx | 143 ++++------- ...FilterEditPopoverSimpleTabContent.test.tsx | 20 +- .../components/controls/ViewQuery.test.tsx | 61 ++--- .../controls/ViewQueryModal.test.tsx | 59 +++-- .../src/extensions/ExtensionsList.test.tsx | 16 +- .../src/extensions/ExtensionsManager.test.ts | 4 +- .../src/extensions/ExtensionsStartup.test.tsx | 4 +- .../features/alerts/AlertReportModal.test.tsx | 74 +++--- .../databases/DatabaseModal/index.test.tsx | 9 +- .../UploadDataModel/UploadDataModal.test.tsx | 19 +- .../EditDataset/EditDataset.test.tsx | 2 +- .../AddDataset/LeftPanel/LeftPanel.test.tsx | 6 +- .../useDatasetMetadataBar.test.tsx | 2 +- .../src/features/home/ChartTable.test.tsx | 4 +- .../src/features/home/DashboardTable.test.tsx | 8 +- .../src/features/home/RightMenu.test.tsx | 55 ++--- .../reports/ReportModal/ReportModal.test.tsx | 6 +- .../rls/RowLevelSecurityModal.test.tsx | 34 ++- .../src/features/tags/BulkTagModal.test.tsx | 2 +- .../src/features/themes/ThemeModal.test.tsx | 35 ++- .../src/features/themes/api.test.ts | 32 +-- .../src/hooks/apiResources/dashboards.test.ts | 2 +- .../apiResources/databaseFunctions.test.ts | 12 +- .../src/hooks/apiResources/queries.test.ts | 12 +- .../src/hooks/apiResources/queryApi.test.ts | 10 +- .../apiResources/queryValidations.test.ts | 16 +- .../src/hooks/apiResources/schemas.test.ts | 46 ++-- .../hooks/apiResources/sqlEditorTabs.test.ts | 18 +- .../src/hooks/apiResources/sqlLab.test.ts | 22 +- .../src/hooks/apiResources/tables.test.ts | 53 ++-- .../src/middleware/asyncEvent.test.ts | 38 +-- .../AlertReportList/AlertReportList.test.jsx | 14 +- .../AnnotationLayerList.test.jsx | 14 +- .../src/pages/Chart/Chart.test.tsx | 31 ++- .../ChartCreation/ChartCreation.test.tsx | 12 +- .../ChartList/ChartList.cardview.test.tsx | 4 +- .../ChartList/ChartList.listview.test.tsx | 33 +-- .../ChartList/ChartList.permissions.test.tsx | 22 +- .../src/pages/ChartList/ChartList.test.tsx | 64 +++-- .../pages/ChartList/ChartList.testHelpers.tsx | 30 ++- .../CssTemplateList/CssTemplateList.test.jsx | 14 +- .../DashboardList/DashboardList.test.jsx | 10 +- .../ExecutionLogList.test.tsx | 8 +- .../src/pages/GroupsList/GroupsList.test.tsx | 15 +- .../src/pages/Home/Home.test.tsx | 26 +- .../src/pages/RolesList/RolesList.test.tsx | 6 +- .../RowLevelSecurityList.test.tsx | 52 ++-- .../SavedQueryList/SavedQueryList.test.tsx | 61 +++-- .../src/pages/SqlLab/SqlLab.test.tsx | 10 +- .../src/pages/ThemeList/ThemeList.test.tsx | 4 +- .../src/pages/UserInfo/UserInfo.test.tsx | 6 +- .../UserRegistrations.test.tsx | 2 +- .../src/pages/UsersList/UsersList.test.tsx | 9 +- 120 files changed, 1520 insertions(+), 1205 deletions(-) diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index cfd73f6083be..3d86cb15592b 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -240,7 +240,7 @@ "eslint-plugin-storybook": "^0.8.0", "eslint-plugin-testing-library": "^7.15.4", "eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors", - "fetch-mock": "^11.1.5", + "fetch-mock": "12.5.5", "fork-ts-checker-webpack-plugin": "^9.1.0", "history": "^5.3.0", "html-webpack-plugin": "^5.6.6", @@ -32105,25 +32105,19 @@ } }, "node_modules/fetch-mock": { - "version": "11.1.5", - "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-11.1.5.tgz", - "integrity": "sha512-KHmZDnZ1ry0pCTrX4YG5DtThHi0MH+GNI9caESnzX/nMJBrvppUHMvLx47M0WY9oAtKOMiPfZDRpxhlHg89BOA==", + "version": "12.5.5", + "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-12.5.5.tgz", + "integrity": "sha512-yeziyj/AsbpaQJ91Z+FvQW3h02JK4VS5FHDCz+xiihgMUsXIuhgg/XjC9UEPNtOruucfTSR2uQWcjevDmSXSUQ==", "dev": true, "license": "MIT", "dependencies": { "@types/glob-to-regexp": "^0.4.4", "dequal": "^2.0.3", "glob-to-regexp": "^0.4.1", - "is-subset": "^0.1.1", "regexparam": "^3.0.0" }, "engines": { - "node": ">=8.0.0" - }, - "peerDependenciesMeta": { - "node-fetch": { - "optional": true - } + "node": ">=18.11.0" } }, "node_modules/fetch-retry": { @@ -36901,7 +36895,9 @@ "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", "integrity": "sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/is-symbol": { "version": "1.1.1", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 30ef0d1d6697..da5fde38abb5 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -321,7 +321,7 @@ "eslint-plugin-storybook": "^0.8.0", "eslint-plugin-testing-library": "^7.15.4", "eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors", - "fetch-mock": "^11.1.5", + "fetch-mock": "12.5.5", "fork-ts-checker-webpack-plugin": "^9.1.0", "history": "^5.3.0", "html-webpack-plugin": "^5.6.6", diff --git a/superset-frontend/packages/superset-ui-core/src/components/ListViewCard/ImageLoader.test.tsx b/superset-frontend/packages/superset-ui-core/src/components/ListViewCard/ImageLoader.test.tsx index abbc6c21cd5a..ad31e512f293 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/ListViewCard/ImageLoader.test.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/ListViewCard/ImageLoader.test.tsx @@ -24,12 +24,18 @@ import { ImageLoader, type BackgroundPosition } from './ImageLoader'; global.URL.createObjectURL = jest.fn(() => '/local_url'); const blob = new Blob([], { type: 'image/png' }); +beforeAll(() => { + fetchMock.mockGlobal(); +}); + +afterAll(() => { + fetchMock.hardReset(); +}); + fetchMock.get( - '/thumbnail', + 'glob:*/thumbnail', { body: blob, headers: { 'Content-Type': 'image/png' } }, - { - sendAsJson: false, - }, + { name: 'thumbnail' }, ); describe('ImageLoader', () => { @@ -44,7 +50,7 @@ describe('ImageLoader', () => { return render(); }; - afterEach(() => fetchMock.resetHistory()); + afterEach(() => fetchMock.clearHistory()); it('is a valid element', async () => { setup(); @@ -57,7 +63,7 @@ describe('ImageLoader', () => { 'src', '/fallback', ); - expect(fetchMock.calls(/thumbnail/)).toHaveLength(1); + expect(fetchMock.callHistory.calls(/thumbnail/)).toHaveLength(1); expect(global.URL.createObjectURL).toHaveBeenCalled(); expect(await screen.findByTestId('image-loader')).toHaveAttribute( 'src', @@ -66,13 +72,14 @@ describe('ImageLoader', () => { }); it('displays fallback image when response is not an image', async () => { - fetchMock.once('/thumbnail2', {}); - setup({ src: '/thumbnail2' }); + fetchMock.once('glob:*/thumbnail2', {}, { name: 'thumbnail2' }); + + setup({ src: 'glob:*/thumbnail2' }); expect(screen.getByTestId('image-loader')).toHaveAttribute( 'src', '/fallback', ); - expect(fetchMock.calls(/thumbnail2/)).toHaveLength(1); + expect(fetchMock.callHistory.calls(/thumbnail2/)).toHaveLength(1); expect(await screen.findByTestId('image-loader')).toHaveAttribute( 'src', '/fallback', diff --git a/superset-frontend/packages/superset-ui-core/test/chart/clients/ChartClient.test.ts b/superset-frontend/packages/superset-ui-core/test/chart/clients/ChartClient.test.ts index b1c879cc221e..17182227323a 100644 --- a/superset-frontend/packages/superset-ui-core/test/chart/clients/ChartClient.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/chart/clients/ChartClient.test.ts @@ -37,6 +37,9 @@ import { SliceIdAndOrFormData } from '../../../src/chart/clients/ChartClient'; configureTranslation(); +beforeAll(() => fetchMock.mockGlobal()); +afterAll(() => fetchMock.hardReset()); + describe('ChartClient', () => { let chartClient: ChartClient; @@ -50,7 +53,7 @@ describe('ChartClient', () => { chartClient = new ChartClient(); }); - afterEach(() => fetchMock.restore()); + afterEach(() => fetchMock.removeRoutes().clearHistory()); describe('new ChartClient(config)', () => { it('creates a client without argument', () => { diff --git a/superset-frontend/packages/superset-ui-core/test/connection/SupersetClient.test.ts b/superset-frontend/packages/superset-ui-core/test/connection/SupersetClient.test.ts index 7f29db6123bb..8174fb8ad433 100644 --- a/superset-frontend/packages/superset-ui-core/test/connection/SupersetClient.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/connection/SupersetClient.test.ts @@ -21,10 +21,13 @@ import fetchMock from 'fetch-mock'; import { SupersetClient, SupersetClientClass } from '@superset-ui/core'; import { LOGIN_GLOB } from './fixtures/constants'; +beforeAll(() => fetchMock.mockGlobal()); +afterAll(() => fetchMock.hardReset()); + describe('SupersetClient', () => { - beforeAll(() => fetchMock.get(LOGIN_GLOB, { result: '' })); + beforeAll(() => fetchMock.get(LOGIN_GLOB, { result: '1234' })); - afterAll(() => fetchMock.restore()); + afterAll(() => fetchMock.removeRoutes().clearHistory()); afterEach(() => SupersetClient.reset()); @@ -108,9 +111,11 @@ describe('SupersetClient', () => { mockDeleteUrl, ]; networkCalls.map((url: string) => - expect(fetchMock.calls(url)[0][1]?.headers).toStrictEqual({ - Accept: 'application/json', - 'X-CSRFToken': '1234', + expect( + fetchMock.callHistory.calls(url)[0].options?.headers, + ).toStrictEqual({ + accept: 'application/json', + 'x-csrftoken': '1234', }), ); @@ -137,6 +142,6 @@ describe('SupersetClient', () => { authenticatedSpy.mockRestore(); csrfSpy.mockRestore(); - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); }); }); diff --git a/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts b/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts index 6a778cdd35d6..d0d3858ccca9 100644 --- a/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts @@ -20,14 +20,15 @@ import fetchMock from 'fetch-mock'; import { SupersetClientClass, ClientConfig, CallApi } from '@superset-ui/core'; import { LOGIN_GLOB } from './fixtures/constants'; +beforeAll(() => fetchMock.mockGlobal()); +afterAll(() => fetchMock.hardReset()); + describe('SupersetClientClass', () => { beforeEach(() => { - fetchMock.reset(); - fetchMock.get(LOGIN_GLOB, { result: '' }); + fetchMock.clearHistory().removeRoutes(); + fetchMock.get(LOGIN_GLOB, { result: '' }, { name: LOGIN_GLOB }); }); - afterAll(() => fetchMock.restore()); - describe('new SupersetClientClass()', () => { it('fallback protocol to https when setting only host', () => { const client = new SupersetClientClass({ host: 'TEST-HOST' }); @@ -89,21 +90,22 @@ describe('SupersetClientClass', () => { }); describe('.init()', () => { - beforeEach(() => - fetchMock.get(LOGIN_GLOB, { result: 1234 }, { overwriteRoutes: true }), - ); - afterEach(() => fetchMock.reset()); + beforeEach(() => { + fetchMock.removeRoute(LOGIN_GLOB); + fetchMock.get(LOGIN_GLOB, { result: 1234 }, { name: LOGIN_GLOB }); + }); + afterEach(() => fetchMock.clearHistory().removeRoutes()); it('calls api/v1/security/csrf_token/ when init() is called if no CSRF token is passed', async () => { - expect.assertions(1); + // expect.assertions(1); await new SupersetClientClass().init(); - expect(fetchMock.calls(LOGIN_GLOB)).toHaveLength(1); + expect(fetchMock.callHistory.calls(LOGIN_GLOB)).toHaveLength(1); }); it('does NOT call api/v1/security/csrf_token/ when init() is called if a CSRF token is passed', async () => { expect.assertions(1); await new SupersetClientClass({ csrfToken: 'abc' }).init(); - expect(fetchMock.calls(LOGIN_GLOB)).toHaveLength(0); + expect(fetchMock.callHistory.calls(LOGIN_GLOB)).toHaveLength(0); }); it('calls api/v1/security/csrf_token/ when init(force=true) is called even if a CSRF token is passed', async () => { @@ -112,20 +114,19 @@ describe('SupersetClientClass', () => { const client = new SupersetClientClass({ csrfToken: initialToken }); await client.init(); - expect(fetchMock.calls(LOGIN_GLOB)).toHaveLength(0); + expect(fetchMock.callHistory.calls(LOGIN_GLOB)).toHaveLength(0); expect(client.csrfToken).toBe(initialToken); await client.init(true); - expect(fetchMock.calls(LOGIN_GLOB)).toHaveLength(1); + expect(fetchMock.callHistory.calls(LOGIN_GLOB)).toHaveLength(1); expect(client.csrfToken).not.toBe(initialToken); }); it('throws if api/v1/security/csrf_token/ returns an error', async () => { expect.assertions(1); const rejectError = { status: 403 }; - fetchMock.get(LOGIN_GLOB, () => Promise.reject(rejectError), { - overwriteRoutes: true, - }); + fetchMock.removeRoute(LOGIN_GLOB); + fetchMock.get(LOGIN_GLOB, { throws: rejectError }, { name: LOGIN_GLOB }); let error; try { @@ -141,7 +142,7 @@ describe('SupersetClientClass', () => { it('throws if api/v1/security/csrf_token/ does not return a token', async () => { expect.assertions(1); - fetchMock.get(LOGIN_GLOB, {}, { overwriteRoutes: true }); + fetchMock.modifyRoute(LOGIN_GLOB, { response: {} }); let error; try { @@ -157,9 +158,8 @@ describe('SupersetClientClass', () => { it('does not set csrfToken if response is not json', async () => { expect.assertions(1); - fetchMock.get(LOGIN_GLOB, '123', { - overwriteRoutes: true, - }); + fetchMock.removeRoute(LOGIN_GLOB); + fetchMock.get(LOGIN_GLOB, { response: '123' }, { name: LOGIN_GLOB }); let error; try { @@ -175,7 +175,7 @@ describe('SupersetClientClass', () => { }); describe('.isAuthenticated()', () => { - afterEach(() => fetchMock.reset()); + afterEach(() => fetchMock.clearHistory().removeRoutes()); it('returns true if there is a token and false if not', async () => { expect.assertions(2); @@ -227,9 +227,8 @@ describe('SupersetClientClass', () => { expect.assertions(4); const rejectValue = { status: 403 }; - fetchMock.get(LOGIN_GLOB, () => Promise.reject(rejectValue), { - overwriteRoutes: true, - }); + fetchMock.removeRoutes(); + fetchMock.get(LOGIN_GLOB, { throws: rejectValue }, { name: LOGIN_GLOB }); const client = new SupersetClientClass({}); let error; @@ -253,18 +252,22 @@ describe('SupersetClientClass', () => { } // reset + fetchMock.removeRoutes(); fetchMock.get( LOGIN_GLOB, { result: 1234 }, { - overwriteRoutes: true, + name: LOGIN_GLOB, }, ); }); }); describe('requests', () => { - afterEach(() => fetchMock.restore()); + afterEach(() => { + fetchMock.removeRoutes(); + fetchMock.clearHistory(); + }); const protocol = 'https:'; const host = 'host'; @@ -306,11 +309,11 @@ describe('SupersetClientClass', () => { await client.delete({ url: mockDeleteUrl }); await client.request({ url: mockRequestUrl, method: 'DELETE' }); - expect(fetchMock.calls(mockGetUrl)).toHaveLength(1); - expect(fetchMock.calls(mockPostUrl)).toHaveLength(1); - expect(fetchMock.calls(mockDeleteUrl)).toHaveLength(1); - expect(fetchMock.calls(mockPutUrl)).toHaveLength(1); - expect(fetchMock.calls(mockRequestUrl)).toHaveLength(1); + expect(fetchMock.callHistory.calls(mockGetUrl)).toHaveLength(1); + expect(fetchMock.callHistory.calls(mockPostUrl)).toHaveLength(1); + expect(fetchMock.callHistory.calls(mockDeleteUrl)).toHaveLength(1); + expect(fetchMock.callHistory.calls(mockPutUrl)).toHaveLength(1); + expect(fetchMock.callHistory.calls(mockRequestUrl)).toHaveLength(1); expect(authSpy).toHaveBeenCalledTimes(5); authSpy.mockRestore(); @@ -331,7 +334,8 @@ describe('SupersetClientClass', () => { await client.init(); await client.get({ url: mockGetUrl }); - const fetchRequest = fetchMock.calls(mockGetUrl)[0][1] as CallApi; + const fetchRequest = fetchMock.callHistory.calls(mockGetUrl)[0] + .options as CallApi; expect(fetchRequest.mode).toBe(clientConfig.mode); expect(fetchRequest.credentials).toBe(clientConfig.credentials); expect(fetchRequest.headers).toEqual( @@ -354,10 +358,11 @@ describe('SupersetClientClass', () => { await client.init(); await client.get({ url: mockGetUrl }); - const fetchRequest = fetchMock.calls(mockGetUrl)[0][1] as CallApi; + const fetchRequest = fetchMock.callHistory.calls(mockGetUrl)[0] + .options as CallApi; expect(fetchRequest.headers).toEqual( expect.objectContaining({ - guestTokenHeader: 'abc123', + guesttokenheader: 'abc123', }), ); }); @@ -370,10 +375,10 @@ describe('SupersetClientClass', () => { await client.init(); await client.get({ url: mockGetUrl }); - expect(fetchMock.calls(mockGetUrl)).toHaveLength(1); + expect(fetchMock.callHistory.calls(mockGetUrl)).toHaveLength(1); await client.get({ endpoint: mockGetEndpoint }); - expect(fetchMock.calls(mockGetUrl)).toHaveLength(2); + expect(fetchMock.callHistory.calls(mockGetUrl)).toHaveLength(2); }); it('supports parsing a response as text', async () => { @@ -384,7 +389,7 @@ describe('SupersetClientClass', () => { url: mockTextUrl, parseMethod: 'text', }); - expect(fetchMock.calls(mockTextUrl)).toHaveLength(1); + expect(fetchMock.callHistory.calls(mockTextUrl)).toHaveLength(1); expect(text).toBe(mockTextJsonResponse); }); @@ -409,7 +414,8 @@ describe('SupersetClientClass', () => { await client.init(); await client.get({ url: mockGetUrl, ...overrideConfig }); - const fetchRequest = fetchMock.calls(mockGetUrl)[0][1] as CallApi; + const fetchRequest = fetchMock.callHistory.calls(mockGetUrl)[0] + .options as CallApi; expect(fetchRequest.mode).toBe(overrideConfig.mode); expect(fetchRequest.credentials).toBe(overrideConfig.credentials); expect(fetchRequest.headers).toEqual( @@ -428,10 +434,10 @@ describe('SupersetClientClass', () => { await client.init(); await client.post({ url: mockPostUrl }); - expect(fetchMock.calls(mockPostUrl)).toHaveLength(1); + expect(fetchMock.callHistory.calls(mockPostUrl)).toHaveLength(1); await client.post({ endpoint: mockPostEndpoint }); - expect(fetchMock.calls(mockPostUrl)).toHaveLength(2); + expect(fetchMock.callHistory.calls(mockPostUrl)).toHaveLength(2); }); it('allows overriding host, headers, mode, and credentials per-request', async () => { @@ -454,7 +460,8 @@ describe('SupersetClientClass', () => { await client.init(); await client.post({ url: mockPostUrl, ...overrideConfig }); - const fetchRequest = fetchMock.calls(mockPostUrl)[0][1] as CallApi; + const fetchRequest = fetchMock.callHistory.calls(mockPostUrl)[0] + .options as CallApi; expect(fetchRequest.mode).toBe(overrideConfig.mode); expect(fetchRequest.credentials).toBe(overrideConfig.credentials); @@ -473,7 +480,7 @@ describe('SupersetClientClass', () => { url: mockTextUrl, parseMethod: 'text', }); - expect(fetchMock.calls(mockTextUrl)).toHaveLength(1); + expect(fetchMock.callHistory.calls(mockTextUrl)).toHaveLength(1); expect(text).toBe(mockTextJsonResponse); }); @@ -485,10 +492,11 @@ describe('SupersetClientClass', () => { await client.init(); await client.post({ url: mockPostUrl, postPayload }); - const fetchRequest = fetchMock.calls(mockPostUrl)[0][1] as CallApi; + const fetchRequest = fetchMock.callHistory.calls(mockPostUrl)[0] + .options as CallApi; const formData = fetchRequest.body as FormData; - expect(fetchMock.calls(mockPostUrl)).toHaveLength(1); + expect(fetchMock.callHistory.calls(mockPostUrl)).toHaveLength(1); Object.entries(postPayload).forEach(([key, value]) => { expect(formData.get(key)).toBe(JSON.stringify(value)); }); @@ -502,10 +510,11 @@ describe('SupersetClientClass', () => { await client.init(); await client.post({ url: mockPostUrl, postPayload, stringify: false }); - const fetchRequest = fetchMock.calls(mockPostUrl)[0][1] as CallApi; + const fetchRequest = fetchMock.callHistory.calls(mockPostUrl)[0] + .options as CallApi; const formData = fetchRequest.body as FormData; - expect(fetchMock.calls(mockPostUrl)).toHaveLength(1); + expect(fetchMock.callHistory.calls(mockPostUrl)).toHaveLength(1); Object.entries(postPayload).forEach(([key, value]) => { expect(formData.get(key)).toBe(String(value)); }); @@ -528,6 +537,7 @@ describe('SupersetClientClass', () => { // @ts-ignore window.location = { pathname: mockRequestPath, + // @ts-ignore search: mockRequestSearch, href: mockHref, }; @@ -535,9 +545,7 @@ describe('SupersetClientClass', () => { .spyOn(SupersetClientClass.prototype, 'ensureAuth') .mockImplementation(); const rejectValue = { status: 401 }; - fetchMock.get(mockRequestUrl, () => Promise.reject(rejectValue), { - overwriteRoutes: true, - }); + fetchMock.get(mockRequestUrl, () => Promise.reject(rejectValue)); }); afterEach(() => { @@ -563,10 +571,11 @@ describe('SupersetClientClass', () => { it('should not redirect again if already on login page', async () => { const client = new SupersetClientClass({}); - // @ts-expect-error + // @ts-ignore window.location = { href: '/login?next=something', pathname: '/login', + // @ts-ignore search: '?next=something', }; @@ -636,7 +645,8 @@ describe('SupersetClientClass', () => { let createElement: any; beforeEach(async () => { - fetchMock.get(LOGIN_GLOB, { result: 1234 }, { overwriteRoutes: true }); + fetchMock.removeRoute(LOGIN_GLOB); + fetchMock.get(LOGIN_GLOB, { result: 1234 }, { name: LOGIN_GLOB }); client = new SupersetClientClass({ protocol, host }); authSpy = jest.spyOn(SupersetClientClass.prototype, 'ensureAuth'); diff --git a/superset-frontend/packages/superset-ui-core/test/connection/callApi/callApi.test.ts b/superset-frontend/packages/superset-ui-core/test/connection/callApi/callApi.test.ts index 387b96575be5..2f222088d358 100644 --- a/superset-frontend/packages/superset-ui-core/test/connection/callApi/callApi.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/connection/callApi/callApi.test.ts @@ -29,14 +29,17 @@ const corruptObject = new BadObject(); /* @ts-expect-error */ BadObject.prototype.toString = undefined; -const mockGetUrl = '/mock/get/url'; -const mockPostUrl = '/mock/post/url'; -const mockPutUrl = '/mock/put/url'; -const mockPatchUrl = '/mock/patch/url'; -const mockCacheUrl = '/mock/cache/url'; -const mockNotFound = '/mock/notfound'; -const mockErrorUrl = '/mock/error/url'; -const mock503 = '/mock/503'; +beforeAll(() => fetchMock.mockGlobal()); +afterAll(() => fetchMock.hardReset()); + +const mockGetUrl = 'glob:*/mock/get/url'; +const mockPostUrl = 'glob:*/mock/post/url'; +const mockPutUrl = 'glob:*/mock/put/url'; +const mockPatchUrl = 'glob:*/mock/patch/url'; +const mockCacheUrl = 'glob:*/mock/cache/url'; +const mockNotFound = 'glob:*/mock/notfound'; +const mockErrorUrl = 'glob:*/mock/error/url'; +const mock503 = 'glob:*/mock/503'; const mockGetPayload = { get: 'payload' }; const mockPostPayload = { post: 'payload' }; @@ -50,20 +53,26 @@ const mockCachePayload = { const mockErrorPayload = { status: 500, statusText: 'Internal error' }; describe('callApi()', () => { - beforeAll(() => fetchMock.get(LOGIN_GLOB, { result: '1234' })); + beforeAll(() => { + fetchMock.mockGlobal(); + fetchMock.get(LOGIN_GLOB, { result: '1234' }); + }); beforeEach(() => { fetchMock.get(mockGetUrl, mockGetPayload); fetchMock.post(mockPostUrl, mockPostPayload); fetchMock.put(mockPutUrl, mockPutPayload); fetchMock.patch(mockPatchUrl, mockPatchPayload); - fetchMock.get(mockCacheUrl, mockCachePayload); + fetchMock.get(mockCacheUrl, mockCachePayload, { name: mockCacheUrl }); fetchMock.get(mockNotFound, { status: 404 }); fetchMock.get(mock503, { status: 503 }); fetchMock.get(mockErrorUrl, () => Promise.reject(mockErrorPayload)); }); - afterEach(() => fetchMock.reset()); + afterEach(() => { + fetchMock.clearHistory(); + fetchMock.removeRoutes(); + }); describe('request config', () => { it('calls the right url with the specified method', async () => { @@ -74,10 +83,10 @@ describe('callApi()', () => { callApi({ url: mockPutUrl, method: 'PUT' }), callApi({ url: mockPatchUrl, method: 'PATCH' }), ]); - expect(fetchMock.calls(mockGetUrl)).toHaveLength(1); - expect(fetchMock.calls(mockPostUrl)).toHaveLength(1); - expect(fetchMock.calls(mockPutUrl)).toHaveLength(1); - expect(fetchMock.calls(mockPatchUrl)).toHaveLength(1); + expect(fetchMock.callHistory.calls(mockGetUrl)).toHaveLength(1); + expect(fetchMock.callHistory.calls(mockPostUrl)).toHaveLength(1); + expect(fetchMock.callHistory.calls(mockPutUrl)).toHaveLength(1); + expect(fetchMock.callHistory.calls(mockPatchUrl)).toHaveLength(1); }); it('passes along mode, cache, credentials, headers, body, signal, and redirect parameters in the request', async () => { @@ -92,12 +101,11 @@ describe('callApi()', () => { }, redirect: 'follow', signal: undefined, - body: 'BODY', }; await callApi(mockRequest); - const calls = fetchMock.calls(mockGetUrl); - const fetchParams = calls[0][1] as RequestInit; + const calls = fetchMock.callHistory.calls(mockGetUrl); + const fetchParams = calls[0].options as RequestInit; expect(calls).toHaveLength(1); expect(fetchParams.mode).toBe(mockRequest.mode); expect(fetchParams.cache).toBe(mockRequest.cache); @@ -119,10 +127,10 @@ describe('callApi()', () => { const postPayload = { key: 'value', anotherKey: 1237 }; await callApi({ url: mockPostUrl, method: 'POST', postPayload }); - const calls = fetchMock.calls(mockPostUrl); + const calls = fetchMock.callHistory.calls(mockPostUrl); expect(calls).toHaveLength(1); - const fetchParams = calls[0][1] as RequestInit; + const fetchParams = calls[0].options as RequestInit; const body = fetchParams.body as FormData; Object.entries(postPayload).forEach(([key, value]) => { @@ -136,10 +144,10 @@ describe('callApi()', () => { const postPayload = { key: 'value', noValue: undefined }; await callApi({ url: mockPostUrl, method: 'POST', postPayload }); - const calls = fetchMock.calls(mockPostUrl); + const calls = fetchMock.callHistory.calls(mockPostUrl); expect(calls).toHaveLength(1); - const fetchParams = calls[0][1] as RequestInit; + const fetchParams = calls[0].options as RequestInit; const body = fetchParams.body as FormData; expect(body.get('key')).toBe(JSON.stringify(postPayload.key)); expect(body.get('noValue')).toBeNull(); @@ -167,13 +175,13 @@ describe('callApi()', () => { }), callApi({ url: mockPostUrl, method: 'POST', jsonPayload: postPayload }), ]); - const calls = fetchMock.calls(mockPostUrl); + const calls = fetchMock.callHistory.calls(mockPostUrl); expect(calls).toHaveLength(3); - const stringified = (calls[0][1] as RequestInit).body as FormData; - const unstringified = (calls[1][1] as RequestInit).body as FormData; + const stringified = (calls[0].options as RequestInit).body as FormData; + const unstringified = (calls[1].options as RequestInit).body as FormData; const jsonRequestBody = JSON.parse( - (calls[2][1] as RequestInit).body as string, + (calls[2].options as RequestInit).body as string, ) as JsonObject; Object.entries(postPayload).forEach(([key, value]) => { @@ -211,9 +219,9 @@ describe('callApi()', () => { stringify: false, }); - const calls = fetchMock.calls(mockPostUrl); + const calls = fetchMock.callHistory.calls(mockPostUrl); expect(calls).toHaveLength(1); - const unstringified = (calls[0][1] as RequestInit).body as FormData; + const unstringified = (calls[0].options as RequestInit).body as FormData; const hasCorruptKey = unstringified.has('corrupt'); expect(hasCorruptKey).toBeFalsy(); // When a corrupt attribute is encountered, a console.error call is made with info about the corrupt attribute @@ -228,10 +236,10 @@ describe('callApi()', () => { const postPayload = { key: 'value', anotherKey: 1237 }; await callApi({ url: mockPutUrl, method: 'PUT', postPayload }); - const calls = fetchMock.calls(mockPutUrl); + const calls = fetchMock.callHistory.calls(mockPutUrl); expect(calls).toHaveLength(1); - const fetchParams = calls[0][1] as RequestInit; + const fetchParams = calls[0].options as RequestInit; const body = fetchParams.body as FormData; Object.entries(postPayload).forEach(([key, value]) => { @@ -245,10 +253,10 @@ describe('callApi()', () => { const postPayload = { key: 'value', noValue: undefined }; await callApi({ url: mockPutUrl, method: 'PUT', postPayload }); - const calls = fetchMock.calls(mockPutUrl); + const calls = fetchMock.callHistory.calls(mockPutUrl); expect(calls).toHaveLength(1); - const fetchParams = calls[0][1] as RequestInit; + const fetchParams = calls[0].options as RequestInit; const body = fetchParams.body as FormData; expect(body.get('key')).toBe(JSON.stringify(postPayload.key)); expect(body.get('noValue')).toBeNull(); @@ -275,11 +283,11 @@ describe('callApi()', () => { stringify: false, }), ]); - const calls = fetchMock.calls(mockPutUrl); + const calls = fetchMock.callHistory.calls(mockPutUrl); expect(calls).toHaveLength(2); - const stringified = (calls[0][1] as RequestInit).body as FormData; - const unstringified = (calls[1][1] as RequestInit).body as FormData; + const stringified = (calls[0].options as RequestInit).body as FormData; + const unstringified = (calls[1].options as RequestInit).body as FormData; Object.entries(postPayload).forEach(([key, value]) => { expect(stringified.get(key)).toBe(JSON.stringify(value)); @@ -294,10 +302,10 @@ describe('callApi()', () => { const postPayload = { key: 'value', anotherKey: 1237 }; await callApi({ url: mockPatchUrl, method: 'PATCH', postPayload }); - const calls = fetchMock.calls(mockPatchUrl); + const calls = fetchMock.callHistory.calls(mockPatchUrl); expect(calls).toHaveLength(1); - const fetchParams = calls[0][1] as RequestInit; + const fetchParams = calls[0].options as RequestInit; const body = fetchParams.body as FormData; Object.entries(postPayload).forEach(([key, value]) => { @@ -311,10 +319,10 @@ describe('callApi()', () => { const postPayload = { key: 'value', noValue: undefined }; await callApi({ url: mockPatchUrl, method: 'PATCH', postPayload }); - const calls = fetchMock.calls(mockPatchUrl); + const calls = fetchMock.callHistory.calls(mockPatchUrl); expect(calls).toHaveLength(1); - const fetchParams = calls[0][1] as RequestInit; + const fetchParams = calls[0].options as RequestInit; const body = fetchParams.body as FormData; expect(body.get('key')).toBe(JSON.stringify(postPayload.key)); expect(body.get('noValue')).toBeNull(); @@ -341,11 +349,11 @@ describe('callApi()', () => { stringify: false, }), ]); - const calls = fetchMock.calls(mockPatchUrl); + const calls = fetchMock.callHistory.calls(mockPatchUrl); expect(calls).toHaveLength(2); - const stringified = (calls[0][1] as RequestInit).body as FormData; - const unstringified = (calls[1][1] as RequestInit).body as FormData; + const stringified = (calls[0].options as RequestInit).body as FormData; + const unstringified = (calls[1].options as RequestInit).body as FormData; Object.entries(postPayload).forEach(([key, value]) => { expect(stringified.get(key)).toBe(JSON.stringify(value)); @@ -373,7 +381,7 @@ describe('callApi()', () => { it('caches requests with ETags', async () => { expect.assertions(2); await callApi({ url: mockCacheUrl, method: 'GET' }); - const calls = fetchMock.calls(mockCacheUrl); + const calls = fetchMock.callHistory.calls(mockCacheUrl); expect(calls).toHaveLength(1); const supersetCache = await caches.open(constants.CACHE_KEY); const cachedResponse = await supersetCache.match(mockCacheUrl); @@ -385,7 +393,7 @@ describe('callApi()', () => { window.location.protocol = 'http:'; await callApi({ url: mockCacheUrl, method: 'GET' }); - const calls = fetchMock.calls(mockCacheUrl); + const calls = fetchMock.callHistory.calls(mockCacheUrl); expect(calls).toHaveLength(1); const supersetCache = await caches.open(constants.CACHE_KEY); @@ -399,7 +407,7 @@ describe('callApi()', () => { Object.defineProperty(constants, 'CACHE_AVAILABLE', { value: false }); const firstResponse = await callApi({ url: mockCacheUrl, method: 'GET' }); - let calls = fetchMock.calls(mockCacheUrl); + let calls = fetchMock.callHistory.calls(mockCacheUrl); expect(calls).toHaveLength(1); const firstBody = await firstResponse.text(); expect(firstBody).toEqual('BODY'); @@ -408,8 +416,8 @@ describe('callApi()', () => { url: mockCacheUrl, method: 'GET', }); - calls = fetchMock.calls(mockCacheUrl); - const fetchParams = calls[1][1] as RequestInit; + calls = fetchMock.callHistory.calls(mockCacheUrl); + const fetchParams = calls[1].options as RequestInit; expect(calls).toHaveLength(2); // second call should not have If-None-Match header expect(fetchParams.headers).toBeUndefined(); @@ -424,14 +432,14 @@ describe('callApi()', () => { expect.assertions(3); // first call sets the cache await callApi({ url: mockCacheUrl, method: 'GET' }); - let calls = fetchMock.calls(mockCacheUrl); + let calls = fetchMock.callHistory.calls(mockCacheUrl); expect(calls).toHaveLength(1); // second call sends the Etag in the If-None-Match header await callApi({ url: mockCacheUrl, method: 'GET' }); - calls = fetchMock.calls(mockCacheUrl); - const fetchParams = calls[1][1] as RequestInit; - const headers = { 'If-None-Match': 'etag' }; + calls = fetchMock.callHistory.calls(mockCacheUrl); + const fetchParams = calls[1].options as RequestInit; + const headers = { 'if-none-match': 'etag' }; expect(calls).toHaveLength(2); expect(fetchParams.headers).toEqual( expect.objectContaining(headers) as typeof fetchParams.headers, @@ -442,16 +450,16 @@ describe('callApi()', () => { expect.assertions(3); // first call sets the cache await callApi({ url: mockCacheUrl, method: 'GET' }); - expect(fetchMock.calls(mockCacheUrl)).toHaveLength(1); + expect(fetchMock.callHistory.calls(mockCacheUrl)).toHaveLength(1); // second call reuses the cached payload on a 304 const mockCachedPayload = { status: 304 }; - fetchMock.get(mockCacheUrl, mockCachedPayload, { overwriteRoutes: true }); + fetchMock.modifyRoute(mockCacheUrl, { response: mockCachedPayload }); const secondResponse = await callApi({ url: mockCacheUrl, method: 'GET', }); - expect(fetchMock.calls(mockCacheUrl)).toHaveLength(2); + expect(fetchMock.callHistory.calls(mockCacheUrl)).toHaveLength(2); const secondBody = await secondResponse.text(); expect(secondBody).toEqual('BODY'); }); @@ -461,7 +469,7 @@ describe('callApi()', () => { // this should never happen, since a 304 is only returned if we have // the cached response and sent the If-None-Match header - const mockUncachedUrl = '/mock/uncached/url'; + const mockUncachedUrl = 'glob:*/mock/uncached/url'; const mockCachedPayload = { status: 304 }; let error; fetchMock.get(mockUncachedUrl, mockCachedPayload); @@ -471,7 +479,7 @@ describe('callApi()', () => { } catch (err) { error = err; } finally { - const calls = fetchMock.calls(mockUncachedUrl); + const calls = fetchMock.callHistory.calls(mockUncachedUrl); expect(calls).toHaveLength(1); expect((error as { message: string }).message).toEqual( 'Received 304 but no content is cached!', @@ -483,7 +491,7 @@ describe('callApi()', () => { expect.assertions(3); const url = mockGetUrl; const response = await callApi({ url, method: 'GET' }); - const calls = fetchMock.calls(url); + const calls = fetchMock.callHistory.calls(url); expect(calls).toHaveLength(1); expect(response.status).toEqual(200); const body = await response.json(); @@ -494,7 +502,7 @@ describe('callApi()', () => { expect.assertions(2); const url = mockNotFound; const response = await callApi({ url, method: 'GET' }); - const calls = fetchMock.calls(url); + const calls = fetchMock.callHistory.calls(url); expect(calls).toHaveLength(1); expect(response.status).toEqual(404); }); @@ -513,7 +521,7 @@ describe('callApi()', () => { error = err; } finally { const err = error as { status: number; statusText: string }; - expect(fetchMock.calls(mockErrorUrl)).toHaveLength(4); + expect(fetchMock.callHistory.calls(mockErrorUrl)).toHaveLength(4); expect(err.status).toBe(mockErrorPayload.status); expect(err.statusText).toBe(mockErrorPayload.statusText); } @@ -531,7 +539,7 @@ describe('callApi()', () => { } catch (err) { error = err as { status: number; statusText: string }; } finally { - expect(fetchMock.calls(mockErrorUrl)).toHaveLength(1); + expect(fetchMock.callHistory.calls(mockErrorUrl)).toHaveLength(1); expect(error?.status).toBe(mockErrorPayload.status); expect(error?.statusText).toBe(mockErrorPayload.statusText); } @@ -545,7 +553,7 @@ describe('callApi()', () => { url, method: 'GET', }); - const calls = fetchMock.calls(url); + const calls = fetchMock.callHistory.calls(url); expect(calls).toHaveLength(4); expect(response.status).toEqual(503); }); @@ -581,7 +589,9 @@ describe('callApi()', () => { const result = await response.json(); expect(response.status).toEqual(200); expect(result).toEqual({ yes: 'ok' }); - expect(fetchMock.lastUrl()).toEqual(`http://localhost/get-search?abc=1`); + expect(fetchMock.callHistory.lastCall()?.url).toEqual( + `http://localhost/get-search?abc=1`, + ); }); it('should accept URLSearchParams', async () => { @@ -596,8 +606,10 @@ describe('callApi()', () => { method: 'POST', jsonPayload: { request: 'ok' }, }); - expect(fetchMock.lastUrl()).toEqual(`http://localhost/post-search?abc=1`); - expect(fetchMock.lastOptions()).toEqual( + expect(fetchMock.callHistory.lastCall()?.url).toEqual( + `http://localhost/post-search?abc=1`, + ); + expect(fetchMock.callHistory.lastCall()?.options).toEqual( expect.objectContaining({ body: JSON.stringify({ request: 'ok' }), }), @@ -634,7 +646,7 @@ describe('callApi()', () => { method: 'POST', postPayload: payload, }); - expect(fetchMock.lastOptions()?.body).toBe(payload); + expect(fetchMock.callHistory.lastCall()?.options.body).toBe(payload); }); it('should ignore "null" postPayload string', async () => { @@ -646,6 +658,6 @@ describe('callApi()', () => { method: 'POST', postPayload: 'null', }); - expect(fetchMock.lastOptions()?.body).toBeUndefined(); + expect(fetchMock.callHistory.lastCall()?.options.body).toBeUndefined(); }); }); diff --git a/superset-frontend/packages/superset-ui-core/test/connection/callApi/callApiAndParseWithTimeout.test.ts b/superset-frontend/packages/superset-ui-core/test/connection/callApi/callApiAndParseWithTimeout.test.ts index e0bf14e6c8ee..a31dd48d3489 100644 --- a/superset-frontend/packages/superset-ui-core/test/connection/callApi/callApiAndParseWithTimeout.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/connection/callApi/callApiAndParseWithTimeout.test.ts @@ -30,15 +30,16 @@ import { LOGIN_GLOB } from '../fixtures/constants'; const mockGetUrl = '/mock/get/url'; const mockGetPayload = { get: 'payload' }; +beforeAll(() => fetchMock.mockGlobal()); +afterAll(() => fetchMock.hardReset()); + describe('callApiAndParseWithTimeout()', () => { beforeAll(() => fetchMock.get(LOGIN_GLOB, { result: '1234' })); beforeEach(() => fetchMock.get(mockGetUrl, mockGetPayload)); - afterAll(() => fetchMock.restore()); - afterEach(() => { - fetchMock.reset(); + fetchMock.removeRoutes().clearHistory(); jest.useRealTimers(); }); @@ -108,7 +109,7 @@ describe('callApiAndParseWithTimeout()', () => { } catch (err) { error = err; } finally { - expect(fetchMock.calls(mockTimeoutUrl)).toHaveLength(1); + expect(fetchMock.callHistory.calls(mockTimeoutUrl)).toHaveLength(1); expect(error).toEqual({ error: 'Request timed out', statusText: 'timeout', diff --git a/superset-frontend/packages/superset-ui-core/test/connection/callApi/parseResponse.test.ts b/superset-frontend/packages/superset-ui-core/test/connection/callApi/parseResponse.test.ts index 4b6192e65ac0..030c90f4a82a 100644 --- a/superset-frontend/packages/superset-ui-core/test/connection/callApi/parseResponse.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/connection/callApi/parseResponse.test.ts @@ -22,12 +22,15 @@ import parseResponse from '../../../src/connection/callApi/parseResponse'; import { LOGIN_GLOB } from '../fixtures/constants'; +beforeAll(() => fetchMock.mockGlobal()); +afterAll(() => fetchMock.hardReset()); + describe('parseResponse()', () => { beforeAll(() => { fetchMock.get(LOGIN_GLOB, { result: '1234' }); }); - afterAll(() => fetchMock.restore()); + afterAll(() => fetchMock.removeRoutes().clearHistory()); const mockGetUrl = '/mock/get/url'; const mockPostUrl = '/mock/post/url'; @@ -45,7 +48,7 @@ describe('parseResponse()', () => { fetchMock.get(mockNoParseUrl, new Response('test response')); }); - afterEach(() => fetchMock.reset()); + afterEach(() => fetchMock.removeRoutes().clearHistory()); it('returns a Promise', () => { const apiPromise = callApi({ url: mockGetUrl, method: 'GET' }); @@ -58,7 +61,7 @@ describe('parseResponse()', () => { const args = await parseResponse( callApi({ url: mockGetUrl, method: 'GET' }), ); - expect(fetchMock.calls(mockGetUrl)).toHaveLength(1); + expect(fetchMock.callHistory.calls(mockGetUrl)).toHaveLength(1); const keys = Object.keys(args); expect(keys).toContain('response'); expect(keys).toContain('json'); @@ -81,7 +84,7 @@ describe('parseResponse()', () => { } catch (err) { error = err as Error; } finally { - expect(fetchMock.calls(mockTextUrl)).toHaveLength(1); + expect(fetchMock.callHistory.calls(mockTextUrl)).toHaveLength(1); expect(error?.stack).toBeDefined(); expect(error?.message).toContain('Unexpected token'); } @@ -99,7 +102,7 @@ describe('parseResponse()', () => { callApi({ url: mockTextParseUrl, method: 'GET' }), 'text', ); - expect(fetchMock.calls(mockTextParseUrl)).toHaveLength(1); + expect(fetchMock.callHistory.calls(mockTextParseUrl)).toHaveLength(1); const keys = Object.keys(args); expect(keys).toContain('response'); expect(keys).toContain('text'); @@ -134,7 +137,7 @@ describe('parseResponse()', () => { callApi({ url: mockNoParseUrl, method: 'GET' }), 'raw', ); - expect(fetchMock.calls(mockNoParseUrl)).toHaveLength(2); + expect(fetchMock.callHistory.calls(mockNoParseUrl)).toHaveLength(2); expect(responseNull.bodyUsed).toBe(false); expect(responseRaw.bodyUsed).toBe(false); }); @@ -193,7 +196,7 @@ describe('parseResponse()', () => { } catch (err) { error = err as { ok: boolean; status: number }; } finally { - expect(fetchMock.calls(mockNotOkayUrl)).toHaveLength(1); + expect(fetchMock.callHistory.calls(mockNotOkayUrl)).toHaveLength(1); expect(error?.ok).toBe(false); expect(error?.status).toBe(404); } diff --git a/superset-frontend/packages/superset-ui-core/test/query/api/legacy/getDatasourceMetadata.test.ts b/superset-frontend/packages/superset-ui-core/test/query/api/legacy/getDatasourceMetadata.test.ts index c5bb3fcd83a1..e5cc8a19beb0 100644 --- a/superset-frontend/packages/superset-ui-core/test/query/api/legacy/getDatasourceMetadata.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/query/api/legacy/getDatasourceMetadata.test.ts @@ -21,10 +21,13 @@ import { getDatasourceMetadata } from '../../../../src/query/api/legacy'; import setupClientForTest from '../setupClientForTest'; +beforeAll(() => fetchMock.mockGlobal()); +afterAll(() => fetchMock.hardReset()); + describe('getFormData()', () => { beforeAll(() => setupClientForTest()); - afterEach(() => fetchMock.restore()); + afterEach(() => fetchMock.clearHistory().removeRoutes()); it('returns datasource metadata for given datasource key', () => { const mockData = { diff --git a/superset-frontend/packages/superset-ui-core/test/query/api/legacy/getFormData.test.ts b/superset-frontend/packages/superset-ui-core/test/query/api/legacy/getFormData.test.ts index 4987d8b91d65..7976e5e8870c 100644 --- a/superset-frontend/packages/superset-ui-core/test/query/api/legacy/getFormData.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/query/api/legacy/getFormData.test.ts @@ -22,10 +22,13 @@ import { getFormData } from '../../../../src/query/api/legacy'; import setupClientForTest from '../setupClientForTest'; +beforeAll(() => fetchMock.mockGlobal()); +afterAll(() => fetchMock.hardReset()); + describe('getFormData()', () => { beforeAll(() => setupClientForTest()); - afterEach(() => fetchMock.restore()); + afterEach(() => fetchMock.clearHistory().removeRoutes()); const mockData = { datasource: '1__table', diff --git a/superset-frontend/packages/superset-ui-core/test/query/api/v1/getChartData.test.ts b/superset-frontend/packages/superset-ui-core/test/query/api/v1/getChartData.test.ts index f88c44a2312f..e2c4d61ac3c3 100644 --- a/superset-frontend/packages/superset-ui-core/test/query/api/v1/getChartData.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/query/api/v1/getChartData.test.ts @@ -20,9 +20,13 @@ import fetchMock from 'fetch-mock'; import { buildQueryContext, ApiV1, VizType } from '@superset-ui/core'; import setupClientForTest from '../setupClientForTest'; +beforeAll(() => fetchMock.mockGlobal()); +afterAll(() => fetchMock.hardReset()); + describe('API v1 > getChartData()', () => { beforeAll(() => setupClientForTest()); - afterEach(() => fetchMock.restore()); + + afterEach(() => fetchMock.clearHistory().removeRoutes()); it('returns a promise of ChartDataResponse', async () => { const response = { diff --git a/superset-frontend/packages/superset-ui-core/test/query/api/v1/makeApi.test.ts b/superset-frontend/packages/superset-ui-core/test/query/api/v1/makeApi.test.ts index d7fcf1c04c85..5e39c6f8cfe2 100644 --- a/superset-frontend/packages/superset-ui-core/test/query/api/v1/makeApi.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/query/api/v1/makeApi.test.ts @@ -21,9 +21,13 @@ import { JsonValue, SupersetClientClass } from '@superset-ui/core'; import { makeApi, SupersetApiError } from '../../../../src/query'; import setupClientForTest from '../setupClientForTest'; +beforeAll(() => fetchMock.mockGlobal()); +afterAll(() => fetchMock.hardReset()); + describe('makeApi()', () => { beforeAll(() => setupClientForTest()); - afterEach(() => fetchMock.restore()); + + afterEach(() => fetchMock.clearHistory().removeRoutes()); it('should expose method and endpoint', () => { const api = makeApi({ @@ -95,7 +99,7 @@ describe('makeApi()', () => { const expected = new FormData(); expected.append('request', JSON.stringify('test')); - const received = fetchMock.lastOptions()?.body as FormData; + const received = fetchMock.callHistory.lastCall()?.options.body as FormData; expect(received).toBeInstanceOf(FormData); expect(received.get('request')).toEqual(expected.get('request')); @@ -109,7 +113,7 @@ describe('makeApi()', () => { }); fetchMock.get('glob:*/test-get-search*', { search: 'get' }); await api({ p1: 1, p2: 2, p3: [1, 2] }); - expect(fetchMock.lastUrl()).toContain( + expect(fetchMock.callHistory.lastCall()?.url).toContain( '/test-get-search?p1=1&p2=2&p3=1%2C2', ); }); @@ -123,7 +127,7 @@ describe('makeApi()', () => { }); fetchMock.get('glob:*/test-post-search*', { rison: 'get' }); await api({ p1: 1, p3: [1, 2] }); - expect(fetchMock.lastUrl()).toContain( + expect(fetchMock.callHistory.lastCall()?.url).toContain( '/test-post-search?q=(p1:1,p3:!(1,2))', ); }); @@ -137,7 +141,9 @@ describe('makeApi()', () => { }); fetchMock.post('glob:*/test-post-search*', { search: 'post' }); await api({ p1: 1, p3: [1, 2] }); - expect(fetchMock.lastUrl()).toContain('/test-post-search?p1=1&p3=1%2C2'); + expect(fetchMock.callHistory.lastCall()?.url).toContain( + '/test-post-search?p1=1&p3=1%2C2', + ); }); it('should throw when requestType is invalid', () => { @@ -215,6 +221,8 @@ describe('makeApi()', () => { fetchMock.delete('glob:*/test-raw-response?*', 'ok'); const result = await api({ field1: 11 }, {}); expect(result).toEqual(200); - expect(fetchMock.lastUrl()).toContain('/test-raw-response?field1=11'); + expect(fetchMock.callHistory.lastCall()?.url).toContain( + '/test-raw-response?field1=11', + ); }); }); diff --git a/superset-frontend/packages/superset-ui-core/test/time-comparison/fetchTimeRange.test.ts b/superset-frontend/packages/superset-ui-core/test/time-comparison/fetchTimeRange.test.ts index 152cdb2b684a..00a1e5cf3a35 100644 --- a/superset-frontend/packages/superset-ui-core/test/time-comparison/fetchTimeRange.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/time-comparison/fetchTimeRange.test.ts @@ -25,7 +25,13 @@ import { formatTimeRangeComparison, } from '../../src/time-comparison/fetchTimeRange'; -afterEach(() => fetchMock.restore()); +beforeAll(() => fetchMock.mockGlobal()); +afterAll(() => fetchMock.hardReset()); + +afterEach(() => { + fetchMock.removeRoutes(); + fetchMock.clearHistory(); +}); test('generates proper time range string', () => { expect( @@ -84,34 +90,41 @@ test('returns a formatted time range from empty response', async () => { }); test('returns a formatted error message from response', async () => { - fetchMock.get('glob:*/api/v1/time_range/?q=%27Last+day%27', { - throws: new Response(JSON.stringify({ message: 'Network error' })), - }); + const getTimeRangeUrl = 'glob:*/api/v1/time_range/?q=%27Last+day%27'; + fetchMock.get( + getTimeRangeUrl, + { + throws: new Response(JSON.stringify({ message: 'Network error' })), + }, + { name: getTimeRangeUrl }, + ); let timeRange = await fetchTimeRange('Last day'); expect(timeRange).toEqual({ error: 'Network error', }); + fetchMock.removeRoute(getTimeRangeUrl); fetchMock.get( - 'glob:*/api/v1/time_range/?q=%27Last+day%27', + getTimeRangeUrl, { throws: new Error('Internal Server Error'), }, - { overwriteRoutes: true }, + { name: getTimeRangeUrl }, ); timeRange = await fetchTimeRange('Last day'); expect(timeRange).toEqual({ error: 'Internal Server Error', }); + fetchMock.removeRoute(getTimeRangeUrl); fetchMock.get( - 'glob:*/api/v1/time_range/?q=%27Last+day%27', + getTimeRangeUrl, { throws: new Response(JSON.stringify({ statusText: 'Network error' }), { statusText: 'Network error', }), }, - { overwriteRoutes: true }, + { name: getTimeRangeUrl }, ); timeRange = await fetchTimeRange('Last day'); expect(timeRange).toEqual({ diff --git a/superset-frontend/spec/helpers/jsDomWithFetchAPI.ts b/superset-frontend/spec/helpers/jsDomWithFetchAPI.ts index 81279356c35f..1db1bafac386 100644 --- a/superset-frontend/spec/helpers/jsDomWithFetchAPI.ts +++ b/superset-frontend/spec/helpers/jsDomWithFetchAPI.ts @@ -31,6 +31,7 @@ export default class FixJSDOMEnvironment extends JSDOMEnvironment { this.global.Response = Response; this.global.AbortSignal = AbortSignal; this.global.AbortController = AbortController; + this.global.ReadableStream = ReadableStream; // Mock MessageChannel to prevent hanging Jest tests with rc-overflow@1.4.1 // Forces rc-overflow to use requestAnimationFrame fallback instead diff --git a/superset-frontend/spec/helpers/shim.tsx b/superset-frontend/spec/helpers/shim.tsx index 112b0256f849..9e49116df420 100644 --- a/superset-frontend/spec/helpers/shim.tsx +++ b/superset-frontend/spec/helpers/shim.tsx @@ -23,6 +23,7 @@ import jQuery from 'jquery'; // https://jestjs.io/docs/jest-object#jestmockmodulename-factory-options // in order to mock modules in test case, so avoid absolute import module import { configure as configureTranslation } from '@apache-superset/core/ui'; +import fetchMock from 'fetch-mock'; import { Worker } from './Worker'; import { IntersectionObserver } from './IntersectionObserver'; import { ResizeObserver } from './ResizeObserver'; @@ -43,6 +44,9 @@ if (defaultView != null) { }); } +fetchMock.mockGlobal(); +fetchMock.config.allowRelativeUrls = true; + const g = global as any; g.window ??= Object.create(window); g.window.location ??= { href: 'about:blank' }; diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.test.js b/superset-frontend/src/SqlLab/actions/sqlLab.test.js index ac6263d72dd5..ae547da87d3d 100644 --- a/superset-frontend/src/SqlLab/actions/sqlLab.test.js +++ b/superset-frontend/src/SqlLab/actions/sqlLab.test.js @@ -86,21 +86,30 @@ describe('async actions', () => { }; let dispatch; + const fetchQueryEndpoint = 'glob:*/api/v1/sqllab/results/*'; + const runQueryEndpoint = 'glob:*/api/v1/sqllab/execute/'; beforeEach(() => { dispatch = sinon.spy(); - }); - - afterEach(() => fetchMock.resetHistory()); + fetchMock.removeRoute(fetchQueryEndpoint); + fetchMock.get( + fetchQueryEndpoint, + JSON.stringify({ + data: mockBigNumber, + query: { sqlEditorId: 'dfsadfs' }, + }), + { name: fetchQueryEndpoint }, + ); - const fetchQueryEndpoint = 'glob:*/api/v1/sqllab/results/*'; - fetchMock.get( - fetchQueryEndpoint, - JSON.stringify({ data: mockBigNumber, query: { sqlEditorId: 'dfsadfs' } }), - ); + fetchMock.removeRoute(runQueryEndpoint); + fetchMock.post(runQueryEndpoint, `{ "data": ${mockBigNumber} }`, { + name: runQueryEndpoint, + }); + }); - const runQueryEndpoint = 'glob:*/api/v1/sqllab/execute/'; - fetchMock.post(runQueryEndpoint, `{ "data": ${mockBigNumber} }`); + afterEach(() => { + fetchMock.clearHistory(); + }); // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('saveQuery', () => { @@ -117,15 +126,15 @@ describe('async actions', () => { const store = mockStore(initialState); return store.dispatch(actions.saveQuery(query, queryId)).then(() => { - expect(fetchMock.calls(saveQueryEndpoint)).toHaveLength(1); + expect(fetchMock.callHistory.calls(saveQueryEndpoint)).toHaveLength(1); }); }); test('posts the correct query object', () => { const store = mockStore(initialState); return store.dispatch(actions.saveQuery(query, queryId)).then(() => { - const call = fetchMock.calls(saveQueryEndpoint)[0]; - const formData = JSON.parse(call[1].body); + const call = fetchMock.callHistory.calls(saveQueryEndpoint)[0]; + const formData = JSON.parse(call.options.body); const mappedQueryToServer = actions.convertQueryToServer(query); Object.keys(mappedQueryToServer).forEach(key => { @@ -172,11 +181,12 @@ describe('async actions', () => { const expectedSql = 'SELECT 1'; beforeEach(() => { + fetchMock.removeRoute(formatQueryEndpoint); fetchMock.post( formatQueryEndpoint, { result: expectedSql }, { - overwriteRoutes: true, + name: formatQueryEndpoint, }, ); }); @@ -185,7 +195,9 @@ describe('async actions', () => { const store = mockStore(initialState); store.dispatch(actions.formatQuery(query, queryId)); await waitFor(() => - expect(fetchMock.calls(formatQueryEndpoint)).toHaveLength(1), + expect(fetchMock.callHistory.calls(formatQueryEndpoint)).toHaveLength( + 1, + ), ); expect(store.getActions()[0].type).toBe(actions.QUERY_EDITOR_SET_SQL); expect(store.getActions()[0].sql).toBe(expectedSql); @@ -209,11 +221,13 @@ describe('async actions', () => { store.dispatch(actions.formatQuery(queryEditorWithoutExtras)); await waitFor(() => - expect(fetchMock.calls(formatQueryEndpoint)).toHaveLength(1), + expect(fetchMock.callHistory.calls(formatQueryEndpoint)).toHaveLength( + 1, + ), ); - const call = fetchMock.calls(formatQueryEndpoint)[0]; - const body = JSON.parse(call[1].body); + const call = fetchMock.callHistory.calls(formatQueryEndpoint)[0]; + const body = JSON.parse(call.options.body); expect(body).toEqual({ sql: 'SELECT * FROM table' }); expect(body.database_id).toBeUndefined(); @@ -238,11 +252,13 @@ describe('async actions', () => { store.dispatch(actions.formatQuery(queryEditorWithDb)); await waitFor(() => - expect(fetchMock.calls(formatQueryEndpoint)).toHaveLength(1), + expect(fetchMock.callHistory.calls(formatQueryEndpoint)).toHaveLength( + 1, + ), ); - const call = fetchMock.calls(formatQueryEndpoint)[0]; - const body = JSON.parse(call[1].body); + const call = fetchMock.callHistory.calls(formatQueryEndpoint)[0]; + const body = JSON.parse(call.options.body); expect(body).toEqual({ sql: 'SELECT * FROM table', @@ -268,11 +284,13 @@ describe('async actions', () => { store.dispatch(actions.formatQuery(queryEditorWithTemplateString)); await waitFor(() => - expect(fetchMock.calls(formatQueryEndpoint)).toHaveLength(1), + expect(fetchMock.callHistory.calls(formatQueryEndpoint)).toHaveLength( + 1, + ), ); - const call = fetchMock.calls(formatQueryEndpoint)[0]; - const body = JSON.parse(call[1].body); + const call = fetchMock.callHistory.calls(formatQueryEndpoint)[0]; + const body = JSON.parse(call.options.body); expect(body).toEqual({ sql: 'SELECT * FROM table WHERE id = {{ user_id }}', @@ -299,11 +317,13 @@ describe('async actions', () => { store.dispatch(actions.formatQuery(queryEditorWithTemplateObject)); await waitFor(() => - expect(fetchMock.calls(formatQueryEndpoint)).toHaveLength(1), + expect(fetchMock.callHistory.calls(formatQueryEndpoint)).toHaveLength( + 1, + ), ); - const call = fetchMock.calls(formatQueryEndpoint)[0]; - const body = JSON.parse(call[1].body); + const call = fetchMock.callHistory.calls(formatQueryEndpoint)[0]; + const body = JSON.parse(call.options.body); expect(body).toEqual({ sql: 'SELECT * FROM table WHERE id = {{ user_id }}', @@ -314,12 +334,11 @@ describe('async actions', () => { test('dispatches QUERY_EDITOR_SET_SQL with formatted result', async () => { const formattedSql = 'SELECT\n *\nFROM\n table'; - fetchMock.post( + fetchMock.removeRoute(formatQueryEndpoint); + fetchMock.route( formatQueryEndpoint, { result: formattedSql }, - { - overwriteRoutes: true, - }, + { name: formatQueryEndpoint }, ); const queryEditorToFormat = { @@ -365,11 +384,13 @@ describe('async actions', () => { store.dispatch(actions.formatQuery(outdatedQueryEditor)); await waitFor(() => - expect(fetchMock.calls(formatQueryEndpoint)).toHaveLength(1), + expect(fetchMock.callHistory.calls(formatQueryEndpoint)).toHaveLength( + 1, + ), ); - const call = fetchMock.calls(formatQueryEndpoint)[0]; - const body = JSON.parse(call[1].body); + const call = fetchMock.callHistory.calls(formatQueryEndpoint)[0]; + const body = JSON.parse(call.options.body); expect(body.sql).toBe('SELECT * FROM updated_table'); expect(body.database_id).toBe(10); @@ -388,7 +409,7 @@ describe('async actions', () => { expect.assertions(1); return makeRequest().then(() => { - expect(fetchMock.calls(fetchQueryEndpoint)).toHaveLength(1); + expect(fetchMock.callHistory.calls(fetchQueryEndpoint)).toHaveLength(1); }); }); @@ -402,7 +423,7 @@ describe('async actions', () => { test.skip('parses large number result without losing precision', () => makeRequest().then(() => { - expect(fetchMock.calls(fetchQueryEndpoint)).toHaveLength(1); + expect(fetchMock.callHistory.calls(fetchQueryEndpoint)).toHaveLength(1); expect(dispatch.callCount).toBe(2); expect(dispatch.getCall(1).lastArg.results.data.toString()).toBe( mockBigNumber, @@ -427,10 +448,11 @@ describe('async actions', () => { test('calls queryFailed on fetch error', () => { expect.assertions(1); + fetchMock.removeRoute(fetchQueryEndpoint); fetchMock.get( fetchQueryEndpoint, { throws: { message: 'error text' } }, - { overwriteRoutes: true }, + { name: fetchQueryEndpoint }, ); const store = mockStore({}); @@ -457,7 +479,7 @@ describe('async actions', () => { expect.assertions(1); return makeRequest().then(() => { - expect(fetchMock.calls(runQueryEndpoint)).toHaveLength(1); + expect(fetchMock.callHistory.calls(runQueryEndpoint)).toHaveLength(1); }); }); @@ -469,9 +491,9 @@ describe('async actions', () => { }); }); - test.skip('parses large number result without losing precision', () => + test('parses large number result without losing precision', () => makeRequest().then(() => { - expect(fetchMock.calls(runQueryEndpoint)).toHaveLength(1); + expect(fetchMock.callHistory.calls(runQueryEndpoint)).toHaveLength(1); expect(dispatch.callCount).toBe(2); expect(dispatch.getCall(1).lastArg.results.data.toString()).toBe( mockBigNumber, @@ -495,6 +517,7 @@ describe('async actions', () => { test('calls queryFailed on fetch error and logs the error details', () => { expect.assertions(2); + fetchMock.removeRoute(runQueryEndpoint); fetchMock.post( runQueryEndpoint, { @@ -504,7 +527,7 @@ describe('async actions', () => { statusText: 'timeout', }, }, - { overwriteRoutes: true }, + { name: runQueryEndpoint }, ); const store = mockStore({}); @@ -550,7 +573,9 @@ describe('async actions', () => { `{ "data": ${mockBigNumber} }`, ); await makeRequest().then(() => { - expect(fetchMock.calls(runQueryEndpointWithParams)).toHaveLength(1); + expect( + fetchMock.callHistory.calls(runQueryEndpointWithParams), + ).toHaveLength(1); }); }); }); @@ -591,7 +616,7 @@ describe('async actions', () => { expect.assertions(1); return makeRequest().then(() => { - expect(fetchMock.calls(stopQueryEndpoint)).toHaveLength(1); + expect(fetchMock.callHistory.calls(stopQueryEndpoint)).toHaveLength(1); }); }); @@ -607,8 +632,8 @@ describe('async actions', () => { expect.assertions(1); return makeRequest().then(() => { - const call = fetchMock.calls(stopQueryEndpoint)[0]; - const body = JSON.parse(call[1].body); + const call = fetchMock.callHistory.calls(stopQueryEndpoint)[0]; + const body = JSON.parse(call.options.body); expect(body.client_id).toBe(baseQuery.id); }); }); @@ -955,7 +980,7 @@ describe('async actions', () => { isFeatureEnabled.mockRestore(); }); - afterEach(() => fetchMock.resetHistory()); + afterEach(() => fetchMock.clearHistory()); // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('addQueryEditor', () => { @@ -978,7 +1003,9 @@ describe('async actions', () => { store.dispatch(actions.addQueryEditor(queryEditor)); expect(store.getActions()).toEqual(expectedActions); - expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0); + expect( + fetchMock.callHistory.calls(updateTabStateEndpoint), + ).toHaveLength(0); }); }); @@ -1121,7 +1148,9 @@ describe('async actions', () => { const request = actions.queryEditorSetAndSaveSql(queryEditor, sql); return request(store.dispatch, store.getState).then(() => { expect(store.getActions()).toEqual(expectedActions); - expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1); + expect( + fetchMock.callHistory.calls(updateTabStateEndpoint), + ).toHaveLength(1); }); }); }); @@ -1143,7 +1172,9 @@ describe('async actions', () => { request(store.dispatch, store.getState); expect(store.getActions()).toEqual(expectedActions); - expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0); + expect( + fetchMock.callHistory.calls(updateTabStateEndpoint), + ).toHaveLength(0); isFeatureEnabled.mockRestore(); }); }); @@ -1325,10 +1356,14 @@ describe('async actions', () => { expectedActionTypes, ); expect(store.getActions()[0].prepend).toBeFalsy(); - expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1); + expect( + fetchMock.callHistory.calls(updateTableSchemaEndpoint), + ).toHaveLength(1); // tab state is not updated, since no query was run - expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0); + expect( + fetchMock.callHistory.calls(updateTabStateEndpoint), + ).toHaveLength(0); }); }); }); @@ -1354,14 +1389,15 @@ describe('async actions', () => { }); beforeEach(() => { + fetchMock.removeRoute(runQueryEndpoint); fetchMock.post(runQueryEndpoint, JSON.stringify(results), { - overwriteRoutes: true, + name: runQueryEndpoint, }); }); afterEach(() => { store.clearActions(); - fetchMock.resetHistory(); + fetchMock.clearHistory(); }); test('updates and runs data preview query when configured', () => { @@ -1382,9 +1418,11 @@ describe('async actions', () => { expect(store.getActions().map(a => a.type)).toEqual( expectedActionTypes, ); - expect(fetchMock.calls(runQueryEndpoint)).toHaveLength(1); + expect(fetchMock.callHistory.calls(runQueryEndpoint)).toHaveLength(1); // tab state is not updated, since the query is a data preview - expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0); + expect( + fetchMock.callHistory.calls(updateTabStateEndpoint), + ).toHaveLength(0); }); }); @@ -1406,9 +1444,11 @@ describe('async actions', () => { expect(store.getActions().map(a => a.type)).toEqual( expectedActionTypes, ); - expect(fetchMock.calls(runQueryEndpoint)).toHaveLength(1); + expect(fetchMock.callHistory.calls(runQueryEndpoint)).toHaveLength(1); // tab state is not updated, since the query is a data preview - expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0); + expect( + fetchMock.callHistory.calls(updateTabStateEndpoint), + ).toHaveLength(0); }); }); }); @@ -1428,13 +1468,13 @@ describe('async actions', () => { ]; return store.dispatch(actions.expandTable(table)).then(() => { expect(store.getActions()).toEqual(expectedActions); - const expandedCalls = fetchMock + const expandedCalls = fetchMock.callHistory .calls() .filter( call => - call[0] && - call[0].includes('/tableschemaview/') && - call[0].includes('/expanded'), + call.url && + call.url.includes('/tableschemaview/') && + call.url.includes('/expanded'), ); expect(expandedCalls).toHaveLength(1); }); @@ -1454,7 +1494,7 @@ describe('async actions', () => { return store.dispatch(actions.expandTable(table)).then(() => { expect(store.getActions()).toEqual(expectedActions); // Check all POST calls to find the expanded endpoint - const expandedCalls = fetchMock + const expandedCalls = fetchMock.callHistory .calls() .filter( call => @@ -1480,13 +1520,13 @@ describe('async actions', () => { return store.dispatch(actions.expandTable(table)).then(() => { expect(store.getActions()).toEqual(expectedActions); // Check all POST calls to find the expanded endpoint - const expandedCalls = fetchMock + const expandedCalls = fetchMock.callHistory .calls() .filter( call => - call[0] && - call[0].includes('/tableschemaview/') && - call[0].includes('/expanded'), + call.url && + call.url.includes('/tableschemaview/') && + call.url.includes('/expanded'), ); expect(expandedCalls).toHaveLength(0); }); @@ -1510,13 +1550,13 @@ describe('async actions', () => { return store.dispatch(actions.expandTable(table)).then(() => { expect(store.getActions()).toEqual(expectedActions); // Check all POST calls to find the expanded endpoint - const expandedCalls = fetchMock + const expandedCalls = fetchMock.callHistory .calls() .filter( call => - call[0] && - call[0].includes('/tableschemaview/') && - call[0].includes('/expanded'), + call.url && + call.url.includes('/tableschemaview/') && + call.url.includes('/expanded'), ); expect(expandedCalls).toHaveLength(0); isFeatureEnabled.mockRestore(); @@ -1539,13 +1579,13 @@ describe('async actions', () => { ]; return store.dispatch(actions.collapseTable(table)).then(() => { expect(store.getActions()).toEqual(expectedActions); - const expandedCalls = fetchMock + const expandedCalls = fetchMock.callHistory .calls() .filter( call => - call[0] && - call[0].includes('/tableschemaview/') && - call[0].includes('/expanded'), + call.url && + call.url.includes('/tableschemaview/') && + call.url.includes('/expanded'), ); expect(expandedCalls).toHaveLength(1); }); @@ -1564,13 +1604,13 @@ describe('async actions', () => { ]; return store.dispatch(actions.collapseTable(table)).then(() => { expect(store.getActions()).toEqual(expectedActions); - const expandedCalls = fetchMock + const expandedCalls = fetchMock.callHistory .calls() .filter( call => - call[0] && - call[0].includes('/tableschemaview/') && - call[0].includes('/expanded'), + call.url && + call.url.includes('/tableschemaview/') && + call.url.includes('/expanded'), ); expect(expandedCalls).toHaveLength(0); }); @@ -1589,13 +1629,13 @@ describe('async actions', () => { ]; return store.dispatch(actions.collapseTable(table)).then(() => { expect(store.getActions()).toEqual(expectedActions); - const expandedCalls = fetchMock + const expandedCalls = fetchMock.callHistory .calls() .filter( call => - call[0] && - call[0].includes('/tableschemaview/') && - call[0].includes('/expanded'), + call.url && + call.url.includes('/tableschemaview/') && + call.url.includes('/expanded'), ); expect(expandedCalls).toHaveLength(0); }); @@ -1618,7 +1658,7 @@ describe('async actions', () => { ]; return store.dispatch(actions.collapseTable(table)).then(() => { expect(store.getActions()).toEqual(expectedActions); - const expandedCalls = fetchMock + const expandedCalls = fetchMock.callHistory .calls() .filter( call => @@ -1647,7 +1687,9 @@ describe('async actions', () => { ]; return store.dispatch(actions.removeTables([table])).then(() => { expect(store.getActions()).toEqual(expectedActions); - expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1); + expect( + fetchMock.callHistory.calls(updateTableSchemaEndpoint), + ).toHaveLength(1); }); }); @@ -1667,7 +1709,9 @@ describe('async actions', () => { ]; return store.dispatch(actions.removeTables(tables)).then(() => { expect(store.getActions()).toEqual(expectedActions); - expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(2); + expect( + fetchMock.callHistory.calls(updateTableSchemaEndpoint), + ).toHaveLength(2); }); }); @@ -1684,7 +1728,9 @@ describe('async actions', () => { ]; return store.dispatch(actions.removeTables(tables)).then(() => { expect(store.getActions()).toEqual(expectedActions); - expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1); + expect( + fetchMock.callHistory.calls(updateTableSchemaEndpoint), + ).toHaveLength(1); }); }); }); @@ -1699,8 +1745,9 @@ describe('async actions', () => { query: { sqlEditorId: 'null' }, query_id: 'efgh', }; + fetchMock.removeRoute(runQueryEndpoint); fetchMock.post(runQueryEndpoint, JSON.stringify(results), { - overwriteRoutes: true, + name: runQueryEndpoint, }); const oldQueryEditor = { ...queryEditor, inLocalStorage: true }; @@ -1777,10 +1824,14 @@ describe('async actions', () => { .dispatch(actions.syncQueryEditor(oldQueryEditor)) .then(() => { expect(store.getActions()).toEqual(expectedActions); - expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(3); + expect( + fetchMock.callHistory.calls(updateTabStateEndpoint), + ).toHaveLength(3); // query editor has 2 tables loaded in the schema viewer - expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(2); + expect( + fetchMock.callHistory.calls(updateTableSchemaEndpoint), + ).toHaveLength(2); }); }); }); diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useAnnotations.test.ts b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useAnnotations.test.ts index 99377c43be07..05d27b8f5ed6 100644 --- a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useAnnotations.test.ts +++ b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useAnnotations.test.ts @@ -50,14 +50,17 @@ jest.mock('@superset-ui/core', () => ({ })); afterEach(() => { - fetchMock.reset(); + fetchMock.clearHistory(); + fetchMock.removeRoutes(); act(() => { store.dispatch(api.util.resetApiState()); }); }); beforeEach(() => { - fetchMock.post(queryValidationApiRoute, fakeApiResult); + fetchMock.post(queryValidationApiRoute, fakeApiResult, { + name: queryValidationApiRoute, + }); }); const initialize = (withValidator = false) => { @@ -115,13 +118,15 @@ const initialize = (withValidator = false) => { test('skips fetching validation if validator is undefined', () => { const { result } = initialize(); expect(result.current.data).toEqual([]); - expect(fetchMock.calls(queryValidationApiRoute)).toHaveLength(0); + expect(fetchMock.callHistory.calls(queryValidationApiRoute)).toHaveLength(0); }); test('returns validation if validator is configured', async () => { const { result, waitFor } = initialize(true); await waitFor(() => - expect(fetchMock.calls(queryValidationApiRoute)).toHaveLength(1), + expect(fetchMock.callHistory.calls(queryValidationApiRoute)).toHaveLength( + 1, + ), ); expect(result.current.data).toEqual( fakeApiResult.result.map(err => ({ @@ -135,13 +140,10 @@ test('returns validation if validator is configured', async () => { test('returns server error description', async () => { const errorMessage = 'Unexpected validation api error'; - fetchMock.post( - queryValidationApiRoute, - { - throws: new Error(errorMessage), - }, - { overwriteRoutes: true }, - ); + fetchMock.removeRoute(queryValidationApiRoute); + fetchMock.post(queryValidationApiRoute, { + throws: new Error(errorMessage), + }); const { result, waitFor } = initialize(true); await waitFor( () => @@ -159,13 +161,10 @@ test('returns server error description', async () => { test('returns session expire description when CSRF token expired', async () => { const errorMessage = 'CSRF token expired'; - fetchMock.post( - queryValidationApiRoute, - { - throws: new Error(errorMessage), - }, - { overwriteRoutes: true }, - ); + fetchMock.removeRoute(queryValidationApiRoute); + fetchMock.post(queryValidationApiRoute, { + throws: new Error(errorMessage), + }); const { result, waitFor } = initialize(true); await waitFor( () => diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.test.ts b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.test.ts index 3e4a75646733..e54322cca182 100644 --- a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.test.ts +++ b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.test.ts @@ -94,7 +94,7 @@ beforeEach(() => { }); afterEach(() => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); act(() => { store.dispatch(api.util.resetApiState()); }); @@ -120,7 +120,7 @@ test('returns keywords including fetched function_names data', async () => { ); await waitFor(() => - expect(fetchMock.calls(dbFunctionNamesApiRoute).length).toBe(1), + expect(fetchMock.callHistory.calls(dbFunctionNamesApiRoute).length).toBe(1), ); fakeSchemaApiResult.forEach(schema => { expect(result.current).toContainEqual( @@ -171,7 +171,7 @@ test('skip fetching if autocomplete skipped', () => { }, ); expect(result.current).toEqual([]); - expect(fetchMock.calls()).toEqual([]); + expect(fetchMock.callHistory.calls()).toEqual([]); }); test('returns column keywords among selected tables', async () => { diff --git a/superset-frontend/src/SqlLab/components/EditorAutoSync/EditorAutoSync.test.tsx b/superset-frontend/src/SqlLab/components/EditorAutoSync/EditorAutoSync.test.tsx index 2116340b7754..0870724176b5 100644 --- a/superset-frontend/src/SqlLab/components/EditorAutoSync/EditorAutoSync.test.tsx +++ b/superset-frontend/src/SqlLab/components/EditorAutoSync/EditorAutoSync.test.tsx @@ -74,13 +74,13 @@ beforeEach(() => { }); afterEach(() => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); }); test('sync the unsaved editor tab state when there are new changes since the last update', async () => { const updateEditorTabState = `glob:*/tabstateview/${defaultQueryEditor.id}`; fetchMock.put(updateEditorTabState, 200); - expect(fetchMock.calls(updateEditorTabState)).toHaveLength(0); + expect(fetchMock.callHistory.calls(updateEditorTabState)).toHaveLength(0); render(, { useRedux: true, initialState: { @@ -91,14 +91,14 @@ test('sync the unsaved editor tab state when there are new changes since the las await act(async () => { jest.advanceTimersByTime(INTERVAL); }); - expect(fetchMock.calls(updateEditorTabState)).toHaveLength(1); - fetchMock.restore(); + expect(fetchMock.callHistory.calls(updateEditorTabState)).toHaveLength(1); + fetchMock.clearHistory().removeRoutes(); }); test('sync the unsaved NEW editor state when there are new in local storage', async () => { const createEditorTabState = `glob:*/tabstateview/`; fetchMock.post(createEditorTabState, { id: 123 }); - expect(fetchMock.calls(createEditorTabState)).toHaveLength(0); + expect(fetchMock.callHistory.calls(createEditorTabState)).toHaveLength(0); render(, { useRedux: true, initialState: { @@ -119,12 +119,14 @@ test('sync the unsaved NEW editor state when there are new in local storage', as await act(async () => { jest.advanceTimersByTime(INTERVAL); }); - expect(fetchMock.calls(createEditorTabState)).toHaveLength(1); - fetchMock.restore(); + expect(fetchMock.callHistory.calls(createEditorTabState)).toHaveLength(1); + fetchMock.clearHistory().removeRoutes(); }); test('sync the active editor id when there are updates in tab history', async () => { - expect(fetchMock.calls(updateActiveEditorTabState)).toHaveLength(0); + expect(fetchMock.callHistory.calls(updateActiveEditorTabState)).toHaveLength( + 0, + ); render(, { useRedux: true, initialState: { @@ -147,18 +149,22 @@ test('sync the active editor id when there are updates in tab history', async () await act(async () => { jest.advanceTimersByTime(INTERVAL); }); - expect(fetchMock.calls(updateActiveEditorTabState)).toHaveLength(1); + expect(fetchMock.callHistory.calls(updateActiveEditorTabState)).toHaveLength( + 1, + ); await act(async () => { jest.advanceTimersByTime(INTERVAL); }); - expect(fetchMock.calls(updateActiveEditorTabState)).toHaveLength(1); + expect(fetchMock.callHistory.calls(updateActiveEditorTabState)).toHaveLength( + 1, + ); }); test('sync the destroyed editor id when there are updates in destroyed editors', async () => { const removeId = 'removed-tab-id'; const deleteEditorState = `glob:*/tabstateview/${removeId}`; fetchMock.delete(deleteEditorState, { id: removeId }); - expect(fetchMock.calls(deleteEditorState)).toHaveLength(0); + expect(fetchMock.callHistory.calls(deleteEditorState)).toHaveLength(0); render(, { useRedux: true, initialState: { @@ -174,17 +180,17 @@ test('sync the destroyed editor id when there are updates in destroyed editors', await act(async () => { jest.advanceTimersByTime(INTERVAL); }); - expect(fetchMock.calls(deleteEditorState)).toHaveLength(1); + expect(fetchMock.callHistory.calls(deleteEditorState)).toHaveLength(1); await act(async () => { jest.advanceTimersByTime(INTERVAL); }); - expect(fetchMock.calls(deleteEditorState)).toHaveLength(1); + expect(fetchMock.callHistory.calls(deleteEditorState)).toHaveLength(1); }); test('skip syncing the unsaved editor tab state when the updates are already synced', async () => { const updateEditorTabState = `glob:*/tabstateview/${defaultQueryEditor.id}`; fetchMock.put(updateEditorTabState, 200); - expect(fetchMock.calls(updateEditorTabState)).toHaveLength(0); + expect(fetchMock.callHistory.calls(updateEditorTabState)).toHaveLength(0); render(, { useRedux: true, initialState: { @@ -203,8 +209,8 @@ test('skip syncing the unsaved editor tab state when the updates are already syn await act(async () => { jest.advanceTimersByTime(INTERVAL); }); - expect(fetchMock.calls(updateEditorTabState)).toHaveLength(0); - fetchMock.restore(); + expect(fetchMock.callHistory.calls(updateEditorTabState)).toHaveLength(0); + fetchMock.clearHistory().removeRoutes(); }); test('renders an error toast when the sync failed', async () => { @@ -212,7 +218,7 @@ test('renders an error toast when the sync failed', async () => { fetchMock.put(updateEditorTabState, { throws: new Error('errorMessage'), }); - expect(fetchMock.calls(updateEditorTabState)).toHaveLength(0); + expect(fetchMock.callHistory.calls(updateEditorTabState)).toHaveLength(0); render( <> @@ -235,5 +241,5 @@ test('renders an error toast when the sync failed', async () => { 'An error occurred while saving your editor state.', expect.anything(), ); - fetchMock.restore(); + fetchMock.clearHistory().removeRoutes(); }); diff --git a/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton/ExploreCtasResultsButton.test.tsx b/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton/ExploreCtasResultsButton.test.tsx index 16cb8374ce70..bcd7e3aa02f7 100644 --- a/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton/ExploreCtasResultsButton.test.tsx +++ b/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton/ExploreCtasResultsButton.test.tsx @@ -62,7 +62,7 @@ describe('ExploreCtasResultsButton', () => { const { getByText } = setup({}, mockStore(initialState)); postFormSpy.mockClear(); - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); fetchMock.post(getOrCreateTableEndpoint, { result: { table_id: 1234 } }); fireEvent.click(getByText('Explore')); @@ -80,7 +80,7 @@ describe('ExploreCtasResultsButton', () => { const { getByText } = setup({}, mockStore(initialState)); postFormSpy.mockClear(); - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); fetchMock.post(getOrCreateTableEndpoint, { throws: new Error('Unexpected all to v1 API'), }); diff --git a/superset-frontend/src/SqlLab/components/PopEditorTab/PopEditorTab.test.tsx b/superset-frontend/src/SqlLab/components/PopEditorTab/PopEditorTab.test.tsx index e66e5bba91c4..1e94927fef54 100644 --- a/superset-frontend/src/SqlLab/components/PopEditorTab/PopEditorTab.test.tsx +++ b/superset-frontend/src/SqlLab/components/PopEditorTab/PopEditorTab.test.tsx @@ -57,7 +57,7 @@ beforeEach(() => { }); afterEach(() => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); }); let replaceState = jest.spyOn(window.history, 'replaceState'); @@ -78,7 +78,7 @@ test('should handle id', async () => { setup('/sqllab?id=1'); await waitFor(() => expect( - fetchMock.calls(`glob:*/api/v1/sqllab/permalink/kv:${id}`), + fetchMock.callHistory.calls(`glob:*/api/v1/sqllab/permalink/kv:${id}`), ).toHaveLength(1), ); expect(replaceState).toHaveBeenCalledWith( @@ -86,7 +86,7 @@ test('should handle id', async () => { expect.anything(), '/sqllab', ); - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); }); test('should handle permalink', async () => { const key = '9sadkfl'; @@ -98,7 +98,7 @@ test('should handle permalink', async () => { setup('/sqllab/p/9sadkfl'); await waitFor(() => expect( - fetchMock.calls(`glob:*/api/v1/sqllab/permalink/${key}`), + fetchMock.callHistory.calls(`glob:*/api/v1/sqllab/permalink/${key}`), ).toHaveLength(1), ); expect(replaceState).toHaveBeenCalledWith( @@ -106,12 +106,14 @@ test('should handle permalink', async () => { expect.anything(), '/sqllab', ); - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); }); test('should handle savedQueryId', async () => { setup('/sqllab?savedQueryId=1'); await waitFor(() => - expect(fetchMock.calls('glob:*/api/v1/saved_query/1')).toHaveLength(1), + expect( + fetchMock.callHistory.calls('glob:*/api/v1/saved_query/1'), + ).toHaveLength(1), ); expect(replaceState).toHaveBeenCalledWith( expect.anything(), diff --git a/superset-frontend/src/SqlLab/components/QueryAutoRefresh/QueryAutoRefresh.test.tsx b/superset-frontend/src/SqlLab/components/QueryAutoRefresh/QueryAutoRefresh.test.tsx index edce1f2ce073..d04361153257 100644 --- a/superset-frontend/src/SqlLab/components/QueryAutoRefresh/QueryAutoRefresh.test.tsx +++ b/superset-frontend/src/SqlLab/components/QueryAutoRefresh/QueryAutoRefresh.test.tsx @@ -57,7 +57,7 @@ describe('QueryAutoRefresh', () => { }); afterEach(() => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); cleanup(); jest.runOnlyPendingTimers(); jest.useRealTimers(); @@ -162,7 +162,7 @@ describe('QueryAutoRefresh', () => { expect( store.getActions().filter(({ type }) => type === REFRESH_QUERIES), ).toHaveLength(0); - expect(fetchMock.calls(refreshApi)).toHaveLength(1); + expect(fetchMock.callHistory.calls(refreshApi)).toHaveLength(1); }); test('Does not fail and attempts to refresh with mixed valid/invalid queries', async () => { @@ -217,7 +217,7 @@ describe('QueryAutoRefresh', () => { ), ); - expect(fetchMock.calls(refreshApi)).toHaveLength(0); + expect(fetchMock.callHistory.calls(refreshApi)).toHaveLength(0); }); test('logs the failed error for async queries', async () => { diff --git a/superset-frontend/src/SqlLab/components/QueryHistory/QueryHistory.test.tsx b/superset-frontend/src/SqlLab/components/QueryHistory/QueryHistory.test.tsx index 52edf19aaf74..a1d2185f3c55 100644 --- a/superset-frontend/src/SqlLab/components/QueryHistory/QueryHistory.test.tsx +++ b/superset-frontend/src/SqlLab/components/QueryHistory/QueryHistory.test.tsx @@ -81,7 +81,7 @@ const setup = (overrides = {}) => ( ); -afterEach(() => fetchMock.reset()); +afterEach(() => fetchMock.clearHistory().removeRoutes()); test('Renders an empty state for query history', () => { render(setup(), { useRedux: true, initialState }); @@ -102,7 +102,7 @@ test('fetches the query history when the persistence mode is enabled', async () fetchMock.get(editorQueryApiRoute, fakeApiResult); render(setup(), { useRedux: true, initialState }); await waitFor(() => - expect(fetchMock.calls(editorQueryApiRoute).length).toBe(1), + expect(fetchMock.callHistory.calls(editorQueryApiRoute).length).toBe(1), ); const queryResultText = screen.getByText(fakeApiResult.result[0].rows); expect(queryResultText).toBeInTheDocument(); @@ -127,7 +127,7 @@ test('fetches the query history by the tabViewId', async () => { }, }); await waitFor(() => - expect(fetchMock.calls(editorQueryApiRoute).length).toBe(1), + expect(fetchMock.callHistory.calls(editorQueryApiRoute).length).toBe(1), ); const queryResultText = screen.getByText(fakeApiResult.result[0].rows); expect(queryResultText).toBeInTheDocument(); @@ -213,7 +213,7 @@ test('displays multiple queries with newest query first', async () => { const { container } = render(setup(), { useRedux: true, initialState }); await waitFor(() => - expect(fetchMock.calls(editorQueryApiRoute).length).toBe(1), + expect(fetchMock.callHistory.calls(editorQueryApiRoute).length).toBe(1), ); expect(screen.getByTestId('listview-table')).toBeVisible(); diff --git a/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx b/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx index e12627777f47..be5a1e22fc37 100644 --- a/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx +++ b/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx @@ -127,7 +127,7 @@ fetchMock.post(reRunQueryEndpoint, { result: [] }); fetchMock.get('glob:*/api/v1/sqllab/results/*', { result: [] }); beforeEach(() => { - fetchMock.resetHistory(); + fetchMock.clearHistory(); }); const middlewares = [thunk]; @@ -151,7 +151,7 @@ describe('ResultSet', () => { // Add cleanup after each test afterEach(async () => { - fetchMock.resetHistory(); + fetchMock.clearHistory(); // Wait for any pending effects to complete await new Promise(resolve => setTimeout(resolve, 0)); }); @@ -250,7 +250,7 @@ describe('ResultSet', () => { }, }); - expect(fetchMock.calls(reRunQueryEndpoint)).toHaveLength(0); + expect(fetchMock.callHistory.calls(reRunQueryEndpoint)).toHaveLength(0); setup(mockedProps, store); expect(store.getActions()).toHaveLength(1); expect(store.getActions()[0].query.errorMessage).toEqual( @@ -258,7 +258,7 @@ describe('ResultSet', () => { ); expect(store.getActions()[0].type).toEqual('START_QUERY'); await waitFor(() => - expect(fetchMock.calls(reRunQueryEndpoint)).toHaveLength(1), + expect(fetchMock.callHistory.calls(reRunQueryEndpoint)).toHaveLength(1), ); }); @@ -276,7 +276,7 @@ describe('ResultSet', () => { }); setup(mockedProps, store); expect(store.getActions()).toEqual([]); - expect(fetchMock.calls(reRunQueryEndpoint)).toHaveLength(0); + expect(fetchMock.callHistory.calls(reRunQueryEndpoint)).toHaveLength(0); }); test('should render cached query', async () => { @@ -622,7 +622,9 @@ describe('ResultSet', () => { }); // Verify the API was called - const resultsCalls = fetchMock.calls('glob:*/api/v1/sqllab/results/*'); + const resultsCalls = fetchMock.callHistory.calls( + 'glob:*/api/v1/sqllab/results/*', + ); expect(resultsCalls).toHaveLength(1); }); diff --git a/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/ShareSqlLabQuery.test.tsx b/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/ShareSqlLabQuery.test.tsx index cd83a05eafcb..33b408eeeddd 100644 --- a/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/ShareSqlLabQuery.test.tsx +++ b/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/ShareSqlLabQuery.test.tsx @@ -82,18 +82,17 @@ describe('ShareSqlLabQuery', () => { const storeQueryMockId = 'ci39c3'; beforeEach(async () => { + fetchMock.removeRoute(storeQueryUrl); fetchMock.post( storeQueryUrl, () => ({ key: storeQueryMockId, url: `/p/${storeQueryMockId}` }), - { - overwriteRoutes: true, - }, + { name: storeQueryUrl }, ); - fetchMock.resetHistory(); + fetchMock.clearHistory(); jest.clearAllMocks(); }); - afterAll(() => fetchMock.reset()); + afterAll(() => fetchMock.hardReset()); // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('via permalink api', () => { @@ -116,10 +115,12 @@ describe('ShareSqlLabQuery', () => { const expected = omit(mockQueryEditor, ['id', 'remoteId']); userEvent.click(button); await waitFor(() => - expect(fetchMock.calls(storeQueryUrl)).toHaveLength(1), + expect(fetchMock.callHistory.calls(storeQueryUrl)).toHaveLength(1), ); expect( - JSON.parse(fetchMock.calls(storeQueryUrl)[0][1]?.body as string), + JSON.parse( + fetchMock.callHistory.calls(storeQueryUrl)[0].options?.body as string, + ), ).toEqual(expected); }); @@ -140,10 +141,12 @@ describe('ShareSqlLabQuery', () => { const expected = omit(unsavedQueryEditor, ['id']); userEvent.click(button); await waitFor(() => - expect(fetchMock.calls(storeQueryUrl)).toHaveLength(1), + expect(fetchMock.callHistory.calls(storeQueryUrl)).toHaveLength(1), ); expect( - JSON.parse(fetchMock.calls(storeQueryUrl)[0][1]?.body as string), + JSON.parse( + fetchMock.callHistory.calls(storeQueryUrl)[0].options?.body as string, + ), ).toEqual(expected); }); }); diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx b/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx index b15bbdadc466..7b5d44b03413 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx +++ b/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx @@ -389,8 +389,8 @@ describe('SqlEditor', () => { // click button fireEvent.click(button); await waitFor(() => { - expect(fetchMock.lastUrl()).toEqual(estimateApi); - expect(fetchMock.lastOptions()).toEqual( + expect(fetchMock.callHistory.lastCall()?.url).toEqual(estimateApi); + expect(fetchMock.callHistory.lastCall()?.options).toEqual( expect.objectContaining({ body: JSON.stringify({ database_id: 2023, @@ -402,11 +402,11 @@ describe('SqlEditor', () => { cache: 'default', credentials: 'same-origin', headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'X-CSRFToken': '1234', + accept: 'application/json', + 'content-type': 'application/json', + 'x-csrftoken': '1234', }, - method: 'POST', + method: 'post', mode: 'same-origin', redirect: 'follow', signal: undefined, @@ -443,10 +443,12 @@ describe('SqlEditor', () => { const indicator = getByTestId('sqlEditor-loading'); expect(indicator).toBeInTheDocument(); await waitFor(() => - expect(fetchMock.calls('glob:*/tabstateview/*').length).toBe(1), + expect( + fetchMock.callHistory.calls('glob:*/tabstateview/*').length, + ).toBe(1), ); // it will be called from EditorAutoSync - expect(fetchMock.calls(switchTabApi).length).toBe(0); + expect(fetchMock.callHistory.calls(switchTabApi).length).toBe(0); }); }); }); diff --git a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/SqlEditorLeftBar.test.tsx b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/SqlEditorLeftBar.test.tsx index dee5e69d1d6e..0719a73cb398 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/SqlEditorLeftBar.test.tsx +++ b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/SqlEditorLeftBar.test.tsx @@ -92,7 +92,7 @@ beforeEach(() => { }); afterEach(() => { - fetchMock.restore(); + fetchMock.clearHistory().removeRoutes(); jest.clearAllMocks(); }); @@ -352,9 +352,9 @@ test('display no compatible schema found when schema api throws errors', async ( reduxState, ); await waitFor(() => - expect(fetchMock.calls('glob:*/api/v1/database/3/schemas/?*')).toHaveLength( - 1, - ), + expect( + fetchMock.callHistory.calls('glob:*/api/v1/database/3/schemas/?*'), + ).toHaveLength(1), ); const select = screen.getByRole('combobox', { name: 'Select schema or type to search schemas', @@ -386,7 +386,7 @@ test('ignore schema api when current schema is deprecated', async () => { }); expect(await screen.findByText(/Database/i)).toBeInTheDocument(); - expect(fetchMock.calls()).not.toContainEqual( + expect(fetchMock.callHistory.calls()).not.toContainEqual( expect.arrayContaining([ expect.stringContaining( `/tables/${mockData.database.id}/${invalidSchemaName}/`, diff --git a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.tsx b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.tsx index f68e8f2eb6a7..0fdf3dfe1a59 100644 --- a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.tsx +++ b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.tsx @@ -49,7 +49,7 @@ beforeEach(() => { }); afterEach(() => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); }); test('should removeQueryEditor', async () => { diff --git a/superset-frontend/src/SqlLab/components/TableElement/TableElement.test.tsx b/superset-frontend/src/SqlLab/components/TableElement/TableElement.test.tsx index cbe0d5efd80b..2bb3ff3f2120 100644 --- a/superset-frontend/src/SqlLab/components/TableElement/TableElement.test.tsx +++ b/superset-frontend/src/SqlLab/components/TableElement/TableElement.test.tsx @@ -70,7 +70,8 @@ beforeEach(() => { }); afterEach(() => { - fetchMock.reset(); + fetchMock.removeRoutes(); + fetchMock.clearHistory(); }); const mockedProps = { @@ -94,10 +95,11 @@ const setupSyncTableTest = () => { mockedIsFeatureEnabled.mockImplementation( featureFlag => featureFlag === FeatureFlag.SqllabBackendPersistence, ); + fetchMock.removeRoute(updateTableSchemaEndpoint); fetchMock.post( updateTableSchemaEndpoint, { id: 100 }, - { overwriteRoutes: true }, + { name: updateTableSchemaEndpoint }, ); return spy; }; @@ -186,10 +188,14 @@ test('removes the table', async () => { await waitFor(() => expect(getAllByTestId('mock-icon-tooltip')).toHaveLength(6), ); - expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(0); + expect(fetchMock.callHistory.calls(updateTableSchemaEndpoint)).toHaveLength( + 0, + ); fireEvent.click(getByText('Remove table preview')); await waitFor(() => - expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1), + expect(fetchMock.callHistory.calls(updateTableSchemaEndpoint)).toHaveLength( + 1, + ), ); mockedIsFeatureEnabled.mockClear(); }); @@ -199,13 +205,21 @@ test('fetches table metadata when expanded', async () => { useRedux: true, initialState, }); - expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(0); - expect(fetchMock.calls(getExtraTableMetadataEndpoint)).toHaveLength(0); + expect(fetchMock.callHistory.calls(getTableMetadataEndpoint)).toHaveLength(0); + expect( + fetchMock.callHistory.calls(getExtraTableMetadataEndpoint), + ).toHaveLength(0); await waitFor(() => - expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(1), + expect(fetchMock.callHistory.calls(getTableMetadataEndpoint)).toHaveLength( + 1, + ), ); - expect(fetchMock.calls(updateTableSchemaExpandedEndpoint)).toHaveLength(0); - expect(fetchMock.calls(getExtraTableMetadataEndpoint)).toHaveLength(1); + expect( + fetchMock.callHistory.calls(updateTableSchemaExpandedEndpoint), + ).toHaveLength(0); + expect( + fetchMock.callHistory.calls(getExtraTableMetadataEndpoint), + ).toHaveLength(1); }); test('refreshes table metadata when triggered', async () => { @@ -219,15 +233,21 @@ test('refreshes table metadata when triggered', async () => { await waitFor(() => expect(getAllByTestId('mock-icon-tooltip')).toHaveLength(6), ); - expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(0); - expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(1); + expect(fetchMock.callHistory.calls(updateTableSchemaEndpoint)).toHaveLength( + 0, + ); + expect(fetchMock.callHistory.calls(getTableMetadataEndpoint)).toHaveLength(1); fireEvent.click(getByText('Refresh table schema')); await waitFor(() => - expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(2), + expect(fetchMock.callHistory.calls(getTableMetadataEndpoint)).toHaveLength( + 2, + ), ); await waitFor(() => - expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1), + expect(fetchMock.callHistory.calls(updateTableSchemaEndpoint)).toHaveLength( + 1, + ), ); }); @@ -283,7 +303,9 @@ test('does not call syncTable when query editor is in localStorage', async () => }); await waitFor(() => { - expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(1); + expect(fetchMock.callHistory.calls(getTableMetadataEndpoint)).toHaveLength( + 1, + ); }); await new Promise(resolve => setTimeout(resolve, 100)); @@ -313,7 +335,9 @@ test('does not call syncTable with non-numeric queryEditorId', async () => { }); await waitFor(() => { - expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(1); + expect(fetchMock.callHistory.calls(getTableMetadataEndpoint)).toHaveLength( + 1, + ); }); await new Promise(resolve => setTimeout(resolve, 100)); @@ -343,7 +367,9 @@ test('does not call syncTable for already initialized tables', async () => { }); await waitFor(() => { - expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(1); + expect(fetchMock.callHistory.calls(getTableMetadataEndpoint)).toHaveLength( + 1, + ); }); await new Promise(resolve => setTimeout(resolve, 100)); diff --git a/superset-frontend/src/SqlLab/components/TablePreview/TablePreview.test.tsx b/superset-frontend/src/SqlLab/components/TablePreview/TablePreview.test.tsx index dadf62888b8a..28e36060e6c1 100644 --- a/superset-frontend/src/SqlLab/components/TablePreview/TablePreview.test.tsx +++ b/superset-frontend/src/SqlLab/components/TablePreview/TablePreview.test.tsx @@ -71,7 +71,7 @@ beforeEach(() => { }); afterEach(() => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); }); const mockedProps = { @@ -103,7 +103,9 @@ test('renders indexes', async () => { initialState, }); await waitFor(() => - expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(1), + expect(fetchMock.callHistory.calls(getTableMetadataEndpoint)).toHaveLength( + 1, + ), ); expect(queryByText(`Indexes (${table.indexes.length})`)).toBeInTheDocument(); }); @@ -126,12 +128,14 @@ test('renders preview', async () => { }, }); await waitFor(() => - expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(1), + expect(fetchMock.callHistory.calls(getTableMetadataEndpoint)).toHaveLength( + 1, + ), ); - expect(fetchMock.calls(fetchPreviewEndpoint)).toHaveLength(0); + expect(fetchMock.callHistory.calls(fetchPreviewEndpoint)).toHaveLength(0); fireEvent.click(getByText('Data preview')); await waitFor(() => - expect(fetchMock.calls(fetchPreviewEndpoint)).toHaveLength(1), + expect(fetchMock.callHistory.calls(fetchPreviewEndpoint)).toHaveLength(1), ); }); @@ -143,12 +147,16 @@ describe('table actions', () => { initialState, }); await waitFor(() => - expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(1), + expect( + fetchMock.callHistory.calls(getTableMetadataEndpoint), + ).toHaveLength(1), ); const refreshButton = getByRole('button', { name: 'sync' }); fireEvent.click(refreshButton); await waitFor(() => - expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(2), + expect( + fetchMock.callHistory.calls(getTableMetadataEndpoint), + ).toHaveLength(2), ); }); @@ -158,7 +166,9 @@ describe('table actions', () => { initialState, }); await waitFor(() => - expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(1), + expect( + fetchMock.callHistory.calls(getTableMetadataEndpoint), + ).toHaveLength(1), ); const viewButton = getByRole('button', { name: 'eye' }); fireEvent.click(viewButton); diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx index 848deda77487..d49697af7e8f 100644 --- a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx +++ b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx @@ -123,10 +123,15 @@ const renderModal = async ( beforeEach(() => { fetchMock - .post(CHART_DATA_ENDPOINT, { body: {} }, {}) - .post(FORM_DATA_KEY_ENDPOINT, { key: '123' }); + .post(CHART_DATA_ENDPOINT, { body: {} }, { name: CHART_DATA_ENDPOINT }) + .post( + FORM_DATA_KEY_ENDPOINT, + { key: '123' }, + { name: FORM_DATA_KEY_ENDPOINT }, + ); }); -afterEach(() => fetchMock.restore()); + +afterEach(() => fetchMock.removeRoutes().clearHistory()); test('should render the title', async () => { await renderModal(); @@ -149,12 +154,11 @@ test('should close the modal', async () => { }); test('should render loading indicator', async () => { + fetchMock.removeRoute(CHART_DATA_ENDPOINT); fetchMock.post( CHART_DATA_ENDPOINT, { body: {} }, - // delay is missing in fetch-mock types - // @ts-ignore - { overwriteRoutes: true, delay: 1000 }, + { name: CHART_DATA_ENDPOINT, delay: 1000 }, ); await renderModal(); expect(screen.getByLabelText('Loading')).toBeInTheDocument(); @@ -175,7 +179,7 @@ test('should generate Explore url', async () => { groupbyFieldName: 'groupby', }, }); - await waitFor(() => fetchMock.called(CHART_DATA_ENDPOINT)); + await waitFor(() => fetchMock.callHistory.called(CHART_DATA_ENDPOINT)); const expectedRequestPayload = { form_data: { ...omitBy( @@ -204,7 +208,7 @@ test('should generate Explore url', async () => { }; const parsedRequestPayload = JSON.parse( - fetchMock.lastCall()?.[1]?.body as string, + fetchMock.callHistory.lastCall()?.options?.body as string, ); expect(parsedRequestPayload.form_data).toEqual( @@ -317,9 +321,9 @@ describe('Embedded mode behavior', () => { }, }); - await waitFor(() => fetchMock.called(CHART_DATA_ENDPOINT)); + await waitFor(() => fetchMock.callHistory.called(CHART_DATA_ENDPOINT)); - expect(fetchMock.called(FORM_DATA_KEY_ENDPOINT)).toBe(false); + expect(fetchMock.callHistory.called(FORM_DATA_KEY_ENDPOINT)).toBe(false); }); test('should render "Edit chart" button in non-embedded mode', async () => { @@ -343,10 +347,10 @@ describe('Embedded mode behavior', () => { }, }); - await waitFor(() => fetchMock.called(CHART_DATA_ENDPOINT)); + await waitFor(() => fetchMock.callHistory.called(CHART_DATA_ENDPOINT)); await waitFor(() => { - expect(fetchMock.called(FORM_DATA_KEY_ENDPOINT)).toBe(true); + expect(fetchMock.callHistory.called(FORM_DATA_KEY_ENDPOINT)).toBe(true); }); expect( @@ -375,13 +379,14 @@ describe('Table view with pagination', () => { ], }; + fetchMock.removeRoute(CHART_DATA_ENDPOINT); fetchMock.post(CHART_DATA_ENDPOINT, mockLargeDataset, { - overwriteRoutes: true, + name: CHART_DATA_ENDPOINT, }); }); afterEach(() => { - fetchMock.restore(); + fetchMock.clearHistory(); }); test('should render table view when Table radio is selected', async () => { @@ -506,6 +511,7 @@ describe('Table view with pagination', () => { test('should handle empty results in table view', async () => { // Mock empty dataset response + fetchMock.removeRoute(CHART_DATA_ENDPOINT); fetchMock.post( CHART_DATA_ENDPOINT, { @@ -517,7 +523,7 @@ describe('Table view with pagination', () => { }, ], }, - { overwriteRoutes: true }, + { name: CHART_DATA_ENDPOINT }, ); await renderModal({ diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillBySubmenu.test.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillBySubmenu.test.tsx index 8bcfc7959b5e..3b60e756911a 100644 --- a/superset-frontend/src/components/Chart/DrillBy/DrillBySubmenu.test.tsx +++ b/superset-frontend/src/components/Chart/DrillBy/DrillBySubmenu.test.tsx @@ -133,7 +133,7 @@ getChartMetadataRegistry().registerValue( afterEach(() => { supersetGetCache.clear(); - fetchMock.restore(); + fetchMock.clearHistory().removeRoutes(); }); test('render disabled menu item for unsupported chart', async () => { diff --git a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.test.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.test.tsx index 3d5213f90e2d..030552823ecd 100644 --- a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.test.tsx +++ b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.test.tsx @@ -115,7 +115,7 @@ const fetchWithData = () => { }; afterEach(() => { - fetchMock.restore(); + fetchMock.clearHistory().removeRoutes(); supersetGetCache.clear(); }); diff --git a/superset-frontend/src/components/Chart/chartActions.test.js b/superset-frontend/src/components/Chart/chartActions.test.js index 18c28de4ab01..6487e10b024e 100644 --- a/superset-frontend/src/components/Chart/chartActions.test.js +++ b/superset-frontend/src/components/Chart/chartActions.test.js @@ -70,15 +70,19 @@ describe('chart actions', () => { let waitForAsyncDataStub; let fakeMetadata; + beforeAll(() => { + fetchMock.get('glob:*api/v1/security/csrf_token/*', { result: '1234' }); + }); + const setupDefaultFetchMock = () => { - fetchMock.post(MOCK_URL, { json: {} }, { overwriteRoutes: true }); + fetchMock.post(`glob:*${MOCK_URL}*`, { json: {} }, { name: MOCK_URL }); }; - beforeAll(() => { + beforeEach(() => { setupDefaultFetchMock(); }); - afterAll(() => fetchMock.restore()); + afterEach(() => fetchMock.clearHistory().removeRoutes()); beforeEach(() => { dispatch = sinon.spy(); @@ -111,7 +115,7 @@ describe('chart actions', () => { .callsFake(data => Promise.resolve(data)); }); - test('should defer abort of previous controller to avoid Redux state mutation', async () => { + test.only('should defer abort of previous controller to avoid Redux state mutation', async () => { jest.useFakeTimers(); const chartKey = 'defer_abort_test'; const formData = { @@ -178,7 +182,7 @@ describe('chart actions', () => { getExploreUrlStub.restore(); getChartDataUriStub.restore(); buildV1ChartDataPayloadStub.restore(); - fetchMock.resetHistory(); + fetchMock.clearHistory(); waitForAsyncDataStub.restore(); global.featureFlags = { @@ -196,8 +200,8 @@ describe('chart actions', () => { const actionThunk = actions.postChartFormData({}, null); await actionThunk(dispatch, mockGetState); - expect(fetchMock.calls(MOCK_URL)).toHaveLength(1); - expect(fetchMock.calls(MOCK_URL)[0][1].body).toBe( + expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1); + expect(fetchMock.callHistory.calls(MOCK_URL)[0].options.body).toBe( JSON.stringify({ some_param: 'fake query!', result_type: 'full', @@ -212,7 +216,7 @@ describe('chart actions', () => { const mockBigIntUrl = '/mock/chart/data/bigint'; const expectedBigNumber = '9223372036854775807'; fetchMock.post(mockBigIntUrl, `{ "value": ${expectedBigNumber} }`, { - overwriteRoutes: true, + name: mockBigIntUrl, }); getChartDataUriStub = sinon .stub(exploreUtils, 'getChartDataUri') @@ -222,7 +226,7 @@ describe('chart actions', () => { formData: fakeMetadata, }); - expect(fetchMock.calls(mockBigIntUrl)).toHaveLength(1); + expect(fetchMock.callHistory.calls(mockBigIntUrl)).toHaveLength(1); expect(json.value.toString()).toEqual(expectedBigNumber); }); @@ -269,7 +273,7 @@ describe('chart actions', () => { return actionThunk(dispatch, mockGetState).then(() => { // chart update, trigger query, update form data, success expect(dispatch.callCount).toBe(5); - expect(fetchMock.calls(MOCK_URL)).toHaveLength(1); + expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1); expect(dispatch.args[0][0].type).toBe(actions.CHART_UPDATE_STARTED); }); }); @@ -279,7 +283,7 @@ describe('chart actions', () => { return actionThunk(dispatch, mockGetState).then(() => { // chart update, trigger query, update form data, success expect(dispatch.callCount).toBe(5); - expect(fetchMock.calls(MOCK_URL)).toHaveLength(1); + expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1); expect(dispatch.args[1][0].type).toBe(actions.TRIGGER_QUERY); }); }); @@ -289,7 +293,7 @@ describe('chart actions', () => { return actionThunk(dispatch, mockGetState).then(() => { // chart update, trigger query, update form data, success expect(dispatch.callCount).toBe(5); - expect(fetchMock.calls(MOCK_URL)).toHaveLength(1); + expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1); expect(dispatch.args[2][0].type).toBe(actions.UPDATE_QUERY_FORM_DATA); }); }); @@ -299,7 +303,7 @@ describe('chart actions', () => { return actionThunk(dispatch, mockGetState).then(() => { // chart update, trigger query, update form data, success expect(dispatch.callCount).toBe(5); - expect(fetchMock.calls(MOCK_URL)).toHaveLength(1); + expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1); expect(typeof dispatch.args[3][0]).toBe('function'); dispatch.args[3][0](dispatch); @@ -313,15 +317,16 @@ describe('chart actions', () => { return actionThunk(dispatch, mockGetState).then(() => { // chart update, trigger query, update form data, success expect(dispatch.callCount).toBe(5); - expect(fetchMock.calls(MOCK_URL)).toHaveLength(1); + expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1); expect(dispatch.args[4][0].type).toBe(actions.CHART_UPDATE_SUCCEEDED); }); }); test('should dispatch CHART_UPDATE_FAILED action upon query timeout', () => { const unresolvingPromise = new Promise(() => {}); + fetchMock.removeRoute(MOCK_URL); fetchMock.post(MOCK_URL, () => unresolvingPromise, { - overwriteRoutes: true, + name: MOCK_URL, }); const timeoutInSec = 1 / 1000; @@ -329,18 +334,21 @@ describe('chart actions', () => { return actionThunk(dispatch, mockGetState).then(() => { // chart update, trigger query, update form data, fail - expect(fetchMock.calls(MOCK_URL)).toHaveLength(1); + expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1); expect(dispatch.callCount).toBe(5); expect(dispatch.args[4][0].type).toBe(actions.CHART_UPDATE_FAILED); + + fetchMock.removeRoute(MOCK_URL); setupDefaultFetchMock(); }); }); test('should dispatch CHART_UPDATE_FAILED action upon non-timeout non-abort failure', () => { + fetchMock.removeRoute(MOCK_URL); fetchMock.post( MOCK_URL, { throws: { statusText: 'misc error' } }, - { overwriteRoutes: true }, + { name: MOCK_URL }, ); const timeoutInSec = 100; // Set to a time that is longer than the time this will take to fail @@ -353,15 +361,17 @@ describe('chart actions', () => { expect(updateFailedAction.type).toBe(actions.CHART_UPDATE_FAILED); expect(updateFailedAction.queriesResponse[0].error).toBe('misc error'); + fetchMock.removeRoute(MOCK_URL); setupDefaultFetchMock(); }); }); test('should dispatch CHART_UPDATE_STOPPED action upon abort', () => { + fetchMock.removeRoute(MOCK_URL); fetchMock.post( MOCK_URL, { throws: { name: 'AbortError' } }, - { overwriteRoutes: true }, + { name: MOCK_URL }, ); const timeoutInSec = 100; @@ -375,6 +385,7 @@ describe('chart actions', () => { expect(types).toContain(actions.CHART_UPDATE_STOPPED); expect(types).not.toContain(actions.CHART_UPDATE_FAILED); + fetchMock.removeRoutes(); setupDefaultFetchMock(); }); }); @@ -384,7 +395,7 @@ describe('chart actions', () => { const mockBigIntUrl = '/mock/chart/data/bigint'; const expectedBigNumber = '9223372036854775807'; fetchMock.post(mockBigIntUrl, `{ "value": ${expectedBigNumber} }`, { - overwriteRoutes: true, + name: mockBigIntUrl, }); getExploreUrlStub = sinon .stub(exploreUtils, 'getExploreUrl') @@ -394,7 +405,7 @@ describe('chart actions', () => { formData: fakeMetadata, }); - expect(fetchMock.calls(mockBigIntUrl)).toHaveLength(1); + expect(fetchMock.callHistory.calls(mockBigIntUrl)).toHaveLength(1); expect(json.result[0].value.toString()).toEqual(expectedBigNumber); }); }); diff --git a/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx b/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx index f8d0425c5e68..fdacff9577fe 100644 --- a/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx +++ b/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx @@ -181,7 +181,9 @@ const schemaApiRoute = 'glob:*/api/v1/database/*/schemas/?*'; const tablesApiRoute = 'glob:*/api/v1/database/*/tables/*'; function setupFetchMock() { - fetchMock.get(databaseApiRoute, fakeDatabaseApiResult); + fetchMock.get(databaseApiRoute, fakeDatabaseApiResult, { + name: databaseApiRoute, + }); fetchMock.get(catalogApiRoute, fakeCatalogApiResult); fetchMock.get(schemaApiRoute, fakeSchemaApiResult); fetchMock.get(tablesApiRoute, fakeFunctionNamesApiResult); @@ -192,7 +194,8 @@ beforeEach(() => { }); afterEach(() => { - fetchMock.reset(); + fetchMock.clearHistory(); + fetchMock.removeRoutes(); act(() => { store.dispatch(api.util.resetApiState()); }); @@ -209,7 +212,7 @@ test('Refresh should work', async () => { render(, { useRedux: true, store }); - expect(fetchMock.calls(schemaApiRoute).length).toBe(0); + expect(fetchMock.callHistory.calls(schemaApiRoute).length).toBe(0); const select = screen.getByRole('combobox', { name: 'Select schema or type to search schemas: public', @@ -218,8 +221,8 @@ test('Refresh should work', async () => { await userEvent.click(select); await waitFor(() => { - expect(fetchMock.calls(databaseApiRoute).length).toBe(1); - expect(fetchMock.calls(schemaApiRoute).length).toBe(1); + expect(fetchMock.callHistory.calls(databaseApiRoute).length).toBe(1); + expect(fetchMock.callHistory.calls(schemaApiRoute).length).toBe(1); expect(props.handleError).toHaveBeenCalledTimes(0); expect(props.onDbChange).toHaveBeenCalledTimes(0); expect(props.onSchemaChange).toHaveBeenCalledTimes(0); @@ -229,8 +232,8 @@ test('Refresh should work', async () => { await userEvent.click(screen.getByRole('button', { name: 'sync' })); await waitFor(() => { - expect(fetchMock.calls(databaseApiRoute).length).toBe(1); - expect(fetchMock.calls(schemaApiRoute).length).toBe(2); + expect(fetchMock.callHistory.calls(databaseApiRoute).length).toBe(1); + expect(fetchMock.callHistory.calls(schemaApiRoute).length).toBe(2); expect(props.handleError).toHaveBeenCalledTimes(0); expect(props.onDbChange).toHaveBeenCalledTimes(0); expect(props.onSchemaChange).toHaveBeenCalledTimes(0); @@ -244,13 +247,14 @@ test('Should database select display options', async () => { name: 'Select database or type to search databases', }); expect(select).toBeInTheDocument(); - await userEvent.click(select); + userEvent.click(select); expect(await screen.findByText('test-mysql')).toBeInTheDocument(); }); test('should display options in order of the api response', async () => { + fetchMock.removeRoute(databaseApiRoute); fetchMock.get(databaseApiRoute, fakeDatabaseApiResultInReverseOrder, { - overwriteRoutes: true, + name: databaseApiRoute, }); const props = createProps(); render(, { @@ -261,7 +265,7 @@ test('should display options in order of the api response', async () => { name: 'Select database or type to search databases', }); expect(select).toBeInTheDocument(); - await userEvent.click(select); + userEvent.click(select); const options = await screen.findAllByRole('option'); expect(options[0]).toHaveTextContent( @@ -273,13 +277,14 @@ test('should display options in order of the api response', async () => { }); test('Should fetch the search keyword when total count exceeds initial options', async () => { + fetchMock.removeRoute(databaseApiRoute); fetchMock.get( databaseApiRoute, { ...fakeDatabaseApiResult, count: fakeDatabaseApiResult.result.length + 1, }, - { overwriteRoutes: true }, + { name: databaseApiRoute }, ); const props = createProps(); @@ -288,18 +293,20 @@ test('Should fetch the search keyword when total count exceeds initial options', name: 'Select database or type to search databases', }); await waitFor(() => - expect(fetchMock.calls(databaseApiRoute)).toHaveLength(1), + expect(fetchMock.callHistory.calls(databaseApiRoute)).toHaveLength(1), ); expect(select).toBeInTheDocument(); await userEvent.type(select, 'keywordtest'); await waitFor(() => - expect(fetchMock.calls(databaseApiRoute)).toHaveLength(2), + expect(fetchMock.callHistory.calls(databaseApiRoute)).toHaveLength(2), + ); + expect(fetchMock.callHistory.calls(databaseApiRoute)[1].url).toContain( + 'keywordtest', ); - expect(fetchMock.calls(databaseApiRoute)[1][0]).toContain('keywordtest'); }); test('should show empty state if there are no options', async () => { - fetchMock.reset(); + fetchMock.removeRoutes(); fetchMock.get(databaseApiRoute, { result: [] }); fetchMock.get(schemaApiRoute, { result: [] }); fetchMock.get(tablesApiRoute, { result: [] }); @@ -367,7 +374,9 @@ test('Sends the correct schema when changing the schema', async () => { useRedux: true, store, }); - await waitFor(() => expect(fetchMock.calls(databaseApiRoute).length).toBe(1)); + await waitFor(() => + expect(fetchMock.callHistory.calls(databaseApiRoute).length).toBe(1), + ); rerender(); expect(props.onSchemaChange).toHaveBeenCalledTimes(0); const select = screen.getByRole('combobox', { diff --git a/superset-frontend/src/components/Datasource/ChangeDatasourceModal/ChangeDatasourceModal.test.jsx b/superset-frontend/src/components/Datasource/ChangeDatasourceModal/ChangeDatasourceModal.test.jsx index 20fbc37be924..2feac5ab8ffd 100644 --- a/superset-frontend/src/components/Datasource/ChangeDatasourceModal/ChangeDatasourceModal.test.jsx +++ b/superset-frontend/src/components/Datasource/ChangeDatasourceModal/ChangeDatasourceModal.test.jsx @@ -54,7 +54,7 @@ fetchMock.get(DATASOURCE_ENDPOINT, DATASOURCE_PAYLOAD); fetchMock.get(INFO_ENDPOINT, {}); afterEach(() => { - fetchMock.resetHistory(); + fetchMock.clearHistory(); }); const setup = (props = mockedProps) => @@ -70,7 +70,9 @@ test('renders', () => { test('fetches datasources', async () => { setup(); - await waitFor(() => expect(fetchMock.calls(INFO_ENDPOINT)).toHaveLength(1)); + await waitFor(() => + expect(fetchMock.callHistory.calls(INFO_ENDPOINT)).toHaveLength(1), + ); }); test('renders confirmation message', async () => { @@ -87,6 +89,6 @@ test('changes the datasource', async () => { const proceedButton = getByRole('button', { name: 'Proceed' }); fireEvent.click(proceedButton); await waitFor(() => - expect(fetchMock.calls(/api\/v1\/dataset\/7/)).toHaveLength(1), + expect(fetchMock.callHistory.calls(/api\/v1\/dataset\/7/)).toHaveLength(1), ); }); diff --git a/superset-frontend/src/components/Datasource/DatasourceModal/DatasourceModal.test.jsx b/superset-frontend/src/components/Datasource/DatasourceModal/DatasourceModal.test.jsx index 87dd400eb363..afd35a26e72f 100644 --- a/superset-frontend/src/components/Datasource/DatasourceModal/DatasourceModal.test.jsx +++ b/superset-frontend/src/components/Datasource/DatasourceModal/DatasourceModal.test.jsx @@ -64,7 +64,7 @@ async function renderAndWait(props = mockedProps) { } beforeEach(() => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); cleanup(); renderAndWait(); fetchMock.post(SAVE_ENDPOINT, SAVE_PAYLOAD); @@ -191,7 +191,7 @@ describe('DatasourceModal', () => { // Render with the initial datasource cleanup(); - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); fetchMock.post(SAVE_ENDPOINT, SAVE_PAYLOAD); fetchMock.put(SAVE_DATASOURCE_ENDPOINT, {}); fetchMock.get(GET_DATASOURCE_ENDPOINT, { result: {} }); @@ -225,16 +225,16 @@ describe('DatasourceModal', () => { // Verify the PUT request was made with override_columns=true await waitFor(() => { - const putCalls = fetchMock + const putCalls = fetchMock.callHistory .calls() .filter( call => - call[0].includes('/api/v1/dataset/7') && - call[0].includes('override_columns') && - call[1]?.method === 'PUT', + call.url.includes('/api/v1/dataset/7') && + call.url.includes('override_columns') && + call.options?.method === 'put', ); expect(putCalls.length).toBeGreaterThan(0); - expect(putCalls[putCalls.length - 1][0]).toContain( + expect(putCalls[putCalls.length - 1].url).toContain( 'override_columns=true', ); }); @@ -252,7 +252,7 @@ describe('DatasourceModal', () => { // Render with the initial datasource cleanup(); - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); fetchMock.post(SAVE_ENDPOINT, SAVE_PAYLOAD); fetchMock.put(SAVE_DATASOURCE_ENDPOINT, {}); fetchMock.get(GET_DATASOURCE_ENDPOINT, { result: {} }); @@ -292,16 +292,16 @@ describe('DatasourceModal', () => { // Verify the PUT request was made with override_columns=false await waitFor(() => { - const putCalls = fetchMock + const putCalls = fetchMock.callHistory .calls() .filter( call => - call[0].includes('/api/v1/dataset/7') && - call[0].includes('override_columns') && - call[1]?.method === 'PUT', + call.url.includes('/api/v1/dataset/7') && + call.url.includes('override_columns') && + call.options?.method === 'put', ); expect(putCalls.length).toBeGreaterThan(0); - expect(putCalls[putCalls.length - 1][0]).toContain( + expect(putCalls[putCalls.length - 1].url).toContain( 'override_columns=false', ); }); diff --git a/superset-frontend/src/components/Datasource/DatasourceModal/DatasourceModal.useModal.test.tsx b/superset-frontend/src/components/Datasource/DatasourceModal/DatasourceModal.useModal.test.tsx index 038631ea2719..2610190b47b5 100644 --- a/superset-frontend/src/components/Datasource/DatasourceModal/DatasourceModal.useModal.test.tsx +++ b/superset-frontend/src/components/Datasource/DatasourceModal/DatasourceModal.useModal.test.tsx @@ -39,14 +39,14 @@ const mockedProps = { }; beforeEach(() => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); fetchMock.put('glob:*/api/v1/dataset/7?override_columns=*', {}); fetchMock.get('glob:*/api/v1/dataset/7', { result: {} }); fetchMock.get('glob:*/api/v1/database/?q=*', { result: [] }); }); afterEach(() => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); jest.clearAllMocks(); }); diff --git a/superset-frontend/src/components/Datasource/components/DatasourceEditor/components/DatasetUsageTab/DatasetUsageTab.test.tsx b/superset-frontend/src/components/Datasource/components/DatasourceEditor/components/DatasetUsageTab/DatasetUsageTab.test.tsx index 8f0a1446c393..103964fd7c27 100644 --- a/superset-frontend/src/components/Datasource/components/DatasourceEditor/components/DatasetUsageTab/DatasetUsageTab.test.tsx +++ b/superset-frontend/src/components/Datasource/components/DatasourceEditor/components/DatasetUsageTab/DatasetUsageTab.test.tsx @@ -119,14 +119,14 @@ beforeAll(() => { }); beforeEach(() => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); jest.clearAllMocks(); // Mock scrollTo for all tests Element.prototype.scrollTo = jest.fn(); }); afterEach(() => { - fetchMock.restore(); + fetchMock.clearHistory().removeRoutes(); // Restore original scrollTo implementation after each test Element.prototype.scrollTo = originalScrollTo; // Restore console.error if it was spied on diff --git a/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditor.test.tsx b/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditor.test.tsx index 0d8bbc9e19f6..43ec1a99b6ac 100644 --- a/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditor.test.tsx +++ b/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditor.test.tsx @@ -42,14 +42,15 @@ jest.mock('@superset-ui/core', () => ({ })); beforeEach(() => { - fetchMock.get(DATASOURCE_ENDPOINT, [], { overwriteRoutes: true }); + fetchMock.get(DATASOURCE_ENDPOINT, [], { name: DATASOURCE_ENDPOINT }); setupDatasourceEditorMocks(); jest.clearAllMocks(); }); afterEach(async () => { await cleanupAsyncOperations(); - fetchMock.restore(); + fetchMock.removeRoutes(); + fetchMock.clearHistory(); // Reset module mock since jest.fn() doesn't support mockRestore() jest.mocked(isFeatureEnabled).mockReset(); // Restore console.error if it was spied on @@ -75,24 +76,25 @@ test('can sync columns from source', async () => { }); const columnsTab = screen.getByTestId('collection-tab-Columns'); - await userEvent.click(columnsTab); + userEvent.click(columnsTab); const syncButton = screen.getByText(/sync columns from source/i); expect(syncButton).toBeInTheDocument(); // Use a Promise to track when fetchMock is called const fetchPromise = new Promise(resolve => { + fetchMock.removeRoute(DATASOURCE_ENDPOINT); fetchMock.get( DATASOURCE_ENDPOINT, - (url: string) => { + ({ url }) => { resolve(url); return []; }, - { overwriteRoutes: true }, + { name: DATASOURCE_ENDPOINT }, ); }); - await userEvent.click(syncButton); + userEvent.click(syncButton); // Wait for the fetch to be called const url = await fetchPromise; @@ -517,19 +519,15 @@ test('fetchUsageData rethrows AbortError without updating state', async () => { const { unmount } = await asyncRender(props); // Mock the API to reject with AbortError - fetchMock.get( - 'glob:*/api/v1/chart/*', - () => { - const error = new Error('The operation was aborted'); - error.name = 'AbortError'; - throw error; - }, - { overwriteRoutes: true }, - ); + fetchMock.get('glob:*/api/v1/chart/*', () => { + const error = new Error('The operation was aborted'); + error.name = 'AbortError'; + throw error; + }); // Navigate to Usage tab to trigger fetchUsageData const usageTab = screen.getByRole('tab', { name: /usage/i }); - await userEvent.click(usageTab); + userEvent.click(usageTab); // Unmount immediately unmount(); @@ -554,7 +552,6 @@ test('immediate unmount after mount does not cause unhandled rejection from init fetchMock.get( 'glob:*/api/v1/chart/*', new Promise(() => {}), // Never resolves - will be aborted - { overwriteRoutes: true }, ); const props = createProps(); diff --git a/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditor.test.utils.tsx b/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditor.test.utils.tsx index 72aa9f02c595..442dfae754d5 100644 --- a/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditor.test.utils.tsx +++ b/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditor.test.utils.tsx @@ -109,21 +109,20 @@ export const fastRender = (renderProps: DatasourceEditorProps) => * Mocks the 3 endpoints called on component mount to prevent test hangs and async warnings. */ export const setupDatasourceEditorMocks = () => { - fetchMock.get( - url => url.includes('/api/v1/chart/'), - { result: [], count: 0, ids: [] }, - { overwriteRoutes: true }, - ); - fetchMock.get( - url => url.includes('/api/v1/database/'), - { result: [], count: 0, ids: [] }, - { overwriteRoutes: true }, - ); - fetchMock.get( - url => url.includes('/api/v1/dataset/related/owners'), - { result: [], count: 0 }, - { overwriteRoutes: true }, - ); + fetchMock.get(call => call.url.includes('/api/v1/chart/'), { + result: [], + count: 0, + ids: [], + }); + fetchMock.get(call => call.url.includes('/api/v1/database/'), { + result: [], + count: 0, + ids: [], + }); + fetchMock.get(call => call.url.includes('/api/v1/dataset/related/owners'), { + result: [], + count: 0, + }); }; /** diff --git a/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditorCurrency.test.tsx b/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditorCurrency.test.tsx index 1d9692c41d14..2c8e913f97d2 100644 --- a/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditorCurrency.test.tsx +++ b/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditorCurrency.test.tsx @@ -61,24 +61,25 @@ const setupCurrencySection = async () => { // Navigate to metrics tab - use findBy which has built-in waiting const metricButton = await screen.findByTestId('collection-tab-Metrics'); - await userEvent.click(metricButton); + userEvent.click(metricButton); // Expand the metric row const expandToggles = await screen.findAllByLabelText(/expand row/i); - await userEvent.click(expandToggles[0]); + userEvent.click(expandToggles[0]); // Wait for currency section to be visible await screen.findByText('Metric currency'); }; beforeEach(() => { - fetchMock.get(DATASOURCE_ENDPOINT, [], { overwriteRoutes: true }); + fetchMock.get(DATASOURCE_ENDPOINT, [], { name: DATASOURCE_ENDPOINT }); setupDatasourceEditorMocks(); }); afterEach(async () => { await cleanupAsyncOperations(); - fetchMock.restore(); + fetchMock.clearHistory(); + fetchMock.removeRoutes(); }); test('renders currency section in metrics tab', async () => { diff --git a/superset-frontend/src/components/ImportModal/ImportModal.test.tsx b/superset-frontend/src/components/ImportModal/ImportModal.test.tsx index 720d3a78643a..a19929bb5b68 100644 --- a/superset-frontend/src/components/ImportModal/ImportModal.test.tsx +++ b/superset-frontend/src/components/ImportModal/ImportModal.test.tsx @@ -28,7 +28,6 @@ const mockStore = configureStore([thunk]); const store = mockStore({}); const DATABASE_IMPORT_URL = 'glob:*/api/v1/database/import/'; -fetchMock.config.overwriteRoutes = true; fetchMock.post(DATABASE_IMPORT_URL, { result: 'OK' }); const requiredProps = { @@ -44,6 +43,7 @@ const requiredProps = { }; afterEach(() => { + fetchMock.clearHistory(); jest.clearAllMocks(); }); @@ -105,11 +105,13 @@ test('should POST with request header `Accept: application/json`', async () => { ); fireEvent.click(getByRole('button', { name: 'Import' })); await waitFor(() => - expect(fetchMock.calls(DATABASE_IMPORT_URL)).toHaveLength(1), + expect(fetchMock.callHistory.calls(DATABASE_IMPORT_URL)).toHaveLength(1), ); - expect(fetchMock.calls(DATABASE_IMPORT_URL)[0][1]?.headers).toStrictEqual({ - Accept: 'application/json', - 'X-CSRFToken': '1234', + expect( + fetchMock.callHistory.calls(DATABASE_IMPORT_URL)[0].options?.headers, + ).toStrictEqual({ + accept: 'application/json', + 'x-csrftoken': '1234', }); }); diff --git a/superset-frontend/src/components/ListView/ListView.test.tsx b/superset-frontend/src/components/ListView/ListView.test.tsx index 9d55ec1ad847..6434b11839f3 100644 --- a/superset-frontend/src/components/ListView/ListView.test.tsx +++ b/superset-frontend/src/components/ListView/ListView.test.tsx @@ -21,7 +21,6 @@ import userEvent from '@testing-library/user-event'; import { QueryParamProvider } from 'use-query-params'; import thunk from 'redux-thunk'; import configureStore from 'redux-mock-store'; -import fetchMock from 'fetch-mock'; import { ReactNode } from 'react'; import { ListView, type ListViewProps } from './ListView'; import { ListViewFilterOperator, type ListViewFetchDataConfig } from './types'; @@ -225,13 +224,11 @@ const factory = (overrides?: Partial) => { // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('ListView', () => { beforeEach(() => { - fetchMock.reset(); jest.clearAllMocks(); factory(); }); afterEach(() => { - fetchMock.reset(); mockedPropsComprehensive.fetchData.mockClear(); mockedPropsComprehensive.bulkActions.forEach(ba => { ba.onSelect.mockClear(); diff --git a/superset-frontend/src/components/TableSelector/TableSelector.test.tsx b/superset-frontend/src/components/TableSelector/TableSelector.test.tsx index f42305ad9f71..0afed6da163e 100644 --- a/superset-frontend/src/components/TableSelector/TableSelector.test.tsx +++ b/superset-frontend/src/components/TableSelector/TableSelector.test.tsx @@ -77,7 +77,7 @@ afterEach(async () => { act(() => { store.dispatch(api.util.resetApiState()); }); - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); // Wait for any pending effects to complete await new Promise(resolve => setTimeout(resolve, 0)); }); diff --git a/superset-frontend/src/components/Tag/utils.test.tsx b/superset-frontend/src/components/Tag/utils.test.tsx index 00c412738bad..19b411590843 100644 --- a/superset-frontend/src/components/Tag/utils.test.tsx +++ b/superset-frontend/src/components/Tag/utils.test.tsx @@ -42,11 +42,11 @@ describe('tagToSelectOption', () => { // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('loadTags', () => { beforeEach(() => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); }); afterEach(() => { - fetchMock.restore(); + fetchMock.clearHistory().removeRoutes(); }); test('constructs correct API query with custom tag filter', async () => { @@ -63,10 +63,10 @@ describe('loadTags', () => { await loadTags('analytics', 0, 25); // Verify the API was called with correct parameters - const calls = fetchMock.calls(); + const calls = fetchMock.callHistory.calls(); expect(calls).toHaveLength(1); - const [url] = calls[0]; + const { url } = calls[0]; expect(url).toContain('/api/v1/tag/?q='); // Extract and decode the query parameter @@ -118,8 +118,8 @@ describe('loadTags', () => { await loadTags('financial-data', 0, 25); - const calls = fetchMock.calls(); - const [url] = calls[0]; + const calls = fetchMock.callHistory.calls(); + const { url } = calls[0]; const urlObj = new URL(url); const queryParam = urlObj.searchParams.get('q'); expect(queryParam).not.toBeNull(); @@ -141,8 +141,8 @@ describe('loadTags', () => { await loadTags('', 2, 10); - const calls = fetchMock.calls(); - const [url] = calls[0]; + const calls = fetchMock.callHistory.calls(); + const { url } = calls[0]; const urlObj = new URL(url); const queryParam = urlObj.searchParams.get('q'); expect(queryParam).not.toBeNull(); @@ -163,11 +163,11 @@ describe('loadTags', () => { await loadTags('search-term', 1, 50); await loadTags('another-search', 5, 100); - const calls = fetchMock.calls(); + const calls = fetchMock.callHistory.calls(); // Verify all calls include the custom tag filter calls.forEach(call => { - const [url] = call; + const { url } = calls[0]; const urlObj = new URL(url); const queryParam = urlObj.searchParams.get('q'); expect(queryParam).not.toBeNull(); @@ -190,8 +190,8 @@ describe('loadTags', () => { await loadTags('test', 0, 25); - const calls = fetchMock.calls(); - const [url] = calls[0]; + const calls = fetchMock.callHistory.calls(); + const { url } = calls[0]; const urlObj = new URL(url); const queryParam = urlObj.searchParams.get('q'); expect(queryParam).not.toBeNull(); diff --git a/superset-frontend/src/dashboard/components/OverwriteConfirm/OverwriteConfirmModal.test.tsx b/superset-frontend/src/dashboard/components/OverwriteConfirm/OverwriteConfirmModal.test.tsx index 645a859bea59..cea0e872870f 100644 --- a/superset-frontend/src/dashboard/components/OverwriteConfirm/OverwriteConfirmModal.test.tsx +++ b/superset-frontend/src/dashboard/components/OverwriteConfirm/OverwriteConfirmModal.test.tsx @@ -56,11 +56,15 @@ test('requests update dashboard api when save button is clicked', async () => { // mock fetch datasets fetchMock.get(fetchDatasetsEndpoint, []); - fetchMock.put(updateDashboardEndpoint, { - id: overwriteConfirmMetadata.dashboardId, - last_modified_time: +new Date(), - result: overwriteConfirmMetadata.data, - }); + fetchMock.put( + updateDashboardEndpoint, + { + id: overwriteConfirmMetadata.dashboardId, + last_modified_time: +new Date(), + result: overwriteConfirmMetadata.data, + }, + { name: updateDashboardEndpoint }, + ); const store = mockStore({ dashboardLayout: { present: {} }, dashboardFilters: {}, @@ -77,15 +81,15 @@ test('requests update dashboard api when save button is clicked', async () => { }, ); const saveButton = await findByTestId('overwrite-confirm-save-button'); - expect(fetchMock.calls(updateDashboardEndpoint)).toHaveLength(0); + expect(fetchMock.callHistory.calls(updateDashboardEndpoint)).toHaveLength(0); fireEvent.click(saveButton); - expect(fetchMock.calls(updateDashboardEndpoint)).toHaveLength(0); + expect(fetchMock.callHistory.calls(updateDashboardEndpoint)).toHaveLength(0); mockAllIsIntersecting(true); fireEvent.click(saveButton); await waitFor(() => - expect(fetchMock.calls(updateDashboardEndpoint)?.[0]?.[1]?.body).toEqual( - JSON.stringify(overwriteConfirmMetadata.data), - ), + expect( + fetchMock.callHistory.calls(updateDashboardEndpoint)?.[0]?.options?.body, + ).toEqual(JSON.stringify(overwriteConfirmMetadata.data)), ); await waitFor(() => expect(store.getActions()).toContainEqual({ diff --git a/superset-frontend/src/dashboard/components/PropertiesModal/PropertiesModal.test.tsx b/superset-frontend/src/dashboard/components/PropertiesModal/PropertiesModal.test.tsx index f047db719e8a..23d37cfc9c75 100644 --- a/superset-frontend/src/dashboard/components/PropertiesModal/PropertiesModal.test.tsx +++ b/superset-frontend/src/dashboard/components/PropertiesModal/PropertiesModal.test.tsx @@ -178,7 +178,7 @@ beforeEach(() => { }); afterAll(() => { - fetchMock.restore(); + fetchMock.clearHistory().removeRoutes(); }); // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks diff --git a/superset-frontend/src/dashboard/components/URLShortLinkButton/URLShortLinkButton.test.tsx b/superset-frontend/src/dashboard/components/URLShortLinkButton/URLShortLinkButton.test.tsx index 72391ccdd230..fe4d92cea0da 100644 --- a/superset-frontend/src/dashboard/components/URLShortLinkButton/URLShortLinkButton.test.tsx +++ b/superset-frontend/src/dashboard/components/URLShortLinkButton/URLShortLinkButton.test.tsx @@ -39,10 +39,10 @@ fetchMock.get( FILTER_STATE_PAYLOAD, ); -fetchMock.post( - `glob:*/api/v1/dashboard/${DASHBOARD_ID}/permalink`, - PERMALINK_PAYLOAD, -); +const postDashboardPermanentlinkMockUrl = `glob:*/api/v1/dashboard/${DASHBOARD_ID}/permalink`; +fetchMock.post(postDashboardPermanentlinkMockUrl, PERMALINK_PAYLOAD, { + name: postDashboardPermanentlinkMockUrl, +}); test('renders with default props', () => { render(, { useRedux: true }); @@ -84,9 +84,8 @@ test('creates email anchor', async () => { }); test('renders error message on short url error', async () => { - fetchMock.mock(`glob:*/api/v1/dashboard/${DASHBOARD_ID}/permalink`, 500, { - overwriteRoutes: true, - }); + fetchMock.removeRoute(postDashboardPermanentlinkMockUrl); + fetchMock.route(`glob:*/api/v1/dashboard/${DASHBOARD_ID}/permalink`, 500); render( <> diff --git a/superset-frontend/src/dashboard/components/menu/ShareMenuItems/ShareMenuItems.test.tsx b/superset-frontend/src/dashboard/components/menu/ShareMenuItems/ShareMenuItems.test.tsx index 33f3d60494ec..2a6905b5e300 100644 --- a/superset-frontend/src/dashboard/components/menu/ShareMenuItems/ShareMenuItems.test.tsx +++ b/superset-frontend/src/dashboard/components/menu/ShareMenuItems/ShareMenuItems.test.tsx @@ -47,15 +47,15 @@ const createProps = () => ({ const { location } = window; +const postDashboardPermalinkMockUrl = `http://localhost/api/v1/dashboard/${DASHBOARD_ID}/permalink`; + beforeAll((): void => { // @ts-ignore delete window.location; fetchMock.post( - `http://localhost/api/v1/dashboard/${DASHBOARD_ID}/permalink`, + postDashboardPermalinkMockUrl, { key: '123', url: 'http://localhost/superset/dashboard/p/123/' }, - { - sendAsJson: true, - }, + { name: postDashboardPermalinkMockUrl }, ); }); @@ -67,6 +67,7 @@ beforeEach(() => { }); afterAll((): void => { + // @ts-ignore window.location = location; }); @@ -188,10 +189,11 @@ test('Click on "Share dashboard by email" and succeed', async () => { }); test('Click on "Share dashboard by email" and fail', async () => { + fetchMock.removeRoute(postDashboardPermalinkMockUrl); fetchMock.post( `http://localhost/api/v1/dashboard/${DASHBOARD_ID}/permalink`, { status: 404 }, - { overwriteRoutes: true }, + { name: postDashboardPermalinkMockUrl }, ); const props = createProps(); render( diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/ScopingModal/ScopingModal.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/ScopingModal/ScopingModal.test.tsx index 16276e702c71..32d5341e92af 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/ScopingModal/ScopingModal.test.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/ScopingModal/ScopingModal.test.tsx @@ -151,11 +151,12 @@ const setup = (props = DEFAULT_PROPS) => const DASHBOARD_UPDATE_URL = 'glob:*api/v1/dashboard/1'; beforeEach(() => { - fetchMock.put(DASHBOARD_UPDATE_URL, 200); + fetchMock.put(DASHBOARD_UPDATE_URL, 200, { name: DASHBOARD_UPDATE_URL }); }); afterEach(() => { - fetchMock.restore(); + fetchMock.clearHistory(); + fetchMock.removeRoutes(); }); test('renders modal', () => { @@ -265,11 +266,12 @@ test('edit scope and save', async () => { userEvent.click(screen.getByText('Save')); - await waitFor(() => fetchMock.called(DASHBOARD_UPDATE_URL)); + await waitFor(() => fetchMock.callHistory.called(DASHBOARD_UPDATE_URL)); expect( JSON.parse( - JSON.parse(fetchMock.lastCall()?.[1]?.body as string).json_metadata, + JSON.parse(fetchMock.callHistory.lastCall()?.options?.body as string) + .json_metadata, ), ).toEqual({ chart_configuration: { diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx index bc2376c6199c..f974194b0588 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx @@ -128,17 +128,28 @@ describe('FilterBar', () => { }; }); + const getTimeRangeNoFilterMockUrl = + 'glob:*/api/v1/time_range/?q=%27No%20filter%27'; + const getTimeRangeLastDayMockUrl = + 'glob:*/api/v1/time_range/?q=%27Last%20day%27'; + const getTimeRangeLastWeekMockUrl = + 'glob:*/api/v1/time_range/?q=%27Last%20week%27'; + beforeEach(() => { jest.clearAllMocks(); + + fetchMock.removeRoute(getTimeRangeNoFilterMockUrl); fetchMock.get( - 'glob:*/api/v1/time_range/?q=%27No%20filter%27', + getTimeRangeNoFilterMockUrl, { result: { since: '', until: '', timeRange: 'No filter' }, }, - { overwriteRoutes: true }, + { name: getTimeRangeNoFilterMockUrl }, ); + + fetchMock.removeRoute(getTimeRangeLastDayMockUrl); fetchMock.get( - 'glob:*/api/v1/time_range/?q=%27Last%20day%27', + getTimeRangeLastDayMockUrl, { result: { since: '2021-04-13T00:00:00', @@ -146,10 +157,12 @@ describe('FilterBar', () => { timeRange: 'Last day', }, }, - { overwriteRoutes: true }, + { name: getTimeRangeLastDayMockUrl }, ); + + fetchMock.removeRoute(getTimeRangeLastWeekMockUrl); fetchMock.get( - 'glob:*/api/v1/time_range/?q=%27Last%20week%27', + getTimeRangeLastWeekMockUrl, { result: { since: '2021-04-07T00:00:00', @@ -157,7 +170,7 @@ describe('FilterBar', () => { timeRange: 'Last week', }, }, - { overwriteRoutes: true }, + { name: getTimeRangeLastWeekMockUrl }, ); mockedMakeApi.mockReturnValue(mockApi); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/FilterBarSettings.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/FilterBarSettings.test.tsx index 54c6c7d03546..8e5ae3b36169 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/FilterBarSettings.test.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/FilterBarSettings.test.tsx @@ -71,7 +71,7 @@ const setup = (dashboardInfoOverride: Partial = {}) => ); beforeEach(() => { - fetchMock.restore(); + fetchMock.clearHistory().removeRoutes(); }); test('Dropdown trigger renders', async () => { @@ -195,7 +195,7 @@ test('On selection change, send request and update checked value', async () => { ).toBeInTheDocument(); await waitFor(() => - expect(fetchMock.lastCall()?.[1]?.body).toEqual( + expect(fetchMock.callHistory.lastCall()?.options?.body).toEqual( JSON.stringify({ json_metadata: JSON.stringify({ ...initialState.dashboardInfo.metadata, diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.test.tsx index 50cfc43962cc..7158c8ef3bcd 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.test.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.test.tsx @@ -68,7 +68,7 @@ const createProps = (extraProps: JsonObject = {}) => ({ }); afterAll(() => { - fetchMock.restore(); + fetchMock.clearHistory().removeRoutes(); }); test('Should render', async () => { diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DatasetSelect.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DatasetSelect.test.tsx index eaf53daca47c..fb8632b09d50 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DatasetSelect.test.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DatasetSelect.test.tsx @@ -57,7 +57,7 @@ const DATASETS = [ const mockOnChange = jest.fn(); afterEach(() => { - fetchMock.restore(); + fetchMock.clearHistory().removeRoutes(); jest.clearAllMocks(); }); @@ -205,7 +205,7 @@ test('includes table_name field in option data structure', async () => { test('uses API count instead of filteredResult.length', async () => { supersetGetCache.clear(); - fetchMock.restore(); + fetchMock.clearHistory().removeRoutes(); fetchMock.get('glob:*/api/v1/dataset/*', { result: [ { @@ -232,7 +232,7 @@ test('uses API count instead of filteredResult.length', async () => { test('returns total count from API when data is filtered', async () => { supersetGetCache.clear(); - fetchMock.restore(); + fetchMock.clearHistory().removeRoutes(); fetchMock.get('glob:*/api/v1/dataset/*', { result: [ diff --git a/superset-frontend/src/explore/actions/datasourcesActions.test.ts b/superset-frontend/src/explore/actions/datasourcesActions.test.ts index ace4913c9b4e..c3e0893c9d87 100644 --- a/superset-frontend/src/explore/actions/datasourcesActions.test.ts +++ b/superset-frontend/src/explore/actions/datasourcesActions.test.ts @@ -110,13 +110,13 @@ test('saveDataset handles success', async () => { const saveDatasetResponse = { data: datasource, }; - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); fetchMock.post(saveDatasetEndpoint, saveDatasetResponse); const dispatch = sinon.spy(); const getState = sinon.spy(() => ({ explore: { datasource } })); const dataset = await saveDataset(SAVE_DATASET_POST_ARGS)(dispatch); - expect(fetchMock.calls(saveDatasetEndpoint)).toHaveLength(1); + expect(fetchMock.callHistory.calls(saveDatasetEndpoint)).toHaveLength(1); expect(dispatch.callCount).toBe(1); const thunk = dispatch.getCall(0).args[0]; thunk(dispatch, getState); @@ -126,7 +126,7 @@ test('saveDataset handles success', async () => { }); test('updateSlice with add to existing dashboard handles failure', async () => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); const sampleError = new Error('sampleError'); mockedGetClientErrorObject.mockImplementation(() => Promise.resolve(sampleError), @@ -142,6 +142,6 @@ test('updateSlice with add to existing dashboard handles failure', async () => { } expect(caughtError).toEqual(sampleError); - expect(fetchMock.calls(saveDatasetEndpoint)).toHaveLength(4); + expect(fetchMock.callHistory.calls(saveDatasetEndpoint)).toHaveLength(4); expect(mockedGetClientErrorObject).toHaveBeenCalledWith(sampleError); }); diff --git a/superset-frontend/src/explore/actions/saveModalActions.test.ts b/superset-frontend/src/explore/actions/saveModalActions.test.ts index d5965a499676..04afdf659e12 100644 --- a/superset-frontend/src/explore/actions/saveModalActions.test.ts +++ b/superset-frontend/src/explore/actions/saveModalActions.test.ts @@ -98,13 +98,19 @@ jest.mock('../exploreUtils', () => ({ buildV1ChartDataPayload: jest.fn(() => queryContext), })); +beforeEach(() => { + fetchMock.removeRoutes(); + fetchMock.clearHistory(); +}); + /** * Tests updateSlice action */ const updateSliceEndpoint = `glob:*/api/v1/chart/${sliceId}`; test('updateSlice handles success', async () => { - fetchMock.reset(); - fetchMock.put(updateSliceEndpoint, sliceResponsePayload); + fetchMock.put(updateSliceEndpoint, sliceResponsePayload, { + name: updateSliceEndpoint, + }); const dispatchSpy = sinon.spy(); const dispatch = (action: any) => { dispatchSpy(action); @@ -136,7 +142,7 @@ test('updateSlice handles success', async () => { sliceName, [], )(dispatch as Dispatch, getState); - expect(fetchMock.calls(updateSliceEndpoint)).toHaveLength(1); + 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'); @@ -150,8 +156,11 @@ test('updateSlice handles success', async () => { }); test('updateSlice handles failure', async () => { - fetchMock.reset(); - fetchMock.put(updateSliceEndpoint, { throws: sampleError }); + fetchMock.put( + updateSliceEndpoint, + { throws: sampleError }, + { name: updateSliceEndpoint }, + ); const dispatchSpy = sinon.spy(); const dispatch = (action: any) => { @@ -192,7 +201,7 @@ test('updateSlice handles failure', async () => { } expect(caughtError).toEqual(sampleError); - expect(fetchMock.calls(updateSliceEndpoint)).toHaveLength(4); + expect(fetchMock.callHistory.calls(updateSliceEndpoint)).toHaveLength(4); expect(dispatchSpy.callCount).toBe(1); expect(dispatchSpy.getCall(0).args[0].type).toBe(SAVE_SLICE_FAILED); }); @@ -202,8 +211,9 @@ test('updateSlice handles failure', async () => { */ const createSliceEndpoint = `glob:*/api/v1/chart/`; test('createSlice handles success', async () => { - fetchMock.reset(); - fetchMock.post(createSliceEndpoint, sliceResponsePayload); + fetchMock.post(createSliceEndpoint, sliceResponsePayload, { + name: createSliceEndpoint, + }); const dispatchSpy = sinon.spy(); const dispatch = (action: any) => dispatchSpy(action); const getState = () => mockExploreState; @@ -211,7 +221,7 @@ test('createSlice handles success', async () => { dispatch as Dispatch, getState, ); - expect(fetchMock.calls(createSliceEndpoint)).toHaveLength(1); + 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); @@ -226,7 +236,6 @@ test('createSlice handles success', async () => { }); test('createSlice handles failure', async () => { - fetchMock.reset(); fetchMock.post(createSliceEndpoint, { throws: sampleError }); const dispatchSpy = sinon.spy(); @@ -241,7 +250,7 @@ test('createSlice handles failure', async () => { } expect(caughtError).toEqual(sampleError); - expect(fetchMock.calls(createSliceEndpoint)).toHaveLength(4); + expect(fetchMock.callHistory.calls(createSliceEndpoint)).toHaveLength(4); expect(dispatchSpy.callCount).toBe(1); expect(dispatchSpy.getCall(0).args[0].type).toBe(SAVE_SLICE_FAILED); }); @@ -257,20 +266,24 @@ const dashboardResponsePayload = { const createDashboardEndpoint = `glob:*/api/v1/dashboard/`; test('createDashboard handles success', async () => { - fetchMock.reset(); - fetchMock.post(createDashboardEndpoint, dashboardResponsePayload); + fetchMock.post(createDashboardEndpoint, dashboardResponsePayload, { + name: createDashboardEndpoint, + }); const dispatch = sinon.spy(); const dashboard = await createDashboard(dashboardName)( dispatch as Dispatch, ); - expect(fetchMock.calls(createDashboardEndpoint)).toHaveLength(1); + expect(fetchMock.callHistory.calls(createDashboardEndpoint)).toHaveLength(1); expect(dispatch.callCount).toBe(0); expect(dashboard).toEqual(dashboardResponsePayload); }); test('createDashboard handles failure', async () => { - fetchMock.reset(); - fetchMock.post(createDashboardEndpoint, { throws: sampleError }); + fetchMock.post( + createDashboardEndpoint, + { throws: sampleError }, + { name: createDashboardEndpoint }, + ); const dispatch = sinon.spy(); let caughtError; try { @@ -280,14 +293,15 @@ test('createDashboard handles failure', async () => { } expect(caughtError).toEqual(sampleError); - expect(fetchMock.calls(createDashboardEndpoint)).toHaveLength(4); + expect(fetchMock.callHistory.calls(createDashboardEndpoint)).toHaveLength(4); expect(dispatch.callCount).toBe(1); expect(dispatch.getCall(0).args[0].type).toBe(SAVE_SLICE_FAILED); }); test('updateSlice with add to new dashboard handles success', async () => { - fetchMock.reset(); - fetchMock.put(updateSliceEndpoint, sliceResponsePayload); + fetchMock.put(updateSliceEndpoint, sliceResponsePayload, { + name: updateSliceEndpoint, + }); const dispatchSpy = sinon.spy(); const dispatch = (action: any) => dispatchSpy(action); const getState = () => mockExploreState; @@ -327,7 +341,7 @@ test('updateSlice with add to new dashboard handles success', async () => { }, )(dispatch as Dispatch, getState); - expect(fetchMock.calls(updateSliceEndpoint)).toHaveLength(1); + 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); @@ -349,8 +363,9 @@ test('updateSlice with add to new dashboard handles success', async () => { }); test('updateSlice with add to existing dashboard handles success', async () => { - fetchMock.reset(); - fetchMock.put(updateSliceEndpoint, sliceResponsePayload); + fetchMock.put(updateSliceEndpoint, sliceResponsePayload, { + name: updateSliceEndpoint, + }); const dispatchSpy = sinon.spy(); const dispatch = (action: any) => dispatchSpy(action); const getState = () => mockExploreState; @@ -389,7 +404,7 @@ test('updateSlice with add to existing dashboard handles success', async () => { }, )(dispatch as Dispatch, getState); - expect(fetchMock.calls(updateSliceEndpoint)).toHaveLength(1); + 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); @@ -422,8 +437,9 @@ const getDashboardSlicesReturnValue = [21, 22, 23]; const getSliceDashboardsEndpoint = `glob:*/api/v1/chart/${sliceId}?q=(select_columns:!(dashboards.id))`; test('getSliceDashboards with slice handles success', async () => { - fetchMock.reset(); - fetchMock.get(getSliceDashboardsEndpoint, dashboardSlicesResponsePayload); + fetchMock.get(getSliceDashboardsEndpoint, dashboardSlicesResponsePayload, { + name: getSliceDashboardsEndpoint, + }); const dispatchSpy = sinon.spy(); const dispatch = (action: any) => dispatchSpy(action); const sliceDashboards = await getSliceDashboards({ @@ -436,14 +452,19 @@ test('getSliceDashboards with slice handles success', async () => { dashboards: [], }, })(dispatch as Dispatch); - expect(fetchMock.calls(getSliceDashboardsEndpoint)).toHaveLength(1); + expect(fetchMock.callHistory.calls(getSliceDashboardsEndpoint)).toHaveLength( + 1, + ); expect(dispatchSpy.callCount).toBe(0); expect(sliceDashboards).toEqual(getDashboardSlicesReturnValue); }); test('getSliceDashboards with slice handles failure', async () => { - fetchMock.reset(); - fetchMock.get(getSliceDashboardsEndpoint, { throws: sampleError }); + fetchMock.get( + getSliceDashboardsEndpoint, + { throws: sampleError }, + { name: getSliceDashboardsEndpoint }, + ); const dispatch = sinon.spy(); let caughtError; try { @@ -462,7 +483,9 @@ test('getSliceDashboards with slice handles failure', async () => { } expect(caughtError).toEqual(sampleError); - expect(fetchMock.calls(getSliceDashboardsEndpoint)).toHaveLength(4); + expect(fetchMock.callHistory.calls(getSliceDashboardsEndpoint)).toHaveLength( + 4, + ); expect(dispatch.callCount).toBe(1); expect(dispatch.getCall(0).args[0].type).toBe(SAVE_SLICE_FAILED); }); diff --git a/superset-frontend/src/explore/components/DataTablesPane/test/DataTablesPane.test.tsx b/superset-frontend/src/explore/components/DataTablesPane/test/DataTablesPane.test.tsx index 2e6249e9e6ed..1d5fc7a63ff8 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/test/DataTablesPane.test.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/test/DataTablesPane.test.tsx @@ -70,7 +70,9 @@ describe('DataTablesPane', () => { useRedux: true, }); userEvent.click(screen.getByText('Results')); - expect(await screen.findByText('0 rows')).toBeVisible(); + expect( + await screen.findByText('0 rows', undefined, { timeout: 5000 }), + ).toBeVisible(); expect(await screen.findByLabelText('Collapse data panel')).toBeVisible(); localStorage.clear(); }); @@ -81,7 +83,9 @@ describe('DataTablesPane', () => { useRedux: true, }); userEvent.click(screen.getByText('Samples')); - expect(await screen.findByText('0 rows')).toBeVisible(); + expect( + await screen.findByText('0 rows', undefined, { timeout: 5000 }), + ).toBeVisible(); expect(await screen.findByLabelText('Collapse data panel')).toBeVisible(); }); @@ -113,7 +117,7 @@ describe('DataTablesPane', () => { const value = await copyToClipboardSpy.mock.calls[0][0](); expect(value).toBe('__timestamp\tgenre\n2009-01-01 00:00:00\tAction\n'); copyToClipboardSpy.mockRestore(); - fetchMock.restore(); + fetchMock.clearHistory().removeRoutes(); }); test('Should not allow copy data table content when canDownload=false', async () => { @@ -141,7 +145,7 @@ describe('DataTablesPane', () => { userEvent.click(screen.getByText('Results')); expect(await screen.findByText('1 row')).toBeVisible(); expect(screen.queryByLabelText('Copy')).not.toBeInTheDocument(); - fetchMock.restore(); + fetchMock.clearHistory().removeRoutes(); }); test('Search table', async () => { @@ -177,7 +181,7 @@ describe('DataTablesPane', () => { await waitForElementToBeRemoved(() => screen.queryByText('Action')); expect(screen.getByText('Horror')).toBeVisible(); expect(screen.queryByText('Action')).not.toBeInTheDocument(); - fetchMock.restore(); + fetchMock.clearHistory().removeRoutes(); }); test('Displaying the data pane is under featureflag', () => { diff --git a/superset-frontend/src/explore/components/DataTablesPane/test/ResultsPaneOnDashboard.test.tsx b/superset-frontend/src/explore/components/DataTablesPane/test/ResultsPaneOnDashboard.test.tsx index cf523a0a6426..6f870e0d384e 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/test/ResultsPaneOnDashboard.test.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/test/ResultsPaneOnDashboard.test.tsx @@ -90,7 +90,7 @@ describe('ResultsPaneOnDashboard', () => { const setForceQuery = jest.fn(); afterAll(() => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); jest.resetAllMocks(); }); diff --git a/superset-frontend/src/explore/components/DataTablesPane/test/SamplesPane.test.tsx b/superset-frontend/src/explore/components/DataTablesPane/test/SamplesPane.test.tsx index 2fa34acf2eae..d5da57b6d3b6 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/test/SamplesPane.test.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/test/SamplesPane.test.tsx @@ -63,7 +63,7 @@ describe('SamplesPane', () => { const setForceQuery = jest.fn(); afterAll(() => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); jest.resetAllMocks(); }); diff --git a/superset-frontend/src/explore/components/ExploreChartHeader/ExploreChartHeader.test.tsx b/superset-frontend/src/explore/components/ExploreChartHeader/ExploreChartHeader.test.tsx index a1969c99612a..5530eacefc5b 100644 --- a/superset-frontend/src/explore/components/ExploreChartHeader/ExploreChartHeader.test.tsx +++ b/superset-frontend/src/explore/components/ExploreChartHeader/ExploreChartHeader.test.tsx @@ -144,9 +144,6 @@ const createProps = (additionalProps = {}) => ({ fetchMock.post( 'http://api/v1/chart/data?form_data=%7B%22slice_id%22%3A318%7D', { body: {} }, - { - sendAsJson: false, - }, ); // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('ExploreChartHeader', () => { diff --git a/superset-frontend/src/explore/components/ExploreChartPanel/ExploreChartPanel.test.jsx b/superset-frontend/src/explore/components/ExploreChartPanel/ExploreChartPanel.test.jsx index a1d53d3ec026..a52ab35b6e72 100644 --- a/superset-frontend/src/explore/components/ExploreChartPanel/ExploreChartPanel.test.jsx +++ b/superset-frontend/src/explore/components/ExploreChartPanel/ExploreChartPanel.test.jsx @@ -170,7 +170,11 @@ describe('ChartContainer', () => { useRedux: true, }); const tabpanel = screen.getByRole('tabpanel', { name: /results/i }); - expect(await within(tabpanel).findByText(/0 rows/i)).toBeInTheDocument(); + expect( + await within(tabpanel).findByText(/0 rows/i, undefined, { + timeout: 5000, + }), + ).toBeInTheDocument(); const gutter = container.querySelector('.gutter'); expect(gutter).toBeVisible(); diff --git a/superset-frontend/src/explore/components/ExploreViewContainer/ExploreViewContainer.test.tsx b/superset-frontend/src/explore/components/ExploreViewContainer/ExploreViewContainer.test.tsx index 44efbaaef516..b2db0cb3b855 100644 --- a/superset-frontend/src/explore/components/ExploreViewContainer/ExploreViewContainer.test.tsx +++ b/superset-frontend/src/explore/components/ExploreViewContainer/ExploreViewContainer.test.tsx @@ -258,11 +258,13 @@ test('retains query mode requirements when query_mode is enabled', async () => { await waitFor(() => renderWithRouter({ initialState: customState })); - const formDataEndpointCalls = fetchMock.calls(/api\/v1\/explore\/form_data/); + const formDataEndpointCalls = fetchMock.callHistory.calls( + /api\/v1\/explore\/form_data/, + ); expect(formDataEndpointCalls.length).toBeGreaterThan(0); const lastCall = formDataEndpointCalls[formDataEndpointCalls.length - 1]; - const body = JSON.parse(lastCall[1]?.body as string); + const body = JSON.parse(lastCall.options?.body as string); const formData = JSON.parse(body.form_data); const queryModeFields = Object.keys( @@ -296,11 +298,13 @@ test('does omit hiddenFormData when query_mode is not enabled', async () => { await waitFor(() => renderWithRouter({ initialState: customState })); - const formDataEndpointCalls = fetchMock.calls(/api\/v1\/explore\/form_data/); + const formDataEndpointCalls = fetchMock.callHistory.calls( + /api\/v1\/explore\/form_data/, + ); expect(formDataEndpointCalls.length).toBeGreaterThan(0); const lastCall = formDataEndpointCalls[formDataEndpointCalls.length - 1]; - const body = JSON.parse(lastCall[1]?.body as string); + const body = JSON.parse(lastCall.options?.body as string); const formData = JSON.parse(body.form_data); Object.keys(customState.explore.hiddenFormData).forEach(key => { diff --git a/superset-frontend/src/explore/components/PropertiesModal/PropertiesModal.test.tsx b/superset-frontend/src/explore/components/PropertiesModal/PropertiesModal.test.tsx index 9abc906c3bfa..6eaaa5dc1983 100644 --- a/superset-frontend/src/explore/components/PropertiesModal/PropertiesModal.test.tsx +++ b/superset-frontend/src/explore/components/PropertiesModal/PropertiesModal.test.tsx @@ -135,7 +135,7 @@ fetchMock.put('glob:*/api/v1/chart/318', { }); afterAll(() => { - fetchMock.resetBehavior(); + fetchMock.clearHistory().removeRoutes(); }); const renderModal = (props: PropertiesModalProps) => diff --git a/superset-frontend/src/explore/components/SaveModal.test.jsx b/superset-frontend/src/explore/components/SaveModal.test.jsx index 3d5d34ec190d..be445884f17f 100644 --- a/superset-frontend/src/explore/components/SaveModal.test.jsx +++ b/superset-frontend/src/explore/components/SaveModal.test.jsx @@ -132,7 +132,7 @@ beforeAll(() => { }); }); -afterAll(() => fetchMock.restore()); +afterAll(() => fetchMock.clearHistory()); const setup = (props = defaultProps, store = initialStore) => render(, { @@ -290,9 +290,9 @@ test('updates slice name and selected dashboard', async () => { }), ); await waitFor(() => - expect(fetchMock.calls(fetchDashboardEndpoint)).toHaveLength(1), + expect(fetchMock.callHistory.calls(fetchDashboardEndpoint)).toHaveLength(1), ); - expect(fetchMock.calls(fetchDashboardEndpoint)[0][0]).toEqual( + expect(fetchMock.callHistory.calls(fetchDashboardEndpoint)[0].url).toEqual( expect.stringContaining(`dashboard/${dashboardId}`), ); expect(createSlice).toHaveBeenCalledWith( @@ -371,17 +371,13 @@ test('dispatches removeChartState when saving and going to dashboard', async () // Mock the dashboard API response const dashboardId = 123; const dashboardUrl = '/superset/dashboard/test-dashboard/'; - fetchMock.get( - `glob:*/api/v1/dashboard/${dashboardId}*`, - { - result: { - id: dashboardId, - dashboard_title: 'Test Dashboard', - url: dashboardUrl, - }, + fetchMock.get(`glob:*/api/v1/dashboard/${dashboardId}*`, { + result: { + id: dashboardId, + dashboard_title: 'Test Dashboard', + url: dashboardUrl, }, - { overwriteRoutes: true }, - ); + }); const mockDispatch = jest.fn(); const mockHistory = { diff --git a/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.test.tsx b/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.test.tsx index 0c6780f57df7..b9f9f9f1a06b 100644 --- a/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.test.tsx +++ b/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.test.tsx @@ -181,7 +181,7 @@ test('fetches Superset annotation layer options', async () => { screen.getByRole('combobox', { name: 'Annotation layer value' }), ); expect(await screen.findByText('Chart A')).toBeInTheDocument(); - expect(fetchMock.calls(nativeLayerApiRoute).length).toBe(1); + expect(fetchMock.callHistory.calls(nativeLayerApiRoute).length).toBe(1); }); test('fetches chart options', async () => { @@ -197,7 +197,7 @@ test('fetches chart options', async () => { screen.getByRole('combobox', { name: 'Annotation layer value' }), ); expect(await screen.findByText('Chart A')).toBeInTheDocument(); - expect(fetchMock.calls(chartApiRoute).length).toBe(1); + expect(fetchMock.callHistory.calls(chartApiRoute).length).toBe(1); }); test('fetches chart on mount if value present', async () => { @@ -207,7 +207,7 @@ test('fetches chart on mount if value present', async () => { annotationType: ANNOTATION_TYPES_METADATA.EVENT.value, sourceType: 'Table', }); - expect(fetchMock.calls(chartApiWithIdRoute).length).toBe(1); + expect(fetchMock.callHistory.calls(chartApiWithIdRoute).length).toBe(1); }); test('keeps apply disabled when missing required fields', async () => { diff --git a/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.tsx b/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.tsx index edfcf62c3b8b..619964f02708 100644 --- a/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.tsx +++ b/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.tsx @@ -41,9 +41,10 @@ beforeEach(() => { }); afterEach(() => { + // @ts-ignore window.location = originalLocation; - fetchMock.reset(); - fetchMock.restore(); + fetchMock.clearHistory(); + fetchMock.removeRoutes(); jest.clearAllMocks(); // Clears mock history but keeps spy in place }); @@ -126,26 +127,25 @@ const createProps = ( ...overrides, }) as unknown as DatasourceControlComponentProps; +const getDbWithQuery = 'glob:*/api/v1/database/?q=*'; +const getDatasetWithAll = 'glob:*/api/v1/dataset/*'; +const putDatasetWithAll = 'glob:*/api/v1/dataset/*'; + async function openAndSaveChanges( datasource: TestDatasource | Record, ) { + fetchMock.removeRoute(getDbWithQuery); + fetchMock.get(getDbWithQuery, { result: [] }, { name: getDbWithQuery }); + + fetchMock.removeRoute('put' + putDatasetWithAll); + fetchMock.put(putDatasetWithAll, {}, { name: 'put' + putDatasetWithAll }); + + fetchMock.removeRoute('get' + getDatasetWithAll); fetchMock.get( - 'glob:*/api/v1/database/?q=*', - { result: [] }, - { overwriteRoutes: true }, - ); - fetchMock.put( - 'glob:*/api/v1/dataset/*', - {}, - { - overwriteRoutes: true, - }, - ); - fetchMock.get( - 'glob:*/api/v1/dataset/*', + getDatasetWithAll, { result: datasource }, { - overwriteRoutes: true, + name: 'get' + getDatasetWithAll, }, ); await userEvent.click(screen.getByTestId('datasource-menu-trigger')); @@ -262,19 +262,16 @@ test('Click on Edit dataset', async () => { SupersetClientGet.mockImplementationOnce( async () => ({ json: { result: [] } }) as any, ); - fetchMock.get( - 'glob:*/api/v1/database/?q=*', - { result: [] }, - { overwriteRoutes: true }, - ); + fetchMock.removeRoute(getDbWithQuery); + fetchMock.get(getDbWithQuery, { result: [] }, { name: getDbWithQuery }); render(, { useRedux: true, useRouter: true, }); - await userEvent.click(screen.getByTestId('datasource-menu-trigger')); + userEvent.click(screen.getByTestId('datasource-menu-trigger')); await act(async () => { - await userEvent.click(screen.getByText('Edit dataset')); + userEvent.click(screen.getByText('Edit dataset')); }); expect( @@ -581,28 +578,16 @@ test('should allow creating new metrics in dataset editor', async () => { }); // Mock API calls for dataset editor - fetchMock.get( - 'glob:*/api/v1/database/?q=*', - { result: [] }, - { overwriteRoutes: true }, - ); + fetchMock.get(getDbWithQuery, { response: { result: [] } }); - fetchMock.get( - 'glob:*/api/v1/dataset/*', - { result: mockDatasourceWithMetrics }, - { overwriteRoutes: true }, - ); + fetchMock.get(getDatasetWithAll, { result: mockDatasourceWithMetrics }); - fetchMock.put( - 'glob:*/api/v1/dataset/*', - { - result: { - ...mockDatasourceWithMetrics, - metrics: [{ id: 1, metric_name: newMetricName }], - }, + fetchMock.put(putDatasetWithAll, { + result: { + ...mockDatasourceWithMetrics, + metrics: [{ id: 1, metric_name: newMetricName }], }, - { overwriteRoutes: true }, - ); + }); SupersetClientGet.mockImplementationOnce( async () => ({ json: { result: [] } }) as any, @@ -614,31 +599,31 @@ test('should allow creating new metrics in dataset editor', async () => { }); // Open datasource menu and click edit dataset - await userEvent.click(screen.getByTestId('datasource-menu-trigger')); - await userEvent.click(await screen.findByTestId('edit-dataset')); + userEvent.click(screen.getByTestId('datasource-menu-trigger')); + userEvent.click(await screen.findByTestId('edit-dataset')); // Wait for modal to appear and navigate to Metrics tab await waitFor(() => { expect(screen.getByText('Metrics')).toBeInTheDocument(); }); - await userEvent.click(screen.getByText('Metrics')); + userEvent.click(screen.getByText('Metrics')); // Click add new metric button const addButton = await screen.findByTestId('crud-add-table-item'); - await userEvent.click(addButton); + userEvent.click(addButton); // Find and fill in the metric name const nameInput = await screen.findByTestId('textarea-editable-title-input'); - await userEvent.clear(nameInput); - await userEvent.type(nameInput, newMetricName); + userEvent.clear(nameInput); + userEvent.type(nameInput, newMetricName); // Save the modal - await userEvent.click(screen.getByTestId('datasource-modal-save')); + userEvent.click(screen.getByTestId('datasource-modal-save')); // Confirm the save const okButton = await screen.findByText('OK'); - await userEvent.click(okButton); + userEvent.click(okButton); // Verify the onDatasourceSave callback was called await waitFor(() => { @@ -658,23 +643,15 @@ test('should allow deleting metrics in dataset editor', async () => { }); // Mock API calls - fetchMock.get( - 'glob:*/api/v1/database/?q=*', - { result: [] }, - { overwriteRoutes: true }, - ); + fetchMock.get('glob:*/api/v1/database/?q=*', { result: [] }); - fetchMock.get( - 'glob:*/api/v1/dataset/*', - { result: mockDatasourceWithMetrics }, - { overwriteRoutes: true }, - ); + fetchMock.get('glob:*/api/v1/dataset/*', { + result: mockDatasourceWithMetrics, + }); - fetchMock.put( - 'glob:*/api/v1/dataset/*', - { result: { ...mockDatasourceWithMetrics, metrics: [] } }, - { overwriteRoutes: true }, - ); + fetchMock.put('glob:*/api/v1/dataset/*', { + result: { ...mockDatasourceWithMetrics, metrics: [] }, + }); SupersetClientGet.mockImplementationOnce( async () => ({ json: { result: [] } }) as any, @@ -722,23 +699,11 @@ test('should handle metric save confirmation modal', async () => { const props = createProps(); // Mock API calls for dataset editor - fetchMock.get( - 'glob:*/api/v1/database/?q=*', - { result: [] }, - { overwriteRoutes: true }, - ); + fetchMock.get('glob:*/api/v1/database/?q=*', { result: [] }); - fetchMock.get( - 'glob:*/api/v1/dataset/*', - { result: mockDatasource }, - { overwriteRoutes: true }, - ); + fetchMock.get('glob:*/api/v1/dataset/*', { result: mockDatasource }); - fetchMock.put( - 'glob:*/api/v1/dataset/*', - { result: mockDatasource }, - { overwriteRoutes: true }, - ); + fetchMock.put('glob:*/api/v1/dataset/*', { result: mockDatasource }); SupersetClientGet.mockImplementationOnce( async () => ({ json: { result: [] } }) as any, @@ -782,23 +747,11 @@ test('should verify real DatasourceControl callback fires on save', async () => }); // Mock API calls with the same datasource (no changes needed for this test) - fetchMock.get( - 'glob:*/api/v1/database/?q=*', - { result: [] }, - { overwriteRoutes: true }, - ); + fetchMock.get('glob:*/api/v1/database/?q=*', { result: [] }); - fetchMock.get( - 'glob:*/api/v1/dataset/*', - { result: mockDatasource }, - { overwriteRoutes: true }, - ); + fetchMock.get('glob:*/api/v1/dataset/*', { result: mockDatasource }); - fetchMock.put( - 'glob:*/api/v1/dataset/*', - { result: mockDatasource }, - { overwriteRoutes: true }, - ); + fetchMock.put('glob:*/api/v1/dataset/*', { result: mockDatasource }); SupersetClientGet.mockImplementationOnce( async () => ({ json: { result: [] } }) as any, 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 d15aef375094..f9b3e13a20b4 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 @@ -174,7 +174,7 @@ const store = mockStore({}); let isFeatureEnabledMock: jest.SpyInstance; beforeEach(() => { - fetchMock.resetHistory(); + fetchMock.clearHistory(); isFeatureEnabledMock = mockedIsFeatureEnabled.mockImplementation( (featureFlag: FeatureFlag) => featureFlag === FeatureFlag.EnableAdvancedDataTypes, @@ -460,7 +460,9 @@ test('should not call API when column has no advanced data type', async () => { }); await waitFor(() => - expect(fetchMock.calls(ADVANCED_DATA_TYPE_ENDPOINT_VALID)).toHaveLength(0), + expect( + fetchMock.callHistory.calls(ADVANCED_DATA_TYPE_ENDPOINT_VALID), + ).toHaveLength(0), ); }); @@ -499,7 +501,9 @@ test('should call API when column has advanced data type', async () => { }); await waitFor(() => - expect(fetchMock.calls(ADVANCED_DATA_TYPE_ENDPOINT_VALID)).toHaveLength(1), + expect( + fetchMock.callHistory.calls(ADVANCED_DATA_TYPE_ENDPOINT_VALID), + ).toHaveLength(1), ); expect(props.validHandler.lastCall.args[0]).toBe(true); }); @@ -539,9 +543,9 @@ test('save button should be disabled if error message from API is returned', asy }); await waitFor(() => - expect(fetchMock.calls(ADVANCED_DATA_TYPE_ENDPOINT_INVALID)).toHaveLength( - 1, - ), + expect( + fetchMock.callHistory.calls(ADVANCED_DATA_TYPE_ENDPOINT_INVALID), + ).toHaveLength(1), ); expect(props.validHandler.lastCall.args[0]).toBe(false); }); @@ -581,7 +585,9 @@ test('advanced data type operator list should update after API response', async }); await waitFor(() => - expect(fetchMock.calls(ADVANCED_DATA_TYPE_ENDPOINT_VALID)).toHaveLength(1), + expect( + fetchMock.callHistory.calls(ADVANCED_DATA_TYPE_ENDPOINT_VALID), + ).toHaveLength(1), ); expect(props.validHandler.lastCall.args[0]).toBe(true); diff --git a/superset-frontend/src/explore/components/controls/ViewQuery.test.tsx b/superset-frontend/src/explore/components/controls/ViewQuery.test.tsx index dbce360a173d..bfea6fca5fa6 100644 --- a/superset-frontend/src/explore/components/controls/ViewQuery.test.tsx +++ b/superset-frontend/src/explore/components/controls/ViewQuery.test.tsx @@ -73,21 +73,30 @@ const formatSqlEndpoint = 'glob:*/api/v1/sqllab/format_sql/'; const formattedSQL = 'SELECT * FROM table;'; beforeEach(() => { - fetchMock.get(datasetApiEndpoint, { - result: { - database: { - backend: 'sqlite', + fetchMock.get( + datasetApiEndpoint, + { + result: { + database: { + backend: 'sqlite', + }, }, }, - }); - fetchMock.post(formatSqlEndpoint, { - result: formattedSQL, - }); + { name: datasetApiEndpoint }, + ); + fetchMock.post( + formatSqlEndpoint, + { + result: formattedSQL, + }, + { name: formatSqlEndpoint }, + ); }); afterEach(() => { jest.resetAllMocks(); - fetchMock.restore(); + fetchMock.clearHistory(); + fetchMock.removeRoutes(); }); const getFormatSwitch = () => @@ -100,7 +109,7 @@ test('renders the component with Formatted SQL and buttons', async () => { expect(screen.getByText('View in SQL Lab')).toBeInTheDocument(); await waitFor(() => - expect(fetchMock.calls(formatSqlEndpoint)).toHaveLength(1), + expect(fetchMock.callHistory.calls(formatSqlEndpoint)).toHaveLength(1), ); expect(container).toHaveTextContent(formattedSQL); @@ -121,7 +130,7 @@ test('shows the original SQL when Format switch is unchecked', async () => { const formatButton = getFormatSwitch(); await waitFor(() => - expect(fetchMock.calls(formatSqlEndpoint)).toHaveLength(1), + expect(fetchMock.callHistory.calls(formatSqlEndpoint)).toHaveLength(1), ); fireEvent.click(formatButton); @@ -134,7 +143,7 @@ test('toggles back to formatted SQL when Format switch is clicked', async () => const formatButton = getFormatSwitch(); await waitFor(() => - expect(fetchMock.calls(formatSqlEndpoint)).toHaveLength(1), + expect(fetchMock.callHistory.calls(formatSqlEndpoint)).toHaveLength(1), ); // Click to format SQL @@ -207,11 +216,8 @@ test('handles dataset API error gracefully when no exploreBackend', async () => explore: undefined, }; - fetchMock.get( - datasetApiEndpoint, - { throws: new Error('API Error') }, - { overwriteRoutes: true }, - ); + fetchMock.removeRoute(datasetApiEndpoint); + fetchMock.get(datasetApiEndpoint, { throws: new Error('API Error') }); setup(mockProps, stateWithoutBackend); @@ -219,7 +225,7 @@ test('handles dataset API error gracefully when no exploreBackend', async () => expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); }); - expect(fetchMock.calls(formatSqlEndpoint)).toHaveLength(0); + expect(fetchMock.callHistory.calls(formatSqlEndpoint)).toHaveLength(0); }); test('handles SQL formatting API error gracefully', async () => { @@ -228,11 +234,8 @@ test('handles SQL formatting API error gracefully', async () => { explore: undefined, }; - fetchMock.post( - formatSqlEndpoint, - { throws: new Error('Format Error') }, - { overwriteRoutes: true }, - ); + fetchMock.removeRoute(formatSqlEndpoint); + fetchMock.post(formatSqlEndpoint, { throws: new Error('Format Error') }); setup(mockProps, stateWithoutBackend); @@ -256,14 +259,14 @@ test('uses exploreBackend from Redux state when available', async () => { setup(mockProps, stateWithBackend); await waitFor(() => { - expect(fetchMock.calls(formatSqlEndpoint)).toHaveLength(1); + expect(fetchMock.callHistory.calls(formatSqlEndpoint)).toHaveLength(1); }); const formatCallBody = JSON.parse( - fetchMock.lastCall(formatSqlEndpoint)?.[1]?.body as string, + fetchMock.callHistory.lastCall(formatSqlEndpoint)?.options.body as string, ); expect(formatCallBody.engine).toBe('postgresql'); - expect(fetchMock.calls(datasetApiEndpoint)).toHaveLength(0); + expect(fetchMock.callHistory.calls(datasetApiEndpoint)).toHaveLength(0); }); test('sends engine as string (not object) when fetched from dataset API', async () => { @@ -275,15 +278,15 @@ test('sends engine as string (not object) when fetched from dataset API', async setup(mockProps, stateWithoutBackend); await waitFor(() => { - expect(fetchMock.calls(datasetApiEndpoint)).toHaveLength(1); + expect(fetchMock.callHistory.calls(datasetApiEndpoint)).toHaveLength(1); }); await waitFor(() => { - expect(fetchMock.calls(formatSqlEndpoint)).toHaveLength(1); + expect(fetchMock.callHistory.calls(formatSqlEndpoint)).toHaveLength(1); }); const formatCallBody = JSON.parse( - fetchMock.lastCall(formatSqlEndpoint)?.[1]?.body as string, + fetchMock.callHistory.lastCall(formatSqlEndpoint)?.options.body as string, ); expect(formatCallBody).toEqual({ diff --git a/superset-frontend/src/explore/components/controls/ViewQueryModal.test.tsx b/superset-frontend/src/explore/components/controls/ViewQueryModal.test.tsx index 64a53aceeb22..8b03ab89c82a 100644 --- a/superset-frontend/src/explore/components/controls/ViewQueryModal.test.tsx +++ b/superset-frontend/src/explore/components/controls/ViewQueryModal.test.tsx @@ -30,7 +30,7 @@ const chartDataEndpoint = 'glob:*/api/v1/chart/data*'; afterEach(() => { jest.resetAllMocks(); - fetchMock.restore(); + fetchMock.clearHistory().removeRoutes(); }); test('renders Alert component when query result contains validation error', async () => { @@ -40,14 +40,18 @@ test('renders Alert component when query result contains validation error', asyn * component instead of showing a blank panel */ // Mock API response with validation error - fetchMock.post(chartDataEndpoint, { - result: [ - { - error: 'Missing temporal column', - language: 'sql', - }, - ], - }); + fetchMock.post( + chartDataEndpoint, + { + result: [ + { + error: 'Missing temporal column', + language: 'sql', + }, + ], + }, + { name: chartDataEndpoint }, + ); render(, { useRedux: true, @@ -55,7 +59,7 @@ test('renders Alert component when query result contains validation error', asyn // Wait for API call to complete await waitFor(() => - expect(fetchMock.calls(chartDataEndpoint)).toHaveLength(1), + expect(fetchMock.callHistory.calls(chartDataEndpoint)).toHaveLength(1), ); // Assert Alert component is rendered with error message @@ -73,15 +77,19 @@ test('renders both Alert and SQL query when parsing error occurs', async () => { * For parsing errors, the SQL was successfully compiled but optimization failed. */ // Mock API response with parsing error (has both query and error) - fetchMock.post(chartDataEndpoint, { - result: [ - { - query: 'SELECT SUM ( Open', - error: "Error parsing near 'Open' at line 1:17", - language: 'sql', - }, - ], - }); + fetchMock.post( + chartDataEndpoint, + { + result: [ + { + query: 'SELECT SUM ( Open', + error: "Error parsing near 'Open' at line 1:17", + language: 'sql', + }, + ], + }, + { name: chartDataEndpoint }, + ); render(, { useRedux: true, @@ -100,9 +108,12 @@ test('renders both Alert and SQL query when parsing error occurs', async () => { // Assert SQL query is also displayed // Note: The SQL is rendered inside a syntax-highlighted code block where // each keyword is in a separate span element - await waitFor(() => { - expect(screen.getByText('SELECT')).toBeInTheDocument(); - expect(screen.getByText('SUM')).toBeInTheDocument(); - expect(screen.getByText('Open')).toBeInTheDocument(); - }); + await waitFor( + () => { + expect(screen.getByText('SELECT')).toBeInTheDocument(); + expect(screen.getByText('SUM')).toBeInTheDocument(); + expect(screen.getByText('Open')).toBeInTheDocument(); + }, + { timeout: 5000 }, + ); }); diff --git a/superset-frontend/src/extensions/ExtensionsList.test.tsx b/superset-frontend/src/extensions/ExtensionsList.test.tsx index b877b34056f1..471a74cb5233 100644 --- a/superset-frontend/src/extensions/ExtensionsList.test.tsx +++ b/superset-frontend/src/extensions/ExtensionsList.test.tsx @@ -18,6 +18,9 @@ */ import { render, waitFor } from 'spec/helpers/testing-library'; import ExtensionsList from './ExtensionsList'; +import fetchMock from 'fetch-mock'; + +beforeAll(() => fetchMock.unmockGlobal()); // Mock initial state for the store const mockInitialState = { @@ -77,11 +80,14 @@ test('displays extension names in the list', async () => { test('displays contributions information', async () => { renderWithStore(); - await waitFor(() => { - // Should show contributions-related content - const bodyText = document.body.textContent || ''; - expect(bodyText).toMatch(/contribution/i); - }); + await waitFor( + () => { + // Should show contributions-related content + const bodyText = document.body.textContent || ''; + expect(bodyText).toMatch(/contribution/i); + }, + { timeout: 4000 }, + ); }); test('calls toast functions when provided', () => { diff --git a/superset-frontend/src/extensions/ExtensionsManager.test.ts b/superset-frontend/src/extensions/ExtensionsManager.test.ts index a3b06349b761..6babbf5f5c45 100644 --- a/superset-frontend/src/extensions/ExtensionsManager.test.ts +++ b/superset-frontend/src/extensions/ExtensionsManager.test.ts @@ -215,7 +215,7 @@ beforeEach(() => { (ExtensionsManager as any).instance = undefined; // Setup fetch mocks for API calls - fetchMock.restore(); + fetchMock.removeRoutes().clearHistory(); fetchMock.put('glob:*/api/v1/extensions/*', { ok: true }); fetchMock.delete('glob:*/api/v1/extensions/*', { ok: true }); fetchMock.get('glob:*/api/v1/extensions/', { @@ -231,7 +231,7 @@ beforeEach(() => { afterEach(() => { // Clean up after each test (ExtensionsManager as any).instance = undefined; - fetchMock.restore(); + fetchMock.removeRoutes().clearHistory(); }); test('creates singleton instance', () => { diff --git a/superset-frontend/src/extensions/ExtensionsStartup.test.tsx b/superset-frontend/src/extensions/ExtensionsStartup.test.tsx index 7c05a2953307..dfa870e4766f 100644 --- a/superset-frontend/src/extensions/ExtensionsStartup.test.tsx +++ b/superset-frontend/src/extensions/ExtensionsStartup.test.tsx @@ -54,7 +54,7 @@ beforeEach(() => { mockIsFeatureEnabled.mockReturnValue(true); // Setup fetch mocks for API calls - fetchMock.restore(); + fetchMock.clearHistory().removeRoutes(); fetchMock.get('glob:*/api/v1/extensions/', { result: [], }); @@ -67,7 +67,7 @@ afterEach(() => { // Reset mocks mockIsFeatureEnabled.mockReset(); - fetchMock.restore(); + fetchMock.clearHistory().removeRoutes(); }); test('renders without crashing', () => { diff --git a/superset-frontend/src/features/alerts/AlertReportModal.test.tsx b/superset-frontend/src/features/alerts/AlertReportModal.test.tsx index 6e78540f6ced..45d896851ad4 100644 --- a/superset-frontend/src/features/alerts/AlertReportModal.test.tsx +++ b/superset-frontend/src/features/alerts/AlertReportModal.test.tsx @@ -119,12 +119,16 @@ fetchMock.get(ownersEndpoint, { result: [] }); fetchMock.get(databaseEndpoint, { result: [] }); fetchMock.get(dashboardEndpoint, { result: [] }); fetchMock.get(chartEndpoint, { result: [{ text: 'table chart', value: 1 }] }); -fetchMock.get(tabsEndpoint, { - result: { - all_tabs: {}, - tab_tree: [], +fetchMock.get( + tabsEndpoint, + { + result: { + all_tabs: {}, + tab_tree: [], + }, }, -}); + { name: tabsEndpoint }, +); // Create a valid alert with all required fields entered for validation check @@ -682,45 +686,37 @@ test('renders dashboard filter dropdowns', async () => { }); test('filter reappears in dropdown after clearing with X icon', async () => { - const tabsWithFiltersEndpoint = 'glob:*/api/v1/dashboard/1/tabs'; const chartDataEndpoint = 'glob:*/api/v1/chart/data*'; - fetchMock.get( - tabsWithFiltersEndpoint, - { - result: { - all_tabs: { tab1: 'Tab 1' }, - tab_tree: [{ title: 'Tab 1', value: 'tab1' }], - native_filters: { - all: [ - { - id: 'NATIVE_FILTER-test1', - name: 'Test Filter 1', - filterType: 'filter_select', - targets: [{ column: { name: 'test_column_1' } }], - adhoc_filters: [], - }, - ], - tab1: [ - { - id: 'NATIVE_FILTER-test2', - name: 'Test Filter 2', - filterType: 'filter_select', - targets: [{ column: { name: 'test_column_2' } }], - adhoc_filters: [], - }, - ], - }, + fetchMock.removeRoute(tabsEndpoint); + fetchMock.get(tabsEndpoint, { + result: { + all_tabs: { tab1: 'Tab 1' }, + tab_tree: [{ title: 'Tab 1', value: 'tab1' }], + native_filters: { + all: [ + { + id: 'NATIVE_FILTER-test1', + name: 'Test Filter 1', + filterType: 'filter_select', + targets: [{ column: { name: 'test_column_1' } }], + adhoc_filters: [], + }, + ], + tab1: [ + { + id: 'NATIVE_FILTER-test2', + name: 'Test Filter 2', + filterType: 'filter_select', + targets: [{ column: { name: 'test_column_2' } }], + adhoc_filters: [], + }, + ], }, }, - { overwriteRoutes: true }, - ); + }); - fetchMock.post( - chartDataEndpoint, - { result: [{ data: [] }] }, - { overwriteRoutes: true }, - ); + fetchMock.post(chartDataEndpoint, { result: [{ data: [] }] }); render(, { useRedux: true, diff --git a/superset-frontend/src/features/databases/DatabaseModal/index.test.tsx b/superset-frontend/src/features/databases/DatabaseModal/index.test.tsx index 9b2b967e2be0..4d90e7d6d580 100644 --- a/superset-frontend/src/features/databases/DatabaseModal/index.test.tsx +++ b/superset-frontend/src/features/databases/DatabaseModal/index.test.tsx @@ -100,7 +100,7 @@ describe('DatabaseModal', () => { configuration_method: 'sqlalchemy_form', }, }); - fetchMock.mock(AVAILABLE_DB_ENDPOINT, { + fetchMock.route(AVAILABLE_DB_ENDPOINT, { databases: [ { available_drivers: ['psycopg2'], @@ -320,7 +320,8 @@ describe('DatabaseModal', () => { jest.clearAllMocks(); }); afterEach(() => { - fetchMock.restore(); + fetchMock.clearHistory(); + fetchMock.removeRoutes(); }); const setup = (propsOverwrite: Partial = {}) => @@ -1402,7 +1403,9 @@ describe('DatabaseModal', () => { expect(connectButton).toBeEnabled(); userEvent.click(connectButton); await waitFor(() => { - expect(fetchMock.calls(VALIDATE_PARAMS_ENDPOINT).length).toEqual(5); + expect( + fetchMock.callHistory.calls(VALIDATE_PARAMS_ENDPOINT).length, + ).toEqual(5); }); }); }); diff --git a/superset-frontend/src/features/databases/UploadDataModel/UploadDataModal.test.tsx b/superset-frontend/src/features/databases/UploadDataModel/UploadDataModal.test.tsx index 81d1f8c31c60..0796de193981 100644 --- a/superset-frontend/src/features/databases/UploadDataModel/UploadDataModal.test.tsx +++ b/superset-frontend/src/features/databases/UploadDataModel/UploadDataModal.test.tsx @@ -94,7 +94,7 @@ beforeEach(() => { }); afterEach(() => { - fetchMock.restore(); + fetchMock.clearHistory().removeRoutes(); }); // Helper function to get common elements @@ -472,16 +472,19 @@ describe('UploadDataModal - Form Submission', () => { const uploadButton = screen.getByRole('button', { name: 'Upload' }); await userEvent.click(uploadButton); - await waitFor(() => fetchMock.called('glob:*api/v1/database/1/upload/'), { - timeout: 10000, - }); - return fetchMock.calls('glob:*api/v1/database/1/upload/')[0]; + await waitFor( + () => fetchMock.callHistory.called('glob:*api/v1/database/1/upload/'), + { + timeout: 10000, + }, + ); + return fetchMock.callHistory.calls('glob:*api/v1/database/1/upload/')[0]; }; test('CSV form submission', async () => { render(, { useRedux: true }); - const [, options] = await fillForm('csv', 'test.csv'); + const { options } = await fillForm('csv', 'test.csv'); const formData = options?.body as FormData; expect(formData.get('type')).toBe('csv'); @@ -493,7 +496,7 @@ describe('UploadDataModal - Form Submission', () => { test('Excel form submission', async () => { render(, { useRedux: true }); - const [, options] = await fillForm('excel', 'test.xls', 'text'); + const { options } = await fillForm('excel', 'test.xls', 'text'); const formData = options?.body as FormData; expect(formData.get('type')).toBe('excel'); @@ -505,7 +508,7 @@ describe('UploadDataModal - Form Submission', () => { test('Columnar form submission', async () => { render(, { useRedux: true }); - const [, options] = await fillForm('columnar', 'test.parquet', 'text'); + const { options } = await fillForm('columnar', 'test.parquet', 'text'); const formData = options?.body as FormData; expect(formData.get('type')).toBe('columnar'); diff --git a/superset-frontend/src/features/datasets/AddDataset/EditDataset/EditDataset.test.tsx b/superset-frontend/src/features/datasets/AddDataset/EditDataset/EditDataset.test.tsx index 5657fa9fcc5a..184e15d11c00 100644 --- a/superset-frontend/src/features/datasets/AddDataset/EditDataset/EditDataset.test.tsx +++ b/superset-frontend/src/features/datasets/AddDataset/EditDataset/EditDataset.test.tsx @@ -35,7 +35,7 @@ test('should render edit dataset view with tabs', async () => { const metricsTab = screen.getByRole('tab', { name: /metrics/i }); const usageTab = screen.getByRole('tab', { name: /usage/i }); - expect(fetchMock.calls(DATASET_ENDPOINT)).toBeTruthy(); + expect(fetchMock.callHistory.calls(DATASET_ENDPOINT)).toBeTruthy(); expect(columnTab).toBeInTheDocument(); expect(metricsTab).toBeInTheDocument(); expect(usageTab).toBeInTheDocument(); diff --git a/superset-frontend/src/features/datasets/AddDataset/LeftPanel/LeftPanel.test.tsx b/superset-frontend/src/features/datasets/AddDataset/LeftPanel/LeftPanel.test.tsx index 4625ce485bfa..847ef1974355 100644 --- a/superset-frontend/src/features/datasets/AddDataset/LeftPanel/LeftPanel.test.tsx +++ b/superset-frontend/src/features/datasets/AddDataset/LeftPanel/LeftPanel.test.tsx @@ -155,7 +155,7 @@ beforeEach(() => { }); afterEach(() => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); }); const mockFun = jest.fn(); @@ -242,7 +242,9 @@ test('searches for a table name', async () => { // Click 'public' schema to access tables userEvent.click(schemaSelect); userEvent.click(screen.getByText('public')); - await waitFor(() => expect(fetchMock.calls(tablesEndpoint).length).toBe(1)); + await waitFor(() => + expect(fetchMock.callHistory.calls(tablesEndpoint).length).toBe(1), + ); userEvent.click(tableSelect); await waitFor(() => { diff --git a/superset-frontend/src/features/datasets/metadataBar/useDatasetMetadataBar.test.tsx b/superset-frontend/src/features/datasets/metadataBar/useDatasetMetadataBar.test.tsx index 7f700657c3d8..6be5603cea29 100644 --- a/superset-frontend/src/features/datasets/metadataBar/useDatasetMetadataBar.test.tsx +++ b/superset-frontend/src/features/datasets/metadataBar/useDatasetMetadataBar.test.tsx @@ -39,7 +39,7 @@ const MOCK_DATASET = { }; afterEach(() => { - fetchMock.restore(); + fetchMock.clearHistory().removeRoutes(); supersetGetCache.clear(); }); diff --git a/superset-frontend/src/features/home/ChartTable.test.tsx b/superset-frontend/src/features/home/ChartTable.test.tsx index 483acb077133..4425fe696a7e 100644 --- a/superset-frontend/src/features/home/ChartTable.test.tsx +++ b/superset-frontend/src/features/home/ChartTable.test.tsx @@ -109,7 +109,9 @@ test('fetches chart favorites and renders chart cards', async () => { await renderChartTable(mockedProps); userEvent.click(screen.getByText(/favorite/i)); await waitFor(() => { - expect(fetchMock.calls(chartFavoriteStatusEndpoint)).toHaveLength(1); + expect( + fetchMock.callHistory.calls(chartFavoriteStatusEndpoint), + ).toHaveLength(1); expect(screen.getAllByText(/cool chart/i)).toHaveLength(3); }); }); diff --git a/superset-frontend/src/features/home/DashboardTable.test.tsx b/superset-frontend/src/features/home/DashboardTable.test.tsx index 9962027f3047..7e4d34897dc9 100644 --- a/superset-frontend/src/features/home/DashboardTable.test.tsx +++ b/superset-frontend/src/features/home/DashboardTable.test.tsx @@ -127,13 +127,15 @@ beforeEach(() => { }), ); + const getDashboardMockUrl = 'glob:*/api/v1/dashboard/*'; + fetchMock.removeRoute(getDashboardMockUrl); fetchMock.get( - 'glob:*/api/v1/dashboard/*', + getDashboardMockUrl, { result: mockDashboards[0], }, - { overwriteRoutes: true }, - ); // Add overwriteRoutes option + { name: getDashboardMockUrl }, + ); // Mock loading state for first render jest.spyOn(hooks, 'useListViewResource').mockImplementationOnce(() => ({ diff --git a/superset-frontend/src/features/home/RightMenu.test.tsx b/superset-frontend/src/features/home/RightMenu.test.tsx index 3454cc0bb9b3..e861a00342a1 100644 --- a/superset-frontend/src/features/home/RightMenu.test.tsx +++ b/superset-frontend/src/features/home/RightMenu.test.tsx @@ -154,19 +154,26 @@ const mockNonExamplesDB = Array.from({ length: 2 }) const useSelectorMock = jest.spyOn(reactRedux, 'useSelector'); +const getDatabaseWithFileFiterMockUrl = + 'glob:*api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))'; +const getDatabaseWithNameFilterMockUrl = + 'glob:*api/v1/database/?q=(filters:!((col:database_name,opr:neq,value:examples)))'; + beforeEach(async () => { useSelectorMock.mockReset(); fetchMock.get( - 'glob:*api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))', + getDatabaseWithFileFiterMockUrl, { result: [], count: 0 }, + { name: getDatabaseWithFileFiterMockUrl }, ); fetchMock.get( - 'glob:*api/v1/database/?q=(filters:!((col:database_name,opr:neq,value:examples)))', + getDatabaseWithNameFilterMockUrl, { result: [], count: 0 }, + { name: getDatabaseWithNameFilterMockUrl }, ); }); -afterEach(() => fetchMock.restore()); +afterEach(() => fetchMock.clearHistory().removeRoutes()); const resetUseSelectorMock = () => { useSelectorMock.mockReturnValueOnce({ @@ -222,28 +229,24 @@ test('If user has permission to upload files AND connect DBs we query existing D useTheme: true, }); await waitFor(() => expect(container).toBeVisible()); - const callsD = fetchMock.calls(/database\/\?q/); + const callsD = fetchMock.callHistory.calls(/database\/\?q/); expect(callsD).toHaveLength(2); - expect(callsD[0][0]).toMatchInlineSnapshot( + expect(callsD[0].url).toMatchInlineSnapshot( `"http://localhost/api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))"`, ); - expect(callsD[1][0]).toMatchInlineSnapshot( + expect(callsD[1].url).toMatchInlineSnapshot( `"http://localhost/api/v1/database/?q=(filters:!((col:database_name,opr:neq,value:examples)))"`, ); }); test('If only examples DB exist we must show the Connect Database option', async () => { const mockedProps = createProps(); - fetchMock.get( - 'glob:*api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))', - { result: [...mockNonExamplesDB], count: 2 }, - { overwriteRoutes: true }, - ); - fetchMock.get( - 'glob:*api/v1/database/?q=(filters:!((col:database_name,opr:neq,value:examples)))', - { result: [], count: 0 }, - { overwriteRoutes: true }, - ); + fetchMock.modifyRoute(getDatabaseWithFileFiterMockUrl, { + response: { result: [...mockNonExamplesDB], count: 2 }, + }); + fetchMock.modifyRoute(getDatabaseWithNameFilterMockUrl, { + response: { result: [], count: 0 }, + }); // Initial Load resetUseSelectorMock(); // setAllowUploads called @@ -266,16 +269,12 @@ test('If only examples DB exist we must show the Connect Database option', async test('If more than just examples DB exist we must show the Create dataset option', async () => { const mockedProps = createProps(); - fetchMock.get( - 'glob:*api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))', - { result: [...mockNonExamplesDB], count: 2 }, - { overwriteRoutes: true }, - ); - fetchMock.get( - 'glob:*api/v1/database/?q=(filters:!((col:database_name,opr:neq,value:examples)))', - { result: [...mockNonExamplesDB], count: 2 }, - { overwriteRoutes: true }, - ); + fetchMock.modifyRoute(getDatabaseWithFileFiterMockUrl, { + response: { result: [...mockNonExamplesDB], count: 2 }, + }); + fetchMock.modifyRoute(getDatabaseWithNameFilterMockUrl, { + response: { result: [...mockNonExamplesDB], count: 2 }, + }); // Initial Load resetUseSelectorMock(); // setAllowUploads called @@ -301,12 +300,10 @@ test('If there is a DB with allow_file_upload set as True the option should be e fetchMock.get( 'glob:*api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))', { result: [...mockNonExamplesDB], count: 2 }, - { overwriteRoutes: true }, ); fetchMock.get( 'glob:*api/v1/database/?q=(filters:!((col:database_name,opr:neq,value:examples)))', { result: [...mockNonExamplesDB], count: 2 }, - { overwriteRoutes: true }, ); // Initial load resetUseSelectorMock(); @@ -338,12 +335,10 @@ test('If there is NOT a DB with allow_file_upload set as True the option should fetchMock.get( 'glob:*api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))', { result: [], count: 0 }, - { overwriteRoutes: true }, ); fetchMock.get( 'glob:*api/v1/database/?q=(filters:!((col:database_name,opr:neq,value:examples)))', { result: [...mockNonExamplesDB], count: 2 }, - { overwriteRoutes: true }, ); // Initial load resetUseSelectorMock(); diff --git a/superset-frontend/src/features/reports/ReportModal/ReportModal.test.tsx b/superset-frontend/src/features/reports/ReportModal/ReportModal.test.tsx index 4f4d90cf36d1..c0c121528f9b 100644 --- a/superset-frontend/src/features/reports/ReportModal/ReportModal.test.tsx +++ b/superset-frontend/src/features/reports/ReportModal/ReportModal.test.tsx @@ -160,9 +160,11 @@ describe('Email Report Modal', () => { // 🐞 ----- There are 2 POST calls at this point ----- 🐞 // addReport's mocked POST return should match the mocked values - expect(fetchMock.lastOptions()?.body).toEqual(stringyReportValues); + expect(fetchMock.callHistory.lastCall()?.options?.body).toEqual( + stringyReportValues, + ); expect(dispatch.callCount).toBe(2); - const reportCalls = fetchMock.calls(REPORT_ENDPOINT); + const reportCalls = fetchMock.callHistory.calls(REPORT_ENDPOINT); expect(reportCalls).toHaveLength(2); }); }); diff --git a/superset-frontend/src/features/rls/RowLevelSecurityModal.test.tsx b/superset-frontend/src/features/rls/RowLevelSecurityModal.test.tsx index d60c16431de4..6a06e3ba05b9 100644 --- a/superset-frontend/src/features/rls/RowLevelSecurityModal.test.tsx +++ b/superset-frontend/src/features/rls/RowLevelSecurityModal.test.tsx @@ -131,7 +131,7 @@ const mockGetTablesResult = { fetchMock.get(getRuleEndpoint, mockGetRuleResult); fetchMock.get(getRelatedRolesEndpoint, mockGetRolesResult); fetchMock.get(getRelatedTablesEndpoint, mockGetTablesResult); -fetchMock.post(postRuleEndpoint, {}); +fetchMock.post(postRuleEndpoint, {}, { name: postRuleEndpoint }); fetchMock.put(putRuleEndpoint, {}); global.URL.createObjectURL = jest.fn(); @@ -159,9 +159,13 @@ describe('Rule modal', () => { await renderAndWait(addNewRuleDefaultProps); const title = screen.getByText('Add Rule'); expect(title).toBeInTheDocument(); - expect(fetchMock.calls(getRuleEndpoint)).toHaveLength(0); - expect(fetchMock.calls(getRelatedTablesEndpoint)).toHaveLength(0); - expect(fetchMock.calls(getRelatedRolesEndpoint)).toHaveLength(0); + expect(fetchMock.callHistory.calls(getRuleEndpoint)).toHaveLength(0); + expect(fetchMock.callHistory.calls(getRelatedTablesEndpoint)).toHaveLength( + 0, + ); + expect(fetchMock.callHistory.calls(getRelatedRolesEndpoint)).toHaveLength( + 0, + ); }); test('Sets correct title for editing existing rule', async () => { @@ -177,9 +181,13 @@ describe('Rule modal', () => { }); const title = screen.getByText('Edit Rule'); expect(title).toBeInTheDocument(); - expect(fetchMock.calls(getRuleEndpoint)).toHaveLength(1); - expect(fetchMock.calls(getRelatedTablesEndpoint)).toHaveLength(0); - expect(fetchMock.calls(getRelatedRolesEndpoint)).toHaveLength(0); + expect(fetchMock.callHistory.calls(getRuleEndpoint)).toHaveLength(1); + expect(fetchMock.callHistory.calls(getRelatedTablesEndpoint)).toHaveLength( + 0, + ); + expect(fetchMock.callHistory.calls(getRelatedRolesEndpoint)).toHaveLength( + 0, + ); }); test('Fills correct values when editing rule', async () => { @@ -260,11 +268,13 @@ describe('Rule modal', () => { const clause = await screen.findByTestId('clause-test'); userEvent.type(clause, 'gender="girl"'); + fetchMock.clearHistory(); + await waitFor(() => userEvent.click(addButton), { timeout: 10000 }); await waitFor( () => { - expect(fetchMock.calls(postRuleEndpoint)).toHaveLength(1); + expect(fetchMock.callHistory.calls(postRuleEndpoint)).toHaveLength(1); }, { timeout: 10000 }, ); @@ -285,12 +295,12 @@ describe('Rule modal', () => { await waitFor( () => { - const allCalls = fetchMock.calls(putRuleEndpoint); + const allCalls = fetchMock.callHistory.calls(putRuleEndpoint); // Find the PUT request among all calls - const putCall = allCalls.find(call => call[1]?.method === 'PUT'); + const putCall = allCalls.find(call => call.options?.method === 'put'); expect(putCall).toBeTruthy(); - expect(putCall?.[1]?.body).toContain('"name":"rls 1"'); - expect(putCall?.[1]?.body).toContain('"filter_type":"Base"'); + expect(putCall?.options?.body).toContain('"name":"rls 1"'); + expect(putCall?.options?.body).toContain('"filter_type":"Base"'); }, { timeout: 10000 }, ); diff --git a/superset-frontend/src/features/tags/BulkTagModal.test.tsx b/superset-frontend/src/features/tags/BulkTagModal.test.tsx index 7c3a58803af6..5b6ba04b9b61 100644 --- a/superset-frontend/src/features/tags/BulkTagModal.test.tsx +++ b/superset-frontend/src/features/tags/BulkTagModal.test.tsx @@ -39,7 +39,7 @@ const mockedProps = { // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('BulkTagModal', () => { afterEach(() => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); jest.clearAllMocks(); }); diff --git a/superset-frontend/src/features/themes/ThemeModal.test.tsx b/superset-frontend/src/features/themes/ThemeModal.test.tsx index 995b3c902f22..2e5d1a516556 100644 --- a/superset-frontend/src/features/themes/ThemeModal.test.tsx +++ b/superset-frontend/src/features/themes/ThemeModal.test.tsx @@ -64,17 +64,28 @@ const mockSystemTheme: ThemeObject = { is_system: true, }; +const postThemeMockName = 'postTheme'; +const putThemeMockName = 'putTheme'; + beforeEach(() => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); fetchMock.get('glob:*/api/v1/theme/1', { result: mockTheme }); fetchMock.get('glob:*/api/v1/theme/2', { result: mockSystemTheme }); fetchMock.get('glob:*/api/v1/theme/*', { result: mockTheme }); - fetchMock.post('glob:*/api/v1/theme/', { result: { ...mockTheme, id: 3 } }); - fetchMock.put('glob:*/api/v1/theme/*', { result: mockTheme }); + fetchMock.post( + 'glob:*/api/v1/theme/', + { result: { ...mockTheme, id: 3 } }, + { name: postThemeMockName }, + ); + fetchMock.put( + 'glob:*/api/v1/theme/*', + { result: mockTheme }, + { name: putThemeMockName }, + ); }); afterEach(() => { - fetchMock.restore(); + fetchMock.clearHistory().removeRoutes(); jest.clearAllMocks(); }); @@ -420,7 +431,7 @@ test('saves changes when clicking Save button in unsaved changes alert', async ( // Wait for API call to complete await screen.findByRole('dialog'); - expect(fetchMock.called()).toBe(true); + expect(fetchMock.callHistory.called()).toBe(true); }); test('discards changes when clicking Discard button in unsaved changes alert', async () => { @@ -451,8 +462,8 @@ test('discards changes when clicking Discard button in unsaved changes alert', a await userEvent.click(discardButton); expect(onHide).toHaveBeenCalled(); - expect(fetchMock.called('glob:*/api/v1/theme/', 'POST')).toBe(false); - expect(fetchMock.called('glob:*/api/v1/theme/*', 'PUT')).toBe(false); + expect(fetchMock.callHistory.called()).toBe(false); + expect(fetchMock.callHistory.called(putThemeMockName)).toBe(false); }); test('creates new theme when saving', async () => { @@ -477,7 +488,7 @@ test('creates new theme when saving', async () => { await userEvent.click(saveButton); expect(await screen.findByRole('dialog')).toBeInTheDocument(); - expect(fetchMock.called('glob:*/api/v1/theme/', 'POST')).toBe(true); + expect(fetchMock.callHistory.called(postThemeMockName)).toBe(true); }); test('updates existing theme when saving', async () => { @@ -504,11 +515,11 @@ test('updates existing theme when saving', async () => { await userEvent.click(saveButton); expect(await screen.findByRole('dialog')).toBeInTheDocument(); - expect(fetchMock.called('glob:*/api/v1/theme/*', 'PUT')).toBe(true); + expect(fetchMock.callHistory.called(putThemeMockName)).toBe(true); }); test('handles API errors gracefully', async () => { - fetchMock.restore(); + fetchMock.clearHistory().removeRoutes(); fetchMock.post('glob:*/api/v1/theme/', 500); render( @@ -532,7 +543,7 @@ test('handles API errors gracefully', async () => { await userEvent.click(saveButton); await screen.findByRole('dialog'); - expect(fetchMock.called()).toBe(true); + expect(fetchMock.callHistory.called()).toBe(true); }); test('applies theme locally when clicking Apply button', async () => { @@ -560,7 +571,7 @@ test('applies theme locally when clicking Apply button', async () => { }); test('disables Apply button when JSON configuration is invalid', async () => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); fetchMock.get('glob:*/api/v1/theme/*', { result: { ...mockTheme, json_data: 'invalid json' }, }); diff --git a/superset-frontend/src/features/themes/api.test.ts b/superset-frontend/src/features/themes/api.test.ts index de2b246b3609..642e6a2a4208 100644 --- a/superset-frontend/src/features/themes/api.test.ts +++ b/superset-frontend/src/features/themes/api.test.ts @@ -27,11 +27,11 @@ import { // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('Theme API', () => { beforeEach(() => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); }); afterEach(() => { - fetchMock.restore(); + fetchMock.clearHistory().removeRoutes(); }); // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks @@ -42,9 +42,11 @@ describe('Theme API', () => { await setSystemDefaultTheme(1); - expect(fetchMock.called('glob:*/api/v1/theme/1/set_system_default')).toBe( - true, - ); + expect( + fetchMock.callHistory.called( + 'glob:*/api/v1/theme/1/set_system_default', + ), + ).toBe(true); }); test('should handle errors properly', async () => { @@ -64,9 +66,9 @@ describe('Theme API', () => { await setSystemDarkTheme(2); - expect(fetchMock.called('glob:*/api/v1/theme/2/set_system_dark')).toBe( - true, - ); + expect( + fetchMock.callHistory.called('glob:*/api/v1/theme/2/set_system_dark'), + ).toBe(true); }); test('should handle errors properly', async () => { @@ -89,9 +91,11 @@ describe('Theme API', () => { await unsetSystemDefaultTheme(); - expect(fetchMock.called('glob:*/api/v1/theme/unset_system_default')).toBe( - true, - ); + expect( + fetchMock.callHistory.called( + 'glob:*/api/v1/theme/unset_system_default', + ), + ).toBe(true); }); test('should handle errors properly', async () => { @@ -111,9 +115,9 @@ describe('Theme API', () => { await unsetSystemDarkTheme(); - expect(fetchMock.called('glob:*/api/v1/theme/unset_system_dark')).toBe( - true, - ); + expect( + fetchMock.callHistory.called('glob:*/api/v1/theme/unset_system_dark'), + ).toBe(true); }); test('should handle errors properly', async () => { diff --git a/superset-frontend/src/hooks/apiResources/dashboards.test.ts b/superset-frontend/src/hooks/apiResources/dashboards.test.ts index 11271a2a14f8..7676c5c8e870 100644 --- a/superset-frontend/src/hooks/apiResources/dashboards.test.ts +++ b/superset-frontend/src/hooks/apiResources/dashboards.test.ts @@ -53,7 +53,7 @@ describe('useDashboardDatasets', () => { ]; beforeEach(() => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); }); test('adds currencyFormats to datasets', async () => { diff --git a/superset-frontend/src/hooks/apiResources/databaseFunctions.test.ts b/superset-frontend/src/hooks/apiResources/databaseFunctions.test.ts index 8eca5112a1a8..70069a004783 100644 --- a/superset-frontend/src/hooks/apiResources/databaseFunctions.test.ts +++ b/superset-frontend/src/hooks/apiResources/databaseFunctions.test.ts @@ -34,7 +34,7 @@ const expectDbId = 'db1'; const dbFunctionNamesApiRoute = `glob:*/api/v1/database/${expectDbId}/function_names/`; afterEach(() => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); act(() => { store.dispatch(api.util.resetApiState()); }); @@ -58,15 +58,15 @@ test('returns api response mapping json result', async () => { }, ); await waitFor(() => - expect(fetchMock.calls(dbFunctionNamesApiRoute).length).toBe(1), + expect(fetchMock.callHistory.calls(dbFunctionNamesApiRoute).length).toBe(1), ); expect(result.current.data).toEqual(expectedResult); - expect(fetchMock.calls(dbFunctionNamesApiRoute).length).toBe(1); + expect(fetchMock.callHistory.calls(dbFunctionNamesApiRoute).length).toBe(1); act(() => { result.current.refetch(); }); await waitFor(() => - expect(fetchMock.calls(dbFunctionNamesApiRoute).length).toBe(2), + expect(fetchMock.callHistory.calls(dbFunctionNamesApiRoute).length).toBe(2), ); expect(result.current.data).toEqual(expectedResult); }); @@ -85,8 +85,8 @@ test('returns cached data without api request', async () => { }, ); await waitFor(() => expect(result.current.data).toEqual(expectedResult)); - expect(fetchMock.calls(dbFunctionNamesApiRoute).length).toBe(1); + expect(fetchMock.callHistory.calls(dbFunctionNamesApiRoute).length).toBe(1); rerender(); await waitFor(() => expect(result.current.data).toEqual(expectedResult)); - expect(fetchMock.calls(dbFunctionNamesApiRoute).length).toBe(1); + expect(fetchMock.callHistory.calls(dbFunctionNamesApiRoute).length).toBe(1); }); diff --git a/superset-frontend/src/hooks/apiResources/queries.test.ts b/superset-frontend/src/hooks/apiResources/queries.test.ts index 76939756e6e5..61f14c0ba31c 100644 --- a/superset-frontend/src/hooks/apiResources/queries.test.ts +++ b/superset-frontend/src/hooks/apiResources/queries.test.ts @@ -66,7 +66,7 @@ const fakeApiResult = { }; afterEach(() => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); act(() => { store.dispatch(api.util.resetApiState()); }); @@ -86,7 +86,7 @@ test('returns api response mapping camelCase keys', async () => { }, ); await waitFor(() => - expect(fetchMock.calls(editorQueryApiRoute).length).toBe(1), + expect(fetchMock.callHistory.calls(editorQueryApiRoute).length).toBe(1), ); const expectedResult = { ...fakeApiResult, @@ -94,7 +94,7 @@ test('returns api response mapping camelCase keys', async () => { }; // Check if the URL contains the expected rison-encoded parameters - const actualUrl = fetchMock.calls(editorQueryApiRoute)[0][0]; + const actualUrl = fetchMock.callHistory.calls(editorQueryApiRoute)[0].url; expect(actualUrl).toContain('/api/v1/query/?q='); // Extract and decode the query parameter @@ -131,7 +131,7 @@ test('merges paginated results', async () => { }), }); await waitFor(() => - expect(fetchMock.calls(editorQueryApiRoute).length).toBe(1), + expect(fetchMock.callHistory.calls(editorQueryApiRoute).length).toBe(1), ); const { result: paginatedResult } = renderHook( () => useEditorQueriesQuery({ editorId, pageIndex: 1 }), @@ -143,11 +143,11 @@ test('merges paginated results', async () => { }, ); await waitFor(() => - expect(fetchMock.calls(editorQueryApiRoute).length).toBe(2), + expect(fetchMock.callHistory.calls(editorQueryApiRoute).length).toBe(2), ); // Check the second call has page=1 - const secondUrl = fetchMock.calls(editorQueryApiRoute)[1][0]; + const secondUrl = fetchMock.callHistory.calls(editorQueryApiRoute)[1].url; expect(secondUrl).toContain('/api/v1/query/?q='); // Extract and decode the query parameter diff --git a/superset-frontend/src/hooks/apiResources/queryApi.test.ts b/superset-frontend/src/hooks/apiResources/queryApi.test.ts index 9f395600ce75..51f65b8acf03 100644 --- a/superset-frontend/src/hooks/apiResources/queryApi.test.ts +++ b/superset-frontend/src/hooks/apiResources/queryApi.test.ts @@ -36,7 +36,7 @@ const mockStore = configureStore(); const store = mockStore(); afterEach(() => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); }); test('supersetClientQuery should build the endpoint with rison encoded query string and return data when successful', async () => { @@ -56,8 +56,8 @@ test('supersetClientQuery should build the endpoint with rison encoded query str getBaseQueryApiMock(store), {}, ); - expect(fetchMock.calls(getEndpoint)).toHaveLength(1); - expect(fetchMock.calls(postEndpoint)).toHaveLength(0); + expect(fetchMock.callHistory.calls(getEndpoint)).toHaveLength(1); + expect(fetchMock.callHistory.calls(postEndpoint)).toHaveLength(0); expect((result.data as JsonResponse).json.result).toEqual(expectedData); await supersetClientQuery( { @@ -68,8 +68,8 @@ test('supersetClientQuery should build the endpoint with rison encoded query str getBaseQueryApiMock(store), {}, ); - expect(fetchMock.calls(getEndpoint)).toHaveLength(1); - expect(fetchMock.calls(postEndpoint)).toHaveLength(1); + expect(fetchMock.callHistory.calls(getEndpoint)).toHaveLength(1); + expect(fetchMock.callHistory.calls(postEndpoint)).toHaveLength(1); }); test('supersetClientQuery should return error when unsuccessful', async () => { diff --git a/superset-frontend/src/hooks/apiResources/queryValidations.test.ts b/superset-frontend/src/hooks/apiResources/queryValidations.test.ts index f1f1f4eb4ade..19be5595ed38 100644 --- a/superset-frontend/src/hooks/apiResources/queryValidations.test.ts +++ b/superset-frontend/src/hooks/apiResources/queryValidations.test.ts @@ -44,7 +44,7 @@ const expectSql = 'SELECT * from example_table'; const expectTemplateParams = '{"a": 1, "v": "str"}'; afterEach(() => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); act(() => { store.dispatch(api.util.resetApiState()); }); @@ -69,12 +69,14 @@ test('returns api response mapping json result', async () => { }, ); await waitFor(() => - expect(fetchMock.calls(queryValidationApiRoute).length).toBe(1), + expect(fetchMock.callHistory.calls(queryValidationApiRoute).length).toBe(1), ); expect(result.current.data).toEqual(expectedResult); - expect(fetchMock.calls(queryValidationApiRoute).length).toBe(1); + expect(fetchMock.callHistory.calls(queryValidationApiRoute).length).toBe(1); expect( - JSON.parse(`${fetchMock.calls(queryValidationApiRoute)[0][1]?.body}`), + JSON.parse( + `${fetchMock.callHistory.calls(queryValidationApiRoute)[0].options?.body}`, + ), ).toEqual({ schema: expectSchema, sql: expectSql, @@ -84,7 +86,7 @@ test('returns api response mapping json result', async () => { result.current.refetch(); }); await waitFor(() => - expect(fetchMock.calls(queryValidationApiRoute).length).toBe(2), + expect(fetchMock.callHistory.calls(queryValidationApiRoute).length).toBe(2), ); expect(result.current.data).toEqual(expectedResult); }); @@ -108,8 +110,8 @@ test('returns cached data without api request', async () => { }, ); await waitFor(() => expect(result.current.data).toEqual(expectedResult)); - expect(fetchMock.calls(queryValidationApiRoute).length).toBe(1); + expect(fetchMock.callHistory.calls(queryValidationApiRoute).length).toBe(1); rerender(); await waitFor(() => expect(result.current.data).toEqual(expectedResult)); - expect(fetchMock.calls(queryValidationApiRoute).length).toBe(1); + expect(fetchMock.callHistory.calls(queryValidationApiRoute).length).toBe(1); }); diff --git a/superset-frontend/src/hooks/apiResources/schemas.test.ts b/superset-frontend/src/hooks/apiResources/schemas.test.ts index b47ef894d067..63516b7a49ad 100644 --- a/superset-frontend/src/hooks/apiResources/schemas.test.ts +++ b/superset-frontend/src/hooks/apiResources/schemas.test.ts @@ -55,7 +55,7 @@ const expectedResult3 = fakeApiResult3.result.map((value: string) => ({ // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('useSchemas hook', () => { beforeEach(() => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); store.dispatch(api.util.resetApiState()); }); @@ -78,10 +78,12 @@ describe('useSchemas hook', () => { }), }, ); - await waitFor(() => expect(fetchMock.calls(schemaApiRoute).length).toBe(1)); + await waitFor(() => + expect(fetchMock.callHistory.calls(schemaApiRoute).length).toBe(1), + ); expect(result.current.data).toEqual(expectedResult); expect( - fetchMock.calls( + fetchMock.callHistory.calls( `end:/api/v1/database/${expectDbId}/schemas/?q=${rison.encode({ force: forceRefresh, })}`, @@ -91,9 +93,11 @@ describe('useSchemas hook', () => { act(() => { result.current.refetch(); }); - await waitFor(() => expect(fetchMock.calls(schemaApiRoute).length).toBe(2)); + await waitFor(() => + expect(fetchMock.callHistory.calls(schemaApiRoute).length).toBe(2), + ); expect( - fetchMock.calls( + fetchMock.callHistory.calls( `end:/api/v1/database/${expectDbId}/schemas/?q=${rison.encode({ force: true, })}`, @@ -120,16 +124,16 @@ describe('useSchemas hook', () => { }, ); await waitFor(() => expect(result.current.data).toEqual(expectedResult)); - expect(fetchMock.calls(schemaApiRoute).length).toBe(1); + expect(fetchMock.callHistory.calls(schemaApiRoute).length).toBe(1); rerender(); await waitFor(() => expect(result.current.data).toEqual(expectedResult)); - expect(fetchMock.calls(schemaApiRoute).length).toBe(1); + expect(fetchMock.callHistory.calls(schemaApiRoute).length).toBe(1); }); test('returns refreshed data after expires', async () => { const expectDbId = 'db1'; const schemaApiRoute = `glob:*/api/v1/database/*/schemas/*`; - fetchMock.get(schemaApiRoute, url => + fetchMock.get(schemaApiRoute, ({ url }) => url.includes(expectDbId) ? fakeApiResult : fakeApiResult2, ); const onSuccess = jest.fn(); @@ -151,21 +155,21 @@ describe('useSchemas hook', () => { await waitFor(() => expect(result.current.currentData).toEqual(expectedResult), ); - expect(fetchMock.calls(schemaApiRoute).length).toBe(1); + expect(fetchMock.callHistory.calls(schemaApiRoute).length).toBe(1); expect(onSuccess).toHaveBeenCalledTimes(1); rerender({ dbId: 'db2' }); await waitFor(() => expect(result.current.currentData).toEqual(expectedResult2), ); - expect(fetchMock.calls(schemaApiRoute).length).toBe(2); + expect(fetchMock.callHistory.calls(schemaApiRoute).length).toBe(2); expect(onSuccess).toHaveBeenCalledTimes(2); rerender({ dbId: expectDbId }); await waitFor(() => expect(result.current.currentData).toEqual(expectedResult), ); - expect(fetchMock.calls(schemaApiRoute).length).toBe(2); + expect(fetchMock.callHistory.calls(schemaApiRoute).length).toBe(2); expect(onSuccess).toHaveBeenCalledTimes(2); // clean up cache @@ -173,8 +177,12 @@ describe('useSchemas hook', () => { store.dispatch(api.util.invalidateTags(['Schemas'])); }); - await waitFor(() => expect(fetchMock.calls(schemaApiRoute).length).toBe(4)); - expect(fetchMock.calls(schemaApiRoute)[2][0]).toContain(expectDbId); + await waitFor(() => + expect(fetchMock.callHistory.calls(schemaApiRoute).length).toBe(4), + ); + expect(fetchMock.callHistory.calls(schemaApiRoute)[2].url).toContain( + expectDbId, + ); await waitFor(() => expect(result.current.currentData).toEqual(expectedResult), ); @@ -184,7 +192,7 @@ describe('useSchemas hook', () => { const dbId = '1'; const expectCatalog = 'catalog3'; const schemaApiRoute = `glob:*/api/v1/database/*/schemas/*`; - fetchMock.get(schemaApiRoute, url => + fetchMock.get(schemaApiRoute, ({ url }) => url.includes(`catalog:${expectCatalog}`) ? fakeApiResult3 : fakeApiResult2, @@ -206,16 +214,20 @@ describe('useSchemas hook', () => { }, ); - await waitFor(() => expect(fetchMock.calls(schemaApiRoute).length).toBe(1)); + await waitFor(() => + expect(fetchMock.callHistory.calls(schemaApiRoute).length).toBe(1), + ); expect(result.current.data).toEqual(expectedResult3); expect(onSuccess).toHaveBeenCalledTimes(1); rerender({ dbId, catalog: 'catalog2' }); - await waitFor(() => expect(fetchMock.calls(schemaApiRoute).length).toBe(2)); + await waitFor(() => + expect(fetchMock.callHistory.calls(schemaApiRoute).length).toBe(2), + ); expect(result.current.data).toEqual(expectedResult2); rerender({ dbId, catalog: expectCatalog }); expect(result.current.data).toEqual(expectedResult3); - expect(fetchMock.calls(schemaApiRoute).length).toBe(2); + expect(fetchMock.callHistory.calls(schemaApiRoute).length).toBe(2); }); }); diff --git a/superset-frontend/src/hooks/apiResources/sqlEditorTabs.test.ts b/superset-frontend/src/hooks/apiResources/sqlEditorTabs.test.ts index 5d6c9a55a906..e86a8f42ecfe 100644 --- a/superset-frontend/src/hooks/apiResources/sqlEditorTabs.test.ts +++ b/superset-frontend/src/hooks/apiResources/sqlEditorTabs.test.ts @@ -47,7 +47,7 @@ const expectedQueryEditor = { }; afterEach(() => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); act(() => { store.dispatch(api.util.resetApiState()); }); @@ -71,10 +71,12 @@ test('puts api request with formData', async () => { }); }); await waitFor(() => - expect(fetchMock.calls(tabStateMutationApiRoute).length).toBe(1), + expect(fetchMock.callHistory.calls(tabStateMutationApiRoute).length).toBe( + 1, + ), ); - const formData = fetchMock.calls(tabStateMutationApiRoute)[0][1] - ?.body as FormData; + const formData = fetchMock.callHistory.calls(tabStateMutationApiRoute)[0] + .options?.body as FormData; expect(formData.get('database_id')).toBe(`${expectedQueryEditor.dbId}`); expect(formData.get('schema')).toBe( JSON.stringify(`${expectedQueryEditor.schema}`), @@ -119,7 +121,9 @@ test('posts activate request with queryEditorId', async () => { result.current[0](expectedQueryEditor.id); }); await waitFor(() => - expect(fetchMock.calls(tabStateMutationApiRoute).length).toBe(1), + expect(fetchMock.callHistory.calls(tabStateMutationApiRoute).length).toBe( + 1, + ), ); }); @@ -139,6 +143,8 @@ test('deletes destoryed query editors', async () => { result.current[0](expectedQueryEditor.id); }); await waitFor(() => - expect(fetchMock.calls(tabStateMutationApiRoute).length).toBe(1), + expect(fetchMock.callHistory.calls(tabStateMutationApiRoute).length).toBe( + 1, + ), ); }); diff --git a/superset-frontend/src/hooks/apiResources/sqlLab.test.ts b/superset-frontend/src/hooks/apiResources/sqlLab.test.ts index 31c7a68b44c4..627d37da1e34 100644 --- a/superset-frontend/src/hooks/apiResources/sqlLab.test.ts +++ b/superset-frontend/src/hooks/apiResources/sqlLab.test.ts @@ -50,7 +50,7 @@ const expectedResult = fakeApiResult.result; const sqlLabInitialStateApiRoute = `glob:*/api/v1/sqllab/`; afterEach(() => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); act(() => { store.dispatch(api.util.resetApiState()); }); @@ -68,16 +68,22 @@ test('returns api response mapping json result', async () => { }), }); await waitFor(() => - expect(fetchMock.calls(sqlLabInitialStateApiRoute).length).toBe(1), + expect(fetchMock.callHistory.calls(sqlLabInitialStateApiRoute).length).toBe( + 1, + ), ); expect(result.current.data).toEqual(expectedResult); - expect(fetchMock.calls(sqlLabInitialStateApiRoute).length).toBe(1); + expect(fetchMock.callHistory.calls(sqlLabInitialStateApiRoute).length).toBe( + 1, + ); // clean up cache act(() => { store.dispatch(api.util.invalidateTags(['SqlLabInitialState'])); }); await waitFor(() => - expect(fetchMock.calls(sqlLabInitialStateApiRoute).length).toBe(2), + expect(fetchMock.callHistory.calls(sqlLabInitialStateApiRoute).length).toBe( + 2, + ), ); expect(result.current.data).toEqual(expectedResult); }); @@ -93,8 +99,12 @@ test('returns cached data without api request', async () => { }, ); await waitFor(() => expect(result.current.data).toEqual(expectedResult)); - expect(fetchMock.calls(sqlLabInitialStateApiRoute).length).toBe(1); + expect(fetchMock.callHistory.calls(sqlLabInitialStateApiRoute).length).toBe( + 1, + ); rerender(); await waitFor(() => expect(result.current.data).toEqual(expectedResult)); - expect(fetchMock.calls(sqlLabInitialStateApiRoute).length).toBe(1); + expect(fetchMock.callHistory.calls(sqlLabInitialStateApiRoute).length).toBe( + 1, + ); }); diff --git a/superset-frontend/src/hooks/apiResources/tables.test.ts b/superset-frontend/src/hooks/apiResources/tables.test.ts index 143c917dcbcc..963938a0e58e 100644 --- a/superset-frontend/src/hooks/apiResources/tables.test.ts +++ b/superset-frontend/src/hooks/apiResources/tables.test.ts @@ -73,7 +73,7 @@ const expectedHasMoreData = { // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('useTables hook', () => { beforeEach(() => { - fetchMock.reset(); + fetchMock.removeRoutes().clearHistory(); store.dispatch(api.util.resetApiState()); }); @@ -102,9 +102,9 @@ describe('useTables hook', () => { }, ); await waitFor(() => expect(result.current.data).toEqual(expectedData)); - expect(fetchMock.calls(schemaApiRoute).length).toBe(1); + expect(fetchMock.callHistory.calls(schemaApiRoute).length).toBe(1); expect( - fetchMock.calls( + fetchMock.callHistory.calls( `end:api/v1/database/${expectDbId}/tables/?q=${rison.encode({ force: false, schema_name: expectedSchema, @@ -116,7 +116,7 @@ describe('useTables hook', () => { }); await waitFor(() => expect( - fetchMock.calls( + fetchMock.callHistory.calls( `end:api/v1/database/${expectDbId}/tables/?q=${rison.encode({ force: true, schema_name: expectedSchema, @@ -124,7 +124,7 @@ describe('useTables hook', () => { ).length, ).toBe(1), ); - expect(fetchMock.calls(schemaApiRoute).length).toBe(1); + expect(fetchMock.callHistory.calls(schemaApiRoute).length).toBe(1); expect(result.current.data).toEqual(expectedData); }); @@ -152,10 +152,12 @@ describe('useTables hook', () => { }), }, ); - await waitFor(() => expect(fetchMock.calls(schemaApiRoute).length).toBe(1)); + await waitFor(() => + expect(fetchMock.callHistory.calls(schemaApiRoute).length).toBe(1), + ); expect(result.current.data).toEqual(undefined); expect( - fetchMock.calls( + fetchMock.callHistory.calls( `end:api/v1/database/${expectDbId}/tables/?q=${rison.encode({ force: false, schema_name: unexpectedSchema, @@ -168,7 +170,7 @@ describe('useTables hook', () => { const expectDbId = 'db1'; const expectedSchema = 'schema2'; const tableApiRoute = `glob:*/api/v1/database/${expectDbId}/tables/?q=*`; - fetchMock.get(tableApiRoute, fakeHasMoreApiResult); + fetchMock.get(tableApiRoute, fakeHasMoreApiResult, { name: tableApiRoute }); fetchMock.get(`glob:*/api/v1/database/${expectDbId}/catalogs/*`, { count: 0, result: [], @@ -189,7 +191,9 @@ describe('useTables hook', () => { }), }, ); - await waitFor(() => expect(fetchMock.calls(tableApiRoute).length).toBe(1)); + await waitFor(() => + expect(fetchMock.callHistory.calls(tableApiRoute).length).toBe(1), + ); expect(result.current.data).toEqual(expectedHasMoreData); }); @@ -220,26 +224,31 @@ describe('useTables hook', () => { ); console.log( 'Called URLs:', - fetchMock.calls().map(call => call[0]), + fetchMock.callHistory.calls().map(call => call.url), ); // Add a catch-all mock to see if any unmocked requests are being made - fetchMock.mock('*', url => { + fetchMock.route('*', url => { console.log('Unmocked request to:', url); return 404; }); - await waitFor(() => expect(fetchMock.calls(tableApiRoute).length).toBe(1)); + await waitFor(() => + expect(fetchMock.callHistory.calls(tableApiRoute).length).toBe(1), + ); rerender(); await waitFor(() => expect(result.current.data).toEqual(expectedData)); - expect(fetchMock.calls(tableApiRoute).length).toBe(1); + expect(fetchMock.callHistory.calls(tableApiRoute).length).toBe(1); }); test('returns refreshed data after expires', async () => { const expectDbId = 'db1'; const expectedSchema = 'schema1'; const tableApiRoute = `glob:*/api/v1/database/${expectDbId}/tables/?q=*`; - fetchMock.get(tableApiRoute, url => - url.includes(expectedSchema) ? fakeApiResult : fakeHasMoreApiResult, + fetchMock.get( + tableApiRoute, + ({ url }) => + url.includes(expectedSchema) ? fakeApiResult : fakeHasMoreApiResult, + { name: tableApiRoute }, ); fetchMock.get(`glob:*/api/v1/database/${expectDbId}/catalogs/*`, { count: 0, @@ -264,34 +273,36 @@ describe('useTables hook', () => { ); await waitFor(() => expect(result.current.data).toEqual(expectedData)); - expect(fetchMock.calls(tableApiRoute).length).toBe(1); + expect(fetchMock.callHistory.calls(tableApiRoute).length).toBe(1); rerender({ schema: 'schema2' }); await waitFor(() => expect(result.current.data).toEqual(expectedHasMoreData), ); - expect(fetchMock.calls(tableApiRoute).length).toBe(2); + expect(fetchMock.callHistory.calls(tableApiRoute).length).toBe(2); rerender({ schema: expectedSchema }); await waitFor(() => expect(result.current.data).toEqual(expectedData)); - expect(fetchMock.calls(tableApiRoute).length).toBe(2); + expect(fetchMock.callHistory.calls(tableApiRoute).length).toBe(2); // clean up cache act(() => { store.dispatch(api.util.invalidateTags(['Tables'])); }); - await waitFor(() => expect(fetchMock.calls(tableApiRoute).length).toBe(3)); + await waitFor(() => + expect(fetchMock.callHistory.calls(tableApiRoute).length).toBe(3), + ); await waitFor(() => expect(result.current.data).toEqual(expectedData)); rerender({ schema: 'schema2' }); await waitFor(() => expect(result.current.data).toEqual(expectedHasMoreData), ); - expect(fetchMock.calls(tableApiRoute).length).toBe(4); + expect(fetchMock.callHistory.calls(tableApiRoute).length).toBe(4); rerender({ schema: expectedSchema }); await waitFor(() => expect(result.current.data).toEqual(expectedData)); - expect(fetchMock.calls(tableApiRoute).length).toBe(4); + expect(fetchMock.callHistory.calls(tableApiRoute).length).toBe(4); }); }); diff --git a/superset-frontend/src/middleware/asyncEvent.test.ts b/superset-frontend/src/middleware/asyncEvent.test.ts index 94dd923aa37f..10da96099dd9 100644 --- a/superset-frontend/src/middleware/asyncEvent.test.ts +++ b/superset-frontend/src/middleware/asyncEvent.test.ts @@ -95,11 +95,11 @@ describe('asyncEvent middleware', () => { }); afterEach(() => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); mockedIsFeatureEnabled.mockRestore(); }); - afterAll(() => fetchMock.reset()); + afterAll(() => fetchMock.clearHistory().removeRoutes()); // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('polling transport', () => { @@ -126,12 +126,12 @@ describe('asyncEvent middleware', () => { await asyncEvent.waitForAsyncData(asyncPendingEvent); expect(actualResolved).toEqual([chartData]); - expect(fetchMock.calls(EVENTS_ENDPOINT)).toHaveLength(1); - expect(fetchMock.calls(CACHED_DATA_ENDPOINT)).toHaveLength(1); + expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(1); + expect(fetchMock.callHistory.calls(CACHED_DATA_ENDPOINT)).toHaveLength(1); }); test('rejects on event error status', async () => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); fetchMock.get(EVENTS_ENDPOINT, { status: 200, body: { result: [asyncErrorEvent] }, @@ -146,12 +146,12 @@ describe('asyncEvent middleware', () => { expect(error).toEqual(errorResponse); } - expect(fetchMock.calls(EVENTS_ENDPOINT)).toHaveLength(1); - expect(fetchMock.calls(CACHED_DATA_ENDPOINT)).toHaveLength(0); + expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(1); + expect(fetchMock.callHistory.calls(CACHED_DATA_ENDPOINT)).toHaveLength(0); }); test('rejects on cached data fetch error', async () => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); fetchMock.get(EVENTS_ENDPOINT, { status: 200, body: { result: [asyncDoneEvent] }, @@ -169,8 +169,8 @@ describe('asyncEvent middleware', () => { expect(error).toEqual('Bad request'); } - expect(fetchMock.calls(EVENTS_ENDPOINT)).toHaveLength(1); - expect(fetchMock.calls(CACHED_DATA_ENDPOINT)).toHaveLength(1); + expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(1); + expect(fetchMock.callHistory.calls(CACHED_DATA_ENDPOINT)).toHaveLength(1); }); }); @@ -210,8 +210,8 @@ describe('asyncEvent middleware', () => { await expect(promise).resolves.toEqual([chartData]); - expect(fetchMock.calls(CACHED_DATA_ENDPOINT)).toHaveLength(1); - expect(fetchMock.calls(EVENTS_ENDPOINT)).toHaveLength(0); + expect(fetchMock.callHistory.calls(CACHED_DATA_ENDPOINT)).toHaveLength(1); + expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(0); }); test('rejects on event error status', async () => { @@ -225,12 +225,12 @@ describe('asyncEvent middleware', () => { await expect(promise).rejects.toEqual(errorResponse); - expect(fetchMock.calls(CACHED_DATA_ENDPOINT)).toHaveLength(0); - expect(fetchMock.calls(EVENTS_ENDPOINT)).toHaveLength(0); + expect(fetchMock.callHistory.calls(CACHED_DATA_ENDPOINT)).toHaveLength(0); + expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(0); }); test('rejects on cached data fetch error', async () => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); fetchMock.get(CACHED_DATA_ENDPOINT, { status: 400, }); @@ -250,8 +250,8 @@ describe('asyncEvent middleware', () => { expect(error).toEqual('Bad request'); } - expect(fetchMock.calls(CACHED_DATA_ENDPOINT)).toHaveLength(1); - expect(fetchMock.calls(EVENTS_ENDPOINT)).toHaveLength(0); + expect(fetchMock.callHistory.calls(CACHED_DATA_ENDPOINT)).toHaveLength(1); + expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(0); }); test('resolves when events are received before listener', async () => { @@ -262,8 +262,8 @@ describe('asyncEvent middleware', () => { const promise = asyncEvent.waitForAsyncData(asyncPendingEvent); await expect(promise).resolves.toEqual([chartData]); - expect(fetchMock.calls(CACHED_DATA_ENDPOINT)).toHaveLength(1); - expect(fetchMock.calls(EVENTS_ENDPOINT)).toHaveLength(0); + expect(fetchMock.callHistory.calls(CACHED_DATA_ENDPOINT)).toHaveLength(1); + expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(0); }); }); }); diff --git a/superset-frontend/src/pages/AlertReportList/AlertReportList.test.jsx b/superset-frontend/src/pages/AlertReportList/AlertReportList.test.jsx index 6aab05ec3755..80ef800b88e0 100644 --- a/superset-frontend/src/pages/AlertReportList/AlertReportList.test.jsx +++ b/superset-frontend/src/pages/AlertReportList/AlertReportList.test.jsx @@ -99,7 +99,7 @@ const renderAlertList = (props = {}) => // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('AlertList', () => { beforeEach(() => { - fetchMock.resetHistory(); + fetchMock.clearHistory(); }); test('renders', async () => { @@ -145,7 +145,9 @@ describe('AlertList', () => { // Wait for delete request await waitFor(() => { - expect(fetchMock.calls(/report\/0/, 'DELETE')).toHaveLength(1); + expect(fetchMock.callHistory.calls(/report\/0/, 'DELETE')).toHaveLength( + 1, + ); }); }, 15000); @@ -196,9 +198,9 @@ describe('AlertList', () => { // Wait for report list API call and tab states to update await waitFor(async () => { // Check API call - const calls = fetchMock.calls(/report\/\?q/); + const calls = fetchMock.callHistory.calls(/report\/\?q/); const hasReportCall = calls.some(call => - call[0].includes('filters:!((col:type,opr:eq,value:Report))'), + call.url.includes('filters:!((col:type,opr:eq,value:Report))'), ); // Check tab states @@ -227,8 +229,8 @@ describe('AlertList', () => { }); // Verify correct API call was made - const reportCalls = fetchMock.calls(/report\/\?q/); - const lastReportCall = reportCalls[reportCalls.length - 1][0]; + const reportCalls = fetchMock.callHistory.calls(/report\/\?q/); + const lastReportCall = reportCalls[reportCalls.length - 1].url; expect(lastReportCall).toContain( 'filters:!((col:type,opr:eq,value:Report))', ); diff --git a/superset-frontend/src/pages/AnnotationLayerList/AnnotationLayerList.test.jsx b/superset-frontend/src/pages/AnnotationLayerList/AnnotationLayerList.test.jsx index 347ad23bd2a6..4ebcf5261070 100644 --- a/superset-frontend/src/pages/AnnotationLayerList/AnnotationLayerList.test.jsx +++ b/superset-frontend/src/pages/AnnotationLayerList/AnnotationLayerList.test.jsx @@ -89,7 +89,7 @@ const renderAnnotationLayersList = (props = {}) => // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('AnnotationLayersList', () => { beforeEach(() => { - fetchMock.resetHistory(); + fetchMock.clearHistory(); }); test('renders', async () => { @@ -121,9 +121,9 @@ describe('AnnotationLayersList', () => { test('fetches layers', async () => { renderAnnotationLayersList(); await waitFor(() => { - const calls = fetchMock.calls(/annotation_layer\/\?q/); + const calls = fetchMock.callHistory.calls(/annotation_layer\/\?q/); expect(calls).toHaveLength(1); - expect(calls[0][0]).toContain( + expect(calls[0].url).toContain( 'order_column:name,order_direction:desc,page:0,page_size:25', ); }); @@ -148,9 +148,9 @@ describe('AnnotationLayersList', () => { // Wait for search API call await waitFor(() => { - const calls = fetchMock.calls(/annotation_layer\/\?q/); + const calls = fetchMock.callHistory.calls(/annotation_layer\/\?q/); const searchCall = calls.find(call => - call[0].includes('filters:!((col:name,opr:ct,value:foo))'), + call.url.includes('filters:!((col:name,opr:ct,value:foo))'), ); expect(searchCall).toBeTruthy(); }); @@ -180,7 +180,9 @@ describe('AnnotationLayersList', () => { // Wait for delete request await waitFor(() => { - expect(fetchMock.calls(/annotation_layer\/0/, 'DELETE')).toHaveLength(1); + expect( + fetchMock.callHistory.calls(/annotation_layer\/0/, 'DELETE'), + ).toHaveLength(1); }); }); diff --git a/superset-frontend/src/pages/Chart/Chart.test.tsx b/superset-frontend/src/pages/Chart/Chart.test.tsx index 3ae03b08f8da..de3787ebde5d 100644 --- a/superset-frontend/src/pages/Chart/Chart.test.tsx +++ b/superset-frontend/src/pages/Chart/Chart.test.tsx @@ -68,7 +68,7 @@ describe('ChartPage', () => { }); afterEach(() => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); }); test('fetches metadata on mount', async () => { @@ -86,7 +86,7 @@ describe('ChartPage', () => { useDnd: true, }); await waitFor(() => - expect(fetchMock.calls(exploreApiRoute).length).toBe(1), + expect(fetchMock.callHistory.calls(exploreApiRoute).length).toBe(1), ); expect(getByTestId('mock-explore-chart-panel')).toBeInTheDocument(); expect(getByTestId('mock-explore-chart-panel')).toHaveTextContent( @@ -132,8 +132,10 @@ describe('ChartPage', () => { timeout: 5000, }, ); - expect(fetchMock.calls(chartApiRoute).length).toEqual(0); - expect(fetchMock.calls(exploreApiRoute).length).toBeGreaterThanOrEqual(1); + expect(fetchMock.callHistory.calls(chartApiRoute).length).toEqual(0); + expect( + fetchMock.callHistory.calls(exploreApiRoute).length, + ).toBeGreaterThanOrEqual(1); }); test('fetches the chart api when explore metadata is prohibited and access from the chart link', async () => { @@ -168,10 +170,15 @@ describe('ChartPage', () => { useRedux: true, useDnd: true, }); - await waitFor(() => expect(fetchMock.calls(chartApiRoute).length).toBe(1), { - timeout: 5000, - }); - expect(fetchMock.calls(exploreApiRoute).length).toBeGreaterThanOrEqual(1); + await waitFor( + () => expect(fetchMock.callHistory.calls(chartApiRoute).length).toBe(1), + { + timeout: 5000, + }, + ); + expect( + fetchMock.callHistory.calls(exploreApiRoute).length, + ).toBeGreaterThanOrEqual(1); expect(getByTestId('mock-explore-chart-panel')).toBeInTheDocument(); expect(getByTestId('mock-explore-chart-panel')).toHaveTextContent( JSON.stringify({ datasource: 123 }).slice(1, -1), @@ -218,7 +225,7 @@ describe('ChartPage', () => { useDnd: true, }); await waitFor(() => - expect(fetchMock.calls(exploreApiRoute).length).toBe(1), + expect(fetchMock.callHistory.calls(exploreApiRoute).length).toBe(1), ); expect(getByTestId('mock-explore-chart-panel')).toHaveTextContent( JSON.stringify({ color_scheme: dashboardFormData.color_scheme }).slice( @@ -266,7 +273,7 @@ describe('ChartPage', () => { }, ); await waitFor(() => - expect(fetchMock.calls(exploreApiRoute).length).toBe(1), + expect(fetchMock.callHistory.calls(exploreApiRoute).length).toBe(1), ); expect(getByTestId('mock-explore-chart-panel')).toHaveTextContent( JSON.stringify({ @@ -277,13 +284,13 @@ describe('ChartPage', () => { ...exploreFormData, show_cell_bars: false, }; - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); fetchMock.get(exploreApiRoute, { result: { dataset: { id: 1 }, form_data: updatedExploreFormData }, }); fireEvent.click(screen.getByText('Change route')); await waitFor(() => - expect(fetchMock.calls(exploreApiRoute).length).toBe(1), + expect(fetchMock.callHistory.calls(exploreApiRoute).length).toBe(1), ); expect(getByTestId('mock-explore-chart-panel')).toHaveTextContent( JSON.stringify({ diff --git a/superset-frontend/src/pages/ChartCreation/ChartCreation.test.tsx b/superset-frontend/src/pages/ChartCreation/ChartCreation.test.tsx index e67f829babb9..4d8311548ac9 100644 --- a/superset-frontend/src/pages/ChartCreation/ChartCreation.test.tsx +++ b/superset-frontend/src/pages/ChartCreation/ChartCreation.test.tsx @@ -195,7 +195,7 @@ test('double-click viz type submits with formatted URL if datasource is selected }); test('dropdown displays matching datasets when user types a search term', async () => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); fetchMock.get(/\/api\/v1\/dataset\/\?q=.*/, { body: { result: [ @@ -232,7 +232,7 @@ test('dropdown displays matching datasets when user types a search term', async }); test('handles special characters in dataset name from URL parameter', async () => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); fetchMock.get(/\/api\/v1\/dataset\/\?q=.*/, { body: { result: [ @@ -269,7 +269,7 @@ test('handles special characters in dataset name from URL parameter', async () = }); test('pre-selects the dataset from URL parameter and shows it in dropdown', async () => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); fetchMock.get(/\/api\/v1\/dataset\/\?q=.*/, { body: { result: [ @@ -303,7 +303,7 @@ test('pre-selects the dataset from URL parameter and shows it in dropdown', asyn }); test('shows loading spinner when dataset parameter is present in URL', async () => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); let resolveRequest: (value: unknown) => void; const requestPromise = new Promise(resolve => { resolveRequest = resolve; @@ -361,8 +361,8 @@ test('shows loading spinner when dataset parameter is present in URL', async () }); test('shows only exact match when loading dataset from URL, not partial matches', async () => { - fetchMock.reset(); - fetchMock.get(/\/api\/v1\/dataset\/\?q=.*/, url => { + fetchMock.clearHistory().removeRoutes(); + fetchMock.get(/\/api\/v1\/dataset\/\?q=.*/, ({ url }) => { if (url.includes('opr:eq')) { return { body: { diff --git a/superset-frontend/src/pages/ChartList/ChartList.cardview.test.tsx b/superset-frontend/src/pages/ChartList/ChartList.cardview.test.tsx index 2ae633e72823..e7df78f09460 100644 --- a/superset-frontend/src/pages/ChartList/ChartList.cardview.test.tsx +++ b/superset-frontend/src/pages/ChartList/ChartList.cardview.test.tsx @@ -68,8 +68,8 @@ describe('ChartList Card View Tests', () => { }); afterEach(() => { - fetchMock.resetHistory(); - fetchMock.restore(); + fetchMock.clearHistory(); + fetchMock.removeRoutes(); }); test('renders ChartList in card view', async () => { diff --git a/superset-frontend/src/pages/ChartList/ChartList.listview.test.tsx b/superset-frontend/src/pages/ChartList/ChartList.listview.test.tsx index 84e1c4e374f8..13d6e73eea85 100644 --- a/superset-frontend/src/pages/ChartList/ChartList.listview.test.tsx +++ b/superset-frontend/src/pages/ChartList/ChartList.listview.test.tsx @@ -25,6 +25,7 @@ import { mockHandleResourceExport, setupMocks, renderChartList, + API_ENDPOINTS, } from './ChartList.testHelpers'; // Increase default timeout for all tests @@ -66,7 +67,8 @@ beforeEach(() => { }); afterEach(() => { - fetchMock.restore(); + fetchMock.clearHistory(); + fetchMock.removeRoutes(); mockIsFeatureEnabled.mockReset(); }); @@ -113,16 +115,14 @@ test('ChartList list view correctly displays dataset names with and without sche ]; // Setup mock with custom charts - fetchMock.reset(); + fetchMock.removeRoutes(); setupMocks(); - fetchMock.get( - 'glob:*/api/v1/chart/?*', - { + fetchMock.modifyRoute(API_ENDPOINTS.CHARTS, { + response: { result: customMockCharts, chart_count: customMockCharts.length, }, - { overwriteRoutes: true }, - ); + }); renderChartList(mockUser); @@ -235,40 +235,41 @@ test('ChartList list view sorts table when clicking column headers', async () => expect(sortableHeaders).toHaveLength(3); const nameHeader = within(table).getByTitle('Name'); - await userEvent.click(nameHeader); + userEvent.click(nameHeader); await waitFor(() => { - const sortCalls = fetchMock + const sortCalls = fetchMock.callHistory .calls(/chart\/\?q/) .filter( call => - call[0].includes('order_column') && call[0].includes('slice_name'), + call.url.includes('order_column') && call.url.includes('slice_name'), ); expect(sortCalls).toHaveLength(1); }); const typeHeader = within(table).getByTitle('Type'); - await userEvent.click(typeHeader); + userEvent.click(typeHeader); await waitFor(() => { - const typeSortCalls = fetchMock + const typeSortCalls = fetchMock.callHistory .calls(/chart\/\?q/) .filter( call => - call[0].includes('order_column') && call[0].includes('viz_type'), + call.url.includes('order_column') && call.url.includes('viz_type'), ); expect(typeSortCalls).toHaveLength(1); }); const lastModifiedHeader = within(table).getByTitle('Last modified'); - await userEvent.click(lastModifiedHeader); + userEvent.click(lastModifiedHeader); await waitFor(() => { - const lastModifiedSortCalls = fetchMock + const lastModifiedSortCalls = fetchMock.callHistory .calls(/chart\/\?q/) .filter( call => - call[0].includes('order_column') && call[0].includes('last_saved_at'), + call.url.includes('order_column') && + call.url.includes('last_saved_at'), ); expect(lastModifiedSortCalls).toHaveLength(1); }); diff --git a/superset-frontend/src/pages/ChartList/ChartList.permissions.test.tsx b/superset-frontend/src/pages/ChartList/ChartList.permissions.test.tsx index d1730538e674..9b2edee4e610 100644 --- a/superset-frontend/src/pages/ChartList/ChartList.permissions.test.tsx +++ b/superset-frontend/src/pages/ChartList/ChartList.permissions.test.tsx @@ -124,17 +124,6 @@ const renderChartList = ( ); }; -// Setup API permissions mock -const setupApiPermissions = (permissions: string[]) => { - fetchMock.get( - API_ENDPOINTS.CHARTS_INFO, - { - permissions, - }, - { overwriteRoutes: true }, - ); -}; - // Render with permissions and wait for load const renderWithPermissions = async ( permissions = PERMISSIONS.ADMIN, @@ -151,8 +140,7 @@ const renderWithPermissions = async ( }); // Convert role permissions to API permissions - const apiPermissions = permissions.map(perm => perm[0]); - setupApiPermissions(apiPermissions); + setupMocks({ [API_ENDPOINTS.CHARTS_INFO]: permissions.map(perm => perm[0]) }); const storeState = createStoreStateWithPermissions(permissions, userId); @@ -176,12 +164,8 @@ const renderWithPermissions = async ( // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('ChartList - Permission-based UI Tests', () => { beforeEach(() => { - setupMocks(); - }); - - afterEach(() => { - fetchMock.resetHistory(); - fetchMock.restore(); + fetchMock.clearHistory(); + fetchMock.removeRoutes(); ( isFeatureEnabled as jest.MockedFunction ).mockReset(); diff --git a/superset-frontend/src/pages/ChartList/ChartList.test.tsx b/superset-frontend/src/pages/ChartList/ChartList.test.tsx index 107a7e2d0224..11d870fdfcaf 100644 --- a/superset-frontend/src/pages/ChartList/ChartList.test.tsx +++ b/superset-frontend/src/pages/ChartList/ChartList.test.tsx @@ -74,13 +74,13 @@ const findFilterByLabel = (labelText: string) => { // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('ChartList', () => { beforeEach(() => { + fetchMock.removeRoutes(); setupMocks(); mockPush.mockClear(); }); afterEach(() => { - fetchMock.resetHistory(); - fetchMock.restore(); + fetchMock.clearHistory(); // Reset feature flag mock ( isFeatureEnabled as jest.MockedFunction @@ -133,12 +133,14 @@ describe('ChartList', () => { test('shows loading state during initial data fetch', async () => { // Delay the chart data response to test loading state + // fetchMock.removeRoute(API_ENDPOINTS.CHARTS) + fetchMock.removeRoutes(); fetchMock.get( API_ENDPOINTS.CHARTS, new Promise(resolve => setTimeout(() => resolve({ result: mockCharts, chart_count: 3 }), 200), ), - { overwriteRoutes: true }, + { name: API_ENDPOINTS.CHARTS }, ); renderChartList(mockUser); @@ -159,12 +161,12 @@ describe('ChartList', () => { renderChartList(mockUser); await waitFor(() => { - const infoCalls = fetchMock.calls(/chart\/_info/); - const dataCalls = fetchMock.calls(/chart\/\?q/); + const infoCalls = fetchMock.callHistory.calls(/chart\/_info/); + const dataCalls = fetchMock.callHistory.calls(/chart\/\?q/); expect(infoCalls).toHaveLength(1); expect(dataCalls).toHaveLength(1); - expect(dataCalls[0][0]).toContain( + expect(dataCalls[0].url).toContain( 'order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:25', ); }); @@ -172,6 +174,8 @@ describe('ChartList', () => { test('shows loading state while API calls are in progress', async () => { // Mock delayed API responses + // fetchMock.removeRoute(API_ENDPOINTS.CHARTS_INFO) + fetchMock.removeRoutes(); fetchMock.get( API_ENDPOINTS.CHARTS_INFO, new Promise(resolve => @@ -180,15 +184,16 @@ describe('ChartList', () => { 100, ), ), - { overwriteRoutes: true }, + { name: API_ENDPOINTS.CHARTS_INFO }, ); + // fetchMock.removeRoute(API_ENDPOINTS.CHARTS) fetchMock.get( API_ENDPOINTS.CHARTS, new Promise(resolve => setTimeout(() => resolve({ result: mockCharts, chart_count: 3 }), 150), ), - { overwriteRoutes: true }, + { name: API_ENDPOINTS.CHARTS }, ); renderChartList(mockUser); @@ -199,8 +204,8 @@ describe('ChartList', () => { // Eventually data should load await waitFor( () => { - const infoCalls = fetchMock.calls(/chart\/_info/); - const dataCalls = fetchMock.calls(/chart\/\?q/); + const infoCalls = fetchMock.callHistory.calls(/chart\/_info/); + const dataCalls = fetchMock.callHistory.calls(/chart\/\?q/); expect(infoCalls).toHaveLength(1); expect(dataCalls).toHaveLength(1); @@ -210,15 +215,6 @@ describe('ChartList', () => { }); test('maintains component structure during loading', async () => { - // Only delay data loading, not permissions - fetchMock.get( - API_ENDPOINTS.CHARTS, - new Promise(resolve => - setTimeout(() => resolve({ result: mockCharts, chart_count: 3 }), 200), - ), - { overwriteRoutes: true }, - ); - renderChartList(mockUser); // Core structure should be available immediately @@ -232,22 +228,14 @@ describe('ChartList', () => { ).toBeInTheDocument(); // Wait for permissions to load, then action buttons should appear - await waitFor( - () => { - expect( - screen.getByRole('button', { name: 'Bulk select' }), - ).toBeInTheDocument(); - }, - { timeout: 500 }, - ); + expect( + await screen.findByRole('button', { name: 'Bulk select' }), + ).toBeInTheDocument(); // Wait for data to eventually load - await waitFor( - () => { - expect(screen.getByText(mockCharts[0].slice_name)).toBeInTheDocument(); - }, - { timeout: 1000 }, - ); + expect( + await screen.findByText(mockCharts[0].slice_name), + ).toBeInTheDocument(); }); test('displays Matrixify tag for charts with matrixify enabled', async () => { @@ -280,10 +268,11 @@ describe('ChartList', () => { test('handles API errors gracefully', async () => { // Mock API failure + fetchMock.removeRoutes(); fetchMock.get( API_ENDPOINTS.CHARTS_INFO, { throws: new Error('API Error') }, - { overwriteRoutes: true }, + { name: API_ENDPOINTS.CHARTS_INFO }, ); renderChartList(mockUser); @@ -295,10 +284,11 @@ describe('ChartList', () => { test('handles empty results', async () => { // Mock empty chart data (not permissions) + fetchMock.removeRoute(API_ENDPOINTS.CHARTS); fetchMock.get( API_ENDPOINTS.CHARTS, { result: [], chart_count: 0 }, - { overwriteRoutes: true }, + { name: API_ENDPOINTS.CHARTS }, ); renderChartList(mockUser); @@ -321,12 +311,12 @@ describe('ChartList', () => { // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('ChartList - Global Filter Interactions', () => { beforeEach(() => { + fetchMock.removeRoutes(); setupMocks(); }); afterEach(() => { - fetchMock.resetHistory(); - fetchMock.restore(); + fetchMock.clearHistory(); // Reset feature flag mock ( isFeatureEnabled as jest.MockedFunction diff --git a/superset-frontend/src/pages/ChartList/ChartList.testHelpers.tsx b/superset-frontend/src/pages/ChartList/ChartList.testHelpers.tsx index a6578935466d..d6518a0a4dfe 100644 --- a/superset-frontend/src/pages/ChartList/ChartList.testHelpers.tsx +++ b/superset-frontend/src/pages/ChartList/ChartList.testHelpers.tsx @@ -291,17 +291,27 @@ export const API_ENDPOINTS = { CATCH_ALL: 'glob:*', }; -export const setupMocks = () => { - fetchMock.reset(); - - fetchMock.get(API_ENDPOINTS.CHARTS_INFO, { - permissions: ['can_read', 'can_write', 'can_export'], - }); +export const setupMocks = ( + payloadMap = { + [API_ENDPOINTS.CHARTS_INFO]: ['can_read', 'can_write', 'can_export'], + }, +) => { + fetchMock.get( + API_ENDPOINTS.CHARTS_INFO, + { + permissions: payloadMap[API_ENDPOINTS.CHARTS_INFO], + }, + { name: API_ENDPOINTS.CHARTS_INFO }, + ); - fetchMock.get(API_ENDPOINTS.CHARTS, { - result: mockCharts, - chart_count: mockCharts.length, - }); + fetchMock.get( + API_ENDPOINTS.CHARTS, + { + result: mockCharts, + chart_count: mockCharts.length, + }, + { name: API_ENDPOINTS.CHARTS }, + ); fetchMock.get(API_ENDPOINTS.CHART_FAVORITE_STATUS, { result: [], diff --git a/superset-frontend/src/pages/CssTemplateList/CssTemplateList.test.jsx b/superset-frontend/src/pages/CssTemplateList/CssTemplateList.test.jsx index e1b574f0b42e..d8f17c541a49 100644 --- a/superset-frontend/src/pages/CssTemplateList/CssTemplateList.test.jsx +++ b/superset-frontend/src/pages/CssTemplateList/CssTemplateList.test.jsx @@ -88,7 +88,7 @@ const renderCssTemplatesList = (props = {}) => // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('CssTemplatesList', () => { beforeEach(() => { - fetchMock.resetHistory(); + fetchMock.clearHistory(); }); test('renders', async () => { @@ -111,9 +111,9 @@ describe('CssTemplatesList', () => { test('fetches templates', async () => { renderCssTemplatesList(); await waitFor(() => { - const calls = fetchMock.calls(/css_template\/\?q/); + const calls = fetchMock.callHistory.calls(/css_template\/\?q/); expect(calls).toHaveLength(1); - expect(calls[0][0]).toContain( + expect(calls[0].url).toContain( 'order_column:template_name,order_direction:desc,page:0,page_size:25', ); }); @@ -138,9 +138,9 @@ describe('CssTemplatesList', () => { // Wait for search API call await waitFor(() => { - const calls = fetchMock.calls(/css_template\/\?q/); + const calls = fetchMock.callHistory.calls(/css_template\/\?q/); const searchCall = calls.find(call => - call[0].includes('filters:!((col:template_name,opr:ct,value:fooo))'), + call.url.includes('filters:!((col:template_name,opr:ct,value:fooo))'), ); expect(searchCall).toBeTruthy(); }); @@ -170,7 +170,9 @@ describe('CssTemplatesList', () => { // Wait for delete request await waitFor(() => { - expect(fetchMock.calls(/css_template\/0/, 'DELETE')).toHaveLength(1); + expect( + fetchMock.callHistory.calls(/css_template\/0/, 'DELETE'), + ).toHaveLength(1); }); }); diff --git a/superset-frontend/src/pages/DashboardList/DashboardList.test.jsx b/superset-frontend/src/pages/DashboardList/DashboardList.test.jsx index 0cd4961eef9f..18879e4e6766 100644 --- a/superset-frontend/src/pages/DashboardList/DashboardList.test.jsx +++ b/superset-frontend/src/pages/DashboardList/DashboardList.test.jsx @@ -100,7 +100,7 @@ describe('DashboardList', () => { isFeatureEnabled.mockImplementation( feature => feature === 'LISTVIEWS_DEFAULT_CARD_VIEW', ); - fetchMock.resetHistory(); + fetchMock.clearHistory(); }); afterEach(() => { @@ -122,7 +122,7 @@ describe('DashboardList', () => { test('fetches info', async () => { renderDashboardList(); await waitFor(() => { - const calls = fetchMock.calls(/dashboard\/_info/); + const calls = fetchMock.callHistory.calls(/dashboard\/_info/); expect(calls).toHaveLength(1); }); }); @@ -130,12 +130,12 @@ describe('DashboardList', () => { test('fetches data', async () => { renderDashboardList(); await waitFor(() => { - const calls = fetchMock.calls(/dashboard\/\?q/); + const calls = fetchMock.callHistory.calls(/dashboard\/\?q/); expect(calls).toHaveLength(1); }); - const calls = fetchMock.calls(/dashboard\/\?q/); - expect(calls[0][0]).toMatchInlineSnapshot( + const calls = fetchMock.callHistory.calls(/dashboard\/\?q/); + expect(calls[0].url).toMatchInlineSnapshot( `"http://localhost/api/v1/dashboard/?q=(order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:25,select_columns:!(id,dashboard_title,published,url,slug,changed_by,changed_by.id,changed_by.first_name,changed_by.last_name,changed_on_delta_humanized,owners,owners.id,owners.first_name,owners.last_name,tags.id,tags.name,tags.type,status,certified_by,certification_details,changed_on))"`, ); }); diff --git a/superset-frontend/src/pages/ExecutionLogList/ExecutionLogList.test.tsx b/superset-frontend/src/pages/ExecutionLogList/ExecutionLogList.test.tsx index e53abc4e120c..a3f8145b60cb 100644 --- a/superset-frontend/src/pages/ExecutionLogList/ExecutionLogList.test.tsx +++ b/superset-frontend/src/pages/ExecutionLogList/ExecutionLogList.test.tsx @@ -72,17 +72,17 @@ describe('ExecutionLog', () => { }); test('fetches report/alert', () => { - const callsQ = fetchMock.calls(/report\/1/); + const callsQ = fetchMock.callHistory.calls(/report\/1/); expect(callsQ).toHaveLength(2); - expect(callsQ[1][0]).toMatchInlineSnapshot( + expect(callsQ[1].url).toMatchInlineSnapshot( `"http://localhost/api/v1/report/1"`, ); }); test('fetches execution logs', () => { - const callsQ = fetchMock.calls(/report\/1\/log/); + const callsQ = fetchMock.callHistory.calls(/report\/1\/log/); expect(callsQ).toHaveLength(1); - expect(callsQ[0][0]).toMatchInlineSnapshot( + expect(callsQ[0].url).toMatchInlineSnapshot( `"http://localhost/api/v1/report/1/log/?q=(order_column:start_dttm,order_direction:desc,page:0,page_size:25)"`, ); }); diff --git a/superset-frontend/src/pages/GroupsList/GroupsList.test.tsx b/superset-frontend/src/pages/GroupsList/GroupsList.test.tsx index abcfe9846550..4acb6c21d474 100644 --- a/superset-frontend/src/pages/GroupsList/GroupsList.test.tsx +++ b/superset-frontend/src/pages/GroupsList/GroupsList.test.tsx @@ -44,6 +44,7 @@ const mockUser = { const rolesEndpoint = 'glob:*/security/roles/?*'; const usersEndpoint = 'glob:*/security/users/?*'; +const groupsEndpoint = 'glob:*/security/groups/*'; const mockRoles = Array.from({ length: 3 }, (_, i) => ({ id: i, @@ -65,9 +66,11 @@ fetchMock.get(rolesEndpoint, { count: 3, }); +fetchMock.get(groupsEndpoint, { result: [] }); + jest.mock('src/dashboard/util/permissionUtils', () => ({ ...jest.requireActual('src/dashboard/util/permissionUtils'), - isUserAdmin: jest.fn(() => true), + isUserAdmin: () => true, })); // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks @@ -86,7 +89,7 @@ describe('GroupsList', () => { }; beforeEach(() => { - fetchMock.resetHistory(); + fetchMock.clearHistory(); }); test('renders the page', async () => { @@ -97,7 +100,9 @@ describe('GroupsList', () => { test('fetches roles on load', async () => { await renderComponent(); await waitFor(() => { - expect(fetchMock.calls(rolesEndpoint).length).toBeGreaterThan(0); + expect(fetchMock.callHistory.calls(rolesEndpoint).length).toBeGreaterThan( + 0, + ); }); }); @@ -108,7 +113,7 @@ describe('GroupsList', () => { expect(await screen.findByTestId('Add Group-modal')).toBeInTheDocument(); }); - test('renders actions column for admin', async () => { + test.only('renders actions column for admin', async () => { await renderComponent(); expect(screen.getAllByText('Actions')[0]).toBeInTheDocument(); }); @@ -124,7 +129,7 @@ describe('GroupsList', () => { expect(within(filtersSelect).getByText(/users/i)).toBeInTheDocument(); }); - test('renders correct columns in the table', async () => { + test.only('renders correct columns in the table', async () => { await renderComponent(); const table = screen.getByRole('table'); diff --git a/superset-frontend/src/pages/Home/Home.test.tsx b/superset-frontend/src/pages/Home/Home.test.tsx index 65e536718fc7..fcdf94a36a4e 100644 --- a/superset-frontend/src/pages/Home/Home.test.tsx +++ b/superset-frontend/src/pages/Home/Home.test.tsx @@ -157,7 +157,7 @@ const renderWelcome = (props = mockedProps) => }); afterEach(() => { - fetchMock.resetHistory(); + fetchMock.clearHistory(); }); test('With sql role - renders', async () => { @@ -189,10 +189,10 @@ test('With sql role - renders distinct recent activities', async () => { test('With sql role - calls api methods in parallel on page load', async () => { await renderWelcome(); - expect(fetchMock.calls(chartsEndpoint)).toHaveLength(2); - expect(fetchMock.calls(recentActivityEndpoint)).toHaveLength(1); - expect(fetchMock.calls(savedQueryEndpoint)).toHaveLength(1); - expect(fetchMock.calls(dashboardsEndpoint)).toHaveLength(2); + expect(fetchMock.callHistory.calls(chartsEndpoint)).toHaveLength(2); + expect(fetchMock.callHistory.calls(recentActivityEndpoint)).toHaveLength(1); + expect(fetchMock.callHistory.calls(savedQueryEndpoint)).toHaveLength(1); + expect(fetchMock.callHistory.calls(dashboardsEndpoint)).toHaveLength(2); }); test('Without sql role - renders', async () => { @@ -214,10 +214,10 @@ test('Without sql role - renders all panels on the page on page load', async () test('Without sql role - calls api methods in parallel on page load', async () => { // @ts-ignore-next-line await renderWelcome(mockedPropsWithoutSqlRole); - expect(fetchMock.calls(chartsEndpoint)).toHaveLength(2); - expect(fetchMock.calls(recentActivityEndpoint)).toHaveLength(1); - expect(fetchMock.calls(savedQueryEndpoint)).toHaveLength(0); - expect(fetchMock.calls(dashboardsEndpoint)).toHaveLength(2); + expect(fetchMock.callHistory.calls(chartsEndpoint)).toHaveLength(2); + expect(fetchMock.callHistory.calls(recentActivityEndpoint)).toHaveLength(1); + expect(fetchMock.callHistory.calls(savedQueryEndpoint)).toHaveLength(0); + expect(fetchMock.callHistory.calls(dashboardsEndpoint)).toHaveLength(2); }); // Mock specific to the tests related to the toggle switch @@ -299,8 +299,8 @@ test('Should not make data fetch calls if `welcome.main.replacement` is defined' screen.getByText('welcome.main.replacement extension component'), ).toBeInTheDocument(); - expect(fetchMock.calls(chartsEndpoint)).toHaveLength(0); - expect(fetchMock.calls(dashboardsEndpoint)).toHaveLength(0); - expect(fetchMock.calls(recentActivityEndpoint)).toHaveLength(0); - expect(fetchMock.calls(savedQueryEndpoint)).toHaveLength(0); + expect(fetchMock.callHistory.calls(chartsEndpoint)).toHaveLength(0); + expect(fetchMock.callHistory.calls(dashboardsEndpoint)).toHaveLength(0); + expect(fetchMock.callHistory.calls(recentActivityEndpoint)).toHaveLength(0); + expect(fetchMock.callHistory.calls(savedQueryEndpoint)).toHaveLength(0); }); diff --git a/superset-frontend/src/pages/RolesList/RolesList.test.tsx b/superset-frontend/src/pages/RolesList/RolesList.test.tsx index 6990fe8bda10..eca699352ed5 100644 --- a/superset-frontend/src/pages/RolesList/RolesList.test.tsx +++ b/superset-frontend/src/pages/RolesList/RolesList.test.tsx @@ -122,7 +122,7 @@ describe('RolesList', () => { return mounted; } beforeEach(() => { - fetchMock.resetHistory(); + fetchMock.clearHistory(); }); test('renders', async () => { @@ -133,7 +133,7 @@ describe('RolesList', () => { test('fetches roles on load', async () => { await renderAndWait(); await waitFor(() => { - const calls = fetchMock.calls(rolesEndpoint); + const calls = fetchMock.callHistory.calls(rolesEndpoint); expect(calls.length).toBeGreaterThan(0); }); }); @@ -141,7 +141,7 @@ describe('RolesList', () => { test('fetches permissions on load', async () => { await renderAndWait(); await waitFor(() => { - const permissionCalls = fetchMock.calls(permissionsEndpoint); + const permissionCalls = fetchMock.callHistory.calls(permissionsEndpoint); expect(permissionCalls.length).toBeGreaterThan(0); }); }); diff --git a/superset-frontend/src/pages/RowLevelSecurityList/RowLevelSecurityList.test.tsx b/superset-frontend/src/pages/RowLevelSecurityList/RowLevelSecurityList.test.tsx index fefb58cec199..f68c44d03d8e 100644 --- a/superset-frontend/src/pages/RowLevelSecurityList/RowLevelSecurityList.test.tsx +++ b/superset-frontend/src/pages/RowLevelSecurityList/RowLevelSecurityList.test.tsx @@ -86,8 +86,16 @@ const mockRules = [ ], }, ]; -fetchMock.get(ruleListEndpoint, { result: mockRules, count: 2 }); -fetchMock.get(ruleInfoEndpoint, { permissions: ['can_read', 'can_write'] }); +fetchMock.get( + ruleListEndpoint, + { result: mockRules, count: 2 }, + { name: ruleListEndpoint }, +); +fetchMock.get( + ruleInfoEndpoint, + { permissions: ['can_read', 'can_write'] }, + { name: ruleInfoEndpoint }, +); global.URL.createObjectURL = jest.fn(); const mockUser = { @@ -122,31 +130,27 @@ describe('RuleList RTL', () => { }); test('fetched data', async () => { - fetchMock.resetHistory(); + fetchMock.clearHistory(); await renderAndWait(); - const apiCalls = fetchMock.calls(/rowlevelsecurity\/\?q/); + const apiCalls = fetchMock.callHistory.calls(/rowlevelsecurity\/\?q/); expect(apiCalls).toHaveLength(1); - expect(apiCalls[0][0]).toMatchInlineSnapshot( + expect(apiCalls[0].url).toMatchInlineSnapshot( `"http://localhost/api/v1/rowlevelsecurity/?q=(order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:25)"`, ); - fetchMock.resetHistory(); + fetchMock.clearHistory(); }); test('renders add rule button on empty state', async () => { - fetchMock.get( - ruleListEndpoint, - { result: [], count: 0 }, - { overwriteRoutes: true }, - ); + fetchMock.modifyRoute(ruleListEndpoint, { + response: { result: [], count: 0 }, + }); await renderAndWait(); const emptyAddRuleButton = await screen.findByTestId('add-rule-empty'); expect(emptyAddRuleButton).toBeInTheDocument(); - fetchMock.get( - ruleListEndpoint, - { result: mockRules, count: 2 }, - { overwriteRoutes: true }, - ); + fetchMock.modifyRoute(ruleListEndpoint, { + response: { result: mockRules, count: 2 }, + }); }); test('renders a "Rule" button to add a rule in bulk action', async () => { @@ -200,11 +204,9 @@ describe('RuleList RTL', () => { }); test('should not renders correct action buttons without write permission', async () => { - fetchMock.get( - ruleInfoEndpoint, - { permissions: ['can_read'] }, - { overwriteRoutes: true }, - ); + fetchMock.modifyRoute(ruleInfoEndpoint, { + response: { permissions: ['can_read'] }, + }); await renderAndWait(); @@ -214,11 +216,9 @@ describe('RuleList RTL', () => { const editActionIcon = screen.queryByTestId('edit-alt'); expect(editActionIcon).not.toBeInTheDocument(); - fetchMock.get( - ruleInfoEndpoint, - { permissions: ['can_read', 'can_write'] }, - { overwriteRoutes: true }, - ); + fetchMock.modifyRoute(ruleInfoEndpoint, { + response: { permissions: ['can_read', 'can_write'] }, + }); }); test('renders popover on new clicking rule button', async () => { diff --git a/superset-frontend/src/pages/SavedQueryList/SavedQueryList.test.tsx b/superset-frontend/src/pages/SavedQueryList/SavedQueryList.test.tsx index 5a31341b71f9..dc89f50fc146 100644 --- a/superset-frontend/src/pages/SavedQueryList/SavedQueryList.test.tsx +++ b/superset-frontend/src/pages/SavedQueryList/SavedQueryList.test.tsx @@ -58,21 +58,29 @@ const queriesEndpoint = 'glob:*/api/v1/saved_query/?*'; const queryEndpoint = 'glob:*/api/v1/saved_query/*'; const permalinkEndpoint = 'glob:*/api/v1/sqllab/permalink'; -fetchMock.get(queriesInfoEndpoint, { - permissions: ['can_write', 'can_read', 'can_export'], -}); - -fetchMock.get(queriesEndpoint, { - ids: [2, 0, 1], - result: mockQueries, - count: mockQueries.length, -}); +fetchMock.get( + queriesInfoEndpoint, + { + permissions: ['can_write', 'can_read', 'can_export'], + }, + { name: queriesInfoEndpoint }, +); + +fetchMock.get( + queriesEndpoint, + { + ids: [2, 0, 1], + result: mockQueries, + count: mockQueries.length, + }, + { name: queriesEndpoint }, +); fetchMock.post(permalinkEndpoint, { url: 'http://localhost/permalink', }); -fetchMock.delete(queryEndpoint, {}); +fetchMock.delete(queryEndpoint, {}, { name: queryEndpoint }); const renderList = (props = {}, storeOverrides = {}) => render( @@ -96,7 +104,7 @@ const renderList = (props = {}, storeOverrides = {}) => // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('SavedQueryList', () => { beforeEach(() => { - fetchMock.resetHistory(); + fetchMock.clearHistory(); }); test('renders', async () => { @@ -148,7 +156,7 @@ describe('SavedQueryList', () => { // Verify API call await waitFor(() => { - expect(fetchMock.calls(/saved_query\/0/, 'DELETE')).toHaveLength(1); + expect(fetchMock.callHistory.calls(/saved_query\/0/)).toHaveLength(1); }); }); @@ -165,21 +173,20 @@ describe('SavedQueryList', () => { // Verify API call await waitFor(() => { - const calls = fetchMock.calls(/saved_query\/\?q/); - expect(calls.length).toBeGreaterThan(0); - const lastCall = calls[calls.length - 1][0]; - expect(lastCall).toContain('order_column'); - expect(lastCall).toContain('page'); + const lastCall = fetchMock.callHistory.lastCall(/saved_query\/\?q/); + expect(lastCall).toBeDefined(); + expect(lastCall?.url).toContain('order_column'); + expect(lastCall?.url).toContain('page'); }); }); test('fetches data', async () => { renderList(); await waitFor(() => { - const calls = fetchMock.calls(/saved_query\/\?q/); - expect(calls).toHaveLength(1); - expect(calls[0][0]).toContain('order_column'); - expect(calls[0][0]).toContain('page'); + const lastCall = fetchMock.callHistory.lastCall(/saved_query\/\?q/); + expect(lastCall).toBeDefined(); + expect(lastCall?.url).toContain('order_column'); + expect(lastCall?.url).toContain('page'); }); }); @@ -195,9 +202,9 @@ describe('SavedQueryList', () => { // Verify API call includes sorting await waitFor(() => { - const calls = fetchMock.calls(/saved_query\/\?q/); - const lastCall = calls[calls.length - 1][0]; - const url = new URL(lastCall); + const url = new URL( + fetchMock.callHistory.lastCall(/saved_query\/\?q/)?.url as string, + ); const params = new URLSearchParams(url.search); const qParam = params.get('q'); expect(qParam).toContain('order_column:label'); @@ -206,17 +213,19 @@ describe('SavedQueryList', () => { test('shows/hides elements based on permissions', async () => { // Mock info response without write permission + fetchMock.removeRoute(queriesInfoEndpoint); fetchMock.get( queriesInfoEndpoint, { permissions: ['can_read'] }, - { overwriteRoutes: true }, + { name: queriesInfoEndpoint }, ); // Mock list response + fetchMock.removeRoute(queriesEndpoint); fetchMock.get( queriesEndpoint, { result: mockQueries, count: mockQueries.length }, - { overwriteRoutes: true }, + { name: queriesEndpoint }, ); renderList(); diff --git a/superset-frontend/src/pages/SqlLab/SqlLab.test.tsx b/superset-frontend/src/pages/SqlLab/SqlLab.test.tsx index 24d5a508a8cf..50b0a49e8b4e 100644 --- a/superset-frontend/src/pages/SqlLab/SqlLab.test.tsx +++ b/superset-frontend/src/pages/SqlLab/SqlLab.test.tsx @@ -56,7 +56,7 @@ const expectedResult = fakeApiResult.result; const sqlLabInitialStateApiRoute = `glob:*/api/v1/sqllab/`; afterEach(() => { - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); act(() => { store.dispatch(api.util.resetApiState()); }); @@ -75,7 +75,9 @@ test('is valid', () => { }); test('fetches initial data and renders', async () => { - expect(fetchMock.calls(sqlLabInitialStateApiRoute).length).toBe(0); + expect(fetchMock.callHistory.calls(sqlLabInitialStateApiRoute).length).toBe( + 0, + ); const storeWithSqlLab = createStore({}, reducers); const { getByTestId } = render(, { useRedux: true, @@ -84,7 +86,9 @@ test('fetches initial data and renders', async () => { }); await waitFor(() => - expect(fetchMock.calls(sqlLabInitialStateApiRoute).length).toBe(1), + expect(fetchMock.callHistory.calls(sqlLabInitialStateApiRoute).length).toBe( + 1, + ), ); expect(getByTestId('mock-sqllab-app')).toBeInTheDocument(); diff --git a/superset-frontend/src/pages/ThemeList/ThemeList.test.tsx b/superset-frontend/src/pages/ThemeList/ThemeList.test.tsx index 69151fcc4626..aff01eb5fd39 100644 --- a/superset-frontend/src/pages/ThemeList/ThemeList.test.tsx +++ b/superset-frontend/src/pages/ThemeList/ThemeList.test.tsx @@ -151,7 +151,7 @@ beforeEach(() => { getAppliedThemeId: mockGetAppliedThemeId, }); - fetchMock.reset(); + fetchMock.clearHistory().removeRoutes(); fetchMock.get(themesInfoEndpoint, { permissions: ['can_read', 'can_write', 'can_export'], }); @@ -164,7 +164,7 @@ beforeEach(() => { }); afterEach(() => { - fetchMock.restore(); + fetchMock.clearHistory().removeRoutes(); jest.clearAllMocks(); }); diff --git a/superset-frontend/src/pages/UserInfo/UserInfo.test.tsx b/superset-frontend/src/pages/UserInfo/UserInfo.test.tsx index e160c5709f3c..e12ce245b4e4 100644 --- a/superset-frontend/src/pages/UserInfo/UserInfo.test.tsx +++ b/superset-frontend/src/pages/UserInfo/UserInfo.test.tsx @@ -73,7 +73,7 @@ describe('UserInfo', () => { }); beforeEach(() => { - fetchMock.restore(); + fetchMock.clearHistory().removeRoutes(); fetchMock.get(meEndpoint, { result: { ...mockUser, @@ -84,7 +84,7 @@ describe('UserInfo', () => { }); afterEach(() => { - fetchMock.restore(); + fetchMock.clearHistory().removeRoutes(); }); test('renders the user info page', async () => { @@ -105,7 +105,7 @@ describe('UserInfo', () => { test('calls the /me endpoint on mount', async () => { await renderPage(); await waitFor(() => { - expect(fetchMock.called(meEndpoint)).toBe(true); + expect(fetchMock.callHistory.called(meEndpoint)).toBe(true); }); }); diff --git a/superset-frontend/src/pages/UserRegistrations/UserRegistrations.test.tsx b/superset-frontend/src/pages/UserRegistrations/UserRegistrations.test.tsx index bfd13601314b..fbdaa57aefc8 100644 --- a/superset-frontend/src/pages/UserRegistrations/UserRegistrations.test.tsx +++ b/superset-frontend/src/pages/UserRegistrations/UserRegistrations.test.tsx @@ -50,7 +50,7 @@ describe('UserRegistrations', () => { }); test('fetches and renders user registrations', async () => { expect(await screen.findByText('User registrations')).toBeVisible(); - const calls = fetchMock.calls(userRegistrationsEndpoint); + const calls = fetchMock.callHistory.calls(userRegistrationsEndpoint); expect(calls.length).toBeGreaterThan(0); }); }); diff --git a/superset-frontend/src/pages/UsersList/UsersList.test.tsx b/superset-frontend/src/pages/UsersList/UsersList.test.tsx index 5edc25b62d47..62ff7ff4756d 100644 --- a/superset-frontend/src/pages/UsersList/UsersList.test.tsx +++ b/superset-frontend/src/pages/UsersList/UsersList.test.tsx @@ -36,6 +36,7 @@ const store = mockStore({}); const rolesEndpoint = 'glob:*/security/roles/?*'; const usersEndpoint = 'glob:*/security/users/?*'; +const groupsEndpoint = 'glob:*/security/groups/*'; const mockRoles = new Array(3).fill(undefined).map((_, i) => ({ id: i, @@ -73,6 +74,8 @@ fetchMock.get(rolesEndpoint, { count: 3, }); +fetchMock.get(groupsEndpoint, { result: [] }); + jest.mock('src/dashboard/util/permissionUtils', () => ({ ...jest.requireActual('src/dashboard/util/permissionUtils'), isUserAdmin: jest.fn(() => true), @@ -107,7 +110,7 @@ describe('UsersList', () => { return mounted; } beforeEach(() => { - fetchMock.resetHistory(); + fetchMock.clearHistory(); }); test('renders', async () => { @@ -118,7 +121,7 @@ describe('UsersList', () => { test('fetches users on load', async () => { await renderAndWait(); await waitFor(() => { - const calls = fetchMock.calls(usersEndpoint); + const calls = fetchMock.callHistory.calls(usersEndpoint); expect(calls.length).toBeGreaterThan(0); }); }); @@ -126,7 +129,7 @@ describe('UsersList', () => { test('fetches roles on load', async () => { await renderAndWait(); await waitFor(() => { - const calls = fetchMock.calls(rolesEndpoint); + const calls = fetchMock.callHistory.calls(rolesEndpoint); expect(calls.length).toBeGreaterThan(0); }); }); From 1cee4c8b19ed75f46b2ece55d479a4e1b6cc55c0 Mon Sep 17 00:00:00 2001 From: hainenber Date: Tue, 27 Jan 2026 23:04:58 +0700 Subject: [PATCH 4/5] chore: sync `fetch-mock` version Signed-off-by: hainenber --- superset-frontend/package-lock.json | 24 ++++-------------------- superset-frontend/package.json | 2 +- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 3d86cb15592b..069256f8e863 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -240,7 +240,7 @@ "eslint-plugin-storybook": "^0.8.0", "eslint-plugin-testing-library": "^7.15.4", "eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors", - "fetch-mock": "12.5.5", + "fetch-mock": "^12.6.0", "fork-ts-checker-webpack-plugin": "^9.1.0", "history": "^5.3.0", "html-webpack-plugin": "^5.6.6", @@ -32105,9 +32105,9 @@ } }, "node_modules/fetch-mock": { - "version": "12.5.5", - "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-12.5.5.tgz", - "integrity": "sha512-yeziyj/AsbpaQJ91Z+FvQW3h02JK4VS5FHDCz+xiihgMUsXIuhgg/XjC9UEPNtOruucfTSR2uQWcjevDmSXSUQ==", + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-12.6.0.tgz", + "integrity": "sha512-oAy0OqAvjAvduqCeWveBix7LLuDbARPqZZ8ERYtBcCURA3gy7EALA3XWq0tCNxsSg+RmmJqyaeeZlOCV9abv6w==", "dev": true, "license": "MIT", "dependencies": { @@ -63818,22 +63818,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/superset-ui-core/node_modules/fetch-mock": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-12.6.0.tgz", - "integrity": "sha512-oAy0OqAvjAvduqCeWveBix7LLuDbARPqZZ8ERYtBcCURA3gy7EALA3XWq0tCNxsSg+RmmJqyaeeZlOCV9abv6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/glob-to-regexp": "^0.4.4", - "dequal": "^2.0.3", - "glob-to-regexp": "^0.4.1", - "regexparam": "^3.0.0" - }, - "engines": { - "node": ">=18.11.0" - } - }, "packages/superset-ui-core/node_modules/hast-util-parse-selector": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index da5fde38abb5..23494da9bbd4 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -321,7 +321,7 @@ "eslint-plugin-storybook": "^0.8.0", "eslint-plugin-testing-library": "^7.15.4", "eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors", - "fetch-mock": "12.5.5", + "fetch-mock": "^12.6.0", "fork-ts-checker-webpack-plugin": "^9.1.0", "history": "^5.3.0", "html-webpack-plugin": "^5.6.6", From 76d835d10b4bd58f95ecdf715a88709bd50aa80a Mon Sep 17 00:00:00 2001 From: hainenber Date: Wed, 28 Jan 2026 07:29:47 +0700 Subject: [PATCH 5/5] chore: combine `clearHistory` and `removeRoutes` to single expression --- .../test/connection/SupersetClientClass.test.ts | 5 +---- .../superset-ui-core/test/connection/callApi/callApi.test.ts | 5 +---- .../test/time-comparison/fetchTimeRange.test.ts | 5 +---- .../components/AceEditorWrapper/useAnnotations.test.ts | 3 +-- .../src/SqlLab/components/TableElement/TableElement.test.tsx | 5 +---- .../components/DatabaseSelector/DatabaseSelector.test.tsx | 3 +-- .../DatasourceEditor/tests/DatasourceEditor.test.tsx | 3 +-- .../DatasourceEditor/tests/DatasourceEditorCurrency.test.tsx | 3 +-- .../CrossFilters/ScopingModal/ScopingModal.test.tsx | 5 +---- .../src/explore/actions/saveModalActions.test.ts | 5 +---- .../controls/DatasourceControl/DatasourceControl.test.tsx | 3 +-- .../src/explore/components/controls/ViewQuery.test.tsx | 3 +-- .../src/features/databases/DatabaseModal/index.test.tsx | 5 +---- .../src/pages/ChartList/ChartList.cardview.test.tsx | 5 +---- .../src/pages/ChartList/ChartList.listview.test.tsx | 2 +- .../src/pages/ChartList/ChartList.permissions.test.tsx | 3 +-- 16 files changed, 16 insertions(+), 47 deletions(-) diff --git a/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts b/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts index d0d3858ccca9..a5e0fe6cc669 100644 --- a/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts @@ -264,10 +264,7 @@ describe('SupersetClientClass', () => { }); describe('requests', () => { - afterEach(() => { - fetchMock.removeRoutes(); - fetchMock.clearHistory(); - }); + afterEach(() => fetchMock.clearHistory().removeRoutes()); const protocol = 'https:'; const host = 'host'; diff --git a/superset-frontend/packages/superset-ui-core/test/connection/callApi/callApi.test.ts b/superset-frontend/packages/superset-ui-core/test/connection/callApi/callApi.test.ts index 2f222088d358..35e8ea24fdbb 100644 --- a/superset-frontend/packages/superset-ui-core/test/connection/callApi/callApi.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/connection/callApi/callApi.test.ts @@ -69,10 +69,7 @@ describe('callApi()', () => { fetchMock.get(mockErrorUrl, () => Promise.reject(mockErrorPayload)); }); - afterEach(() => { - fetchMock.clearHistory(); - fetchMock.removeRoutes(); - }); + afterEach(() => fetchMock.clearHistory().removeRoutes()); describe('request config', () => { it('calls the right url with the specified method', async () => { diff --git a/superset-frontend/packages/superset-ui-core/test/time-comparison/fetchTimeRange.test.ts b/superset-frontend/packages/superset-ui-core/test/time-comparison/fetchTimeRange.test.ts index 00a1e5cf3a35..84bbaa5e6282 100644 --- a/superset-frontend/packages/superset-ui-core/test/time-comparison/fetchTimeRange.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/time-comparison/fetchTimeRange.test.ts @@ -28,10 +28,7 @@ import { beforeAll(() => fetchMock.mockGlobal()); afterAll(() => fetchMock.hardReset()); -afterEach(() => { - fetchMock.removeRoutes(); - fetchMock.clearHistory(); -}); +afterEach(() => fetchMock.clearHistory().removeRoutes()); test('generates proper time range string', () => { expect( diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useAnnotations.test.ts b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useAnnotations.test.ts index 05d27b8f5ed6..4fcf7132b3c9 100644 --- a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useAnnotations.test.ts +++ b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useAnnotations.test.ts @@ -50,8 +50,7 @@ jest.mock('@superset-ui/core', () => ({ })); afterEach(() => { - fetchMock.clearHistory(); - fetchMock.removeRoutes(); + fetchMock.clearHistory().removeRoutes(); act(() => { store.dispatch(api.util.resetApiState()); }); diff --git a/superset-frontend/src/SqlLab/components/TableElement/TableElement.test.tsx b/superset-frontend/src/SqlLab/components/TableElement/TableElement.test.tsx index 2bb3ff3f2120..990e6eb44885 100644 --- a/superset-frontend/src/SqlLab/components/TableElement/TableElement.test.tsx +++ b/superset-frontend/src/SqlLab/components/TableElement/TableElement.test.tsx @@ -69,10 +69,7 @@ beforeEach(() => { fetchMock.post(updateTableSchemaEndpoint, {}); }); -afterEach(() => { - fetchMock.removeRoutes(); - fetchMock.clearHistory(); -}); +afterEach(() => fetchMock.clearHistory().removeRoutes()); const mockedProps = { table: { diff --git a/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx b/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx index fdacff9577fe..4e92d4dbbba3 100644 --- a/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx +++ b/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx @@ -194,8 +194,7 @@ beforeEach(() => { }); afterEach(() => { - fetchMock.clearHistory(); - fetchMock.removeRoutes(); + fetchMock.clearHistory().removeRoutes(); act(() => { store.dispatch(api.util.resetApiState()); }); diff --git a/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditor.test.tsx b/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditor.test.tsx index 43ec1a99b6ac..5e243224f1a0 100644 --- a/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditor.test.tsx +++ b/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditor.test.tsx @@ -49,8 +49,7 @@ beforeEach(() => { afterEach(async () => { await cleanupAsyncOperations(); - fetchMock.removeRoutes(); - fetchMock.clearHistory(); + fetchMock.clearHistory().removeRoutes(); // Reset module mock since jest.fn() doesn't support mockRestore() jest.mocked(isFeatureEnabled).mockReset(); // Restore console.error if it was spied on diff --git a/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditorCurrency.test.tsx b/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditorCurrency.test.tsx index 2c8e913f97d2..445a47b087fa 100644 --- a/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditorCurrency.test.tsx +++ b/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditorCurrency.test.tsx @@ -78,8 +78,7 @@ beforeEach(() => { afterEach(async () => { await cleanupAsyncOperations(); - fetchMock.clearHistory(); - fetchMock.removeRoutes(); + fetchMock.clearHistory().removeRoutes(); }); test('renders currency section in metrics tab', async () => { diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/ScopingModal/ScopingModal.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/ScopingModal/ScopingModal.test.tsx index 32d5341e92af..d11c15754772 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/ScopingModal/ScopingModal.test.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/ScopingModal/ScopingModal.test.tsx @@ -154,10 +154,7 @@ beforeEach(() => { fetchMock.put(DASHBOARD_UPDATE_URL, 200, { name: DASHBOARD_UPDATE_URL }); }); -afterEach(() => { - fetchMock.clearHistory(); - fetchMock.removeRoutes(); -}); +afterEach(() => fetchMock.clearHistory().removeRoutes()); test('renders modal', () => { setup(); diff --git a/superset-frontend/src/explore/actions/saveModalActions.test.ts b/superset-frontend/src/explore/actions/saveModalActions.test.ts index 04afdf659e12..253afdddc10d 100644 --- a/superset-frontend/src/explore/actions/saveModalActions.test.ts +++ b/superset-frontend/src/explore/actions/saveModalActions.test.ts @@ -98,10 +98,7 @@ jest.mock('../exploreUtils', () => ({ buildV1ChartDataPayload: jest.fn(() => queryContext), })); -beforeEach(() => { - fetchMock.removeRoutes(); - fetchMock.clearHistory(); -}); +beforeEach(() => fetchMock.clearHistory().removeRoutes()); /** * Tests updateSlice action diff --git a/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.tsx b/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.tsx index 619964f02708..5f982bb8d1fc 100644 --- a/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.tsx +++ b/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.tsx @@ -43,8 +43,7 @@ beforeEach(() => { afterEach(() => { // @ts-ignore window.location = originalLocation; - fetchMock.clearHistory(); - fetchMock.removeRoutes(); + fetchMock.clearHistory().removeRoutes(); jest.clearAllMocks(); // Clears mock history but keeps spy in place }); diff --git a/superset-frontend/src/explore/components/controls/ViewQuery.test.tsx b/superset-frontend/src/explore/components/controls/ViewQuery.test.tsx index bfea6fca5fa6..5fd67dafa076 100644 --- a/superset-frontend/src/explore/components/controls/ViewQuery.test.tsx +++ b/superset-frontend/src/explore/components/controls/ViewQuery.test.tsx @@ -95,8 +95,7 @@ beforeEach(() => { afterEach(() => { jest.resetAllMocks(); - fetchMock.clearHistory(); - fetchMock.removeRoutes(); + fetchMock.clearHistory().removeRoutes(); }); const getFormatSwitch = () => diff --git a/superset-frontend/src/features/databases/DatabaseModal/index.test.tsx b/superset-frontend/src/features/databases/DatabaseModal/index.test.tsx index 4d90e7d6d580..2cc9fbe1a87f 100644 --- a/superset-frontend/src/features/databases/DatabaseModal/index.test.tsx +++ b/superset-frontend/src/features/databases/DatabaseModal/index.test.tsx @@ -319,10 +319,7 @@ describe('DatabaseModal', () => { beforeEach(() => { jest.clearAllMocks(); }); - afterEach(() => { - fetchMock.clearHistory(); - fetchMock.removeRoutes(); - }); + afterEach(() => fetchMock.clearHistory().removeRoutes()); const setup = (propsOverwrite: Partial = {}) => render(, { diff --git a/superset-frontend/src/pages/ChartList/ChartList.cardview.test.tsx b/superset-frontend/src/pages/ChartList/ChartList.cardview.test.tsx index e7df78f09460..e64204329393 100644 --- a/superset-frontend/src/pages/ChartList/ChartList.cardview.test.tsx +++ b/superset-frontend/src/pages/ChartList/ChartList.cardview.test.tsx @@ -67,10 +67,7 @@ describe('ChartList Card View Tests', () => { ); }); - afterEach(() => { - fetchMock.clearHistory(); - fetchMock.removeRoutes(); - }); + afterEach(() => fetchMock.clearHistory().removeRoutes()); test('renders ChartList in card view', async () => { renderChartList(mockUser); diff --git a/superset-frontend/src/pages/ChartList/ChartList.listview.test.tsx b/superset-frontend/src/pages/ChartList/ChartList.listview.test.tsx index 13d6e73eea85..094e5530bcd4 100644 --- a/superset-frontend/src/pages/ChartList/ChartList.listview.test.tsx +++ b/superset-frontend/src/pages/ChartList/ChartList.listview.test.tsx @@ -67,7 +67,7 @@ beforeEach(() => { }); afterEach(() => { - fetchMock.clearHistory(); + fetchMock.clearHistory().removeRoutes(); fetchMock.removeRoutes(); mockIsFeatureEnabled.mockReset(); }); diff --git a/superset-frontend/src/pages/ChartList/ChartList.permissions.test.tsx b/superset-frontend/src/pages/ChartList/ChartList.permissions.test.tsx index 9b2edee4e610..5406f7b4caff 100644 --- a/superset-frontend/src/pages/ChartList/ChartList.permissions.test.tsx +++ b/superset-frontend/src/pages/ChartList/ChartList.permissions.test.tsx @@ -164,8 +164,7 @@ const renderWithPermissions = async ( // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('ChartList - Permission-based UI Tests', () => { beforeEach(() => { - fetchMock.clearHistory(); - fetchMock.removeRoutes(); + fetchMock.clearHistory().removeRoutes(); ( isFeatureEnabled as jest.MockedFunction ).mockReset();