From 474f1e204440ead1f1806a304ab4d1387cc42252 Mon Sep 17 00:00:00 2001
From: Geido <60598000+geido@users.noreply.github.com>
Date: Wed, 14 Apr 2021 16:54:24 +0300
Subject: [PATCH] test: Add tests for Dashboard Header and
HeaderActionsDropdown components (#13973)
* Add tests for HeaderActionsDropdown - WIP
* Fix trigger node
* Add types
* Clean up
* Add tests for Header
* Delete obsolete tests
* Add factory and clean up
* Add opposite case
* Fix file name
* Include latest changes
---
.../components/HeaderActionsDropdown_spec.jsx | 174 -----------
.../dashboard/components/Header_spec.jsx | 244 ---------------
.../components/Header/Header.test.tsx | 288 ++++++++++++++++++
.../HeaderActionsDropdown.test.tsx | 200 ++++++++++++
.../HeaderActionsDropdown/index.jsx} | 20 +-
.../{Header.jsx => Header/index.jsx} | 15 +-
.../src/dashboard/components/Header/types.ts | 98 ++++++
7 files changed, 603 insertions(+), 436 deletions(-)
delete mode 100644 superset-frontend/spec/javascripts/dashboard/components/HeaderActionsDropdown_spec.jsx
delete mode 100644 superset-frontend/spec/javascripts/dashboard/components/Header_spec.jsx
create mode 100644 superset-frontend/src/dashboard/components/Header/Header.test.tsx
create mode 100644 superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx
rename superset-frontend/src/dashboard/components/{HeaderActionsDropdown.jsx => Header/HeaderActionsDropdown/index.jsx} (93%)
rename superset-frontend/src/dashboard/components/{Header.jsx => Header/index.jsx} (97%)
create mode 100644 superset-frontend/src/dashboard/components/Header/types.ts
diff --git a/superset-frontend/spec/javascripts/dashboard/components/HeaderActionsDropdown_spec.jsx b/superset-frontend/spec/javascripts/dashboard/components/HeaderActionsDropdown_spec.jsx
deleted file mode 100644
index fa60986ceaa..00000000000
--- a/superset-frontend/spec/javascripts/dashboard/components/HeaderActionsDropdown_spec.jsx
+++ /dev/null
@@ -1,174 +0,0 @@
-/**
- * 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 React from 'react';
-import { shallow } from 'enzyme';
-import { Menu, NoAnimationDropdown } from 'src/common/components';
-import RefreshIntervalModal from 'src/dashboard/components/RefreshIntervalModal';
-import HeaderActionsDropdown from 'src/dashboard/components/HeaderActionsDropdown';
-import SaveModal from 'src/dashboard/components/SaveModal';
-import CssEditor from 'src/dashboard/components/CssEditor';
-import fetchMock from 'fetch-mock';
-import ShareMenuItems from 'src/dashboard/components/menu/ShareMenuItems';
-
-fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', {});
-
-describe('HeaderActionsDropdown', () => {
- const props = {
- addSuccessToast: () => {},
- addDangerToast: () => {},
- customCss: '',
- dashboardId: 1,
- dashboardInfo: {},
- dashboardTitle: 'Title',
- editMode: false,
- expandedSlices: {},
- filters: {},
- forceRefreshAllCharts: () => {},
- hasUnsavedChanges: false,
- isLoading: false,
- layout: {},
- onChange: () => {},
- onSave: () => {},
- refreshFrequency: 200,
- setRefreshFrequency: () => {},
- shouldPersistRefreshFrequency: true,
- showPropertiesModal: () => {},
- startPeriodicRender: () => {},
- updateCss: () => {},
- userCanEdit: false,
- userCanSave: false,
- lastModifiedTime: 0,
- };
-
- function setup(overrideProps) {
- const wrapper = shallow(
- ,
- );
- const menu = shallow(
-
{wrapper.find(NoAnimationDropdown).props().overlay}
,
- );
- return { wrapper, menu };
- }
-
- describe('readonly-user', () => {
- const overrideProps = { userCanSave: false, userCanShare: false };
-
- it('should render the DropdownButton', () => {
- const { wrapper } = setup(overrideProps);
- expect(wrapper.find(NoAnimationDropdown)).toExist();
- });
-
- it('should not render the SaveModal', () => {
- const { menu } = setup(overrideProps);
- expect(menu.find(SaveModal)).not.toExist();
- });
-
- it('should render available Menu items', () => {
- const { menu } = setup(overrideProps);
- expect(menu.find(Menu.Item)).toHaveLength(4);
- });
-
- it('should render the RefreshIntervalModal', () => {
- const { menu } = setup(overrideProps);
- expect(menu.find(RefreshIntervalModal)).toExist();
- });
-
- it('should not render the ShareMenuItems', () => {
- const { menu } = setup(overrideProps);
- expect(menu.find(ShareMenuItems)).not.toExist();
- });
-
- it('should not render the CssEditor', () => {
- const { menu } = setup(overrideProps);
- expect(menu.find(CssEditor)).not.toExist();
- });
- });
-
- describe('write-user', () => {
- const overrideProps = { userCanSave: true, userCanShare: true };
-
- it('should render the DropdownButton', () => {
- const { wrapper } = setup(overrideProps);
- expect(wrapper.find(NoAnimationDropdown)).toExist();
- });
-
- it('should render the SaveModal', () => {
- const { menu } = setup(overrideProps);
- expect(menu.find(SaveModal)).toExist();
- });
-
- it('should render available Menu items', () => {
- const { menu } = setup(overrideProps);
- expect(menu.find(Menu.Item)).toHaveLength(5);
- });
-
- it('should render the RefreshIntervalModal', () => {
- const { menu } = setup(overrideProps);
- expect(menu.find(RefreshIntervalModal)).toExist();
- });
-
- it('should render the ShareMenuItems', () => {
- const { menu } = setup(overrideProps);
- expect(menu.find(ShareMenuItems)).toExist();
- });
-
- it('should not render the CssEditor', () => {
- const { menu } = setup(overrideProps);
- expect(menu.find(CssEditor)).not.toExist();
- });
- });
-
- describe('write-user-with-edit-mode', () => {
- const overrideProps = {
- userCanSave: true,
- editMode: true,
- userCanShare: true,
- };
-
- it('should render the DropdownButton', () => {
- const { wrapper } = setup(overrideProps);
- expect(wrapper.find(NoAnimationDropdown)).toExist();
- });
-
- it('should render the SaveModal', () => {
- const { menu } = setup(overrideProps);
- expect(menu.find(SaveModal)).toExist();
- });
-
- it('should render available MenuItems', () => {
- const { menu } = setup(overrideProps);
- expect(menu.find(Menu.Item)).toHaveLength(6);
- });
-
- it('should render the RefreshIntervalModal', () => {
- const { menu } = setup(overrideProps);
- expect(menu.find(RefreshIntervalModal)).toExist();
- });
-
- it('should render the ShareMenuItems', () => {
- const { menu } = setup(overrideProps);
- expect(menu.find(ShareMenuItems)).toExist();
- });
-
- it('should render the CssEditor', () => {
- const { menu } = setup(overrideProps);
- expect(menu.find(CssEditor)).toExist();
- });
- });
-});
diff --git a/superset-frontend/spec/javascripts/dashboard/components/Header_spec.jsx b/superset-frontend/spec/javascripts/dashboard/components/Header_spec.jsx
deleted file mode 100644
index e5a7f52e74c..00000000000
--- a/superset-frontend/spec/javascripts/dashboard/components/Header_spec.jsx
+++ /dev/null
@@ -1,244 +0,0 @@
-/**
- * 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 React from 'react';
-import { styledMount as mount } from 'spec/helpers/theming';
-import Header from 'src/dashboard/components/Header';
-import EditableTitle from 'src/components/EditableTitle';
-import FaveStar from 'src/components/FaveStar';
-import PublishedStatus from 'src/dashboard/components/PublishedStatus';
-import HeaderActionsDropdown from 'src/dashboard/components/HeaderActionsDropdown';
-import Button from 'src/components/Button';
-import UndoRedoKeyListeners from 'src/dashboard/components/UndoRedoKeyListeners';
-
-describe('Header', () => {
- const props = {
- addSuccessToast: () => {},
- addDangerToast: () => {},
- addWarningToast: () => {},
- dashboardInfo: {
- id: 1,
- dash_edit_perm: true,
- dash_save_perm: true,
- userId: 1,
- metadata: {},
- common: {
- conf: {},
- },
- },
- dashboardTitle: 'title',
- charts: {},
- layout: {},
- filters: {},
- expandedSlices: {},
- css: '',
- customCss: '',
- isStarred: false,
- isLoading: false,
- lastModifiedTime: 0,
- refreshFrequency: 0,
- shouldPersistRefreshFrequency: false,
- onSave: () => {},
- onChange: () => {},
- fetchFaveStar: () => {},
- fetchCharts: () => {},
- saveFaveStar: () => {},
- savePublished: () => {},
- isPublished: false,
- updateDashboardTitle: () => {},
- editMode: false,
- setEditMode: () => {},
- showBuilderPane: () => {},
- updateCss: () => {},
- setColorSchemeAndUnsavedChanges: () => {},
- logEvent: () => {},
- setRefreshFrequency: () => {},
- hasUnsavedChanges: false,
- maxUndoHistoryExceeded: false,
-
- // redux
- onUndo: () => {},
- onRedo: () => {},
- undoLength: 0,
- redoLength: 0,
- setMaxUndoHistoryExceeded: () => {},
- maxUndoHistoryToast: () => {},
- dashboardInfoChanged: () => {},
- dashboardTitleChanged: () => {},
- };
-
- function setup(overrideProps) {
- const wrapper = mount();
- return wrapper;
- }
-
- describe('read-only-user', () => {
- const overrideProps = {
- dashboardInfo: {
- ...props.dashboardInfo,
- id: 1,
- dash_edit_perm: false,
- dash_save_perm: false,
- userId: 1,
- },
- };
-
- it('should render the EditableTitle', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(EditableTitle)).toExist();
- });
-
- it('should render the PublishedStatus', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(PublishedStatus)).toExist();
- });
-
- it('should render the FaveStar', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(FaveStar)).toExist();
- });
-
- it('should render the HeaderActionsDropdown', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(HeaderActionsDropdown)).toExist();
- });
-
- it('should not set up undo/redo', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(UndoRedoKeyListeners)).not.toExist();
- });
- });
-
- describe('write-user', () => {
- const overrideProps = {
- editMode: false,
- dashboardInfo: {
- ...props.dashboardInfo,
- id: 1,
- dash_edit_perm: true,
- dash_save_perm: true,
- userId: 1,
- },
- };
-
- it('should render the EditableTitle', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(EditableTitle)).toExist();
- });
-
- it('should render the PublishedStatus', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(PublishedStatus)).toExist();
- });
-
- it('should render the FaveStar', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(FaveStar)).toExist();
- });
-
- it('should render the HeaderActionsDropdown', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(HeaderActionsDropdown)).toExist();
- });
-
- it('should not set up undo/redo', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(UndoRedoKeyListeners)).not.toExist();
- });
- });
-
- describe('write-user-with-edit-mode', () => {
- const overrideProps = {
- editMode: true,
- dashboardInfo: {
- ...props.dashboardInfo,
- id: 1,
- dash_edit_perm: true,
- dash_save_perm: true,
- userId: 1,
- },
- };
-
- it('should render the EditableTitle', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(EditableTitle)).toExist();
- });
-
- it('should render the FaveStar', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(FaveStar)).toExist();
- });
-
- it('should render the PublishedStatus', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(PublishedStatus)).toExist();
- });
-
- it('should render the HeaderActionsDropdown', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(HeaderActionsDropdown)).toExist();
- });
-
- it('should render five Buttons', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(Button)).toHaveLength(4);
- });
-
- it('should set up undo/redo', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(UndoRedoKeyListeners)).toExist();
- });
- });
-
- describe('logged-out-user', () => {
- const overrideProps = {
- dashboardInfo: {
- ...props.dashboardInfo,
- id: 1,
- dash_edit_perm: false,
- dash_save_perm: false,
- userId: null,
- },
- };
-
- it('should render the EditableTitle', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(EditableTitle)).toExist();
- });
-
- it('should render the PublishedStatus', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(PublishedStatus)).toExist();
- });
-
- it('should not render the FaveStar', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(FaveStar)).not.toExist();
- });
-
- it('should render the HeaderActionsDropdown', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(HeaderActionsDropdown)).toExist();
- });
-
- it('should not set up undo/redo', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(UndoRedoKeyListeners)).not.toExist();
- });
- });
-});
diff --git a/superset-frontend/src/dashboard/components/Header/Header.test.tsx b/superset-frontend/src/dashboard/components/Header/Header.test.tsx
new file mode 100644
index 00000000000..d5393b22755
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/Header/Header.test.tsx
@@ -0,0 +1,288 @@
+/**
+ * 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 React from 'react';
+import { render, screen, fireEvent } from 'spec/helpers/testing-library';
+import userEvent from '@testing-library/user-event';
+import fetchMock from 'fetch-mock';
+import { HeaderProps } from './types';
+import Header from '.';
+
+const createProps = () => ({
+ addSuccessToast: jest.fn(),
+ addDangerToast: jest.fn(),
+ addWarningToast: jest.fn(),
+ dashboardInfo: {
+ id: 1,
+ dash_edit_perm: false,
+ dash_save_perm: false,
+ dash_share_perm: false,
+ userId: 1,
+ metadata: {},
+ common: {
+ conf: {},
+ },
+ },
+ dashboardTitle: 'Dashboard Title',
+ charts: {},
+ layout: {},
+ expandedSlices: {},
+ css: '',
+ customCss: '',
+ isStarred: false,
+ isLoading: false,
+ lastModifiedTime: 0,
+ refreshFrequency: 0,
+ shouldPersistRefreshFrequency: false,
+ onSave: jest.fn(),
+ onChange: jest.fn(),
+ fetchFaveStar: jest.fn(),
+ fetchCharts: jest.fn(),
+ saveFaveStar: jest.fn(),
+ savePublished: jest.fn(),
+ isPublished: false,
+ updateDashboardTitle: jest.fn(),
+ editMode: false,
+ setEditMode: jest.fn(),
+ showBuilderPane: jest.fn(),
+ updateCss: jest.fn(),
+ setColorSchemeAndUnsavedChanges: jest.fn(),
+ logEvent: jest.fn(),
+ setRefreshFrequency: jest.fn(),
+ hasUnsavedChanges: false,
+ maxUndoHistoryExceeded: false,
+ onUndo: jest.fn(),
+ onRedo: jest.fn(),
+ undoLength: 0,
+ redoLength: 0,
+ setMaxUndoHistoryExceeded: jest.fn(),
+ maxUndoHistoryToast: jest.fn(),
+ dashboardInfoChanged: jest.fn(),
+ dashboardTitleChanged: jest.fn(),
+});
+const props = createProps();
+const editableProps = {
+ ...props,
+ editMode: true,
+ dashboardInfo: {
+ ...props.dashboardInfo,
+ dash_edit_perm: true,
+ dash_save_perm: true,
+ },
+};
+const undoProps = {
+ ...editableProps,
+ undoLength: 1,
+};
+const redoProps = {
+ ...editableProps,
+ redoLength: 1,
+};
+
+fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', {});
+
+function setup(props: HeaderProps) {
+ return (
+
+
+
+ );
+}
+
+async function openActionsDropdown() {
+ const btn = screen.getByRole('img', { name: 'more-horiz' });
+ userEvent.click(btn);
+ expect(await screen.findByRole('menu')).toBeInTheDocument();
+}
+
+test('should render', () => {
+ const mockedProps = createProps();
+ const { container } = render(setup(mockedProps));
+ expect(container).toBeInTheDocument();
+});
+
+test('should render the title', () => {
+ const mockedProps = createProps();
+ render(setup(mockedProps));
+ expect(screen.getByText('Dashboard Title')).toBeInTheDocument();
+});
+
+test('should render the editable title', () => {
+ render(setup(editableProps));
+ expect(screen.getByDisplayValue('Dashboard Title')).toBeInTheDocument();
+});
+
+test('should edit the title', () => {
+ render(setup(editableProps));
+ const editableTitle = screen.getByDisplayValue('Dashboard Title');
+ expect(editableProps.onChange).not.toHaveBeenCalled();
+ userEvent.click(editableTitle);
+ userEvent.clear(editableTitle);
+ userEvent.type(editableTitle, 'New Title');
+ userEvent.click(document.body);
+ expect(editableProps.onChange).toHaveBeenCalled();
+ expect(screen.getByDisplayValue('New Title')).toBeInTheDocument();
+});
+
+test('should render the "Draft" status', () => {
+ const mockedProps = createProps();
+ render(setup(mockedProps));
+ expect(screen.getByText('Draft')).toBeInTheDocument();
+});
+
+test('should publish', () => {
+ render(setup(editableProps));
+ const draft = screen.getByText('Draft');
+ expect(editableProps.savePublished).not.toHaveBeenCalled();
+ userEvent.click(draft);
+ expect(editableProps.savePublished).toHaveBeenCalledTimes(1);
+});
+
+test('should render the "Undo" action as disabled', () => {
+ render(setup(editableProps));
+ expect(screen.getByTitle('Undo').parentElement).toBeDisabled();
+});
+
+test('should undo', () => {
+ render(setup(undoProps));
+ const undo = screen.getByTitle('Undo');
+ expect(undoProps.onUndo).not.toHaveBeenCalled();
+ userEvent.click(undo);
+ expect(undoProps.onUndo).toHaveBeenCalledTimes(1);
+});
+
+test('should undo with key listener', () => {
+ undoProps.onUndo.mockReset();
+ render(setup(undoProps));
+ expect(undoProps.onUndo).not.toHaveBeenCalled();
+ fireEvent.keyDown(document.body, { key: 'z', code: 'KeyZ', ctrlKey: true });
+ expect(undoProps.onUndo).toHaveBeenCalledTimes(1);
+});
+
+test('should render the "Redo" action as disabled', () => {
+ render(setup(editableProps));
+ expect(screen.getByTitle('Redo').parentElement).toBeDisabled();
+});
+
+test('should redo', () => {
+ render(setup(redoProps));
+ const redo = screen.getByTitle('Redo');
+ expect(redoProps.onRedo).not.toHaveBeenCalled();
+ userEvent.click(redo);
+ expect(redoProps.onRedo).toHaveBeenCalledTimes(1);
+});
+
+test('should redo with key listener', () => {
+ redoProps.onRedo.mockReset();
+ render(setup(redoProps));
+ expect(redoProps.onRedo).not.toHaveBeenCalled();
+ fireEvent.keyDown(document.body, { key: 'y', code: 'KeyY', ctrlKey: true });
+ expect(redoProps.onRedo).toHaveBeenCalledTimes(1);
+});
+
+test('should render the "Discard changes" button', () => {
+ render(setup(editableProps));
+ expect(screen.getByText('Discard changes')).toBeInTheDocument();
+});
+
+test('should render the "Save" button as disabled', () => {
+ render(setup(editableProps));
+ expect(screen.getByText('Save').parentElement).toBeDisabled();
+});
+
+test('should save', () => {
+ const unsavedProps = {
+ ...editableProps,
+ hasUnsavedChanges: true,
+ };
+ render(setup(unsavedProps));
+ const save = screen.getByText('Save');
+ expect(unsavedProps.onSave).not.toHaveBeenCalled();
+ userEvent.click(save);
+ expect(unsavedProps.onSave).toHaveBeenCalledTimes(1);
+});
+
+test('should NOT render the "Draft" status', () => {
+ const mockedProps = createProps();
+ const publishedProps = {
+ ...mockedProps,
+ isPublished: true,
+ };
+ render(setup(publishedProps));
+ expect(screen.queryByText('Draft')).not.toBeInTheDocument();
+});
+
+test('should render the unselected fave icon', () => {
+ const mockedProps = createProps();
+ render(setup(mockedProps));
+ expect(mockedProps.fetchFaveStar).toHaveBeenCalled();
+ expect(
+ screen.getByRole('img', { name: 'favorite-unselected' }),
+ ).toBeInTheDocument();
+});
+
+test('should render the selected fave icon', () => {
+ const mockedProps = createProps();
+ const favedProps = {
+ ...mockedProps,
+ isStarred: true,
+ };
+ render(setup(favedProps));
+ expect(
+ screen.getByRole('img', { name: 'favorite-selected' }),
+ ).toBeInTheDocument();
+});
+
+test('should fave', async () => {
+ const mockedProps = createProps();
+ render(setup(mockedProps));
+ const fave = screen.getByRole('img', { name: 'favorite-unselected' });
+ expect(mockedProps.saveFaveStar).not.toHaveBeenCalled();
+ userEvent.click(fave);
+ expect(mockedProps.saveFaveStar).toHaveBeenCalledTimes(1);
+});
+
+test('should toggle the edit mode', () => {
+ const mockedProps = createProps();
+ const canEditProps = {
+ ...mockedProps,
+ dashboardInfo: {
+ ...mockedProps.dashboardInfo,
+ dash_edit_perm: true,
+ },
+ };
+ render(setup(canEditProps));
+ const editDashboard = screen.getByTitle('Edit dashboard');
+ expect(screen.queryByTitle('Edit dashboard')).toBeInTheDocument();
+ userEvent.click(editDashboard);
+ expect(mockedProps.logEvent).toHaveBeenCalled();
+});
+
+test('should render the dropdown icon', () => {
+ const mockedProps = createProps();
+ render(setup(mockedProps));
+ expect(screen.getByRole('img', { name: 'more-horiz' })).toBeInTheDocument();
+});
+
+test('should refresh the charts', async () => {
+ const mockedProps = createProps();
+ render(setup(mockedProps));
+ await openActionsDropdown();
+ userEvent.click(screen.getByText('Refresh dashboard'));
+ expect(mockedProps.fetchCharts).toHaveBeenCalledTimes(1);
+});
diff --git a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx
new file mode 100644
index 00000000000..039c1ef97c6
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx
@@ -0,0 +1,200 @@
+/**
+ * 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 React from 'react';
+import { render, screen } from 'spec/helpers/testing-library';
+import userEvent from '@testing-library/user-event';
+import fetchMock from 'fetch-mock';
+import { HeaderDropdownProps } from 'src/dashboard/components/Header/types';
+import HeaderActionsDropdown from '.';
+
+const createProps = () => ({
+ addSuccessToast: jest.fn(),
+ addDangerToast: jest.fn(),
+ customCss: '#save-dash-split-button{margin-left: 100px;}',
+ dashboardId: 1,
+ dashboardInfo: {
+ id: 1,
+ dash_edit_perm: true,
+ dash_save_perm: true,
+ userId: 1,
+ metadata: {},
+ common: {
+ conf: {},
+ },
+ },
+ dashboardTitle: 'Title',
+ editMode: false,
+ expandedSlices: {},
+ forceRefreshAllCharts: jest.fn(),
+ hasUnsavedChanges: false,
+ isLoading: false,
+ layout: {},
+ onChange: jest.fn(),
+ onSave: jest.fn(),
+ refreshFrequency: 200,
+ setRefreshFrequency: jest.fn(),
+ shouldPersistRefreshFrequency: false,
+ showPropertiesModal: jest.fn(),
+ startPeriodicRender: jest.fn(),
+ updateCss: jest.fn(),
+ userCanEdit: false,
+ userCanSave: false,
+ userCanShare: false,
+ lastModifiedTime: 0,
+});
+const editModeOnProps = {
+ ...createProps(),
+ editMode: true,
+};
+
+function setup(props: HeaderDropdownProps) {
+ return (
+
+
+
+ );
+}
+
+fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', {});
+
+async function openDropdown() {
+ const btn = screen.getByRole('img', { name: 'more-horiz' });
+ userEvent.click(btn);
+ expect(await screen.findByRole('menu')).toBeInTheDocument();
+}
+
+test('should render', () => {
+ const mockedProps = createProps();
+ const { container } = render(setup(mockedProps));
+ expect(container).toBeInTheDocument();
+});
+
+test('should render the dropdown button', () => {
+ const mockedProps = createProps();
+ render(setup(mockedProps));
+ expect(screen.getByRole('button')).toBeInTheDocument();
+});
+
+test('should render the dropdown icon', () => {
+ const mockedProps = createProps();
+ render(setup(mockedProps));
+ expect(screen.getByRole('img', { name: 'more-horiz' })).toBeInTheDocument();
+});
+
+test('should open the dropdown', async () => {
+ const mockedProps = createProps();
+ render(setup(mockedProps));
+ await openDropdown();
+ expect(await screen.findByRole('menu')).toBeInTheDocument();
+});
+
+test('should render the menu items', async () => {
+ const mockedProps = createProps();
+ render(setup(mockedProps));
+ await openDropdown();
+ expect(screen.getAllByRole('menuitem')).toHaveLength(4);
+ expect(screen.getByText('Refresh dashboard')).toBeInTheDocument();
+ expect(screen.getByText('Set auto-refresh interval')).toBeInTheDocument();
+ expect(screen.getByText('Download as image')).toBeInTheDocument();
+ expect(screen.getByText('Toggle fullscreen')).toBeInTheDocument();
+});
+
+test('should render the menu items in edit mode', async () => {
+ render(setup(editModeOnProps));
+ await openDropdown();
+ expect(screen.getAllByRole('menuitem')).toHaveLength(5);
+ expect(screen.getByText('Refresh dashboard')).toBeInTheDocument();
+ expect(screen.getByText('Set auto-refresh interval')).toBeInTheDocument();
+ expect(screen.getByText('Set filter mapping')).toBeInTheDocument();
+ expect(screen.getByText('Edit dashboard properties')).toBeInTheDocument();
+ expect(screen.getByText('Edit CSS')).toBeInTheDocument();
+});
+
+test('should show the share actions', async () => {
+ const mockedProps = createProps();
+ const canShareProps = {
+ ...mockedProps,
+ userCanShare: true,
+ };
+ render(setup(canShareProps));
+ await openDropdown();
+ expect(screen.getByText('Copy dashboard URL')).toBeInTheDocument();
+ expect(screen.getByText('Share dashboard by email')).toBeInTheDocument();
+});
+
+test('should render the "Save Modal" when user can save', async () => {
+ const mockedProps = createProps();
+ const canSaveProps = {
+ ...mockedProps,
+ userCanSave: true,
+ };
+ render(setup(canSaveProps));
+ await openDropdown();
+ expect(screen.getByText('Save as')).toBeInTheDocument();
+});
+
+test('should NOT render the "Save Modal" menu item when user cannot save', async () => {
+ const mockedProps = createProps();
+ render(setup(mockedProps));
+ await openDropdown();
+ expect(screen.queryByText('Save as')).not.toBeInTheDocument();
+});
+
+test('should render the "Refresh dashboard" menu item as disabled when loading', async () => {
+ const mockedProps = createProps();
+ const loadingProps = {
+ ...mockedProps,
+ isLoading: true,
+ };
+ render(setup(loadingProps));
+ await openDropdown();
+ expect(screen.getByText('Refresh dashboard')).toHaveClass(
+ 'ant-dropdown-menu-item-disabled',
+ );
+});
+
+test('should NOT render the "Refresh dashboard" menu item as disabled', async () => {
+ const mockedProps = createProps();
+ render(setup(mockedProps));
+ await openDropdown();
+ expect(screen.getByText('Refresh dashboard')).not.toHaveClass(
+ 'ant-dropdown-menu-item-disabled',
+ );
+});
+
+test('should render with custom css', () => {
+ const mockedProps = createProps();
+ render(setup(mockedProps));
+ expect(screen.getByRole('button')).toHaveStyle('margin-left: 100px');
+});
+
+test('should refresh the charts', async () => {
+ const mockedProps = createProps();
+ render(setup(mockedProps));
+ await openDropdown();
+ userEvent.click(screen.getByText('Refresh dashboard'));
+ expect(mockedProps.forceRefreshAllCharts).toHaveBeenCalledTimes(1);
+});
+
+test('should show the properties modal', async () => {
+ render(setup(editModeOnProps));
+ await openDropdown();
+ userEvent.click(screen.getByText('Edit dashboard properties'));
+ expect(editModeOnProps.showPropertiesModal).toHaveBeenCalledTimes(1);
+});
diff --git a/superset-frontend/src/dashboard/components/HeaderActionsDropdown.jsx b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx
similarity index 93%
rename from superset-frontend/src/dashboard/components/HeaderActionsDropdown.jsx
rename to superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx
index ba255fb3224..607fcdfd9d2 100644
--- a/superset-frontend/src/dashboard/components/HeaderActionsDropdown.jsx
+++ b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx
@@ -25,16 +25,16 @@ import { Menu, NoAnimationDropdown } from 'src/common/components';
import Icon from 'src/components/Icon';
import { URL_PARAMS } from 'src/constants';
import ShareMenuItems from 'src/dashboard/components/menu/ShareMenuItems';
-import CssEditor from './CssEditor';
-import RefreshIntervalModal from './RefreshIntervalModal';
-import SaveModal from './SaveModal';
-import injectCustomCss from '../util/injectCustomCss';
-import { SAVE_TYPE_NEWDASHBOARD } from '../util/constants';
-import FilterScopeModal from './filterscope/FilterScopeModal';
-import downloadAsImage from '../../utils/downloadAsImage';
-import getDashboardUrl from '../util/getDashboardUrl';
-import { getActiveFilters } from '../util/activeDashboardFilters';
-import { getUrlParam } from '../../utils/urlUtils';
+import CssEditor from 'src/dashboard/components/CssEditor';
+import RefreshIntervalModal from 'src/dashboard/components/RefreshIntervalModal';
+import SaveModal from 'src/dashboard/components/SaveModal';
+import injectCustomCss from 'src/dashboard/util/injectCustomCss';
+import { SAVE_TYPE_NEWDASHBOARD } from 'src/dashboard/util/constants';
+import FilterScopeModal from 'src/dashboard/components/filterscope/FilterScopeModal';
+import downloadAsImage from 'src/utils/downloadAsImage';
+import getDashboardUrl from 'src/dashboard/util/getDashboardUrl';
+import { getActiveFilters } from 'src/dashboard/util/activeDashboardFilters';
+import { getUrlParam } from 'src/utils/urlUtils';
const propTypes = {
addSuccessToast: PropTypes.func.isRequired,
diff --git a/superset-frontend/src/dashboard/components/Header.jsx b/superset-frontend/src/dashboard/components/Header/index.jsx
similarity index 97%
rename from superset-frontend/src/dashboard/components/Header.jsx
rename to superset-frontend/src/dashboard/components/Header/index.jsx
index 1e5d1868fe6..6befcbbc789 100644
--- a/superset-frontend/src/dashboard/components/Header.jsx
+++ b/superset-frontend/src/dashboard/components/Header/index.jsx
@@ -34,19 +34,18 @@ import Button from 'src/components/Button';
import EditableTitle from 'src/components/EditableTitle';
import FaveStar from 'src/components/FaveStar';
import { safeStringify } from 'src/utils/safeStringify';
+import HeaderActionsDropdown from 'src/dashboard/components/Header/HeaderActionsDropdown';
+import PublishedStatus from 'src/dashboard/components/PublishedStatus';
+import UndoRedoKeyListeners from 'src/dashboard/components/UndoRedoKeyListeners';
+import PropertiesModal from 'src/dashboard/components/PropertiesModal';
import { chartPropShape } from 'src/dashboard/util/propShapes';
-import HeaderActionsDropdown from './HeaderActionsDropdown';
-import PublishedStatus from './PublishedStatus';
-import UndoRedoKeyListeners from './UndoRedoKeyListeners';
-import PropertiesModal from './PropertiesModal';
-
import {
UNDO_LIMIT,
SAVE_TYPE_OVERWRITE,
DASHBOARD_POSITION_DATA_LIMIT,
-} from '../util/constants';
-import setPeriodicRunner from '../util/setPeriodicRunner';
-import { options as PeriodicRefreshOptions } from './RefreshIntervalModal';
+} from 'src/dashboard/util/constants';
+import setPeriodicRunner from 'src/dashboard/util/setPeriodicRunner';
+import { options as PeriodicRefreshOptions } from 'src/dashboard/components/RefreshIntervalModal';
const propTypes = {
addSuccessToast: PropTypes.func.isRequired,
diff --git a/superset-frontend/src/dashboard/components/Header/types.ts b/superset-frontend/src/dashboard/components/Header/types.ts
new file mode 100644
index 00000000000..5580136cb83
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/Header/types.ts
@@ -0,0 +1,98 @@
+/**
+ * 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 { Layout } from 'src/dashboard/types';
+import { ChartState } from 'src/explore/types';
+
+interface DashboardInfo {
+ id: number;
+ userId: number;
+ dash_edit_perm: boolean;
+ dash_save_perm: boolean;
+ metadata?: Record;
+ common?: { conf: Record };
+}
+
+export interface HeaderDropdownProps {
+ addSuccessToast: () => void;
+ addDangerToast: () => void;
+ customCss: string;
+ colorNamespace?: string;
+ colorScheme?: string;
+ dashboardId: number;
+ dashboardInfo: DashboardInfo;
+ dashboardTitle: string;
+ editMode: boolean;
+ expandedSlices: Record;
+ forceRefreshAllCharts: () => void;
+ hasUnsavedChanges: boolean;
+ isLoading: boolean;
+ layout: Layout;
+ onChange: () => void;
+ onSave: () => void;
+ refreshFrequency: number;
+ setRefreshFrequency: () => void;
+ shouldPersistRefreshFrequency: boolean;
+ showPropertiesModal: () => void;
+ startPeriodicRender: () => void;
+ updateCss: () => void;
+ userCanEdit: boolean;
+ userCanSave: boolean;
+ lastModifiedTime: number;
+}
+
+export interface HeaderProps {
+ addSuccessToast: () => void;
+ addDangerToast: () => void;
+ addWarningToast: () => void;
+ colorNamespace?: string;
+ charts: ChartState | {};
+ colorScheme?: string;
+ customCss: string;
+ dashboardInfo: DashboardInfo;
+ dashboardTitle: string;
+ setColorSchemeAndUnsavedChanges: () => void;
+ isStarred: boolean;
+ isPublished: boolean;
+ onChange: () => void;
+ onSave: () => void;
+ fetchFaveStar: () => void;
+ saveFaveStar: () => void;
+ savePublished: () => void;
+ updateDashboardTitle: () => void;
+ editMode: boolean;
+ setEditMode: () => void;
+ showBuilderPane: () => void;
+ updateCss: () => void;
+ logEvent: () => void;
+ hasUnsavedChanges: boolean;
+ maxUndoHistoryExceeded: boolean;
+ lastModifiedTime: number;
+ onUndo: () => void;
+ onRedo: () => void;
+ undoLength: number;
+ redoLength: number;
+ setMaxUndoHistoryExceeded: () => void;
+ maxUndoHistoryToast: () => void;
+ refreshFrequency: number;
+ shouldPersistRefreshFrequency: boolean;
+ setRefreshFrequency: () => void;
+ dashboardInfoChanged: () => void;
+ dashboardTitleChanged: () => void;
+}