Initial tests.

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
Eric Peterson
2022-01-03 18:36:30 +01:00
parent 3e5400cfa8
commit c84c404fc8
3 changed files with 414 additions and 3 deletions
@@ -0,0 +1,365 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ApiProvider } from '@backstage/core-app-api';
import { TestApiRegistry } from '@backstage/test-utils';
import { screen, render, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { searchApiRef } from '../../apis';
import { SearchContextProvider, useSearch } from '../SearchContext';
import { SearchFilter } from './SearchFilter';
const SearchContextFilterSpy = ({ name }: { name: string }) => {
const { filters } = useSearch();
const value = filters[name];
return (
<span data-testid={`${name}-filter-spy`}>
{Array.isArray(value) ? value.join(',') : value}
</span>
);
};
describe('SearchFilter.Autocomplete', () => {
const query = jest.fn().mockResolvedValue({});
const mockApis = TestApiRegistry.from([searchApiRef, { query }]);
const emptySearchContext = {
term: '',
types: [],
filters: {},
};
const name = 'field';
const values = ['value1', 'value2'];
it('renders as expected', async () => {
render(
<ApiProvider apis={mockApis}>
<SearchContextProvider>
<SearchFilter.Autocomplete name={name} values={values} />
</SearchContextProvider>
</ApiProvider>,
);
const autocomplete = screen.getByRole('combobox');
const input = within(autocomplete).getByRole('textbox');
userEvent.click(input);
await waitFor(() => {
screen.getByRole('listbox');
});
expect(screen.getByRole('option', { name: values[0] })).toBeInTheDocument();
expect(screen.getByRole('option', { name: values[1] })).toBeInTheDocument();
});
it('renders as expected with async values', async () => {
render(
<ApiProvider apis={mockApis}>
<SearchContextProvider>
<SearchFilter.Autocomplete
name={name}
asyncValues={async () => values}
/>
</SearchContextProvider>
</ApiProvider>,
);
const autocomplete = screen.getByRole('combobox');
const input = within(autocomplete).getByRole('textbox');
userEvent.click(input);
await waitFor(() => {
screen.getByRole('listbox');
});
expect(screen.getByRole('option', { name: values[0] })).toBeInTheDocument();
expect(screen.getByRole('option', { name: values[1] })).toBeInTheDocument();
});
it('does not affect unrelated filter state', async () => {
render(
<ApiProvider apis={mockApis}>
<SearchContextProvider
initialState={{
...emptySearchContext,
...{ filters: { unrelated: 'value' } },
}}
>
<SearchFilter.Autocomplete name={name} values={values} />
<SearchContextFilterSpy name={name} />
<SearchContextFilterSpy name="unrelated" />
</SearchContextProvider>
</ApiProvider>,
);
// The spy should show the initial value.
expect(screen.getByTestId('unrelated-filter-spy')).toHaveTextContent(
'value',
);
// Select a value from the autocomplete filter.
const autocomplete = screen.getByRole('combobox');
const input = within(autocomplete).getByRole('textbox');
userEvent.click(input);
await waitFor(() => {
screen.getByRole('listbox');
});
userEvent.click(screen.getByRole('option', { name: values[1] }));
// Wait for the autocomplete filter's value to change.
await waitFor(() => {
expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent(
values[1],
);
});
// Unrelated filter spy should maintain the same value.
expect(screen.getByTestId('unrelated-filter-spy')).toHaveTextContent(
'value',
);
});
describe('single', () => {
it('renders as expected with defaultValue', async () => {
render(
<ApiProvider apis={mockApis}>
<SearchContextProvider>
<SearchFilter.Autocomplete
name={name}
values={values}
defaultValue={values[1]}
/>
<SearchContextFilterSpy name={name} />
</SearchContextProvider>
</ApiProvider>,
);
const autocomplete = screen.getByRole('combobox');
const input = within(autocomplete).getByRole('textbox');
await waitFor(() => {
expect(input).toHaveValue(values[1]);
expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent(
values[1],
);
});
});
it('renders as expected with initial context', async () => {
render(
<ApiProvider apis={mockApis}>
<SearchContextProvider
initialState={{
...emptySearchContext,
...{ filters: { [name]: values[0] } },
}}
>
<SearchFilter.Autocomplete name={name} values={values} />
<SearchContextFilterSpy name={name} />
</SearchContextProvider>
</ApiProvider>,
);
const autocomplete = screen.getByRole('combobox');
const input = within(autocomplete).getByRole('textbox');
await waitFor(() => {
expect(input).toHaveValue(values[0]);
expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent(
values[0],
);
});
});
it('sets filter state when selecting a value', async () => {
render(
<ApiProvider apis={mockApis}>
<SearchContextProvider>
<SearchFilter.Autocomplete name={name} values={values} />
<SearchContextFilterSpy name={name} />
</SearchContextProvider>
</ApiProvider>,
);
// Select the first option in the autocomplete.
const autocomplete = screen.getByRole('combobox');
const input = within(autocomplete).getByRole('textbox');
userEvent.click(input);
await waitFor(() => {
screen.getByRole('listbox');
});
userEvent.click(screen.getByRole('option', { name: values[0] }));
// The value should be present in the context.
await waitFor(() => {
expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent(
values[0],
);
});
// Click the "Clear" button to remove the value.
const clearButton = within(autocomplete).getByLabelText('Clear');
userEvent.click(clearButton);
// That value should have been unset from the context.
await waitFor(() => {
expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent('');
});
});
});
describe('multiple', () => {
it('renders as expected with defaultValue', async () => {
render(
<ApiProvider apis={mockApis}>
<SearchContextProvider>
<SearchFilter.Autocomplete
multiple
name={name}
values={values}
defaultValue={values}
/>
<SearchContextFilterSpy name={name} />
</SearchContextProvider>
</ApiProvider>,
);
await waitFor(() => {
expect(screen.getByText(values[0])).toBeInTheDocument();
expect(screen.getByText(values[1])).toBeInTheDocument();
expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent(
values.join(','),
);
});
});
it('renders as expected with initial context', async () => {
render(
<ApiProvider apis={mockApis}>
<SearchContextProvider
initialState={{
...emptySearchContext,
...{ filters: { [name]: values } },
}}
>
<SearchFilter.Autocomplete multiple name={name} values={values} />
<SearchContextFilterSpy name={name} />
</SearchContextProvider>
</ApiProvider>,
);
await waitFor(() => {
expect(screen.getByText(values[0])).toBeInTheDocument();
expect(screen.getByText(values[1])).toBeInTheDocument();
expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent(
values.join(','),
);
});
});
it('respects tag limit configuration', async () => {
render(
<ApiProvider apis={mockApis}>
<SearchContextProvider>
<SearchFilter.Autocomplete
multiple
limitTags={1}
name={name}
values={values}
/>
</SearchContextProvider>
</ApiProvider>,
);
const autocomplete = screen.getByRole('combobox');
const input = within(autocomplete).getByRole('textbox');
// Select the second value.
userEvent.click(input);
await waitFor(() => {
screen.getByRole('listbox');
});
userEvent.click(screen.getByRole('option', { name: values[1] }));
await waitFor(() => {
expect(
screen.getByRole('button', { name: values[1] }),
).toBeInTheDocument();
});
// Select the first value.
userEvent.click(input);
await waitFor(() => {
screen.getByRole('listbox');
});
userEvent.click(screen.getByRole('option', { name: values[0] }));
await waitFor(() => {
expect(
screen.getByRole('button', { name: values[0] }),
).toBeInTheDocument();
});
// Blur the field and only one tag should be shown with a +1.
input.blur();
expect(
screen.queryByRole('button', { name: values[0] }),
).not.toBeInTheDocument();
expect(screen.getByText('+1')).toBeInTheDocument();
});
it('sets filter state when selecting a value', async () => {
render(
<ApiProvider apis={mockApis}>
<SearchContextProvider>
<SearchFilter.Autocomplete multiple name={name} values={values} />
<SearchContextFilterSpy name={name} />
</SearchContextProvider>
</ApiProvider>,
);
const autocomplete = screen.getByRole('combobox');
// Select both values in the autocomplete.
const input = within(autocomplete).getByRole('textbox');
userEvent.click(input);
await waitFor(() => {
screen.getByRole('listbox');
});
userEvent.click(screen.getByRole('option', { name: values[0] }));
userEvent.click(input);
await waitFor(() => {
screen.getByRole('listbox');
});
userEvent.click(screen.getByRole('option', { name: values[1] }));
// Both options should be present in the context.
await waitFor(() => {
expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent(
values.join(','),
);
});
// Click the "Clear" button to remove the value.
const clearButton = within(autocomplete).getByLabelText('Clear');
userEvent.click(clearButton);
// There should be no content in the filter context.
await waitFor(() => {
expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent('');
});
});
});
});
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React, { ChangeEvent, useState } from 'react';
import React, { ChangeEvent, useEffect, useState } from 'react';
import { Chip, TextField } from '@material-ui/core';
import {
Autocomplete,
@@ -38,6 +38,7 @@ export const AutocompleteFilter = (props: SearchAutocompleteFilterProps) => {
asyncValues,
asyncDebounce,
className,
defaultValue,
multiple,
name,
values: givenValues,
@@ -51,7 +52,23 @@ export const AutocompleteFilter = (props: SearchAutocompleteFilterProps) => {
givenValues,
asyncDebounce,
);
const { setFilters } = useSearch();
const { filters, setFilters } = useSearch();
const filterValue =
(filters[name] as string | string[] | undefined) || (multiple ? [] : null);
useEffect(() => {
const defaultIsEmpty = !defaultValue;
const defaultIsEmptyArray =
Array.isArray(defaultValue) && defaultValue.length === 0;
if (!defaultIsEmpty && !defaultIsEmptyArray) {
setFilters(prevFilters => ({
...prevFilters,
[name]: defaultValue,
}));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Set new filter values on input change.
const handleChange = (
@@ -92,10 +109,11 @@ export const AutocompleteFilter = (props: SearchAutocompleteFilterProps) => {
<Autocomplete
multiple={multiple}
className={className}
id={`${multiple ?? 'multi-'}select-filter-${name}--select`}
id={`${multiple ? 'multi-' : ''}select-filter-${name}--select`}
options={values || []}
loading={loading}
limitTags={limitTags}
value={filterValue}
onChange={handleChange}
onInputChange={(_, newValue) => setInputValue(newValue)}
renderInput={renderInput}
@@ -207,6 +207,34 @@ describe('SearchFilter', () => {
).toBeInTheDocument();
});
it('Renders values when provided asynchronously', async () => {
render(
<SearchContextProvider initialState={initialState}>
<SearchFilter.Select name={name} asyncValues={async () => values} />
</SearchContextProvider>,
);
await waitFor(() => {
expect(screen.getByRole('button')).toBeInTheDocument();
expect(
screen.getByRole('button').getAttribute('aria-disabled'),
).not.toBe('true');
});
userEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(screen.getByRole('listbox')).toBeInTheDocument();
});
expect(
screen.getByRole('option', { name: values[0] }),
).toBeInTheDocument();
expect(
screen.getByRole('option', { name: values[1] }),
).toBeInTheDocument();
});
it('Renders correctly based on filter state', async () => {
render(
<SearchContextProvider