Fixed ShortUrlsList test

This commit is contained in:
Alejandro Celaya 2021-11-08 23:41:17 +01:00
parent 5f33059de1
commit ed038b9799
3 changed files with 50 additions and 47 deletions

View file

@ -1,25 +1,20 @@
import { head, keys, values } from 'ramda'; import { head, keys, pipe, values } from 'ramda';
import { FC, useEffect, useState } from 'react'; import { FC, useEffect, useMemo, useState } from 'react';
import { RouteComponentProps } from 'react-router'; import { RouteComponentProps } from 'react-router';
import { Card } from 'reactstrap'; import { Card } from 'reactstrap';
import SortingDropdown from '../utils/SortingDropdown'; import SortingDropdown from '../utils/SortingDropdown';
import { determineOrderDir, Order, OrderDir } from '../utils/helpers/ordering'; import { determineOrderDir, Order, OrderDir } from '../utils/helpers/ordering';
import { getServerId, SelectedServer } from '../servers/data'; import { getServerId, SelectedServer } from '../servers/data';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { parseQuery } from '../utils/helpers/query';
import { Topics } from '../mercure/helpers/Topics'; import { Topics } from '../mercure/helpers/Topics';
import { TableOrderIcon } from '../utils/table/TableOrderIcon'; import { TableOrderIcon } from '../utils/table/TableOrderIcon';
import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList'; import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
import { OrderableFields, ShortUrlsListParams, SORTABLE_FIELDS } from './reducers/shortUrlsListParams'; import { OrderableFields, ShortUrlsListParams, SORTABLE_FIELDS } from './reducers/shortUrlsListParams';
import { ShortUrlsTableProps } from './ShortUrlsTable'; import { ShortUrlsTableProps } from './ShortUrlsTable';
import Paginator from './Paginator'; import Paginator from './Paginator';
import { ShortUrlListRouteParams, useShortUrlsQuery } from './helpers/hooks';
interface RouteParams { interface ShortUrlsListProps extends RouteComponentProps<ShortUrlListRouteParams> {
page: string;
serverId: string;
}
export interface ShortUrlsListProps extends RouteComponentProps<RouteParams> {
selectedServer: SelectedServer; selectedServer: SelectedServer;
shortUrlsList: ShortUrlsListState; shortUrlsList: ShortUrlsListState;
listShortUrls: (params: ShortUrlsListParams) => void; listShortUrls: (params: ShortUrlsListParams) => void;
@ -29,21 +24,26 @@ export interface ShortUrlsListProps extends RouteComponentProps<RouteParams> {
type ShortUrlsOrder = Order<OrderableFields>; type ShortUrlsOrder = Order<OrderableFields>;
const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>) => boundToMercureHub(({ const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>, SearchBar: FC) => boundToMercureHub(({
listShortUrls, listShortUrls,
resetShortUrlParams, resetShortUrlParams,
shortUrlsListParams, shortUrlsListParams,
match, match,
location, location,
history,
shortUrlsList, shortUrlsList,
selectedServer, selectedServer,
}: ShortUrlsListProps) => { }: ShortUrlsListProps) => {
const serverId = getServerId(selectedServer);
const { orderBy } = shortUrlsListParams; const { orderBy } = shortUrlsListParams;
const [ order, setOrder ] = useState<ShortUrlsOrder>({ const [ order, setOrder ] = useState<ShortUrlsOrder>({
field: orderBy && (head(keys(orderBy)) as OrderableFields), field: orderBy && (head(keys(orderBy)) as OrderableFields),
dir: orderBy && head(values(orderBy)), dir: orderBy && head(values(orderBy)),
}); });
const [{ tags, search }, toFirstPage ] = useShortUrlsQuery({ history, match, location });
const decodedTags = useMemo(() => tags?.split(',').map(decodeURIComponent) ?? [], [ tags ]);
const { pagination } = shortUrlsList?.shortUrls ?? {}; const { pagination } = shortUrlsList?.shortUrls ?? {};
const refreshList = (extraParams: ShortUrlsListParams) => listShortUrls({ ...shortUrlsListParams, ...extraParams }); const refreshList = (extraParams: ShortUrlsListParams) => listShortUrls({ ...shortUrlsListParams, ...extraParams });
const handleOrderBy = (field?: OrderableFields, dir?: OrderDir) => { const handleOrderBy = (field?: OrderableFields, dir?: OrderDir) => {
setOrder({ field, dir }); setOrder({ field, dir });
@ -52,30 +52,31 @@ const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>) => boundToMercur
const orderByColumn = (field: OrderableFields) => () => const orderByColumn = (field: OrderableFields) => () =>
handleOrderBy(field, determineOrderDir(field, order.field, order.dir)); handleOrderBy(field, determineOrderDir(field, order.field, order.dir));
const renderOrderIcon = (field: OrderableFields) => <TableOrderIcon currentOrder={order} field={field} />; const renderOrderIcon = (field: OrderableFields) => <TableOrderIcon currentOrder={order} field={field} />;
const addTag = pipe(
(newTag: string) => [ ...new Set([ ...decodedTags, newTag ]) ].join(','),
(tags) => toFirstPage({ tags }),
);
useEffect(() => resetShortUrlParams, []);
useEffect(() => { useEffect(() => {
const { tag } = parseQuery<{ tag?: string }>(location.search); refreshList({ page: match.params.page, searchTerm: search, tags: decodedTags, itemsPerPage: undefined });
const tags = tag ? [ decodeURIComponent(tag) ] : shortUrlsListParams.tags; }, [ match.params.page, search, decodedTags ]);
refreshList({ page: match.params.page, tags, itemsPerPage: undefined });
return resetShortUrlParams;
}, []);
return ( return (
<> <>
<div className="mb-3"><SearchBar /></div>
<div className="d-block d-lg-none mb-3"> <div className="d-block d-lg-none mb-3">
<SortingDropdown items={SORTABLE_FIELDS} order={order} onChange={handleOrderBy} /> <SortingDropdown items={SORTABLE_FIELDS} order={order} onChange={handleOrderBy} />
</div> </div>
<Card body className="pb-1"> <Card body className="pb-1">
<ShortUrlsTable <ShortUrlsTable
orderByColumn={orderByColumn}
renderOrderIcon={renderOrderIcon}
selectedServer={selectedServer} selectedServer={selectedServer}
shortUrlsList={shortUrlsList} shortUrlsList={shortUrlsList}
onTagClick={(tag) => refreshList({ tags: [ ...shortUrlsListParams.tags ?? [], tag ] })} orderByColumn={orderByColumn}
renderOrderIcon={renderOrderIcon}
onTagClick={addTag}
/> />
<Paginator paginator={pagination} serverId={getServerId(selectedServer)} /> <Paginator paginator={pagination} serverId={serverId} currentQueryString={location.search} />
</Card> </Card>
</> </>
); );

View file

@ -35,7 +35,9 @@ export const ShortUrlsTable = (ShortUrlsRow: FC<ShortUrlsRowProps>) => ({
if (error) { if (error) {
return ( return (
<tr> <tr>
<td colSpan={6} className="text-center table-danger">Something went wrong while loading short URLs :(</td> <td colSpan={6} className="text-center table-danger text-dark">
Something went wrong while loading short URLs :(
</td>
</tr> </tr>
); );
} }

View file

@ -1,19 +1,24 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import { ReactElement } from 'react'; import { ReactElement } from 'react';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import shortUrlsListCreator, { ShortUrlsListProps } from '../../src/short-urls/ShortUrlsList'; import { History, Location } from 'history';
import { match } from 'react-router';
import shortUrlsListCreator from '../../src/short-urls/ShortUrlsList';
import { ShortUrl } from '../../src/short-urls/data'; import { ShortUrl } from '../../src/short-urls/data';
import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub'; import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
import { ShortUrlsList as ShortUrlsListModel } from '../../src/short-urls/reducers/shortUrlsList'; import { ShortUrlsList as ShortUrlsListModel } from '../../src/short-urls/reducers/shortUrlsList';
import SortingDropdown from '../../src/utils/SortingDropdown'; import SortingDropdown from '../../src/utils/SortingDropdown';
import { OrderableFields, OrderBy } from '../../src/short-urls/reducers/shortUrlsListParams'; import { OrderableFields, OrderBy } from '../../src/short-urls/reducers/shortUrlsListParams';
import Paginator from '../../src/short-urls/Paginator'; import Paginator from '../../src/short-urls/Paginator';
import { ReachableServer } from '../../src/servers/data';
import { ShortUrlListRouteParams } from '../../src/short-urls/helpers/hooks';
describe('<ShortUrlsList />', () => { describe('<ShortUrlsList />', () => {
let wrapper: ShallowWrapper; let wrapper: ShallowWrapper;
const ShortUrlsTable = () => null; const ShortUrlsTable = () => null;
const SearchBar = () => null;
const listShortUrlsMock = jest.fn(); const listShortUrlsMock = jest.fn();
const resetShortUrlParamsMock = jest.fn(); const push = jest.fn();
const shortUrlsList = Mock.of<ShortUrlsListModel>({ const shortUrlsList = Mock.of<ShortUrlsListModel>({
shortUrls: { shortUrls: {
data: [ data: [
@ -26,22 +31,18 @@ describe('<ShortUrlsList />', () => {
], ],
}, },
}); });
const ShortUrlsList = shortUrlsListCreator(ShortUrlsTable); const ShortUrlsList = shortUrlsListCreator(ShortUrlsTable, SearchBar);
const createWrapper = (orderBy: OrderBy = {}) => shallow( const createWrapper = (orderBy: OrderBy = {}) => shallow(
<ShortUrlsList <ShortUrlsList
{...Mock.all<ShortUrlsListProps>()}
{...Mock.of<MercureBoundProps>({ mercureInfo: { loading: true } })} {...Mock.of<MercureBoundProps>({ mercureInfo: { loading: true } })}
listShortUrls={listShortUrlsMock} listShortUrls={listShortUrlsMock}
resetShortUrlParams={resetShortUrlParamsMock} resetShortUrlParams={jest.fn()}
shortUrlsListParams={{ shortUrlsListParams={{ page: '1', orderBy }}
page: '1', match={Mock.of<match<ShortUrlListRouteParams>>({ params: {} })}
tags: [ 'test tag' ], location={Mock.of<Location>({ search: '?tags=test%20tag&search=example.com' })}
searchTerm: 'example.com',
orderBy,
}}
match={{ params: {} } as any}
location={{} as any}
shortUrlsList={shortUrlsList} shortUrlsList={shortUrlsList}
history={Mock.of<History>({ push })}
selectedServer={Mock.of<ReachableServer>({ id: '1' })}
/>, />,
).dive(); // Dive is needed as this component is wrapped in a HOC ).dive(); // Dive is needed as this component is wrapped in a HOC
@ -56,6 +57,11 @@ describe('<ShortUrlsList />', () => {
expect(wrapper.find(ShortUrlsTable)).toHaveLength(1); expect(wrapper.find(ShortUrlsTable)).toHaveLength(1);
expect(wrapper.find(SortingDropdown)).toHaveLength(1); expect(wrapper.find(SortingDropdown)).toHaveLength(1);
expect(wrapper.find(Paginator)).toHaveLength(1); expect(wrapper.find(Paginator)).toHaveLength(1);
expect(wrapper.find(SearchBar)).toHaveLength(1);
});
it('passes current query to paginator', () => {
expect(wrapper.find(Paginator).prop('currentQueryString')).toEqual('?tags=test%20tag&search=example.com');
}); });
it('gets list refreshed every time a tag is clicked', () => { it('gets list refreshed every time a tag is clicked', () => {
@ -63,16 +69,10 @@ describe('<ShortUrlsList />', () => {
wrapper.find(ShortUrlsTable).simulate('tagClick', 'bar'); wrapper.find(ShortUrlsTable).simulate('tagClick', 'bar');
wrapper.find(ShortUrlsTable).simulate('tagClick', 'baz'); wrapper.find(ShortUrlsTable).simulate('tagClick', 'baz');
expect(listShortUrlsMock).toHaveBeenCalledTimes(3); expect(push).toHaveBeenCalledTimes(3);
expect(listShortUrlsMock).toHaveBeenNthCalledWith(1, expect.objectContaining({ expect(push).toHaveBeenNthCalledWith(1, expect.stringContaining(`tags=${encodeURIComponent('test tag,foo')}`));
tags: [ 'test tag', 'foo' ], expect(push).toHaveBeenNthCalledWith(2, expect.stringContaining(`tags=${encodeURIComponent('test tag,bar')}`));
})); expect(push).toHaveBeenNthCalledWith(3, expect.stringContaining(`tags=${encodeURIComponent('test tag,baz')}`));
expect(listShortUrlsMock).toHaveBeenNthCalledWith(2, expect.objectContaining({
tags: [ 'test tag', 'bar' ],
}));
expect(listShortUrlsMock).toHaveBeenNthCalledWith(3, expect.objectContaining({
tags: [ 'test tag', 'baz' ],
}));
}); });
it('invokes order icon rendering', () => { it('invokes order icon rendering', () => {
@ -88,7 +88,7 @@ describe('<ShortUrlsList />', () => {
expect(renderIcon('visits').props.currentOrder).toEqual({ field: 'visits', dir: 'ASC' }); expect(renderIcon('visits').props.currentOrder).toEqual({ field: 'visits', dir: 'ASC' });
}); });
it('handles order by through table', () => { it('handles order through table', () => {
const orderByColumn: (field: OrderableFields) => Function = wrapper.find(ShortUrlsTable).prop('orderByColumn'); const orderByColumn: (field: OrderableFields) => Function = wrapper.find(ShortUrlsTable).prop('orderByColumn');
orderByColumn('visits')(); orderByColumn('visits')();
@ -107,7 +107,7 @@ describe('<ShortUrlsList />', () => {
})); }));
}); });
it('handles order by through dropdown', () => { it('handles order through dropdown', () => {
expect(wrapper.find(SortingDropdown).prop('order')).toEqual({}); expect(wrapper.find(SortingDropdown).prop('order')).toEqual({});
wrapper.find(SortingDropdown).simulate('change', 'visits', 'ASC'); wrapper.find(SortingDropdown).simulate('change', 'visits', 'ASC');