This is an automated email from the ASF dual-hosted git repository.

rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 5f0efd2be9d test: fix CI OOM crashes in DatasourceControl test and 
flaky FileHandleer test (#38430)
5f0efd2be9d is described below

commit 5f0efd2be9df7e1ffef967efd17a94c34289a1ce
Author: Joe Li <[email protected]>
AuthorDate: Thu Mar 5 12:05:58 2026 -0800

    test: fix CI OOM crashes in DatasourceControl test and flaky FileHandleer 
test (#38430)
    
    Co-authored-by: Claude Opus 4.6 <[email protected]>
---
 .../DatasourceControl/DatasourceControl.test.tsx   | 268 +++++----------------
 .../src/pages/FileHandler/index.test.tsx           |  22 +-
 2 files changed, 83 insertions(+), 207 deletions(-)

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 b51e87fb2bc..a589ab83fc1 100644
--- 
a/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.tsx
+++ 
b/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.tsx
@@ -24,7 +24,6 @@ import { DatasourceType, JsonObject, SupersetClient } from 
'@superset-ui/core';
 import {
   render,
   screen,
-  act,
   userEvent,
   waitFor,
 } from 'spec/helpers/testing-library';
@@ -32,7 +31,20 @@ import { fallbackExploreInitialData } from 
'src/explore/fixtures';
 import type { ColumnObject } from 'src/features/datasets/types';
 import DatasourceControl from '.';
 
-const SupersetClientGet = jest.spyOn(SupersetClient, 'get');
+// Mock DatasourceEditor to avoid mounting the full 2,500+ line editor tree.
+// The heavy editor (CollectionTable, FilterableTable, DatabaseSelector, etc.)
+// causes OOM in CI when rendered repeatedly. These tests only need to verify
+// DatasourceControl's callback wiring through the modal save flow.
+// Editor internals are tested in DatasourceEditor.test.tsx.
+jest.mock('src/components/Datasource/components/DatasourceEditor', () => ({
+  __esModule: true,
+  default: () =>
+    require('react').createElement(
+      'div',
+      { 'data-test': 'mock-datasource-editor' },
+      'Mock Editor',
+    ),
+}));
 
 let originalLocation: Location;
 
@@ -42,8 +54,19 @@ beforeEach(() => {
 
 afterEach(() => {
   window.location = originalLocation;
-  fetchMock.clearHistory().removeRoutes();
-  jest.clearAllMocks(); // Clears mock history but keeps spy in place
+
+  try {
+    const unmatched = fetchMock.callHistory.calls('unmatched');
+    if (unmatched.length > 0) {
+      const urls = unmatched.map(call => call.url).join(', ');
+      throw new Error(
+        `fetchMock: ${unmatched.length} unmatched call(s): ${urls}`,
+      );
+    }
+  } finally {
+    fetchMock.clearHistory().removeRoutes();
+    jest.restoreAllMocks();
+  }
 });
 
 interface TestDatasource {
@@ -234,16 +257,16 @@ test('Should show SQL Lab for sql_lab role', async () => {
 
 test('Click on Swap dataset option', async () => {
   const props = createProps();
-  SupersetClientGet.mockImplementationOnce(
-    async ({ endpoint }: { endpoint: string }) => {
+  jest
+    .spyOn(SupersetClient, 'get')
+    .mockImplementation(async ({ endpoint }: { endpoint: string }) => {
       if (endpoint.includes('_info')) {
         return {
           json: { permissions: ['can_read', 'can_write'] },
         } as any;
       }
       return { json: { result: [] } } as any;
-    },
-  );
+    });
 
   render(<DatasourceControl {...props} />, {
     useRedux: true,
@@ -251,9 +274,8 @@ test('Click on Swap dataset option', async () => {
   });
   await userEvent.click(screen.getByTestId('datasource-menu-trigger'));
 
-  await act(async () => {
-    await userEvent.click(screen.getByText('Swap dataset'));
-  });
+  await userEvent.click(screen.getByText('Swap dataset'));
+
   expect(
     screen.getByText(
       'Changing the dataset may break the chart if the chart relies on columns 
or metadata that does not exist in the target dataset',
@@ -263,25 +285,18 @@ test('Click on Swap dataset option', async () => {
 
 test('Click on Edit dataset', async () => {
   const props = createProps();
-  SupersetClientGet.mockImplementationOnce(
-    async () => ({ json: { result: [] } }) as any,
-  );
   fetchMock.removeRoute(getDbWithQuery);
   fetchMock.get(getDbWithQuery, { result: [] }, { name: getDbWithQuery });
   render(<DatasourceControl {...props} />, {
     useRedux: true,
     useRouter: true,
   });
-  userEvent.click(screen.getByTestId('datasource-menu-trigger'));
+  await userEvent.click(screen.getByTestId('datasource-menu-trigger'));
 
-  await act(async () => {
-    userEvent.click(screen.getByText('Edit dataset'));
-  });
+  await userEvent.click(screen.getByText('Edit dataset'));
 
   expect(
-    screen.getByText(
-      'Changing these settings will affect all charts using this dataset, 
including charts owned by other people.',
-    ),
+    await screen.findByTestId('mock-datasource-editor'),
   ).toBeInTheDocument();
 });
 
@@ -289,9 +304,6 @@ test('Edit dataset should be disabled when user is not 
admin', async () => {
   const props = createProps();
   props.user.roles = {};
   props.datasource.owners = [];
-  SupersetClientGet.mockImplementationOnce(
-    async () => ({ json: { result: [] } }) as any,
-  );
 
   render(<DatasourceControl {...props} />, {
     useRedux: true,
@@ -330,9 +342,7 @@ test('Click on View in SQL Lab', async () => {
 
   expect(queryByTestId('mock-sqllab-route')).not.toBeInTheDocument();
 
-  await act(async () => {
-    await userEvent.click(screen.getByText('View in SQL Lab'));
-  });
+  await userEvent.click(screen.getByText('View in SQL Lab'));
 
   expect(getByTestId('mock-sqllab-route')).toBeInTheDocument();
   
expect(JSON.parse(`${getByTestId('mock-sqllab-route').textContent}`)).toEqual(
@@ -570,235 +580,87 @@ test('should show forbidden dataset state', () => {
   expect(screen.getByText(error.statusText)).toBeVisible();
 });
 
-test('should allow creating new metrics in dataset editor', async () => {
-  const newMetricName = `test_metric_${Date.now()}`;
-  const mockDatasourceWithMetrics = {
-    ...mockDatasource,
-    metrics: [],
-  };
-
+test('should fire onDatasourceSave when saving with new metrics', async () => {
   const props = createProps({
-    datasource: mockDatasourceWithMetrics,
+    datasource: { ...mockDatasource, metrics: [] },
   });
 
-  // Mock API calls for dataset editor
-  fetchMock.get(getDbWithQuery, { response: { result: [] } });
-
-  fetchMock.get(getDatasetWithAll, { result: mockDatasourceWithMetrics });
-
-  fetchMock.put(putDatasetWithAll, {
-    result: {
-      ...mockDatasourceWithMetrics,
-      metrics: [{ id: 1, metric_name: newMetricName }],
-    },
-  });
-
-  SupersetClientGet.mockImplementationOnce(
-    async () => ({ json: { result: [] } }) as any,
-  );
-
   render(<DatasourceControl {...props} />, {
     useRedux: true,
     useRouter: true,
   });
 
-  // Open datasource menu and click 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 openAndSaveChanges({
+    ...mockDatasource,
+    metrics: [{ id: 1, metric_name: 'test_metric' }],
   });
 
-  userEvent.click(screen.getByText('Metrics'));
-
-  // Click add new metric button
-  const addButton = await screen.findByTestId('crud-add-table-item');
-  userEvent.click(addButton);
-
-  // Find and fill in the metric name
-  const nameInput = await screen.findByTestId('textarea-editable-title-input');
-  userEvent.clear(nameInput);
-  userEvent.type(nameInput, newMetricName);
-
-  // Save the modal
-  userEvent.click(screen.getByTestId('datasource-modal-save'));
-
-  // Confirm the save
-  const okButton = await screen.findByText('OK');
-  userEvent.click(okButton);
-
-  // Verify the onDatasourceSave callback was called
   await waitFor(() => {
-    expect(props.onDatasourceSave).toHaveBeenCalled();
+    expect(props.onDatasourceSave).toHaveBeenCalledWith(
+      expect.objectContaining({
+        metrics: [{ id: 1, metric_name: 'test_metric' }],
+      }),
+    );
   });
 });
 
-test('should allow deleting metrics in dataset editor', async () => {
-  const existingMetricName = 'existing_metric';
-  const mockDatasourceWithMetrics = {
-    ...mockDatasource,
-    metrics: [{ id: 1, metric_name: existingMetricName }],
-  };
-
+test('should fire onDatasourceSave when saving with removed metrics', async () 
=> {
   const props = createProps({
-    datasource: mockDatasourceWithMetrics,
-  });
-
-  // Mock API calls
-  fetchMock.get('glob:*/api/v1/database/?q=*', { result: [] });
-
-  fetchMock.get('glob:*/api/v1/dataset/*', {
-    result: mockDatasourceWithMetrics,
-  });
-
-  fetchMock.put('glob:*/api/v1/dataset/*', {
-    result: { ...mockDatasourceWithMetrics, metrics: [] },
+    datasource: {
+      ...mockDatasource,
+      metrics: [{ id: 1, metric_name: 'existing_metric' }],
+    },
   });
 
-  SupersetClientGet.mockImplementationOnce(
-    async () => ({ json: { result: [] } }) as any,
-  );
-
   render(<DatasourceControl {...props} />, {
     useRedux: true,
     useRouter: true,
   });
 
-  // Open edit dataset modal
-  await userEvent.click(screen.getByTestId('datasource-menu-trigger'));
-  await userEvent.click(await screen.findByTestId('edit-dataset'));
-
-  // Navigate to Metrics tab
-  await waitFor(() => {
-    expect(screen.getByText('Metrics')).toBeInTheDocument();
-  });
-  await userEvent.click(screen.getByText('Metrics'));
-
-  // Find existing metric and delete it
-  const metricRow = (await 
screen.findByText(existingMetricName)).closest('tr');
-  expect(metricRow).toBeInTheDocument();
-
-  const deleteButton = metricRow?.querySelector(
-    '[data-test="crud-delete-icon"]',
-  );
-  expect(deleteButton).toBeInTheDocument();
-  await userEvent.click(deleteButton!);
-
-  // Save the changes
-  await userEvent.click(screen.getByTestId('datasource-modal-save'));
+  await openAndSaveChanges({ ...mockDatasource, metrics: [] });
 
-  // Confirm the save
-  const okButton = await screen.findByText('OK');
-  await userEvent.click(okButton);
-
-  // Verify the onDatasourceSave callback was called
   await waitFor(() => {
-    expect(props.onDatasourceSave).toHaveBeenCalled();
+    expect(props.onDatasourceSave).toHaveBeenCalledWith(
+      expect.objectContaining({ metrics: [] }),
+    );
   });
 });
 
 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: [] });
-
-  fetchMock.get('glob:*/api/v1/dataset/*', { result: mockDatasource });
-
-  fetchMock.put('glob:*/api/v1/dataset/*', { result: mockDatasource });
-
-  SupersetClientGet.mockImplementationOnce(
-    async () => ({ json: { result: [] } }) as any,
-  );
-
   render(<DatasourceControl {...props} />, {
     useRedux: true,
     useRouter: true,
   });
 
-  // Open edit dataset modal
-  await userEvent.click(screen.getByTestId('datasource-menu-trigger'));
-  await userEvent.click(await screen.findByTestId('edit-dataset'));
-
-  // Save without making changes
-  const saveButton = await screen.findByTestId('datasource-modal-save');
-  await userEvent.click(saveButton);
+  await openAndSaveChanges(mockDatasource);
 
-  // Verify confirmation modal appears
-  await waitFor(() => {
-    expect(screen.getByText('OK')).toBeInTheDocument();
-  });
-
-  // Click OK to confirm
-  await userEvent.click(screen.getByText('OK'));
-
-  // Verify the save was processed
   await waitFor(() => {
     expect(props.onDatasourceSave).toHaveBeenCalled();
   });
 });
 
-test('should verify real DatasourceControl callback fires on save', async () 
=> {
-  // This test verifies that the REAL DatasourceControl component calls 
onDatasourceSave
-  // This is simpler than the full metric creation flow but tests the key 
integration
-
+test('should fire onDatasourceSave callback on save', async () => {
   const mockOnDatasourceSave = jest.fn();
   const props = createProps({
     datasource: mockDatasource,
     onDatasourceSave: mockOnDatasourceSave,
   });
 
-  // Mock API calls with the same datasource (no changes needed for this test)
-  fetchMock.get('glob:*/api/v1/database/?q=*', { result: [] });
-
-  fetchMock.get('glob:*/api/v1/dataset/*', { result: mockDatasource });
-
-  fetchMock.put('glob:*/api/v1/dataset/*', { result: mockDatasource });
-
-  SupersetClientGet.mockImplementationOnce(
-    async () => ({ json: { result: [] } }) as any,
-  );
-
-  // Render the REAL DatasourceControl component
   render(<DatasourceControl {...props} />, {
     useRedux: true,
     useRouter: true,
   });
 
-  // Verify the real component rendered
-  expect(screen.getByTestId('datasource-control')).toBeInTheDocument();
+  await openAndSaveChanges(mockDatasource);
 
-  // Open dataset editor
-  await userEvent.click(screen.getByTestId('datasource-menu-trigger'));
-  await userEvent.click(await screen.findByTestId('edit-dataset'));
-
-  // Wait for modal to open
-  await waitFor(() => {
-    expect(screen.getByText('Columns')).toBeInTheDocument();
-  });
-
-  // Save without making changes (this should still trigger the callback)
-  await userEvent.click(screen.getByTestId('datasource-modal-save'));
-  const okButton = await screen.findByText('OK');
-  await userEvent.click(okButton);
-
-  // Verify the REAL component called the callback
-  // This tests that the integration point works (regardless of what data is 
passed)
   await waitFor(() => {
-    expect(mockOnDatasourceSave).toHaveBeenCalled();
+    expect(mockOnDatasourceSave).toHaveBeenCalledWith(
+      expect.objectContaining({
+        id: expect.any(Number),
+        name: expect.any(String),
+      }),
+    );
   });
-
-  // Verify it was called with a datasource object
-  expect(mockOnDatasourceSave).toHaveBeenCalledWith(
-    expect.objectContaining({
-      id: expect.any(Number),
-      name: expect.any(String),
-    }),
-  );
 });
-
-// Note: Cross-component integration test removed due to complex Redux/user 
context setup
-// The existing callback tests provide sufficient coverage for metric creation 
workflows
-// Future enhancement could add MetricsControl integration when test 
infrastructure supports it
diff --git a/superset-frontend/src/pages/FileHandler/index.test.tsx 
b/superset-frontend/src/pages/FileHandler/index.test.tsx
index aa0c9d9bab1..3f7f15471f3 100644
--- a/superset-frontend/src/pages/FileHandler/index.test.tsx
+++ b/superset-frontend/src/pages/FileHandler/index.test.tsx
@@ -17,7 +17,12 @@
  * under the License.
  */
 import { ComponentType } from 'react';
-import { render, screen, waitFor } from 'spec/helpers/testing-library';
+import {
+  render,
+  screen,
+  userEvent,
+  waitFor,
+} from 'spec/helpers/testing-library';
 import { MemoryRouter, Route } from 'react-router-dom';
 import FileHandler from './index';
 
@@ -108,6 +113,8 @@ type LaunchQueue = {
   ) => void;
 };
 
+const pendingTimerIds = new Set<ReturnType<typeof setTimeout>>();
+
 const setupLaunchQueue = (fileHandle: MockFileHandle | null = null) => {
   let savedConsumer:
     | ((params: { files?: MockFileHandle[] }) => void | Promise<void>)
@@ -116,11 +123,13 @@ const setupLaunchQueue = (fileHandle: MockFileHandle | 
null = null) => {
     setConsumer: (consumer: (params: { files?: MockFileHandle[] }) => void) => 
{
       savedConsumer = consumer;
       if (fileHandle) {
-        setTimeout(() => {
+        const id = setTimeout(() => {
+          pendingTimerIds.delete(id);
           consumer({
             files: [fileHandle],
           });
         }, 0);
+        pendingTimerIds.add(id);
       }
     },
   };
@@ -136,6 +145,12 @@ beforeEach(() => {
   delete (window as any).launchQueue;
 });
 
+afterEach(() => {
+  pendingTimerIds.forEach(id => clearTimeout(id));
+  pendingTimerIds.clear();
+  delete (window as any).launchQueue;
+});
+
 test('shows error when launchQueue is not supported', async () => {
   render(
     <MemoryRouter initialEntries={['/superset/file-handler']}>
@@ -345,8 +360,7 @@ test('modal close redirects to welcome page', async () => {
   expect(modal).toBeInTheDocument();
 
   // Click the close button in the mocked modal
-  const closeButton = screen.getByRole('button', { name: 'Close' });
-  closeButton.click();
+  await userEvent.click(screen.getByRole('button', { name: 'Close' }));
 
   await waitFor(() => {
     expect(mockHistoryPush).toHaveBeenCalledWith('/superset/welcome/');

Reply via email to