Search: Add unit tests for SearchResultsTable (#51269)

* initial scaffolding for unit tests

* more tests for searchresultstable

* skip failing test, remove unused css

* Correctly mock stuff so TableCell renders correctly!
This commit is contained in:
Ashley Harrison 2022-06-28 12:48:59 +01:00 committed by GitHub
parent 3ab410de0b
commit 2429fe1c70
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 173 additions and 91 deletions

View File

@ -0,0 +1,162 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { Subject } from 'rxjs';
import {
applyFieldOverrides,
ArrayVector,
createTheme,
DataFrame,
DataFrameView,
FieldType,
toDataFrame,
} from '@grafana/data';
import { DashboardQueryResult, getGrafanaSearcher, QueryResponse } from '../../service';
import { DashboardSearchItemType } from '../../types';
import { SearchResultsTable } from './SearchResultsTable';
describe('SearchResultsTable', () => {
const mockOnTagSelected = jest.fn();
const mockClearSelection = jest.fn();
const mockSelectionToggle = jest.fn();
const mockSelection = jest.fn();
const mockKeyboardEvents = new Subject<React.KeyboardEvent>();
describe('when there is data', () => {
const searchData = toDataFrame({
name: 'A',
fields: [
{ name: 'kind', type: FieldType.string, config: {}, values: [DashboardSearchItemType.DashDB] },
{ name: 'uid', type: FieldType.string, config: {}, values: ['my-dashboard-1'] },
{ name: 'name', type: FieldType.string, config: {}, values: ['My dashboard 1'] },
{ name: 'panel_type', type: FieldType.string, config: {}, values: [''] },
{ name: 'url', type: FieldType.string, config: {}, values: ['/my-dashboard-1'] },
{ name: 'tags', type: FieldType.other, config: {}, values: [['foo', 'bar']] },
{ name: 'ds_uid', type: FieldType.other, config: {}, values: [''] },
{ name: 'location', type: FieldType.string, config: {}, values: ['/my-dashboard-1'] },
],
});
const dataFrames = applyFieldOverrides({
data: [searchData],
fieldConfig: {
defaults: {},
overrides: [],
},
replaceVariables: (value, vars, format) => {
return vars && value === '${__value.text}' ? vars['__value'].value.text : value;
},
theme: createTheme(),
});
const mockSearchResult: QueryResponse = {
isItemLoaded: jest.fn(),
loadMoreItems: jest.fn(),
totalRows: searchData.length,
view: new DataFrameView<DashboardQueryResult>(dataFrames[0]),
};
beforeAll(() => {
jest.spyOn(getGrafanaSearcher(), 'search').mockResolvedValue(mockSearchResult);
});
it('shows the table with the correct accessible label', () => {
render(
<SearchResultsTable
keyboardEvents={mockKeyboardEvents}
response={mockSearchResult}
onTagSelected={mockOnTagSelected}
selection={mockSelection}
selectionToggle={mockSelectionToggle}
clearSelection={mockClearSelection}
height={1000}
width={1000}
/>
);
expect(screen.getByRole('table', { name: 'Search results table' })).toBeInTheDocument();
});
it('has the correct row headers', async () => {
render(
<SearchResultsTable
keyboardEvents={mockKeyboardEvents}
response={mockSearchResult}
onTagSelected={mockOnTagSelected}
selection={mockSelection}
selectionToggle={mockSelectionToggle}
clearSelection={mockClearSelection}
height={1000}
width={1000}
/>
);
expect(screen.getByRole('columnheader', { name: 'Name' })).toBeInTheDocument();
expect(screen.getByRole('columnheader', { name: 'Type' })).toBeInTheDocument();
expect(screen.getByRole('columnheader', { name: 'Tags' })).toBeInTheDocument();
});
it('displays the data correctly in the table', () => {
render(
<SearchResultsTable
keyboardEvents={mockKeyboardEvents}
response={mockSearchResult}
onTagSelected={mockOnTagSelected}
selection={mockSelection}
selectionToggle={mockSelectionToggle}
clearSelection={mockClearSelection}
height={1000}
width={1000}
/>
);
const rows = screen.getAllByRole('row');
expect(rows).toHaveLength(2);
expect(screen.getByText('My dashboard 1')).toBeInTheDocument();
expect(screen.getByText('foo')).toBeInTheDocument();
expect(screen.getByText('bar')).toBeInTheDocument();
});
});
describe('when there is no data', () => {
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 mockEmptySearchResult: QueryResponse = {
isItemLoaded: jest.fn(),
loadMoreItems: jest.fn(),
totalRows: emptySearchData.length,
view: new DataFrameView<DashboardQueryResult>(emptySearchData),
};
beforeAll(() => {
jest.spyOn(getGrafanaSearcher(), 'search').mockResolvedValue(mockEmptySearchResult);
});
it('shows a "No data" message', () => {
render(
<SearchResultsTable
keyboardEvents={mockKeyboardEvents}
response={mockEmptySearchResult}
onTagSelected={mockOnTagSelected}
selection={mockSelection}
selectionToggle={mockSelectionToggle}
clearSelection={mockClearSelection}
height={1000}
width={1000}
/>
);
expect(screen.queryByRole('table', { name: 'Search results table' })).not.toBeInTheDocument();
expect(screen.getByText('No data')).toBeInTheDocument();
});
});
});

View File

@ -1,6 +1,6 @@
/* eslint-disable react/jsx-no-undef */
import { css } from '@emotion/css';
import React, { useEffect, useMemo, useRef, useCallback } from 'react';
import React, { useEffect, useMemo, useRef, useCallback, useState } from 'react';
import { useTable, Column, TableOptions, Cell, useAbsoluteLayout } from 'react-table';
import { FixedSizeList } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
@ -50,7 +50,7 @@ export const SearchResultsTable = React.memo(
const styles = useStyles2(getStyles);
const tableStyles = useStyles2(getTableStyles);
const infiniteLoaderRef = useRef<InfiniteLoader>(null);
const listRef = useRef<FixedSizeList>(null);
const [listEl, setListEl] = useState<FixedSizeList | null>(null);
const highlightIndex = useSearchKeyboardNavigation(keyboardEvents, 0, response);
const memoizedData = useMemo(() => {
@ -69,10 +69,10 @@ export const SearchResultsTable = React.memo(
if (infiniteLoaderRef.current) {
infiniteLoaderRef.current.resetloadMoreItemsCache();
}
if (listRef.current) {
listRef.current.scrollTo(0);
if (listEl) {
listEl.scrollTo(0);
}
}, [memoizedData]);
}, [memoizedData, listEl]);
// React-table column definitions
const memoizedColumns = useMemo(() => {
@ -135,7 +135,7 @@ export const SearchResultsTable = React.memo(
}
return (
<div {...getTableProps()} aria-label="Search result table" role="table">
<div {...getTableProps()} aria-label="Search results table" role="table">
<div>
{headerGroups.map((headerGroup) => {
const { key, ...headerGroupProps } = headerGroup.getHeaderGroupProps();
@ -162,9 +162,12 @@ export const SearchResultsTable = React.memo(
itemCount={rows.length}
loadMoreItems={response.loadMoreItems}
>
{({ onItemsRendered }) => (
{({ onItemsRendered, ref }) => (
<FixedSizeList
ref={listRef}
ref={(innerRef) => {
ref(innerRef);
setListEl(innerRef);
}}
onItemsRendered={onItemsRendered}
height={height - HEADER_HEIGHT}
itemCount={rows.length}
@ -194,27 +197,6 @@ const getStyles = (theme: GrafanaTheme2) => {
justify-content: center;
height: 100%;
`,
table: css`
width: 100%;
`,
cellIcon: css`
display: flex;
align-items: center;
`,
nameCellStyle: css`
border-right: none;
padding: ${theme.spacing(1)} ${theme.spacing(1)} ${theme.spacing(1)} ${theme.spacing(2)};
overflow: hidden;
text-overflow: ellipsis;
user-select: text;
white-space: nowrap;
&:hover {
box-shadow: none;
}
`,
headerNameStyle: css`
padding-left: ${theme.spacing(1)};
`,
headerCell: css`
padding: ${theme.spacing(1)};
`,
@ -239,67 +221,5 @@ const getStyles = (theme: GrafanaTheme2) => {
text-overflow: ellipsis;
}
`,
typeIcon: css`
margin-left: 5px;
margin-right: 9.5px;
vertical-align: middle;
display: inline-block;
margin-bottom: ${theme.v1.spacing.xxs};
fill: ${theme.colors.text.secondary};
`,
datasourceItem: css`
span {
&:hover {
color: ${theme.colors.text.link};
}
}
`,
missingTitleText: css`
color: ${theme.colors.text.disabled};
font-style: italic;
`,
invalidDatasourceItem: css`
color: ${theme.colors.error.main};
text-decoration: line-through;
`,
typeText: css`
color: ${theme.colors.text.secondary};
padding-top: ${theme.spacing(1)};
`,
locationItem: css`
color: ${theme.colors.text.secondary};
margin-right: 12px;
`,
sortedHeader: css`
text-align: right;
padding-right: ${theme.spacing(2)};
`,
sortedItems: css`
text-align: right;
padding: ${theme.spacing(1)} ${theme.spacing(3)} ${theme.spacing(1)} ${theme.spacing(1)};
`,
locationCellStyle: css`
padding-top: ${theme.spacing(1)};
padding-right: ${theme.spacing(1)};
`,
checkboxHeader: css`
margin-left: 2px;
`,
checkbox: css`
margin-left: 10px;
margin-right: 10px;
margin-top: 5px;
`,
infoWrap: css`
color: ${theme.colors.text.secondary};
span {
margin-right: 10px;
}
`,
tagList: css`
padding-top: ${theme.spacing(0.5)};
justify-content: flex-start;
flex-wrap: nowrap;
`,
};
};