diff --git a/packages/grafana-test-utils/src/handlers/all-handlers.ts b/packages/grafana-test-utils/src/handlers/all-handlers.ts index 89662264875..4b4bf959496 100644 --- a/packages/grafana-test-utils/src/handlers/all-handlers.ts +++ b/packages/grafana-test-utils/src/handlers/all-handlers.ts @@ -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, diff --git a/packages/grafana-test-utils/src/handlers/api/folders/handlers.ts b/packages/grafana-test-utils/src/handlers/api/folders/handlers.ts index 2dcac8b0160..6da13af3efe 100644 --- a/packages/grafana-test-utils/src/handlers/api/folders/handlers.ts +++ b/packages/grafana-test-utils/src/handlers/api/folders/handlers.ts @@ -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, diff --git a/packages/grafana-test-utils/src/handlers/api/search/handlers.ts b/packages/grafana-test-utils/src/handlers/api/search/handlers.ts new file mode 100644 index 00000000000..195af7dac1d --- /dev/null +++ b/packages/grafana-test-utils/src/handlers/api/search/handlers.ts @@ -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()]; diff --git a/public/app/api/clients/folder/v1beta1/hooks.test.ts b/public/app/api/clients/folder/v1beta1/hooks.test.ts index e974c563b33..5c3936a4847 100644 --- a/public/app/api/clients/folder/v1beta1/hooks.test.ts +++ b/public/app/api/clients/folder/v1beta1/hooks.test.ts @@ -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, diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx index a9a0dda6274..b7775e5c249 100644 --- a/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx @@ -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) { }; } -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, diff --git a/public/app/features/browse-dashboards/components/BrowseView.test.tsx b/public/app/features/browse-dashboards/components/BrowseView.test.tsx index 929f5bbf53c..d9e7fa6c806 100644 --- a/public/app/features/browse-dashboards/components/BrowseView.test.tsx +++ b/public/app/features/browse-dashboards/components/BrowseView.test.tsx @@ -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) { - rtlRender({ui}, 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( - + ); expect(await screen.findByText('This folder is empty')).toBeInTheDocument(); }); diff --git a/public/app/features/browse-dashboards/components/DashboardsTree.test.tsx b/public/app/features/browse-dashboards/components/DashboardsTree.test.tsx index e8109397870..3e1b597405a 100644 --- a/public/app/features/browse-dashboards/components/DashboardsTree.test.tsx +++ b/public/app/features/browse-dashboards/components/DashboardsTree.test.tsx @@ -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) { - rtlRender({ui}, 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( { /> ); 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(); }); }); diff --git a/public/app/features/browse-dashboards/fixtures/dashboardsTreeItem.fixture.ts b/public/app/features/browse-dashboards/fixtures/dashboardsTreeItem.fixture.ts index 7b5d17a2825..f9e12c3f80f 100644 --- a/public/app/features/browse-dashboards/fixtures/dashboardsTreeItem.fixture.ts +++ b/public/app/features/browse-dashboards/fixtures/dashboardsTreeItem.fixture.ts @@ -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 { - 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>, @@ -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>, @@ -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 { const folder = wellFormedFolder(seed, undefined, { uid: 'sharedwithme', @@ -73,15 +63,3 @@ export function sharedWithMeFolder(seed = 1): DashboardsTreeItem ({ 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 >;