This is an automated email from the ASF dual-hosted git repository. jli pushed a commit to branch fix-missing-dataset-list-test-helper in repository https://gitbox.apache.org/repos/asf/superset.git
commit e13418fdcb1e5efbedca0ea6511aadedc9a69150 Author: Joe Li <[email protected]> AuthorDate: Mon Feb 2 19:35:30 2026 -0800 fix(tests): update DatasetList tests to new fetch-mock API The fetch-mock library was upgraded and the API changed: - fetchMock.reset() → fetchMock.removeRoutes() - fetchMock.resetHistory() → fetchMock.clearHistory() - fetchMock.restore() → fetchMock.removeRoutes() - fetchMock.calls() → fetchMock.callHistory.calls() - call[0] → call.url, call[1] → call.options - { overwriteRoutes: true } option removed (no longer exists) - QueryParamProvider now requires adapter prop Co-Authored-By: Claude Opus 4.5 <[email protected]> --- .../DatasetList/DatasetList.behavior.test.tsx | 42 +- .../DatasetList/DatasetList.integration.test.tsx | 52 +-- .../DatasetList/DatasetList.listview.test.tsx | 446 +++++++++------------ .../DatasetList/DatasetList.permissions.test.tsx | 14 +- .../src/pages/DatasetList/DatasetList.test.tsx | 101 ++--- .../pages/DatasetList/DatasetList.testHelpers.tsx | 83 ++-- 6 files changed, 318 insertions(+), 420 deletions(-) diff --git a/superset-frontend/src/pages/DatasetList/DatasetList.behavior.test.tsx b/superset-frontend/src/pages/DatasetList/DatasetList.behavior.test.tsx index af73876de11..3f56d7cef5f 100644 --- a/superset-frontend/src/pages/DatasetList/DatasetList.behavior.test.tsx +++ b/superset-frontend/src/pages/DatasetList/DatasetList.behavior.test.tsx @@ -62,8 +62,8 @@ afterEach(async () => { // Reset browser history state to prevent query params leaking between tests window.history.replaceState({}, '', '/'); - fetchMock.resetHistory(); - fetchMock.restore(); + fetchMock.clearHistory(); + fetchMock.removeRoutes(); jest.restoreAllMocks(); }); @@ -97,7 +97,7 @@ test('typing in search triggers debounced API call with search filter', async () const searchInput = within(searchContainer).getByRole('textbox'); // Record initial API calls - const initialCallCount = fetchMock.calls(API_ENDPOINTS.DATASETS).length; + const initialCallCount = fetchMock.callHistory.calls(API_ENDPOINTS.DATASETS).length; // Type search query and submit with Enter to trigger the debounced fetch await userEvent.type(searchInput, 'sales{enter}'); @@ -105,16 +105,16 @@ test('typing in search triggers debounced API call with search filter', async () // Wait for debounced API call await waitFor( () => { - const calls = fetchMock.calls(API_ENDPOINTS.DATASETS); + const calls = fetchMock.callHistory.calls(API_ENDPOINTS.DATASETS); expect(calls.length).toBeGreaterThan(initialCallCount); }, { timeout: 5000 }, ); // Verify the latest API call includes search filter in URL - const calls = fetchMock.calls(API_ENDPOINTS.DATASETS); + const calls = fetchMock.callHistory.calls(API_ENDPOINTS.DATASETS); const latestCall = calls[calls.length - 1]; - const url = latestCall[0] as string; + const url = latestCall.url as string; // URL should contain filters parameter with search term expect(url).toContain('filters'); @@ -140,7 +140,7 @@ test('500 error triggers danger toast with error message', async () => { status: 500, body: { message: 'Internal Server Error' }, }, - { overwriteRoutes: true }, + ); // Pass toast spy directly via props to bypass withToasts HOC @@ -162,7 +162,7 @@ test('500 error triggers danger toast with error message', async () => { // Verify toast message contains error keywords expect(addDangerToast.mock.calls.length).toBeGreaterThan(0); - const toastMessage = String(addDangerToast.mock.calls[0][0]); + const toastMessage = String(addDangerToast.mock.calls[0].url); expect( toastMessage.includes('error') || toastMessage.includes('Error') || @@ -177,7 +177,7 @@ test('network timeout triggers danger toast', async () => { fetchMock.get( API_ENDPOINTS.DATASETS, { throws: new Error('Network timeout') }, - { overwriteRoutes: true }, + ); // Pass toast spy directly via props to bypass withToasts HOC @@ -199,7 +199,7 @@ test('network timeout triggers danger toast', async () => { // Verify toast message contains timeout/network keywords expect(addDangerToast.mock.calls.length).toBeGreaterThan(0); - const toastMessage = String(addDangerToast.mock.calls[0][0]); + const toastMessage = String(addDangerToast.mock.calls[0].url); expect( toastMessage.includes('timeout') || toastMessage.includes('Timeout') || @@ -218,7 +218,7 @@ test('clicking delete opens modal with related objects count', async () => { fetchMock.get( API_ENDPOINTS.DATASETS, { result: [datasetToDelete], count: 1 }, - { overwriteRoutes: true }, + ); renderDatasetList(mockAdminUser); @@ -259,7 +259,7 @@ test('clicking export calls handleResourceExport with dataset ID', async () => { fetchMock.get( API_ENDPOINTS.DATASETS, { result: [datasetToExport], count: 1 }, - { overwriteRoutes: true }, + ); renderDatasetList(mockAdminUser); @@ -293,13 +293,13 @@ test('clicking duplicate opens modal and submits duplicate request', async () => fetchMock.get( API_ENDPOINTS.DATASETS, { result: [datasetToDuplicate], count: 1 }, - { overwriteRoutes: true }, + ); fetchMock.post( API_ENDPOINTS.DATASET_DUPLICATE, { id: 999, table_name: 'Copy of Dataset' }, - { overwriteRoutes: true }, + ); const addSuccessToast = jest.fn(); @@ -314,7 +314,7 @@ test('clicking duplicate opens modal and submits duplicate request', async () => }); // Track initial dataset list API calls BEFORE duplicate action - const initialDatasetCallCount = fetchMock.calls( + const initialDatasetCallCount = fetchMock.callHistory.calls( API_ENDPOINTS.DATASETS, ).length; @@ -341,11 +341,11 @@ test('clicking duplicate opens modal and submits duplicate request', async () => // Verify duplicate API was called with correct payload await waitFor(() => { - const calls = fetchMock.calls(API_ENDPOINTS.DATASET_DUPLICATE); + const calls = fetchMock.callHistory.calls(API_ENDPOINTS.DATASET_DUPLICATE); expect(calls.length).toBeGreaterThan(0); // Verify POST body contains correct dataset info - const requestBody = JSON.parse(calls[0][1]?.body as string); + const requestBody = JSON.parse(calls[0].options?.body as string); expect(requestBody.base_model_id).toBe(datasetToDuplicate.id); expect(requestBody.table_name).toBe('Copy of Dataset'); }); @@ -358,7 +358,7 @@ test('clicking duplicate opens modal and submits duplicate request', async () => // Verify refreshData() is called (observable via new dataset list API call) await waitFor( () => { - const datasetCalls = fetchMock.calls(API_ENDPOINTS.DATASETS); + const datasetCalls = fetchMock.callHistory.calls(API_ENDPOINTS.DATASETS); expect(datasetCalls.length).toBeGreaterThan(initialDatasetCallCount); }, { timeout: 3000 }, @@ -382,7 +382,7 @@ test('certified dataset shows badge and tooltip with certification details', asy fetchMock.get( API_ENDPOINTS.DATASETS, { result: [certifiedDataset], count: 1 }, - { overwriteRoutes: true }, + ); renderDatasetList(mockAdminUser); @@ -423,7 +423,7 @@ test('dataset with warning shows icon and tooltip with markdown content', async fetchMock.get( API_ENDPOINTS.DATASETS, { result: [datasetWithWarning], count: 1 }, - { overwriteRoutes: true }, + ); renderDatasetList(mockAdminUser); @@ -458,7 +458,7 @@ test('dataset name links to Explore with correct URL and accessible label', asyn fetchMock.get( API_ENDPOINTS.DATASETS, { result: [dataset], count: 1 }, - { overwriteRoutes: true }, + ); renderDatasetList(mockAdminUser); diff --git a/superset-frontend/src/pages/DatasetList/DatasetList.integration.test.tsx b/superset-frontend/src/pages/DatasetList/DatasetList.integration.test.tsx index 0b228509b2a..80490826077 100644 --- a/superset-frontend/src/pages/DatasetList/DatasetList.integration.test.tsx +++ b/superset-frontend/src/pages/DatasetList/DatasetList.integration.test.tsx @@ -62,8 +62,8 @@ afterEach(async () => { // Reset browser history state to prevent query params leaking between tests window.history.replaceState({}, '', '/'); - fetchMock.resetHistory(); - fetchMock.restore(); + fetchMock.clearHistory(); + fetchMock.removeRoutes(); jest.restoreAllMocks(); }); @@ -72,11 +72,10 @@ test('ListView provider correctly merges filter + sort + pagination state on ref // the ListView provider correctly merges them for the API call. // Component tests verify individual pieces persist; this verifies they COMBINE correctly. - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: mockDatasets, count: mockDatasets.length }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { + result: mockDatasets, + count: mockDatasets.length, + }); renderDatasetList(mockAdminUser); @@ -90,30 +89,34 @@ test('ListView provider correctly merges filter + sort + pagination state on ref name: /Name/i, }); - const callsBeforeSort = fetchMock.calls(API_ENDPOINTS.DATASETS).length; + const callsBeforeSort = fetchMock.callHistory.calls( + API_ENDPOINTS.DATASETS, + ).length; await userEvent.click(nameHeader); // Wait for sort-triggered refetch to complete before applying filter await waitFor(() => { - expect(fetchMock.calls(API_ENDPOINTS.DATASETS).length).toBeGreaterThan( - callsBeforeSort, - ); + expect( + fetchMock.callHistory.calls(API_ENDPOINTS.DATASETS).length, + ).toBeGreaterThan(callsBeforeSort); }); // 2. Apply a filter using selectOption helper - const beforeFilterCallCount = fetchMock.calls(API_ENDPOINTS.DATASETS).length; + const beforeFilterCallCount = fetchMock.callHistory.calls( + API_ENDPOINTS.DATASETS, + ).length; await selectOption('Virtual', 'Type'); // Wait for filter API call to complete await waitFor(() => { - const calls = fetchMock.calls(API_ENDPOINTS.DATASETS); + const calls = fetchMock.callHistory.calls(API_ENDPOINTS.DATASETS); expect(calls.length).toBeGreaterThan(beforeFilterCallCount); }); // 3. Verify the final API call contains ALL three state pieces merged correctly - const calls = fetchMock.calls(API_ENDPOINTS.DATASETS); + const calls = fetchMock.callHistory.calls(API_ENDPOINTS.DATASETS); const latestCall = calls[calls.length - 1]; - const url = latestCall[0] as string; + const url = latestCall.url as string; // Decode the rison payload using URL parser const risonPayload = new URL(url, 'http://localhost').searchParams.get('q'); @@ -147,11 +150,10 @@ test('bulk action orchestration: selection → action → cleanup cycle works co setupBulkDeleteMocks(); - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: mockDatasets, count: mockDatasets.length }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { + result: mockDatasets, + count: mockDatasets.length, + }); renderDatasetList(mockAdminUser); @@ -213,7 +215,7 @@ test('bulk action orchestration: selection → action → cleanup cycle works co await userEvent.type(confirmInput, 'DELETE'); // Capture datasets call count before confirming - const datasetsCallCountBeforeDelete = fetchMock.calls( + const datasetsCallCountBeforeDelete = fetchMock.callHistory.calls( API_ENDPOINTS.DATASETS, ).length; @@ -224,7 +226,9 @@ test('bulk action orchestration: selection → action → cleanup cycle works co // 3. Wait for bulk delete API call to be made await waitFor(() => { - const deleteCalls = fetchMock.calls(API_ENDPOINTS.DATASET_BULK_DELETE); + const deleteCalls = fetchMock.callHistory.calls( + API_ENDPOINTS.DATASET_BULK_DELETE, + ); expect(deleteCalls.length).toBeGreaterThan(0); }); @@ -235,7 +239,9 @@ test('bulk action orchestration: selection → action → cleanup cycle works co // Wait for datasets refetch after delete await waitFor(() => { - const datasetsCallCount = fetchMock.calls(API_ENDPOINTS.DATASETS).length; + const datasetsCallCount = fetchMock.callHistory.calls( + API_ENDPOINTS.DATASETS, + ).length; expect(datasetsCallCount).toBeGreaterThan(datasetsCallCountBeforeDelete); }); diff --git a/superset-frontend/src/pages/DatasetList/DatasetList.listview.test.tsx b/superset-frontend/src/pages/DatasetList/DatasetList.listview.test.tsx index b43f78dc9c4..84573b873ee 100644 --- a/superset-frontend/src/pages/DatasetList/DatasetList.listview.test.tsx +++ b/superset-frontend/src/pages/DatasetList/DatasetList.listview.test.tsx @@ -112,11 +112,7 @@ const setupErrorTestScenario = ({ }); // Configure fetchMock to return single dataset - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [dataset], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { result: [dataset], count: 1 }); // Render component with toast mocks renderDatasetList(mockAdminUser, { @@ -143,8 +139,8 @@ afterEach(async () => { // QueryParamProvider reads from window.history, which persists across renders window.history.replaceState({}, '', '/'); - fetchMock.resetHistory(); - fetchMock.restore(); + fetchMock.clearHistory(); + fetchMock.removeRoutes(); jest.restoreAllMocks(); }); @@ -199,11 +195,7 @@ test('renders all required column headers', async () => { test('displays dataset name in Name column', async () => { const dataset = mockDatasets[0]; - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [dataset], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { result: [dataset], count: 1 }); renderDatasetList(mockAdminUser); @@ -216,11 +208,10 @@ test('displays dataset type as Physical or Virtual', async () => { const physicalDataset = mockDatasets[0]; // kind: 'physical' const virtualDataset = mockDatasets[1]; // kind: 'virtual' - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [physicalDataset, virtualDataset], count: 2 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { + result: [physicalDataset, virtualDataset], + count: 2, + }); renderDatasetList(mockAdminUser); @@ -234,11 +225,7 @@ test('displays dataset type as Physical or Virtual', async () => { test('displays database name in Database column', async () => { const dataset = mockDatasets[0]; - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [dataset], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { result: [dataset], count: 1 }); renderDatasetList(mockAdminUser); @@ -252,11 +239,7 @@ test('displays database name in Database column', async () => { test('displays schema name in Schema column', async () => { const dataset = mockDatasets[0]; - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [dataset], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { result: [dataset], count: 1 }); renderDatasetList(mockAdminUser); @@ -268,11 +251,7 @@ test('displays schema name in Schema column', async () => { test('displays last modified date in humanized format', async () => { const dataset = mockDatasets[0]; - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [dataset], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { result: [dataset], count: 1 }); renderDatasetList(mockAdminUser); @@ -296,21 +275,23 @@ test('sorting by Name column updates API call with sort parameter', async () => }); // Record initial calls - const initialCalls = fetchMock.calls(API_ENDPOINTS.DATASETS).length; + const initialCalls = fetchMock.callHistory.calls( + API_ENDPOINTS.DATASETS, + ).length; // Click Name header to sort await userEvent.click(nameHeader); // Wait for new API call await waitFor(() => { - const calls = fetchMock.calls(API_ENDPOINTS.DATASETS); + const calls = fetchMock.callHistory.calls(API_ENDPOINTS.DATASETS); expect(calls.length).toBeGreaterThan(initialCalls); }); // Verify latest call includes sort parameter - const calls = fetchMock.calls(API_ENDPOINTS.DATASETS); + const calls = fetchMock.callHistory.calls(API_ENDPOINTS.DATASETS); const latestCall = calls[calls.length - 1]; - const url = latestCall[0] as string; + const url = latestCall.url as string; // URL should contain order_column for sorting expect(url).toMatch(/order_column|sort/); @@ -328,17 +309,19 @@ test('sorting by Database column updates sort parameter', async () => { name: /Database/i, }); - const initialCalls = fetchMock.calls(API_ENDPOINTS.DATASETS).length; + const initialCalls = fetchMock.callHistory.calls( + API_ENDPOINTS.DATASETS, + ).length; await userEvent.click(databaseHeader); await waitFor(() => { - const calls = fetchMock.calls(API_ENDPOINTS.DATASETS); + const calls = fetchMock.callHistory.calls(API_ENDPOINTS.DATASETS); expect(calls.length).toBeGreaterThan(initialCalls); }); - const calls = fetchMock.calls(API_ENDPOINTS.DATASETS); - const url = calls[calls.length - 1][0] as string; + const calls = fetchMock.callHistory.calls(API_ENDPOINTS.DATASETS); + const url = calls[calls.length - 1].url as string; expect(url).toMatch(/order_column|sort/); }); @@ -354,28 +337,26 @@ test('sorting by Last modified column updates sort parameter', async () => { name: /Last modified/i, }); - const initialCalls = fetchMock.calls(API_ENDPOINTS.DATASETS).length; + const initialCalls = fetchMock.callHistory.calls( + API_ENDPOINTS.DATASETS, + ).length; await userEvent.click(modifiedHeader); await waitFor(() => { - const calls = fetchMock.calls(API_ENDPOINTS.DATASETS); + const calls = fetchMock.callHistory.calls(API_ENDPOINTS.DATASETS); expect(calls.length).toBeGreaterThan(initialCalls); }); - const calls = fetchMock.calls(API_ENDPOINTS.DATASETS); - const url = calls[calls.length - 1][0] as string; + const calls = fetchMock.callHistory.calls(API_ENDPOINTS.DATASETS); + const url = calls[calls.length - 1].url as string; expect(url).toMatch(/order_column|sort/); }); test('export button triggers handleResourceExport with dataset ID', async () => { const dataset = mockDatasets[0]; - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [dataset], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { result: [dataset], count: 1 }); renderDatasetList(mockAdminUser); @@ -403,11 +384,7 @@ test('delete button opens modal with dataset details', async () => { setupDeleteMocks(dataset.id); - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [dataset], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { result: [dataset], count: 1 }); renderDatasetList(mockAdminUser); @@ -429,11 +406,10 @@ test('delete action successfully deletes dataset and refreshes list', async () = const datasetToDelete = mockDatasets[0]; setupDeleteMocks(datasetToDelete.id); - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [datasetToDelete], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { + result: [datasetToDelete], + count: 1, + }); renderDatasetList(mockAdminUser, { addSuccessToast: mockAddSuccessToast, @@ -455,7 +431,9 @@ test('delete action successfully deletes dataset and refreshes list', async () = await userEvent.type(confirmInput, 'DELETE'); // Track API calls before confirm - const callsBefore = fetchMock.calls(API_ENDPOINTS.DATASETS).length; + const callsBefore = fetchMock.callHistory.calls( + API_ENDPOINTS.DATASETS, + ).length; // Click confirm - find the danger button (last delete button in modal) const confirmButton = within(modal) @@ -465,11 +443,11 @@ test('delete action successfully deletes dataset and refreshes list', async () = // Wait for delete API call await waitFor(() => { - const deleteCalls = fetchMock.calls( + const deleteCalls = fetchMock.callHistory.calls( `glob:*/api/v1/dataset/${datasetToDelete.id}`, ); const hasDelete = deleteCalls.some( - call => (call[1] as RequestInit)?.method === 'DELETE', + call => (call.options as RequestInit)?.method === 'DELETE', ); expect(hasDelete).toBe(true); }); @@ -482,9 +460,9 @@ test('delete action successfully deletes dataset and refreshes list', async () = // List refreshes await waitFor(() => { - expect(fetchMock.calls(API_ENDPOINTS.DATASETS).length).toBeGreaterThan( - callsBefore, - ); + expect( + fetchMock.callHistory.calls(API_ENDPOINTS.DATASETS).length, + ).toBeGreaterThan(callsBefore); }); }); @@ -492,11 +470,7 @@ test('delete action cancel closes modal without deleting', async () => { const dataset = mockDatasets[0]; setupDeleteMocks(dataset.id); - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [dataset], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { result: [dataset], count: 1 }); renderDatasetList(mockAdminUser); @@ -520,9 +494,11 @@ test('delete action cancel closes modal without deleting', async () => { }); // No delete API call made (only related_objects GET was called) - const deleteCalls = fetchMock.calls(`glob:*/api/v1/dataset/${dataset.id}`); + const deleteCalls = fetchMock.callHistory.calls( + `glob:*/api/v1/dataset/${dataset.id}`, + ); const hasDeleteMethod = deleteCalls.some( - call => (call[1] as RequestInit)?.method === 'DELETE', + call => (call.options as RequestInit)?.method === 'DELETE', ); expect(hasDeleteMethod).toBe(false); @@ -534,11 +510,7 @@ test('duplicate action successfully duplicates virtual dataset', async () => { const virtualDataset = mockDatasets[1]; // Virtual dataset (kind: 'virtual') setupDuplicateMocks(); - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [virtualDataset], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { result: [virtualDataset], count: 1 }); renderDatasetList(mockAdminUser, { addSuccessToast: mockAddSuccessToast, @@ -560,7 +532,9 @@ test('duplicate action successfully duplicates virtual dataset', async () => { await userEvent.type(input, 'Copy of Analytics'); // Track API calls before submit - const callsBefore = fetchMock.calls(API_ENDPOINTS.DATASETS).length; + const callsBefore = fetchMock.callHistory.calls( + API_ENDPOINTS.DATASETS, + ).length; // Submit const submitButton = within(modal).getByRole('button', { @@ -570,7 +544,9 @@ test('duplicate action successfully duplicates virtual dataset', async () => { // Wait for duplicate API call and modal closes await waitFor(() => { - const dupCalls = fetchMock.calls(API_ENDPOINTS.DATASET_DUPLICATE); + const dupCalls = fetchMock.callHistory.calls( + API_ENDPOINTS.DATASET_DUPLICATE, + ); expect(dupCalls.length).toBeGreaterThan(0); // Modal closes (duplicate success doesn't show toast, just closes modal) expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); @@ -578,9 +554,9 @@ test('duplicate action successfully duplicates virtual dataset', async () => { // List refreshes await waitFor(() => { - expect(fetchMock.calls(API_ENDPOINTS.DATASETS).length).toBeGreaterThan( - callsBefore, - ); + expect( + fetchMock.callHistory.calls(API_ENDPOINTS.DATASETS).length, + ).toBeGreaterThan(callsBefore); }); }); @@ -588,11 +564,10 @@ test('duplicate button visible only for virtual datasets', async () => { const physicalDataset = mockDatasets[0]; // kind: 'physical' const virtualDataset = mockDatasets[1]; // kind: 'virtual' - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [physicalDataset, virtualDataset], count: 2 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { + result: [physicalDataset, virtualDataset], + count: 2, + }); renderDatasetList(mockAdminUser); @@ -648,11 +623,10 @@ test('bulk select enables checkboxes', async () => { }, 30000); test('selecting all datasets shows correct count in toolbar', async () => { - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: mockDatasets, count: mockDatasets.length }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { + result: mockDatasets, + count: mockDatasets.length, + }); renderDatasetList(mockAdminUser); @@ -688,11 +662,10 @@ test('selecting all datasets shows correct count in toolbar', async () => { }, 30000); test('bulk export triggers export with selected IDs', async () => { - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [mockDatasets[0]], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { + result: [mockDatasets[0]], + count: 1, + }); renderDatasetList(mockAdminUser); @@ -731,11 +704,10 @@ test('bulk export triggers export with selected IDs', async () => { test('bulk delete opens confirmation modal', async () => { setupBulkDeleteMocks(); - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [mockDatasets[0]], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { + result: [mockDatasets[0]], + count: 1, + }); renderDatasetList(mockAdminUser); @@ -838,11 +810,10 @@ test('certified badge appears for certified datasets', async () => { }), }; - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [certifiedDataset], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { + result: [certifiedDataset], + count: 1, + }); renderDatasetList(mockAdminUser); @@ -869,11 +840,10 @@ test('warning icon appears for datasets with warnings', async () => { }), }; - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [datasetWithWarning], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { + result: [datasetWithWarning], + count: 1, + }); renderDatasetList(mockAdminUser); @@ -898,11 +868,10 @@ test('info tooltip appears for datasets with descriptions', async () => { description: 'Sales data from Q4 2024', }; - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [datasetWithDescription], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { + result: [datasetWithDescription], + count: 1, + }); renderDatasetList(mockAdminUser); @@ -924,11 +893,7 @@ test('info tooltip appears for datasets with descriptions', async () => { test('dataset name links to Explore page', async () => { const dataset = mockDatasets[0]; - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [dataset], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { result: [dataset], count: 1 }); renderDatasetList(mockAdminUser); @@ -948,11 +913,10 @@ test('dataset name links to Explore page', async () => { test('physical dataset shows delete, export, and edit actions (no duplicate)', async () => { const physicalDataset = mockDatasets[0]; // kind: 'physical' - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [physicalDataset], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { + result: [physicalDataset], + count: 1, + }); renderDatasetList(mockAdminUser); @@ -980,11 +944,7 @@ test('physical dataset shows delete, export, and edit actions (no duplicate)', a test('virtual dataset shows delete, export, edit, and duplicate actions', async () => { const virtualDataset = mockDatasets[1]; // kind: 'virtual' - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [virtualDataset], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { result: [virtualDataset], count: 1 }); renderDatasetList(mockAdminUser); @@ -1013,11 +973,7 @@ test('edit action is enabled for dataset owner', async () => { owners: [{ id: mockAdminUser.userId, username: 'admin' }], }; - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [dataset], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { result: [dataset], count: 1 }); renderDatasetList(mockAdminUser); @@ -1040,11 +996,7 @@ test('edit action is disabled for non-owner', async () => { owners: [{ id: 999, username: 'other_user' }], // Different user }; - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [dataset], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { result: [dataset], count: 1 }); // Use a non-admin user to test ownership check const regularUser = { @@ -1073,11 +1025,7 @@ test('all action buttons are clickable and enabled for admin user', async () => owners: [{ id: mockAdminUser.userId, username: 'admin' }], }; - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [virtualDataset], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { result: [virtualDataset], count: 1 }); renderDatasetList(mockAdminUser); @@ -1112,11 +1060,10 @@ test('all action buttons are clickable and enabled for admin user', async () => }); test('displays error when initial dataset fetch fails with 500', async () => { - fetchMock.get( - API_ENDPOINTS.DATASETS, - { status: 500, body: { message: 'Internal Server Error' } }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { + status: 500, + body: { message: 'Internal Server Error' }, + }); renderDatasetList(mockAdminUser, { addDangerToast: mockAddDangerToast, @@ -1134,11 +1081,10 @@ test('displays error when initial dataset fetch fails with 500', async () => { }); test('displays error when initial dataset fetch fails with 403 permission denied', async () => { - fetchMock.get( - API_ENDPOINTS.DATASETS, - { status: 403, body: { message: 'Access Denied' } }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { + status: 403, + body: { message: 'Access Denied' }, + }); renderDatasetList(mockAdminUser, { addDangerToast: mockAddDangerToast, @@ -1150,7 +1096,7 @@ test('displays error when initial dataset fetch fails with 403 permission denied }); // Verify toast message contains the 403-specific "Access Denied" text - const toastMessage = String(mockAddDangerToast.mock.calls[0][0]); + const toastMessage = String(mockAddDangerToast.mock.calls[0].url); expect(toastMessage).toContain('Access Denied'); // No dataset names from mockDatasets should appear in the document @@ -1402,20 +1348,22 @@ test('sort order persists after deleting a dataset', async () => { }); // Record initial API calls count - const initialCalls = fetchMock.calls(API_ENDPOINTS.DATASETS).length; + const initialCalls = fetchMock.callHistory.calls( + API_ENDPOINTS.DATASETS, + ).length; // Click Name header to sort await userEvent.click(nameHeader); // Wait for new API call with sort parameter await waitFor(() => { - const calls = fetchMock.calls(API_ENDPOINTS.DATASETS); + const calls = fetchMock.callHistory.calls(API_ENDPOINTS.DATASETS); expect(calls.length).toBeGreaterThan(initialCalls); }); // Record the sort parameter from the API call after sorting - const callsAfterSort = fetchMock.calls(API_ENDPOINTS.DATASETS); - const sortedUrl = callsAfterSort[callsAfterSort.length - 1][0] as string; + const callsAfterSort = fetchMock.callHistory.calls(API_ENDPOINTS.DATASETS); + const sortedUrl = callsAfterSort[callsAfterSort.length - 1].url as string; expect(sortedUrl).toMatch(/order_column|sort/); // Delete a dataset - get delete button from first row only @@ -1433,7 +1381,9 @@ test('sort order persists after deleting a dataset', async () => { await userEvent.type(confirmInput, 'DELETE'); // Record call count before delete to track refetch - const callsBeforeDelete = fetchMock.calls(API_ENDPOINTS.DATASETS).length; + const callsBeforeDelete = fetchMock.callHistory.calls( + API_ENDPOINTS.DATASETS, + ).length; const confirmButton = within(modal) .getAllByRole('button', { name: /^delete$/i }) @@ -1452,7 +1402,9 @@ test('sort order persists after deleting a dataset', async () => { // Wait for list refetch to complete (prevents async cleanup error) await waitFor(() => { - const currentCalls = fetchMock.calls(API_ENDPOINTS.DATASETS).length; + const currentCalls = fetchMock.callHistory.calls( + API_ENDPOINTS.DATASETS, + ).length; expect(currentCalls).toBeGreaterThan(callsBeforeDelete); }); @@ -1476,11 +1428,10 @@ test('sort order persists after deleting a dataset', async () => { // test. Component tests here focus on individual behaviors. test('bulk selection clears when filter changes', async () => { - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: mockDatasets, count: mockDatasets.length }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { + result: mockDatasets, + count: mockDatasets.length, + }); renderDatasetList(mockAdminUser); @@ -1528,7 +1479,9 @@ test('bulk selection clears when filter changes', async () => { }); // Record API call count before filter - const beforeFilterCallCount = fetchMock.calls(API_ENDPOINTS.DATASETS).length; + const beforeFilterCallCount = fetchMock.callHistory.calls( + API_ENDPOINTS.DATASETS, + ).length; // Wait for filter combobox to be ready before applying filter await screen.findByRole('combobox', { name: 'Type' }); @@ -1538,14 +1491,14 @@ test('bulk selection clears when filter changes', async () => { // Wait for filter API call to complete await waitFor(() => { - const calls = fetchMock.calls(API_ENDPOINTS.DATASETS); + const calls = fetchMock.callHistory.calls(API_ENDPOINTS.DATASETS); expect(calls.length).toBeGreaterThan(beforeFilterCallCount); }); // Verify filter was applied by decoding URL payload - const urlAfterFilter = fetchMock + const urlAfterFilter = fetchMock.callHistory .calls(API_ENDPOINTS.DATASETS) - .at(-1)?.[0] as string; + .at(-1)?.url as string; const risonAfterFilter = new URL( urlAfterFilter, 'http://localhost', @@ -1578,19 +1531,22 @@ test('type filter API call includes correct filter parameter', async () => { await screen.findByRole('combobox', { name: 'Type' }); // Snapshot call count before filter - const callsBeforeFilter = fetchMock.calls(API_ENDPOINTS.DATASETS).length; + const callsBeforeFilter = fetchMock.callHistory.calls( + API_ENDPOINTS.DATASETS, + ).length; // Apply Type filter await selectOption('Virtual', 'Type'); // Wait for filter API call to complete await waitFor(() => { - const calls = fetchMock.calls(API_ENDPOINTS.DATASETS); + const calls = fetchMock.callHistory.calls(API_ENDPOINTS.DATASETS); expect(calls.length).toBeGreaterThan(callsBeforeFilter); }); // Verify the latest API call includes the Type filter - const url = fetchMock.calls(API_ENDPOINTS.DATASETS).at(-1)?.[0] as string; + const url = fetchMock.callHistory.calls(API_ENDPOINTS.DATASETS).at(-1) + ?.url as string; expect(url).toContain('filters'); // searchParams.get() already URL-decodes, so pass directly to rison.decode @@ -1622,21 +1578,23 @@ test('type filter persists after duplicating a dataset', async () => { await screen.findByRole('combobox', { name: 'Type' }); // Snapshot call count before filter - const callsBeforeFilter = fetchMock.calls(API_ENDPOINTS.DATASETS).length; + const callsBeforeFilter = fetchMock.callHistory.calls( + API_ENDPOINTS.DATASETS, + ).length; // Apply Type filter await selectOption('Virtual', 'Type'); // Wait for filter API call to complete await waitFor(() => { - const calls = fetchMock.calls(API_ENDPOINTS.DATASETS); + const calls = fetchMock.callHistory.calls(API_ENDPOINTS.DATASETS); expect(calls.length).toBeGreaterThan(callsBeforeFilter); }); // Verify filter is present by checking the latest API call - const urlAfterFilter = fetchMock + const urlAfterFilter = fetchMock.callHistory .calls(API_ENDPOINTS.DATASETS) - .at(-1)?.[0] as string; + .at(-1)?.url as string; const risonAfterFilter = new URL( urlAfterFilter, 'http://localhost', @@ -1654,7 +1612,7 @@ test('type filter persists after duplicating a dataset', async () => { ); // Capture datasets API call count BEFORE any duplicate operations - const datasetsCallCountBeforeDuplicate = fetchMock.calls( + const datasetsCallCountBeforeDuplicate = fetchMock.callHistory.calls( API_ENDPOINTS.DATASETS, ).length; @@ -1682,20 +1640,24 @@ test('type filter persists after duplicating a dataset', async () => { // Wait for duplicate API call to be made await waitFor(() => { - const duplicateCalls = fetchMock.calls(API_ENDPOINTS.DATASET_DUPLICATE); + const duplicateCalls = fetchMock.callHistory.calls( + API_ENDPOINTS.DATASET_DUPLICATE, + ); expect(duplicateCalls.length).toBeGreaterThan(0); }); // Wait for datasets refetch to occur (proves duplicate triggered a refresh) await waitFor(() => { - const datasetsCallCount = fetchMock.calls(API_ENDPOINTS.DATASETS).length; + const datasetsCallCount = fetchMock.callHistory.calls( + API_ENDPOINTS.DATASETS, + ).length; expect(datasetsCallCount).toBeGreaterThan(datasetsCallCountBeforeDuplicate); }); // Verify Type filter persisted in the NEW datasets API call after duplication - const urlAfterDuplicate = fetchMock + const urlAfterDuplicate = fetchMock.callHistory .calls(API_ENDPOINTS.DATASETS) - .at(-1)?.[0] as string; + .at(-1)?.url as string; const risonAfterDuplicate = new URL( urlAfterDuplicate, 'http://localhost', @@ -1729,11 +1691,7 @@ test('edit action shows error toast when dataset fetch fails', async () => { ], }; - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [ownedDataset], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { result: [ownedDataset], count: 1 }); // Mock SupersetClient.get to fail for the specific dataset endpoint jest.spyOn(SupersetClient, 'get').mockImplementation(async request => { @@ -1776,11 +1734,10 @@ test('bulk export error shows toast and clears loading state', async () => { // Mock handleResourceExport to throw an error mockHandleResourceExport.mockRejectedValueOnce(new Error('Export failed')); - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [mockDatasets[0]], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { + result: [mockDatasets[0]], + count: 1, + }); renderDatasetList(mockAdminUser, { addDangerToast: mockAddDangerToast, @@ -1836,17 +1793,15 @@ test('bulk export error shows toast and clears loading state', async () => { test('bulk delete error shows toast without refreshing list', async () => { // Mock bulk delete to fail - fetchMock.delete( - API_ENDPOINTS.DATASET_BULK_DELETE, - { status: 500, body: { message: 'Bulk delete failed' } }, - { overwriteRoutes: true }, - ); + fetchMock.delete(API_ENDPOINTS.DATASET_BULK_DELETE, { + status: 500, + body: { message: 'Bulk delete failed' }, + }); - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [mockDatasets[0]], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { + result: [mockDatasets[0]], + count: 1, + }); renderDatasetList(mockAdminUser, { addDangerToast: mockAddDangerToast, @@ -1919,11 +1874,10 @@ test('bulk select shows "N Selected (Virtual)" for virtual-only selection', asyn // Use only virtual datasets const virtualDatasets = mockDatasets.filter(d => d.kind === 'virtual'); - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: virtualDatasets, count: virtualDatasets.length }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { + result: virtualDatasets, + count: virtualDatasets.length, + }); renderDatasetList(mockAdminUser); @@ -1966,11 +1920,10 @@ test('bulk select shows "N Selected (Physical)" for physical-only selection', as // Use only physical datasets const physicalDatasets = mockDatasets.filter(d => d.kind === 'physical'); - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: physicalDatasets, count: physicalDatasets.length }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { + result: physicalDatasets, + count: physicalDatasets.length, + }); renderDatasetList(mockAdminUser); @@ -2017,11 +1970,10 @@ test('bulk select shows mixed count for virtual and physical selection', async ( mockDatasets.find(d => d.kind === 'virtual')!, ]; - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: mixedDatasets, count: mixedDatasets.length }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { + result: mixedDatasets, + count: mixedDatasets.length, + }); renderDatasetList(mockAdminUser); @@ -2081,20 +2033,12 @@ test('delete modal shows affected dashboards with overflow for >10 items', async title: `Dashboard ${i + 1}`, })); - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [dataset], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { result: [dataset], count: 1 }); - fetchMock.get( - `glob:*/api/v1/dataset/${dataset.id}/related_objects*`, - { - charts: { count: 0, result: [] }, - dashboards: { count: 15, result: manyDashboards }, - }, - { overwriteRoutes: true }, - ); + fetchMock.get(`glob:*/api/v1/dataset/${dataset.id}/related_objects*`, { + charts: { count: 0, result: [] }, + dashboards: { count: 15, result: manyDashboards }, + }); renderDatasetList(mockAdminUser); @@ -2126,20 +2070,12 @@ test('delete modal shows affected dashboards with overflow for >10 items', async test('delete modal hides affected dashboards section when count is zero', async () => { const dataset = mockDatasets[0]; - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [dataset], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { result: [dataset], count: 1 }); - fetchMock.get( - `glob:*/api/v1/dataset/${dataset.id}/related_objects*`, - { - charts: { count: 2, result: [{ id: 1, slice_name: 'Chart 1' }] }, - dashboards: { count: 0, result: [] }, - }, - { overwriteRoutes: true }, - ); + fetchMock.get(`glob:*/api/v1/dataset/${dataset.id}/related_objects*`, { + charts: { count: 2, result: [{ id: 1, slice_name: 'Chart 1' }] }, + dashboards: { count: 0, result: [] }, + }); renderDatasetList(mockAdminUser); @@ -2172,20 +2108,12 @@ test('delete modal shows affected charts with overflow for >10 items', async () slice_name: `Chart ${i + 1}`, })); - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [dataset], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { result: [dataset], count: 1 }); - fetchMock.get( - `glob:*/api/v1/dataset/${dataset.id}/related_objects*`, - { - charts: { count: 12, result: manyCharts }, - dashboards: { count: 0, result: [] }, - }, - { overwriteRoutes: true }, - ); + fetchMock.get(`glob:*/api/v1/dataset/${dataset.id}/related_objects*`, { + charts: { count: 12, result: manyCharts }, + dashboards: { count: 0, result: [] }, + }); renderDatasetList(mockAdminUser); diff --git a/superset-frontend/src/pages/DatasetList/DatasetList.permissions.test.tsx b/superset-frontend/src/pages/DatasetList/DatasetList.permissions.test.tsx index a997ff9a551..331e1c4f9d8 100644 --- a/superset-frontend/src/pages/DatasetList/DatasetList.permissions.test.tsx +++ b/superset-frontend/src/pages/DatasetList/DatasetList.permissions.test.tsx @@ -51,8 +51,8 @@ afterEach(async () => { // Reset browser history to prevent query param leakage window.history.replaceState({}, '', '/'); - fetchMock.resetHistory(); - fetchMock.restore(); + fetchMock.clearHistory(); + fetchMock.removeRoutes(); jest.restoreAllMocks(); }); @@ -240,7 +240,7 @@ test('action buttons respect user permissions', async () => { fetchMock.get( API_ENDPOINTS.DATASETS, { result: [dataset], count: 1 }, - { overwriteRoutes: true }, + ); renderDatasetList(mockAdminUser); @@ -270,7 +270,7 @@ test('read-only user sees no delete or duplicate buttons in row', async () => { fetchMock.get( API_ENDPOINTS.DATASETS, { result: [dataset], count: 1 }, - { overwriteRoutes: true }, + ); renderDatasetList(mockReadOnlyUser); @@ -309,7 +309,7 @@ test('write user sees edit, delete, and export actions', async () => { fetchMock.get( API_ENDPOINTS.DATASETS, { result: [dataset], count: 1 }, - { overwriteRoutes: true }, + ); renderDatasetList(mockWriteUser); @@ -348,7 +348,7 @@ test('export-only user has no Actions column (no write/duplicate permissions)', fetchMock.get( API_ENDPOINTS.DATASETS, { result: [dataset], count: 1 }, - { overwriteRoutes: true }, + ); renderDatasetList(mockExportOnlyUser); @@ -385,7 +385,7 @@ test('user with can_duplicate sees duplicate button only for virtual datasets', fetchMock.get( API_ENDPOINTS.DATASETS, { result: [physicalDataset, virtualDataset], count: 2 }, - { overwriteRoutes: true }, + ); renderDatasetList(mockAdminUser); diff --git a/superset-frontend/src/pages/DatasetList/DatasetList.test.tsx b/superset-frontend/src/pages/DatasetList/DatasetList.test.tsx index 51ad4cbab7a..6e9808762a1 100644 --- a/superset-frontend/src/pages/DatasetList/DatasetList.test.tsx +++ b/superset-frontend/src/pages/DatasetList/DatasetList.test.tsx @@ -53,8 +53,8 @@ afterEach(async () => { // Reset browser history state to prevent query params leaking between tests window.history.replaceState({}, '', '/'); - fetchMock.resetHistory(); - fetchMock.restore(); + fetchMock.clearHistory(); + fetchMock.removeRoutes(); jest.restoreAllMocks(); }); @@ -74,7 +74,6 @@ test('shows loading state during initial data fetch', () => { new Promise(resolve => setTimeout(() => resolve({ result: [], count: 0 }), 10000), ), - { overwriteRoutes: true }, ); renderDatasetList(mockAdminUser); @@ -93,7 +92,6 @@ test('maintains component structure during loading', () => { new Promise(resolve => setTimeout(() => resolve({ result: [], count: 0 }), 10000), ), - { overwriteRoutes: true }, ); renderDatasetList(mockAdminUser); @@ -215,11 +213,10 @@ test('handles datasets with missing fields and renders gracefully', async () => sql: null, }; - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [datasetWithMissingFields], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { + result: [datasetWithMissingFields], + count: 1, + }); renderDatasetList(mockAdminUser); @@ -242,11 +239,7 @@ test('handles datasets with missing fields and renders gracefully', async () => }); test('handles empty results (shows empty state)', async () => { - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [], count: 0 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { result: [], count: 0 }); renderDatasetList(mockAdminUser); @@ -258,7 +251,7 @@ test('makes correct initial API call on load', async () => { renderDatasetList(mockAdminUser); await waitFor(() => { - const calls = fetchMock.calls(API_ENDPOINTS.DATASETS); + const calls = fetchMock.callHistory.calls(API_ENDPOINTS.DATASETS); expect(calls.length).toBeGreaterThan(0); }); }); @@ -267,9 +260,9 @@ test('API call includes correct page size', async () => { renderDatasetList(mockAdminUser); await waitFor(() => { - const calls = fetchMock.calls(API_ENDPOINTS.DATASETS); + const calls = fetchMock.callHistory.calls(API_ENDPOINTS.DATASETS); expect(calls.length).toBeGreaterThan(0); - const url = calls[0][0] as string; + const url = calls[0].url as string; expect(url).toContain('page_size'); }); }); @@ -281,7 +274,9 @@ test('typing in name filter updates input value and triggers API with decoded se const searchInput = within(searchContainer).getByRole('textbox'); // Record initial API calls - const initialCallCount = fetchMock.calls(API_ENDPOINTS.DATASETS).length; + const initialCallCount = fetchMock.callHistory.calls( + API_ENDPOINTS.DATASETS, + ).length; // Type in search box and press Enter to trigger search await userEvent.type(searchInput, 'sales{enter}'); @@ -294,11 +289,11 @@ test('typing in name filter updates input value and triggers API with decoded se // Wait for API call after Enter key press await waitFor( () => { - const calls = fetchMock.calls(API_ENDPOINTS.DATASETS); + const calls = fetchMock.callHistory.calls(API_ENDPOINTS.DATASETS); expect(calls.length).toBeGreaterThan(initialCallCount); // Get latest API call - const url = calls[calls.length - 1][0] as string; + const url = calls[calls.length - 1].url as string; // Verify URL contains search filter expect(url).toContain('filters'); @@ -351,9 +346,7 @@ test('handles 500 error on initial load without crashing', async () => { fetchMock.get( API_ENDPOINTS.DATASETS, { throws: new Error('Internal Server Error') }, - { - overwriteRoutes: true, - }, + {}, ); renderDatasetList(mockAdminUser, { @@ -369,9 +362,7 @@ test('handles 500 error on initial load without crashing', async () => { test('handles 403 error on _info endpoint and disables create actions', async () => { const addDangerToast = jest.fn(); - fetchMock.get(API_ENDPOINTS.DATASETS_INFO, mockApiError403, { - overwriteRoutes: true, - }); + fetchMock.get(API_ENDPOINTS.DATASETS_INFO, mockApiError403, {}); renderDatasetList(mockAdminUser, { addDangerToast, @@ -391,11 +382,9 @@ test('handles 403 error on _info endpoint and disables create actions', async () }); test('handles network timeout without crashing', async () => { - fetchMock.get( - API_ENDPOINTS.DATASETS, - { throws: new Error('Network timeout') }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { + throws: new Error('Network timeout'), + }); renderDatasetList(mockAdminUser, { addDangerToast: jest.fn(), @@ -412,7 +401,7 @@ test('component requires explicit mocks for all API endpoints', async () => { setupMocks(); // Clear call history to start fresh - fetchMock.resetHistory(); + fetchMock.clearHistory(); // Render component with standard setup renderDatasetList(mockAdminUser); @@ -421,15 +410,15 @@ test('component requires explicit mocks for all API endpoints', async () => { await waitForDatasetsPageReady(); // Verify that critical endpoints were called and had mocks available - const newDatasetsCalls = fetchMock.calls(API_ENDPOINTS.DATASETS); - const newInfoCalls = fetchMock.calls(API_ENDPOINTS.DATASETS_INFO); + const newDatasetsCalls = fetchMock.callHistory.calls(API_ENDPOINTS.DATASETS); + const newInfoCalls = fetchMock.callHistory.calls(API_ENDPOINTS.DATASETS_INFO); // These should have been called during render expect(newDatasetsCalls.length).toBeGreaterThan(0); expect(newInfoCalls.length).toBeGreaterThan(0); // Verify no unmatched calls (all endpoints were mocked) - const unmatchedCalls = fetchMock.calls(false); // false = unmatched only + const unmatchedCalls = fetchMock.callHistory.calls(false); // false = unmatched only expect(unmatchedCalls.length).toBe(0); }); @@ -453,11 +442,10 @@ test('renders datasets with certification data', async () => { }), }; - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [certifiedDataset], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { + result: [certifiedDataset], + count: 1, + }); renderDatasetList(mockAdminUser); @@ -481,11 +469,10 @@ test('displays datasets with warning_markdown', async () => { }), }; - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [datasetWithWarning], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { + result: [datasetWithWarning], + count: 1, + }); renderDatasetList(mockAdminUser); @@ -503,11 +490,10 @@ test('displays datasets with warning_markdown', async () => { test('displays dataset with multiple owners', async () => { const datasetWithOwners = mockDatasets[1]; // Has 2 owners: Jane Smith, Bob Jones - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [datasetWithOwners], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { + result: [datasetWithOwners], + count: 1, + }); renderDatasetList(mockAdminUser); @@ -525,11 +511,10 @@ test('displays dataset with multiple owners', async () => { test('displays ModifiedInfo with humanized date', async () => { const datasetWithModified = mockDatasets[0]; // changed_by_name: 'John Doe', changed_on: '1 day ago' - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [datasetWithModified], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { + result: [datasetWithModified], + count: 1, + }); renderDatasetList(mockAdminUser); @@ -548,11 +533,7 @@ test('displays ModifiedInfo with humanized date', async () => { test('dataset name links to Explore with correct explore_url', async () => { const dataset = mockDatasets[0]; // explore_url: '/explore/?datasource=1__table' - fetchMock.get( - API_ENDPOINTS.DATASETS, - { result: [dataset], count: 1 }, - { overwriteRoutes: true }, - ); + fetchMock.get(API_ENDPOINTS.DATASETS, { result: [dataset], count: 1 }); renderDatasetList(mockAdminUser); diff --git a/superset-frontend/src/pages/DatasetList/DatasetList.testHelpers.tsx b/superset-frontend/src/pages/DatasetList/DatasetList.testHelpers.tsx index 844ad8f9163..6b160eac5eb 100644 --- a/superset-frontend/src/pages/DatasetList/DatasetList.testHelpers.tsx +++ b/superset-frontend/src/pages/DatasetList/DatasetList.testHelpers.tsx @@ -23,6 +23,7 @@ import { Provider } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; import { configureStore } from '@reduxjs/toolkit'; import { QueryParamProvider } from 'use-query-params'; +import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5'; import DatasetList from 'src/pages/DatasetList'; import handleResourceExport from 'src/utils/export'; @@ -331,11 +332,8 @@ export const API_ENDPOINTS = { // Setup API permissions mock (for permission-based testing) export const setupApiPermissions = (permissions: string[]) => { - fetchMock.get( - API_ENDPOINTS.DATASETS_INFO, - { permissions }, - { overwriteRoutes: true }, - ); + fetchMock.removeRoute(API_ENDPOINTS.DATASETS_INFO); + fetchMock.get(API_ENDPOINTS.DATASETS_INFO, { permissions }); }; // Store utilities @@ -384,7 +382,7 @@ export const renderDatasetList = ( return render( <Provider store={store}> <MemoryRouter> - <QueryParamProvider> + <QueryParamProvider adapter={ReactRouter5Adapter}> <DatasetList user={user} {...props} /> </QueryParamProvider> </MemoryRouter> @@ -402,36 +400,27 @@ export const waitForDatasetsPageReady = async () => { // Helper functions for specific operations export const setupDeleteMocks = (datasetId: number) => { - fetchMock.get( - `glob:*/api/v1/dataset/${datasetId}/related_objects*`, - { - charts: mockRelatedCharts, - dashboards: mockRelatedDashboards, - }, - { overwriteRoutes: true }, - ); + fetchMock.get(`glob:*/api/v1/dataset/${datasetId}/related_objects*`, { + charts: mockRelatedCharts, + dashboards: mockRelatedDashboards, + }); - fetchMock.delete( - `glob:*/api/v1/dataset/${datasetId}`, - { message: 'Dataset deleted successfully' }, - { overwriteRoutes: true }, - ); + fetchMock.delete(`glob:*/api/v1/dataset/${datasetId}`, { + message: 'Dataset deleted successfully', + }); }; export const setupDuplicateMocks = () => { - fetchMock.post( - API_ENDPOINTS.DATASET_DUPLICATE, - { id: 999, table_name: 'Copy of Dataset' }, - { overwriteRoutes: true }, - ); + fetchMock.post(API_ENDPOINTS.DATASET_DUPLICATE, { + id: 999, + table_name: 'Copy of Dataset', + }); }; export const setupBulkDeleteMocks = () => { - fetchMock.delete( - API_ENDPOINTS.DATASET_BULK_DELETE, - { message: '3 datasets deleted successfully' }, - { overwriteRoutes: true }, - ); + fetchMock.delete(API_ENDPOINTS.DATASET_BULK_DELETE, { + message: '3 datasets deleted successfully', + }); }; // Setup error mocks for negative flow testing @@ -439,25 +428,17 @@ export const setupDeleteErrorMocks = ( datasetId: number, statusCode: number, ) => { - fetchMock.get( - `glob:*/api/v1/dataset/${datasetId}/related_objects*`, - { - status: statusCode, - body: { message: 'Failed to fetch related objects' }, - }, - { overwriteRoutes: true }, - ); + fetchMock.get(`glob:*/api/v1/dataset/${datasetId}/related_objects*`, { + status: statusCode, + body: { message: 'Failed to fetch related objects' }, + }); }; export const setupDuplicateErrorMocks = (statusCode: number) => { - fetchMock.post( - API_ENDPOINTS.DATASET_DUPLICATE, - { - status: statusCode, - body: { message: 'Failed to duplicate dataset' }, - }, - { overwriteRoutes: true }, - ); + fetchMock.post(API_ENDPOINTS.DATASET_DUPLICATE, { + status: statusCode, + body: { message: 'Failed to duplicate dataset' }, + }); }; /** @@ -468,11 +449,13 @@ export const setupDuplicateErrorMocks = (statusCode: number) => { * @throws If any unmocked endpoints were called or expected endpoints weren't called */ export const assertOnlyExpectedCalls = (expectedEndpoints: string[]) => { - const allCalls = fetchMock.calls(true); // Get all calls including unmatched - const unmatchedCalls = allCalls.filter(call => call.isUnmatched); + const allCalls = fetchMock.callHistory.calls(); + const unmatchedCalls = allCalls.filter( + call => call.route?.config?.name === 'unmatched', + ); if (unmatchedCalls.length > 0) { - const unmatchedUrls = unmatchedCalls.map(call => call[0]); + const unmatchedUrls = unmatchedCalls.map(call => call.url); throw new Error( `Unmocked endpoints called: ${unmatchedUrls.join(', ')}. ` + 'Add explicit mocks in setupMocks() or test setup.', @@ -481,7 +464,7 @@ export const assertOnlyExpectedCalls = (expectedEndpoints: string[]) => { // Verify expected endpoints were called expectedEndpoints.forEach(endpoint => { - const calls = fetchMock.calls(endpoint); + const calls = fetchMock.callHistory.calls(endpoint); if (calls.length === 0) { throw new Error( `Expected endpoint not called: ${endpoint}. ` + @@ -493,7 +476,7 @@ export const assertOnlyExpectedCalls = (expectedEndpoints: string[]) => { // MSW setup using fetch-mock (following ChartList pattern) export const setupMocks = () => { - fetchMock.reset(); + fetchMock.removeRoutes(); fetchMock.get(API_ENDPOINTS.DATASETS_INFO, { permissions: ['can_read', 'can_write', 'can_export', 'can_duplicate'],
