From 7ed5e2f2966a3d80fec868f4e808164b2cb27d4a Mon Sep 17 00:00:00 2001 From: geido Date: Tue, 6 Apr 2021 19:41:35 +0300 Subject: [PATCH 01/10] Add tests for HeaderActionsDropdown - WIP --- .../HeaderActionsDropdown.test.tsx | 162 ++++++++++++++++++ .../HeaderActionsDropdown_spec.jsx | 2 +- .../HeaderActionsDropdown/index.jsx} | 23 +-- .../components/Header}/Header_spec.jsx | 4 +- .../{Header.jsx => Header/index.jsx} | 16 +- 5 files changed, 185 insertions(+), 22 deletions(-) create mode 100644 superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx rename superset-frontend/{spec/javascripts/dashboard/components => src/dashboard/components/Header/HeaderActionsDropdown}/HeaderActionsDropdown_spec.jsx (98%) rename superset-frontend/src/dashboard/components/{HeaderActionsDropdown.jsx => Header/HeaderActionsDropdown/index.jsx} (93%) rename superset-frontend/{spec/javascripts/dashboard/components => src/dashboard/components/Header}/Header_spec.jsx (98%) rename superset-frontend/src/dashboard/components/{Header.jsx => Header/index.jsx} (96%) 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 0000000000000..7955af2dfa35d --- /dev/null +++ b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx @@ -0,0 +1,162 @@ +/** + * 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 HeaderActionsDropdown from '.'; + +const mockedProps = { + addSuccessToast: jest.fn(), + addDangerToast: jest.fn(), + customCss: '#save-dash-split-button{margin-left: 100px;}', + dashboardId: 1, + dashboardInfo: {}, + dashboardTitle: 'Title', + editMode: true, + expandedSlices: {}, + filters: {}, + forceRefreshAllCharts: jest.fn(), + hasUnsavedChanges: false, + isLoading: false, + layout: {}, + onChange: jest.fn(), + onSave: jest.fn(), + refreshFrequency: 200, + setRefreshFrequency: jest.fn(), + shouldPersistRefreshFrequency: true, + showPropertiesModal: jest.fn(), + startPeriodicRender: jest.fn(), + updateCss: jest.fn(), + userCanEdit: true, + userCanSave: true, + lastModifiedTime: 0, +}; + +const editModeOffProps = { + ...mockedProps, + editMode: false, +}; + +fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', {}); + +async function openDropdown() { + const btn = screen.getByRole('button'); + userEvent.click(btn); + expect(await screen.findByRole('menu')).toBeInTheDocument(); +} + +test('should render', () => { + const { container } = render(); + expect(container).toBeInTheDocument(); +}); + +test('should render the dropdown button', () => { + render(); + expect(screen.getByRole('button')).toBeInTheDocument(); +}); + +test('should render the dropdown icon', () => { + render(); + expect(screen.getByRole('img', { name: 'more-horiz' })).toBeInTheDocument(); +}); + +test('should open the dropdown', async () => { + render(); + await openDropdown(); + expect(await screen.findByRole('menu')).toBeInTheDocument(); +}); + +test('should render the menu items in edit mode', async () => { + render(); + await openDropdown(); + expect(screen.getAllByRole('menuitem')).toHaveLength(8); + expect(screen.getByText('Save as')).toBeInTheDocument(); + expect(screen.getByText('Copy dashboard URL')).toBeInTheDocument(); + expect(screen.getByText('Share dashboard by email')).toBeInTheDocument(); + 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 render the menu items when not in edit mode', async () => { + render(); + await openDropdown(); + expect(screen.getAllByRole('menuitem')).toHaveLength(7); + expect(screen.getByText('Save as')).toBeInTheDocument(); + expect(screen.getByText('Copy dashboard URL')).toBeInTheDocument(); + expect(screen.getByText('Share dashboard by email')).toBeInTheDocument(); + 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 "Save Modal" when user can save', async () => { + render(); + await openDropdown(); + expect(screen.getByText('Save as')).toBeInTheDocument(); +}); + +test('should NOT render the "Save Modal" menu item when user cannot save', async () => { + const cannotSaveProps = { + ...mockedProps, + userCanSave: false, + }; + render(); + await openDropdown(); + expect(screen.queryByText('Save as')).not.toBeInTheDocument(); +}); + +test('should render the "Refresh dashboard" menu item as disabled when loading', async () => { + const loadingProps = { + ...mockedProps, + isLoading: true, + }; + render(); + await openDropdown(); + expect(screen.getByText('Refresh dashboard')).toHaveClass( + 'ant-dropdown-menu-item-disabled', + ); +}); + +test('should render with custom css', () => { + const loadingProps = { + ...mockedProps, + isLoading: true, + }; + render(); + expect(screen.getByRole('button')).toHaveStyle('margin-left: 100px'); +}); + +test('should refresh the charts', async () => { + render(); + await openDropdown(); + userEvent.click(screen.getByText('Refresh dashboard')); + expect(mockedProps.forceRefreshAllCharts).toHaveBeenCalledTimes(1); +}); + +test('should show the properties modal', async () => { + render(); + await openDropdown(); + userEvent.click(screen.getByText('Edit dashboard properties')); + expect(mockedProps.showPropertiesModal).toHaveBeenCalledTimes(1); +}); diff --git a/superset-frontend/spec/javascripts/dashboard/components/HeaderActionsDropdown_spec.jsx b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown_spec.jsx similarity index 98% rename from superset-frontend/spec/javascripts/dashboard/components/HeaderActionsDropdown_spec.jsx rename to superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown_spec.jsx index ea3b65ca28eb8..9379284192235 100644 --- a/superset-frontend/spec/javascripts/dashboard/components/HeaderActionsDropdown_spec.jsx +++ b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown_spec.jsx @@ -20,11 +20,11 @@ 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'; +import HeaderActionsDropdown from '.'; fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', {}); 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 5d27f1f7f5917..a3d0343bfb61c 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, @@ -312,9 +312,10 @@ class HeaderActionsDropdown extends React.PureComponent { triggerNode.closest(SCREENSHOT_NODE_SELECTOR) - } + }*/ > diff --git a/superset-frontend/spec/javascripts/dashboard/components/Header_spec.jsx b/superset-frontend/src/dashboard/components/Header/Header_spec.jsx similarity index 98% rename from superset-frontend/spec/javascripts/dashboard/components/Header_spec.jsx rename to superset-frontend/src/dashboard/components/Header/Header_spec.jsx index 5e040dd5b6a83..c427a99169147 100644 --- a/superset-frontend/spec/javascripts/dashboard/components/Header_spec.jsx +++ b/superset-frontend/src/dashboard/components/Header/Header_spec.jsx @@ -18,13 +18,13 @@ */ import React from 'react'; import { shallow } from 'enzyme'; -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'; +import HeaderActionsDropdown from './HeaderActionsDropdown'; +import Header from '.'; describe('Header', () => { const props = { diff --git a/superset-frontend/src/dashboard/components/Header.jsx b/superset-frontend/src/dashboard/components/Header/index.jsx similarity index 96% rename from superset-frontend/src/dashboard/components/Header.jsx rename to superset-frontend/src/dashboard/components/Header/index.jsx index 8ad6c57e69ad1..cce1b1eceb297 100644 --- a/superset-frontend/src/dashboard/components/Header.jsx +++ b/superset-frontend/src/dashboard/components/Header/index.jsx @@ -35,19 +35,19 @@ import EditableTitle from 'src/components/EditableTitle'; import FaveStar from 'src/components/FaveStar'; import { safeStringify } from 'src/utils/safeStringify'; -import HeaderActionsDropdown from './HeaderActionsDropdown'; -import PublishedStatus from './PublishedStatus'; -import UndoRedoKeylisteners from './UndoRedoKeylisteners'; -import PropertiesModal from './PropertiesModal'; +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 '../util/propShapes'; +import { chartPropShape } from 'src/dashboard/util/propShapes'; 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, From f5e586af255b172208a242c2ed77e66ac549c8fc Mon Sep 17 00:00:00 2001 From: geido Date: Wed, 7 Apr 2021 15:45:53 +0300 Subject: [PATCH 02/10] Fix trigger node --- .../HeaderActionsDropdown.test.tsx | 37 ++++++++++--------- .../Header/HeaderActionsDropdown/index.jsx | 3 +- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx index 7955af2dfa35d..5842ef770b4c5 100644 --- a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx +++ b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx @@ -48,12 +48,19 @@ const mockedProps = { userCanSave: true, lastModifiedTime: 0, }; - const editModeOffProps = { ...mockedProps, editMode: false, }; +function setup(props: any) { + return ( +
+ +
+ ); +} + fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', {}); async function openDropdown() { @@ -63,28 +70,28 @@ async function openDropdown() { } test('should render', () => { - const { container } = render(); + const { container } = render(setup(mockedProps)); expect(container).toBeInTheDocument(); }); test('should render the dropdown button', () => { - render(); + render(setup(mockedProps)); expect(screen.getByRole('button')).toBeInTheDocument(); }); test('should render the dropdown icon', () => { - render(); + render(setup(mockedProps)); expect(screen.getByRole('img', { name: 'more-horiz' })).toBeInTheDocument(); }); test('should open the dropdown', async () => { - render(); + render(setup(mockedProps)); await openDropdown(); expect(await screen.findByRole('menu')).toBeInTheDocument(); }); test('should render the menu items in edit mode', async () => { - render(); + render(setup(mockedProps)); await openDropdown(); expect(screen.getAllByRole('menuitem')).toHaveLength(8); expect(screen.getByText('Save as')).toBeInTheDocument(); @@ -98,7 +105,7 @@ test('should render the menu items in edit mode', async () => { }); test('should render the menu items when not in edit mode', async () => { - render(); + render(setup(editModeOffProps)); await openDropdown(); expect(screen.getAllByRole('menuitem')).toHaveLength(7); expect(screen.getByText('Save as')).toBeInTheDocument(); @@ -111,7 +118,7 @@ test('should render the menu items when not in edit mode', async () => { }); test('should render the "Save Modal" when user can save', async () => { - render(); + render(setup(mockedProps)); await openDropdown(); expect(screen.getByText('Save as')).toBeInTheDocument(); }); @@ -121,7 +128,7 @@ test('should NOT render the "Save Modal" menu item when user cannot save', async ...mockedProps, userCanSave: false, }; - render(); + render(setup(cannotSaveProps)); await openDropdown(); expect(screen.queryByText('Save as')).not.toBeInTheDocument(); }); @@ -131,7 +138,7 @@ test('should render the "Refresh dashboard" menu item as disabled when loading', ...mockedProps, isLoading: true, }; - render(); + render(setup(loadingProps)); await openDropdown(); expect(screen.getByText('Refresh dashboard')).toHaveClass( 'ant-dropdown-menu-item-disabled', @@ -139,23 +146,19 @@ test('should render the "Refresh dashboard" menu item as disabled when loading', }); test('should render with custom css', () => { - const loadingProps = { - ...mockedProps, - isLoading: true, - }; - render(); + render(setup(mockedProps)); expect(screen.getByRole('button')).toHaveStyle('margin-left: 100px'); }); test('should refresh the charts', async () => { - render(); + render(setup(mockedProps)); await openDropdown(); userEvent.click(screen.getByText('Refresh dashboard')); expect(mockedProps.forceRefreshAllCharts).toHaveBeenCalledTimes(1); }); test('should show the properties modal', async () => { - render(); + render(setup(mockedProps)); await openDropdown(); userEvent.click(screen.getByText('Edit dashboard properties')); expect(mockedProps.showPropertiesModal).toHaveBeenCalledTimes(1); diff --git a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx index a3d0343bfb61c..a70756452b3ad 100644 --- a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx +++ b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx @@ -312,10 +312,9 @@ class HeaderActionsDropdown extends React.PureComponent { triggerNode.closest(SCREENSHOT_NODE_SELECTOR) - }*/ + } > From 5a958d6adfa5b759f4740fb416a9ca4ade5d32ad Mon Sep 17 00:00:00 2001 From: geido Date: Wed, 7 Apr 2021 16:29:24 +0300 Subject: [PATCH 03/10] Add types --- .../components/Header/Header.test.tsx | 87 +++++++++++++++++++ .../HeaderActionsDropdown.test.tsx | 15 +++- .../src/dashboard/components/Header/types.ts | 79 +++++++++++++++++ 3 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 superset-frontend/src/dashboard/components/Header/Header.test.tsx create mode 100644 superset-frontend/src/dashboard/components/Header/types.ts 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 0000000000000..f5e5ba0c6c8ea --- /dev/null +++ b/superset-frontend/src/dashboard/components/Header/Header.test.tsx @@ -0,0 +1,87 @@ +/** + * 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 { HeaderProps } from './types'; +import Header from '.'; + +const mockedProps = { + addSuccessToast: jest.fn(), + addDangerToast: jest.fn(), + addWarningToast: jest.fn(), + dashboardInfo: { + id: 1, + dash_edit_perm: true, + dash_save_perm: true, + userId: 1, + metadata: {}, + common: { + conf: {}, + }, + }, + dashboardTitle: '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(), +}; + +fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', {}); + +function setup(props: HeaderProps) { + return
; +} + +test('should render', () => { + const { container } = render(setup(mockedProps)); + expect(container).toBeInTheDocument(); +}); diff --git a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx index 5842ef770b4c5..c61915411f309 100644 --- a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx +++ b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx @@ -20,6 +20,7 @@ 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 mockedProps = { @@ -27,11 +28,19 @@ const mockedProps = { addDangerToast: jest.fn(), customCss: '#save-dash-split-button{margin-left: 100px;}', dashboardId: 1, - dashboardInfo: {}, + dashboardInfo: { + id: 1, + dash_edit_perm: true, + dash_save_perm: true, + userId: 1, + metadata: {}, + common: { + conf: {}, + }, + }, dashboardTitle: 'Title', editMode: true, expandedSlices: {}, - filters: {}, forceRefreshAllCharts: jest.fn(), hasUnsavedChanges: false, isLoading: false, @@ -53,7 +62,7 @@ const editModeOffProps = { editMode: false, }; -function setup(props: any) { +function setup(props: HeaderDropdownProps) { return (
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 0000000000000..47ce82f4b42e5 --- /dev/null +++ b/superset-frontend/src/dashboard/components/Header/types.ts @@ -0,0 +1,79 @@ +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; +} From a98c71bbbb51eaf781049ca89c3557336b9029dc Mon Sep 17 00:00:00 2001 From: geido Date: Wed, 7 Apr 2021 16:38:35 +0300 Subject: [PATCH 04/10] Clean up --- .../HeaderActionsDropdown.test.tsx | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx index c61915411f309..b4e7123c81cc0 100644 --- a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx +++ b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx @@ -39,7 +39,7 @@ const mockedProps = { }, }, dashboardTitle: 'Title', - editMode: true, + editMode: false, expandedSlices: {}, forceRefreshAllCharts: jest.fn(), hasUnsavedChanges: false, @@ -49,17 +49,17 @@ const mockedProps = { onSave: jest.fn(), refreshFrequency: 200, setRefreshFrequency: jest.fn(), - shouldPersistRefreshFrequency: true, + shouldPersistRefreshFrequency: false, showPropertiesModal: jest.fn(), startPeriodicRender: jest.fn(), updateCss: jest.fn(), - userCanEdit: true, - userCanSave: true, + userCanEdit: false, + userCanSave: false, lastModifiedTime: 0, }; -const editModeOffProps = { +const editModeOnProps = { ...mockedProps, - editMode: false, + editMode: true, }; function setup(props: HeaderDropdownProps) { @@ -99,45 +99,43 @@ test('should open the dropdown', async () => { expect(await screen.findByRole('menu')).toBeInTheDocument(); }); -test('should render the menu items in edit mode', async () => { +test('should render the menu items', async () => { render(setup(mockedProps)); await openDropdown(); - expect(screen.getAllByRole('menuitem')).toHaveLength(8); - expect(screen.getByText('Save as')).toBeInTheDocument(); + expect(screen.getAllByRole('menuitem')).toHaveLength(6); expect(screen.getByText('Copy dashboard URL')).toBeInTheDocument(); expect(screen.getByText('Share dashboard by email')).toBeInTheDocument(); 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(); + expect(screen.getByText('Download as image')).toBeInTheDocument(); + expect(screen.getByText('Toggle fullscreen')).toBeInTheDocument(); }); -test('should render the menu items when not in edit mode', async () => { - render(setup(editModeOffProps)); +test('should render the menu items in edit mode', async () => { + render(setup(editModeOnProps)); await openDropdown(); expect(screen.getAllByRole('menuitem')).toHaveLength(7); - expect(screen.getByText('Save as')).toBeInTheDocument(); expect(screen.getByText('Copy dashboard URL')).toBeInTheDocument(); expect(screen.getByText('Share dashboard by email')).toBeInTheDocument(); 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(); + expect(screen.getByText('Set filter mapping')).toBeInTheDocument(); + expect(screen.getByText('Edit dashboard properties')).toBeInTheDocument(); + expect(screen.getByText('Edit CSS')).toBeInTheDocument(); }); test('should render the "Save Modal" when user can save', async () => { - render(setup(mockedProps)); + 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 cannotSaveProps = { - ...mockedProps, - userCanSave: false, - }; - render(setup(cannotSaveProps)); + render(setup(mockedProps)); await openDropdown(); expect(screen.queryByText('Save as')).not.toBeInTheDocument(); }); @@ -167,7 +165,7 @@ test('should refresh the charts', async () => { }); test('should show the properties modal', async () => { - render(setup(mockedProps)); + render(setup(editModeOnProps)); await openDropdown(); userEvent.click(screen.getByText('Edit dashboard properties')); expect(mockedProps.showPropertiesModal).toHaveBeenCalledTimes(1); From 00aceb306017bf29444062108b6e84c992967a9c Mon Sep 17 00:00:00 2001 From: geido Date: Wed, 7 Apr 2021 19:03:02 +0300 Subject: [PATCH 05/10] Add tests for Header --- .../components/Header/Header.test.tsx | 199 +++++++++++++++++- .../HeaderActionsDropdown.test.tsx | 2 +- 2 files changed, 195 insertions(+), 6 deletions(-) diff --git a/superset-frontend/src/dashboard/components/Header/Header.test.tsx b/superset-frontend/src/dashboard/components/Header/Header.test.tsx index f5e5ba0c6c8ea..4671cfa26cf08 100644 --- a/superset-frontend/src/dashboard/components/Header/Header.test.tsx +++ b/superset-frontend/src/dashboard/components/Header/Header.test.tsx @@ -17,7 +17,7 @@ * under the License. */ import React from 'react'; -import { render, screen } from 'spec/helpers/testing-library'; +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'; @@ -29,15 +29,15 @@ const mockedProps = { addWarningToast: jest.fn(), dashboardInfo: { id: 1, - dash_edit_perm: true, - dash_save_perm: true, + dash_edit_perm: false, + dash_save_perm: false, userId: 1, metadata: {}, common: { conf: {}, }, }, - dashboardTitle: 'Title', + dashboardTitle: 'Dashboard Title', charts: {}, layout: {}, expandedSlices: {}, @@ -74,14 +74,203 @@ const mockedProps = { dashboardInfoChanged: jest.fn(), dashboardTitleChanged: jest.fn(), }; +const editableProps = { + ...mockedProps, + editMode: true, + dashboardInfo: { + ...mockedProps.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
; + 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 { container } = render(setup(mockedProps)); expect(container).toBeInTheDocument(); }); + +test('should render the title', () => { + 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(mockedProps.onChange).not.toHaveBeenCalled(); + userEvent.click(editableTitle); + userEvent.clear(editableTitle); + userEvent.type(editableTitle, 'New Title'); + userEvent.click(document.body); + expect(mockedProps.onChange).toHaveBeenCalled(); + expect(screen.getByDisplayValue('New Title')).toBeInTheDocument(); +}); + +test('should render the "Draft" status', () => { + render(setup(mockedProps)); + expect(screen.getByText('Draft')).toBeInTheDocument(); +}); + +test('should publish', () => { + render(setup(editableProps)); + const draft = screen.getByText('Draft'); + expect(mockedProps.savePublished).not.toHaveBeenCalled(); + userEvent.click(draft); + expect(mockedProps.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(mockedProps.onUndo).not.toHaveBeenCalled(); + userEvent.click(undo); + expect(mockedProps.onUndo).toHaveBeenCalledTimes(1); +}); + +test('should undo with key listener', () => { + undoProps.onUndo.mockReset(); + render(setup(undoProps)); + expect(mockedProps.onUndo).not.toHaveBeenCalled(); + fireEvent.keyDown(document.body, { key: 'z', code: 'KeyZ', ctrlKey: true }); + expect(mockedProps.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(mockedProps.onRedo).not.toHaveBeenCalled(); + userEvent.click(redo); + expect(mockedProps.onRedo).toHaveBeenCalledTimes(1); +}); + +test('should redo with key listener', () => { + undoProps.onRedo.mockReset(); + render(setup(redoProps)); + expect(mockedProps.onRedo).not.toHaveBeenCalled(); + fireEvent.keyDown(document.body, { key: 'y', code: 'KeyY', ctrlKey: true }); + expect(mockedProps.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(mockedProps.onSave).not.toHaveBeenCalled(); + userEvent.click(save); + expect(mockedProps.onSave).toHaveBeenCalledTimes(1); +}); + +test('should NOT render the "Draft" status', () => { + const publishedProps = { + ...mockedProps, + isPublished: true, + }; + render(setup(publishedProps)); + expect(screen.queryByText('Draft')).not.toBeInTheDocument(); +}); + +test('should render the unselected fave icon', () => { + render(setup(mockedProps)); + expect(mockedProps.fetchFaveStar).toHaveBeenCalled(); + expect( + screen.getByRole('img', { name: 'favorite-unselected' }), + ).toBeInTheDocument(); +}); + +test('should render the selected fave icon', () => { + const favedProps = { + ...mockedProps, + isStarred: true, + }; + render(setup(favedProps)); + expect( + screen.getByRole('img', { name: 'favorite-selected' }), + ).toBeInTheDocument(); +}); + +test('should fave', async () => { + 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 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', () => { + render(setup(mockedProps)); + expect(screen.getByRole('img', { name: 'more-horiz' })).toBeInTheDocument(); +}); + +test('should refresh the charts', async () => { + 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 index b4e7123c81cc0..3c8b0ac982979 100644 --- a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx +++ b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx @@ -73,7 +73,7 @@ function setup(props: HeaderDropdownProps) { fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', {}); async function openDropdown() { - const btn = screen.getByRole('button'); + const btn = screen.getByRole('img', { name: 'more-horiz' }); userEvent.click(btn); expect(await screen.findByRole('menu')).toBeInTheDocument(); } From d681a15534bc1c9f799b282e73d33d6a6b726d9d Mon Sep 17 00:00:00 2001 From: geido Date: Wed, 7 Apr 2021 19:06:09 +0300 Subject: [PATCH 06/10] Delete obsolete tests --- .../HeaderActionsDropdown_spec.jsx | 170 ------------ .../components/Header/Header_spec.jsx | 244 ------------------ 2 files changed, 414 deletions(-) delete mode 100644 superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown_spec.jsx delete mode 100644 superset-frontend/src/dashboard/components/Header/Header_spec.jsx diff --git a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown_spec.jsx b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown_spec.jsx deleted file mode 100644 index 9379284192235..0000000000000 --- a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown_spec.jsx +++ /dev/null @@ -1,170 +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 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'; -import HeaderActionsDropdown from '.'; - -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 }; - - 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 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', () => { - const overrideProps = { userCanSave: 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 }; - - 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/src/dashboard/components/Header/Header_spec.jsx b/superset-frontend/src/dashboard/components/Header/Header_spec.jsx deleted file mode 100644 index c427a99169147..0000000000000 --- a/superset-frontend/src/dashboard/components/Header/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 { shallow } from 'enzyme'; -import EditableTitle from 'src/components/EditableTitle'; -import FaveStar from 'src/components/FaveStar'; -import PublishedStatus from 'src/dashboard/components/PublishedStatus'; -import Button from 'src/components/Button'; -import UndoRedoKeylisteners from 'src/dashboard/components/UndoRedoKeylisteners'; -import HeaderActionsDropdown from './HeaderActionsDropdown'; -import Header from '.'; - -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 = shallow(
); - 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(); - }); - }); -}); From 70cdbc9c835dee00cb3d4152298a7316bba396aa Mon Sep 17 00:00:00 2001 From: geido Date: Fri, 9 Apr 2021 19:45:29 +0300 Subject: [PATCH 07/10] Add factory and clean up --- .../components/Header/Header.test.tsx | 49 ++++++++++++------- .../HeaderActionsDropdown.test.tsx | 18 +++++-- .../src/dashboard/components/Header/types.ts | 19 +++++++ 3 files changed, 63 insertions(+), 23 deletions(-) diff --git a/superset-frontend/src/dashboard/components/Header/Header.test.tsx b/superset-frontend/src/dashboard/components/Header/Header.test.tsx index 4671cfa26cf08..fe8dd56a53dd6 100644 --- a/superset-frontend/src/dashboard/components/Header/Header.test.tsx +++ b/superset-frontend/src/dashboard/components/Header/Header.test.tsx @@ -23,7 +23,7 @@ import fetchMock from 'fetch-mock'; import { HeaderProps } from './types'; import Header from '.'; -const mockedProps = { +const createProps = () => ({ addSuccessToast: jest.fn(), addDangerToast: jest.fn(), addWarningToast: jest.fn(), @@ -73,12 +73,13 @@ const mockedProps = { maxUndoHistoryToast: jest.fn(), dashboardInfoChanged: jest.fn(), dashboardTitleChanged: jest.fn(), -}; +}); +const props = createProps(); const editableProps = { - ...mockedProps, + ...props, editMode: true, dashboardInfo: { - ...mockedProps.dashboardInfo, + ...props.dashboardInfo, dash_edit_perm: true, dash_save_perm: true, }, @@ -109,11 +110,13 @@ async function openActionsDropdown() { } 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(); }); @@ -126,16 +129,17 @@ test('should render the editable title', () => { test('should edit the title', () => { render(setup(editableProps)); const editableTitle = screen.getByDisplayValue('Dashboard Title'); - expect(mockedProps.onChange).not.toHaveBeenCalled(); + expect(editableProps.onChange).not.toHaveBeenCalled(); userEvent.click(editableTitle); userEvent.clear(editableTitle); userEvent.type(editableTitle, 'New Title'); userEvent.click(document.body); - expect(mockedProps.onChange).toHaveBeenCalled(); + 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(); }); @@ -143,9 +147,9 @@ test('should render the "Draft" status', () => { test('should publish', () => { render(setup(editableProps)); const draft = screen.getByText('Draft'); - expect(mockedProps.savePublished).not.toHaveBeenCalled(); + expect(editableProps.savePublished).not.toHaveBeenCalled(); userEvent.click(draft); - expect(mockedProps.savePublished).toHaveBeenCalledTimes(1); + expect(editableProps.savePublished).toHaveBeenCalledTimes(1); }); test('should render the "Undo" action as disabled', () => { @@ -156,17 +160,17 @@ test('should render the "Undo" action as disabled', () => { test('should undo', () => { render(setup(undoProps)); const undo = screen.getByTitle('Undo'); - expect(mockedProps.onUndo).not.toHaveBeenCalled(); + expect(undoProps.onUndo).not.toHaveBeenCalled(); userEvent.click(undo); - expect(mockedProps.onUndo).toHaveBeenCalledTimes(1); + expect(undoProps.onUndo).toHaveBeenCalledTimes(1); }); test('should undo with key listener', () => { undoProps.onUndo.mockReset(); render(setup(undoProps)); - expect(mockedProps.onUndo).not.toHaveBeenCalled(); + expect(undoProps.onUndo).not.toHaveBeenCalled(); fireEvent.keyDown(document.body, { key: 'z', code: 'KeyZ', ctrlKey: true }); - expect(mockedProps.onUndo).toHaveBeenCalledTimes(1); + expect(undoProps.onUndo).toHaveBeenCalledTimes(1); }); test('should render the "Redo" action as disabled', () => { @@ -177,17 +181,17 @@ test('should render the "Redo" action as disabled', () => { test('should redo', () => { render(setup(redoProps)); const redo = screen.getByTitle('Redo'); - expect(mockedProps.onRedo).not.toHaveBeenCalled(); + expect(redoProps.onRedo).not.toHaveBeenCalled(); userEvent.click(redo); - expect(mockedProps.onRedo).toHaveBeenCalledTimes(1); + expect(redoProps.onRedo).toHaveBeenCalledTimes(1); }); test('should redo with key listener', () => { - undoProps.onRedo.mockReset(); + redoProps.onRedo.mockReset(); render(setup(redoProps)); - expect(mockedProps.onRedo).not.toHaveBeenCalled(); + expect(redoProps.onRedo).not.toHaveBeenCalled(); fireEvent.keyDown(document.body, { key: 'y', code: 'KeyY', ctrlKey: true }); - expect(mockedProps.onRedo).toHaveBeenCalledTimes(1); + expect(redoProps.onRedo).toHaveBeenCalledTimes(1); }); test('should render the "Discard changes" button', () => { @@ -207,12 +211,13 @@ test('should save', () => { }; render(setup(unsavedProps)); const save = screen.getByText('Save'); - expect(mockedProps.onSave).not.toHaveBeenCalled(); + expect(unsavedProps.onSave).not.toHaveBeenCalled(); userEvent.click(save); - expect(mockedProps.onSave).toHaveBeenCalledTimes(1); + expect(unsavedProps.onSave).toHaveBeenCalledTimes(1); }); test('should NOT render the "Draft" status', () => { + const mockedProps = createProps(); const publishedProps = { ...mockedProps, isPublished: true, @@ -222,6 +227,7 @@ test('should NOT render the "Draft" status', () => { }); test('should render the unselected fave icon', () => { + const mockedProps = createProps(); render(setup(mockedProps)); expect(mockedProps.fetchFaveStar).toHaveBeenCalled(); expect( @@ -230,6 +236,7 @@ test('should render the unselected fave icon', () => { }); test('should render the selected fave icon', () => { + const mockedProps = createProps(); const favedProps = { ...mockedProps, isStarred: true, @@ -241,6 +248,7 @@ test('should render the selected fave icon', () => { }); test('should fave', async () => { + const mockedProps = createProps(); render(setup(mockedProps)); const fave = screen.getByRole('img', { name: 'favorite-unselected' }); expect(mockedProps.saveFaveStar).not.toHaveBeenCalled(); @@ -249,6 +257,7 @@ test('should fave', async () => { }); test('should toggle the edit mode', () => { + const mockedProps = createProps(); const canEditProps = { ...mockedProps, dashboardInfo: { @@ -264,11 +273,13 @@ test('should toggle the edit mode', () => { }); 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')); diff --git a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx index 3c8b0ac982979..b3476b6b6118d 100644 --- a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx +++ b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx @@ -23,7 +23,7 @@ import fetchMock from 'fetch-mock'; import { HeaderDropdownProps } from 'src/dashboard/components/Header/types'; import HeaderActionsDropdown from '.'; -const mockedProps = { +const createProps = () => ({ addSuccessToast: jest.fn(), addDangerToast: jest.fn(), customCss: '#save-dash-split-button{margin-left: 100px;}', @@ -56,9 +56,9 @@ const mockedProps = { userCanEdit: false, userCanSave: false, lastModifiedTime: 0, -}; +}); const editModeOnProps = { - ...mockedProps, + ...createProps(), editMode: true, }; @@ -79,27 +79,32 @@ async function openDropdown() { } 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(6); @@ -125,6 +130,7 @@ test('should render the menu items in edit mode', async () => { }); test('should render the "Save Modal" when user can save', async () => { + const mockedProps = createProps(); const canSaveProps = { ...mockedProps, userCanSave: true, @@ -135,12 +141,14 @@ test('should render the "Save Modal" when user can save', async () => { }); 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, @@ -153,11 +161,13 @@ test('should render the "Refresh dashboard" menu item as disabled when loading', }); 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')); @@ -168,5 +178,5 @@ test('should show the properties modal', async () => { render(setup(editModeOnProps)); await openDropdown(); userEvent.click(screen.getByText('Edit dashboard properties')); - expect(mockedProps.showPropertiesModal).toHaveBeenCalledTimes(1); + expect(editModeOnProps.showPropertiesModal).toHaveBeenCalledTimes(1); }); diff --git a/superset-frontend/src/dashboard/components/Header/types.ts b/superset-frontend/src/dashboard/components/Header/types.ts index 47ce82f4b42e5..5580136cb836e 100644 --- a/superset-frontend/src/dashboard/components/Header/types.ts +++ b/superset-frontend/src/dashboard/components/Header/types.ts @@ -1,3 +1,22 @@ +/** + * 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'; From cc8a07b717321e46de46befafd2efc0ba89c970d Mon Sep 17 00:00:00 2001 From: geido Date: Fri, 9 Apr 2021 19:49:51 +0300 Subject: [PATCH 08/10] Add opposite case --- .../HeaderActionsDropdown/HeaderActionsDropdown.test.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx index b3476b6b6118d..ac87d6d358d31 100644 --- a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx +++ b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx @@ -160,6 +160,15 @@ test('should render the "Refresh dashboard" menu item as disabled when loading', ); }); +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)); From 391ffb4c86889dc6d21c30c485e5e48815c236e2 Mon Sep 17 00:00:00 2001 From: geido Date: Tue, 13 Apr 2021 21:25:50 +0300 Subject: [PATCH 09/10] Fix file name --- superset-frontend/src/dashboard/components/Header/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset-frontend/src/dashboard/components/Header/index.jsx b/superset-frontend/src/dashboard/components/Header/index.jsx index 0b8e364f27141..6befcbbc789f4 100644 --- a/superset-frontend/src/dashboard/components/Header/index.jsx +++ b/superset-frontend/src/dashboard/components/Header/index.jsx @@ -36,7 +36,7 @@ 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 UndoRedoKeyListeners from 'src/dashboard/components/UndoRedoKeyListeners'; import PropertiesModal from 'src/dashboard/components/PropertiesModal'; import { chartPropShape } from 'src/dashboard/util/propShapes'; import { From a64fa0d64bebff8470696c70770ddca550f43a77 Mon Sep 17 00:00:00 2001 From: geido Date: Wed, 14 Apr 2021 13:09:16 +0300 Subject: [PATCH 10/10] Include latest changes --- .../components/Header/Header.test.tsx | 1 + .../HeaderActionsDropdown.test.tsx | 21 +++++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/superset-frontend/src/dashboard/components/Header/Header.test.tsx b/superset-frontend/src/dashboard/components/Header/Header.test.tsx index fe8dd56a53dd6..d5393b2275597 100644 --- a/superset-frontend/src/dashboard/components/Header/Header.test.tsx +++ b/superset-frontend/src/dashboard/components/Header/Header.test.tsx @@ -31,6 +31,7 @@ const createProps = () => ({ id: 1, dash_edit_perm: false, dash_save_perm: false, + dash_share_perm: false, userId: 1, metadata: {}, common: { diff --git a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx index ac87d6d358d31..039c1ef97c6d1 100644 --- a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx +++ b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx @@ -55,6 +55,7 @@ const createProps = () => ({ updateCss: jest.fn(), userCanEdit: false, userCanSave: false, + userCanShare: false, lastModifiedTime: 0, }); const editModeOnProps = { @@ -107,9 +108,7 @@ test('should render the menu items', async () => { const mockedProps = createProps(); render(setup(mockedProps)); await openDropdown(); - expect(screen.getAllByRole('menuitem')).toHaveLength(6); - expect(screen.getByText('Copy dashboard URL')).toBeInTheDocument(); - expect(screen.getByText('Share dashboard by email')).toBeInTheDocument(); + 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(); @@ -119,9 +118,7 @@ test('should render the menu items', async () => { test('should render the menu items in edit mode', async () => { render(setup(editModeOnProps)); await openDropdown(); - expect(screen.getAllByRole('menuitem')).toHaveLength(7); - expect(screen.getByText('Copy dashboard URL')).toBeInTheDocument(); - expect(screen.getByText('Share dashboard by email')).toBeInTheDocument(); + 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(); @@ -129,6 +126,18 @@ test('should render the menu items in edit mode', async () => { 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 = {