diff --git a/e2e/shared/smokeTestScenario.ts b/e2e/shared/smokeTestScenario.ts index 7c95681deb4..d5ee30ed4e9 100644 --- a/e2e/shared/smokeTestScenario.ts +++ b/e2e/shared/smokeTestScenario.ts @@ -10,7 +10,7 @@ export const smokeTestScenario = { scenario: () => { // wait for time to be set to account for any layout shift e2e().contains('2020-01-01 00:00:00 to 2020-01-01 06:00:00').should('be.visible'); - e2e.components.PageToolbar.itemButton('Add panel button').click(); + e2e.components.PageToolbar.itemButton('Add button').click(); e2e.components.PageToolbar.itemButton('Add new visualization menu item').click(); e2e.components.DataSource.TestData.QueryTab.scenarioSelectContainer() diff --git a/e2e/smoke-tests-suite/panels_smokescreen.spec.ts b/e2e/smoke-tests-suite/panels_smokescreen.spec.ts index 49a1db6687d..a914014066f 100644 --- a/e2e/smoke-tests-suite/panels_smokescreen.spec.ts +++ b/e2e/smoke-tests-suite/panels_smokescreen.spec.ts @@ -12,12 +12,12 @@ e2e.scenario({ // TODO: Try and use e2e.flows.addPanel() instead of block below try { - e2e.components.PageToolbar.itemButton('Add panel button').should('be.visible'); - e2e.components.PageToolbar.itemButton('Add panel button').click(); + e2e.components.PageToolbar.itemButton('Add button').should('be.visible'); + e2e.components.PageToolbar.itemButton('Add button').click(); } catch (e) { // Depending on the screen size, the "Add panel" button might be hidden e2e.components.PageToolbar.item('Show more items').click(); - e2e.components.PageToolbar.item('Add panel button').last().click(); + e2e.components.PageToolbar.item('Add button').last().click(); } e2e.pages.AddDashboard.itemButton('Add new visualization menu item').should('be.visible'); e2e.pages.AddDashboard.itemButton('Add new visualization menu item').click(); diff --git a/packages/grafana-e2e/src/flows/addDashboard.ts b/packages/grafana-e2e/src/flows/addDashboard.ts index c0deeff25fa..2526a03422e 100644 --- a/packages/grafana-e2e/src/flows/addDashboard.ts +++ b/packages/grafana-e2e/src/flows/addDashboard.ts @@ -144,7 +144,7 @@ export const addDashboard = (config?: Partial) => { e2e.pages.SaveDashboardAsModal.newName().clear().type(title, { force: true }); e2e.pages.SaveDashboardAsModal.save().click(); e2e.flows.assertSuccessNotification(); - e2e.pages.AddDashboard.addNewPanel().should('be.visible'); + e2e.pages.AddDashboard.itemButton('Create new panel button').should('be.visible'); e2e().logToConsole('Added dashboard with title:', title); diff --git a/packages/grafana-e2e/src/flows/configurePanel.ts b/packages/grafana-e2e/src/flows/configurePanel.ts index 54125a20a56..59bc72216db 100644 --- a/packages/grafana-e2e/src/flows/configurePanel.ts +++ b/packages/grafana-e2e/src/flows/configurePanel.ts @@ -92,12 +92,12 @@ export const configurePanel = (config: PartialAddPanelConfig | PartialEditPanelC e2e.components.Panels.Panel.headerItems('Edit').click(); } else { try { - e2e.components.PageToolbar.itemButton('Add panel button').should('be.visible'); - e2e.components.PageToolbar.itemButton('Add panel button').click(); + e2e.components.PageToolbar.itemButton('Add button').should('be.visible'); + e2e.components.PageToolbar.itemButton('Add button').click(); } catch (e) { - // Depending on the screen size, the "Add panel" button might be hidden + // Depending on the screen size, the "Add" button might be hidden e2e.components.PageToolbar.item('Show more items').click(); - e2e.components.PageToolbar.item('Add panel button').last().click(); + e2e.components.PageToolbar.item('Add button').last().click(); } e2e.pages.AddDashboard.itemButton('Add new visualization menu item').should('be.visible'); e2e.pages.AddDashboard.itemButton('Add new visualization menu item').click(); diff --git a/public/app/features/dashboard/components/AddPanelButton/AddPanelButton.test.tsx b/public/app/features/dashboard/components/AddPanelButton/AddPanelButton.test.tsx new file mode 100644 index 00000000000..69bc4cf2c90 --- /dev/null +++ b/public/app/features/dashboard/components/AddPanelButton/AddPanelButton.test.tsx @@ -0,0 +1,48 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import { defaultDashboard } from '@grafana/schema'; + +import { createDashboardModelFixture } from '../../state/__fixtures__/dashboardFixtures'; + +import AddPanelButton, { Props } from './AddPanelButton'; +jest.mock('./AddPanelMenu', () => ({ + ...jest.requireActual('./AddPanelMenu'), + __esModule: true, + default: () =>
Menu
, +})); + +function setup(options?: Partial) { + const props = { + dashboard: createDashboardModelFixture(defaultDashboard), + }; + const { rerender } = render(); + + return rerender; +} + +beforeEach(() => { + jest.clearAllMocks(); +}); + +it('renders button', () => { + setup(); + + expect(screen.getByRole('button', { name: 'Add' })).toBeInTheDocument(); +}); + +it('renders button without menu when menu is not open', () => { + setup(); + + expect(screen.queryByText('Menu')).not.toBeInTheDocument(); +}); + +it('renders button with menu when menu is open', async () => { + const user = userEvent.setup(); + setup(); + + await user.click(screen.getByRole('button', { name: 'Add' })); + + expect(screen.queryByText('Menu')).toBeInTheDocument(); +}); diff --git a/public/app/features/dashboard/components/AddPanelButton/AddPanelButton.tsx b/public/app/features/dashboard/components/AddPanelButton/AddPanelButton.tsx index 1be12e93a07..869cb4f8dde 100644 --- a/public/app/features/dashboard/components/AddPanelButton/AddPanelButton.tsx +++ b/public/app/features/dashboard/components/AddPanelButton/AddPanelButton.tsx @@ -7,13 +7,13 @@ import { Dropdown, Button, useTheme2, Icon } from '@grafana/ui'; import { Trans } from 'app/core/internationalization'; import { DashboardModel } from 'app/features/dashboard/state'; -import { AddPanelMenu } from './AddPanelMenu'; +import AddPanelMenu from './AddPanelMenu'; -interface Props { +export interface Props { dashboard: DashboardModel; } -export const AddPanelButton = ({ dashboard }: Props) => { +const AddPanelButton = ({ dashboard }: Props) => { const styles = getStyles(useTheme2()); const [isMenuOpen, setIsMenuOpen] = useState(false); @@ -29,7 +29,7 @@ export const AddPanelButton = ({ dashboard }: Props) => { size="lg" fill="text" className={cx(styles.button, styles.buttonIcon, styles.buttonText)} - data-testid={selectors.components.PageToolbar.itemButton('Add panel button')} + data-testid={selectors.components.PageToolbar.itemButton('Add button')} > Add @@ -38,6 +38,8 @@ export const AddPanelButton = ({ dashboard }: Props) => { ); }; +export default AddPanelButton; + function getStyles(theme: GrafanaTheme2) { return { button: css({ diff --git a/public/app/features/dashboard/components/AddPanelButton/AddPanelMenu.test.tsx b/public/app/features/dashboard/components/AddPanelButton/AddPanelMenu.test.tsx new file mode 100644 index 00000000000..8473453e3d3 --- /dev/null +++ b/public/app/features/dashboard/components/AddPanelButton/AddPanelMenu.test.tsx @@ -0,0 +1,138 @@ +import { act, fireEvent, render, screen } from '@testing-library/react'; +import React from 'react'; + +import { PluginType } from '@grafana/data'; +import { locationService, reportInteraction } from '@grafana/runtime'; +import { defaultDashboard } from '@grafana/schema'; +import { createDashboardModelFixture } from 'app/features/dashboard/state/__fixtures__/dashboardFixtures'; +import { + onCreateNewPanel, + onCreateNewRow, + onAddLibraryPanel, + getCopiedPanelPlugin, +} from 'app/features/dashboard/utils/dashboard'; + +import AddPanelMenu from './AddPanelMenu'; + +jest.mock('app/types', () => ({ + ...jest.requireActual('app/types'), + useDispatch: () => jest.fn(), + useSelector: () => jest.fn(), +})); + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + locationService: { + partial: jest.fn(), + }, + reportInteraction: jest.fn(), + config: {}, +})); + +jest.mock('app/features/dashboard/utils/dashboard', () => ({ + onCreateNewPanel: jest.fn(), + onCreateNewRow: jest.fn(), + onAddLibraryPanel: jest.fn(), + getCopiedPanelPlugin: jest.fn(), +})); + +function setup() { + const props = { + dashboard: createDashboardModelFixture(defaultDashboard), + }; + const { rerender } = render(); + + return rerender; +} + +beforeEach(() => { + jest.clearAllMocks(); +}); + +it('renders menu list with correct menu items', () => { + setup(); + + expect(screen.getByText('visualization', { exact: false })).toBeInTheDocument(); + expect(screen.getByText('row', { exact: false })).toBeInTheDocument(); + expect(screen.getByText('library', { exact: false })).toBeInTheDocument(); + expect(screen.getByText('paste panel', { exact: false })).toBeInTheDocument(); +}); + +it('renders with all buttons enabled except paste a panel', () => { + // getCopiedPanelPluginMock().mockReset(); + + setup(); + + expect(screen.getByText('visualization', { exact: false })).not.toBeDisabled(); + expect(screen.getByText('row', { exact: false })).not.toBeDisabled(); + expect(screen.getByText('library', { exact: false })).not.toBeDisabled(); + expect(screen.getByText('paste panel', { exact: false })).toBeDisabled(); +}); + +it('renders with all buttons enabled', () => { + (getCopiedPanelPlugin as jest.Mock).mockReturnValue({ + id: 'someid', + name: 'nameofit', + type: PluginType.panel, + info: { + author: { + name: 'author name', + }, + description: 'description', + links: [], + logos: { + small: 'small', + large: 'large', + }, + updated: 'updated', + version: 'version', + }, + module: 'module', + baseUrl: 'url', + sort: 2, + defaults: { gridPos: { w: 200, h: 100 }, title: 'some title' }, + }); + + setup(); + + expect(screen.getByText('visualization', { exact: false })).not.toBeDisabled(); + expect(screen.getByText('row', { exact: false })).not.toBeDisabled(); + expect(screen.getByText('library', { exact: false })).not.toBeDisabled(); + expect(screen.getByText('paste panel', { exact: false })).not.toBeDisabled(); +}); + +it('creates new visualization when clicked on menu item Visualization', () => { + setup(); + + act(() => { + fireEvent.click(screen.getByRole('menuitem', { name: 'Visualization' })); + }); + + expect(reportInteraction).toHaveBeenCalledWith('dashboards_toolbar_add_clicked', { item: 'add_visualization' }); + expect(locationService.partial).toHaveBeenCalled(); + expect(onCreateNewPanel).toHaveBeenCalled(); +}); + +it('creates new row when clicked on menu item Row', () => { + setup(); + + act(() => { + fireEvent.click(screen.getByRole('menuitem', { name: 'Row' })); + }); + + expect(reportInteraction).toHaveBeenCalledWith('dashboards_toolbar_add_clicked', { item: 'add_row' }); + expect(locationService.partial).not.toHaveBeenCalled(); + expect(onCreateNewRow).toHaveBeenCalled(); +}); + +it('adds a library panel when clicked on menu item Import from library', () => { + setup(); + + act(() => { + fireEvent.click(screen.getByRole('menuitem', { name: 'Import from library' })); + }); + + expect(reportInteraction).toHaveBeenCalledWith('dashboards_toolbar_add_clicked', { item: 'import_from_library' }); + expect(locationService.partial).not.toHaveBeenCalled(); + expect(onAddLibraryPanel).toHaveBeenCalled(); +}); diff --git a/public/app/features/dashboard/components/AddPanelButton/AddPanelMenu.tsx b/public/app/features/dashboard/components/AddPanelButton/AddPanelMenu.tsx index 4ec49a60bfc..01ff2616cb2 100644 --- a/public/app/features/dashboard/components/AddPanelButton/AddPanelMenu.tsx +++ b/public/app/features/dashboard/components/AddPanelButton/AddPanelMenu.tsx @@ -16,11 +16,11 @@ import { useDispatch, useSelector } from 'app/types'; import { setInitialDatasource } from '../../state/reducers'; -interface Props { +export interface Props { dashboard: DashboardModel; } -export const AddPanelMenu = ({ dashboard }: Props) => { +const AddPanelMenu = ({ dashboard }: Props) => { const copiedPanelPlugin = useMemo(() => getCopiedPanelPlugin(), []); const dispatch = useDispatch(); const initialDatasource = useSelector((state) => state.dashboard.initialDatasource); @@ -29,8 +29,8 @@ export const AddPanelMenu = ({ dashboard }: Props) => { { const id = onCreateNewPanel(dashboard, initialDatasource); reportInteraction('dashboards_toolbar_add_clicked', { item: 'add_visualization' }); @@ -40,8 +40,8 @@ export const AddPanelMenu = ({ dashboard }: Props) => { /> { reportInteraction('dashboards_toolbar_add_clicked', { item: 'add_row' }); onCreateNewRow(dashboard); @@ -49,8 +49,8 @@ export const AddPanelMenu = ({ dashboard }: Props) => { /> { reportInteraction('dashboards_toolbar_add_clicked', { item: 'import_from_library' }); onAddLibraryPanel(dashboard); @@ -58,8 +58,8 @@ export const AddPanelMenu = ({ dashboard }: Props) => { /> { reportInteraction('dashboards_toolbar_add_clicked', { item: 'paste_panel' }); onPasteCopiedPanel(dashboard, copiedPanelPlugin); @@ -69,3 +69,5 @@ export const AddPanelMenu = ({ dashboard }: Props) => { ); }; + +export default AddPanelMenu; diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index bc5f5734fc7..b3efafc9e39 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -23,7 +23,7 @@ import { appEvents } from 'app/core/core'; import { useBusEvent } from 'app/core/hooks/useBusEvent'; import { t, Trans } from 'app/core/internationalization'; import { setStarred } from 'app/core/reducers/navBarTree'; -import { AddPanelButton } from 'app/features/dashboard/components/AddPanelButton/AddPanelButton'; +import AddPanelButton from 'app/features/dashboard/components/AddPanelButton/AddPanelButton'; import { SaveDashboardDrawer } from 'app/features/dashboard/components/SaveDashboard/SaveDashboardDrawer'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { DashboardModel } from 'app/features/dashboard/state'; diff --git a/public/app/features/dashboard/dashgrid/DashboardEmpty.test.tsx b/public/app/features/dashboard/dashgrid/DashboardEmpty.test.tsx new file mode 100644 index 00000000000..7e207589daf --- /dev/null +++ b/public/app/features/dashboard/dashgrid/DashboardEmpty.test.tsx @@ -0,0 +1,103 @@ +import { act, fireEvent, render, screen } from '@testing-library/react'; +import React from 'react'; + +import { locationService, reportInteraction } from '@grafana/runtime'; +import { defaultDashboard } from '@grafana/schema'; + +import { createDashboardModelFixture } from '../state/__fixtures__/dashboardFixtures'; +import { onCreateNewPanel, onCreateNewRow, onAddLibraryPanel } from '../utils/dashboard'; + +import DashboardEmpty, { Props } from './DashboardEmpty'; + +jest.mock('app/types', () => ({ + ...jest.requireActual('app/types'), + useDispatch: () => jest.fn(), + useSelector: () => jest.fn(), +})); + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + locationService: { + partial: jest.fn(), + }, + reportInteraction: jest.fn(), + config: {}, +})); + +jest.mock('app/features/dashboard/utils/dashboard', () => ({ + onCreateNewPanel: jest.fn(), + onCreateNewRow: jest.fn(), + onAddLibraryPanel: jest.fn(), +})); + +function setup(options?: Partial) { + const props = { + dashboard: createDashboardModelFixture(defaultDashboard), + canCreate: options?.canCreate ?? true, + }; + const { rerender } = render(); + + return rerender; +} + +beforeEach(() => { + jest.clearAllMocks(); +}); + +it('renders page with correct title for an empty dashboard', () => { + setup(); + + expect(screen.getByText('your new dashboard', { exact: false })).toBeInTheDocument(); +}); + +it('renders with all buttons enabled when canCreate is true', () => { + setup(); + + expect(screen.getByRole('button', { name: 'Add visualization' })).not.toBeDisabled(); + expect(screen.getByRole('button', { name: 'Add row' })).not.toBeDisabled(); + expect(screen.getByRole('button', { name: 'Import library panel' })).not.toBeDisabled(); +}); + +it('renders with all buttons disabled when canCreate is false', () => { + setup({ canCreate: false }); + + expect(screen.getByRole('button', { name: 'Add visualization' })).toBeDisabled(); + expect(screen.getByRole('button', { name: 'Add row' })).toBeDisabled(); + expect(screen.getByRole('button', { name: 'Import library panel' })).toBeDisabled(); +}); + +it('creates new visualization when clicked Add visualization', () => { + setup(); + + act(() => { + fireEvent.click(screen.getByRole('button', { name: 'Add visualization' })); + }); + + expect(reportInteraction).toHaveBeenCalledWith('dashboards_emptydashboard_clicked', { item: 'add_visualization' }); + expect(locationService.partial).toHaveBeenCalled(); + expect(onCreateNewPanel).toHaveBeenCalled(); +}); + +it('creates new row when clicked Add row', () => { + setup(); + + act(() => { + fireEvent.click(screen.getByRole('button', { name: 'Add row' })); + }); + + expect(reportInteraction).toHaveBeenCalledWith('dashboards_emptydashboard_clicked', { item: 'add_row' }); + expect(locationService.partial).not.toHaveBeenCalled(); + expect(onCreateNewRow).toHaveBeenCalled(); +}); + +it('adds a library panel when clicked Import library panel', () => { + setup(); + + act(() => { + fireEvent.click(screen.getByRole('button', { name: 'Import library panel' })); + }); + + expect(reportInteraction).toHaveBeenCalledWith('dashboards_emptydashboard_clicked', { item: 'import_from_library' }); + expect(locationService.partial).not.toHaveBeenCalled(); + expect(onAddLibraryPanel).toHaveBeenCalled(); +}); diff --git a/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx b/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx index 59fce997504..f7a101660a4 100644 --- a/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx @@ -2,6 +2,7 @@ import { css, cx } from '@emotion/css'; import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { locationService, reportInteraction } from '@grafana/runtime'; import { Button, useStyles2 } from '@grafana/ui'; import { H1, H3, P } from '@grafana/ui/src/unstable'; @@ -17,7 +18,7 @@ export interface Props { canCreate: boolean; } -export const DashboardEmpty = ({ dashboard, canCreate }: Props) => { +const DashboardEmpty = ({ dashboard, canCreate }: Props) => { const styles = useStyles2(getStyles); const dispatch = useDispatch(); const initialDatasource = useSelector((state) => state.dashboard.initialDatasource); @@ -44,7 +45,7 @@ export const DashboardEmpty = ({ dashboard, canCreate }: Props) => {