Folders: Refactor Browse/Dashboards list tests (#109421)

This commit is contained in:
Tom Ratcliffe
2025-08-21 15:46:12 +01:00
committed by GitHub
parent 91748fe115
commit 82d36a259e
9 changed files with 133 additions and 133 deletions
@@ -1,14 +1,19 @@
import { HttpHandler } from 'msw';
import folderHandlers from './api/folders/handlers';
import searchHandlers from './api/search/handlers';
import teamsHandlers from './api/teams/handlers';
import appPlatformDashboardv0alpha1Handlers from './apis/dashboard.grafana.app/v0alpha1/handlers';
import appPlatformFolderv1beta1Handlers from './apis/folder.grafana.app/v1beta1/handlers';
import appPlatformIamv0alpha1Handlers from './apis/iam.grafana.app/v0alpha1/handlers';
const allHandlers: HttpHandler[] = [
// Legacy handlers
...teamsHandlers,
...folderHandlers,
...searchHandlers,
// App platform handlers
...appPlatformDashboardv0alpha1Handlers,
...appPlatformFolderv1beta1Handlers,
...appPlatformIamv0alpha1Handlers,
@@ -1,3 +1,4 @@
import { Chance } from 'chance';
import { HttpResponse, http } from 'msw';
import { treeViewersCanEdit, wellFormedTree } from '../../../fixtures/folders';
@@ -19,7 +20,6 @@ const additionalProperties = {
created: '2025-07-14T12:07:36+02:00',
createdBy: 'Anonymous',
hasAcl: false,
id: 1,
orgId: 1,
updated: '2025-07-15T18:01:36+02:00',
updatedBy: 'Anonymous',
@@ -42,10 +42,11 @@ const listFoldersHandler = () =>
const folders = tree
.filter((v) => v.item.kind === 'folder' && v.item.parentUID === parentUid)
.map((folder) => {
const random = Chance(folder.item.uid);
return {
id: random.integer({ min: 1, max: 1000 }),
uid: folder.item.uid,
title: folder.item.kind === 'folder' ? folder.item.title : "invalid - this shouldn't happen",
...additionalProperties,
};
})
.sort((a, b) => collator.compare(a.title, b.title)) // API always sorts by title
@@ -66,7 +67,10 @@ const getFolderHandler = () =>
return HttpResponse.json({ message: 'folder not found', status: 'not-found' }, { status: 404 });
}
const random = Chance(folder.item.uid);
return HttpResponse.json({
id: random.integer({ min: 1, max: 1000 }),
title: folder?.item.title,
uid: folder?.item.uid,
...additionalProperties,
@@ -0,0 +1,73 @@
import { Chance } from 'chance';
import { HttpResponse, http } from 'msw';
import { wellFormedTree } from '../../../fixtures/folders';
const [mockTree] = wellFormedTree();
type FilterArray = Array<(v: (typeof mockTree)[number]) => boolean>;
const slugify = (str: string) => {
return str
.toLowerCase()
.replace(/[^\w ]+/g, '')
.replace(/ +/g, '-');
};
const getLegacySearchHandler = () =>
http.get('/api/search', ({ request }) => {
const folderFilter = new URL(request.url).searchParams.get('folderUIDs') || null;
const typeFilter = new URL(request.url).searchParams.get('type') || null;
// Workaround for the fixture kind being 'dashboard' instead of 'dash-db'
const mappedTypeFilter = typeFilter === 'dash-db' ? 'dashboard' : typeFilter;
const response = mockTree
.filter((filterItem) => {
const filters: FilterArray = [];
if (folderFilter && folderFilter !== 'general') {
filters.push(
({ item }) => (item.kind === 'folder' || item.kind === 'dashboard') && item.parentUID === folderFilter
);
}
if (folderFilter === 'general') {
filters.push(
({ item }) => (item.kind === 'folder' || item.kind === 'dashboard') && item.parentUID === undefined
);
}
if (mappedTypeFilter) {
filters.push(({ item }) => item.kind === mappedTypeFilter);
}
return filters.every((filterPredicate) => filterPredicate(filterItem));
})
.map(({ item }) => {
const random = Chance(item.uid);
const slugified = slugify(item.title || '');
const parentFolder =
item.kind === 'dashboard'
? mockTree.find((t) => t.item.kind === 'folder' && t.item.uid === item.parentUID)
: undefined;
return {
id: random.integer({ min: 1, max: 1000 }),
uid: item.uid,
orgId: 1,
title: item.title,
uri: `db/${slugified}`,
url: `/d/${item.uid}/${slugified}`,
folderUid: parentFolder?.item.uid,
folderTitle: parentFolder?.item.title,
slug: '',
type: item.kind,
tags: [],
isStarred: false,
sortMeta: 0,
isDeleted: false,
};
});
return HttpResponse.json(response);
});
export default [getLegacySearchHandler()];
@@ -83,7 +83,7 @@ describe('useGetFolderQueryFacade', () => {
config.featureToggles.foldersAppPlatformAPI = false;
const result = await renderFolderHook();
expect(result.current.data).toMatchObject({
id: 1,
id: 791,
title: folderA_folderA.item.title,
url: expectedUrl,
uid: expectedUid,
@@ -17,7 +17,7 @@ import BrowseDashboardsPage from './BrowseDashboardsPage';
import * as permissions from './permissions';
setupMockServer();
const [mockTree, { dashbdD, folderA, folderA_folderA }] = getFolderFixtures();
const [_, { dashbdD, folderA, folderA_folderA }] = getFolderFixtures();
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
@@ -89,29 +89,6 @@ function render(...[ui, options]: Parameters<typeof rtlRender>) {
};
}
jest.mock('app/features/browse-dashboards/api/services', () => {
const orig = jest.requireActual('app/features/browse-dashboards/api/services');
return {
...orig,
listFolders(parentUID?: string) {
const childrenForUID = mockTree
.filter((v) => v.item.kind === 'folder' && v.item.parentUID === parentUID)
.map((v) => v.item);
return Promise.resolve(childrenForUID);
},
listDashboards(parentUID?: string) {
const childrenForUID = mockTree
.filter((v) => v.item.kind === 'dashboard' && v.item.parentUID === parentUID)
.map((v) => v.item);
return Promise.resolve(childrenForUID);
},
};
});
describe('browse-dashboards BrowseDashboardsPage', () => {
const mockPermissions = {
canCreateDashboards: true,
@@ -1,42 +1,21 @@
import { getByLabelText, render as rtlRender, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TestProvider } from 'test/helpers/TestProvider';
import { getByLabelText, render, screen, userEvent } from 'test/test-utils';
import { selectors } from '@grafana/e2e-selectors';
import { setBackendSrv } from '@grafana/runtime';
import { setupMockServer } from '@grafana/test-utils/server';
import { getFolderFixtures } from '@grafana/test-utils/unstable';
import { backendSrv } from 'app/core/services/backend_srv';
import { contextSrv } from 'app/core/services/context_srv';
import { DashboardViewItem } from 'app/features/search/types';
import { AccessControlAction } from 'app/types/accessControl';
import { BrowseView } from './BrowseView';
const [mockTree, { folderA, folderA_folderA, folderA_folderB, folderA_folderB_dashbdB, dashbdD, folderB_empty }] =
getFolderFixtures();
function render(...[ui, options]: Parameters<typeof rtlRender>) {
rtlRender(<TestProvider>{ui}</TestProvider>, options);
}
jest.mock('app/features/browse-dashboards/api/services', () => {
const orig = jest.requireActual('app/features/browse-dashboards/api/services');
return {
...orig,
listFolders(parentUID?: string) {
const childrenForUID = mockTree
.filter((v) => v.item.kind === 'folder' && v.item.parentUID === parentUID)
.map((v) => v.item);
return Promise.resolve(childrenForUID);
},
listDashboards(parentUID?: string) {
const childrenForUID = mockTree
.filter((v) => v.item.kind === 'dashboard' && v.item.parentUID === parentUID)
.map((v) => v.item);
return Promise.resolve(childrenForUID);
},
};
});
setBackendSrv(backendSrv);
setupMockServer();
describe('browse-dashboards BrowseView', () => {
const WIDTH = 800;
@@ -48,13 +27,12 @@ describe('browse-dashboards BrowseView', () => {
canDeleteDashboards: true,
};
afterEach(() => {
// Reset permissions back to defaults
Object.assign(mockPermissions, {
canEditFolders: true,
canEditDashboards: true,
canDeleteFolders: true,
canDeleteDashboards: true,
beforeEach(() => {
jest.spyOn(contextSrv, 'hasPermission').mockImplementation((permission: string) => {
if (permission === AccessControlAction.FoldersRead) {
return true;
}
return false;
});
});
@@ -63,7 +41,7 @@ describe('browse-dashboards BrowseView', () => {
await screen.findByText(folderA.item.title);
await expandFolder(folderA.item);
expect(screen.queryByText(folderA_folderA.item.title)).toBeInTheDocument();
expect(screen.getByText(folderA_folderA.item.title)).toBeInTheDocument();
await collapseFolder(folderA.item);
expect(screen.queryByText(folderA_folderA.item.title)).not.toBeInTheDocument();
@@ -169,13 +147,20 @@ describe('browse-dashboards BrowseView', () => {
});
it('shows a simple message if the user has viewer rights', async () => {
mockPermissions.canEditFolders = false;
mockPermissions.canEditDashboards = false;
mockPermissions.canDeleteFolders = false;
mockPermissions.canDeleteDashboards = false;
const mockPermissionsDisabled = {
canEditFolders: false,
canEditDashboards: false,
canDeleteFolders: false,
canDeleteDashboards: false,
};
render(
<BrowseView permissions={mockPermissions} folderUID={folderB_empty.item.uid} width={WIDTH} height={HEIGHT} />
<BrowseView
permissions={mockPermissionsDisabled}
folderUID={folderB_empty.item.uid}
width={WIDTH}
height={HEIGHT}
/>
);
expect(await screen.findByText('This folder is empty')).toBeInTheDocument();
});
@@ -1,24 +1,16 @@
import { render as rtlRender, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TestProvider } from 'test/helpers/TestProvider';
import { assertIsDefined } from 'test/helpers/asserts';
import { render, screen } from 'test/test-utils';
import { selectors } from '@grafana/e2e-selectors';
import { config } from '@grafana/runtime';
import { getFolderFixtures } from '@grafana/test-utils/unstable';
import {
sharedWithMeFolder,
wellFormedDashboard,
wellFormedEmptyFolder,
wellFormedFolder,
} from '../fixtures/dashboardsTreeItem.fixture';
import { sharedWithMeFolder } from '../fixtures/dashboardsTreeItem.fixture';
import { SelectionState } from '../types';
import { DashboardsTree } from './DashboardsTree';
function render(...[ui, options]: Parameters<typeof rtlRender>) {
rtlRender(<TestProvider>{ui}</TestProvider>, options);
}
const [_, { folderA: folder, folderB_empty: emptyFolderIndicator, dashbdD: dashboard }] = getFolderFixtures();
describe('browse-dashboards DashboardsTree', () => {
const WIDTH = 800;
@@ -30,9 +22,6 @@ describe('browse-dashboards DashboardsTree', () => {
canDeleteDashboards: true,
};
const folder = wellFormedFolder(1);
const emptyFolderIndicator = wellFormedEmptyFolder();
const dashboard = wellFormedDashboard(2);
const noop = () => {};
const isSelected = () => SelectionState.Unselected;
const allItemsAreLoaded = () => true;
@@ -67,8 +56,8 @@ describe('browse-dashboards DashboardsTree', () => {
requestLoadMore={requestLoadMore}
/>
);
expect(screen.queryByText(dashboard.item.title)).toBeInTheDocument();
expect(screen.queryByText(assertIsDefined(dashboard.item.tags)[0])).toBeInTheDocument();
expect(screen.getByText(dashboard.item.title)).toBeInTheDocument();
expect(screen.getByText(assertIsDefined(dashboard.item.tags)[0])).toBeInTheDocument();
expect(screen.getByTestId(selectors.pages.BrowseDashboards.table.checkbox(dashboard.item.uid))).toBeInTheDocument();
});
@@ -113,7 +102,7 @@ describe('browse-dashboards DashboardsTree', () => {
/>
);
expect(screen.queryByText(folder.item.title)).toBeInTheDocument();
expect(screen.getByText(folder.item.title)).toBeInTheDocument();
});
it('renders a folder link', () => {
@@ -181,7 +170,7 @@ describe('browse-dashboards DashboardsTree', () => {
it('calls onFolderClick when a folder button is clicked', async () => {
const handler = jest.fn();
render(
const { user } = render(
<DashboardsTree
permissions={mockPermissions}
items={[folder]}
@@ -196,7 +185,7 @@ describe('browse-dashboards DashboardsTree', () => {
/>
);
const folderButton = screen.getByLabelText(`Expand folder ${folder.item.title}`);
await userEvent.click(folderButton);
await user.click(folderButton);
expect(handler).toHaveBeenCalledWith(folder.item.uid, true);
});
@@ -216,6 +205,6 @@ describe('browse-dashboards DashboardsTree', () => {
requestLoadMore={requestLoadMore}
/>
);
expect(screen.queryByText('No items')).toBeInTheDocument();
expect(screen.getByText('No items')).toBeInTheDocument();
});
});
@@ -1,28 +1,12 @@
import { Chance } from 'chance';
import { getFolderFixtures } from '@grafana/test-utils/unstable';
import { DashboardViewItem } from 'app/features/search/types';
import { DashboardsTreeItem, UIDashboardViewItem } from '../types';
export function wellFormedEmptyFolder(
seed = 1,
partial?: Partial<DashboardsTreeItem<UIDashboardViewItem>>
): DashboardsTreeItem<UIDashboardViewItem> {
const random = Chance(seed);
return {
item: {
kind: 'ui',
uiKind: 'empty-folder',
uid: random.guid(),
},
level: 0,
isOpen: false,
...partial,
};
}
import { DashboardsTreeItem } from '../types';
/**
* @deprecated Use wellFormedTree from @grafana/test-utils/unstable instead (or re-evaluate test approach in general)
*/
export function wellFormedDashboard(
seed = 1,
partial?: Partial<DashboardsTreeItem<DashboardViewItem>>,
@@ -44,6 +28,9 @@ export function wellFormedDashboard(
};
}
/**
* @deprecated Use wellFormedTree from @grafana/test-utils/unstable instead (or re-evaluate test approach in general)
*/
export function wellFormedFolder(
seed = 1,
partial?: Partial<DashboardsTreeItem<DashboardViewItem>>,
@@ -66,6 +53,9 @@ export function wellFormedFolder(
};
}
/**
* @deprecated Use wellFormedTree from @grafana/test-utils/unstable instead (or re-evaluate test approach in general)
*/
export function sharedWithMeFolder(seed = 1): DashboardsTreeItem<DashboardViewItem> {
const folder = wellFormedFolder(seed, undefined, {
uid: 'sharedwithme',
@@ -73,15 +63,3 @@ export function sharedWithMeFolder(seed = 1): DashboardsTreeItem<DashboardViewIt
});
return folder;
}
export function treeViewersCanEdit() {
const [, { folderA, folderC }] = getFolderFixtures();
return [
[folderA, folderC],
{
folderA,
folderC,
},
] as const;
}
@@ -22,13 +22,6 @@ jest.mock('react-router-dom-v5-compat', () => ({
useNavigate: () => mockNavigate,
}));
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getAppEvents: () => ({
publish: jest.fn(),
}),
}));
jest.mock('../hooks/useCreateOrUpdateRepository');
jest.mock('app/api/clients/provisioning/v0alpha1', () => ({
...jest.requireActual('app/api/clients/provisioning/v0alpha1'),
@@ -38,10 +31,6 @@ jest.mock('app/api/clients/provisioning/v0alpha1', () => ({
useCreateRepositoryJobsMutation: jest.fn(),
}));
jest.mock('app/features/browse-dashboards/api/services', () => ({
PAGE_SIZE: 20,
}));
const mockUseCreateOrUpdateRepository = useCreateOrUpdateRepository as jest.MockedFunction<
typeof useCreateOrUpdateRepository
>;