From e22856ff7467e8b33b6c90347fa482e9f998e9c3 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 23 Dec 2021 10:38:02 +0100 Subject: [PATCH] Added logic in reducers to fallback to a different date interval if default one returns no visits --- src/visits/reducers/common.ts | 25 +++++++++-- src/visits/reducers/orphanVisits.ts | 21 +++++++-- src/visits/reducers/shortUrlVisits.ts | 21 ++++++--- src/visits/reducers/tagVisits.ts | 14 ++++-- test/visits/reducers/orphanVisits.test.ts | 43 ++++++++++++++++++- test/visits/reducers/shortUrlVisits.test.ts | 43 ++++++++++++++++++- test/visits/reducers/tagVisits.test.ts | 47 +++++++++++++++++++-- 7 files changed, 193 insertions(+), 21 deletions(-) diff --git a/src/visits/reducers/common.ts b/src/visits/reducers/common.ts index 4e43445b..174b36df 100644 --- a/src/visits/reducers/common.ts +++ b/src/visits/reducers/common.ts @@ -1,9 +1,10 @@ import { flatten, prop, range, splitEvery } from 'ramda'; import { Action, Dispatch } from 'redux'; -import { ShlinkPaginator, ShlinkVisits } from '../../api/types'; +import { ShlinkPaginator, ShlinkVisits, ShlinkVisitsParams } from '../../api/types'; import { Visit } from '../types'; import { parseApiError } from '../../api/utils'; import { ApiErrorAction } from '../../api/types/actions'; +import { dateToMatchingInterval } from '../../utils/dates/types'; const ITEMS_PER_PAGE = 5000; const PARALLEL_REQUESTS_COUNT = 4; @@ -13,16 +14,19 @@ const isLastPage = ({ currentPage, pagesCount }: ShlinkPaginator): boolean => cu const calcProgress = (total: number, current: number): number => current * 100 / total; type VisitsLoader = (page: number, itemsPerPage: number) => Promise; +type LastVisitLoader = () => Promise; interface ActionMap { start: string; large: string; finish: string; error: string; progress: string; + fallbackToInterval: string; } export const getVisitsWithLoader = async & { visits: Visit[] }>( visitsLoader: VisitsLoader, + lastVisitLoader: LastVisitLoader, extraFinishActionData: Partial, actionMap: ActionMap, dispatch: Dispatch, @@ -69,10 +73,25 @@ export const getVisitsWithLoader = async & { visits: V }; try { - const visits = await loadVisits(); + const [ visits, lastVisit ] = await Promise.all([ loadVisits(), lastVisitLoader() ]); - dispatch({ ...extraFinishActionData, visits, type: actionMap.finish }); + dispatch( + !visits.length && lastVisit + ? { type: actionMap.fallbackToInterval, fallbackInterval: dateToMatchingInterval(lastVisit.date) } + : { ...extraFinishActionData, visits, type: actionMap.finish }, + ); } catch (e: any) { dispatch({ type: actionMap.error, errorData: parseApiError(e) }); } }; + +export const lastVisitLoaderForLoader = ( + doFallbackRange: boolean, + loader: (params: ShlinkVisitsParams) => Promise, +): LastVisitLoader => { + if (!doFallbackRange) { + return async () => Promise.resolve(undefined); + } + + return async () => loader({ page: 1, itemsPerPage: 1 }).then((result) => result.data[0]); +}; diff --git a/src/visits/reducers/orphanVisits.ts b/src/visits/reducers/orphanVisits.ts index 4a86c8e3..f212f323 100644 --- a/src/visits/reducers/orphanVisits.ts +++ b/src/visits/reducers/orphanVisits.ts @@ -1,5 +1,12 @@ import { Action, Dispatch } from 'redux'; -import { OrphanVisit, OrphanVisitType, Visit, VisitsInfo, VisitsLoadProgressChangedAction } from '../types'; +import { + OrphanVisit, + OrphanVisitType, + Visit, + VisitsFallbackIntervalAction, + VisitsInfo, + VisitsLoadProgressChangedAction, +} from '../types'; import { buildActionCreator, buildReducer } from '../../utils/helpers/redux'; import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; import { GetState } from '../../container/types'; @@ -7,7 +14,7 @@ import { ShlinkVisitsParams } from '../../api/types'; import { isOrphanVisit } from '../types/helpers'; import { ApiErrorAction } from '../../api/types/actions'; import { isBetween } from '../../utils/helpers/date'; -import { getVisitsWithLoader } from './common'; +import { getVisitsWithLoader, lastVisitLoaderForLoader } from './common'; import { CREATE_VISITS, CreateVisitsAction } from './visitCreation'; /* eslint-disable padding-line-between-statements */ @@ -17,6 +24,7 @@ export const GET_ORPHAN_VISITS = 'shlink/orphanVisits/GET_ORPHAN_VISITS'; export const GET_ORPHAN_VISITS_LARGE = 'shlink/orphanVisits/GET_ORPHAN_VISITS_LARGE'; export const GET_ORPHAN_VISITS_CANCEL = 'shlink/orphanVisits/GET_ORPHAN_VISITS_CANCEL'; export const GET_ORPHAN_VISITS_PROGRESS_CHANGED = 'shlink/orphanVisits/GET_ORPHAN_VISITS_PROGRESS_CHANGED'; +export const GET_ORPHAN_VISITS_FALLBACK_TO_INTERVAL = 'shlink/orphanVisits/GET_ORPHAN_VISITS_FALLBACK_TO_INTERVAL'; /* eslint-enable padding-line-between-statements */ export interface OrphanVisitsAction extends Action { @@ -26,6 +34,7 @@ export interface OrphanVisitsAction extends Action { type OrphanVisitsCombinedAction = OrphanVisitsAction & VisitsLoadProgressChangedAction +& VisitsFallbackIntervalAction & CreateVisitsAction & ApiErrorAction; @@ -41,10 +50,11 @@ const initialState: VisitsInfo = { export default buildReducer({ [GET_ORPHAN_VISITS_START]: () => ({ ...initialState, loading: true }), [GET_ORPHAN_VISITS_ERROR]: (_, { errorData }) => ({ ...initialState, error: true, errorData }), - [GET_ORPHAN_VISITS]: (_, { visits, query }) => ({ ...initialState, visits, query }), + [GET_ORPHAN_VISITS]: (state, { visits, query }) => ({ ...state, visits, query, loading: false, error: false }), [GET_ORPHAN_VISITS_LARGE]: (state) => ({ ...state, loadingLarge: true }), [GET_ORPHAN_VISITS_CANCEL]: (state) => ({ ...state, cancelLoad: true }), [GET_ORPHAN_VISITS_PROGRESS_CHANGED]: (state, { progress }) => ({ ...state, progress }), + [GET_ORPHAN_VISITS_FALLBACK_TO_INTERVAL]: (state, { fallbackInterval }) => ({ ...state, fallbackInterval }), [CREATE_VISITS]: (state, { createdVisits }) => { const { visits, query = {} } = state; const { startDate, endDate } = query; @@ -62,6 +72,7 @@ const matchesType = (visit: OrphanVisit, orphanVisitsType?: OrphanVisitType) => export const getOrphanVisits = (buildShlinkApiClient: ShlinkApiClientBuilder) => ( query: ShlinkVisitsParams = {}, orphanVisitsType?: OrphanVisitType, + doFallbackRange = false, ) => async (dispatch: Dispatch, getState: GetState) => { const { getOrphanVisits } = buildShlinkApiClient(getState); const visitsLoader = async (page: number, itemsPerPage: number) => getOrphanVisits({ ...query, page, itemsPerPage }) @@ -70,6 +81,7 @@ export const getOrphanVisits = (buildShlinkApiClient: ShlinkApiClientBuilder) => return { ...result, data: visits }; }); + const lastVisitLoader = lastVisitLoaderForLoader(doFallbackRange, getOrphanVisits); const shouldCancel = () => getState().orphanVisits.cancelLoad; const extraFinishActionData: Partial = { query }; const actionMap = { @@ -78,9 +90,10 @@ export const getOrphanVisits = (buildShlinkApiClient: ShlinkApiClientBuilder) => finish: GET_ORPHAN_VISITS, error: GET_ORPHAN_VISITS_ERROR, progress: GET_ORPHAN_VISITS_PROGRESS_CHANGED, + fallbackToInterval: GET_ORPHAN_VISITS_FALLBACK_TO_INTERVAL, }; - return getVisitsWithLoader(visitsLoader, extraFinishActionData, actionMap, dispatch, shouldCancel); + return getVisitsWithLoader(visitsLoader, lastVisitLoader, extraFinishActionData, actionMap, dispatch, shouldCancel); }; export const cancelGetOrphanVisits = buildActionCreator(GET_ORPHAN_VISITS_CANCEL); diff --git a/src/visits/reducers/shortUrlVisits.ts b/src/visits/reducers/shortUrlVisits.ts index 81b26ca0..7d5daa17 100644 --- a/src/visits/reducers/shortUrlVisits.ts +++ b/src/visits/reducers/shortUrlVisits.ts @@ -1,6 +1,6 @@ import { Action, Dispatch } from 'redux'; import { shortUrlMatches } from '../../short-urls/helpers'; -import { Visit, VisitsInfo, VisitsLoadProgressChangedAction } from '../types'; +import { Visit, VisitsFallbackIntervalAction, VisitsInfo, VisitsLoadProgressChangedAction } from '../types'; import { ShortUrlIdentifier } from '../../short-urls/data'; import { buildActionCreator, buildReducer } from '../../utils/helpers/redux'; import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; @@ -8,7 +8,7 @@ import { GetState } from '../../container/types'; import { ShlinkVisitsParams } from '../../api/types'; import { ApiErrorAction } from '../../api/types/actions'; import { isBetween } from '../../utils/helpers/date'; -import { getVisitsWithLoader } from './common'; +import { getVisitsWithLoader, lastVisitLoaderForLoader } from './common'; import { CREATE_VISITS, CreateVisitsAction } from './visitCreation'; /* eslint-disable padding-line-between-statements */ @@ -18,6 +18,7 @@ export const GET_SHORT_URL_VISITS = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS' export const GET_SHORT_URL_VISITS_LARGE = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_LARGE'; export const GET_SHORT_URL_VISITS_CANCEL = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_CANCEL'; export const GET_SHORT_URL_VISITS_PROGRESS_CHANGED = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_PROGRESS_CHANGED'; +export const GET_SHORT_URL_VISITS_FALLBACK_TO_INTERVAL = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_FALLBACK_TO_INTERVAL'; /* eslint-enable padding-line-between-statements */ export interface ShortUrlVisits extends VisitsInfo, ShortUrlIdentifier {} @@ -29,6 +30,7 @@ interface ShortUrlVisitsAction extends Action, ShortUrlIdentifier { type ShortUrlVisitsCombinedAction = ShortUrlVisitsAction & VisitsLoadProgressChangedAction +& VisitsFallbackIntervalAction & CreateVisitsAction & ApiErrorAction; @@ -46,16 +48,19 @@ const initialState: ShortUrlVisits = { export default buildReducer({ [GET_SHORT_URL_VISITS_START]: () => ({ ...initialState, loading: true }), [GET_SHORT_URL_VISITS_ERROR]: (_, { errorData }) => ({ ...initialState, error: true, errorData }), - [GET_SHORT_URL_VISITS]: (_, { visits, query, shortCode, domain }) => ({ - ...initialState, + [GET_SHORT_URL_VISITS]: (state, { visits, query, shortCode, domain }) => ({ + ...state, visits, shortCode, domain, query, + loading: false, + error: false, }), [GET_SHORT_URL_VISITS_LARGE]: (state) => ({ ...state, loadingLarge: true }), [GET_SHORT_URL_VISITS_CANCEL]: (state) => ({ ...state, cancelLoad: true }), [GET_SHORT_URL_VISITS_PROGRESS_CHANGED]: (state, { progress }) => ({ ...state, progress }), + [GET_SHORT_URL_VISITS_FALLBACK_TO_INTERVAL]: (state, { fallbackInterval }) => ({ ...state, fallbackInterval }), [CREATE_VISITS]: (state, { createdVisits }) => { const { shortCode, domain, visits, query = {} } = state; const { startDate, endDate } = query; @@ -73,12 +78,17 @@ export default buildReducer({ export const getShortUrlVisits = (buildShlinkApiClient: ShlinkApiClientBuilder) => ( shortCode: string, query: ShlinkVisitsParams = {}, + doFallbackRange = false, ) => async (dispatch: Dispatch, getState: GetState) => { const { getShortUrlVisits } = buildShlinkApiClient(getState); const visitsLoader = async (page: number, itemsPerPage: number) => getShortUrlVisits( shortCode, { ...query, page, itemsPerPage }, ); + const lastVisitLoader = lastVisitLoaderForLoader( + doFallbackRange, + async (params) => getShortUrlVisits(shortCode, { ...params, domain: query.domain }), + ); const shouldCancel = () => getState().shortUrlVisits.cancelLoad; const extraFinishActionData: Partial = { shortCode, query, domain: query.domain }; const actionMap = { @@ -87,9 +97,10 @@ export const getShortUrlVisits = (buildShlinkApiClient: ShlinkApiClientBuilder) finish: GET_SHORT_URL_VISITS, error: GET_SHORT_URL_VISITS_ERROR, progress: GET_SHORT_URL_VISITS_PROGRESS_CHANGED, + fallbackToInterval: GET_SHORT_URL_VISITS_FALLBACK_TO_INTERVAL, }; - return getVisitsWithLoader(visitsLoader, extraFinishActionData, actionMap, dispatch, shouldCancel); + return getVisitsWithLoader(visitsLoader, lastVisitLoader, extraFinishActionData, actionMap, dispatch, shouldCancel); }; export const cancelGetShortUrlVisits = buildActionCreator(GET_SHORT_URL_VISITS_CANCEL); diff --git a/src/visits/reducers/tagVisits.ts b/src/visits/reducers/tagVisits.ts index c628021f..ff96f90b 100644 --- a/src/visits/reducers/tagVisits.ts +++ b/src/visits/reducers/tagVisits.ts @@ -1,12 +1,12 @@ import { Action, Dispatch } from 'redux'; -import { Visit, VisitsInfo, VisitsLoadProgressChangedAction } from '../types'; +import { Visit, VisitsFallbackIntervalAction, VisitsInfo, VisitsLoadProgressChangedAction } from '../types'; import { buildActionCreator, buildReducer } from '../../utils/helpers/redux'; import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; import { GetState } from '../../container/types'; import { ShlinkVisitsParams } from '../../api/types'; import { ApiErrorAction } from '../../api/types/actions'; import { isBetween } from '../../utils/helpers/date'; -import { getVisitsWithLoader } from './common'; +import { getVisitsWithLoader, lastVisitLoaderForLoader } from './common'; import { CREATE_VISITS, CreateVisitsAction } from './visitCreation'; /* eslint-disable padding-line-between-statements */ @@ -16,6 +16,7 @@ export const GET_TAG_VISITS = 'shlink/tagVisits/GET_TAG_VISITS'; export const GET_TAG_VISITS_LARGE = 'shlink/tagVisits/GET_TAG_VISITS_LARGE'; export const GET_TAG_VISITS_CANCEL = 'shlink/tagVisits/GET_TAG_VISITS_CANCEL'; export const GET_TAG_VISITS_PROGRESS_CHANGED = 'shlink/tagVisits/GET_TAG_VISITS_PROGRESS_CHANGED'; +export const GET_TAG_VISITS_FALLBACK_TO_INTERVAL = 'shlink/tagVisits/GET_TAG_VISITS_FALLBACK_TO_INTERVAL'; /* eslint-enable padding-line-between-statements */ export interface TagVisits extends VisitsInfo { @@ -30,6 +31,7 @@ export interface TagVisitsAction extends Action { type TagsVisitsCombinedAction = TagVisitsAction & VisitsLoadProgressChangedAction +& VisitsFallbackIntervalAction & CreateVisitsAction & ApiErrorAction; @@ -46,10 +48,11 @@ const initialState: TagVisits = { export default buildReducer({ [GET_TAG_VISITS_START]: () => ({ ...initialState, loading: true }), [GET_TAG_VISITS_ERROR]: (_, { errorData }) => ({ ...initialState, error: true, errorData }), - [GET_TAG_VISITS]: (_, { visits, tag, query }) => ({ ...initialState, visits, tag, query }), + [GET_TAG_VISITS]: (state, { visits, tag, query }) => ({ ...state, visits, tag, query, loading: false, error: false }), [GET_TAG_VISITS_LARGE]: (state) => ({ ...state, loadingLarge: true }), [GET_TAG_VISITS_CANCEL]: (state) => ({ ...state, cancelLoad: true }), [GET_TAG_VISITS_PROGRESS_CHANGED]: (state, { progress }) => ({ ...state, progress }), + [GET_TAG_VISITS_FALLBACK_TO_INTERVAL]: (state, { fallbackInterval }) => ({ ...state, fallbackInterval }), [CREATE_VISITS]: (state, { createdVisits }) => { const { tag, visits, query = {} } = state; const { startDate, endDate } = query; @@ -64,12 +67,14 @@ export default buildReducer({ export const getTagVisits = (buildShlinkApiClient: ShlinkApiClientBuilder) => ( tag: string, query: ShlinkVisitsParams = {}, + doFallbackRange = false, ) => async (dispatch: Dispatch, getState: GetState) => { const { getTagVisits } = buildShlinkApiClient(getState); const visitsLoader = async (page: number, itemsPerPage: number) => getTagVisits( tag, { ...query, page, itemsPerPage }, ); + const lastVisitLoader = lastVisitLoaderForLoader(doFallbackRange, async (params) => getTagVisits(tag, params)); const shouldCancel = () => getState().tagVisits.cancelLoad; const extraFinishActionData: Partial = { tag, query }; const actionMap = { @@ -78,9 +83,10 @@ export const getTagVisits = (buildShlinkApiClient: ShlinkApiClientBuilder) => ( finish: GET_TAG_VISITS, error: GET_TAG_VISITS_ERROR, progress: GET_TAG_VISITS_PROGRESS_CHANGED, + fallbackToInterval: GET_TAG_VISITS_FALLBACK_TO_INTERVAL, }; - return getVisitsWithLoader(visitsLoader, extraFinishActionData, actionMap, dispatch, shouldCancel); + return getVisitsWithLoader(visitsLoader, lastVisitLoader, extraFinishActionData, actionMap, dispatch, shouldCancel); }; export const cancelGetTagVisits = buildActionCreator(GET_TAG_VISITS_CANCEL); diff --git a/test/visits/reducers/orphanVisits.test.ts b/test/visits/reducers/orphanVisits.test.ts index 86296131..8b1e31ba 100644 --- a/test/visits/reducers/orphanVisits.test.ts +++ b/test/visits/reducers/orphanVisits.test.ts @@ -1,5 +1,5 @@ import { Mock } from 'ts-mockery'; -import { addDays, subDays } from 'date-fns'; +import { addDays, formatISO, subDays } from 'date-fns'; import reducer, { getOrphanVisits, cancelGetOrphanVisits, @@ -9,6 +9,7 @@ import reducer, { GET_ORPHAN_VISITS_LARGE, GET_ORPHAN_VISITS_CANCEL, GET_ORPHAN_VISITS_PROGRESS_CHANGED, + GET_ORPHAN_VISITS_FALLBACK_TO_INTERVAL, } from '../../../src/visits/reducers/orphanVisits'; import { CREATE_VISITS } from '../../../src/visits/reducers/visitCreation'; import { rangeOf } from '../../../src/utils/utils'; @@ -17,6 +18,7 @@ import { ShlinkVisits } from '../../../src/api/types'; import ShlinkApiClient from '../../../src/api/services/ShlinkApiClient'; import { ShlinkState } from '../../../src/container/types'; import { formatIsoDate } from '../../../src/utils/helpers/date'; +import { DateInterval } from '../../../src/utils/dates/types'; describe('orphanVisitsReducer', () => { const now = new Date(); @@ -116,6 +118,13 @@ describe('orphanVisitsReducer', () => { expect(state).toEqual(expect.objectContaining({ progress: 85 })); }); + + it('returns fallbackInterval on GET_ORPHAN_VISITS_FALLBACK_TO_INTERVAL', () => { + const fallbackInterval: DateInterval = 'last30Days'; + const state = reducer(undefined, { type: GET_ORPHAN_VISITS_FALLBACK_TO_INTERVAL, fallbackInterval } as any); + + expect(state).toEqual(expect.objectContaining({ fallbackInterval })); + }); }); describe('getOrphanVisits', () => { @@ -163,6 +172,38 @@ describe('orphanVisitsReducer', () => { expect(dispatchMock).toHaveBeenNthCalledWith(2, { type: GET_ORPHAN_VISITS, visits, query: query ?? {} }); expect(ShlinkApiClient.getOrphanVisits).toHaveBeenCalledTimes(1); }); + + it.each([ + [ + [ Mock.of({ date: formatISO(subDays(new Date(), 5)) }) ], + { type: GET_ORPHAN_VISITS_FALLBACK_TO_INTERVAL, fallbackInterval: 'last7Days' }, + ], + [ + [ Mock.of({ date: formatISO(subDays(new Date(), 200)) }) ], + { type: GET_ORPHAN_VISITS_FALLBACK_TO_INTERVAL, fallbackInterval: 'last365Days' }, + ], + [[], expect.objectContaining({ type: GET_ORPHAN_VISITS }) ], + ])('dispatches fallback interval when the list of visits is empty', async (lastVisits, expectedSecondDispatch) => { + const buildVisitsResult = (data: Visit[] = []): ShlinkVisits => ({ + data, + pagination: { + currentPage: 1, + pagesCount: 1, + totalItems: 1, + }, + }); + const getShlinkOrphanVisits = jest.fn() + .mockResolvedValueOnce(buildVisitsResult()) + .mockResolvedValueOnce(buildVisitsResult(lastVisits)); + const ShlinkApiClient = Mock.of({ getOrphanVisits: getShlinkOrphanVisits }); + + await getOrphanVisits(() => ShlinkApiClient)({}, undefined, true)(dispatchMock, getState); + + expect(dispatchMock).toHaveBeenCalledTimes(2); + expect(dispatchMock).toHaveBeenNthCalledWith(1, { type: GET_ORPHAN_VISITS_START }); + expect(dispatchMock).toHaveBeenNthCalledWith(2, expectedSecondDispatch); + expect(getShlinkOrphanVisits).toHaveBeenCalledTimes(2); + }); }); describe('cancelGetOrphanVisits', () => { diff --git a/test/visits/reducers/shortUrlVisits.test.ts b/test/visits/reducers/shortUrlVisits.test.ts index 8d45f7da..0c6d3433 100644 --- a/test/visits/reducers/shortUrlVisits.test.ts +++ b/test/visits/reducers/shortUrlVisits.test.ts @@ -1,5 +1,5 @@ import { Mock } from 'ts-mockery'; -import { addDays, subDays } from 'date-fns'; +import { addDays, formatISO, subDays } from 'date-fns'; import reducer, { getShortUrlVisits, cancelGetShortUrlVisits, @@ -9,6 +9,7 @@ import reducer, { GET_SHORT_URL_VISITS_LARGE, GET_SHORT_URL_VISITS_CANCEL, GET_SHORT_URL_VISITS_PROGRESS_CHANGED, + GET_SHORT_URL_VISITS_FALLBACK_TO_INTERVAL, ShortUrlVisits, } from '../../../src/visits/reducers/shortUrlVisits'; import { CREATE_VISITS } from '../../../src/visits/reducers/visitCreation'; @@ -18,6 +19,7 @@ import { ShlinkVisits } from '../../../src/api/types'; import ShlinkApiClient from '../../../src/api/services/ShlinkApiClient'; import { ShlinkState } from '../../../src/container/types'; import { formatIsoDate } from '../../../src/utils/helpers/date'; +import { DateInterval } from '../../../src/utils/dates/types'; describe('shortUrlVisitsReducer', () => { const now = new Date(); @@ -137,6 +139,13 @@ describe('shortUrlVisitsReducer', () => { expect(state).toEqual(expect.objectContaining({ progress: 85 })); }); + + it('returns fallbackInterval on GET_SHORT_URL_VISITS_FALLBACK_TO_INTERVAL', () => { + const fallbackInterval: DateInterval = 'last30Days'; + const state = reducer(undefined, { type: GET_SHORT_URL_VISITS_FALLBACK_TO_INTERVAL, fallbackInterval } as any); + + expect(state).toEqual(expect.objectContaining({ fallbackInterval })); + }); }); describe('getShortUrlVisits', () => { @@ -209,6 +218,38 @@ describe('shortUrlVisitsReducer', () => { visits: [ ...visitsMocks, ...visitsMocks, ...visitsMocks ], })); }); + + it.each([ + [ + [ Mock.of({ date: formatISO(subDays(new Date(), 5)) }) ], + { type: GET_SHORT_URL_VISITS_FALLBACK_TO_INTERVAL, fallbackInterval: 'last7Days' }, + ], + [ + [ Mock.of({ date: formatISO(subDays(new Date(), 200)) }) ], + { type: GET_SHORT_URL_VISITS_FALLBACK_TO_INTERVAL, fallbackInterval: 'last365Days' }, + ], + [[], expect.objectContaining({ type: GET_SHORT_URL_VISITS }) ], + ])('dispatches fallback interval when the list of visits is empty', async (lastVisits, expectedSecondDispatch) => { + const buildVisitsResult = (data: Visit[] = []): ShlinkVisits => ({ + data, + pagination: { + currentPage: 1, + pagesCount: 1, + totalItems: 1, + }, + }); + const getShlinkShortUrlVisits = jest.fn() + .mockResolvedValueOnce(buildVisitsResult()) + .mockResolvedValueOnce(buildVisitsResult(lastVisits)); + const ShlinkApiClient = Mock.of({ getShortUrlVisits: getShlinkShortUrlVisits }); + + await getShortUrlVisits(() => ShlinkApiClient)('abc123', {}, true)(dispatchMock, getState); + + expect(dispatchMock).toHaveBeenCalledTimes(2); + expect(dispatchMock).toHaveBeenNthCalledWith(1, { type: GET_SHORT_URL_VISITS_START }); + expect(dispatchMock).toHaveBeenNthCalledWith(2, expectedSecondDispatch); + expect(getShlinkShortUrlVisits).toHaveBeenCalledTimes(2); + }); }); describe('cancelGetShortUrlVisits', () => { diff --git a/test/visits/reducers/tagVisits.test.ts b/test/visits/reducers/tagVisits.test.ts index f026e5bb..d37f5043 100644 --- a/test/visits/reducers/tagVisits.test.ts +++ b/test/visits/reducers/tagVisits.test.ts @@ -1,5 +1,5 @@ import { Mock } from 'ts-mockery'; -import { addDays, subDays } from 'date-fns'; +import { addDays, formatISO, subDays } from 'date-fns'; import reducer, { getTagVisits, cancelGetTagVisits, @@ -9,6 +9,7 @@ import reducer, { GET_TAG_VISITS_LARGE, GET_TAG_VISITS_CANCEL, GET_TAG_VISITS_PROGRESS_CHANGED, + GET_TAG_VISITS_FALLBACK_TO_INTERVAL, TagVisits, } from '../../../src/visits/reducers/tagVisits'; import { CREATE_VISITS } from '../../../src/visits/reducers/visitCreation'; @@ -18,6 +19,7 @@ import { ShlinkVisits } from '../../../src/api/types'; import ShlinkApiClient from '../../../src/api/services/ShlinkApiClient'; import { ShlinkState } from '../../../src/container/types'; import { formatIsoDate } from '../../../src/utils/helpers/date'; +import { DateInterval } from '../../../src/utils/dates/types'; describe('tagVisitsReducer', () => { const now = new Date(); @@ -137,6 +139,13 @@ describe('tagVisitsReducer', () => { expect(state).toEqual(expect.objectContaining({ progress: 85 })); }); + + it('returns fallbackInterval on GET_TAG_VISITS_FALLBACK_TO_INTERVAL', () => { + const fallbackInterval: DateInterval = 'last30Days'; + const state = reducer(undefined, { type: GET_TAG_VISITS_FALLBACK_TO_INTERVAL, fallbackInterval } as any); + + expect(state).toEqual(expect.objectContaining({ fallbackInterval })); + }); }); describe('getTagVisits', () => { @@ -149,8 +158,9 @@ describe('tagVisitsReducer', () => { const getState = () => Mock.of({ tagVisits: { cancelLoad: false }, }); + const tag = 'foo'; - beforeEach(jest.resetAllMocks); + beforeEach(jest.clearAllMocks); it('dispatches start and error when promise is rejected', async () => { const ShlinkApiClient = buildApiClientMock(Promise.reject({})); @@ -168,7 +178,6 @@ describe('tagVisitsReducer', () => { [{}], ])('dispatches start and success when promise is resolved', async (query) => { const visits = visitsMocks; - const tag = 'foo'; const ShlinkApiClient = buildApiClientMock(Promise.resolve({ data: visitsMocks, pagination: { @@ -185,6 +194,38 @@ describe('tagVisitsReducer', () => { expect(dispatchMock).toHaveBeenNthCalledWith(2, { type: GET_TAG_VISITS, visits, tag, query: query ?? {} }); expect(ShlinkApiClient.getTagVisits).toHaveBeenCalledTimes(1); }); + + it.each([ + [ + [ Mock.of({ date: formatISO(subDays(new Date(), 20)) }) ], + { type: GET_TAG_VISITS_FALLBACK_TO_INTERVAL, fallbackInterval: 'last30Days' }, + ], + [ + [ Mock.of({ date: formatISO(subDays(new Date(), 100)) }) ], + { type: GET_TAG_VISITS_FALLBACK_TO_INTERVAL, fallbackInterval: 'last180Days' }, + ], + [[], expect.objectContaining({ type: GET_TAG_VISITS }) ], + ])('dispatches fallback interval when the list of visits is empty', async (lastVisits, expectedSecondDispatch) => { + const buildVisitsResult = (data: Visit[] = []): ShlinkVisits => ({ + data, + pagination: { + currentPage: 1, + pagesCount: 1, + totalItems: 1, + }, + }); + const getShlinkTagVisits = jest.fn() + .mockResolvedValueOnce(buildVisitsResult()) + .mockResolvedValueOnce(buildVisitsResult(lastVisits)); + const ShlinkApiClient = Mock.of({ getTagVisits: getShlinkTagVisits }); + + await getTagVisits(() => ShlinkApiClient)(tag, {}, true)(dispatchMock, getState); + + expect(dispatchMock).toHaveBeenCalledTimes(2); + expect(dispatchMock).toHaveBeenNthCalledWith(1, { type: GET_TAG_VISITS_START }); + expect(dispatchMock).toHaveBeenNthCalledWith(2, expectedSecondDispatch); + expect(getShlinkTagVisits).toHaveBeenCalledTimes(2); + }); }); describe('cancelGetTagVisits', () => {