Files
superset2/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/useExploreAdditionalActionsMenu.test.tsx
T
2026-08-04 12:37:46 -07:00

320 lines
9.6 KiB
TypeScript

/**
* 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 { ComponentType } from 'react';
import { FeatureFlag } from '@superset-ui/core';
import { render, screen, waitFor } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import downloadAsImage from 'src/utils/downloadAsImage';
import downloadAsPdf from 'src/utils/downloadAsPdf';
import {
useExploreAdditionalActionsMenu,
getExportScreenshotMenuItems,
} from './index';
import * as exploreUtils from 'src/explore/exploreUtils';
jest.mock('src/explore/exploreUtils', () => ({
__esModule: true,
...jest.requireActual('src/explore/exploreUtils'),
exportChart: jest.fn(),
getChartKey: jest.fn(() => 'test_chart_key'),
}));
jest.mock('src/utils/downloadAsImage', () => ({
__esModule: true,
default: jest.fn(() => jest.fn()),
}));
jest.mock('src/utils/downloadAsPdf', () => ({
__esModule: true,
default: jest.fn(() => jest.fn()),
}));
const mockDownloadAsImage = downloadAsImage as jest.MockedFunction<
typeof downloadAsImage
>;
const mockDownloadAsPdf = downloadAsPdf as jest.MockedFunction<
typeof downloadAsPdf
>;
const mockExportChart = exploreUtils.exportChart as jest.Mock;
const mockAddDangerToast = jest.fn();
jest.mock('src/components/MessageToasts/withToasts', () => ({
__esModule: true,
default: (component: ComponentType) => component,
useToasts: () => ({
addDangerToast: mockAddDangerToast,
addSuccessToast: jest.fn(),
}),
}));
jest.mock('src/logger/actions', () => ({
logEvent: jest.fn(() => ({ type: 'LOG_EVENT' })),
}));
jest.mock('@superset-ui/core', () => ({
...jest.requireActual('@superset-ui/core'),
getChartMetadataRegistry: jest.fn(() => ({
get: jest.fn(() => ({ behaviors: ['EXPORT_CURRENT_VIEW'] })),
})),
}));
const defaultProps = {
latestQueryFormData: {
datasource: '1__table',
viz_type: 'pivot_table_v2',
},
canDownloadCSV: true,
slice: { slice_id: 1, slice_name: 'Test Chart' },
ownState: {},
dashboards: [],
onOpenInEditor: jest.fn(),
onOpenPropertiesModal: jest.fn(),
showReportModal: jest.fn(),
setCurrentReportDeleting: jest.fn(),
};
type TestComponentProps = typeof defaultProps;
type HookParams = Parameters<typeof useExploreAdditionalActionsMenu>;
const TestComponent = (props: TestComponentProps) => {
const [menu] = useExploreAdditionalActionsMenu(
props.latestQueryFormData as HookParams[0],
props.canDownloadCSV,
props.slice as HookParams[2],
props.onOpenInEditor,
props.onOpenPropertiesModal,
props.ownState as HookParams[5],
props.dashboards as HookParams[6],
props.showReportModal,
props.setCurrentReportDeleting,
);
return <div>{menu}</div>;
};
beforeEach(() => {
jest.clearAllMocks();
mockExportChart.mockResolvedValue(undefined);
});
test('shows 413 error toast when exportCSV fails with 413', async () => {
mockExportChart.mockRejectedValue({ status: 413 });
render(<TestComponent {...defaultProps} />, { useRedux: true });
userEvent.hover(await screen.findByText('Data Export Options'));
userEvent.hover(await screen.findByText('Export All Data'));
userEvent.click(await screen.findByText('Export to original .CSV'));
await waitFor(() => {
expect(mockAddDangerToast).toHaveBeenCalledWith(
expect.stringMatching(/The chart data is too large to download/),
);
});
});
test('shows 413 error toast when exportCSVPivoted fails with 413', async () => {
mockExportChart.mockRejectedValue({ status: 413 });
render(<TestComponent {...defaultProps} />, { useRedux: true });
userEvent.hover(await screen.findByText('Data Export Options'));
userEvent.hover(await screen.findByText('Export All Data'));
userEvent.click(await screen.findByText('Export to pivoted .CSV'));
await waitFor(() => {
expect(mockAddDangerToast).toHaveBeenCalledWith(
expect.stringMatching(/The chart data is too large to download/),
);
});
});
test('shows 413 error toast when Export Current View CSV server path fails with 413', async () => {
mockExportChart.mockRejectedValue({ status: 413 });
render(
<TestComponent
{...defaultProps}
latestQueryFormData={{
datasource: '1__table',
viz_type: 'table',
}}
ownState={{}}
/>,
{ useRedux: true },
);
userEvent.hover(await screen.findByText('Data Export Options'));
userEvent.hover(await screen.findByText('Export Current View'));
userEvent.click(await screen.findByText('Export to .CSV'));
await waitFor(() => {
expect(mockAddDangerToast).toHaveBeenCalledWith(
expect.stringMatching(/The chart data is too large to download/),
);
});
});
const CHART_SELECTOR = '.panel-body .chart-container';
const SLICE_NAME = 'My chart';
const CHART_ID = 42;
const domEvent = {} as React.MouseEvent;
const buildScreenshotItems = () => {
const setIsDropdownVisible = jest.fn();
const dispatch = jest.fn();
const items = getExportScreenshotMenuItems({
chartSelector: CHART_SELECTOR,
sliceName: SLICE_NAME,
chartId: CHART_ID,
theme: {} as any,
setIsDropdownVisible,
dispatch,
submenuKey: 'export_png_submenu',
transparentKey: 'export_png_transparent',
solidKey: 'export_png_solid',
pdfKey: 'export_pdf',
}) as any[];
return { items, setIsDropdownVisible, dispatch };
};
test('getExportScreenshotMenuItems builds the PNG submenu and PDF item with the provided keys', () => {
const { items } = buildScreenshotItems();
const [pngSubmenu, pdfItem] = items;
expect(pngSubmenu.key).toBe('export_png_submenu');
expect(pngSubmenu.children).toHaveLength(2);
expect(pngSubmenu.children[0].key).toBe('export_png_transparent');
expect(pngSubmenu.children[1].key).toBe('export_png_solid');
expect(pdfItem.key).toBe('export_pdf');
});
test('getExportScreenshotMenuItems transparent option downloads a transparent PNG and dispatches a log event', () => {
const { items, setIsDropdownVisible, dispatch } = buildScreenshotItems();
items[0].children[0].onClick({ domEvent });
expect(mockDownloadAsImage).toHaveBeenCalledWith(
CHART_SELECTOR,
SLICE_NAME,
true,
expect.anything(),
{ format: 'png', backgroundType: 'transparent' },
);
expect(setIsDropdownVisible).toHaveBeenCalledWith(false);
expect(dispatch).toHaveBeenCalledTimes(1);
});
test('getExportScreenshotMenuItems solid option downloads a solid PNG and dispatches a log event', () => {
const { items, setIsDropdownVisible, dispatch } = buildScreenshotItems();
items[0].children[1].onClick({ domEvent });
expect(mockDownloadAsImage).toHaveBeenCalledWith(
CHART_SELECTOR,
SLICE_NAME,
true,
expect.anything(),
{ format: 'png', backgroundType: 'solid' },
);
expect(setIsDropdownVisible).toHaveBeenCalledWith(false);
expect(dispatch).toHaveBeenCalledTimes(1);
});
test('getExportScreenshotMenuItems PDF option calls downloadAsPdf and dispatches a log event', () => {
const { items, setIsDropdownVisible, dispatch } = buildScreenshotItems();
items[1].onClick({ domEvent });
expect(mockDownloadAsPdf).toHaveBeenCalledWith(
CHART_SELECTOR,
SLICE_NAME,
true,
);
expect(setIsDropdownVisible).toHaveBeenCalledWith(false);
expect(dispatch).toHaveBeenCalledTimes(1);
});
/**
* The version-history gate, exercised against the state shape hydrateExplore
* actually produces rather than a mocked can_overwrite. Every seeded chart
* ships with an empty editors list, so a membership-only gate hid the action
* from everyone on a fresh install.
*/
const renderMenuFor = (
slice: Record<string, unknown>,
user: Record<string, unknown>,
) =>
render(<TestComponent {...defaultProps} slice={slice as never} />, {
useRedux: true,
initialState: {
user,
explore: { can_overwrite: false, slice },
},
});
const adminUser = {
userId: 1,
username: 'admin',
permissions: {},
roles: { Admin: [] },
};
const gammaUser = {
userId: 2,
username: 'gamma',
permissions: {},
roles: { Gamma: [] },
};
test('an admin sees version history on a chart that has no editors', async () => {
window.featureFlags = { [FeatureFlag.VersionHistory]: true };
renderMenuFor(
{ slice_id: 1, slice_name: 'Test Chart', editors: [] },
adminUser,
);
expect(await screen.findByText('View version history')).toBeInTheDocument();
});
test('a non-editor without the admin role does not', async () => {
window.featureFlags = { [FeatureFlag.VersionHistory]: true };
renderMenuFor(
{ slice_id: 1, slice_name: 'Test Chart', editors: [] },
gammaUser,
);
await screen.findByText('View query');
expect(screen.queryByText('View version history')).not.toBeInTheDocument();
});
test('the item stays hidden while the feature flag is off', async () => {
window.featureFlags = { [FeatureFlag.VersionHistory]: false };
renderMenuFor(
{ slice_id: 1, slice_name: 'Test Chart', editors: [] },
adminUser,
);
await screen.findByText('View query');
expect(screen.queryByText('View version history')).not.toBeInTheDocument();
});