test(DatasourceControl): mock DatasourceEditor to fix CI OOM crashes

DatasourceControl.test.tsx consistently OOM-crashes Jest workers in CI
(shard 7) because the last 4 tests render the full DatasourceEditor
(2,500+ lines, 150+ imports) without mocking. Each test mounts this
massive tree, compounding memory until crash.

Mock DatasourceEditor with a lightweight stub since these tests only
verify DatasourceControl's callback wiring through the modal save flow,
not editor internals. Also remove stale SupersetClientGet.mockImplementationOnce
calls from 2 earlier tests that leaked unconsumed mocks into subsequent
tests (jest.clearAllMocks doesn't clear mock implementations).

Results: heap 615MB→501MB, test time 33s→20s, heavy tests 5500ms→118ms.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Evan Rusackas
2026-03-02 12:08:36 -08:00
parent 52758b36ff
commit db03fb10e6

View File

@@ -32,6 +32,20 @@ import { fallbackExploreInitialData } from 'src/explore/fixtures';
import type { ColumnObject } from 'src/features/datasets/types';
import DatasourceControl from '.';
// Mock DatasourceEditor to avoid mounting the full 2500+ line editor tree.
// The heavy editor (with 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.
jest.mock('src/components/Datasource/components/DatasourceEditor', () => ({
__esModule: true,
default: () =>
require('react').createElement(
'div',
{ 'data-test': 'mock-datasource-editor' },
'Mock Editor',
),
}));
const SupersetClientGet = jest.spyOn(SupersetClient, 'get');
let originalLocation: Location;
@@ -263,9 +277,6 @@ 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} />, {
@@ -278,20 +289,13 @@ test('Click on Edit dataset', async () => {
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.',
),
).toBeInTheDocument();
expect(screen.getByTestId('mock-datasource-editor')).toBeInTheDocument();
});
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,
@@ -571,129 +575,42 @@ test('should show forbidden dataset state', () => {
});
test('should allow creating new metrics in dataset editor', async () => {
const newMetricName = `test_metric_${Date.now()}`;
const mockDatasourceWithMetrics = {
...mockDatasource,
metrics: [],
};
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();
// The GET response after save includes the new metric
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();
});
});
test('should allow deleting metrics in dataset editor', async () => {
const existingMetricName = 'existing_metric';
const mockDatasourceWithMetrics = {
...mockDatasource,
metrics: [{ id: 1, metric_name: existingMetricName }],
};
const props = createProps({
datasource: mockDatasourceWithMetrics,
datasource: {
...mockDatasource,
metrics: [{ id: 1, metric_name: 'existing_metric' }],
},
});
// 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: [] },
});
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'));
// The GET response after save reflects the metric was deleted
await openAndSaveChanges({ ...mockDatasource, metrics: [] });
// 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'));
// 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();
});
@@ -702,95 +619,66 @@ test('should allow deleting metrics in dataset editor', async () => {
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,
});
// Set up fetch mocks for the save flow
fetchMock.removeRoute(getDbWithQuery);
fetchMock.get(getDbWithQuery, { result: [] }, { name: getDbWithQuery });
fetchMock.removeRoute(putDatasetWithAllMockRouteName);
fetchMock.put(
putDatasetWithAll,
{},
{ name: putDatasetWithAllMockRouteName },
);
fetchMock.removeRoute(getDatasetWithAllMockRouteName);
fetchMock.get(
getDatasetWithAll,
{ result: mockDatasource },
{ name: getDatasetWithAllMockRouteName },
);
// 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);
// Click save to trigger confirmation modal
await userEvent.click(await screen.findByTestId('datasource-modal-save'));
// Verify confirmation modal appears
await waitFor(() => {
expect(screen.getByText('OK')).toBeInTheDocument();
});
expect(await screen.findByText('OK')).toBeInTheDocument();
// Click OK to confirm
// Confirm save
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 verify DatasourceControl callback fires 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();
// Open dataset editor
await userEvent.click(screen.getByTestId('datasource-menu-trigger'));
await userEvent.click(await screen.findByTestId('edit-dataset'));
await openAndSaveChanges(mockDatasource);
// 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();
});
// Verify it was called with a datasource object
// Verify callback received a datasource object
expect(mockOnDatasourceSave).toHaveBeenCalledWith(
expect.objectContaining({
id: expect.any(Number),