diff --git a/README.md b/README.md index b0e7a324..be8dc30e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # shlink-web-client -[![Build Status](https://img.shields.io/github/workflow/status/shlinkio/shlink-web-client/Continuous%20integration/main?logo=github&style=flat-square)](https://github.com/shlinkio/shlink-web-client/actions?query=workflow%3A%22Continuous+integration%22) -[![Code Coverage](https://img.shields.io/codecov/c/gh/shlinkio/shlink-web-client/main?style=flat-square)](https://app.codecov.io/gh/shlinkio/shlink-web-client) +[![Build Status](https://img.shields.io/github/workflow/status/shlinkio/shlink-web-client/Continuous%20integration/develop?logo=github&style=flat-square)](https://github.com/shlinkio/shlink-web-client/actions?query=workflow%3A%22Continuous+integration%22) +[![Code Coverage](https://img.shields.io/codecov/c/gh/shlinkio/shlink-web-client/develop?style=flat-square)](https://app.codecov.io/gh/shlinkio/shlink-web-client) [![GitHub release](https://img.shields.io/github/release/shlinkio/shlink-web-client.svg?style=flat-square)](https://github.com/shlinkio/shlink-web-client/releases/latest) [![Docker pulls](https://img.shields.io/docker/pulls/shlinkio/shlink-web-client.svg?logo=docker&style=flat-square)](https://hub.docker.com/r/shlinkio/shlink-web-client/) [![GitHub license](https://img.shields.io/github/license/shlinkio/shlink-web-client.svg?style=flat-square)](https://github.com/shlinkio/shlink-web-client/blob/main/LICENSE) diff --git a/jest.config.js b/jest.config.js index 3745d53d..c954a343 100644 --- a/jest.config.js +++ b/jest.config.js @@ -10,7 +10,7 @@ module.exports = { coverageThreshold: { global: { statements: 85, - branches: 75, + branches: 80, functions: 80, lines: 85, }, diff --git a/src/short-urls/ShortUrlsList.tsx b/src/short-urls/ShortUrlsList.tsx index 414bc913..570f57d1 100644 --- a/src/short-urls/ShortUrlsList.tsx +++ b/src/short-urls/ShortUrlsList.tsx @@ -6,7 +6,7 @@ import { RouteComponentProps } from 'react-router'; import { Card } from 'reactstrap'; import SortingDropdown from '../utils/SortingDropdown'; import { determineOrderDir, OrderDir } from '../utils/utils'; -import { isReachableServer, SelectedServer } from '../servers/data'; +import { getServerId, SelectedServer } from '../servers/data'; import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { parseQuery } from '../utils/helpers/query'; import { Topics } from '../mercure/helpers/Topics'; @@ -95,7 +95,7 @@ const ShortUrlsList = (ShortUrlsTable: FC) => boundToMercur shortUrlsList={shortUrlsList} onTagClick={(tag) => refreshList({ tags: [ ...shortUrlsListParams.tags ?? [], tag ] })} /> - + ); diff --git a/src/short-urls/reducers/shortUrlsListParams.ts b/src/short-urls/reducers/shortUrlsListParams.ts index beb5c812..0dd034aa 100644 --- a/src/short-urls/reducers/shortUrlsListParams.ts +++ b/src/short-urls/reducers/shortUrlsListParams.ts @@ -14,6 +14,8 @@ export const SORTABLE_FIELDS = { export type OrderableFields = keyof typeof SORTABLE_FIELDS; +export type OrderBy = Partial>; + export interface ShortUrlsListParams { page?: string; itemsPerPage?: number; @@ -21,7 +23,7 @@ export interface ShortUrlsListParams { searchTerm?: string; startDate?: string; endDate?: string; - orderBy?: Partial>; + orderBy?: OrderBy; } const initialState: ShortUrlsListParams = { diff --git a/test/short-urls/ShortUrlsList.test.tsx b/test/short-urls/ShortUrlsList.test.tsx index 072b39be..cc0406ce 100644 --- a/test/short-urls/ShortUrlsList.test.tsx +++ b/test/short-urls/ShortUrlsList.test.tsx @@ -1,10 +1,13 @@ import { shallow, ShallowWrapper } from 'enzyme'; +import { ReactElement } from 'react'; import { Mock } from 'ts-mockery'; import shortUrlsListCreator, { ShortUrlsListProps } from '../../src/short-urls/ShortUrlsList'; import { ShortUrl } from '../../src/short-urls/data'; import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub'; import { ShortUrlsList as ShortUrlsListModel } from '../../src/short-urls/reducers/shortUrlsList'; import SortingDropdown from '../../src/utils/SortingDropdown'; +import { OrderableFields, OrderBy } from '../../src/short-urls/reducers/shortUrlsListParams'; +import Paginator from '../../src/short-urls/Paginator'; describe('', () => { let wrapper: ShallowWrapper; @@ -23,36 +26,124 @@ describe('', () => { ], }, }); - const ShortUrlsList = shortUrlsListCreator(ShortUrlsTable); + const createWrapper = (orderBy: OrderBy = {}) => shallow( + ()} + {...Mock.of({ mercureInfo: { loading: true } })} + listShortUrls={listShortUrlsMock} + resetShortUrlParams={resetShortUrlParamsMock} + shortUrlsListParams={{ + page: '1', + tags: [ 'test tag' ], + searchTerm: 'example.com', + orderBy, + }} + match={{ params: {} } as any} + location={{} as any} + shortUrlsList={shortUrlsList} + />, + ).dive(); // Dive is needed as this component is wrapped in a HOC beforeEach(() => { - wrapper = shallow( - ()} - {...Mock.of({ mercureInfo: { loading: true } })} - listShortUrls={listShortUrlsMock} - resetShortUrlParams={resetShortUrlParamsMock} - shortUrlsListParams={{ - page: '1', - tags: [ 'test tag' ], - searchTerm: 'example.com', - }} - match={{ params: {} } as any} - location={{} as any} - shortUrlsList={shortUrlsList} - />, - ).dive(); // Dive is needed as this component is wrapped in a HOC + wrapper = createWrapper(); }); afterEach(jest.resetAllMocks); afterEach(() => wrapper?.unmount()); - it('wraps a ShortUrlsTable', () => { + it('wraps expected components', () => { expect(wrapper.find(ShortUrlsTable)).toHaveLength(1); + expect(wrapper.find(SortingDropdown)).toHaveLength(1); + expect(wrapper.find(Paginator)).toHaveLength(1); }); - it('wraps a SortingDropdown', () => { - expect(wrapper.find(SortingDropdown)).toHaveLength(1); + it('gets list refreshed every time a tag is clicked', () => { + wrapper.find(ShortUrlsTable).simulate('tagClick', 'foo'); + wrapper.find(ShortUrlsTable).simulate('tagClick', 'bar'); + wrapper.find(ShortUrlsTable).simulate('tagClick', 'baz'); + + expect(listShortUrlsMock).toHaveBeenCalledTimes(3); + expect(listShortUrlsMock).toHaveBeenNthCalledWith(1, expect.objectContaining({ + tags: [ 'test tag', 'foo' ], + })); + 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', () => { + const renderIcon = (field: OrderableFields) => + (wrapper.find(ShortUrlsTable).prop('renderOrderIcon') as (field: OrderableFields) => ReactElement | null)(field); // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion + + expect(renderIcon('visits')).toEqual(null); + + wrapper.find(SortingDropdown).simulate('change', 'visits'); + expect(renderIcon('visits')).toEqual(null); + + wrapper.find(SortingDropdown).simulate('change', 'visits', 'ASC'); + expect(renderIcon('visits')).not.toEqual(null); + }); + + it('handles order by through table', () => { + const orderByColumn: (field: OrderableFields) => Function = wrapper.find(ShortUrlsTable).prop('orderByColumn'); + + orderByColumn('visits')(); + orderByColumn('title')(); + orderByColumn('shortCode')(); + + expect(listShortUrlsMock).toHaveBeenCalledTimes(3); + expect(listShortUrlsMock).toHaveBeenNthCalledWith(1, expect.objectContaining({ + orderBy: { visits: 'ASC' }, + })); + expect(listShortUrlsMock).toHaveBeenNthCalledWith(2, expect.objectContaining({ + orderBy: { title: 'ASC' }, + })); + expect(listShortUrlsMock).toHaveBeenNthCalledWith(3, expect.objectContaining({ + orderBy: { shortCode: 'ASC' }, + })); + }); + + it('handles order by through dropdown', () => { + expect(wrapper.find(SortingDropdown).prop('orderField')).not.toBeDefined(); + expect(wrapper.find(SortingDropdown).prop('orderDir')).not.toBeDefined(); + + wrapper.find(SortingDropdown).simulate('change', 'visits', 'ASC'); + + expect(wrapper.find(SortingDropdown).prop('orderField')).toEqual('visits'); + expect(wrapper.find(SortingDropdown).prop('orderDir')).toEqual('ASC'); + + wrapper.find(SortingDropdown).simulate('change', 'shortCode', 'DESC'); + + expect(wrapper.find(SortingDropdown).prop('orderField')).toEqual('shortCode'); + expect(wrapper.find(SortingDropdown).prop('orderDir')).toEqual('DESC'); + + wrapper.find(SortingDropdown).simulate('change', undefined, undefined); + + expect(wrapper.find(SortingDropdown).prop('orderField')).toEqual(undefined); + expect(wrapper.find(SortingDropdown).prop('orderDir')).toEqual(undefined); + + expect(listShortUrlsMock).toHaveBeenCalledTimes(3); + expect(listShortUrlsMock).toHaveBeenNthCalledWith(1, expect.objectContaining({ + orderBy: { visits: 'ASC' }, + })); + expect(listShortUrlsMock).toHaveBeenNthCalledWith(2, expect.objectContaining({ + orderBy: { shortCode: 'DESC' }, + })); + expect(listShortUrlsMock).toHaveBeenNthCalledWith(3, expect.objectContaining({ orderBy: undefined })); + }); + + it.each([ + [ Mock.of({ visits: 'ASC' }), 'visits', 'ASC' ], + [ Mock.of({ title: 'DESC' }), 'title', 'DESC' ], + [ Mock.of(), undefined, undefined ], + ])('has expected initial ordering', (initialOrderBy, expectedField, expectedDir) => { + const wrapper = createWrapper(initialOrderBy); + + expect(wrapper.find(SortingDropdown).prop('orderField')).toEqual(expectedField); + expect(wrapper.find(SortingDropdown).prop('orderDir')).toEqual(expectedDir); }); });