Folders: Refactor Browse/Dashboards list tests (#109421)
This commit is contained in:
@@ -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
|
||||
>;
|
||||
|
||||
Reference in New Issue
Block a user