mirror of
https://github.com/apache/superset.git
synced 2026-04-18 15:44:57 +00:00
test: fix CI OOM crashes in DatasourceControl test and flaky FileHandleer test (#38430)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
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'));
|
||||
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();
|
||||
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'));
|
||||
await openAndSaveChanges(mockDatasource);
|
||||
|
||||
// Save without making changes
|
||||
const saveButton = await screen.findByTestId('datasource-modal-save');
|
||||
await userEvent.click(saveButton);
|
||||
|
||||
// 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();
|
||||
expect(mockOnDatasourceSave).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
name: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// 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
|
||||
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
|
||||
|
||||
@@ -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/');
|
||||
|
||||
Reference in New Issue
Block a user