mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Chore: Unit tests for FolderSection
(#50961)
* unit test scaffolding for FolderSection * remove redundant spinner + more test scaffolding * more test experimentation * clear localStorage between test runs * more tests + add back spinner when renderStandaloneBody * bit of tidy up * update Spinner data-testid * fix import ordering
This commit is contained in:
parent
fcbe0059c2
commit
1b9dd378f2
@ -32,7 +32,7 @@ export const Spinner: FC<Props> = (props: Props) => {
|
||||
const { className, inline = false, iconClassName, style, size = 16 } = props;
|
||||
const styles = getStyles(size, inline);
|
||||
return (
|
||||
<div style={style} className={cx(styles.wrapper, className)}>
|
||||
<div data-testid="Spinner" style={style} className={cx(styles.wrapper, className)}>
|
||||
<Icon className={cx('fa-spin', iconClassName)} name="fa fa-spinner" />
|
||||
</div>
|
||||
);
|
||||
|
@ -0,0 +1,209 @@
|
||||
import { render, screen, act } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
|
||||
import { ArrayVector, DataFrame, DataFrameView, FieldType } from '@grafana/data';
|
||||
|
||||
import { DashboardQueryResult, getGrafanaSearcher, QueryResponse } from '../../service';
|
||||
import { DashboardSearchItemType } from '../../types';
|
||||
|
||||
import { FolderSection } from './FolderSection';
|
||||
|
||||
describe('FolderSection', () => {
|
||||
let grafanaSearcherSpy: jest.SpyInstance;
|
||||
const mockOnTagSelected = jest.fn();
|
||||
const mockSelectionToggle = jest.fn();
|
||||
const mockSelection = jest.fn();
|
||||
const mockSection = {
|
||||
kind: 'folder',
|
||||
uid: 'my-folder',
|
||||
title: 'My folder',
|
||||
};
|
||||
|
||||
// need to make sure we clear localStorage
|
||||
// otherwise tests can interfere with each other and the starting expanded state of the component
|
||||
afterEach(() => {
|
||||
window.localStorage.clear();
|
||||
});
|
||||
|
||||
describe('when where are no results', () => {
|
||||
const emptySearchData: DataFrame = {
|
||||
fields: [
|
||||
{ name: 'kind', type: FieldType.string, config: {}, values: new ArrayVector([]) },
|
||||
{ name: 'name', type: FieldType.string, config: {}, values: new ArrayVector([]) },
|
||||
{ name: 'uid', type: FieldType.string, config: {}, values: new ArrayVector([]) },
|
||||
{ name: 'url', type: FieldType.string, config: {}, values: new ArrayVector([]) },
|
||||
{ name: 'tags', type: FieldType.other, config: {}, values: new ArrayVector([]) },
|
||||
{ name: 'location', type: FieldType.string, config: {}, values: new ArrayVector([]) },
|
||||
],
|
||||
length: 0,
|
||||
};
|
||||
|
||||
const mockSearchResult: QueryResponse = {
|
||||
isItemLoaded: jest.fn(),
|
||||
loadMoreItems: jest.fn(),
|
||||
totalRows: emptySearchData.length,
|
||||
view: new DataFrameView<DashboardQueryResult>(emptySearchData),
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
grafanaSearcherSpy = jest.spyOn(getGrafanaSearcher(), 'search').mockResolvedValue(mockSearchResult);
|
||||
});
|
||||
|
||||
it('shows the folder title as the header', async () => {
|
||||
render(<FolderSection section={mockSection} onTagSelected={mockOnTagSelected} />);
|
||||
expect(await screen.findByRole('button', { name: mockSection.title })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('when renderStandaloneBody is set', () => {
|
||||
it('shows a "No results found" message and does not show the folder title header', async () => {
|
||||
render(<FolderSection renderStandaloneBody section={mockSection} onTagSelected={mockOnTagSelected} />);
|
||||
expect(await screen.findByText('No results found')).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: mockSection.title })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a loading spinner whilst waiting for the results', async () => {
|
||||
// mock the query promise so we can resolve manually
|
||||
let promiseResolver: (arg0: QueryResponse) => void;
|
||||
const promise = new Promise((resolve) => {
|
||||
promiseResolver = resolve;
|
||||
});
|
||||
grafanaSearcherSpy.mockImplementationOnce(() => promise);
|
||||
|
||||
render(<FolderSection renderStandaloneBody section={mockSection} onTagSelected={mockOnTagSelected} />);
|
||||
expect(await screen.findByTestId('Spinner')).toBeInTheDocument();
|
||||
|
||||
// resolve the promise
|
||||
await act(async () => {
|
||||
promiseResolver(mockSearchResult);
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('Spinner')).not.toBeInTheDocument();
|
||||
expect(await screen.findByText('No results found')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows a "No results found" message when expanding the folder', async () => {
|
||||
render(<FolderSection section={mockSection} onTagSelected={mockOnTagSelected} />);
|
||||
|
||||
await userEvent.click(await screen.findByRole('button', { name: mockSection.title }));
|
||||
expect(getGrafanaSearcher().search).toHaveBeenCalled();
|
||||
expect(await screen.findByText('No results found')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are results', () => {
|
||||
const searchData: DataFrame = {
|
||||
fields: [
|
||||
{ name: 'kind', type: FieldType.string, config: {}, values: new ArrayVector([DashboardSearchItemType.DashDB]) },
|
||||
{ name: 'name', type: FieldType.string, config: {}, values: new ArrayVector(['My dashboard 1']) },
|
||||
{ name: 'uid', type: FieldType.string, config: {}, values: new ArrayVector(['my-dashboard-1']) },
|
||||
{ name: 'url', type: FieldType.string, config: {}, values: new ArrayVector(['/my-dashboard-1']) },
|
||||
{ name: 'tags', type: FieldType.other, config: {}, values: new ArrayVector([['foo', 'bar']]) },
|
||||
{ name: 'location', type: FieldType.string, config: {}, values: new ArrayVector(['/my-dashboard-1']) },
|
||||
],
|
||||
length: 1,
|
||||
};
|
||||
|
||||
const mockSearchResult: QueryResponse = {
|
||||
isItemLoaded: jest.fn(),
|
||||
loadMoreItems: jest.fn(),
|
||||
totalRows: searchData.length,
|
||||
view: new DataFrameView<DashboardQueryResult>(searchData),
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
grafanaSearcherSpy = jest.spyOn(getGrafanaSearcher(), 'search').mockResolvedValue(mockSearchResult);
|
||||
});
|
||||
|
||||
it('shows the folder title as the header', async () => {
|
||||
render(<FolderSection section={mockSection} onTagSelected={mockOnTagSelected} />);
|
||||
expect(await screen.findByRole('button', { name: mockSection.title })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('when renderStandaloneBody is set', () => {
|
||||
it('shows the folder children and does not render the folder title', async () => {
|
||||
render(<FolderSection renderStandaloneBody section={mockSection} onTagSelected={mockOnTagSelected} />);
|
||||
expect(await screen.findByText('My dashboard 1')).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: mockSection.title })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a loading spinner whilst waiting for the results', async () => {
|
||||
// mock the query promise so we can resolve manually
|
||||
let promiseResolver: (arg0: QueryResponse) => void;
|
||||
const promise = new Promise((resolve) => {
|
||||
promiseResolver = resolve;
|
||||
});
|
||||
grafanaSearcherSpy.mockImplementationOnce(() => promise);
|
||||
|
||||
render(<FolderSection renderStandaloneBody section={mockSection} onTagSelected={mockOnTagSelected} />);
|
||||
expect(await screen.findByTestId('Spinner')).toBeInTheDocument();
|
||||
|
||||
// resolve the promise
|
||||
await act(async () => {
|
||||
promiseResolver(mockSearchResult);
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('Spinner')).not.toBeInTheDocument();
|
||||
expect(await screen.findByText('My dashboard 1')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows the folder contents when expanding the folder', async () => {
|
||||
render(<FolderSection section={mockSection} onTagSelected={mockOnTagSelected} />);
|
||||
|
||||
await userEvent.click(await screen.findByRole('button', { name: mockSection.title }));
|
||||
expect(getGrafanaSearcher().search).toHaveBeenCalled();
|
||||
expect(await screen.findByText('My dashboard 1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('when clicking the checkbox', () => {
|
||||
it('does not expand the section', async () => {
|
||||
render(
|
||||
<FolderSection
|
||||
section={mockSection}
|
||||
selection={mockSelection}
|
||||
selectionToggle={mockSelectionToggle}
|
||||
onTagSelected={mockOnTagSelected}
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(await screen.findByRole('checkbox', { name: 'Select folder' }));
|
||||
expect(screen.queryByText('My dashboard 1')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('selects only the folder if the folder is not expanded', async () => {
|
||||
render(
|
||||
<FolderSection
|
||||
section={mockSection}
|
||||
selection={mockSelection}
|
||||
selectionToggle={mockSelectionToggle}
|
||||
onTagSelected={mockOnTagSelected}
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(await screen.findByRole('checkbox', { name: 'Select folder' }));
|
||||
expect(mockSelectionToggle).toHaveBeenCalledWith('folder', 'my-folder');
|
||||
expect(mockSelectionToggle).not.toHaveBeenCalledWith('dashboard', 'my-dashboard-1');
|
||||
});
|
||||
|
||||
it('selects the folder and all children when the folder is expanded', async () => {
|
||||
render(
|
||||
<FolderSection
|
||||
section={mockSection}
|
||||
selection={mockSelection}
|
||||
selectionToggle={mockSelectionToggle}
|
||||
onTagSelected={mockOnTagSelected}
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(await screen.findByRole('button', { name: mockSection.title }));
|
||||
expect(getGrafanaSearcher().search).toHaveBeenCalled();
|
||||
|
||||
await userEvent.click(await screen.findByRole('checkbox', { name: 'Select folder' }));
|
||||
expect(mockSelectionToggle).toHaveBeenCalledWith('folder', 'my-folder');
|
||||
expect(mockSelectionToggle).toHaveBeenCalledWith('dashboard', 'my-dashboard-1');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -3,7 +3,7 @@ import React, { FC } from 'react';
|
||||
import { useAsync, useLocalStorage } from 'react-use';
|
||||
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { Card, Checkbox, CollapsableSection, Icon, Spinner, stylesFactory, useTheme } from '@grafana/ui';
|
||||
import { Card, Checkbox, CollapsableSection, Icon, IconName, Spinner, stylesFactory, useTheme } from '@grafana/ui';
|
||||
import { getSectionStorageKey } from 'app/features/search/utils';
|
||||
import { useUniqueId } from 'app/plugins/datasource/influxdb/components/useUniqueId';
|
||||
|
||||
@ -18,7 +18,7 @@ export interface DashboardSection {
|
||||
title: string;
|
||||
selected?: boolean; // not used ? keyboard
|
||||
url?: string;
|
||||
icon?: string;
|
||||
icon?: IconName;
|
||||
itemsUIDs?: string[]; // for pseudo folders
|
||||
}
|
||||
|
||||
@ -46,7 +46,7 @@ export const FolderSection: FC<SectionHeaderProps> = ({
|
||||
|
||||
const results = useAsync(async () => {
|
||||
if (!sectionExpanded && !renderStandaloneBody) {
|
||||
return Promise.resolve([] as DashboardSectionItem[]);
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
let folderUid: string | undefined = section.uid;
|
||||
let folderTitle: string | undefined = section.title;
|
||||
@ -65,21 +65,18 @@ export const FolderSection: FC<SectionHeaderProps> = ({
|
||||
}
|
||||
|
||||
const raw = await getGrafanaSearcher().search({ ...query, tags });
|
||||
const v = raw.view.map(
|
||||
(item) =>
|
||||
({
|
||||
uid: item.uid,
|
||||
title: item.name,
|
||||
url: item.url,
|
||||
uri: item.url,
|
||||
type: item.kind === 'folder' ? DashboardSearchItemType.DashFolder : DashboardSearchItemType.DashDB,
|
||||
id: 666, // do not use me!
|
||||
isStarred: false,
|
||||
tags: item.tags ?? [],
|
||||
folderUid,
|
||||
folderTitle,
|
||||
} as DashboardSectionItem)
|
||||
);
|
||||
const v = raw.view.map<DashboardSectionItem>((item) => ({
|
||||
uid: item.uid,
|
||||
title: item.name,
|
||||
url: item.url,
|
||||
uri: item.url,
|
||||
type: item.kind === 'folder' ? DashboardSearchItemType.DashFolder : DashboardSearchItemType.DashDB,
|
||||
id: 666, // do not use me!
|
||||
isStarred: false,
|
||||
tags: item.tags ?? [],
|
||||
folderUid,
|
||||
folderTitle,
|
||||
}));
|
||||
return v;
|
||||
}, [sectionExpanded, section, tags]);
|
||||
|
||||
@ -117,11 +114,9 @@ export const FolderSection: FC<SectionHeaderProps> = ({
|
||||
}
|
||||
|
||||
const renderResults = () => {
|
||||
if (!results.value?.length) {
|
||||
if (results.loading) {
|
||||
return <Spinner className={styles.spinner} />;
|
||||
}
|
||||
|
||||
if (!results.value) {
|
||||
return null;
|
||||
} else if (results.value.length === 0) {
|
||||
return (
|
||||
<Card>
|
||||
<Card.Heading>No results found</Card.Heading>
|
||||
@ -151,7 +146,11 @@ export const FolderSection: FC<SectionHeaderProps> = ({
|
||||
|
||||
// Skip the folder wrapper
|
||||
if (renderStandaloneBody) {
|
||||
return <div className={styles.folderViewResults}>{renderResults()}</div>;
|
||||
return (
|
||||
<div className={styles.folderViewResults}>
|
||||
{!results.value?.length && results.loading ? <Spinner className={styles.spinner} /> : renderResults()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@ -171,7 +170,7 @@ export const FolderSection: FC<SectionHeaderProps> = ({
|
||||
)}
|
||||
|
||||
<div className={styles.icon}>
|
||||
<Icon name={icon as any} />
|
||||
<Icon name={icon} />
|
||||
</div>
|
||||
|
||||
<div className={styles.text}>
|
||||
|
@ -36,7 +36,7 @@ export const FolderView = ({ selection, selectionToggle, onTagSelected, tags, hi
|
||||
if (ids.length) {
|
||||
const itemsUIDs = await getBackendSrv().get(`/api/dashboards/ids/${ids.slice(0, 30).join(',')}`);
|
||||
if (itemsUIDs.length) {
|
||||
folders.push({ title: 'Recent', icon: 'clock', kind: 'query-recent', uid: '__recent', itemsUIDs });
|
||||
folders.push({ title: 'Recent', icon: 'clock-nine', kind: 'query-recent', uid: '__recent', itemsUIDs });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user