Search: add unit tests for FolderView (#51114)
* add unit tests for FolderView * add basic unit test for Alert component * prevent flicker of `No results found`
This commit is contained in:
@@ -44,6 +44,7 @@
|
|||||||
"@react-aria/focus": "3.6.0",
|
"@react-aria/focus": "3.6.0",
|
||||||
"@react-aria/menu": "3.5.0",
|
"@react-aria/menu": "3.5.0",
|
||||||
"@react-aria/overlays": "3.9.0",
|
"@react-aria/overlays": "3.9.0",
|
||||||
|
"@react-aria/utils": "3.13.0",
|
||||||
"@react-stately/menu": "3.3.0",
|
"@react-stately/menu": "3.3.0",
|
||||||
"@sentry/browser": "6.19.7",
|
"@sentry/browser": "6.19.7",
|
||||||
"ansicolor": "1.1.100",
|
"ansicolor": "1.1.100",
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Alert } from './Alert';
|
||||||
|
|
||||||
|
describe('Alert', () => {
|
||||||
|
it('sets the accessible label correctly based on the title', () => {
|
||||||
|
render(<Alert title="Uh oh spagghettios!" />);
|
||||||
|
expect(screen.getByRole('alert', { name: 'Uh oh spagghettios!' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
|
import { useId } from '@react-aria/utils';
|
||||||
import React, { HTMLAttributes, ReactNode } from 'react';
|
import React, { HTMLAttributes, ReactNode } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
@@ -56,19 +57,24 @@ export const Alert = React.forwardRef<HTMLDivElement, Props>(
|
|||||||
) => {
|
) => {
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const styles = getStyles(theme, severity, elevated, bottomSpacing, topSpacing);
|
const styles = getStyles(theme, severity, elevated, bottomSpacing, topSpacing);
|
||||||
|
const titleId = useId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cx(styles.alert, className)}
|
className={cx(styles.alert, className)}
|
||||||
data-testid={selectors.components.Alert.alertV2(severity)}
|
data-testid={selectors.components.Alert.alertV2(severity)}
|
||||||
|
role="alert"
|
||||||
|
aria-labelledby={titleId}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
<div className={styles.icon}>
|
<div className={styles.icon}>
|
||||||
<Icon size="xl" name={getIconFromSeverity(severity) as IconName} />
|
<Icon size="xl" name={getIconFromSeverity(severity) as IconName} />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.body} role="alert">
|
<div className={styles.body}>
|
||||||
<div className={styles.title}>{title}</div>
|
<div id={titleId} className={styles.title}>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
{children && <div className={styles.content}>{children}</div>}
|
{children && <div className={styles.content}>{children}</div>}
|
||||||
</div>
|
</div>
|
||||||
{/* If onRemove is specified, giving preference to onRemove */}
|
{/* If onRemove is specified, giving preference to onRemove */}
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export const FolderSection: FC<SectionHeaderProps> = ({
|
|||||||
const renderResults = () => {
|
const renderResults = () => {
|
||||||
if (!results.value) {
|
if (!results.value) {
|
||||||
return null;
|
return null;
|
||||||
} else if (results.value.length === 0) {
|
} else if (results.value.length === 0 && !results.loading) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<Card.Heading>No results found</Card.Heading>
|
<Card.Heading>No results found</Card.Heading>
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import { render, screen, act } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { ArrayVector, DataFrame, DataFrameView, FieldType } from '@grafana/data';
|
||||||
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
|
||||||
|
import { ContextSrv, setContextSrv } from '../../../../core/services/context_srv';
|
||||||
|
import impressionSrv from '../../../../core/services/impression_srv';
|
||||||
|
import { DashboardQueryResult, getGrafanaSearcher, QueryResponse } from '../../service';
|
||||||
|
import { DashboardSearchItemType } from '../../types';
|
||||||
|
|
||||||
|
import { FolderView } from './FolderView';
|
||||||
|
|
||||||
|
jest.mock('@grafana/runtime', () => ({
|
||||||
|
...jest.requireActual('@grafana/runtime'),
|
||||||
|
getBackendSrv: () => ({
|
||||||
|
get: jest.fn().mockResolvedValue(['foo']),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('FolderView', () => {
|
||||||
|
let grafanaSearcherSpy: jest.SpyInstance;
|
||||||
|
const mockOnTagSelected = jest.fn();
|
||||||
|
const mockSelectionToggle = jest.fn();
|
||||||
|
const mockSelection = jest.fn();
|
||||||
|
|
||||||
|
const folderData: DataFrame = {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'kind',
|
||||||
|
type: FieldType.string,
|
||||||
|
config: {},
|
||||||
|
values: new ArrayVector([DashboardSearchItemType.DashFolder]),
|
||||||
|
},
|
||||||
|
{ name: 'name', type: FieldType.string, config: {}, values: new ArrayVector(['My folder 1']) },
|
||||||
|
{ name: 'uid', type: FieldType.string, config: {}, values: new ArrayVector(['my-folder-1']) },
|
||||||
|
{ name: 'url', type: FieldType.string, config: {}, values: new ArrayVector(['/my-folder-1']) },
|
||||||
|
],
|
||||||
|
length: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockSearchResult: QueryResponse = {
|
||||||
|
isItemLoaded: jest.fn(),
|
||||||
|
loadMoreItems: jest.fn(),
|
||||||
|
totalRows: folderData.length,
|
||||||
|
view: new DataFrameView<DashboardQueryResult>(folderData),
|
||||||
|
};
|
||||||
|
|
||||||
|
let contextSrv: ContextSrv;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
contextSrv = new ContextSrv();
|
||||||
|
setContextSrv(contextSrv);
|
||||||
|
grafanaSearcherSpy = jest.spyOn(getGrafanaSearcher(), 'search').mockResolvedValue(mockSearchResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows a spinner whilst the results are loading', 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(
|
||||||
|
<FolderView onTagSelected={mockOnTagSelected} selection={mockSelection} selectionToggle={mockSelectionToggle} />
|
||||||
|
);
|
||||||
|
expect(await screen.findByTestId('Spinner')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// resolve the promise
|
||||||
|
await act(async () => {
|
||||||
|
promiseResolver(mockSearchResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.queryByTestId('Spinner')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not show the starred items if not signed in', async () => {
|
||||||
|
contextSrv.isSignedIn = false;
|
||||||
|
render(
|
||||||
|
<FolderView onTagSelected={mockOnTagSelected} selection={mockSelection} selectionToggle={mockSelectionToggle} />
|
||||||
|
);
|
||||||
|
expect((await screen.findAllByTestId(selectors.components.Search.sectionV2))[0]).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole('button', { name: 'Starred' })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the starred items if signed in', async () => {
|
||||||
|
contextSrv.isSignedIn = true;
|
||||||
|
render(
|
||||||
|
<FolderView onTagSelected={mockOnTagSelected} selection={mockSelection} selectionToggle={mockSelectionToggle} />
|
||||||
|
);
|
||||||
|
expect(await screen.findByRole('button', { name: 'Starred' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not show the recent items if no dashboards have been opened recently', async () => {
|
||||||
|
jest.spyOn(impressionSrv, 'getDashboardOpened').mockReturnValue([]);
|
||||||
|
render(
|
||||||
|
<FolderView onTagSelected={mockOnTagSelected} selection={mockSelection} selectionToggle={mockSelectionToggle} />
|
||||||
|
);
|
||||||
|
expect((await screen.findAllByTestId(selectors.components.Search.sectionV2))[0]).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole('button', { name: 'Recent' })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the recent items if any dashboards have recently been opened', async () => {
|
||||||
|
jest.spyOn(impressionSrv, 'getDashboardOpened').mockReturnValue([12345]);
|
||||||
|
render(
|
||||||
|
<FolderView onTagSelected={mockOnTagSelected} selection={mockSelection} selectionToggle={mockSelectionToggle} />
|
||||||
|
);
|
||||||
|
expect(await screen.findByRole('button', { name: 'Recent' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the general folder by default', async () => {
|
||||||
|
render(
|
||||||
|
<FolderView onTagSelected={mockOnTagSelected} selection={mockSelection} selectionToggle={mockSelectionToggle} />
|
||||||
|
);
|
||||||
|
expect(await screen.findByRole('button', { name: 'General' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when hidePseudoFolders is set', () => {
|
||||||
|
it('does not show the starred items even if signed in', async () => {
|
||||||
|
contextSrv.isSignedIn = true;
|
||||||
|
render(
|
||||||
|
<FolderView
|
||||||
|
hidePseudoFolders
|
||||||
|
onTagSelected={mockOnTagSelected}
|
||||||
|
selection={mockSelection}
|
||||||
|
selectionToggle={mockSelectionToggle}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(await screen.findByRole('button', { name: 'General' })).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole('button', { name: 'Starred' })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not show the recent items even if recent dashboards have been opened', async () => {
|
||||||
|
jest.spyOn(impressionSrv, 'getDashboardOpened').mockReturnValue([12345]);
|
||||||
|
render(
|
||||||
|
<FolderView
|
||||||
|
hidePseudoFolders
|
||||||
|
onTagSelected={mockOnTagSelected}
|
||||||
|
selection={mockSelection}
|
||||||
|
selectionToggle={mockSelectionToggle}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(await screen.findByRole('button', { name: 'General' })).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole('button', { name: 'Recent' })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows an error state if any of the calls reject for a specific reason', async () => {
|
||||||
|
// reject with a specific Error object
|
||||||
|
grafanaSearcherSpy.mockRejectedValueOnce(new Error('Uh oh spagghettios!'));
|
||||||
|
render(
|
||||||
|
<FolderView onTagSelected={mockOnTagSelected} selection={mockSelection} selectionToggle={mockSelectionToggle} />
|
||||||
|
);
|
||||||
|
expect(await screen.findByRole('alert', { name: 'Uh oh spagghettios!' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows a general error state if any of the calls reject', async () => {
|
||||||
|
// reject with nothing
|
||||||
|
grafanaSearcherSpy.mockRejectedValueOnce(null);
|
||||||
|
render(
|
||||||
|
<FolderView onTagSelected={mockOnTagSelected} selection={mockSelection} selectionToggle={mockSelectionToggle} />
|
||||||
|
);
|
||||||
|
expect(await screen.findByRole('alert', { name: 'Something went wrong' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,10 +5,10 @@ import { useAsync } from 'react-use';
|
|||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { getBackendSrv } from '@grafana/runtime';
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
import { Spinner, useStyles2 } from '@grafana/ui';
|
import { Alert, Spinner, useStyles2 } from '@grafana/ui';
|
||||||
import { contextSrv } from 'app/core/core';
|
|
||||||
import impressionSrv from 'app/core/services/impression_srv';
|
|
||||||
|
|
||||||
|
import { contextSrv } from '../../../../core/services/context_srv';
|
||||||
|
import impressionSrv from '../../../../core/services/impression_srv';
|
||||||
import { GENERAL_FOLDER_UID } from '../../constants';
|
import { GENERAL_FOLDER_UID } from '../../constants';
|
||||||
import { getGrafanaSearcher } from '../../service';
|
import { getGrafanaSearcher } from '../../service';
|
||||||
import { SearchResultsProps } from '../components/SearchResultsTable';
|
import { SearchResultsProps } from '../components/SearchResultsTable';
|
||||||
@@ -58,41 +58,33 @@ export const FolderView = ({ selection, selectionToggle, onTagSelected, tags, hi
|
|||||||
return folders;
|
return folders;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (results.loading) {
|
const renderResults = () => {
|
||||||
return <Spinner />;
|
if (results.loading) {
|
||||||
}
|
return <Spinner className={styles.spinner} />;
|
||||||
if (!results.value) {
|
} else if (!results.value) {
|
||||||
return <div>?</div>;
|
return <Alert className={styles.error} title={results.error ? results.error.message : 'Something went wrong'} />;
|
||||||
}
|
} else {
|
||||||
|
return results.value.map((section) => (
|
||||||
|
<div data-testid={selectors.components.Search.sectionV2} className={styles.section} key={section.title}>
|
||||||
|
{section.title && (
|
||||||
|
<FolderSection
|
||||||
|
selection={selection}
|
||||||
|
selectionToggle={selectionToggle}
|
||||||
|
onTagSelected={onTagSelected}
|
||||||
|
section={section}
|
||||||
|
tags={tags}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return <div className={styles.wrapper}>{renderResults()}</div>;
|
||||||
<div className={styles.wrapper}>
|
|
||||||
{results.value.map((section) => {
|
|
||||||
return (
|
|
||||||
<div data-testid={selectors.components.Search} className={styles.section} key={section.title}>
|
|
||||||
{section.title && (
|
|
||||||
<FolderSection
|
|
||||||
selection={selection}
|
|
||||||
selectionToggle={selectionToggle}
|
|
||||||
onTagSelected={onTagSelected}
|
|
||||||
section={section}
|
|
||||||
tags={tags}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => {
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
const { md, sm } = theme.v1.spacing;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
virtualizedGridItemWrapper: css`
|
|
||||||
padding: 4px;
|
|
||||||
`,
|
|
||||||
wrapper: css`
|
wrapper: css`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -113,40 +105,14 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
border-bottom: solid 1px ${theme.v1.colors.border2};
|
border-bottom: solid 1px ${theme.v1.colors.border2};
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
sectionItems: css`
|
|
||||||
margin: 0 24px 0 32px;
|
|
||||||
`,
|
|
||||||
spinner: css`
|
spinner: css`
|
||||||
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
|
||||||
min-height: 100px;
|
min-height: 100px;
|
||||||
`,
|
`,
|
||||||
gridContainer: css`
|
error: css`
|
||||||
display: grid;
|
margin: ${theme.spacing(4)} auto;
|
||||||
gap: ${sm};
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
|
||||||
margin-bottom: ${md};
|
|
||||||
`,
|
|
||||||
resultsContainer: css`
|
|
||||||
position: relative;
|
|
||||||
flex-grow: 10;
|
|
||||||
margin-bottom: ${md};
|
|
||||||
background: ${theme.v1.colors.bg1};
|
|
||||||
border: 1px solid ${theme.v1.colors.border1};
|
|
||||||
border-radius: 3px;
|
|
||||||
height: 100%;
|
|
||||||
`,
|
|
||||||
noResults: css`
|
|
||||||
padding: ${md};
|
|
||||||
background: ${theme.v1.colors.bg2};
|
|
||||||
font-style: italic;
|
|
||||||
margin-top: ${theme.v1.spacing.md};
|
|
||||||
`,
|
|
||||||
listModeWrapper: css`
|
|
||||||
position: relative;
|
|
||||||
height: 100%;
|
|
||||||
padding: ${md};
|
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4701,6 +4701,7 @@ __metadata:
|
|||||||
"@react-aria/focus": 3.6.0
|
"@react-aria/focus": 3.6.0
|
||||||
"@react-aria/menu": 3.5.0
|
"@react-aria/menu": 3.5.0
|
||||||
"@react-aria/overlays": 3.9.0
|
"@react-aria/overlays": 3.9.0
|
||||||
|
"@react-aria/utils": 3.13.0
|
||||||
"@react-stately/menu": 3.3.0
|
"@react-stately/menu": 3.3.0
|
||||||
"@rollup/plugin-alias": ^3.1.9
|
"@rollup/plugin-alias": ^3.1.9
|
||||||
"@rollup/plugin-commonjs": 22.0.0
|
"@rollup/plugin-commonjs": 22.0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user