mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-08-02 09:01:52 +00:00
Move shlink-web-component tests to their own folder
This commit is contained in:
218
shlink-web-component/test/visits/reducers/domainVisits.test.ts
Normal file
218
shlink-web-component/test/visits/reducers/domainVisits.test.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { addDays, formatISO, subDays } from 'date-fns';
|
||||
import type { ShlinkApiClient } from '../../../../src/api/services/ShlinkApiClient';
|
||||
import type { ShlinkState } from '../../../../src/container/types';
|
||||
import { rangeOf } from '../../../../src/utils/utils';
|
||||
import type { ShlinkVisits } from '../../../src/api/types';
|
||||
import type { ShortUrl } from '../../../src/short-urls/data';
|
||||
import { formatIsoDate } from '../../../src/utils/dates/helpers/date';
|
||||
import type { DateInterval } from '../../../src/utils/dates/helpers/dateIntervals';
|
||||
import type {
|
||||
DomainVisits, LoadDomainVisits,
|
||||
} from '../../../src/visits/reducers/domainVisits';
|
||||
import {
|
||||
DEFAULT_DOMAIN,
|
||||
domainVisitsReducerCreator,
|
||||
getDomainVisits as getDomainVisitsCreator,
|
||||
} from '../../../src/visits/reducers/domainVisits';
|
||||
import { createNewVisits } from '../../../src/visits/reducers/visitCreation';
|
||||
import type { Visit } from '../../../src/visits/types';
|
||||
|
||||
describe('domainVisitsReducer', () => {
|
||||
const now = new Date();
|
||||
const visitsMocks = rangeOf(2, () => fromPartial<Visit>({}));
|
||||
const getDomainVisitsCall = vi.fn();
|
||||
const buildApiClientMock = () => fromPartial<ShlinkApiClient>({ getDomainVisits: getDomainVisitsCall });
|
||||
const getDomainVisits = getDomainVisitsCreator(buildApiClientMock);
|
||||
const { reducer, cancelGetVisits: cancelGetDomainVisits } = domainVisitsReducerCreator(getDomainVisits);
|
||||
|
||||
describe('reducer', () => {
|
||||
const buildState = (data: Partial<DomainVisits>) => fromPartial<DomainVisits>(data);
|
||||
|
||||
it('returns loading on GET_DOMAIN_VISITS_START', () => {
|
||||
const { loading } = reducer(
|
||||
buildState({ loading: false }),
|
||||
getDomainVisits.pending('', fromPartial<LoadDomainVisits>({})),
|
||||
);
|
||||
expect(loading).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns loadingLarge on GET_DOMAIN_VISITS_LARGE', () => {
|
||||
const { loadingLarge } = reducer(buildState({ loadingLarge: false }), getDomainVisits.large());
|
||||
expect(loadingLarge).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns cancelLoad on GET_DOMAIN_VISITS_CANCEL', () => {
|
||||
const { cancelLoad } = reducer(buildState({ cancelLoad: false }), cancelGetDomainVisits());
|
||||
expect(cancelLoad).toEqual(true);
|
||||
});
|
||||
|
||||
it('stops loading and returns error on GET_DOMAIN_VISITS_ERROR', () => {
|
||||
const state = reducer(
|
||||
buildState({ loading: true, error: false }),
|
||||
getDomainVisits.rejected(null, '', fromPartial({})),
|
||||
);
|
||||
const { loading, error } = state;
|
||||
|
||||
expect(loading).toEqual(false);
|
||||
expect(error).toEqual(true);
|
||||
});
|
||||
|
||||
it('return visits on GET_DOMAIN_VISITS', () => {
|
||||
const actionVisits: Visit[] = [fromPartial({}), fromPartial({})];
|
||||
const { loading, error, visits } = reducer(
|
||||
buildState({ loading: true, error: false }),
|
||||
getDomainVisits.fulfilled({ visits: actionVisits }, '', fromPartial({})),
|
||||
);
|
||||
|
||||
expect(loading).toEqual(false);
|
||||
expect(error).toEqual(false);
|
||||
expect(visits).toEqual(actionVisits);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ domain: 'foo.com' }, 'foo.com', visitsMocks.length + 1],
|
||||
[{ domain: 'bar.com' }, 'foo.com', visitsMocks.length],
|
||||
[fromPartial<DomainVisits>({ domain: 'foo.com' }), 'foo.com', visitsMocks.length + 1],
|
||||
[fromPartial<DomainVisits>({ domain: DEFAULT_DOMAIN }), null, visitsMocks.length + 1],
|
||||
[
|
||||
fromPartial<DomainVisits>({
|
||||
domain: 'foo.com',
|
||||
query: { endDate: formatIsoDate(subDays(now, 1)) ?? undefined },
|
||||
}),
|
||||
'foo.com',
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
fromPartial<DomainVisits>({
|
||||
domain: 'foo.com',
|
||||
query: { startDate: formatIsoDate(addDays(now, 1)) ?? undefined },
|
||||
}),
|
||||
'foo.com',
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
fromPartial<DomainVisits>({
|
||||
domain: 'foo.com',
|
||||
query: {
|
||||
startDate: formatIsoDate(subDays(now, 5)) ?? undefined,
|
||||
endDate: formatIsoDate(subDays(now, 2)) ?? undefined,
|
||||
},
|
||||
}),
|
||||
'foo.com',
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
fromPartial<DomainVisits>({
|
||||
domain: 'foo.com',
|
||||
query: {
|
||||
startDate: formatIsoDate(subDays(now, 5)) ?? undefined,
|
||||
endDate: formatIsoDate(addDays(now, 3)) ?? undefined,
|
||||
},
|
||||
}),
|
||||
'foo.com',
|
||||
visitsMocks.length + 1,
|
||||
],
|
||||
[
|
||||
fromPartial<DomainVisits>({
|
||||
domain: 'bar.com',
|
||||
query: {
|
||||
startDate: formatIsoDate(subDays(now, 5)) ?? undefined,
|
||||
endDate: formatIsoDate(addDays(now, 3)) ?? undefined,
|
||||
},
|
||||
}),
|
||||
'foo.com',
|
||||
visitsMocks.length,
|
||||
],
|
||||
])('prepends new visits on CREATE_VISIT', (state, shortUrlDomain, expectedVisits) => {
|
||||
const shortUrl = fromPartial<ShortUrl>({ domain: shortUrlDomain });
|
||||
const { visits } = reducer(buildState({ ...state, visits: visitsMocks }), createNewVisits([
|
||||
fromPartial({ shortUrl, visit: { date: formatIsoDate(now) ?? undefined } }),
|
||||
]));
|
||||
|
||||
expect(visits).toHaveLength(expectedVisits);
|
||||
});
|
||||
|
||||
it('returns new progress on GET_DOMAIN_VISITS_PROGRESS_CHANGED', () => {
|
||||
const { progress } = reducer(undefined, getDomainVisits.progressChanged(85));
|
||||
expect(progress).toEqual(85);
|
||||
});
|
||||
|
||||
it('returns fallbackInterval on GET_DOMAIN_VISITS_FALLBACK_TO_INTERVAL', () => {
|
||||
const fallbackInterval: DateInterval = 'last30Days';
|
||||
const state = reducer(
|
||||
undefined,
|
||||
getDomainVisits.fallbackToInterval(fallbackInterval),
|
||||
);
|
||||
|
||||
expect(state).toEqual(expect.objectContaining({ fallbackInterval }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDomainVisits', () => {
|
||||
const dispatchMock = vi.fn();
|
||||
const getState = () => fromPartial<ShlinkState>({
|
||||
domainVisits: { cancelLoad: false },
|
||||
});
|
||||
const domain = 'foo.com';
|
||||
|
||||
it.each([
|
||||
[undefined],
|
||||
[{}],
|
||||
])('dispatches start and success when promise is resolved', async (query) => {
|
||||
const visits = visitsMocks;
|
||||
getDomainVisitsCall.mockResolvedValue({
|
||||
data: visitsMocks,
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
pagesCount: 1,
|
||||
totalItems: 1,
|
||||
},
|
||||
});
|
||||
|
||||
await getDomainVisits({ domain, query })(dispatchMock, getState, {});
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(2);
|
||||
expect(dispatchMock).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
payload: { visits, domain, query: query ?? {} },
|
||||
}));
|
||||
expect(getDomainVisitsCall).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
[fromPartial<Visit>({ date: formatISO(subDays(now, 20)) })],
|
||||
getDomainVisits.fallbackToInterval('last30Days'),
|
||||
3,
|
||||
],
|
||||
[
|
||||
[fromPartial<Visit>({ date: formatISO(subDays(now, 100)) })],
|
||||
getDomainVisits.fallbackToInterval('last180Days'),
|
||||
3,
|
||||
],
|
||||
[[], expect.objectContaining({ type: getDomainVisits.fulfilled.toString() }), 2],
|
||||
])('dispatches fallback interval when the list of visits is empty', async (
|
||||
lastVisits,
|
||||
expectedSecondDispatch,
|
||||
expectedDispatchCalls,
|
||||
) => {
|
||||
const buildVisitsResult = (data: Visit[] = []): ShlinkVisits => ({
|
||||
data,
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
pagesCount: 1,
|
||||
totalItems: 1,
|
||||
},
|
||||
});
|
||||
getDomainVisitsCall
|
||||
.mockResolvedValueOnce(buildVisitsResult())
|
||||
.mockResolvedValueOnce(buildVisitsResult(lastVisits));
|
||||
|
||||
await getDomainVisits({ domain, doIntervalFallback: true })(dispatchMock, getState, {});
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(expectedDispatchCalls);
|
||||
expect(dispatchMock).toHaveBeenNthCalledWith(2, expectedSecondDispatch);
|
||||
expect(getDomainVisitsCall).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,184 @@
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { addDays, formatISO, subDays } from 'date-fns';
|
||||
import type { ShlinkApiClient } from '../../../../src/api/services/ShlinkApiClient';
|
||||
import type { ShlinkState } from '../../../../src/container/types';
|
||||
import { rangeOf } from '../../../../src/utils/utils';
|
||||
import type { ShlinkVisits } from '../../../src/api/types';
|
||||
import { formatIsoDate } from '../../../src/utils/dates/helpers/date';
|
||||
import type { DateInterval } from '../../../src/utils/dates/helpers/dateIntervals';
|
||||
import {
|
||||
getNonOrphanVisits as getNonOrphanVisitsCreator,
|
||||
nonOrphanVisitsReducerCreator,
|
||||
} from '../../../src/visits/reducers/nonOrphanVisits';
|
||||
import type { VisitsInfo } from '../../../src/visits/reducers/types';
|
||||
import { createNewVisits } from '../../../src/visits/reducers/visitCreation';
|
||||
import type { Visit } from '../../../src/visits/types';
|
||||
|
||||
describe('nonOrphanVisitsReducer', () => {
|
||||
const now = new Date();
|
||||
const visitsMocks = rangeOf(2, () => fromPartial<Visit>({}));
|
||||
const getNonOrphanVisitsCall = vi.fn();
|
||||
const buildShlinkApiClient = () => fromPartial<ShlinkApiClient>({ getNonOrphanVisits: getNonOrphanVisitsCall });
|
||||
const getNonOrphanVisits = getNonOrphanVisitsCreator(buildShlinkApiClient);
|
||||
const { reducer, cancelGetVisits: cancelGetNonOrphanVisits } = nonOrphanVisitsReducerCreator(getNonOrphanVisits);
|
||||
|
||||
describe('reducer', () => {
|
||||
const buildState = (data: Partial<VisitsInfo>) => fromPartial<VisitsInfo>(data);
|
||||
|
||||
it('returns loading on GET_NON_ORPHAN_VISITS_START', () => {
|
||||
const { loading } = reducer(buildState({ loading: false }), getNonOrphanVisits.pending('', {}));
|
||||
expect(loading).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns loadingLarge on GET_NON_ORPHAN_VISITS_LARGE', () => {
|
||||
const { loadingLarge } = reducer(buildState({ loadingLarge: false }), getNonOrphanVisits.large());
|
||||
expect(loadingLarge).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns cancelLoad on GET_NON_ORPHAN_VISITS_CANCEL', () => {
|
||||
const { cancelLoad } = reducer(buildState({ cancelLoad: false }), cancelGetNonOrphanVisits());
|
||||
expect(cancelLoad).toEqual(true);
|
||||
});
|
||||
|
||||
it('stops loading and returns error on GET_NON_ORPHAN_VISITS_ERROR', () => {
|
||||
const { loading, error } = reducer(
|
||||
buildState({ loading: true, error: false }),
|
||||
getNonOrphanVisits.rejected(null, '', {}),
|
||||
);
|
||||
|
||||
expect(loading).toEqual(false);
|
||||
expect(error).toEqual(true);
|
||||
});
|
||||
|
||||
it('return visits on GET_NON_ORPHAN_VISITS', () => {
|
||||
const actionVisits: Visit[] = [fromPartial({}), fromPartial({})];
|
||||
const { loading, error, visits } = reducer(
|
||||
buildState({ loading: true, error: false }),
|
||||
getNonOrphanVisits.fulfilled({ visits: actionVisits }, '', {}),
|
||||
);
|
||||
|
||||
expect(loading).toEqual(false);
|
||||
expect(error).toEqual(false);
|
||||
expect(visits).toEqual(actionVisits);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{}, visitsMocks.length + 2],
|
||||
[
|
||||
fromPartial<VisitsInfo>({
|
||||
query: { endDate: formatIsoDate(subDays(now, 1)) ?? undefined },
|
||||
}),
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
fromPartial<VisitsInfo>({
|
||||
query: { startDate: formatIsoDate(addDays(now, 1)) ?? undefined },
|
||||
}),
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
fromPartial<VisitsInfo>({
|
||||
query: {
|
||||
startDate: formatIsoDate(subDays(now, 5)) ?? undefined,
|
||||
endDate: formatIsoDate(subDays(now, 2)) ?? undefined,
|
||||
},
|
||||
}),
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
fromPartial<VisitsInfo>({
|
||||
query: {
|
||||
startDate: formatIsoDate(subDays(now, 5)) ?? undefined,
|
||||
endDate: formatIsoDate(addDays(now, 3)) ?? undefined,
|
||||
},
|
||||
}),
|
||||
visitsMocks.length + 2,
|
||||
],
|
||||
])('prepends new visits on CREATE_VISIT', (state, expectedVisits) => {
|
||||
const prevState = buildState({ ...state, visits: visitsMocks });
|
||||
const visit = fromPartial<Visit>({ date: formatIsoDate(now) ?? undefined });
|
||||
|
||||
const { visits } = reducer(prevState, createNewVisits([{ visit }, { visit }]));
|
||||
|
||||
expect(visits).toHaveLength(expectedVisits);
|
||||
});
|
||||
|
||||
it('returns new progress on GET_NON_ORPHAN_VISITS_PROGRESS_CHANGED', () => {
|
||||
const { progress } = reducer(undefined, getNonOrphanVisits.progressChanged(85));
|
||||
expect(progress).toEqual(85);
|
||||
});
|
||||
|
||||
it('returns fallbackInterval on GET_NON_ORPHAN_VISITS_FALLBACK_TO_INTERVAL', () => {
|
||||
const fallbackInterval: DateInterval = 'last30Days';
|
||||
const state = reducer(undefined, getNonOrphanVisits.fallbackToInterval(fallbackInterval));
|
||||
|
||||
expect(state).toEqual(expect.objectContaining({ fallbackInterval }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNonOrphanVisits', () => {
|
||||
const dispatchMock = vi.fn();
|
||||
const getState = () => fromPartial<ShlinkState>({
|
||||
orphanVisits: { cancelLoad: false },
|
||||
});
|
||||
|
||||
it.each([
|
||||
[undefined],
|
||||
[{}],
|
||||
])('dispatches start and success when promise is resolved', async (query) => {
|
||||
const visits = visitsMocks.map((visit) => ({ ...visit, visitedUrl: '' }));
|
||||
getNonOrphanVisitsCall.mockResolvedValue({
|
||||
data: visits,
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
pagesCount: 1,
|
||||
totalItems: 1,
|
||||
},
|
||||
});
|
||||
|
||||
await getNonOrphanVisits({ query })(dispatchMock, getState, {});
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(2);
|
||||
expect(dispatchMock).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
payload: { visits, query: query ?? {} },
|
||||
}));
|
||||
expect(getNonOrphanVisitsCall).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
[fromPartial<Visit>({ date: formatISO(subDays(now, 5)) })],
|
||||
getNonOrphanVisits.fallbackToInterval('last7Days'),
|
||||
3,
|
||||
],
|
||||
[
|
||||
[fromPartial<Visit>({ date: formatISO(subDays(now, 200)) })],
|
||||
getNonOrphanVisits.fallbackToInterval('last365Days'),
|
||||
3,
|
||||
],
|
||||
[[], expect.objectContaining({ type: getNonOrphanVisits.fulfilled.toString() }), 2],
|
||||
])('dispatches fallback interval when the list of visits is empty', async (
|
||||
lastVisits,
|
||||
expectedSecondDispatch,
|
||||
expectedAmountOfDispatches,
|
||||
) => {
|
||||
const buildVisitsResult = (data: Visit[] = []): ShlinkVisits => ({
|
||||
data,
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
pagesCount: 1,
|
||||
totalItems: 1,
|
||||
},
|
||||
});
|
||||
getNonOrphanVisitsCall
|
||||
.mockResolvedValueOnce(buildVisitsResult())
|
||||
.mockResolvedValueOnce(buildVisitsResult(lastVisits));
|
||||
|
||||
await getNonOrphanVisits({ doIntervalFallback: true })(dispatchMock, getState, {});
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(expectedAmountOfDispatches);
|
||||
expect(dispatchMock).toHaveBeenNthCalledWith(2, expectedSecondDispatch);
|
||||
expect(getNonOrphanVisitsCall).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
184
shlink-web-component/test/visits/reducers/orphanVisits.test.ts
Normal file
184
shlink-web-component/test/visits/reducers/orphanVisits.test.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { addDays, formatISO, subDays } from 'date-fns';
|
||||
import type { ShlinkApiClient } from '../../../../src/api/services/ShlinkApiClient';
|
||||
import type { ShlinkState } from '../../../../src/container/types';
|
||||
import { rangeOf } from '../../../../src/utils/utils';
|
||||
import type { ShlinkVisits } from '../../../src/api/types';
|
||||
import { formatIsoDate } from '../../../src/utils/dates/helpers/date';
|
||||
import type { DateInterval } from '../../../src/utils/dates/helpers/dateIntervals';
|
||||
import {
|
||||
getOrphanVisits as getOrphanVisitsCreator,
|
||||
orphanVisitsReducerCreator,
|
||||
} from '../../../src/visits/reducers/orphanVisits';
|
||||
import type { VisitsInfo } from '../../../src/visits/reducers/types';
|
||||
import { createNewVisits } from '../../../src/visits/reducers/visitCreation';
|
||||
import type { Visit } from '../../../src/visits/types';
|
||||
|
||||
describe('orphanVisitsReducer', () => {
|
||||
const now = new Date();
|
||||
const visitsMocks = rangeOf(2, () => fromPartial<Visit>({}));
|
||||
const getOrphanVisitsCall = vi.fn();
|
||||
const buildShlinkApiClientMock = () => fromPartial<ShlinkApiClient>({ getOrphanVisits: getOrphanVisitsCall });
|
||||
const getOrphanVisits = getOrphanVisitsCreator(buildShlinkApiClientMock);
|
||||
const { reducer, cancelGetVisits: cancelGetOrphanVisits } = orphanVisitsReducerCreator(getOrphanVisits);
|
||||
|
||||
describe('reducer', () => {
|
||||
const buildState = (data: Partial<VisitsInfo>) => fromPartial<VisitsInfo>(data);
|
||||
|
||||
it('returns loading on GET_ORPHAN_VISITS_START', () => {
|
||||
const { loading } = reducer(buildState({ loading: false }), getOrphanVisits.pending('', {}));
|
||||
expect(loading).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns loadingLarge on GET_ORPHAN_VISITS_LARGE', () => {
|
||||
const { loadingLarge } = reducer(buildState({ loadingLarge: false }), getOrphanVisits.large());
|
||||
expect(loadingLarge).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns cancelLoad on GET_ORPHAN_VISITS_CANCEL', () => {
|
||||
const { cancelLoad } = reducer(buildState({ cancelLoad: false }), cancelGetOrphanVisits());
|
||||
expect(cancelLoad).toEqual(true);
|
||||
});
|
||||
|
||||
it('stops loading and returns error on GET_ORPHAN_VISITS_ERROR', () => {
|
||||
const { loading, error } = reducer(
|
||||
buildState({ loading: true, error: false }),
|
||||
getOrphanVisits.rejected(null, '', {}),
|
||||
);
|
||||
|
||||
expect(loading).toEqual(false);
|
||||
expect(error).toEqual(true);
|
||||
});
|
||||
|
||||
it('return visits on GET_ORPHAN_VISITS', () => {
|
||||
const actionVisits: Visit[] = [fromPartial({}), fromPartial({})];
|
||||
const { loading, error, visits } = reducer(
|
||||
buildState({ loading: true, error: false }),
|
||||
getOrphanVisits.fulfilled({ visits: actionVisits }, '', {}),
|
||||
);
|
||||
|
||||
expect(loading).toEqual(false);
|
||||
expect(error).toEqual(false);
|
||||
expect(visits).toEqual(actionVisits);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{}, visitsMocks.length + 2],
|
||||
[
|
||||
fromPartial<VisitsInfo>({
|
||||
query: { endDate: formatIsoDate(subDays(now, 1)) ?? undefined },
|
||||
}),
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
fromPartial<VisitsInfo>({
|
||||
query: { startDate: formatIsoDate(addDays(now, 1)) ?? undefined },
|
||||
}),
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
fromPartial<VisitsInfo>({
|
||||
query: {
|
||||
startDate: formatIsoDate(subDays(now, 5)) ?? undefined,
|
||||
endDate: formatIsoDate(subDays(now, 2)) ?? undefined,
|
||||
},
|
||||
}),
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
fromPartial<VisitsInfo>({
|
||||
query: {
|
||||
startDate: formatIsoDate(subDays(now, 5)) ?? undefined,
|
||||
endDate: formatIsoDate(addDays(now, 3)) ?? undefined,
|
||||
},
|
||||
}),
|
||||
visitsMocks.length + 2,
|
||||
],
|
||||
])('prepends new visits on CREATE_VISIT', (state, expectedVisits) => {
|
||||
const prevState = buildState({ ...state, visits: visitsMocks });
|
||||
const visit = fromPartial<Visit>({ date: formatIsoDate(now) ?? undefined });
|
||||
|
||||
const { visits } = reducer(prevState, createNewVisits([{ visit }, { visit }]));
|
||||
|
||||
expect(visits).toHaveLength(expectedVisits);
|
||||
});
|
||||
|
||||
it('returns new progress on GET_ORPHAN_VISITS_PROGRESS_CHANGED', () => {
|
||||
const { progress } = reducer(undefined, getOrphanVisits.progressChanged(85));
|
||||
expect(progress).toEqual(85);
|
||||
});
|
||||
|
||||
it('returns fallbackInterval on GET_ORPHAN_VISITS_FALLBACK_TO_INTERVAL', () => {
|
||||
const fallbackInterval: DateInterval = 'last30Days';
|
||||
const state = reducer(undefined, getOrphanVisits.fallbackToInterval(fallbackInterval));
|
||||
|
||||
expect(state).toEqual(expect.objectContaining({ fallbackInterval }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOrphanVisits', () => {
|
||||
const dispatchMock = vi.fn();
|
||||
const getState = () => fromPartial<ShlinkState>({
|
||||
orphanVisits: { cancelLoad: false },
|
||||
});
|
||||
|
||||
it.each([
|
||||
[undefined],
|
||||
[{}],
|
||||
])('dispatches start and success when promise is resolved', async (query) => {
|
||||
const visits = visitsMocks.map((visit) => ({ ...visit, visitedUrl: '' }));
|
||||
getOrphanVisitsCall.mockResolvedValue({
|
||||
data: visits,
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
pagesCount: 1,
|
||||
totalItems: 1,
|
||||
},
|
||||
});
|
||||
|
||||
await getOrphanVisits({ query })(dispatchMock, getState, {});
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(2);
|
||||
expect(dispatchMock).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
payload: { visits, query: query ?? {} },
|
||||
}));
|
||||
expect(getOrphanVisitsCall).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
[fromPartial<Visit>({ date: formatISO(subDays(now, 5)) })],
|
||||
getOrphanVisits.fallbackToInterval('last7Days'),
|
||||
3,
|
||||
],
|
||||
[
|
||||
[fromPartial<Visit>({ date: formatISO(subDays(now, 200)) })],
|
||||
getOrphanVisits.fallbackToInterval('last365Days'),
|
||||
3,
|
||||
],
|
||||
[[], expect.objectContaining({ type: getOrphanVisits.fulfilled.toString() }), 2],
|
||||
])('dispatches fallback interval when the list of visits is empty', async (
|
||||
lastVisits,
|
||||
expectedSecondDispatch,
|
||||
expectedDispatchCalls,
|
||||
) => {
|
||||
const buildVisitsResult = (data: Visit[] = []): ShlinkVisits => ({
|
||||
data,
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
pagesCount: 1,
|
||||
totalItems: 1,
|
||||
},
|
||||
});
|
||||
getOrphanVisitsCall
|
||||
.mockResolvedValueOnce(buildVisitsResult())
|
||||
.mockResolvedValueOnce(buildVisitsResult(lastVisits));
|
||||
|
||||
await getOrphanVisits({ doIntervalFallback: true })(dispatchMock, getState, {});
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(expectedDispatchCalls);
|
||||
expect(dispatchMock).toHaveBeenNthCalledWith(2, expectedSecondDispatch);
|
||||
expect(getOrphanVisitsCall).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
232
shlink-web-component/test/visits/reducers/shortUrlVisits.test.ts
Normal file
232
shlink-web-component/test/visits/reducers/shortUrlVisits.test.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { addDays, formatISO, subDays } from 'date-fns';
|
||||
import type { ShlinkApiClient } from '../../../../src/api/services/ShlinkApiClient';
|
||||
import type { ShlinkState } from '../../../../src/container/types';
|
||||
import { rangeOf } from '../../../../src/utils/utils';
|
||||
import type { ShlinkVisits } from '../../../src/api/types';
|
||||
import { formatIsoDate } from '../../../src/utils/dates/helpers/date';
|
||||
import type { DateInterval } from '../../../src/utils/dates/helpers/dateIntervals';
|
||||
import type {
|
||||
ShortUrlVisits } from '../../../src/visits/reducers/shortUrlVisits';
|
||||
import {
|
||||
getShortUrlVisits as getShortUrlVisitsCreator,
|
||||
shortUrlVisitsReducerCreator,
|
||||
} from '../../../src/visits/reducers/shortUrlVisits';
|
||||
import { createNewVisits } from '../../../src/visits/reducers/visitCreation';
|
||||
import type { Visit } from '../../../src/visits/types';
|
||||
|
||||
describe('shortUrlVisitsReducer', () => {
|
||||
const now = new Date();
|
||||
const visitsMocks = rangeOf(2, () => fromPartial<Visit>({}));
|
||||
const getShortUrlVisitsCall = vi.fn();
|
||||
const buildApiClientMock = () => fromPartial<ShlinkApiClient>({ getShortUrlVisits: getShortUrlVisitsCall });
|
||||
const getShortUrlVisits = getShortUrlVisitsCreator(buildApiClientMock);
|
||||
const { reducer, cancelGetVisits: cancelGetShortUrlVisits } = shortUrlVisitsReducerCreator(getShortUrlVisits);
|
||||
|
||||
describe('reducer', () => {
|
||||
const buildState = (data: Partial<ShortUrlVisits>) => fromPartial<ShortUrlVisits>(data);
|
||||
|
||||
it('returns loading on GET_SHORT_URL_VISITS_START', () => {
|
||||
const { loading } = reducer(buildState({ loading: false }), getShortUrlVisits.pending('', { shortCode: '' }));
|
||||
expect(loading).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns loadingLarge on GET_SHORT_URL_VISITS_LARGE', () => {
|
||||
const { loadingLarge } = reducer(buildState({ loadingLarge: false }), getShortUrlVisits.large());
|
||||
expect(loadingLarge).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns cancelLoad on GET_SHORT_URL_VISITS_CANCEL', () => {
|
||||
const { cancelLoad } = reducer(buildState({ cancelLoad: false }), cancelGetShortUrlVisits());
|
||||
expect(cancelLoad).toEqual(true);
|
||||
});
|
||||
|
||||
it('stops loading and returns error on GET_SHORT_URL_VISITS_ERROR', () => {
|
||||
const { loading, error } = reducer(
|
||||
buildState({ loading: true, error: false }),
|
||||
getShortUrlVisits.rejected(null, '', { shortCode: '' }),
|
||||
);
|
||||
|
||||
expect(loading).toEqual(false);
|
||||
expect(error).toEqual(true);
|
||||
});
|
||||
|
||||
it('return visits on GET_SHORT_URL_VISITS', () => {
|
||||
const actionVisits: Visit[] = [fromPartial({}), fromPartial({})];
|
||||
const { loading, error, visits } = reducer(
|
||||
buildState({ loading: true, error: false }),
|
||||
getShortUrlVisits.fulfilled({ visits: actionVisits }, '', { shortCode: '' }),
|
||||
);
|
||||
|
||||
expect(loading).toEqual(false);
|
||||
expect(error).toEqual(false);
|
||||
expect(visits).toEqual(actionVisits);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ shortCode: 'abc123' }, visitsMocks.length + 1],
|
||||
[{ shortCode: 'def456' }, visitsMocks.length],
|
||||
[
|
||||
fromPartial<ShortUrlVisits>({
|
||||
shortCode: 'abc123',
|
||||
query: { endDate: formatIsoDate(subDays(now, 1)) ?? undefined },
|
||||
}),
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
fromPartial<ShortUrlVisits>({
|
||||
shortCode: 'abc123',
|
||||
query: { startDate: formatIsoDate(addDays(now, 1)) ?? undefined },
|
||||
}),
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
fromPartial<ShortUrlVisits>({
|
||||
shortCode: 'abc123',
|
||||
query: {
|
||||
startDate: formatIsoDate(subDays(now, 5)) ?? undefined,
|
||||
endDate: formatIsoDate(subDays(now, 2)) ?? undefined,
|
||||
},
|
||||
}),
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
fromPartial<ShortUrlVisits>({
|
||||
shortCode: 'abc123',
|
||||
query: {
|
||||
startDate: formatIsoDate(subDays(now, 5)) ?? undefined,
|
||||
endDate: formatIsoDate(addDays(now, 3)) ?? undefined,
|
||||
},
|
||||
}),
|
||||
visitsMocks.length + 1,
|
||||
],
|
||||
[
|
||||
fromPartial<ShortUrlVisits>({
|
||||
shortCode: 'def456',
|
||||
query: {
|
||||
startDate: formatIsoDate(subDays(now, 5)) ?? undefined,
|
||||
endDate: formatIsoDate(addDays(now, 3)) ?? undefined,
|
||||
},
|
||||
}),
|
||||
visitsMocks.length,
|
||||
],
|
||||
])('prepends new visits on CREATE_VISIT', (state, expectedVisits) => {
|
||||
const shortUrl = {
|
||||
shortCode: 'abc123',
|
||||
};
|
||||
const prevState = buildState({
|
||||
...state,
|
||||
visits: visitsMocks,
|
||||
});
|
||||
|
||||
const { visits } = reducer(
|
||||
prevState,
|
||||
createNewVisits([fromPartial({ shortUrl, visit: { date: formatIsoDate(now) ?? undefined } })]),
|
||||
);
|
||||
|
||||
expect(visits).toHaveLength(expectedVisits);
|
||||
});
|
||||
|
||||
it('returns new progress on GET_SHORT_URL_VISITS_PROGRESS_CHANGED', () => {
|
||||
const { progress } = reducer(undefined, getShortUrlVisits.progressChanged(85));
|
||||
expect(progress).toEqual(85);
|
||||
});
|
||||
|
||||
it('returns fallbackInterval on GET_SHORT_URL_VISITS_FALLBACK_TO_INTERVAL', () => {
|
||||
const fallbackInterval: DateInterval = 'last30Days';
|
||||
const state = reducer(undefined, getShortUrlVisits.fallbackToInterval(fallbackInterval));
|
||||
|
||||
expect(state).toEqual(expect.objectContaining({ fallbackInterval }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getShortUrlVisits', () => {
|
||||
const dispatchMock = vi.fn();
|
||||
const getState = () => fromPartial<ShlinkState>({
|
||||
shortUrlVisits: { cancelLoad: false },
|
||||
});
|
||||
|
||||
it.each([
|
||||
[undefined, undefined],
|
||||
[{}, undefined],
|
||||
[{ domain: 'foobar.com' }, 'foobar.com'],
|
||||
])('dispatches start and success when promise is resolved', async (query, domain) => {
|
||||
const visits = visitsMocks;
|
||||
const shortCode = 'abc123';
|
||||
getShortUrlVisitsCall.mockResolvedValue({
|
||||
data: visitsMocks,
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
pagesCount: 1,
|
||||
totalItems: 1,
|
||||
},
|
||||
});
|
||||
|
||||
await getShortUrlVisits({ shortCode, query })(dispatchMock, getState, {});
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(2);
|
||||
expect(dispatchMock).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
payload: { visits, shortCode, domain, query: query ?? {} },
|
||||
}));
|
||||
expect(getShortUrlVisitsCall).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('performs multiple API requests when response contains more pages', async () => {
|
||||
const expectedRequests = 3;
|
||||
getShortUrlVisitsCall.mockImplementation(async (_, { page }) =>
|
||||
Promise.resolve({
|
||||
data: visitsMocks,
|
||||
pagination: {
|
||||
currentPage: page,
|
||||
pagesCount: expectedRequests,
|
||||
totalItems: 1,
|
||||
},
|
||||
}));
|
||||
|
||||
await getShortUrlVisits({ shortCode: 'abc123' })(dispatchMock, getState, {});
|
||||
|
||||
expect(getShortUrlVisitsCall).toHaveBeenCalledTimes(expectedRequests);
|
||||
expect(dispatchMock).toHaveBeenNthCalledWith(3, expect.objectContaining({
|
||||
payload: expect.objectContaining({
|
||||
visits: [...visitsMocks, ...visitsMocks, ...visitsMocks],
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
[fromPartial<Visit>({ date: formatISO(subDays(now, 5)) })],
|
||||
getShortUrlVisits.fallbackToInterval('last7Days'),
|
||||
3,
|
||||
],
|
||||
[
|
||||
[fromPartial<Visit>({ date: formatISO(subDays(now, 200)) })],
|
||||
getShortUrlVisits.fallbackToInterval('last365Days'),
|
||||
3,
|
||||
],
|
||||
[[], expect.objectContaining({ type: getShortUrlVisits.fulfilled.toString() }), 2],
|
||||
])('dispatches fallback interval when the list of visits is empty', async (
|
||||
lastVisits,
|
||||
expectedSecondDispatch,
|
||||
expectedDispatchCalls,
|
||||
) => {
|
||||
const buildVisitsResult = (data: Visit[] = []): ShlinkVisits => ({
|
||||
data,
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
pagesCount: 1,
|
||||
totalItems: 1,
|
||||
},
|
||||
});
|
||||
getShortUrlVisitsCall
|
||||
.mockResolvedValueOnce(buildVisitsResult())
|
||||
.mockResolvedValueOnce(buildVisitsResult(lastVisits));
|
||||
|
||||
await getShortUrlVisits({ shortCode: 'abc123', doIntervalFallback: true })(dispatchMock, getState, {});
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(expectedDispatchCalls);
|
||||
expect(dispatchMock).toHaveBeenNthCalledWith(2, expectedSecondDispatch);
|
||||
expect(getShortUrlVisitsCall).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
209
shlink-web-component/test/visits/reducers/tagVisits.test.ts
Normal file
209
shlink-web-component/test/visits/reducers/tagVisits.test.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { addDays, formatISO, subDays } from 'date-fns';
|
||||
import type { ShlinkApiClient } from '../../../../src/api/services/ShlinkApiClient';
|
||||
import type { ShlinkState } from '../../../../src/container/types';
|
||||
import { rangeOf } from '../../../../src/utils/utils';
|
||||
import type { ShlinkVisits } from '../../../src/api/types';
|
||||
import { formatIsoDate } from '../../../src/utils/dates/helpers/date';
|
||||
import type { DateInterval } from '../../../src/utils/dates/helpers/dateIntervals';
|
||||
import type {
|
||||
TagVisits } from '../../../src/visits/reducers/tagVisits';
|
||||
import {
|
||||
getTagVisits as getTagVisitsCreator,
|
||||
tagVisitsReducerCreator,
|
||||
} from '../../../src/visits/reducers/tagVisits';
|
||||
import { createNewVisits } from '../../../src/visits/reducers/visitCreation';
|
||||
import type { Visit } from '../../../src/visits/types';
|
||||
|
||||
describe('tagVisitsReducer', () => {
|
||||
const now = new Date();
|
||||
const visitsMocks = rangeOf(2, () => fromPartial<Visit>({}));
|
||||
const getTagVisitsCall = vi.fn();
|
||||
const buildShlinkApiClientMock = () => fromPartial<ShlinkApiClient>({ getTagVisits: getTagVisitsCall });
|
||||
const getTagVisits = getTagVisitsCreator(buildShlinkApiClientMock);
|
||||
const { reducer, cancelGetVisits: cancelGetTagVisits } = tagVisitsReducerCreator(getTagVisits);
|
||||
|
||||
describe('reducer', () => {
|
||||
const buildState = (data: Partial<TagVisits>) => fromPartial<TagVisits>(data);
|
||||
|
||||
it('returns loading on GET_TAG_VISITS_START', () => {
|
||||
const { loading } = reducer(buildState({ loading: false }), getTagVisits.pending('', { tag: '' }));
|
||||
expect(loading).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns loadingLarge on GET_TAG_VISITS_LARGE', () => {
|
||||
const { loadingLarge } = reducer(buildState({ loadingLarge: false }), getTagVisits.large());
|
||||
expect(loadingLarge).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns cancelLoad on GET_TAG_VISITS_CANCEL', () => {
|
||||
const { cancelLoad } = reducer(buildState({ cancelLoad: false }), cancelGetTagVisits());
|
||||
expect(cancelLoad).toEqual(true);
|
||||
});
|
||||
|
||||
it('stops loading and returns error on GET_TAG_VISITS_ERROR', () => {
|
||||
const { loading, error } = reducer(
|
||||
buildState({ loading: true, error: false }),
|
||||
getTagVisits.rejected(null, '', { tag: '' }),
|
||||
);
|
||||
|
||||
expect(loading).toEqual(false);
|
||||
expect(error).toEqual(true);
|
||||
});
|
||||
|
||||
it('return visits on GET_TAG_VISITS', () => {
|
||||
const actionVisits: Visit[] = [fromPartial({}), fromPartial({})];
|
||||
const { loading, error, visits } = reducer(
|
||||
buildState({ loading: true, error: false }),
|
||||
getTagVisits.fulfilled({ visits: actionVisits }, '', { tag: '' }),
|
||||
);
|
||||
|
||||
expect(loading).toEqual(false);
|
||||
expect(error).toEqual(false);
|
||||
expect(visits).toEqual(actionVisits);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ tag: 'foo' }, visitsMocks.length + 1],
|
||||
[{ tag: 'bar' }, visitsMocks.length],
|
||||
[
|
||||
fromPartial<TagVisits>({
|
||||
tag: 'foo',
|
||||
query: { endDate: formatIsoDate(subDays(now, 1)) ?? undefined },
|
||||
}),
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
fromPartial<TagVisits>({
|
||||
tag: 'foo',
|
||||
query: { startDate: formatIsoDate(addDays(now, 1)) ?? undefined },
|
||||
}),
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
fromPartial<TagVisits>({
|
||||
tag: 'foo',
|
||||
query: {
|
||||
startDate: formatIsoDate(subDays(now, 5)) ?? undefined,
|
||||
endDate: formatIsoDate(subDays(now, 2)) ?? undefined,
|
||||
},
|
||||
}),
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
fromPartial<TagVisits>({
|
||||
tag: 'foo',
|
||||
query: {
|
||||
startDate: formatIsoDate(subDays(now, 5)) ?? undefined,
|
||||
endDate: formatIsoDate(addDays(now, 3)) ?? undefined,
|
||||
},
|
||||
}),
|
||||
visitsMocks.length + 1,
|
||||
],
|
||||
[
|
||||
fromPartial<TagVisits>({
|
||||
tag: 'bar',
|
||||
query: {
|
||||
startDate: formatIsoDate(subDays(now, 5)) ?? undefined,
|
||||
endDate: formatIsoDate(addDays(now, 3)) ?? undefined,
|
||||
},
|
||||
}),
|
||||
visitsMocks.length,
|
||||
],
|
||||
])('prepends new visits on CREATE_VISIT', (state, expectedVisits) => {
|
||||
const shortUrl = {
|
||||
tags: ['foo', 'baz'],
|
||||
};
|
||||
const prevState = buildState({
|
||||
...state,
|
||||
visits: visitsMocks,
|
||||
});
|
||||
|
||||
const { visits } = reducer(
|
||||
prevState,
|
||||
createNewVisits([fromPartial({ shortUrl, visit: { date: formatIsoDate(now) ?? undefined } })]),
|
||||
);
|
||||
|
||||
expect(visits).toHaveLength(expectedVisits);
|
||||
});
|
||||
|
||||
it('returns new progress on GET_TAG_VISITS_PROGRESS_CHANGED', () => {
|
||||
const { progress } = reducer(undefined, getTagVisits.progressChanged(85));
|
||||
expect(progress).toEqual(85);
|
||||
});
|
||||
|
||||
it('returns fallbackInterval on GET_TAG_VISITS_FALLBACK_TO_INTERVAL', () => {
|
||||
const fallbackInterval: DateInterval = 'last30Days';
|
||||
const state = reducer(undefined, getTagVisits.fallbackToInterval(fallbackInterval));
|
||||
|
||||
expect(state).toEqual(expect.objectContaining({ fallbackInterval }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTagVisits', () => {
|
||||
const dispatchMock = vi.fn();
|
||||
const getState = () => fromPartial<ShlinkState>({
|
||||
tagVisits: { cancelLoad: false },
|
||||
});
|
||||
const tag = 'foo';
|
||||
|
||||
it.each([
|
||||
[undefined],
|
||||
[{}],
|
||||
])('dispatches start and success when promise is resolved', async (query) => {
|
||||
const visits = visitsMocks;
|
||||
getTagVisitsCall.mockResolvedValue({
|
||||
data: visitsMocks,
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
pagesCount: 1,
|
||||
totalItems: 1,
|
||||
},
|
||||
});
|
||||
|
||||
await getTagVisits({ tag, query })(dispatchMock, getState, {});
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(2);
|
||||
expect(dispatchMock).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
payload: { visits, tag, query: query ?? {} },
|
||||
}));
|
||||
expect(getTagVisitsCall).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
[fromPartial<Visit>({ date: formatISO(subDays(now, 20)) })],
|
||||
getTagVisits.fallbackToInterval('last30Days'),
|
||||
3,
|
||||
],
|
||||
[
|
||||
[fromPartial<Visit>({ date: formatISO(subDays(now, 100)) })],
|
||||
getTagVisits.fallbackToInterval('last180Days'),
|
||||
3,
|
||||
],
|
||||
[[], expect.objectContaining({ type: getTagVisits.fulfilled.toString() }), 2],
|
||||
])('dispatches fallback interval when the list of visits is empty', async (
|
||||
lastVisits,
|
||||
expectedSecondDispatch,
|
||||
expectedDispatchCalls,
|
||||
) => {
|
||||
const buildVisitsResult = (data: Visit[] = []): ShlinkVisits => ({
|
||||
data,
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
pagesCount: 1,
|
||||
totalItems: 1,
|
||||
},
|
||||
});
|
||||
getTagVisitsCall
|
||||
.mockResolvedValueOnce(buildVisitsResult())
|
||||
.mockResolvedValueOnce(buildVisitsResult(lastVisits));
|
||||
|
||||
await getTagVisits({ tag, doIntervalFallback: true })(dispatchMock, getState, {});
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(expectedDispatchCalls);
|
||||
expect(dispatchMock).toHaveBeenNthCalledWith(2, expectedSecondDispatch);
|
||||
expect(getTagVisitsCall).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import type { ShortUrl } from '../../../src/short-urls/data';
|
||||
import { createNewVisits } from '../../../src/visits/reducers/visitCreation';
|
||||
import type { Visit } from '../../../src/visits/types';
|
||||
|
||||
describe('visitCreationReducer', () => {
|
||||
describe('createNewVisits', () => {
|
||||
const shortUrl = fromPartial<ShortUrl>({});
|
||||
const visit = fromPartial<Visit>({});
|
||||
|
||||
it('just returns the action with proper type', () => {
|
||||
const { payload } = createNewVisits([{ shortUrl, visit }]);
|
||||
expect(payload).toEqual({ createdVisits: [{ shortUrl, visit }] });
|
||||
});
|
||||
});
|
||||
});
|
||||
165
shlink-web-component/test/visits/reducers/visitsOverview.test.ts
Normal file
165
shlink-web-component/test/visits/reducers/visitsOverview.test.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import type { ShlinkApiClient } from '../../../../src/api/services/ShlinkApiClient';
|
||||
import type { ShlinkState } from '../../../../src/container/types';
|
||||
import type { ShlinkVisitsOverview } from '../../../src/api/types';
|
||||
import { createNewVisits } from '../../../src/visits/reducers/visitCreation';
|
||||
import type {
|
||||
PartialVisitsSummary,
|
||||
VisitsOverview,
|
||||
} from '../../../src/visits/reducers/visitsOverview';
|
||||
import {
|
||||
loadVisitsOverview as loadVisitsOverviewCreator,
|
||||
visitsOverviewReducerCreator,
|
||||
} from '../../../src/visits/reducers/visitsOverview';
|
||||
import type { OrphanVisit } from '../../../src/visits/types';
|
||||
|
||||
describe('visitsOverviewReducer', () => {
|
||||
const getVisitsOverview = vi.fn();
|
||||
const buildApiClientMock = () => fromPartial<ShlinkApiClient>({ getVisitsOverview });
|
||||
const loadVisitsOverview = loadVisitsOverviewCreator(buildApiClientMock);
|
||||
const { reducer } = visitsOverviewReducerCreator(loadVisitsOverview);
|
||||
|
||||
describe('reducer', () => {
|
||||
const state = (payload: Partial<VisitsOverview> = {}) => fromPartial<VisitsOverview>(payload);
|
||||
|
||||
it('returns loading on GET_OVERVIEW_START', () => {
|
||||
const { loading } = reducer(
|
||||
state({ loading: false, error: false }),
|
||||
loadVisitsOverview.pending(''),
|
||||
);
|
||||
|
||||
expect(loading).toEqual(true);
|
||||
});
|
||||
|
||||
it('stops loading and returns error on GET_OVERVIEW_ERROR', () => {
|
||||
const { loading, error } = reducer(
|
||||
state({ loading: true, error: false }),
|
||||
loadVisitsOverview.rejected(null, ''),
|
||||
);
|
||||
|
||||
expect(loading).toEqual(false);
|
||||
expect(error).toEqual(true);
|
||||
});
|
||||
|
||||
it('return visits overview on GET_OVERVIEW', () => {
|
||||
const action = loadVisitsOverview.fulfilled(fromPartial({
|
||||
nonOrphanVisits: { total: 100 },
|
||||
}), 'requestId');
|
||||
const { loading, error, nonOrphanVisits } = reducer(state({ loading: true, error: false }), action);
|
||||
|
||||
expect(loading).toEqual(false);
|
||||
expect(error).toEqual(false);
|
||||
expect(nonOrphanVisits.total).toEqual(100);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[50, 53],
|
||||
[0, 3],
|
||||
])('returns updated amounts on CREATE_VISITS', (providedOrphanVisitsCount, expectedOrphanVisitsCount) => {
|
||||
const { nonOrphanVisits, orphanVisits } = reducer(
|
||||
state({
|
||||
nonOrphanVisits: { total: 100 },
|
||||
orphanVisits: { total: providedOrphanVisitsCount },
|
||||
}),
|
||||
createNewVisits([
|
||||
fromPartial({ visit: {} }),
|
||||
fromPartial({ visit: {} }),
|
||||
fromPartial({ visit: fromPartial<OrphanVisit>({ visitedUrl: '' }) }),
|
||||
fromPartial({ visit: fromPartial<OrphanVisit>({ visitedUrl: '' }) }),
|
||||
fromPartial({ visit: fromPartial<OrphanVisit>({ visitedUrl: '' }) }),
|
||||
]),
|
||||
);
|
||||
|
||||
expect(nonOrphanVisits.total).toEqual(102);
|
||||
expect(orphanVisits.total).toEqual(expectedOrphanVisitsCount);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
{} satisfies Omit<PartialVisitsSummary, 'total'>,
|
||||
{} satisfies Omit<PartialVisitsSummary, 'total'>,
|
||||
{ total: 103 } satisfies PartialVisitsSummary,
|
||||
{ total: 203 } satisfies PartialVisitsSummary,
|
||||
],
|
||||
[
|
||||
{ bots: 35 } satisfies Omit<PartialVisitsSummary, 'total'>,
|
||||
{ bots: 35 } satisfies Omit<PartialVisitsSummary, 'total'>,
|
||||
{ total: 103, bots: 37 } satisfies PartialVisitsSummary,
|
||||
{ total: 203, bots: 36 } satisfies PartialVisitsSummary,
|
||||
],
|
||||
[
|
||||
{ nonBots: 41, bots: 85 } satisfies Omit<PartialVisitsSummary, 'total'>,
|
||||
{ nonBots: 63, bots: 27 } satisfies Omit<PartialVisitsSummary, 'total'>,
|
||||
{ total: 103, nonBots: 42, bots: 87 } satisfies PartialVisitsSummary,
|
||||
{ total: 203, nonBots: 65, bots: 28 } satisfies PartialVisitsSummary,
|
||||
],
|
||||
[
|
||||
{ nonBots: 56 } satisfies Omit<PartialVisitsSummary, 'total'>,
|
||||
{ nonBots: 99 } satisfies Omit<PartialVisitsSummary, 'total'>,
|
||||
{ total: 103, nonBots: 57 } satisfies PartialVisitsSummary,
|
||||
{ total: 203, nonBots: 101 } satisfies PartialVisitsSummary,
|
||||
],
|
||||
])('takes bots and non-bots into consideration when creating visits', (
|
||||
initialNonOrphanVisits,
|
||||
initialOrphanVisits,
|
||||
expectedNonOrphanVisits,
|
||||
expectedOrphanVisits,
|
||||
) => {
|
||||
const { nonOrphanVisits, orphanVisits } = reducer(
|
||||
state({
|
||||
nonOrphanVisits: { total: 100, ...initialNonOrphanVisits },
|
||||
orphanVisits: { total: 200, ...initialOrphanVisits },
|
||||
}),
|
||||
createNewVisits([
|
||||
fromPartial({ visit: {} }),
|
||||
fromPartial({ visit: { potentialBot: true } }),
|
||||
fromPartial({ visit: { potentialBot: true } }),
|
||||
fromPartial({ visit: fromPartial<OrphanVisit>({ visitedUrl: '' }) }),
|
||||
fromPartial({ visit: fromPartial<OrphanVisit>({ visitedUrl: '' }) }),
|
||||
fromPartial({ visit: fromPartial<OrphanVisit>({ visitedUrl: '', potentialBot: true }) }),
|
||||
]),
|
||||
);
|
||||
|
||||
expect(nonOrphanVisits).toEqual(expectedNonOrphanVisits);
|
||||
expect(orphanVisits).toEqual(expectedOrphanVisits);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadVisitsOverview', () => {
|
||||
const dispatchMock = vi.fn();
|
||||
const getState = () => fromPartial<ShlinkState>({});
|
||||
|
||||
it.each([
|
||||
[
|
||||
// Shlink <3.5.0
|
||||
{ visitsCount: 50, orphanVisitsCount: 20 } satisfies ShlinkVisitsOverview,
|
||||
{
|
||||
nonOrphanVisits: { total: 50, nonBots: undefined, bots: undefined },
|
||||
orphanVisits: { total: 20, nonBots: undefined, bots: undefined },
|
||||
},
|
||||
],
|
||||
[
|
||||
// Shlink >=3.5.0
|
||||
{
|
||||
nonOrphanVisits: { total: 50, nonBots: 20, bots: 30 },
|
||||
orphanVisits: { total: 50, nonBots: 20, bots: 30 },
|
||||
visitsCount: 3,
|
||||
orphanVisitsCount: 3,
|
||||
} satisfies ShlinkVisitsOverview,
|
||||
{
|
||||
nonOrphanVisits: { total: 50, nonBots: 20, bots: 30 },
|
||||
orphanVisits: { total: 50, nonBots: 20, bots: 30 },
|
||||
},
|
||||
],
|
||||
])('dispatches start and success when promise is resolved', async (serverResult, dispatchedPayload) => {
|
||||
const resolvedOverview = fromPartial<ShlinkVisitsOverview>(serverResult);
|
||||
getVisitsOverview.mockResolvedValue(resolvedOverview);
|
||||
|
||||
await loadVisitsOverview()(dispatchMock, getState, {});
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(2);
|
||||
expect(dispatchMock).toHaveBeenNthCalledWith(2, expect.objectContaining({ payload: dispatchedPayload }));
|
||||
expect(getVisitsOverview).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user