2025-08-01 20:34:33 +08:00
|
|
|
import { fireEvent, render, screen } from 'test/test-utils';
|
2023-07-12 17:37:08 +08:00
|
|
|
|
2025-08-06 15:07:23 +08:00
|
|
|
import { setBackendSrv } from '@grafana/runtime';
|
2025-08-01 20:34:33 +08:00
|
|
|
import { setupMockServer } from '@grafana/test-utils/server';
|
|
|
|
import { getFolderFixtures } from '@grafana/test-utils/unstable';
|
2023-07-12 17:37:08 +08:00
|
|
|
import { backendSrv } from 'app/core/services/backend_srv';
|
|
|
|
|
|
|
|
import { NestedFolderPicker } from './NestedFolderPicker';
|
|
|
|
|
2025-08-01 20:34:33 +08:00
|
|
|
const [_, { folderA, folderB, folderC, folderA_folderA, folderA_folderB, folderA_folderC }] = getFolderFixtures();
|
2023-07-12 17:37:08 +08:00
|
|
|
|
2025-08-01 20:34:33 +08:00
|
|
|
setupMockServer();
|
|
|
|
setBackendSrv(backendSrv);
|
2023-07-12 17:37:08 +08:00
|
|
|
|
|
|
|
describe('NestedFolderPicker', () => {
|
|
|
|
const mockOnChange = jest.fn();
|
2023-07-19 22:32:55 +08:00
|
|
|
const originalScrollIntoView = window.HTMLElement.prototype.scrollIntoView;
|
2023-07-12 17:37:08 +08:00
|
|
|
|
|
|
|
beforeAll(() => {
|
2023-07-19 22:32:55 +08:00
|
|
|
window.HTMLElement.prototype.scrollIntoView = function () {};
|
2023-07-12 17:37:08 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
afterAll(() => {
|
2023-07-19 22:32:55 +08:00
|
|
|
window.HTMLElement.prototype.scrollIntoView = originalScrollIntoView;
|
2023-07-12 17:37:08 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
afterEach(() => {
|
2023-07-19 22:32:55 +08:00
|
|
|
jest.resetAllMocks();
|
2023-07-12 17:37:08 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
it('renders a button with the correct label when no folder is selected', async () => {
|
|
|
|
render(<NestedFolderPicker onChange={mockOnChange} />);
|
|
|
|
expect(await screen.findByRole('button', { name: 'Select folder' })).toBeInTheDocument();
|
|
|
|
});
|
|
|
|
|
2023-07-19 22:32:55 +08:00
|
|
|
it('renders a button with the correct label when a folder is selected', async () => {
|
2025-08-01 20:34:33 +08:00
|
|
|
render(<NestedFolderPicker onChange={mockOnChange} value={folderA.item.uid} />);
|
2023-07-19 22:32:55 +08:00
|
|
|
expect(
|
|
|
|
await screen.findByRole('button', { name: `Select folder: ${folderA.item.title} currently selected` })
|
|
|
|
).toBeInTheDocument();
|
2023-07-12 17:37:08 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
it('clicking the button opens the folder picker', async () => {
|
2025-08-01 20:34:33 +08:00
|
|
|
const { user } = render(<NestedFolderPicker onChange={mockOnChange} />);
|
2023-07-12 17:37:08 +08:00
|
|
|
|
2023-07-19 21:10:43 +08:00
|
|
|
// Open the picker and wait for children to load
|
|
|
|
const button = await screen.findByRole('button', { name: 'Select folder' });
|
2025-08-01 20:34:33 +08:00
|
|
|
await user.click(button);
|
2023-07-19 21:10:43 +08:00
|
|
|
await screen.findByLabelText(folderA.item.title);
|
2023-07-12 17:37:08 +08:00
|
|
|
|
|
|
|
// Select folder button is no longer visible
|
|
|
|
expect(screen.queryByRole('button', { name: 'Select folder' })).not.toBeInTheDocument();
|
|
|
|
|
|
|
|
// Search input and folder tree are visible
|
2023-07-14 18:58:43 +08:00
|
|
|
expect(screen.getByPlaceholderText('Search folders')).toBeInTheDocument();
|
2023-07-12 17:37:08 +08:00
|
|
|
expect(screen.getByLabelText('Dashboards')).toBeInTheDocument();
|
|
|
|
expect(screen.getByLabelText(folderA.item.title)).toBeInTheDocument();
|
2024-03-19 19:44:52 +08:00
|
|
|
// expect(screen.getByLabelText(folderB.item.title)).toBeInTheDocument();
|
2023-07-12 17:37:08 +08:00
|
|
|
expect(screen.getByLabelText(folderC.item.title)).toBeInTheDocument();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('can select a folder from the picker', async () => {
|
2025-08-01 20:34:33 +08:00
|
|
|
const { user } = render(<NestedFolderPicker onChange={mockOnChange} />);
|
2023-07-12 17:37:08 +08:00
|
|
|
|
2023-07-19 21:10:43 +08:00
|
|
|
// Open the picker and wait for children to load
|
|
|
|
const button = await screen.findByRole('button', { name: 'Select folder' });
|
2025-08-01 20:34:33 +08:00
|
|
|
await user.click(button);
|
2023-07-19 21:10:43 +08:00
|
|
|
await screen.findByLabelText(folderA.item.title);
|
2023-07-12 17:37:08 +08:00
|
|
|
|
2025-08-01 20:34:33 +08:00
|
|
|
await user.click(screen.getByLabelText(folderA.item.title));
|
2023-07-21 22:40:39 +08:00
|
|
|
expect(mockOnChange).toHaveBeenCalledWith(folderA.item.uid, folderA.item.title);
|
2023-07-12 17:37:08 +08:00
|
|
|
});
|
|
|
|
|
2024-01-24 21:18:01 +08:00
|
|
|
it('can clear a selection if clearable is specified', async () => {
|
2025-08-01 20:34:33 +08:00
|
|
|
const { user } = render(<NestedFolderPicker clearable value={folderA.item.uid} onChange={mockOnChange} />);
|
2024-01-24 21:18:01 +08:00
|
|
|
|
2025-08-01 20:34:33 +08:00
|
|
|
await user.click(await screen.findByRole('button', { name: 'Clear selection' }));
|
2024-01-24 21:18:01 +08:00
|
|
|
expect(mockOnChange).toHaveBeenCalledWith(undefined, undefined);
|
|
|
|
});
|
|
|
|
|
2023-07-19 22:32:55 +08:00
|
|
|
it('can select a folder from the picker with the keyboard', async () => {
|
2025-08-01 20:34:33 +08:00
|
|
|
const { user } = render(<NestedFolderPicker onChange={mockOnChange} />);
|
2023-07-19 22:32:55 +08:00
|
|
|
const button = await screen.findByRole('button', { name: 'Select folder' });
|
|
|
|
|
2025-08-01 20:34:33 +08:00
|
|
|
await user.click(button);
|
2023-07-19 22:32:55 +08:00
|
|
|
|
2025-08-01 20:34:33 +08:00
|
|
|
await user.keyboard('{ArrowDown}{ArrowDown}{Enter}');
|
|
|
|
expect(mockOnChange).toHaveBeenCalledWith(folderC.item.uid, folderC.item.title);
|
2023-07-19 22:32:55 +08:00
|
|
|
});
|
|
|
|
|
2024-02-22 02:02:37 +08:00
|
|
|
it('shows the root folder by default', async () => {
|
2025-08-01 20:34:33 +08:00
|
|
|
const { user } = render(<NestedFolderPicker onChange={mockOnChange} />);
|
2024-02-22 02:02:37 +08:00
|
|
|
|
|
|
|
// Open the picker and wait for children to load
|
|
|
|
const button = await screen.findByRole('button', { name: 'Select folder' });
|
2025-08-01 20:34:33 +08:00
|
|
|
await user.click(button);
|
2024-02-22 02:02:37 +08:00
|
|
|
await screen.findByLabelText(folderA.item.title);
|
|
|
|
|
2025-08-01 20:34:33 +08:00
|
|
|
await user.click(screen.getByLabelText('Dashboards'));
|
2024-02-22 02:02:37 +08:00
|
|
|
expect(mockOnChange).toHaveBeenCalledWith('', 'Dashboards');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('hides the root folder if the prop says so', async () => {
|
2025-08-01 20:34:33 +08:00
|
|
|
const { user } = render(<NestedFolderPicker showRootFolder={false} onChange={mockOnChange} />);
|
2024-02-22 02:02:37 +08:00
|
|
|
|
|
|
|
// Open the picker and wait for children to load
|
|
|
|
const button = await screen.findByRole('button', { name: 'Select folder' });
|
2025-08-01 20:34:33 +08:00
|
|
|
await user.click(button);
|
2024-02-22 02:02:37 +08:00
|
|
|
await screen.findByLabelText(folderA.item.title);
|
|
|
|
|
|
|
|
expect(screen.queryByLabelText('Dashboards')).not.toBeInTheDocument();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('hides folders specififed by UID', async () => {
|
2025-08-01 20:34:33 +08:00
|
|
|
const { user } = render(<NestedFolderPicker excludeUIDs={[folderC.item.uid]} onChange={mockOnChange} />);
|
2024-02-22 02:02:37 +08:00
|
|
|
|
|
|
|
// Open the picker and wait for children to load
|
|
|
|
const button = await screen.findByRole('button', { name: 'Select folder' });
|
2025-08-01 20:34:33 +08:00
|
|
|
await user.click(button);
|
2024-03-19 19:44:52 +08:00
|
|
|
await screen.findByLabelText(folderA.item.title);
|
2024-02-22 02:02:37 +08:00
|
|
|
|
2024-03-19 19:44:52 +08:00
|
|
|
expect(screen.queryByLabelText(folderC.item.title)).not.toBeInTheDocument();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('by default only shows items the user can edit', async () => {
|
2025-08-01 20:34:33 +08:00
|
|
|
const { user } = render(<NestedFolderPicker onChange={mockOnChange} />);
|
2024-03-19 19:44:52 +08:00
|
|
|
|
|
|
|
const button = await screen.findByRole('button', { name: 'Select folder' });
|
2025-08-01 20:34:33 +08:00
|
|
|
await user.click(button);
|
2024-03-19 19:44:52 +08:00
|
|
|
await screen.findByLabelText(folderA.item.title);
|
|
|
|
|
|
|
|
expect(screen.queryByLabelText(folderB.item.title)).not.toBeInTheDocument(); // folderB is not editable
|
|
|
|
expect(screen.getByLabelText(folderC.item.title)).toBeInTheDocument(); // but folderC is
|
|
|
|
});
|
|
|
|
|
|
|
|
it('shows items the user can view, with the prop', async () => {
|
2025-08-01 20:34:33 +08:00
|
|
|
const { user } = render(<NestedFolderPicker permission="view" onChange={mockOnChange} />);
|
2024-03-19 19:44:52 +08:00
|
|
|
|
|
|
|
const button = await screen.findByRole('button', { name: 'Select folder' });
|
2025-08-01 20:34:33 +08:00
|
|
|
await user.click(button);
|
2024-03-19 19:44:52 +08:00
|
|
|
await screen.findByLabelText(folderA.item.title);
|
|
|
|
|
|
|
|
expect(screen.getByLabelText(folderB.item.title)).toBeInTheDocument();
|
|
|
|
expect(screen.getByLabelText(folderC.item.title)).toBeInTheDocument();
|
2024-02-22 02:02:37 +08:00
|
|
|
});
|
|
|
|
|
2025-08-06 15:07:23 +08:00
|
|
|
it('can expand and collapse a folder to show its children', async () => {
|
|
|
|
const { user } = render(<NestedFolderPicker permission="view" onChange={mockOnChange} />);
|
2023-07-19 22:32:55 +08:00
|
|
|
|
2025-08-06 15:07:23 +08:00
|
|
|
// Open the picker and wait for children to load
|
|
|
|
const button = await screen.findByRole('button', { name: 'Select folder' });
|
|
|
|
await user.click(button);
|
|
|
|
await screen.findByLabelText(folderA.item.title);
|
2023-07-19 22:32:55 +08:00
|
|
|
|
2025-08-06 15:07:23 +08:00
|
|
|
// Expand Folder A
|
|
|
|
// Note: we need to use mouseDown here because userEvent's click event doesn't get prevented correctly
|
|
|
|
fireEvent.mouseDown(screen.getByRole('button', { name: `Expand folder ${folderA.item.title}` }));
|
2024-01-15 19:43:19 +08:00
|
|
|
|
2025-08-06 15:07:23 +08:00
|
|
|
// Folder A's children are visible
|
|
|
|
expect(await screen.findByLabelText(folderA_folderA.item.title)).toBeInTheDocument();
|
|
|
|
expect(await screen.findByLabelText(folderA_folderB.item.title)).toBeInTheDocument();
|
2024-01-15 19:43:19 +08:00
|
|
|
|
2025-08-06 15:07:23 +08:00
|
|
|
// Collapse Folder A
|
|
|
|
// Note: we need to use mouseDown here because userEvent's click event doesn't get prevented correctly
|
|
|
|
fireEvent.mouseDown(screen.getByRole('button', { name: `Collapse folder ${folderA.item.title}` }));
|
|
|
|
expect(screen.queryByLabelText(folderA_folderA.item.title)).not.toBeInTheDocument();
|
|
|
|
expect(screen.queryByLabelText(folderA_folderB.item.title)).not.toBeInTheDocument();
|
2024-01-15 19:43:19 +08:00
|
|
|
|
2025-08-06 15:07:23 +08:00
|
|
|
// Expand Folder A again
|
|
|
|
// Note: we need to use mouseDown here because userEvent's click event doesn't get prevented correctly
|
|
|
|
fireEvent.mouseDown(screen.getByRole('button', { name: `Expand folder ${folderA.item.title}` }));
|
2024-01-15 19:43:19 +08:00
|
|
|
|
2025-08-06 15:07:23 +08:00
|
|
|
// Select the first child
|
|
|
|
await user.click(screen.getByLabelText(folderA_folderA.item.title));
|
|
|
|
expect(mockOnChange).toHaveBeenCalledWith(folderA_folderA.item.uid, folderA_folderA.item.title);
|
2023-07-19 22:32:55 +08:00
|
|
|
});
|
|
|
|
|
2025-08-06 15:07:23 +08:00
|
|
|
it('can expand and collapse a folder to show its children with the keyboard', async () => {
|
|
|
|
const { user } = render(<NestedFolderPicker permission="view" onChange={mockOnChange} />);
|
|
|
|
const button = await screen.findByRole('button', { name: 'Select folder' });
|
2024-01-15 19:43:19 +08:00
|
|
|
|
2025-08-06 15:07:23 +08:00
|
|
|
await user.click(button);
|
2023-07-19 22:32:55 +08:00
|
|
|
|
2025-08-06 15:07:23 +08:00
|
|
|
// Expand Folder A
|
|
|
|
await user.keyboard('{ArrowDown}{ArrowDown}{ArrowDown}{ArrowDown}{ArrowRight}');
|
2023-07-12 17:37:08 +08:00
|
|
|
|
2025-08-06 15:07:23 +08:00
|
|
|
// Folder A's children are visible
|
|
|
|
expect(await screen.findByLabelText(folderA_folderA.item.title)).toBeInTheDocument();
|
|
|
|
expect(await screen.findByLabelText(folderA_folderB.item.title)).toBeInTheDocument();
|
|
|
|
expect(await screen.findByLabelText(folderA_folderC.item.title)).toBeInTheDocument();
|
2023-07-12 17:37:08 +08:00
|
|
|
|
2025-08-06 15:07:23 +08:00
|
|
|
// Collapse Folder A
|
|
|
|
await user.keyboard('{ArrowLeft}');
|
|
|
|
expect(screen.queryByLabelText(folderA_folderA.item.title)).not.toBeInTheDocument();
|
|
|
|
expect(screen.queryByLabelText(folderA_folderB.item.title)).not.toBeInTheDocument();
|
2023-07-19 22:32:55 +08:00
|
|
|
|
2025-08-06 15:07:23 +08:00
|
|
|
// Expand Folder A again
|
|
|
|
await user.keyboard('{ArrowRight}');
|
2023-07-19 22:32:55 +08:00
|
|
|
|
2025-08-06 15:07:23 +08:00
|
|
|
// Select the first child
|
|
|
|
await user.keyboard('{ArrowDown}{Enter}');
|
|
|
|
expect(mockOnChange).toHaveBeenCalledWith(folderA_folderC.item.uid, folderA_folderC.item.title);
|
2023-07-12 17:37:08 +08:00
|
|
|
});
|
|
|
|
});
|