mirror of
https://github.com/apache/superset.git
synced 2026-04-20 08:34:37 +00:00
618 lines
18 KiB
TypeScript
618 lines
18 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 { KeyboardEvent, ReactElement } from 'react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { render, screen } from 'spec/helpers/testing-library';
|
|
import { FeatureFlag, VizType } from '@superset-ui/core';
|
|
import mockState from 'spec/fixtures/mockState';
|
|
import { Menu } from 'src/components/Menu';
|
|
import SliceHeaderControls, {
|
|
SliceHeaderControlsProps,
|
|
handleDropdownNavigation,
|
|
} from '.';
|
|
|
|
jest.mock('src/components/Dropdown', () => {
|
|
const original = jest.requireActual('src/components/Dropdown');
|
|
return {
|
|
...original,
|
|
NoAnimationDropdown: (props: any) => (
|
|
<div data-test="NoAnimationDropdown" className="ant-dropdown">
|
|
{props.overlay}
|
|
{props.children}
|
|
</div>
|
|
),
|
|
};
|
|
});
|
|
|
|
const createProps = (viz_type = VizType.Sunburst) =>
|
|
({
|
|
addDangerToast: jest.fn(),
|
|
addSuccessToast: jest.fn(),
|
|
exploreChart: jest.fn(),
|
|
exportCSV: jest.fn(),
|
|
exportFullCSV: jest.fn(),
|
|
exportXLSX: jest.fn(),
|
|
exportFullXLSX: jest.fn(),
|
|
forceRefresh: jest.fn(),
|
|
handleToggleFullSize: jest.fn(),
|
|
toggleExpandSlice: jest.fn(),
|
|
logEvent: jest.fn(),
|
|
slice: {
|
|
slice_id: 371,
|
|
slice_url: '/explore/?form_data=%7B%22slice_id%22%3A%20371%7D',
|
|
slice_name: 'Vaccine Candidates per Country & Stage',
|
|
slice_description: 'Table of vaccine candidates for 100 countries',
|
|
form_data: {
|
|
adhoc_filters: [],
|
|
color_scheme: 'supersetColors',
|
|
datasource: '58__table',
|
|
...(viz_type === VizType.Sunburst
|
|
? { columns: ['product_category', 'clinical_stage'] }
|
|
: { groupby: ['product_category', 'clinical_stage'] }),
|
|
linear_color_scheme: 'schemeYlOrBr',
|
|
metric: 'count',
|
|
queryFields: {
|
|
groupby: 'groupby',
|
|
metric: 'metrics',
|
|
secondary_metric: 'metrics',
|
|
},
|
|
row_limit: 10000,
|
|
slice_id: 371,
|
|
time_range: 'No filter',
|
|
url_params: {},
|
|
viz_type,
|
|
},
|
|
viz_type,
|
|
datasource: '58__table',
|
|
description: 'test-description',
|
|
description_markeddown: '',
|
|
owners: [],
|
|
modified: '<span class="no-wrap">22 hours ago</span>',
|
|
changed_on: 1617143411523,
|
|
},
|
|
isCached: [false],
|
|
isExpanded: false,
|
|
cachedDttm: [''],
|
|
updatedDttm: 1617213803803,
|
|
supersetCanExplore: true,
|
|
supersetCanCSV: true,
|
|
componentId: 'CHART-fYo7IyvKZQ',
|
|
dashboardId: 26,
|
|
isFullSize: false,
|
|
chartStatus: 'rendered',
|
|
showControls: true,
|
|
supersetCanShare: true,
|
|
formData: {
|
|
slice_id: 1,
|
|
datasource: '58__table',
|
|
viz_type: VizType.Sunburst,
|
|
},
|
|
exploreUrl: '/explore',
|
|
}) as SliceHeaderControlsProps;
|
|
|
|
const renderWrapper = (
|
|
overrideProps?: SliceHeaderControlsProps,
|
|
roles?: Record<string, string[][]>,
|
|
) => {
|
|
const props = overrideProps || createProps();
|
|
return render(<SliceHeaderControls {...props} />, {
|
|
useRedux: true,
|
|
useRouter: true,
|
|
initialState: {
|
|
...mockState,
|
|
user: {
|
|
...mockState.user,
|
|
roles: roles ?? {
|
|
Admin: [['can_samples', 'Datasource']],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
};
|
|
|
|
test('Should render', () => {
|
|
renderWrapper();
|
|
expect(
|
|
screen.getByRole('button', { name: 'More Options' }),
|
|
).toBeInTheDocument();
|
|
expect(screen.getByTestId('NoAnimationDropdown')).toBeInTheDocument();
|
|
});
|
|
|
|
test('Should render default props', () => {
|
|
const props = createProps();
|
|
|
|
// @ts-ignore
|
|
delete props.forceRefresh;
|
|
// @ts-ignore
|
|
delete props.toggleExpandSlice;
|
|
// @ts-ignore
|
|
delete props.exploreChart;
|
|
// @ts-ignore
|
|
delete props.exportCSV;
|
|
// @ts-ignore
|
|
delete props.exportXLSX;
|
|
// @ts-ignore
|
|
delete props.cachedDttm;
|
|
// @ts-ignore
|
|
delete props.updatedDttm;
|
|
// @ts-ignore
|
|
delete props.isCached;
|
|
// @ts-ignore
|
|
delete props.isExpanded;
|
|
|
|
renderWrapper(props);
|
|
expect(
|
|
screen.getByRole('menuitem', { name: 'Enter fullscreen' }),
|
|
).toBeInTheDocument();
|
|
expect(
|
|
screen.getByRole('menuitem', { name: /Force refresh/ }),
|
|
).toBeInTheDocument();
|
|
expect(
|
|
screen.getByRole('menuitem', { name: 'Show chart description' }),
|
|
).toBeInTheDocument();
|
|
expect(
|
|
screen.getByRole('menuitem', { name: 'Edit chart' }),
|
|
).toBeInTheDocument();
|
|
expect(
|
|
screen.getByRole('menuitem', { name: 'Download' }),
|
|
).toBeInTheDocument();
|
|
expect(screen.getByRole('menuitem', { name: 'Share' })).toBeInTheDocument();
|
|
|
|
expect(
|
|
screen.getByRole('button', { name: 'More Options' }),
|
|
).toBeInTheDocument();
|
|
expect(screen.getByTestId('NoAnimationDropdown')).toBeInTheDocument();
|
|
});
|
|
|
|
test('Should "export to CSV"', async () => {
|
|
const props = createProps();
|
|
renderWrapper(props);
|
|
expect(props.exportCSV).toHaveBeenCalledTimes(0);
|
|
userEvent.hover(screen.getByText('Download'));
|
|
userEvent.click(await screen.findByText('Export to .CSV'));
|
|
expect(props.exportCSV).toHaveBeenCalledTimes(1);
|
|
expect(props.exportCSV).toHaveBeenCalledWith(371);
|
|
});
|
|
|
|
test('Should "export to Excel"', async () => {
|
|
const props = createProps();
|
|
renderWrapper(props);
|
|
expect(props.exportXLSX).toHaveBeenCalledTimes(0);
|
|
userEvent.hover(screen.getByText('Download'));
|
|
userEvent.click(await screen.findByText('Export to Excel'));
|
|
expect(props.exportXLSX).toHaveBeenCalledTimes(1);
|
|
expect(props.exportXLSX).toHaveBeenCalledWith(371);
|
|
});
|
|
|
|
test('Export full CSV is under featureflag', async () => {
|
|
(global as any).featureFlags = {
|
|
[FeatureFlag.AllowFullCsvExport]: false,
|
|
};
|
|
const props = createProps(VizType.Table);
|
|
renderWrapper(props);
|
|
userEvent.hover(screen.getByText('Download'));
|
|
expect(await screen.findByText('Export to .CSV')).toBeInTheDocument();
|
|
expect(screen.queryByText('Export to full .CSV')).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('Should "export full CSV"', async () => {
|
|
(global as any).featureFlags = {
|
|
[FeatureFlag.AllowFullCsvExport]: true,
|
|
};
|
|
const props = createProps(VizType.Table);
|
|
renderWrapper(props);
|
|
expect(props.exportFullCSV).toHaveBeenCalledTimes(0);
|
|
userEvent.hover(screen.getByText('Download'));
|
|
userEvent.click(await screen.findByText('Export to full .CSV'));
|
|
expect(props.exportFullCSV).toHaveBeenCalledTimes(1);
|
|
expect(props.exportFullCSV).toHaveBeenCalledWith(371);
|
|
});
|
|
|
|
test('Should not show export full CSV if report is not table', async () => {
|
|
(global as any).featureFlags = {
|
|
[FeatureFlag.AllowFullCsvExport]: true,
|
|
};
|
|
renderWrapper();
|
|
userEvent.hover(screen.getByText('Download'));
|
|
expect(await screen.findByText('Export to .CSV')).toBeInTheDocument();
|
|
expect(screen.queryByText('Export to full .CSV')).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('Export full Excel is under featureflag', async () => {
|
|
(global as any).featureFlags = {
|
|
[FeatureFlag.AllowFullCsvExport]: false,
|
|
};
|
|
const props = createProps(VizType.Table);
|
|
renderWrapper(props);
|
|
userEvent.hover(screen.getByText('Download'));
|
|
expect(await screen.findByText('Export to Excel')).toBeInTheDocument();
|
|
expect(screen.queryByText('Export to full Excel')).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('Should "export full Excel"', async () => {
|
|
(global as any).featureFlags = {
|
|
[FeatureFlag.AllowFullCsvExport]: true,
|
|
};
|
|
const props = createProps(VizType.Table);
|
|
renderWrapper(props);
|
|
expect(props.exportFullXLSX).toHaveBeenCalledTimes(0);
|
|
userEvent.hover(screen.getByText('Download'));
|
|
userEvent.click(await screen.findByText('Export to full Excel'));
|
|
expect(props.exportFullXLSX).toHaveBeenCalledTimes(1);
|
|
expect(props.exportFullXLSX).toHaveBeenCalledWith(371);
|
|
});
|
|
|
|
test('Should not show export full Excel if report is not table', async () => {
|
|
(global as any).featureFlags = {
|
|
[FeatureFlag.AllowFullCsvExport]: true,
|
|
};
|
|
renderWrapper();
|
|
userEvent.hover(screen.getByText('Download'));
|
|
expect(await screen.findByText('Export to Excel')).toBeInTheDocument();
|
|
expect(screen.queryByText('Export to full Excel')).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('Should "Show chart description"', () => {
|
|
const props = createProps();
|
|
renderWrapper(props);
|
|
expect(props.toggleExpandSlice).toHaveBeenCalledTimes(0);
|
|
userEvent.click(screen.getByText('Show chart description'));
|
|
expect(props.toggleExpandSlice).toHaveBeenCalledTimes(1);
|
|
expect(props.toggleExpandSlice).toHaveBeenCalledWith(371);
|
|
});
|
|
|
|
test('Should "Force refresh"', () => {
|
|
const props = createProps();
|
|
renderWrapper(props);
|
|
expect(props.forceRefresh).toHaveBeenCalledTimes(0);
|
|
userEvent.click(screen.getByText('Force refresh'));
|
|
expect(props.forceRefresh).toHaveBeenCalledTimes(1);
|
|
expect(props.forceRefresh).toHaveBeenCalledWith(371, 26);
|
|
expect(props.addSuccessToast).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('Should "Enter fullscreen"', () => {
|
|
const props = createProps();
|
|
renderWrapper(props);
|
|
|
|
expect(props.handleToggleFullSize).toHaveBeenCalledTimes(0);
|
|
userEvent.click(screen.getByText('Enter fullscreen'));
|
|
expect(props.handleToggleFullSize).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('Drill to detail modal is under featureflag', () => {
|
|
(global as any).featureFlags = {
|
|
[FeatureFlag.DrillToDetail]: false,
|
|
};
|
|
const props = createProps();
|
|
renderWrapper(props);
|
|
expect(screen.queryByText('Drill to detail')).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('Should show "Drill to detail" with `can_explore` & `can_samples` perms', () => {
|
|
(global as any).featureFlags = {
|
|
[FeatureFlag.DrillToDetail]: true,
|
|
};
|
|
const props = {
|
|
...createProps(),
|
|
supersetCanExplore: true,
|
|
};
|
|
props.slice.slice_id = 18;
|
|
renderWrapper(props, {
|
|
Admin: [['can_samples', 'Datasource']],
|
|
});
|
|
expect(screen.getByText('Drill to detail')).toBeInTheDocument();
|
|
});
|
|
|
|
test('Should show "Drill to detail" with `can_drill` & `can_samples` perms', () => {
|
|
(global as any).featureFlags = {
|
|
[FeatureFlag.DrillToDetail]: true,
|
|
};
|
|
const props = {
|
|
...createProps(),
|
|
supersetCanExplore: false,
|
|
};
|
|
props.slice.slice_id = 18;
|
|
renderWrapper(props, {
|
|
Admin: [
|
|
['can_samples', 'Datasource'],
|
|
['can_drill', 'Dashboard'],
|
|
],
|
|
});
|
|
expect(screen.getByText('Drill to detail')).toBeInTheDocument();
|
|
});
|
|
|
|
test('Should show "Drill to detail" with both `canexplore` + `can_drill` & `can_samples` perms', () => {
|
|
(global as any).featureFlags = {
|
|
[FeatureFlag.DrillToDetail]: true,
|
|
};
|
|
const props = {
|
|
...createProps(),
|
|
supersetCanExplore: true,
|
|
};
|
|
props.slice.slice_id = 18;
|
|
renderWrapper(props, {
|
|
Admin: [
|
|
['can_samples', 'Datasource'],
|
|
['can_drill', 'Dashboard'],
|
|
],
|
|
});
|
|
expect(screen.getByText('Drill to detail')).toBeInTheDocument();
|
|
});
|
|
|
|
test('Should not show "Drill to detail" with neither of required perms', () => {
|
|
(global as any).featureFlags = {
|
|
[FeatureFlag.DrillToDetail]: true,
|
|
};
|
|
const props = {
|
|
...createProps(),
|
|
supersetCanExplore: false,
|
|
};
|
|
props.slice.slice_id = 18;
|
|
renderWrapper(props, {
|
|
Admin: [['invalid_permission', 'Dashboard']],
|
|
});
|
|
expect(screen.queryByText('Drill to detail')).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('Should not show "Drill to detail" only `can_dril` perm', () => {
|
|
(global as any).featureFlags = {
|
|
[FeatureFlag.DrillToDetail]: true,
|
|
};
|
|
const props = {
|
|
...createProps(),
|
|
supersetCanExplore: false,
|
|
};
|
|
props.slice.slice_id = 18;
|
|
renderWrapper(props, {
|
|
Admin: [['can_drill', 'Dashboard']],
|
|
});
|
|
expect(screen.queryByText('Drill to detail')).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('Should show "View query"', () => {
|
|
const props = {
|
|
...createProps(),
|
|
supersetCanExplore: false,
|
|
};
|
|
props.slice.slice_id = 18;
|
|
renderWrapper(props, {
|
|
Admin: [['can_view_query', 'Dashboard']],
|
|
});
|
|
expect(screen.getByText('View query')).toBeInTheDocument();
|
|
});
|
|
|
|
test('Should not show "View query"', () => {
|
|
const props = {
|
|
...createProps(),
|
|
supersetCanExplore: false,
|
|
};
|
|
props.slice.slice_id = 18;
|
|
renderWrapper(props, {
|
|
Admin: [['invalid_permission', 'Dashboard']],
|
|
});
|
|
expect(screen.queryByText('View query')).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('Should show "View as table"', () => {
|
|
const props = {
|
|
...createProps(),
|
|
supersetCanExplore: false,
|
|
};
|
|
props.slice.slice_id = 18;
|
|
renderWrapper(props, {
|
|
Admin: [['can_view_chart_as_table', 'Dashboard']],
|
|
});
|
|
expect(screen.getByText('View as table')).toBeInTheDocument();
|
|
});
|
|
|
|
test('Should not show "View as table"', () => {
|
|
const props = {
|
|
...createProps(),
|
|
supersetCanExplore: false,
|
|
};
|
|
props.slice.slice_id = 18;
|
|
renderWrapper(props, {
|
|
Admin: [['invalid_permission', 'Dashboard']],
|
|
});
|
|
expect(screen.queryByText('View as table')).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('Should not show the "Edit chart" button', () => {
|
|
const props = {
|
|
...createProps(),
|
|
supersetCanExplore: false,
|
|
};
|
|
props.slice.slice_id = 18;
|
|
renderWrapper(props, {
|
|
Admin: [
|
|
['can_samples', 'Datasource'],
|
|
['can_view_query', 'Dashboard'],
|
|
['can_view_chart_as_table', 'Dashboard'],
|
|
],
|
|
});
|
|
expect(screen.queryByText('Edit chart')).not.toBeInTheDocument();
|
|
});
|
|
|
|
describe('handleDropdownNavigation', () => {
|
|
const mockToggleDropdown = jest.fn();
|
|
const mockSetSelectedKeys = jest.fn();
|
|
const mockSetOpenKeys = jest.fn();
|
|
|
|
const menu = (
|
|
<Menu selectedKeys={['item1']}>
|
|
<Menu.Item key="item1">Item 1</Menu.Item>
|
|
<Menu.Item key="item2">Item 2</Menu.Item>
|
|
<Menu.Item key="item3">Item 3</Menu.Item>
|
|
</Menu>
|
|
);
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
test('should continue with system tab navigation if dropdown is closed and tab key is pressed', () => {
|
|
const event = {
|
|
key: 'Tab',
|
|
preventDefault: jest.fn(),
|
|
} as unknown as KeyboardEvent<HTMLDivElement>;
|
|
|
|
handleDropdownNavigation(
|
|
event,
|
|
false,
|
|
<div />,
|
|
mockToggleDropdown,
|
|
mockSetSelectedKeys,
|
|
mockSetOpenKeys,
|
|
);
|
|
expect(mockToggleDropdown).not.toHaveBeenCalled();
|
|
expect(mockSetSelectedKeys).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test(`should prevent default behavior and toggle dropdown if dropdown
|
|
is closed and action key is pressed`, () => {
|
|
const event = {
|
|
key: 'Enter',
|
|
preventDefault: jest.fn(),
|
|
} as unknown as KeyboardEvent<HTMLDivElement>;
|
|
|
|
handleDropdownNavigation(
|
|
event,
|
|
false,
|
|
<div />,
|
|
mockToggleDropdown,
|
|
mockSetSelectedKeys,
|
|
mockSetOpenKeys,
|
|
);
|
|
expect(mockToggleDropdown).toHaveBeenCalled();
|
|
expect(mockSetSelectedKeys).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test(`should trigger menu item click,
|
|
clear selected keys, close dropdown, and focus on menu trigger
|
|
if action key is pressed and menu item is selected`, () => {
|
|
const event = {
|
|
key: 'Enter',
|
|
preventDefault: jest.fn(),
|
|
currentTarget: { focus: jest.fn() },
|
|
} as unknown as KeyboardEvent<HTMLDivElement>;
|
|
|
|
handleDropdownNavigation(
|
|
event,
|
|
true,
|
|
menu,
|
|
mockToggleDropdown,
|
|
mockSetSelectedKeys,
|
|
mockSetOpenKeys,
|
|
);
|
|
expect(mockToggleDropdown).toHaveBeenCalled();
|
|
expect(mockSetSelectedKeys).toHaveBeenCalledWith([]);
|
|
expect(event.currentTarget.focus).toHaveBeenCalled();
|
|
});
|
|
|
|
test('should select the next menu item if down arrow key is pressed', () => {
|
|
const event = {
|
|
key: 'ArrowDown',
|
|
preventDefault: jest.fn(),
|
|
} as unknown as KeyboardEvent<HTMLDivElement>;
|
|
|
|
handleDropdownNavigation(
|
|
event,
|
|
true,
|
|
menu,
|
|
mockToggleDropdown,
|
|
mockSetSelectedKeys,
|
|
mockSetOpenKeys,
|
|
);
|
|
expect(mockSetSelectedKeys).toHaveBeenCalledWith(['item2']);
|
|
});
|
|
|
|
test('should select the previous menu item if up arrow key is pressed', () => {
|
|
const event = {
|
|
key: 'ArrowUp',
|
|
preventDefault: jest.fn(),
|
|
} as unknown as KeyboardEvent<HTMLDivElement>;
|
|
|
|
handleDropdownNavigation(
|
|
event,
|
|
true,
|
|
menu,
|
|
mockToggleDropdown,
|
|
mockSetSelectedKeys,
|
|
mockSetOpenKeys,
|
|
);
|
|
expect(mockSetSelectedKeys).toHaveBeenCalledWith(['item1']);
|
|
});
|
|
|
|
test('should close dropdown menu if escape key is pressed', () => {
|
|
const event = {
|
|
key: 'Escape',
|
|
preventDefault: jest.fn(),
|
|
} as unknown as KeyboardEvent<HTMLDivElement>;
|
|
|
|
handleDropdownNavigation(
|
|
event,
|
|
true,
|
|
<div />,
|
|
mockToggleDropdown,
|
|
mockSetSelectedKeys,
|
|
mockSetOpenKeys,
|
|
);
|
|
expect(mockToggleDropdown).toHaveBeenCalled();
|
|
expect(mockSetSelectedKeys).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('should do nothing if an unsupported key is pressed', () => {
|
|
const event = {
|
|
key: 'Shift',
|
|
preventDefault: jest.fn(),
|
|
} as unknown as KeyboardEvent<HTMLDivElement>;
|
|
|
|
handleDropdownNavigation(
|
|
event,
|
|
true,
|
|
<div />,
|
|
mockToggleDropdown,
|
|
mockSetSelectedKeys,
|
|
mockSetOpenKeys,
|
|
);
|
|
expect(mockToggleDropdown).not.toHaveBeenCalled();
|
|
expect(mockSetSelectedKeys).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('should find a child element with a key', () => {
|
|
const item = {
|
|
props: {
|
|
children: [
|
|
<div key="1">Child 1</div>,
|
|
<div key="2">Child 2</div>,
|
|
<div key="3">Child 3</div>,
|
|
],
|
|
},
|
|
};
|
|
|
|
const childWithKey = item?.props?.children?.find(
|
|
(child: ReactElement) => child?.key,
|
|
);
|
|
|
|
expect(childWithKey).toBeDefined();
|
|
});
|
|
});
|