diff --git a/.eslintrc b/.eslintrc index dd2b27c9..84103e15 100644 --- a/.eslintrc +++ b/.eslintrc @@ -69,7 +69,8 @@ "ignoreComments": true }], "no-mixed-operators": "off", - "react/display-name": "off" + "react/display-name": "off", + "@typescript-eslint/require-array-sort-compare": "off" } } ] diff --git a/src/container/types.ts b/src/container/types.ts index ae26671d..f5fc3ba8 100644 --- a/src/container/types.ts +++ b/src/container/types.ts @@ -9,6 +9,9 @@ import { ShortUrlEdition } from '../short-urls/reducers/shortUrlEdition'; import { ShortUrlsListParams } from '../short-urls/reducers/shortUrlsListParams'; import { ShortUrlTags } from '../short-urls/reducers/shortUrlTags'; import { ShortUrlsList } from '../short-urls/reducers/shortUrlsList'; +import { TagDeletion } from '../tags/reducers/tagDelete'; +import { TagEdition } from '../tags/reducers/tagEdit'; +import { TagsList } from '../tags/reducers/tagsList'; export interface ShlinkState { servers: ServersMap; @@ -23,9 +26,9 @@ export interface ShlinkState { shortUrlVisits: any; tagVisits: any; shortUrlDetail: any; - tagsList: any; - tagDelete: any; - tagEdit: any; + tagsList: TagsList; + tagDelete: TagDeletion; + tagEdit: TagEdition; mercureInfo: MercureInfo; settings: Settings; } diff --git a/src/tags/reducers/tagDelete.js b/src/tags/reducers/tagDelete.ts similarity index 58% rename from src/tags/reducers/tagDelete.js rename to src/tags/reducers/tagDelete.ts index 14486bea..8fa85414 100644 --- a/src/tags/reducers/tagDelete.js +++ b/src/tags/reducers/tagDelete.ts @@ -1,5 +1,8 @@ -import { handleActions } from 'redux-actions'; import PropTypes from 'prop-types'; +import { Action, Dispatch } from 'redux'; +import { buildReducer } from '../../utils/helpers/redux'; +import { GetState } from '../../container/types'; +import { ShlinkApiClientBuilder } from '../../utils/services/types'; /* eslint-disable padding-line-between-statements */ export const DELETE_TAG_START = 'shlink/deleteTag/DELETE_TAG_START'; @@ -8,23 +11,36 @@ export const DELETE_TAG = 'shlink/deleteTag/DELETE_TAG'; export const TAG_DELETED = 'shlink/deleteTag/TAG_DELETED'; /* eslint-enable padding-line-between-statements */ +/** @deprecated Use TagDeletion interface */ export const tagDeleteType = PropTypes.shape({ deleting: PropTypes.bool, error: PropTypes.bool, }); -const initialState = { +export interface TagDeletion { + deleting: boolean; + error: boolean; +} + +export interface DeleteTagAction extends Action { + tag: string; +} + +const initialState: TagDeletion = { deleting: false, error: false, }; -export default handleActions({ +export default buildReducer({ [DELETE_TAG_START]: () => ({ deleting: true, error: false }), [DELETE_TAG_ERROR]: () => ({ deleting: false, error: true }), [DELETE_TAG]: () => ({ deleting: false, error: false }), }, initialState); -export const deleteTag = (buildShlinkApiClient) => (tag) => async (dispatch, getState) => { +export const deleteTag = (buildShlinkApiClient: ShlinkApiClientBuilder) => (tag: string) => async ( + dispatch: Dispatch, + getState: GetState, +) => { dispatch({ type: DELETE_TAG_START }); const { deleteTags } = buildShlinkApiClient(getState); @@ -38,4 +54,4 @@ export const deleteTag = (buildShlinkApiClient) => (tag) => async (dispatch, get } }; -export const tagDeleted = (tag) => ({ type: TAG_DELETED, tag }); +export const tagDeleted = (tag: string): DeleteTagAction => ({ type: TAG_DELETED, tag }); diff --git a/src/tags/reducers/tagEdit.js b/src/tags/reducers/tagEdit.ts similarity index 53% rename from src/tags/reducers/tagEdit.js rename to src/tags/reducers/tagEdit.ts index 137f36df..b3e631c8 100644 --- a/src/tags/reducers/tagEdit.js +++ b/src/tags/reducers/tagEdit.ts @@ -1,5 +1,9 @@ import { pick } from 'ramda'; -import { handleActions } from 'redux-actions'; +import { Action, Dispatch } from 'redux'; +import { buildReducer } from '../../utils/helpers/redux'; +import { GetState } from '../../container/types'; +import { ShlinkApiClientBuilder } from '../../utils/services/types'; +import ColorGenerator from '../../utils/services/ColorGenerator'; /* eslint-disable padding-line-between-statements */ export const EDIT_TAG_START = 'shlink/editTag/EDIT_TAG_START'; @@ -9,27 +13,41 @@ export const EDIT_TAG = 'shlink/editTag/EDIT_TAG'; export const TAG_EDITED = 'shlink/editTag/TAG_EDITED'; -const initialState = { +export interface TagEdition { + oldName: string; + newName: string; + editing: boolean; + error: boolean; +} + +export interface EditTagAction extends Action { + oldName: string; + newName: string; + color: string; +} + +const initialState: TagEdition = { oldName: '', newName: '', editing: false, error: false, }; -export default handleActions({ +export default buildReducer({ [EDIT_TAG_START]: (state) => ({ ...state, editing: true, error: false }), [EDIT_TAG_ERROR]: (state) => ({ ...state, editing: false, error: true }), - [EDIT_TAG]: (state, action) => ({ + [EDIT_TAG]: (_, action) => ({ ...pick([ 'oldName', 'newName' ], action), editing: false, error: false, }), }, initialState); -export const editTag = (buildShlinkApiClient, colorGenerator) => (oldName, newName, color) => async ( - dispatch, - getState, -) => { +export const editTag = (buildShlinkApiClient: ShlinkApiClientBuilder, colorGenerator: ColorGenerator) => ( + oldName: string, + newName: string, + color: string, +) => async (dispatch: Dispatch, getState: GetState) => { dispatch({ type: EDIT_TAG_START }); const { editTag } = buildShlinkApiClient(getState); @@ -44,7 +62,7 @@ export const editTag = (buildShlinkApiClient, colorGenerator) => (oldName, newNa } }; -export const tagEdited = (oldName, newName, color) => ({ +export const tagEdited = (oldName: string, newName: string, color: string): EditTagAction => ({ type: TAG_EDITED, oldName, newName, diff --git a/src/tags/reducers/tagsList.js b/src/tags/reducers/tagsList.js deleted file mode 100644 index 643b3a94..00000000 --- a/src/tags/reducers/tagsList.js +++ /dev/null @@ -1,99 +0,0 @@ -import { handleActions } from 'redux-actions'; -import { isEmpty, reject } from 'ramda'; -import PropTypes from 'prop-types'; -import { CREATE_VISIT } from '../../visits/reducers/visitCreation'; -import { TAG_DELETED } from './tagDelete'; -import { TAG_EDITED } from './tagEdit'; - -/* eslint-disable padding-line-between-statements */ -export const LIST_TAGS_START = 'shlink/tagsList/LIST_TAGS_START'; -export const LIST_TAGS_ERROR = 'shlink/tagsList/LIST_TAGS_ERROR'; -export const LIST_TAGS = 'shlink/tagsList/LIST_TAGS'; -export const FILTER_TAGS = 'shlink/tagsList/FILTER_TAGS'; -/* eslint-enable padding-line-between-statements */ - -const TagStatsType = PropTypes.shape({ - shortUrlsCount: PropTypes.number, - visitsCount: PropTypes.number, -}); - -export const TagsListType = PropTypes.shape({ - tags: PropTypes.arrayOf(PropTypes.string), - filteredTags: PropTypes.arrayOf(PropTypes.string), - stats: PropTypes.objectOf(TagStatsType), // Record - loading: PropTypes.bool, - error: PropTypes.bool, -}); - -const initialState = { - tags: [], - filteredTags: [], - stats: {}, - loading: false, - error: false, -}; - -const renameTag = (oldName, newName) => (tag) => tag === oldName ? newName : tag; -const rejectTag = (tags, tagToReject) => reject((tag) => tag === tagToReject, tags); -const increaseVisitsForTags = (tags, stats) => tags.reduce((stats, tag) => { - if (!stats[tag]) { - return stats; - } - - const tagStats = stats[tag]; - - tagStats.visitsCount = tagStats.visitsCount + 1; - stats[tag] = tagStats; - - return stats; -}, { ...stats }); - -export default handleActions({ - [LIST_TAGS_START]: () => ({ ...initialState, loading: true }), - [LIST_TAGS_ERROR]: () => ({ ...initialState, error: true }), - [LIST_TAGS]: (state, { tags, stats }) => ({ ...initialState, stats, tags, filteredTags: tags }), - [TAG_DELETED]: (state, { tag }) => ({ - ...state, - tags: rejectTag(state.tags, tag), - filteredTags: rejectTag(state.filteredTags, tag), - }), - [TAG_EDITED]: (state, { oldName, newName }) => ({ - ...state, - tags: state.tags.map(renameTag(oldName, newName)).sort(), - filteredTags: state.filteredTags.map(renameTag(oldName, newName)).sort(), - }), - [FILTER_TAGS]: (state, { searchTerm }) => ({ - ...state, - filteredTags: state.tags.filter((tag) => tag.toLowerCase().match(searchTerm)), - }), - [CREATE_VISIT]: (state, { shortUrl }) => ({ - ...state, - stats: increaseVisitsForTags(shortUrl.tags, state.stats), - }), -}, initialState); - -export const listTags = (buildShlinkApiClient, force = true) => () => async (dispatch, getState) => { - const { tagsList } = getState(); - - if (!force && (tagsList.loading || !isEmpty(tagsList.tags))) { - return; - } - - dispatch({ type: LIST_TAGS_START }); - - try { - const { listTags } = buildShlinkApiClient(getState); - const { tags, stats = [] } = await listTags(); - const processedStats = stats.reduce((acc, { tag, shortUrlsCount, visitsCount }) => { - acc[tag] = { shortUrlsCount, visitsCount }; - - return acc; - }, {}); - - dispatch({ tags, stats: processedStats, type: LIST_TAGS }); - } catch (e) { - dispatch({ type: LIST_TAGS_ERROR }); - } -}; - -export const filterTags = (searchTerm) => ({ type: FILTER_TAGS, searchTerm }); diff --git a/src/tags/reducers/tagsList.ts b/src/tags/reducers/tagsList.ts new file mode 100644 index 00000000..207e64a8 --- /dev/null +++ b/src/tags/reducers/tagsList.ts @@ -0,0 +1,125 @@ +import { isEmpty, reject } from 'ramda'; +import PropTypes from 'prop-types'; +import { Action, Dispatch } from 'redux'; +import { CREATE_VISIT, CreateVisitAction } from '../../visits/reducers/visitCreation'; +import { buildReducer } from '../../utils/helpers/redux'; +import { ShlinkApiClientBuilder, ShlinkTags } from '../../utils/services/types'; +import { GetState } from '../../container/types'; +import { DeleteTagAction, TAG_DELETED } from './tagDelete'; +import { EditTagAction, TAG_EDITED } from './tagEdit'; + +/* eslint-disable padding-line-between-statements */ +export const LIST_TAGS_START = 'shlink/tagsList/LIST_TAGS_START'; +export const LIST_TAGS_ERROR = 'shlink/tagsList/LIST_TAGS_ERROR'; +export const LIST_TAGS = 'shlink/tagsList/LIST_TAGS'; +export const FILTER_TAGS = 'shlink/tagsList/FILTER_TAGS'; +/* eslint-enable padding-line-between-statements */ + +/** @deprecated Use TagsList interface instead */ +export const TagsListType = PropTypes.shape({ + tags: PropTypes.arrayOf(PropTypes.string), + filteredTags: PropTypes.arrayOf(PropTypes.string), + stats: PropTypes.objectOf(PropTypes.shape({ + shortUrlsCount: PropTypes.number, + visitsCount: PropTypes.number, + })), // Record + loading: PropTypes.bool, + error: PropTypes.bool, +}); + +type TagsStats = Record; + +export interface TagsList { + tags: string[]; + filteredTags: string[]; + stats: TagsStats; + loading: boolean; + error: boolean; +} + +interface ListTagsAction extends Action { + tags: string[]; + stats: TagsStats; +} + +interface FilterTagsAction extends Action { + searchTerm: string; +} + +type ListTagsCombinedAction = ListTagsAction & DeleteTagAction & CreateVisitAction & EditTagAction & FilterTagsAction; + +const initialState = { + tags: [], + filteredTags: [], + stats: {}, + loading: false, + error: false, +}; + +const renameTag = (oldName: string, newName: string) => (tag: string) => tag === oldName ? newName : tag; +const rejectTag = (tags: string[], tagToReject: string) => reject((tag) => tag === tagToReject, tags); +const increaseVisitsForTags = (tags: string[], stats: TagsStats) => tags.reduce((stats, tag) => { + if (!stats[tag]) { + return stats; + } + + const tagStats = stats[tag]; + + tagStats.visitsCount = tagStats.visitsCount + 1; + stats[tag] = tagStats; + + return stats; +}, { ...stats }); + +export default buildReducer({ + [LIST_TAGS_START]: () => ({ ...initialState, loading: true }), + [LIST_TAGS_ERROR]: () => ({ ...initialState, error: true }), + [LIST_TAGS]: (_, { tags, stats }) => ({ ...initialState, stats, tags, filteredTags: tags }), + [TAG_DELETED]: (state, { tag }) => ({ + ...state, + tags: rejectTag(state.tags, tag), + filteredTags: rejectTag(state.filteredTags, tag), + }), + [TAG_EDITED]: (state, { oldName, newName }) => ({ + ...state, + tags: state.tags.map(renameTag(oldName, newName)).sort(), + filteredTags: state.filteredTags.map(renameTag(oldName, newName)).sort(), + }), + [FILTER_TAGS]: (state, { searchTerm }) => ({ + ...state, + filteredTags: state.tags.filter((tag) => tag.toLowerCase().match(searchTerm)), + }), + [CREATE_VISIT]: (state, { shortUrl }) => ({ + ...state, + stats: increaseVisitsForTags(shortUrl.tags, state.stats), + }), +}, initialState); + +export const listTags = (buildShlinkApiClient: ShlinkApiClientBuilder, force = true) => () => async ( + dispatch: Dispatch, + getState: GetState, +) => { + const { tagsList } = getState(); + + if (!force && (tagsList.loading || !isEmpty(tagsList.tags))) { + return; + } + + dispatch({ type: LIST_TAGS_START }); + + try { + const { listTags } = buildShlinkApiClient(getState); + const { tags, stats = [] }: ShlinkTags = await listTags(); + const processedStats = stats.reduce((acc, { tag, shortUrlsCount, visitsCount }) => { + acc[tag] = { shortUrlsCount, visitsCount }; + + return acc; + }, {}); + + dispatch({ tags, stats: processedStats, type: LIST_TAGS }); + } catch (e) { + dispatch({ type: LIST_TAGS_ERROR }); + } +}; + +export const filterTags = (searchTerm: string): FilterTagsAction => ({ type: FILTER_TAGS, searchTerm }); diff --git a/src/utils/services/types.ts b/src/utils/services/types.ts index b52a83ae..a1c55880 100644 --- a/src/utils/services/types.ts +++ b/src/utils/services/types.ts @@ -15,6 +15,17 @@ export interface ShlinkHealth { version: string; } +interface ShlinkTagsStats { + tag: string; + shortUrlsCount: number; + visitsCount: number; +} + +export interface ShlinkTags { + tags: string[]; + stats?: ShlinkTagsStats[]; +} + export interface ProblemDetailsError { type: string; detail: string; diff --git a/test/tags/reducers/tagDelete.test.js b/test/tags/reducers/tagDelete.test.ts similarity index 79% rename from test/tags/reducers/tagDelete.test.js rename to test/tags/reducers/tagDelete.test.ts index 1978917a..e032f3da 100644 --- a/test/tags/reducers/tagDelete.test.js +++ b/test/tags/reducers/tagDelete.test.ts @@ -1,3 +1,4 @@ +import { Mock } from 'ts-mockery'; import reducer, { DELETE_TAG_START, DELETE_TAG_ERROR, @@ -6,25 +7,27 @@ import reducer, { tagDeleted, deleteTag, } from '../../../src/tags/reducers/tagDelete'; +import ShlinkApiClient from '../../../src/utils/services/ShlinkApiClient'; +import { ShlinkState } from '../../../src/container/types'; describe('tagDeleteReducer', () => { describe('reducer', () => { it('returns loading on DELETE_TAG_START', () => { - expect(reducer({}, { type: DELETE_TAG_START })).toEqual({ + expect(reducer(undefined, { type: DELETE_TAG_START })).toEqual({ deleting: true, error: false, }); }); it('returns error on DELETE_TAG_ERROR', () => { - expect(reducer({}, { type: DELETE_TAG_ERROR })).toEqual({ + expect(reducer(undefined, { type: DELETE_TAG_ERROR })).toEqual({ deleting: false, error: true, }); }); it('returns tag names on DELETE_TAG', () => { - expect(reducer({}, { type: DELETE_TAG })).toEqual({ + expect(reducer(undefined, { type: DELETE_TAG })).toEqual({ deleting: false, error: false, }); @@ -40,11 +43,11 @@ describe('tagDeleteReducer', () => { }); describe('deleteTag', () => { - const createApiClientMock = (result) => ({ - deleteTags: jest.fn(() => result), + const createApiClientMock = (result: Promise) => Mock.of({ + deleteTags: jest.fn(async () => result), }); const dispatch = jest.fn(); - const getState = () => ({}); + const getState = () => Mock.all(); afterEach(() => dispatch.mockReset()); diff --git a/test/tags/reducers/tagEdit.test.js b/test/tags/reducers/tagEdit.test.ts similarity index 72% rename from test/tags/reducers/tagEdit.test.js rename to test/tags/reducers/tagEdit.test.ts index f70a3fa7..d825ce42 100644 --- a/test/tags/reducers/tagEdit.test.js +++ b/test/tags/reducers/tagEdit.test.ts @@ -1,3 +1,4 @@ +import { Mock } from 'ts-mockery'; import reducer, { EDIT_TAG_START, EDIT_TAG_ERROR, @@ -5,26 +6,38 @@ import reducer, { TAG_EDITED, tagEdited, editTag, + EditTagAction, } from '../../../src/tags/reducers/tagEdit'; +import ShlinkApiClient from '../../../src/utils/services/ShlinkApiClient'; +import ColorGenerator from '../../../src/utils/services/ColorGenerator'; +import { ShlinkState } from '../../../src/container/types'; describe('tagEditReducer', () => { + const oldName = 'foo'; + const newName = 'bar'; + const color = '#ff0000'; + describe('reducer', () => { it('returns loading on EDIT_TAG_START', () => { - expect(reducer({}, { type: EDIT_TAG_START })).toEqual({ + expect(reducer(undefined, Mock.of({ type: EDIT_TAG_START }))).toEqual({ editing: true, error: false, + oldName: '', + newName: '', }); }); it('returns error on EDIT_TAG_ERROR', () => { - expect(reducer({}, { type: EDIT_TAG_ERROR })).toEqual({ + expect(reducer(undefined, Mock.of({ type: EDIT_TAG_ERROR }))).toEqual({ editing: false, error: true, + oldName: '', + newName: '', }); }); it('returns tag names on EDIT_TAG', () => { - expect(reducer({}, { type: EDIT_TAG, oldName: 'foo', newName: 'bar' })).toEqual({ + expect(reducer(undefined, { type: EDIT_TAG, oldName, newName, color })).toEqual({ editing: false, error: false, oldName: 'foo', @@ -44,24 +57,18 @@ describe('tagEditReducer', () => { }); describe('editTag', () => { - const createApiClientMock = (result) => ({ - editTag: jest.fn(() => result), + const createApiClientMock = (result: Promise) => Mock.of({ + editTag: jest.fn(async () => result), }); - const colorGenerator = { + const colorGenerator = Mock.of({ setColorForKey: jest.fn(), - }; - const dispatch = jest.fn(); - const getState = () => ({}); - - afterEach(() => { - colorGenerator.setColorForKey.mockReset(); - dispatch.mockReset(); }); + const dispatch = jest.fn(); + const getState = () => Mock.of(); + + afterEach(jest.clearAllMocks); it('calls API on success', async () => { - const oldName = 'foo'; - const newName = 'bar'; - const color = '#ff0000'; const apiClientMock = createApiClientMock(Promise.resolve()); const dispatchable = editTag(() => apiClientMock, colorGenerator)(oldName, newName, color); @@ -80,9 +87,6 @@ describe('tagEditReducer', () => { it('throws on error', async () => { const error = 'Error'; - const oldName = 'foo'; - const newName = 'bar'; - const color = '#ff0000'; const apiClientMock = createApiClientMock(Promise.reject(error)); const dispatchable = editTag(() => apiClientMock, colorGenerator)(oldName, newName, color); diff --git a/test/tags/reducers/tagsList.test.js b/test/tags/reducers/tagsList.test.ts similarity index 75% rename from test/tags/reducers/tagsList.test.js rename to test/tags/reducers/tagsList.test.ts index da86b564..884efc1b 100644 --- a/test/tags/reducers/tagsList.test.js +++ b/test/tags/reducers/tagsList.test.ts @@ -1,24 +1,30 @@ +import { Mock } from 'ts-mockery'; import reducer, { FILTER_TAGS, filterTags, LIST_TAGS, LIST_TAGS_ERROR, - LIST_TAGS_START, listTags, + LIST_TAGS_START, + listTags, + TagsList, } from '../../../src/tags/reducers/tagsList'; import { TAG_DELETED } from '../../../src/tags/reducers/tagDelete'; import { TAG_EDITED } from '../../../src/tags/reducers/tagEdit'; +import { ShlinkState } from '../../../src/container/types'; describe('tagsListReducer', () => { + const state = (props: Partial) => Mock.of(props); + describe('reducer', () => { it('returns loading on LIST_TAGS_START', () => { - expect(reducer({}, { type: LIST_TAGS_START })).toEqual(expect.objectContaining({ + expect(reducer(undefined, { type: LIST_TAGS_START } as any)).toEqual(expect.objectContaining({ loading: true, error: false, })); }); it('returns error on LIST_TAGS_ERROR', () => { - expect(reducer({}, { type: LIST_TAGS_ERROR })).toEqual(expect.objectContaining({ + expect(reducer(undefined, { type: LIST_TAGS_ERROR } as any)).toEqual(expect.objectContaining({ loading: false, error: true, })); @@ -27,7 +33,7 @@ describe('tagsListReducer', () => { it('returns provided tags as filtered and regular tags on LIST_TAGS', () => { const tags = [ 'foo', 'bar', 'baz' ]; - expect(reducer({}, { type: LIST_TAGS, tags })).toEqual({ + expect(reducer(undefined, { type: LIST_TAGS, tags } as any)).toEqual({ tags, filteredTags: tags, loading: false, @@ -40,7 +46,7 @@ describe('tagsListReducer', () => { const tag = 'foo'; const expectedTags = [ 'bar', 'baz' ]; - expect(reducer({ tags, filteredTags: tags }, { type: TAG_DELETED, tag })).toEqual({ + expect(reducer(state({ tags, filteredTags: tags }), { type: TAG_DELETED, tag } as any)).toEqual({ tags: expectedTags, filteredTags: expectedTags, }); @@ -52,7 +58,7 @@ describe('tagsListReducer', () => { const newName = 'renamed'; const expectedTags = [ 'foo', 'renamed', 'baz' ].sort(); - expect(reducer({ tags, filteredTags: tags }, { type: TAG_EDITED, oldName, newName })).toEqual({ + expect(reducer(state({ tags, filteredTags: tags }), { type: TAG_EDITED, oldName, newName } as any)).toEqual({ tags: expectedTags, filteredTags: expectedTags, }); @@ -63,7 +69,7 @@ describe('tagsListReducer', () => { const searchTerm = 'fo'; const filteredTags = [ 'foo', 'foo2', 'fo' ]; - expect(reducer({ tags }, { type: FILTER_TAGS, searchTerm })).toEqual({ + expect(reducer(state({ tags }), { type: FILTER_TAGS, searchTerm } as any)).toEqual({ tags, filteredTags, }); @@ -76,19 +82,14 @@ describe('tagsListReducer', () => { describe('listTags', () => { const dispatch = jest.fn(); - const getState = jest.fn(() => ({})); + const getState = jest.fn(() => Mock.all()); const buildShlinkApiClient = jest.fn(); const listTagsMock = jest.fn(); - afterEach(() => { - dispatch.mockReset(); - getState.mockClear(); - buildShlinkApiClient.mockReset(); - listTagsMock.mockReset(); - }); + afterEach(jest.clearAllMocks); - const assertNoAction = async (tagsList) => { - getState.mockReturnValue({ tagsList }); + const assertNoAction = async (tagsList: TagsList) => { + getState.mockReturnValue(Mock.of({ tagsList })); await listTags(buildShlinkApiClient, false)()(dispatch, getState); @@ -97,8 +98,11 @@ describe('tagsListReducer', () => { expect(getState).toHaveBeenCalledTimes(1); }; - it('does nothing when loading', async () => await assertNoAction({ loading: true })); - it('does nothing when list is not empty', async () => await assertNoAction({ loading: false, tags: [ 'foo', 'bar' ] })); + it('does nothing when loading', async () => assertNoAction(state({ loading: true }))); + it( + 'does nothing when list is not empty', + async () => assertNoAction(state({ loading: false, tags: [ 'foo', 'bar' ] })), + ); it('dispatches loaded lists when no error occurs', async () => { const tags = [ 'foo', 'bar', 'baz' ];