From 05fbfdaa1333dfcbd141cce8af0335432e42eeea Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Tue, 21 Jun 2022 09:35:03 +0100 Subject: [PATCH] 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` --- packages/grafana-ui/package.json | 1 + .../src/components/Alert/Alert.test.tsx | 11 ++ .../grafana-ui/src/components/Alert/Alert.tsx | 10 +- .../search/page/components/FolderSection.tsx | 2 +- .../page/components/FolderView.test.tsx | 172 ++++++++++++++++++ .../search/page/components/FolderView.tsx | 90 +++------ yarn.lock | 1 + 7 files changed, 222 insertions(+), 65 deletions(-) create mode 100644 packages/grafana-ui/src/components/Alert/Alert.test.tsx create mode 100644 public/app/features/search/page/components/FolderView.test.tsx diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index f860bf9b55f..8643d0b0eb9 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -44,6 +44,7 @@ "@react-aria/focus": "3.6.0", "@react-aria/menu": "3.5.0", "@react-aria/overlays": "3.9.0", + "@react-aria/utils": "3.13.0", "@react-stately/menu": "3.3.0", "@sentry/browser": "6.19.7", "ansicolor": "1.1.100", diff --git a/packages/grafana-ui/src/components/Alert/Alert.test.tsx b/packages/grafana-ui/src/components/Alert/Alert.test.tsx new file mode 100644 index 00000000000..d8f7c670066 --- /dev/null +++ b/packages/grafana-ui/src/components/Alert/Alert.test.tsx @@ -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(); + expect(screen.getByRole('alert', { name: 'Uh oh spagghettios!' })).toBeInTheDocument(); + }); +}); diff --git a/packages/grafana-ui/src/components/Alert/Alert.tsx b/packages/grafana-ui/src/components/Alert/Alert.tsx index 5acb3049b10..f5588c8c09b 100644 --- a/packages/grafana-ui/src/components/Alert/Alert.tsx +++ b/packages/grafana-ui/src/components/Alert/Alert.tsx @@ -1,4 +1,5 @@ import { css, cx } from '@emotion/css'; +import { useId } from '@react-aria/utils'; import React, { HTMLAttributes, ReactNode } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; @@ -56,19 +57,24 @@ export const Alert = React.forwardRef( ) => { const theme = useTheme2(); const styles = getStyles(theme, severity, elevated, bottomSpacing, topSpacing); + const titleId = useId(); return (
-
-
{title}
+
+
+ {title} +
{children &&
{children}
}
{/* If onRemove is specified, giving preference to onRemove */} diff --git a/public/app/features/search/page/components/FolderSection.tsx b/public/app/features/search/page/components/FolderSection.tsx index e769ba82704..c4b5ec5df33 100644 --- a/public/app/features/search/page/components/FolderSection.tsx +++ b/public/app/features/search/page/components/FolderSection.tsx @@ -116,7 +116,7 @@ export const FolderSection: FC = ({ const renderResults = () => { if (!results.value) { return null; - } else if (results.value.length === 0) { + } else if (results.value.length === 0 && !results.loading) { return ( No results found diff --git a/public/app/features/search/page/components/FolderView.test.tsx b/public/app/features/search/page/components/FolderView.test.tsx new file mode 100644 index 00000000000..5ced047cc31 --- /dev/null +++ b/public/app/features/search/page/components/FolderView.test.tsx @@ -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(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( + + ); + 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( + + ); + 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( + + ); + 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( + + ); + 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( + + ); + expect(await screen.findByRole('button', { name: 'Recent' })).toBeInTheDocument(); + }); + + it('shows the general folder by default', async () => { + render( + + ); + 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( + + ); + 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( + + ); + 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( + + ); + 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( + + ); + expect(await screen.findByRole('alert', { name: 'Something went wrong' })).toBeInTheDocument(); + }); +}); diff --git a/public/app/features/search/page/components/FolderView.tsx b/public/app/features/search/page/components/FolderView.tsx index 3430581e555..7ca5fb21b09 100644 --- a/public/app/features/search/page/components/FolderView.tsx +++ b/public/app/features/search/page/components/FolderView.tsx @@ -5,10 +5,10 @@ import { useAsync } from 'react-use'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { getBackendSrv } from '@grafana/runtime'; -import { Spinner, useStyles2 } from '@grafana/ui'; -import { contextSrv } from 'app/core/core'; -import impressionSrv from 'app/core/services/impression_srv'; +import { Alert, Spinner, useStyles2 } from '@grafana/ui'; +import { contextSrv } from '../../../../core/services/context_srv'; +import impressionSrv from '../../../../core/services/impression_srv'; import { GENERAL_FOLDER_UID } from '../../constants'; import { getGrafanaSearcher } from '../../service'; import { SearchResultsProps } from '../components/SearchResultsTable'; @@ -58,41 +58,33 @@ export const FolderView = ({ selection, selectionToggle, onTagSelected, tags, hi return folders; }, []); - if (results.loading) { - return ; - } - if (!results.value) { - return
?
; - } + const renderResults = () => { + if (results.loading) { + return ; + } else if (!results.value) { + return ; + } else { + return results.value.map((section) => ( +
+ {section.title && ( + + )} +
+ )); + } + }; - return ( -
- {results.value.map((section) => { - return ( -
- {section.title && ( - - )} -
- ); - })} -
- ); + return
{renderResults()}
; }; const getStyles = (theme: GrafanaTheme2) => { - const { md, sm } = theme.v1.spacing; - return { - virtualizedGridItemWrapper: css` - padding: 4px; - `, wrapper: css` display: flex; flex-direction: column; @@ -113,40 +105,14 @@ const getStyles = (theme: GrafanaTheme2) => { border-bottom: solid 1px ${theme.v1.colors.border2}; } `, - sectionItems: css` - margin: 0 24px 0 32px; - `, spinner: css` + align-items: center; display: flex; justify-content: center; - align-items: center; min-height: 100px; `, - gridContainer: css` - display: grid; - 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}; + error: css` + margin: ${theme.spacing(4)} auto; `, }; }; diff --git a/yarn.lock b/yarn.lock index 71ec7452fc9..6aefa1f7def 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4701,6 +4701,7 @@ __metadata: "@react-aria/focus": 3.6.0 "@react-aria/menu": 3.5.0 "@react-aria/overlays": 3.9.0 + "@react-aria/utils": 3.13.0 "@react-stately/menu": 3.3.0 "@rollup/plugin-alias": ^3.1.9 "@rollup/plugin-commonjs": 22.0.0