From a018fa5cfb6f304e6bfe5d90b1793ba880cbb1bd Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Thu, 25 May 2023 11:26:39 +0100 Subject: [PATCH] Nested folders: More browse unit tests (#68994) * improved BrowseDashboardsPage unit tests * better * add unit tests for FolderActionsButton --- .../BrowseDashboardsPage.test.tsx | 271 +++++++++++++++--- .../BrowseFolderAlertingPage.test.tsx | 7 + .../BrowseFolderLibraryPanelsPage.test.tsx | 7 + .../components/FolderActionsButton.test.tsx | 142 +++++++++ 4 files changed, 392 insertions(+), 35 deletions(-) create mode 100644 public/app/features/browse-dashboards/components/FolderActionsButton.test.tsx diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx index 4c7a4741afb..f8eb338be18 100644 --- a/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx @@ -1,16 +1,30 @@ -import { render as rtlRender, screen, waitFor } from '@testing-library/react'; +import 'whatwg-fetch'; // fetch polyfill +import { render as rtlRender, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { rest } from 'msw'; +import { setupServer, SetupServer } from 'msw/node'; import React, { ComponentProps } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { TestProvider } from 'test/helpers/TestProvider'; import { selectors } from '@grafana/e2e-selectors'; +import { contextSrv } from 'app/core/core'; import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps'; +import { backendSrv } from 'app/core/services/backend_srv'; import BrowseDashboardsPage, { Props } from './BrowseDashboardsPage'; import { wellFormedTree } from './fixtures/dashboardsTreeItem.fixture'; import * as permissions from './permissions'; -const [mockTree, { dashbdD, folderA }] = wellFormedTree(); +const [mockTree, { dashbdD, folderA, folderA_folderA }] = wellFormedTree(); + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getBackendSrv: () => backendSrv, + config: { + ...jest.requireActual('@grafana/runtime').config, + unifiedAlertingEnabled: true, + }, +})); jest.mock('react-virtualized-auto-sizer', () => { return { @@ -22,7 +36,7 @@ jest.mock('react-virtualized-auto-sizer', () => { }); function render(...[ui, options]: Parameters) { - rtlRender( + const { rerender } = rtlRender( ) { , options ); + + const wrappedRerender = (ui: React.ReactElement) => { + rerender( + + {ui} + + ); + }; + return { + rerender: wrappedRerender, + }; } jest.mock('app/features/search/service/folders', () => { @@ -53,6 +87,37 @@ jest.mock('app/features/search/service/folders', () => { describe('browse-dashboards BrowseDashboardsPage', () => { let props: Props; + let server: SetupServer; + + beforeAll(() => { + server = setupServer( + rest.get('/api/folders/:uid', (_, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + title: folderA.item.title, + uid: folderA.item.uid, + }) + ); + }), + rest.get('/api/search', (_, res, ctx) => { + return res(ctx.status(200), ctx.json({})); + }), + rest.get('/api/search/sorting', (_, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + sortOptions: [], + }) + ); + }) + ); + server.listen(); + }); + + afterAll(() => { + server.close(); + }); beforeEach(() => { props = { @@ -66,55 +131,191 @@ describe('browse-dashboards BrowseDashboardsPage', () => { canCreateFolder: true, }; }); + jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true); }); - it('displays a search input', async () => { - render(); - expect(await screen.findByPlaceholderText('Search for dashboards and folders')).toBeInTheDocument(); + afterEach(() => { + jest.restoreAllMocks(); + server.resetHandlers(); }); - it('displays the filters and hides the actions initially', async () => { - render(); - await screen.findByPlaceholderText('Search for dashboards and folders'); + describe('at the root level', () => { + it('displays "Dashboards" as the page title', async () => { + render(); + expect(await screen.findByRole('heading', { name: 'Dashboards' })).toBeInTheDocument(); + }); - expect(screen.queryByText('Sort')).toBeInTheDocument(); - expect(screen.queryByText('Filter by tag')).toBeInTheDocument(); + it('displays a search input', async () => { + render(); + expect(await screen.findByPlaceholderText('Search for dashboards and folders')).toBeInTheDocument(); + }); - expect(screen.queryByRole('button', { name: 'Move' })).not.toBeInTheDocument(); - expect(screen.queryByRole('button', { name: 'Delete' })).not.toBeInTheDocument(); - }); + it('shows the "New" button', async () => { + render(); + expect(await screen.findByRole('button', { name: 'New' })).toBeInTheDocument(); + }); - it('selecting an item hides the filters and shows the actions instead', async () => { - render(); + it('does not show the "New" button if the user does not have permissions', async () => { + jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => { + return { + canEditInFolder: false, + canCreateDashboards: false, + canCreateFolder: false, + }; + }); + render(); + expect(await screen.findByRole('heading', { name: 'Dashboards' })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'New' })).not.toBeInTheDocument(); + }); - const checkbox = await screen.findByTestId(selectors.pages.BrowseDashbards.table.checkbox(dashbdD.item.uid)); - await userEvent.click(checkbox); + it('does not show "Folder actions"', async () => { + render(); + expect(await screen.findByRole('heading', { name: 'Dashboards' })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Folder actions' })).not.toBeInTheDocument(); + }); - // Check the filters are now hidden - expect(screen.queryByText('Filter by tag')).not.toBeInTheDocument(); - expect(screen.queryByText('Sort')).not.toBeInTheDocument(); + it('does not show any tabs', async () => { + render(); + expect(await screen.findByRole('heading', { name: 'Dashboards' })).toBeInTheDocument(); - // Check the actions are now visible - expect(screen.getByRole('button', { name: 'Move' })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Delete' })).toBeInTheDocument(); - }); + expect(screen.queryByRole('tab', { name: 'Tab Dashboards' })).not.toBeInTheDocument(); + expect(screen.queryByRole('tab', { name: 'Tab Panels' })).not.toBeInTheDocument(); + expect(screen.queryByRole('tab', { name: 'Tab Alert rules' })).not.toBeInTheDocument(); + }); - it('navigating into a child item resets the selected state', async () => { - render(); + it('displays the filters and hides the actions initially', async () => { + render(); + await screen.findByPlaceholderText('Search for dashboards and folders'); - const checkbox = await screen.findByTestId(selectors.pages.BrowseDashbards.table.checkbox(folderA.item.uid)); - await userEvent.click(checkbox); + expect(await screen.findByText('Sort')).toBeInTheDocument(); + expect(await screen.findByText('Filter by tag')).toBeInTheDocument(); - // Check the actions are now visible - expect(screen.getByRole('button', { name: 'Move' })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Delete' })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Move' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Delete' })).not.toBeInTheDocument(); + }); - await userEvent.click(screen.getByRole('link', { name: folderA.item.title })); + it('selecting an item hides the filters and shows the actions instead', async () => { + render(); - // Check the actions are no longer visible - waitFor(() => { + const checkbox = await screen.findByTestId(selectors.pages.BrowseDashbards.table.checkbox(dashbdD.item.uid)); + await userEvent.click(checkbox); + + // Check the filters are now hidden + expect(screen.queryByText('Filter by tag')).not.toBeInTheDocument(); + expect(screen.queryByText('Sort')).not.toBeInTheDocument(); + + // Check the actions are now visible + expect(screen.getByRole('button', { name: 'Move' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Delete' })).toBeInTheDocument(); + }); + + it('navigating into a child item resets the selected state', async () => { + const { rerender } = render(); + + const checkbox = await screen.findByTestId(selectors.pages.BrowseDashbards.table.checkbox(folderA.item.uid)); + await userEvent.click(checkbox); + + // Check the actions are now visible + expect(screen.getByRole('button', { name: 'Move' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Delete' })).toBeInTheDocument(); + + const updatedProps = { ...props }; + updatedProps.match.params = { uid: folderA.item.uid }; + rerender(); + + // Check the filters are now visible again + expect(await screen.findByText('Filter by tag')).toBeInTheDocument(); + expect(await screen.findByText('Sort')).toBeInTheDocument(); + + // Check the actions are no longer visible expect(screen.queryByRole('button', { name: 'Move' })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Delete' })).not.toBeInTheDocument(); }); }); + + describe('for a child folder', () => { + beforeEach(() => { + props.match.params = { uid: folderA.item.uid }; + }); + + it('shows the folder name as the page title', async () => { + render(); + expect(await screen.findByRole('heading', { name: folderA.item.title })).toBeInTheDocument(); + }); + + it('displays a search input', async () => { + render(); + expect(await screen.findByPlaceholderText('Search for dashboards and folders')).toBeInTheDocument(); + }); + + it('shows the "New" button', async () => { + render(); + expect(await screen.findByRole('button', { name: 'New' })).toBeInTheDocument(); + }); + + it('does not show the "New" button if the user does not have permissions', async () => { + jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => { + return { + canEditInFolder: false, + canCreateDashboards: false, + canCreateFolder: false, + }; + }); + render(); + expect(await screen.findByRole('heading', { name: folderA.item.title })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'New' })).not.toBeInTheDocument(); + }); + + it('shows the "Folder actions" button', async () => { + render(); + expect(await screen.findByRole('button', { name: 'Folder actions' })).toBeInTheDocument(); + }); + + it('does not show the "Folder actions" button if the user does not have permissions', async () => { + jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false); + render(); + expect(await screen.findByRole('heading', { name: folderA.item.title })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Folder actions' })).not.toBeInTheDocument(); + }); + + it('displays all the folder tabs and shows the "Dashboards" tab as selected', async () => { + render(); + expect(await screen.findByRole('tab', { name: 'Tab Dashboards' })).toBeInTheDocument(); + expect(await screen.findByRole('tab', { name: 'Tab Dashboards' })).toHaveAttribute('aria-selected', 'true'); + + expect(await screen.findByRole('tab', { name: 'Tab Panels' })).toBeInTheDocument(); + expect(await screen.findByRole('tab', { name: 'Tab Panels' })).toHaveAttribute('aria-selected', 'false'); + + expect(await screen.findByRole('tab', { name: 'Tab Alert rules' })).toBeInTheDocument(); + expect(await screen.findByRole('tab', { name: 'Tab Alert rules' })).toHaveAttribute('aria-selected', 'false'); + }); + + it('displays the filters and hides the actions initially', async () => { + render(); + await screen.findByPlaceholderText('Search for dashboards and folders'); + + expect(await screen.findByText('Sort')).toBeInTheDocument(); + expect(await screen.findByText('Filter by tag')).toBeInTheDocument(); + + expect(screen.queryByRole('button', { name: 'Move' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Delete' })).not.toBeInTheDocument(); + }); + + it('selecting an item hides the filters and shows the actions instead', async () => { + render(); + + const checkbox = await screen.findByTestId( + selectors.pages.BrowseDashbards.table.checkbox(folderA_folderA.item.uid) + ); + await userEvent.click(checkbox); + + // Check the filters are now hidden + expect(screen.queryByText('Filter by tag')).not.toBeInTheDocument(); + expect(screen.queryByText('Sort')).not.toBeInTheDocument(); + + // Check the actions are now visible + expect(screen.getByRole('button', { name: 'Move' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Delete' })).toBeInTheDocument(); + }); + }); }); diff --git a/public/app/features/browse-dashboards/BrowseFolderAlertingPage.test.tsx b/public/app/features/browse-dashboards/BrowseFolderAlertingPage.test.tsx index 956cf12bf10..964c4aec837 100644 --- a/public/app/features/browse-dashboards/BrowseFolderAlertingPage.test.tsx +++ b/public/app/features/browse-dashboards/BrowseFolderAlertingPage.test.tsx @@ -91,6 +91,13 @@ describe('browse-dashboards BrowseFolderAlertingPage', () => { expect(await screen.findByRole('button', { name: 'Folder actions' })).toBeInTheDocument(); }); + it('does not display the "Folder actions" button if the user does not have permissions', async () => { + jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false); + render(); + expect(await screen.findByRole('heading', { name: mockFolderName })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Folder actions' })).not.toBeInTheDocument(); + }); + it('displays all the folder tabs and shows the "Alert rules" tab as selected', async () => { render(); expect(await screen.findByRole('tab', { name: 'Tab Dashboards' })).toBeInTheDocument(); diff --git a/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.test.tsx b/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.test.tsx index 6409a844921..10b9708cb38 100644 --- a/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.test.tsx +++ b/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.test.tsx @@ -96,6 +96,13 @@ describe('browse-dashboards BrowseFolderLibraryPanelsPage', () => { expect(await screen.findByRole('button', { name: 'Folder actions' })).toBeInTheDocument(); }); + it('does not display the "Folder actions" button if the user does not have permissions', async () => { + jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false); + render(); + expect(await screen.findByRole('heading', { name: mockFolderName })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Folder actions' })).not.toBeInTheDocument(); + }); + it('displays all the folder tabs and shows the "Library panels" tab as selected', async () => { render(); expect(await screen.findByRole('tab', { name: 'Tab Dashboards' })).toBeInTheDocument(); diff --git a/public/app/features/browse-dashboards/components/FolderActionsButton.test.tsx b/public/app/features/browse-dashboards/components/FolderActionsButton.test.tsx new file mode 100644 index 00000000000..2551b3fa49a --- /dev/null +++ b/public/app/features/browse-dashboards/components/FolderActionsButton.test.tsx @@ -0,0 +1,142 @@ +import { render as rtlRender, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { TestProvider } from 'test/helpers/TestProvider'; + +import { appEvents, contextSrv } from 'app/core/core'; +import { AccessControlAction, FolderDTO } from 'app/types'; +import { ShowModalReactEvent } from 'app/types/events'; + +import { DeleteModal } from './BrowseActions/DeleteModal'; +import { MoveModal } from './BrowseActions/MoveModal'; +import { FolderActionsButton } from './FolderActionsButton'; + +function render(...[ui, options]: Parameters) { + rtlRender({ui}, options); +} + +// Mock out the Permissions component for now +jest.mock('app/core/components/AccessControl', () => ({ + Permissions: () =>
Hello!
, +})); + +describe('browse-dashboards FolderActionsButton', () => { + const mockFolder: FolderDTO = { + canAdmin: true, + canDelete: true, + canEdit: true, + canSave: true, + created: '', + createdBy: '', + hasAcl: true, + id: 1, + title: 'myFolder', + uid: '12345', + updated: '', + updatedBy: '', + url: '', + version: 1, + }; + + beforeEach(() => { + jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('does not render anything when the user has no permissions to do anything', () => { + jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false); + render(); + expect(screen.queryByRole('button', { name: 'Folder actions' })).not.toBeInTheDocument(); + }); + + it('renders a "Folder actions" button when the user has permissions to do something', () => { + render(); + expect(screen.getByRole('button', { name: 'Folder actions' })).toBeInTheDocument(); + }); + + it('renders all the options if the user has full permissions', async () => { + render(); + + await userEvent.click(screen.getByRole('button', { name: 'Folder actions' })); + expect(screen.getByRole('menuitem', { name: 'Manage permissions' })).toBeInTheDocument(); + expect(screen.getByRole('menuitem', { name: 'Move' })).toBeInTheDocument(); + expect(screen.getByRole('menuitem', { name: 'Delete' })).toBeInTheDocument(); + }); + + it('does not render the "Manage permissions" option if the user does not have permission to view permissions', async () => { + jest + .spyOn(contextSrv, 'hasPermission') + .mockImplementation((permission: string) => permission !== AccessControlAction.FoldersPermissionsRead); + render(); + + await userEvent.click(screen.getByRole('button', { name: 'Folder actions' })); + expect(screen.queryByRole('menuitem', { name: 'Manage permissions' })).not.toBeInTheDocument(); + expect(screen.getByRole('menuitem', { name: 'Move' })).toBeInTheDocument(); + expect(screen.getByRole('menuitem', { name: 'Delete' })).toBeInTheDocument(); + }); + + it('does not render the "Move" option if the user does not have permission to edit', async () => { + jest + .spyOn(contextSrv, 'hasPermission') + .mockImplementation((permission: string) => permission !== AccessControlAction.FoldersWrite); + render(); + + await userEvent.click(screen.getByRole('button', { name: 'Folder actions' })); + expect(screen.getByRole('menuitem', { name: 'Manage permissions' })).toBeInTheDocument(); + expect(screen.queryByRole('menuitem', { name: 'Move' })).not.toBeInTheDocument(); + expect(screen.getByRole('menuitem', { name: 'Delete' })).toBeInTheDocument(); + }); + + it('does not render the "Delete" option if the user does not have permission to delete', async () => { + jest + .spyOn(contextSrv, 'hasPermission') + .mockImplementation((permission: string) => permission !== AccessControlAction.FoldersDelete); + render(); + + await userEvent.click(screen.getByRole('button', { name: 'Folder actions' })); + expect(screen.getByRole('menuitem', { name: 'Manage permissions' })).toBeInTheDocument(); + expect(screen.getByRole('menuitem', { name: 'Move' })).toBeInTheDocument(); + expect(screen.queryByRole('menuitem', { name: 'Delete' })).not.toBeInTheDocument(); + }); + + it('clicking the "Manage permissions" option opens the permissions drawer', async () => { + render(); + + await userEvent.click(screen.getByRole('button', { name: 'Folder actions' })); + await userEvent.click(screen.getByRole('menuitem', { name: 'Manage permissions' })); + expect(screen.getByRole('dialog', { name: 'Drawer title Permissions' })).toBeInTheDocument(); + }); + + it('clicking the "Move" option opens the move modal', async () => { + jest.spyOn(appEvents, 'publish'); + render(); + + await userEvent.click(screen.getByRole('button', { name: 'Folder actions' })); + await userEvent.click(screen.getByRole('menuitem', { name: 'Move' })); + expect(appEvents.publish).toHaveBeenCalledWith( + new ShowModalReactEvent( + expect.objectContaining({ + component: MoveModal, + }) + ) + ); + }); + + it('clicking the "Delete" option opens the delete modal', async () => { + jest.spyOn(appEvents, 'publish'); + render(); + + await userEvent.click(screen.getByRole('button', { name: 'Folder actions' })); + await userEvent.click(screen.getByRole('menuitem', { name: 'Delete' })); + expect(appEvents.publish).toHaveBeenCalledWith( + new ShowModalReactEvent( + expect.objectContaining({ + component: DeleteModal, + }) + ) + ); + }); +});