diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx index 0f840d87430..79c41f30aa1 100644 --- a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx +++ b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx @@ -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', + }), + ); + }); }); diff --git a/superset-frontend/src/components/Chart/DrillDetail/DownloadDropdown.test.tsx b/superset-frontend/src/components/Chart/DrillDetail/DownloadDropdown.test.tsx new file mode 100644 index 00000000000..f09c364dcbc --- /dev/null +++ b/superset-frontend/src/components/Chart/DrillDetail/DownloadDropdown.test.tsx @@ -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( + , + ); + +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(); +}); diff --git a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.test.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.test.tsx index 030552823ec..f9d9b924878 100644 --- a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.test.tsx +++ b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.test.tsx @@ -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( + , + { + 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();