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 fa60986cea..0000000000 --- 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 e5a7f52e74..0000000000 --- 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 0000000000..d5393b2275 --- /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 0000000000..039c1ef97c --- /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 ba255fb322..607fcdfd9d 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 1e5d1868fe..6befcbbc78 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 0000000000..5580136cb8 --- /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; +}