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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { isEmpty, pipe } from 'ramda';
|
import { isEmpty, pipe } from 'ramda';
|
||||||
import { parseISO } from 'date-fns';
|
import { parseISO } from 'date-fns';
|
||||||
|
import { RouteChildrenProps } from 'react-router-dom';
|
||||||
import SearchField from '../utils/SearchField';
|
import SearchField from '../utils/SearchField';
|
||||||
import Tag from '../tags/helpers/Tag';
|
import Tag from '../tags/helpers/Tag';
|
||||||
import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
|
import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
|
||||||
|
@ -9,17 +10,21 @@ import { formatIsoDate } from '../utils/helpers/date';
|
||||||
import ColorGenerator from '../utils/services/ColorGenerator';
|
import ColorGenerator from '../utils/services/ColorGenerator';
|
||||||
import { DateRange } from '../utils/dates/types';
|
import { DateRange } from '../utils/dates/types';
|
||||||
import { ShortUrlsListParams } from './reducers/shortUrlsListParams';
|
import { ShortUrlsListParams } from './reducers/shortUrlsListParams';
|
||||||
|
import { ShortUrlListRouteParams, useShortUrlsQuery } from './helpers/hooks';
|
||||||
import './SearchBar.scss';
|
import './SearchBar.scss';
|
||||||
|
|
||||||
interface SearchBarProps {
|
export interface SearchBarProps extends RouteChildrenProps<ShortUrlListRouteParams> {
|
||||||
listShortUrls: (params: ShortUrlsListParams) => void;
|
listShortUrls: (params: ShortUrlsListParams) => void;
|
||||||
shortUrlsListParams: ShortUrlsListParams;
|
shortUrlsListParams: ShortUrlsListParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateOrNull = (date?: string) => date ? parseISO(date) : null;
|
const dateOrNull = (date?: string) => date ? parseISO(date) : null;
|
||||||
|
|
||||||
const SearchBar = (colorGenerator: ColorGenerator) => ({ listShortUrls, shortUrlsListParams }: SearchBarProps) => {
|
const SearchBar = (colorGenerator: ColorGenerator) => (
|
||||||
const selectedTags = shortUrlsListParams.tags ?? [];
|
{ listShortUrls, shortUrlsListParams, ...rest }: SearchBarProps,
|
||||||
|
) => {
|
||||||
|
const [{ search, tags }, toFirstPage ] = useShortUrlsQuery(rest);
|
||||||
|
const selectedTags = tags?.split(',').map(decodeURIComponent) ?? [];
|
||||||
const setDates = pipe(
|
const setDates = pipe(
|
||||||
({ startDate, endDate }: DateRange) => ({
|
({ startDate, endDate }: DateRange) => ({
|
||||||
startDate: formatIsoDate(startDate) ?? undefined,
|
startDate: formatIsoDate(startDate) ?? undefined,
|
||||||
|
@ -27,10 +32,19 @@ const SearchBar = (colorGenerator: ColorGenerator) => ({ listShortUrls, shortUrl
|
||||||
}),
|
}),
|
||||||
(dates) => listShortUrls({ ...shortUrlsListParams, ...dates }),
|
(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 (
|
return (
|
||||||
<div className="search-bar-container">
|
<div className="search-bar-container">
|
||||||
<SearchField onChange={(searchTerm) => listShortUrls({ ...shortUrlsListParams, searchTerm })} />
|
<SearchField initialValue={search} onChange={setSearch} />
|
||||||
|
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
|
@ -47,24 +61,12 @@ const SearchBar = (colorGenerator: ColorGenerator) => ({ listShortUrls, shortUrl
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isEmpty(selectedTags) && (
|
{selectedTags.length > 0 && (
|
||||||
<h4 className="search-bar__selected-tag mt-3">
|
<h4 className="search-bar__selected-tag mt-3">
|
||||||
<FontAwesomeIcon icon={tagsIcon} className="search-bar__tags-icon" />
|
<FontAwesomeIcon icon={tagsIcon} className="search-bar__tags-icon" />
|
||||||
|
|
||||||
{selectedTags.map((tag) => (
|
{selectedTags.map((tag) =>
|
||||||
<Tag
|
<Tag colorGenerator={colorGenerator} key={tag} text={tag} clearable onClose={() => removeTag(tag)} />)}
|
||||||
colorGenerator={colorGenerator}
|
|
||||||
key={tag}
|
|
||||||
text={tag}
|
|
||||||
clearable
|
|
||||||
onClose={() => listShortUrls(
|
|
||||||
{
|
|
||||||
...shortUrlsListParams,
|
|
||||||
tags: selectedTags.filter((selectedTag) => selectedTag !== tag),
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</h4>
|
</h4>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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 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 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 { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { Mock } from 'ts-mockery';
|
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 SearchField from '../../src/utils/SearchField';
|
||||||
import Tag from '../../src/tags/helpers/Tag';
|
import Tag from '../../src/tags/helpers/Tag';
|
||||||
import { DateRangeSelector } from '../../src/utils/dates/DateRangeSelector';
|
import { DateRangeSelector } from '../../src/utils/dates/DateRangeSelector';
|
||||||
import ColorGenerator from '../../src/utils/services/ColorGenerator';
|
import ColorGenerator from '../../src/utils/services/ColorGenerator';
|
||||||
|
import { ShortUrlListRouteParams } from '../../src/short-urls/helpers/hooks';
|
||||||
|
|
||||||
describe('<SearchBar />', () => {
|
describe('<SearchBar />', () => {
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
const listShortUrlsMock = jest.fn();
|
const listShortUrlsMock = jest.fn();
|
||||||
const SearchBar = searchBarCreator(Mock.all<ColorGenerator>());
|
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(jest.clearAllMocks);
|
||||||
afterEach(() => wrapper?.unmount());
|
afterEach(() => wrapper?.unmount());
|
||||||
|
|
||||||
it('renders a SearchField', () => {
|
it('renders some children components SearchField', () => {
|
||||||
wrapper = shallow(<SearchBar shortUrlsListParams={{}} listShortUrls={listShortUrlsMock} />);
|
const wrapper = createWrapper();
|
||||||
|
|
||||||
expect(wrapper.find(SearchField)).toHaveLength(1);
|
expect(wrapper.find(SearchField)).toHaveLength(1);
|
||||||
});
|
|
||||||
|
|
||||||
it('renders a DateRangeSelector', () => {
|
|
||||||
wrapper = shallow(<SearchBar shortUrlsListParams={{}} listShortUrls={listShortUrlsMock} />);
|
|
||||||
|
|
||||||
expect(wrapper.find(DateRangeSelector)).toHaveLength(1);
|
expect(wrapper.find(DateRangeSelector)).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders no tags when the list of tags is empty', () => {
|
it.each([
|
||||||
wrapper = shallow(<SearchBar shortUrlsListParams={{}} listShortUrls={listShortUrlsMock} />);
|
[ '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);
|
expect(wrapper.find(Tag)).toHaveLength(expectedTagComps);
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates short URLs list when search field changes', () => {
|
it('updates short URLs list when search field changes', () => {
|
||||||
wrapper = shallow(<SearchBar shortUrlsListParams={{}} listShortUrls={listShortUrlsMock} />);
|
const wrapper = createWrapper();
|
||||||
const searchField = wrapper.find(SearchField);
|
const searchField = wrapper.find(SearchField);
|
||||||
|
|
||||||
expect(listShortUrlsMock).not.toHaveBeenCalled();
|
expect(push).not.toHaveBeenCalled();
|
||||||
searchField.simulate('change');
|
searchField.simulate('change', 'search-term');
|
||||||
expect(listShortUrlsMock).toHaveBeenCalledTimes(1);
|
expect(push).toHaveBeenCalledWith('/server/1/list-short-urls/1?search=search-term');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates short URLs list when a tag is removed', () => {
|
it('updates short URLs list when a tag is removed', () => {
|
||||||
wrapper = shallow(
|
const wrapper = createWrapper({ location: Mock.of<Location>({ search: 'tags=foo,bar' }) });
|
||||||
<SearchBar shortUrlsListParams={{ tags: [ 'foo' ] }} listShortUrls={listShortUrlsMock} />,
|
|
||||||
);
|
|
||||||
const tag = wrapper.find(Tag).first();
|
const tag = wrapper.find(Tag).first();
|
||||||
|
|
||||||
expect(listShortUrlsMock).not.toHaveBeenCalled();
|
expect(push).not.toHaveBeenCalled();
|
||||||
tag.simulate('close');
|
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', () => {
|
it('updates short URLs list when date range changes', () => {
|
||||||
wrapper = shallow(
|
const wrapper = createWrapper();
|
||||||
<SearchBar shortUrlsListParams={{}} listShortUrls={listShortUrlsMock} />,
|
|
||||||
);
|
|
||||||
const dateRange = wrapper.find(DateRangeSelector);
|
const dateRange = wrapper.find(DateRangeSelector);
|
||||||
|
|
||||||
expect(listShortUrlsMock).not.toHaveBeenCalled();
|
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('query', () => {
|
||||||
describe('parseQuery', () => {
|
describe('parseQuery', () => {
|
||||||
|
@ -22,15 +22,4 @@ describe('query', () => {
|
||||||
expect(stringifyQuery(queryObj)).toEqual(expectedResult);
|
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