Test filter utility hooks.

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
Eric Peterson
2022-01-05 14:57:07 +01:00
parent 73811cfb33
commit 3b0000669a
@@ -0,0 +1,295 @@
/*
* Copyright 2022 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 React from 'react';
import { ApiProvider } from '@backstage/core-app-api';
import { TestApiRegistry } from '@backstage/test-utils';
import { act, renderHook } from '@testing-library/react-hooks';
import { SearchContextProvider, useSearch } from '../SearchContext';
import { useDefaultFilterValue, useAsyncFilterValues } from './hooks';
import { searchApiRef } from '../../apis';
describe('SearchFilter.hooks', () => {
describe('useDefaultFilterValue', () => {
const query = jest.fn().mockResolvedValue({});
const mockApis = TestApiRegistry.from([searchApiRef, { query }]);
const wrapper = ({
children,
overrides = {},
}: {
children?: any;
overrides?: any;
}) => {
const emptySearchContext = {
term: '',
types: [],
filters: {},
};
return (
<ApiProvider apis={mockApis}>
<SearchContextProvider
initialState={{ ...emptySearchContext, ...overrides }}
>
{children}
</SearchContextProvider>
</ApiProvider>
);
};
it('should set non-empty string value', async () => {
const expectedFilter = 'someField';
const expectedValue = 'someValue';
const { result, waitForNextUpdate } = renderHook(
() => {
useDefaultFilterValue(expectedFilter, expectedValue);
return useSearch();
},
{
wrapper,
},
);
await waitForNextUpdate();
expect(result.current.filters[expectedFilter]).toEqual(expectedValue);
});
it('should set non-empty array value', async () => {
const expectedFilter = 'someField';
const expectedValue = ['someValue', 'anotherValue'];
const { result, waitForNextUpdate } = renderHook(
() => {
useDefaultFilterValue(expectedFilter, expectedValue);
return useSearch();
},
{
wrapper,
},
);
await waitForNextUpdate();
expect(result.current.filters[expectedFilter]).toEqual(expectedValue);
});
it('should not set undefined value', async () => {
const expectedFilter = 'someField';
const expectedValue = 'notEmpty';
const { result, waitForNextUpdate } = renderHook(
() => {
useDefaultFilterValue(expectedFilter, undefined);
return useSearch();
},
{
wrapper,
initialProps: {
overrides: {
filters: {
[expectedFilter]: expectedValue,
},
},
},
},
);
await waitForNextUpdate();
expect(result.current.filters[expectedFilter]).toEqual(expectedValue);
});
it('should not set null value', async () => {
const expectedFilter = 'someField';
const expectedValue = 'notEmpty';
const { result, waitForNextUpdate } = renderHook(
() => {
useDefaultFilterValue(expectedFilter, null);
return useSearch();
},
{
wrapper,
initialProps: {
overrides: {
filters: {
[expectedFilter]: expectedValue,
},
},
},
},
);
await waitForNextUpdate();
expect(result.current.filters[expectedFilter]).toEqual(expectedValue);
});
it('should not set empty string value', async () => {
const expectedFilter = 'someField';
const expectedValue = 'notEmpty';
const { result, waitForNextUpdate } = renderHook(
() => {
useDefaultFilterValue(expectedFilter, '');
return useSearch();
},
{
wrapper,
initialProps: {
overrides: {
filters: {
[expectedFilter]: expectedValue,
},
},
},
},
);
await waitForNextUpdate();
expect(result.current.filters[expectedFilter]).toEqual(expectedValue);
});
it('should not set empty array value', async () => {
const expectedFilter = 'someField';
const expectedValue = ['not', 'empty'];
const { result, waitForNextUpdate } = renderHook(
() => {
useDefaultFilterValue(expectedFilter, []);
return useSearch();
},
{
wrapper,
initialProps: {
overrides: {
filters: {
[expectedFilter]: expectedValue,
},
},
},
},
);
await waitForNextUpdate();
expect(result.current.filters[expectedFilter]).toEqual(expectedValue);
});
it('should not affect unrelated filters', async () => {
const expectedFilter = 'someField';
const expectedValue = 'someValue';
const { result, waitForNextUpdate } = renderHook(
() => {
useDefaultFilterValue(expectedFilter, expectedValue);
return useSearch();
},
{
wrapper,
initialProps: {
overrides: {
filters: {
unrelatedField: 'unrelatedValue',
},
},
},
},
);
await waitForNextUpdate();
expect(result.current.filters.unrelatedField).toEqual('unrelatedValue');
});
});
describe('useAsyncFilterValues', () => {
it('should immediately return given values when provided', () => {
const givenValues = ['value1', 'value2'];
const { result } = renderHook(() =>
useAsyncFilterValues(undefined, '', givenValues),
);
expect(result.current.loading).toEqual(false);
expect(result.current.value).toEqual(givenValues);
});
it('should return resolved values of provided async function', async () => {
const expectedValues = ['value1', 'value2'];
const asyncFn = () => Promise.resolve(expectedValues);
const { result, waitForNextUpdate } = renderHook(() =>
useAsyncFilterValues(asyncFn, '', undefined, 1),
);
expect(result.current.loading).toEqual(true);
await waitForNextUpdate();
expect(result.current.loading).toEqual(false);
expect(result.current.value).toEqual(expectedValues);
});
it('should debounce method invocation', async () => {
const expectedValues = ['value1', 'value2'];
const asyncFn = jest.fn().mockResolvedValue(expectedValues);
renderHook(() => useAsyncFilterValues(asyncFn, '', undefined, 10));
expect(asyncFn).not.toHaveBeenCalled();
// Allow 6 milliseconds to pass.
await act(() => new Promise(resolve => setTimeout(resolve, 6)));
expect(asyncFn).not.toHaveBeenCalled();
// Allow an additional 6 milliseconds to pass.
await act(() => new Promise(resolve => setTimeout(resolve, 6)));
expect(asyncFn).toHaveBeenCalled();
});
it('should call provided method once per provided input', async () => {
const asyncFn = jest
.fn()
.mockImplementation((x: string) => Promise.resolve([x]));
const { rerender, waitForNextUpdate } = renderHook(
(props: { inputValue: string } = { inputValue: '' }) =>
useAsyncFilterValues(asyncFn, props.inputValue, undefined, 1),
);
expect(asyncFn).not.toHaveBeenCalled();
await waitForNextUpdate();
expect(asyncFn).toHaveBeenCalledTimes(1);
expect(asyncFn).toHaveBeenCalledWith('');
// Re-render with different input value.
rerender({ inputValue: 'somethingElse' });
await waitForNextUpdate();
expect(asyncFn).toHaveBeenCalledTimes(2);
expect(asyncFn).toHaveBeenLastCalledWith('somethingElse');
});
it('should not call provided method more than once when re-rendered with same input', async () => {
const expectedValues = ['value1', 'value2'];
const asyncFn = jest.fn().mockResolvedValue(expectedValues);
const { rerender, waitForNextUpdate } = renderHook(
(props: { inputValue: string } = { inputValue: '' }) =>
useAsyncFilterValues(asyncFn, props.inputValue, undefined, 1),
);
expect(asyncFn).not.toHaveBeenCalled();
await waitForNextUpdate();
expect(asyncFn).toHaveBeenCalledTimes(1);
// Re-render multiple times with the same input.
rerender();
expect(asyncFn).toHaveBeenCalledTimes(1);
rerender();
expect(asyncFn).toHaveBeenCalledTimes(1);
});
});
});