This is an automated email from the ASF dual-hosted git repository. jli pushed a commit to branch fix-master-ci in repository https://gitbox.apache.org/repos/asf/superset.git
commit fe5f0b007b5e90d2030619f0f511bbe108ad5cd1 Author: Joe Li <[email protected]> AuthorDate: Wed Mar 4 16:12:55 2026 -0800 test(DatasourceControl): mock DatasourceEditor to fix CI OOM crashes DatasourceControl.test.tsx consistently OOM-crashes Jest workers in CI 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 (covered by DatasourceEditor.test.tsx). Also scope the SupersetClient.get spy into its single consuming test with explicit restore to eliminate cross-test mock leaks, fix missing await on userEvent calls, and rename tests to reflect their actual scope. Results: heap 615MB→287MB, heavy tests 5500ms→350ms each. Co-Authored-By: Claude Opus 4.6 <[email protected]> --- .../DatasourceControl/DatasourceControl.test.tsx | 257 +++++---------------- 1 file changed, 53 insertions(+), 204 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 b51e87fb2b..f5dfca9739 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; @@ -43,7 +55,7 @@ beforeEach(() => { afterEach(() => { window.location = originalLocation; fetchMock.clearHistory().removeRoutes(); - jest.clearAllMocks(); // Clears mock history but keeps spy in place + jest.restoreAllMocks(); }); interface TestDatasource { @@ -234,16 +246,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 +263,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,35 +274,23 @@ 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.', - ), - ).toBeInTheDocument(); + expect(await screen.findByTestId('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, @@ -330,9 +329,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 +567,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, - }); - - // 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 }], - }, + datasource: { ...mockDatasource, metrics: [] }, }); - 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
