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/');