mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 09:30:31 +03:00
Improved SearchBar test
This commit is contained in:
parent
3bc5b4c154
commit
5f33059de1
5 changed files with 88 additions and 65 deletions
|
@ -2,6 +2,7 @@ import { faTags as tagsIcon } from '@fortawesome/free-solid-svg-icons';
|
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { isEmpty, pipe } from 'ramda';
|
||||
import { parseISO } from 'date-fns';
|
||||
import { RouteChildrenProps } from 'react-router-dom';
|
||||
import SearchField from '../utils/SearchField';
|
||||
import Tag from '../tags/helpers/Tag';
|
||||
import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
|
||||
|
@ -9,17 +10,21 @@ import { formatIsoDate } from '../utils/helpers/date';
|
|||
import ColorGenerator from '../utils/services/ColorGenerator';
|
||||
import { DateRange } from '../utils/dates/types';
|
||||
import { ShortUrlsListParams } from './reducers/shortUrlsListParams';
|
||||
import { ShortUrlListRouteParams, useShortUrlsQuery } from './helpers/hooks';
|
||||
import './SearchBar.scss';
|
||||
|
||||
interface SearchBarProps {
|
||||
export interface SearchBarProps extends RouteChildrenProps<ShortUrlListRouteParams> {
|
||||
listShortUrls: (params: ShortUrlsListParams) => void;
|
||||
shortUrlsListParams: ShortUrlsListParams;
|
||||
}
|
||||
|
||||
const dateOrNull = (date?: string) => date ? parseISO(date) : null;
|
||||
|
||||
const SearchBar = (colorGenerator: ColorGenerator) => ({ listShortUrls, shortUrlsListParams }: SearchBarProps) => {
|
||||
const selectedTags = shortUrlsListParams.tags ?? [];
|
||||
const SearchBar = (colorGenerator: ColorGenerator) => (
|
||||
{ listShortUrls, shortUrlsListParams, ...rest }: SearchBarProps,
|
||||
) => {
|
||||
const [{ search, tags }, toFirstPage ] = useShortUrlsQuery(rest);
|
||||
const selectedTags = tags?.split(',').map(decodeURIComponent) ?? [];
|
||||
const setDates = pipe(
|
||||
({ startDate, endDate }: DateRange) => ({
|
||||
startDate: formatIsoDate(startDate) ?? undefined,
|
||||
|
@ -27,10 +32,19 @@ const SearchBar = (colorGenerator: ColorGenerator) => ({ listShortUrls, shortUrl
|
|||
}),
|
||||
(dates) => listShortUrls({ ...shortUrlsListParams, ...dates }),
|
||||
);
|
||||
const setSearch = pipe(
|
||||
(searchTerm: string) => isEmpty(searchTerm) ? undefined : searchTerm,
|
||||
(search) => toFirstPage({ search }),
|
||||
);
|
||||
const removeTag = pipe(
|
||||
(tag: string) => selectedTags.filter((selectedTag) => selectedTag !== tag),
|
||||
(tagsList) => tagsList.length === 0 ? undefined : tagsList.join(','),
|
||||
(tags) => toFirstPage({ tags }),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="search-bar-container">
|
||||
<SearchField onChange={(searchTerm) => listShortUrls({ ...shortUrlsListParams, searchTerm })} />
|
||||
<SearchField initialValue={search} onChange={setSearch} />
|
||||
|
||||
<div className="mt-3">
|
||||
<div className="row">
|
||||
|
@ -47,24 +61,12 @@ const SearchBar = (colorGenerator: ColorGenerator) => ({ listShortUrls, shortUrl
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{!isEmpty(selectedTags) && (
|
||||
{selectedTags.length > 0 && (
|
||||
<h4 className="search-bar__selected-tag mt-3">
|
||||
<FontAwesomeIcon icon={tagsIcon} className="search-bar__tags-icon" />
|
||||
|
||||
{selectedTags.map((tag) => (
|
||||
<Tag
|
||||
colorGenerator={colorGenerator}
|
||||
key={tag}
|
||||
text={tag}
|
||||
clearable
|
||||
onClose={() => listShortUrls(
|
||||
{
|
||||
...shortUrlsListParams,
|
||||
tags: selectedTags.filter((selectedTag) => selectedTag !== tag),
|
||||
},
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
{selectedTags.map((tag) =>
|
||||
<Tag colorGenerator={colorGenerator} key={tag} text={tag} clearable onClose={() => removeTag(tag)} />)}
|
||||
</h4>
|
||||
)}
|
||||
</div>
|
||||
|
|
29
src/short-urls/helpers/hooks.ts
Normal file
29
src/short-urls/helpers/hooks.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { RouteChildrenProps } from 'react-router-dom';
|
||||
import { useMemo } from 'react';
|
||||
import { isEmpty } from 'ramda';
|
||||
import { parseQuery, stringifyQuery } from '../../utils/helpers/query';
|
||||
|
||||
type ServerIdRouteProps = RouteChildrenProps<{ serverId: string }>;
|
||||
type ToFirstPage = (extra: Partial<ShortUrlsQuery>) => void;
|
||||
|
||||
export interface ShortUrlListRouteParams {
|
||||
page: string;
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
interface ShortUrlsQuery {
|
||||
tags?: string;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export const useShortUrlsQuery = ({ history, location, match }: ServerIdRouteProps): [ShortUrlsQuery, ToFirstPage] => {
|
||||
const query = useMemo(() => parseQuery<ShortUrlsQuery>(location.search), [ location ]);
|
||||
const toFirstPageWithExtra = (extra: Partial<ShortUrlsQuery>) => {
|
||||
const evolvedQuery = stringifyQuery({ ...query, ...extra });
|
||||
const queryString = isEmpty(evolvedQuery) ? '' : `?${evolvedQuery}`;
|
||||
|
||||
history.push(`/server/${match?.params.serverId}/list-short-urls/1${queryString}`);
|
||||
};
|
||||
|
||||
return [ query, toFirstPageWithExtra ];
|
||||
};
|
|
@ -3,6 +3,3 @@ import qs from 'qs';
|
|||
export const parseQuery = <T>(search: string) => qs.parse(search, { ignoreQueryPrefix: true }) as unknown as T;
|
||||
|
||||
export const stringifyQuery = (query: any): string => qs.stringify(query, { arrayFormat: 'brackets' });
|
||||
|
||||
export const evolveStringifiedQuery = (currentQuery: string, extra: any): string =>
|
||||
stringifyQuery({ ...parseQuery(currentQuery), ...extra });
|
||||
|
|
|
@ -1,69 +1,75 @@
|
|||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
import { Mock } from 'ts-mockery';
|
||||
import searchBarCreator from '../../src/short-urls/SearchBar';
|
||||
import { History, Location } from 'history';
|
||||
import { match } from 'react-router';
|
||||
import searchBarCreator, { SearchBarProps } from '../../src/short-urls/SearchBar';
|
||||
import SearchField from '../../src/utils/SearchField';
|
||||
import Tag from '../../src/tags/helpers/Tag';
|
||||
import { DateRangeSelector } from '../../src/utils/dates/DateRangeSelector';
|
||||
import ColorGenerator from '../../src/utils/services/ColorGenerator';
|
||||
import { ShortUrlListRouteParams } from '../../src/short-urls/helpers/hooks';
|
||||
|
||||
describe('<SearchBar />', () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
const listShortUrlsMock = jest.fn();
|
||||
const SearchBar = searchBarCreator(Mock.all<ColorGenerator>());
|
||||
const push = jest.fn();
|
||||
const createWrapper = (props: Partial<SearchBarProps> = {}) => {
|
||||
wrapper = shallow(
|
||||
<SearchBar
|
||||
listShortUrls={listShortUrlsMock}
|
||||
shortUrlsListParams={{}}
|
||||
history={Mock.of<History>({ push })}
|
||||
location={Mock.of<Location>({ search: '' })}
|
||||
match={Mock.of<match<ShortUrlListRouteParams>>({ params: { serverId: '1' } })}
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
|
||||
return wrapper;
|
||||
};
|
||||
|
||||
afterEach(jest.clearAllMocks);
|
||||
afterEach(() => wrapper?.unmount());
|
||||
|
||||
it('renders a SearchField', () => {
|
||||
wrapper = shallow(<SearchBar shortUrlsListParams={{}} listShortUrls={listShortUrlsMock} />);
|
||||
it('renders some children components SearchField', () => {
|
||||
const wrapper = createWrapper();
|
||||
|
||||
expect(wrapper.find(SearchField)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders a DateRangeSelector', () => {
|
||||
wrapper = shallow(<SearchBar shortUrlsListParams={{}} listShortUrls={listShortUrlsMock} />);
|
||||
|
||||
expect(wrapper.find(DateRangeSelector)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders no tags when the list of tags is empty', () => {
|
||||
wrapper = shallow(<SearchBar shortUrlsListParams={{}} listShortUrls={listShortUrlsMock} />);
|
||||
it.each([
|
||||
[ 'tags=foo,bar,baz', 3 ],
|
||||
[ 'tags=foo,baz', 2 ],
|
||||
[ '', 0 ],
|
||||
[ 'foo=bar', 0 ],
|
||||
])('renders the proper amount of tags', (search, expectedTagComps) => {
|
||||
const wrapper = createWrapper({ location: Mock.of<Location>({ search }) });
|
||||
|
||||
expect(wrapper.find(Tag)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('renders the proper amount of tags', () => {
|
||||
const tags = [ 'foo', 'bar', 'baz' ];
|
||||
|
||||
wrapper = shallow(<SearchBar shortUrlsListParams={{ tags }} listShortUrls={listShortUrlsMock} />);
|
||||
|
||||
expect(wrapper.find(Tag)).toHaveLength(tags.length);
|
||||
expect(wrapper.find(Tag)).toHaveLength(expectedTagComps);
|
||||
});
|
||||
|
||||
it('updates short URLs list when search field changes', () => {
|
||||
wrapper = shallow(<SearchBar shortUrlsListParams={{}} listShortUrls={listShortUrlsMock} />);
|
||||
const wrapper = createWrapper();
|
||||
const searchField = wrapper.find(SearchField);
|
||||
|
||||
expect(listShortUrlsMock).not.toHaveBeenCalled();
|
||||
searchField.simulate('change');
|
||||
expect(listShortUrlsMock).toHaveBeenCalledTimes(1);
|
||||
expect(push).not.toHaveBeenCalled();
|
||||
searchField.simulate('change', 'search-term');
|
||||
expect(push).toHaveBeenCalledWith('/server/1/list-short-urls/1?search=search-term');
|
||||
});
|
||||
|
||||
it('updates short URLs list when a tag is removed', () => {
|
||||
wrapper = shallow(
|
||||
<SearchBar shortUrlsListParams={{ tags: [ 'foo' ] }} listShortUrls={listShortUrlsMock} />,
|
||||
);
|
||||
const wrapper = createWrapper({ location: Mock.of<Location>({ search: 'tags=foo,bar' }) });
|
||||
const tag = wrapper.find(Tag).first();
|
||||
|
||||
expect(listShortUrlsMock).not.toHaveBeenCalled();
|
||||
expect(push).not.toHaveBeenCalled();
|
||||
tag.simulate('close');
|
||||
expect(listShortUrlsMock).toHaveBeenCalledTimes(1);
|
||||
expect(push).toHaveBeenCalledWith('/server/1/list-short-urls/1?tags=bar');
|
||||
});
|
||||
|
||||
it('updates short URLs list when date range changes', () => {
|
||||
wrapper = shallow(
|
||||
<SearchBar shortUrlsListParams={{}} listShortUrls={listShortUrlsMock} />,
|
||||
);
|
||||
const wrapper = createWrapper();
|
||||
const dateRange = wrapper.find(DateRangeSelector);
|
||||
|
||||
expect(listShortUrlsMock).not.toHaveBeenCalled();
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { evolveStringifiedQuery, parseQuery, stringifyQuery } from '../../../src/utils/helpers/query';
|
||||
import { parseQuery, stringifyQuery } from '../../../src/utils/helpers/query';
|
||||
|
||||
describe('query', () => {
|
||||
describe('parseQuery', () => {
|
||||
|
@ -22,15 +22,4 @@ describe('query', () => {
|
|||
expect(stringifyQuery(queryObj)).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('evolveStringifiedQuery', () => {
|
||||
it.each([
|
||||
[ '?foo=bar', { baz: 123 }, 'foo=bar&baz=123' ],
|
||||
[ 'foo=bar', { baz: 123 }, 'foo=bar&baz=123' ],
|
||||
[ 'foo=bar&baz=hello', { baz: 'world' }, 'foo=bar&baz=world' ],
|
||||
[ '?', { foo: 'some', bar: 'thing' }, 'foo=some&bar=thing' ],
|
||||
])('adds and overwrites params on provided query string', (query, extra, expected) => {
|
||||
expect(evolveStringifiedQuery(query, extra)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue