test(drill-detail): cover download handlers for CSV/XLSX flows

Adds unit tests for DownloadDropdown menu clicks and integration tests
for DrillDetailPane (asserts SupersetClient.postForm payload: result_type
'drill_detail', ROW_LIMIT, dashboardId) and DrillByModal (asserts
exportChart called with correct resultFormat).

Bumps patch coverage on new download logic from ~45% toward full
coverage of the added code paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
alex-poor
2026-04-15 07:15:08 +12:00
parent d8c8849621
commit 6e1f03b469
3 changed files with 196 additions and 1 deletions

View File

@@ -40,6 +40,19 @@ jest.mock('src/dashboard/util/isEmbedded', () => ({
isEmbedded: jest.fn(() => false),
}));
jest.mock('src/explore/exploreUtils', () => {
const actual = jest.requireActual('src/explore/exploreUtils');
return {
...actual,
exportChart: jest.fn(),
};
});
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { exportChart: exportChartMock } = jest.requireMock(
'src/explore/exploreUtils',
) as { exportChart: jest.Mock };
const CHART_DATA_ENDPOINT = 'glob:*/api/v1/chart/data*';
const FORM_DATA_KEY_ENDPOINT = 'glob:*/api/v1/explore/form_data';
@@ -553,4 +566,64 @@ describe('Table view with pagination', () => {
// Table should still be rendered without crashes
expect(screen.getByTestId('drill-by-results-table')).toBeInTheDocument();
});
test('CSV download calls exportChart with drilledFormData', async () => {
exportChartMock.mockClear();
await renderModal({
column: { column_name: 'state', verbose_name: null },
drillByConfig: {
filters: [{ col: 'gender', op: '==', val: 'boy' }],
groupbyFieldName: 'groupby',
},
});
const tableRadio = await screen.findByRole('radio', { name: /table/i });
userEvent.click(tableRadio);
await waitFor(() =>
expect(screen.getByTestId('drill-by-results-table')).toBeInTheDocument(),
);
await userEvent.click(
await screen.findByRole('button', { name: 'Download' }),
);
await userEvent.click(await screen.findByText('Export to CSV'));
expect(exportChartMock).toHaveBeenCalledTimes(1);
expect(exportChartMock).toHaveBeenCalledWith(
expect.objectContaining({
resultFormat: 'csv',
resultType: 'full',
}),
);
});
test('XLSX download calls exportChart with xlsx format', async () => {
exportChartMock.mockClear();
await renderModal({
column: { column_name: 'state', verbose_name: null },
drillByConfig: {
filters: [{ col: 'gender', op: '==', val: 'boy' }],
groupbyFieldName: 'groupby',
},
});
const tableRadio = await screen.findByRole('radio', { name: /table/i });
userEvent.click(tableRadio);
await waitFor(() =>
expect(screen.getByTestId('drill-by-results-table')).toBeInTheDocument(),
);
await userEvent.click(
await screen.findByRole('button', { name: 'Download' }),
);
await userEvent.click(await screen.findByText('Export to Excel'));
expect(exportChartMock).toHaveBeenCalledTimes(1);
expect(exportChartMock).toHaveBeenCalledWith(
expect.objectContaining({
resultFormat: 'xlsx',
resultType: 'full',
}),
);
});
});

View File

@@ -0,0 +1,57 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { render, screen, userEvent } from 'spec/helpers/testing-library';
import DownloadDropdown from './DownloadDropdown';
const onDownloadCSV = jest.fn();
const onDownloadXLSX = jest.fn();
beforeEach(() => {
onDownloadCSV.mockClear();
onDownloadXLSX.mockClear();
});
const setup = () =>
render(
<DownloadDropdown
onDownloadCSV={onDownloadCSV}
onDownloadXLSX={onDownloadXLSX}
/>,
);
test('renders a download trigger with accessible label', () => {
setup();
expect(screen.getByRole('button', { name: 'Download' })).toBeInTheDocument();
});
test('fires onDownloadCSV when CSV menu item is selected', async () => {
setup();
await userEvent.click(screen.getByRole('button', { name: 'Download' }));
await userEvent.click(await screen.findByText('Export to CSV'));
expect(onDownloadCSV).toHaveBeenCalledTimes(1);
expect(onDownloadXLSX).not.toHaveBeenCalled();
});
test('fires onDownloadXLSX when Excel menu item is selected', async () => {
setup();
await userEvent.click(screen.getByRole('button', { name: 'Download' }));
await userEvent.click(await screen.findByText('Export to Excel'));
expect(onDownloadXLSX).toHaveBeenCalledTimes(1);
expect(onDownloadCSV).not.toHaveBeenCalled();
});

View File

@@ -18,7 +18,12 @@
*/
import fetchMock from 'fetch-mock';
import { QueryFormData, SupersetClient } from '@superset-ui/core';
import { render, screen, waitFor } from 'spec/helpers/testing-library';
import {
render,
screen,
userEvent,
waitFor,
} from 'spec/helpers/testing-library';
import { getMockStoreWithNativeFilters } from 'spec/fixtures/mockStore';
import chartQueries, { sliceId } from 'spec/fixtures/mockChartQueries';
import { supersetGetCache } from 'src/utils/cachedSupersetGet';
@@ -189,6 +194,66 @@ test('should render the error', async () => {
expect(screen.getByText('Error: Something went wrong')).toBeInTheDocument();
});
describe('download actions', () => {
const renderWithDownloadPermission = () =>
render(
<DrillDetailPane
initialFilters={[]}
formData={chart.form_data as unknown as QueryFormData}
/>,
{
useRedux: true,
initialState: {
user: { roles: { Admin: [['can_csv', 'Superset']] } },
common: { conf: { SAMPLES_ROW_LIMIT: 10, ROW_LIMIT: 50000 } },
dashboardInfo: { id: 123 },
},
},
);
const clickDownloadItem = async (label: string) => {
await userEvent.click(
await screen.findByRole('button', { name: 'Download' }),
);
await userEvent.click(await screen.findByText(label));
};
test('CSV export posts drill_detail payload with ROW_LIMIT', async () => {
fetchWithData();
const postFormSpy = jest
.spyOn(SupersetClient, 'postForm')
.mockImplementation(() => Promise.resolve());
renderWithDownloadPermission();
await clickDownloadItem('Export to CSV');
expect(postFormSpy).toHaveBeenCalledTimes(1);
const body = postFormSpy.mock.calls[0][1] as { form_data: string };
const payload = JSON.parse(body.form_data);
expect(payload.result_type).toBe('drill_detail');
expect(payload.result_format).toBe('csv');
expect(payload.queries[0].row_limit).toBe(50000);
expect(payload.form_data.dashboardId).toBe(123);
postFormSpy.mockRestore();
});
test('XLSX export uses xlsx result_format', async () => {
fetchWithData();
const postFormSpy = jest
.spyOn(SupersetClient, 'postForm')
.mockImplementation(() => Promise.resolve());
renderWithDownloadPermission();
await clickDownloadItem('Export to Excel');
expect(postFormSpy).toHaveBeenCalledTimes(1);
const body = postFormSpy.mock.calls[0][1] as { form_data: string };
const payload = JSON.parse(body.form_data);
expect(payload.result_format).toBe('xlsx');
postFormSpy.mockRestore();
});
});
test('should use verbose_map for column headers when available', async () => {
jest.restoreAllMocks();