Fix shlink-web-component tests

This commit is contained in:
Alejandro Celaya 2023-08-04 11:16:01 +02:00
parent bdcfcee60e
commit 4d8477a32c
54 changed files with 345 additions and 431 deletions

View file

@ -14,7 +14,7 @@ export const VisitsHighlightCard: FC<VisitsHighlightCardProps> = ({ loading, exc
<HighlightCard <HighlightCard
tooltip={ tooltip={
visitsSummary.bots !== undefined visitsSummary.bots !== undefined
? <>{excludeBots ? 'Plus' : 'Including'} <b>{prettify(visitsSummary.bots)}</b> potential bot visits</> ? <>{excludeBots ? 'Plus' : 'Including'} <strong>{prettify(visitsSummary.bots)}</strong> potential bot visits</>
: undefined : undefined
} }
{...rest} {...rest}

View file

@ -1,5 +1,5 @@
import type { FC, ReactElement } from 'react'; import type { FC, ReactElement } from 'react';
import { useToggle } from '../../src/utils/helpers/hooks'; import { useToggle } from '../../../shlink-frontend-kit/src';
interface RenderModalArgs { interface RenderModalArgs {
isOpen: boolean; isOpen: boolean;

View file

@ -1,12 +1,11 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router'; import { MemoryRouter } from 'react-router';
import { AsideMenu } from '../../src/common/AsideMenu'; import { AsideMenu } from '../../src/common/AsideMenu';
describe('<AsideMenu />', () => { describe('<AsideMenu />', () => {
const setUp = () => render( const setUp = () => render(
<MemoryRouter> <MemoryRouter>
<AsideMenu selectedServer={fromPartial({ id: 'abc123', version: '2.8.0' })} /> <AsideMenu routePrefix="/abc123" />
</MemoryRouter>, </MemoryRouter>,
); );

View file

@ -1,7 +1,7 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import type { InvalidArgumentError, ProblemDetailsError } from '../../src/api/types/errors'; import type { InvalidArgumentError, ProblemDetailsError } from '../../src/api-contract';
import { ErrorTypeV2, ErrorTypeV3 } from '../../src/api/types/errors'; import { ErrorTypeV2, ErrorTypeV3 } from '../../src/api-contract';
import type { ShlinkApiErrorProps } from '../../src/common/ShlinkApiError'; import type { ShlinkApiErrorProps } from '../../src/common/ShlinkApiError';
import { ShlinkApiError } from '../../src/common/ShlinkApiError'; import { ShlinkApiError } from '../../src/common/ShlinkApiError';

View file

@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ShlinkDomainRedirects } from '../../src/api/types'; import type { ShlinkDomainRedirects } from '../../src/api-contract';
import type { Domain } from '../../src/domains/data'; import type { Domain } from '../../src/domains/data';
import { DomainRow } from '../../src/domains/DomainRow'; import { DomainRow } from '../../src/domains/DomainRow';
@ -21,7 +21,6 @@ describe('<DomainRow />', () => {
<DomainRow <DomainRow
domain={domain} domain={domain}
defaultRedirects={defaultRedirects} defaultRedirects={defaultRedirects}
selectedServer={fromPartial({})}
editDomainRedirects={vi.fn()} editDomainRedirects={vi.fn()}
checkDomainHealth={vi.fn()} checkDomainHealth={vi.fn()}
/> />

View file

@ -1,26 +1,29 @@
import { screen, waitForElementToBeRemoved } from '@testing-library/react'; import { screen, waitForElementToBeRemoved } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import type { SelectedServer } from '../../../../src/servers/data';
import type { SemVer } from '../../../../src/utils/helpers/version';
import type { Domain } from '../../../src/domains/data'; import type { Domain } from '../../../src/domains/data';
import { DomainDropdown } from '../../../src/domains/helpers/DomainDropdown'; import { DomainDropdown } from '../../../src/domains/helpers/DomainDropdown';
import { FeaturesProvider } from '../../../src/utils/features';
import { RoutesPrefixProvider } from '../../../src/utils/routesPrefix';
import { renderWithEvents } from '../../__helpers__/setUpTest'; import { renderWithEvents } from '../../__helpers__/setUpTest';
describe('<DomainDropdown />', () => { describe('<DomainDropdown />', () => {
const editDomainRedirects = vi.fn().mockResolvedValue(undefined); const editDomainRedirects = vi.fn().mockResolvedValue(undefined);
const setUp = (domain?: Domain, selectedServer?: SelectedServer) => renderWithEvents( const setUp = ({ domain, withVisits = true }: { domain?: Domain; withVisits?: boolean } = {}) => renderWithEvents(
<MemoryRouter> <MemoryRouter>
<DomainDropdown <RoutesPrefixProvider value="/server/123">
domain={domain ?? fromPartial({})} <FeaturesProvider value={fromPartial({ domainVisits: withVisits })}>
selectedServer={selectedServer ?? fromPartial({})} <DomainDropdown
editDomainRedirects={editDomainRedirects} domain={domain ?? fromPartial({})}
/> editDomainRedirects={editDomainRedirects}
/>
</FeaturesProvider>
</RoutesPrefixProvider>
</MemoryRouter>, </MemoryRouter>,
); );
it('renders expected menu items', () => { it('renders expected menu items', () => {
setUp(); setUp({ withVisits: false });
expect(screen.queryByText('Visit stats')).not.toBeInTheDocument(); expect(screen.queryByText('Visit stats')).not.toBeInTheDocument();
expect(screen.getByText('Edit redirects')).toBeInTheDocument(); expect(screen.getByText('Edit redirects')).toBeInTheDocument();
@ -30,37 +33,17 @@ describe('<DomainDropdown />', () => {
[true, '_DEFAULT'], [true, '_DEFAULT'],
[false, ''], [false, ''],
])('points first link to the proper section', (isDefault, expectedLink) => { ])('points first link to the proper section', (isDefault, expectedLink) => {
setUp( setUp({ domain: fromPartial({ domain: 'foo.com', isDefault }) });
fromPartial({ domain: 'foo.com', isDefault }),
fromPartial({ version: '3.1.0', id: '123' }),
);
expect(screen.getByText('Visit stats')).toHaveAttribute('href', `/server/123/domain/foo.com${expectedLink}/visits`); expect(screen.getByText('Visit stats')).toHaveAttribute('href', `/server/123/domain/foo.com${expectedLink}/visits`);
}); });
it.each([
[true, '2.9.0' as SemVer, false],
[true, '2.10.0' as SemVer, true],
[false, '2.9.0' as SemVer, true],
])('allows editing certain the domains', (isDefault, serverVersion, canBeEdited) => {
setUp(
fromPartial({ domain: 'foo.com', isDefault }),
fromPartial({ version: serverVersion, id: '123' }),
);
if (canBeEdited) {
expect(screen.getByText('Edit redirects')).not.toHaveAttribute('disabled');
} else {
expect(screen.getByText('Edit redirects')).toHaveAttribute('disabled');
}
});
it.each([ it.each([
['foo.com'], ['foo.com'],
['bar.org'], ['bar.org'],
['baz.net'], ['baz.net'],
])('displays modal when editing redirects', async (domain) => { ])('displays modal when editing redirects', async (domain) => {
const { user } = setUp(fromPartial({ domain, isDefault: false })); const { user } = setUp({ domain: fromPartial({ domain, isDefault: false }) });
expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
expect(screen.queryByRole('form')).not.toBeInTheDocument(); expect(screen.queryByRole('form')).not.toBeInTheDocument();

View file

@ -1,6 +1,6 @@
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ShlinkApiClient } from '../../../../src/api/services/ShlinkApiClient'; import type { ShlinkApiClient } from '../../../../src/api/services/ShlinkApiClient';
import type { ShlinkDomainRedirects } from '../../../src/api/types'; import type { ShlinkDomainRedirects } from '../../../src/api-contract';
import { editDomainRedirects } from '../../../src/domains/reducers/domainRedirects'; import { editDomainRedirects } from '../../../src/domains/reducers/domainRedirects';
describe('domainRedirectsReducer', () => { describe('domainRedirectsReducer', () => {

View file

@ -1,8 +1,6 @@
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ShlinkApiClient } from '../../../../src/api/services/ShlinkApiClient'; import type { ShlinkApiClient, ShlinkDomainRedirects } from '../../../src/api-contract';
import type { ShlinkState } from '../../../../src/container/types'; import { parseApiError } from '../../../src/api-contract/utils';
import type { ShlinkDomainRedirects } from '../../../src/api/types';
import { parseApiError } from '../../../src/api/utils';
import type { Domain } from '../../../src/domains/data'; import type { Domain } from '../../../src/domains/data';
import type { EditDomainRedirects } from '../../../src/domains/reducers/domainRedirects'; import type { EditDomainRedirects } from '../../../src/domains/reducers/domainRedirects';
import { editDomainRedirects } from '../../../src/domains/reducers/domainRedirects'; import { editDomainRedirects } from '../../../src/domains/reducers/domainRedirects';
@ -17,35 +15,35 @@ describe('domainsListReducer', () => {
const getState = vi.fn(); const getState = vi.fn();
const listDomains = vi.fn(); const listDomains = vi.fn();
const health = vi.fn(); const health = vi.fn();
const buildShlinkApiClient = () => fromPartial<ShlinkApiClient>({ listDomains, health }); const apiClientFactory = () => fromPartial<ShlinkApiClient>({ listDomains, health });
const filteredDomains: Domain[] = [ const filteredDomains: Domain[] = [
fromPartial({ domain: 'foo', status: 'validating' }), fromPartial({ domain: 'foo', status: 'validating' }),
fromPartial({ domain: 'Boo', status: 'validating' }), fromPartial({ domain: 'Boo', status: 'validating' }),
]; ];
const domains: Domain[] = [...filteredDomains, fromPartial({ domain: 'bar', status: 'validating' })]; const domains: Domain[] = [...filteredDomains, fromPartial({ domain: 'bar', status: 'validating' })];
const error = { type: 'NOT_FOUND', status: 404 } as unknown as Error; const error = { type: 'NOT_FOUND', status: 404 } as unknown as Error;
const editDomainRedirectsThunk = editDomainRedirects(buildShlinkApiClient); const editDomainRedirectsThunk = editDomainRedirects(apiClientFactory);
const { reducer, listDomains: listDomainsAction, checkDomainHealth, filterDomains } = domainsListReducerCreator( const { reducer, listDomains: listDomainsAction, checkDomainHealth, filterDomains } = domainsListReducerCreator(
buildShlinkApiClient, apiClientFactory,
editDomainRedirectsThunk, editDomainRedirectsThunk,
); );
describe('reducer', () => { describe('reducer', () => {
it('returns loading on LIST_DOMAINS_START', () => { it('returns loading on LIST_DOMAINS_START', () => {
expect(reducer(undefined, listDomainsAction.pending(''))).toEqual( expect(reducer(undefined, listDomainsAction.pending('', {}))).toEqual(
{ domains: [], filteredDomains: [], loading: true, error: false }, { domains: [], filteredDomains: [], loading: true, error: false },
); );
}); });
it('returns error on LIST_DOMAINS_ERROR', () => { it('returns error on LIST_DOMAINS_ERROR', () => {
expect(reducer(undefined, listDomainsAction.rejected(error, ''))).toEqual( expect(reducer(undefined, listDomainsAction.rejected(error, '', {}))).toEqual(
{ domains: [], filteredDomains: [], loading: false, error: true, errorData: parseApiError(error) }, { domains: [], filteredDomains: [], loading: false, error: true, errorData: parseApiError(error) },
); );
}); });
it('returns domains on LIST_DOMAINS', () => { it('returns domains on LIST_DOMAINS', () => {
expect( expect(
reducer(undefined, listDomainsAction.fulfilled({ domains }, '')), reducer(undefined, listDomainsAction.fulfilled({ domains }, '', {})),
).toEqual({ domains, filteredDomains: domains, loading: false, error: false }); ).toEqual({ domains, filteredDomains: domains, loading: false, error: false });
}); });
@ -93,7 +91,7 @@ describe('domainsListReducer', () => {
it('dispatches domains once loaded', async () => { it('dispatches domains once loaded', async () => {
listDomains.mockResolvedValue({ data: domains }); listDomains.mockResolvedValue({ data: domains });
await listDomainsAction()(dispatch, getState, {}); await listDomainsAction({})(dispatch, getState, {});
expect(dispatch).toHaveBeenCalledTimes(2); expect(dispatch).toHaveBeenCalledTimes(2);
expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({ expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({
@ -116,33 +114,13 @@ describe('domainsListReducer', () => {
describe('checkDomainHealth', () => { describe('checkDomainHealth', () => {
const domain = 'example.com'; const domain = 'example.com';
it('dispatches invalid status when selected server does not have all required data', async () => {
getState.mockReturnValue(fromPartial<ShlinkState>({
selectedServer: {},
}));
await checkDomainHealth(domain)(dispatch, getState, {});
expect(getState).toHaveBeenCalledTimes(1);
expect(health).not.toHaveBeenCalled();
expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({
payload: { domain, status: 'invalid' },
}));
});
it('dispatches invalid status when health endpoint returns an error', async () => { it('dispatches invalid status when health endpoint returns an error', async () => {
getState.mockReturnValue(fromPartial<ShlinkState>({
selectedServer: {
url: 'https://myerver.com',
apiKey: '123',
},
}));
health.mockRejectedValue({}); health.mockRejectedValue({});
await checkDomainHealth(domain)(dispatch, getState, {}); await checkDomainHealth(domain)(dispatch, getState, {});
expect(getState).toHaveBeenCalledTimes(1);
expect(health).toHaveBeenCalledTimes(1); expect(health).toHaveBeenCalledTimes(1);
expect(health).toHaveBeenCalledWith(domain);
expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({ expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({
payload: { domain, status: 'invalid' }, payload: { domain, status: 'invalid' },
})); }));
@ -155,18 +133,12 @@ describe('domainsListReducer', () => {
healthStatus, healthStatus,
expectedStatus, expectedStatus,
) => { ) => {
getState.mockReturnValue(fromPartial<ShlinkState>({
selectedServer: {
url: 'https://myerver.com',
apiKey: '123',
},
}));
health.mockResolvedValue({ status: healthStatus }); health.mockResolvedValue({ status: healthStatus });
await checkDomainHealth(domain)(dispatch, getState, {}); await checkDomainHealth(domain)(dispatch, getState, {});
expect(getState).toHaveBeenCalledTimes(1);
expect(health).toHaveBeenCalledTimes(1); expect(health).toHaveBeenCalledTimes(1);
expect(health).toHaveBeenCalledWith(domain);
expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({ expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({
payload: { domain, status: expectedStatus }, payload: { domain, status: expectedStatus },
})); }));

View file

@ -1,6 +1,6 @@
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ShlinkApiClient } from '../../../../src/api/services/ShlinkApiClient'; import type { Settings } from '../../../src';
import type { GetState } from '../../../../src/container/types'; import type { ShlinkApiClient } from '../../../src/api-contract';
import { mercureInfoReducerCreator } from '../../../src/mercure/reducers/mercureInfo'; import { mercureInfoReducerCreator } from '../../../src/mercure/reducers/mercureInfo';
describe('mercureInfoReducer', () => { describe('mercureInfoReducer', () => {
@ -14,21 +14,21 @@ describe('mercureInfoReducer', () => {
describe('reducer', () => { describe('reducer', () => {
it('returns loading on GET_MERCURE_INFO_START', () => { it('returns loading on GET_MERCURE_INFO_START', () => {
expect(reducer(undefined, loadMercureInfo.pending(''))).toEqual({ expect(reducer(undefined, loadMercureInfo.pending('', {}))).toEqual({
loading: true, loading: true,
error: false, error: false,
}); });
}); });
it('returns error on GET_MERCURE_INFO_ERROR', () => { it('returns error on GET_MERCURE_INFO_ERROR', () => {
expect(reducer(undefined, loadMercureInfo.rejected(null, ''))).toEqual({ expect(reducer(undefined, loadMercureInfo.rejected(null, '', {}))).toEqual({
loading: false, loading: false,
error: true, error: true,
}); });
}); });
it('returns mercure info on GET_MERCURE_INFO', () => { it('returns mercure info on GET_MERCURE_INFO', () => {
expect(reducer(undefined, loadMercureInfo.fulfilled(mercureInfo, ''))).toEqual( expect(reducer(undefined, loadMercureInfo.fulfilled(mercureInfo, '', {}))).toEqual(
expect.objectContaining({ ...mercureInfo, loading: false, error: false }), expect.objectContaining({ ...mercureInfo, loading: false, error: false }),
); );
}); });
@ -36,17 +36,15 @@ describe('mercureInfoReducer', () => {
describe('loadMercureInfo', () => { describe('loadMercureInfo', () => {
const dispatch = vi.fn(); const dispatch = vi.fn();
const createGetStateMock = (enabled: boolean): GetState => vi.fn().mockReturnValue({ const createSettings = (enabled: boolean): Settings => fromPartial({
settings: { realTimeUpdates: { enabled },
realTimeUpdates: { enabled },
},
}); });
it('dispatches error when real time updates are disabled', async () => { it('dispatches error when real time updates are disabled', async () => {
getMercureInfo.mockResolvedValue(mercureInfo); getMercureInfo.mockResolvedValue(mercureInfo);
const getState = createGetStateMock(false); const settings = createSettings(false);
await loadMercureInfo()(dispatch, getState, {}); await loadMercureInfo(settings)(dispatch, vi.fn(), {});
expect(getMercureInfo).not.toHaveBeenCalled(); expect(getMercureInfo).not.toHaveBeenCalled();
expect(dispatch).toHaveBeenCalledTimes(2); expect(dispatch).toHaveBeenCalledTimes(2);
@ -57,9 +55,9 @@ describe('mercureInfoReducer', () => {
it('calls API on success', async () => { it('calls API on success', async () => {
getMercureInfo.mockResolvedValue(mercureInfo); getMercureInfo.mockResolvedValue(mercureInfo);
const getState = createGetStateMock(true); const settings = createSettings(true);
await loadMercureInfo()(dispatch, getState, {}); await loadMercureInfo(settings)(dispatch, vi.fn(), {});
expect(getMercureInfo).toHaveBeenCalledTimes(1); expect(getMercureInfo).toHaveBeenCalledTimes(1);
expect(dispatch).toHaveBeenCalledTimes(2); expect(dispatch).toHaveBeenCalledTimes(2);

View file

@ -4,6 +4,8 @@ import { MemoryRouter } from 'react-router-dom';
import type { MercureInfo } from '../../src/mercure/reducers/mercureInfo'; import type { MercureInfo } from '../../src/mercure/reducers/mercureInfo';
import { Overview as overviewCreator } from '../../src/overview/Overview'; import { Overview as overviewCreator } from '../../src/overview/Overview';
import { prettify } from '../../src/utils/helpers/numbers'; import { prettify } from '../../src/utils/helpers/numbers';
import { RoutesPrefixProvider } from '../../src/utils/routesPrefix';
import { SettingsProvider } from '../../src/utils/settings';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
describe('<Overview />', () => { describe('<Overview />', () => {
@ -16,26 +18,28 @@ describe('<Overview />', () => {
const shortUrls = { const shortUrls = {
pagination: { totalItems: 83710 }, pagination: { totalItems: 83710 },
}; };
const serverId = '123'; const routesPrefix = '/server/123';
const setUp = (loading = false, excludeBots = false) => renderWithEvents( const setUp = (loading = false, excludeBots = false) => renderWithEvents(
<MemoryRouter> <MemoryRouter>
<Overview <SettingsProvider value={fromPartial({ visits: { excludeBots } })}>
listShortUrls={listShortUrls} <RoutesPrefixProvider value={routesPrefix}>
listTags={listTags} <Overview
loadVisitsOverview={loadVisitsOverview} listShortUrls={listShortUrls}
shortUrlsList={fromPartial({ loading, shortUrls })} listTags={listTags}
tagsList={fromPartial({ loading, tags: ['foo', 'bar', 'baz'] })} loadVisitsOverview={loadVisitsOverview}
visitsOverview={fromPartial({ shortUrlsList={fromPartial({ loading, shortUrls })}
loading, tagsList={fromPartial({ loading, tags: ['foo', 'bar', 'baz'] })}
nonOrphanVisits: { total: 3456, bots: 1000, nonBots: 2456 }, visitsOverview={fromPartial({
orphanVisits: { total: 28, bots: 15, nonBots: 13 }, loading,
})} nonOrphanVisits: { total: 3456, bots: 1000, nonBots: 2456 },
selectedServer={fromPartial({ id: serverId })} orphanVisits: { total: 28, bots: 15, nonBots: 13 },
createNewVisits={vi.fn()} })}
loadMercureInfo={vi.fn()} createNewVisits={vi.fn()}
mercureInfo={fromPartial<MercureInfo>({})} loadMercureInfo={vi.fn()}
settings={fromPartial({ visits: { excludeBots } })} mercureInfo={fromPartial<MercureInfo>({})}
/> />
</RoutesPrefixProvider>
</SettingsProvider>
</MemoryRouter>, </MemoryRouter>,
); );
@ -75,12 +79,13 @@ describe('<Overview />', () => {
const links = screen.getAllByRole('link'); const links = screen.getAllByRole('link');
expect(links).toHaveLength(5); expect(links).toHaveLength(6);
expect(links[0]).toHaveAttribute('href', `/server/${serverId}/orphan-visits`); expect(links[0]).toHaveAttribute('href', `${routesPrefix}/non-orphan-visits`);
expect(links[1]).toHaveAttribute('href', `/server/${serverId}/list-short-urls/1`); expect(links[1]).toHaveAttribute('href', `${routesPrefix}/orphan-visits`);
expect(links[2]).toHaveAttribute('href', `/server/${serverId}/manage-tags`); expect(links[2]).toHaveAttribute('href', `${routesPrefix}/list-short-urls/1`);
expect(links[3]).toHaveAttribute('href', `/server/${serverId}/create-short-url`); expect(links[3]).toHaveAttribute('href', `${routesPrefix}/manage-tags`);
expect(links[4]).toHaveAttribute('href', `/server/${serverId}/list-short-urls/1`); expect(links[4]).toHaveAttribute('href', `${routesPrefix}/create-short-url`);
expect(links[5]).toHaveAttribute('href', `${routesPrefix}/list-short-urls/1`);
}); });
it.each([ it.each([

View file

@ -1,27 +1,17 @@
import { screen, waitFor } from '@testing-library/react'; import { screen, waitFor } from '@testing-library/react';
import type { ReactNode } from 'react'; import type { PropsWithChildren } from 'react';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import type { HighlightCardProps } from '../../../src/overview/helpers/HighlightCard'; import type { HighlightCardProps } from '../../../src/overview/helpers/HighlightCard';
import { HighlightCard } from '../../../src/overview/helpers/HighlightCard'; import { HighlightCard } from '../../../src/overview/helpers/HighlightCard';
import { renderWithEvents } from '../../__helpers__/setUpTest'; import { renderWithEvents } from '../../__helpers__/setUpTest';
describe('<HighlightCard />', () => { describe('<HighlightCard />', () => {
const setUp = (props: HighlightCardProps & { children?: ReactNode }) => renderWithEvents( const setUp = (props: PropsWithChildren<Partial<HighlightCardProps>>) => renderWithEvents(
<MemoryRouter> <MemoryRouter>
<HighlightCard {...props} /> <HighlightCard link="" title="" {...props} />
</MemoryRouter>, </MemoryRouter>,
); );
it.each([
[undefined],
[''],
])('does not render icon when there is no link', (link) => {
setUp({ title: 'foo', link });
expect(screen.queryByRole('img', { hidden: true })).not.toBeInTheDocument();
expect(screen.queryByRole('link')).not.toBeInTheDocument();
});
it.each([ it.each([
['foo'], ['foo'],
['bar'], ['bar'],

View file

@ -1,18 +1,21 @@
import { screen, waitFor } from '@testing-library/react'; import { screen, waitFor } from '@testing-library/react';
import { MemoryRouter } from 'react-router';
import type { VisitsHighlightCardProps } from '../../../src/overview/helpers/VisitsHighlightCard'; import type { VisitsHighlightCardProps } from '../../../src/overview/helpers/VisitsHighlightCard';
import { VisitsHighlightCard } from '../../../src/overview/helpers/VisitsHighlightCard'; import { VisitsHighlightCard } from '../../../src/overview/helpers/VisitsHighlightCard';
import { renderWithEvents } from '../../__helpers__/setUpTest'; import { renderWithEvents } from '../../__helpers__/setUpTest';
describe('<VisitsHighlightCard />', () => { describe('<VisitsHighlightCard />', () => {
const setUp = (props: Partial<VisitsHighlightCardProps> = {}) => renderWithEvents( const setUp = (props: Partial<VisitsHighlightCardProps> = {}) => renderWithEvents(
<VisitsHighlightCard <MemoryRouter>
loading={false} <VisitsHighlightCard
visitsSummary={{ total: 0 }} loading={false}
excludeBots={false} visitsSummary={{ total: 0 }}
title="" excludeBots={false}
link="" title=""
{...props} link=""
/>, {...props}
/>
</MemoryRouter>,
); );
it.each([ it.each([

View file

@ -2,6 +2,7 @@ import { render, screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { CreateShortUrl as createShortUrlsCreator } from '../../src/short-urls/CreateShortUrl'; import { CreateShortUrl as createShortUrlsCreator } from '../../src/short-urls/CreateShortUrl';
import type { ShortUrlCreation } from '../../src/short-urls/reducers/shortUrlCreation'; import type { ShortUrlCreation } from '../../src/short-urls/reducers/shortUrlCreation';
import { SettingsProvider } from '../../src/utils/settings';
describe('<CreateShortUrl />', () => { describe('<CreateShortUrl />', () => {
const ShortUrlForm = () => <span>ShortUrlForm</span>; const ShortUrlForm = () => <span>ShortUrlForm</span>;
@ -11,13 +12,13 @@ describe('<CreateShortUrl />', () => {
const createShortUrl = vi.fn(async () => Promise.resolve()); const createShortUrl = vi.fn(async () => Promise.resolve());
const CreateShortUrl = createShortUrlsCreator(ShortUrlForm, CreateShortUrlResult); const CreateShortUrl = createShortUrlsCreator(ShortUrlForm, CreateShortUrlResult);
const setUp = () => render( const setUp = () => render(
<CreateShortUrl <SettingsProvider value={fromPartial({ shortUrlCreation })}>
shortUrlCreation={shortUrlCreationResult} <CreateShortUrl
createShortUrl={createShortUrl} shortUrlCreation={shortUrlCreationResult}
selectedServer={null} createShortUrl={createShortUrl}
resetCreateShortUrl={() => {}} resetCreateShortUrl={() => {}}
settings={fromPartial({ shortUrlCreation })} />
/>, </SettingsProvider>,
); );
it('renders computed initial state', () => { it('renders computed initial state', () => {

View file

@ -4,20 +4,21 @@ import { MemoryRouter } from 'react-router-dom';
import { EditShortUrl as createEditShortUrl } from '../../src/short-urls/EditShortUrl'; import { EditShortUrl as createEditShortUrl } from '../../src/short-urls/EditShortUrl';
import type { ShortUrlDetail } from '../../src/short-urls/reducers/shortUrlDetail'; import type { ShortUrlDetail } from '../../src/short-urls/reducers/shortUrlDetail';
import type { ShortUrlEdition } from '../../src/short-urls/reducers/shortUrlEdition'; import type { ShortUrlEdition } from '../../src/short-urls/reducers/shortUrlEdition';
import { SettingsProvider } from '../../src/utils/settings';
describe('<EditShortUrl />', () => { describe('<EditShortUrl />', () => {
const shortUrlCreation = { validateUrls: true }; const shortUrlCreation = { validateUrls: true };
const EditShortUrl = createEditShortUrl(() => <span>ShortUrlForm</span>); const EditShortUrl = createEditShortUrl(() => <span>ShortUrlForm</span>);
const setUp = (detail: Partial<ShortUrlDetail> = {}, edition: Partial<ShortUrlEdition> = {}) => render( const setUp = (detail: Partial<ShortUrlDetail> = {}, edition: Partial<ShortUrlEdition> = {}) => render(
<MemoryRouter> <MemoryRouter>
<EditShortUrl <SettingsProvider value={fromPartial({ shortUrlCreation })}>
settings={fromPartial({ shortUrlCreation })} <EditShortUrl
selectedServer={null} shortUrlDetail={fromPartial(detail)}
shortUrlDetail={fromPartial(detail)} shortUrlEdition={fromPartial(edition)}
shortUrlEdition={fromPartial(edition)} getShortUrlDetail={vi.fn()}
getShortUrlDetail={vi.fn()} editShortUrl={vi.fn(async () => Promise.resolve())}
editShortUrl={vi.fn(async () => Promise.resolve())} />
/> </SettingsProvider>
</MemoryRouter>, </MemoryRouter>,
); );

View file

@ -1,7 +1,7 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import type { ShlinkPaginator } from '../../src/api/types'; import type { ShlinkPaginator } from '../../src/api-contract';
import { Paginator } from '../../src/short-urls/Paginator'; import { Paginator } from '../../src/short-urls/Paginator';
import { ELLIPSIS } from '../../src/utils/helpers/pagination'; import { ELLIPSIS } from '../../src/utils/helpers/pagination';
@ -9,7 +9,7 @@ describe('<Paginator />', () => {
const buildPaginator = (pagesCount?: number) => fromPartial<ShlinkPaginator>({ pagesCount, currentPage: 1 }); const buildPaginator = (pagesCount?: number) => fromPartial<ShlinkPaginator>({ pagesCount, currentPage: 1 });
const setUp = (paginator?: ShlinkPaginator, currentQueryString?: string) => render( const setUp = (paginator?: ShlinkPaginator, currentQueryString?: string) => render(
<MemoryRouter> <MemoryRouter>
<Paginator serverId="abc123" paginator={paginator} currentQueryString={currentQueryString} /> <Paginator paginator={paginator} currentQueryString={currentQueryString} />
</MemoryRouter>, </MemoryRouter>,
); );

View file

@ -2,25 +2,26 @@ import { screen } from '@testing-library/react';
import type { UserEvent } from '@testing-library/user-event/setup/setup'; import type { UserEvent } from '@testing-library/user-event/setup/setup';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { formatISO } from 'date-fns'; import { formatISO } from 'date-fns';
import type { ReachableServer, SelectedServer } from '../../../src/servers/data';
import type { OptionalString } from '../../../src/utils/utils'; import type { OptionalString } from '../../../src/utils/utils';
import type { Mode } from '../../src/short-urls/ShortUrlForm'; import type { Mode } from '../../src/short-urls/ShortUrlForm';
import { ShortUrlForm as createShortUrlForm } from '../../src/short-urls/ShortUrlForm'; import { ShortUrlForm as createShortUrlForm } from '../../src/short-urls/ShortUrlForm';
import { parseDate } from '../../src/utils/dates/helpers/date'; import { parseDate } from '../../src/utils/dates/helpers/date';
import { FeaturesProvider } from '../../src/utils/features';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
describe('<ShortUrlForm />', () => { describe('<ShortUrlForm />', () => {
const createShortUrl = vi.fn(async () => Promise.resolve()); const createShortUrl = vi.fn(async () => Promise.resolve());
const ShortUrlForm = createShortUrlForm(() => <span>TagsSelector</span>, () => <span>DomainSelector</span>); const ShortUrlForm = createShortUrlForm(() => <span>TagsSelector</span>, () => <span>DomainSelector</span>);
const setUp = (selectedServer: SelectedServer = null, mode: Mode = 'create', title?: OptionalString) => const setUp = (withDeviceLongUrls = false, mode: Mode = 'create', title?: OptionalString) =>
renderWithEvents( renderWithEvents(
<ShortUrlForm <FeaturesProvider value={fromPartial({ deviceLongUrls: withDeviceLongUrls })}>
selectedServer={selectedServer} <ShortUrlForm
mode={mode} mode={mode}
saving={false} saving={false}
initialState={{ validateUrl: true, findIfExists: false, title, longUrl: '' }} initialState={{ validateUrl: true, findIfExists: false, title, longUrl: '' }}
onSave={createShortUrl} onSave={createShortUrl}
/>, />
</FeaturesProvider>,
); );
it.each([ it.each([
@ -29,14 +30,14 @@ describe('<ShortUrlForm />', () => {
await user.type(screen.getByPlaceholderText('Custom slug'), 'my-slug'); await user.type(screen.getByPlaceholderText('Custom slug'), 'my-slug');
}, },
{ customSlug: 'my-slug' }, { customSlug: 'my-slug' },
null, false,
], ],
[ [
async (user: UserEvent) => { async (user: UserEvent) => {
await user.type(screen.getByPlaceholderText('Short code length'), '15'); await user.type(screen.getByPlaceholderText('Short code length'), '15');
}, },
{ shortCodeLength: '15' }, { shortCodeLength: '15' },
null, false,
], ],
[ [
async (user: UserEvent) => { async (user: UserEvent) => {
@ -49,10 +50,10 @@ describe('<ShortUrlForm />', () => {
ios: 'https://ios.com', ios: 'https://ios.com',
}, },
}, },
fromPartial<ReachableServer>({ version: '3.5.0' }), true,
], ],
])('saves short URL with data set in form controls', async (extraFields, extraExpectedValues, selectedServer) => { ])('saves short URL with data set in form controls', async (extraFields, extraExpectedValues, withDeviceLongUrls) => {
const { user } = setUp(selectedServer); const { user } = setUp(withDeviceLongUrls);
const validSince = parseDate('2017-01-01', 'yyyy-MM-dd'); const validSince = parseDate('2017-01-01', 'yyyy-MM-dd');
const validUntil = parseDate('2017-01-06', 'yyyy-MM-dd'); const validUntil = parseDate('2017-01-06', 'yyyy-MM-dd');
@ -83,7 +84,7 @@ describe('<ShortUrlForm />', () => {
])( ])(
'renders expected amount of cards based on server capabilities and mode', 'renders expected amount of cards based on server capabilities and mode',
(mode, expectedAmountOfCards) => { (mode, expectedAmountOfCards) => {
setUp(null, mode); setUp(false, mode);
const cards = screen.queryAllByRole('heading'); const cards = screen.queryAllByRole('heading');
expect(cards).toHaveLength(expectedAmountOfCards); expect(cards).toHaveLength(expectedAmountOfCards);
@ -100,7 +101,7 @@ describe('<ShortUrlForm />', () => {
[undefined, false, undefined], [undefined, false, undefined],
['old title', false, null], ['old title', false, null],
])('sends expected title based on original and new values', async (originalTitle, withNewTitle, expectedSentTitle) => { ])('sends expected title based on original and new values', async (originalTitle, withNewTitle, expectedSentTitle) => {
const { user } = setUp(fromPartial({ version: '2.6.0' }), 'create', originalTitle); const { user } = setUp(false, 'create', originalTitle);
await user.type(screen.getByPlaceholderText('URL to be shortened'), 'https://long-domain.com/foo/bar'); await user.type(screen.getByPlaceholderText('URL to be shortened'), 'https://long-domain.com/foo/bar');
await user.clear(screen.getByPlaceholderText('Title')); await user.clear(screen.getByPlaceholderText('Title'));
@ -114,19 +115,10 @@ describe('<ShortUrlForm />', () => {
})); }));
}); });
it.each([ it('shows device-specific long URLs only when supported', () => {
[fromPartial<ReachableServer>({ version: '3.0.0' }), false], setUp(true);
[fromPartial<ReachableServer>({ version: '3.4.0' }), false],
[fromPartial<ReachableServer>({ version: '3.5.0' }), true],
[fromPartial<ReachableServer>({ version: '3.6.0' }), true],
])('shows device-specific long URLs only for servers supporting it', (selectedServer, fieldsExist) => {
setUp(selectedServer);
const placeholders = ['Android-specific redirection', 'iOS-specific redirection', 'Desktop-specific redirection'];
if (fieldsExist) { const placeholders = ['Android-specific redirection', 'iOS-specific redirection', 'Desktop-specific redirection'];
placeholders.forEach((placeholder) => expect(screen.getByPlaceholderText(placeholder)).toBeInTheDocument()); placeholders.forEach((placeholder) => expect(screen.getByPlaceholderText(placeholder)).toBeInTheDocument());
} else {
placeholders.forEach((placeholder) => expect(screen.queryByPlaceholderText(placeholder)).not.toBeInTheDocument());
}
}); });
}); });

View file

@ -2,10 +2,12 @@ import { screen, waitFor } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { endOfDay, formatISO, startOfDay } from 'date-fns'; import { endOfDay, formatISO, startOfDay } from 'date-fns';
import { MemoryRouter, useLocation, useNavigate } from 'react-router-dom'; import { MemoryRouter, useLocation, useNavigate } from 'react-router-dom';
import type { ReachableServer, SelectedServer } from '../../../src/servers/data';
import { ShortUrlsFilteringBar as filteringBarCreator } from '../../src/short-urls/ShortUrlsFilteringBar'; import { ShortUrlsFilteringBar as filteringBarCreator } from '../../src/short-urls/ShortUrlsFilteringBar';
import { formatIsoDate } from '../../src/utils/dates/helpers/date'; import { formatIsoDate } from '../../src/utils/dates/helpers/date';
import type { DateRange } from '../../src/utils/dates/helpers/dateIntervals'; import type { DateRange } from '../../src/utils/dates/helpers/dateIntervals';
import { FeaturesProvider } from '../../src/utils/features';
import { RoutesPrefixProvider } from '../../src/utils/routesPrefix';
import { SettingsProvider } from '../../src/utils/settings';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
vi.mock('react-router-dom', async () => ({ vi.mock('react-router-dom', async () => ({
@ -20,18 +22,19 @@ describe('<ShortUrlsFilteringBar />', () => {
const navigate = vi.fn(); const navigate = vi.fn();
const handleOrderBy = vi.fn(); const handleOrderBy = vi.fn();
const now = new Date(); const now = new Date();
const setUp = (search = '', selectedServer?: SelectedServer) => { const setUp = (search = '', filterDisabledUrls = true) => {
(useLocation as any).mockReturnValue({ search }); (useLocation as any).mockReturnValue({ search });
(useNavigate as any).mockReturnValue(navigate); (useNavigate as any).mockReturnValue(navigate);
return renderWithEvents( return renderWithEvents(
<MemoryRouter> <MemoryRouter>
<ShortUrlsFilteringBar <SettingsProvider value={fromPartial({ visits: {} })}>
selectedServer={selectedServer ?? fromPartial({})} <FeaturesProvider value={fromPartial({ filterDisabledUrls })}>
order={{}} <RoutesPrefixProvider value="/server/1">
handleOrderBy={handleOrderBy} <ShortUrlsFilteringBar order={{}} handleOrderBy={handleOrderBy} />
settings={fromPartial({ visits: {} })} </RoutesPrefixProvider>
/> </FeaturesProvider>
</SettingsProvider>
</MemoryRouter>, </MemoryRouter>,
); );
}; };
@ -71,16 +74,14 @@ describe('<ShortUrlsFilteringBar />', () => {
}); });
it.each([ it.each([
['tags=foo,bar,baz', fromPartial<ReachableServer>({ version: '3.0.0' }), true], { search: 'tags=foo,bar,baz', shouldHaveComponent: true },
['tags=foo,bar', fromPartial<ReachableServer>({ version: '3.1.0' }), true], { search: 'tags=foo,bar', shouldHaveComponent: true },
['tags=foo', fromPartial<ReachableServer>({ version: '3.0.0' }), false], { search: 'tags=foo', shouldHaveComponent: false },
['', fromPartial<ReachableServer>({ version: '3.0.0' }), false], { search: '', shouldHaveComponent: false },
['tags=foo,bar,baz', fromPartial<ReachableServer>({ version: '2.10.0' }), false],
['', fromPartial<ReachableServer>({ version: '2.10.0' }), false],
])( ])(
'renders tags mode toggle if the server supports it and there is more than one tag selected', 'renders tags mode toggle if there is more than one tag selected',
(search, selectedServer, shouldHaveComponent) => { ({ search, shouldHaveComponent }) => {
setUp(search, selectedServer); setUp(search);
if (shouldHaveComponent) { if (shouldHaveComponent) {
expect(screen.getByLabelText('Change tags mode')).toBeInTheDocument(); expect(screen.getByLabelText('Change tags mode')).toBeInTheDocument();
@ -95,7 +96,7 @@ describe('<ShortUrlsFilteringBar />', () => {
['&tagsMode=all', 'With all the tags.'], ['&tagsMode=all', 'With all the tags.'],
['&tagsMode=any', 'With any of the tags.'], ['&tagsMode=any', 'With any of the tags.'],
])('expected tags mode tooltip title', async (initialTagsMode, expectedToggleText) => { ])('expected tags mode tooltip title', async (initialTagsMode, expectedToggleText) => {
const { user } = setUp(`tags=foo,bar${initialTagsMode}`, fromPartial({ version: '3.0.0' })); const { user } = setUp(`tags=foo,bar${initialTagsMode}`, true);
await user.hover(screen.getByLabelText('Change tags mode')); await user.hover(screen.getByLabelText('Change tags mode'));
expect(await screen.findByRole('tooltip')).toHaveTextContent(expectedToggleText); expect(await screen.findByRole('tooltip')).toHaveTextContent(expectedToggleText);
@ -106,7 +107,7 @@ describe('<ShortUrlsFilteringBar />', () => {
['&tagsMode=all', 'tagsMode=any'], ['&tagsMode=all', 'tagsMode=any'],
['&tagsMode=any', 'tagsMode=all'], ['&tagsMode=any', 'tagsMode=all'],
])('redirects to first page when tags mode changes', async (initialTagsMode, expectedRedirectTagsMode) => { ])('redirects to first page when tags mode changes', async (initialTagsMode, expectedRedirectTagsMode) => {
const { user } = setUp(`tags=foo,bar${initialTagsMode}`, fromPartial({ version: '3.0.0' })); const { user } = setUp(`tags=foo,bar${initialTagsMode}`, true);
expect(navigate).not.toHaveBeenCalled(); expect(navigate).not.toHaveBeenCalled();
await user.click(screen.getByLabelText('Change tags mode')); await user.click(screen.getByLabelText('Change tags mode'));
@ -124,7 +125,7 @@ describe('<ShortUrlsFilteringBar />', () => {
['excludePastValidUntil=false', /Exclude enabled in the past/, 'excludePastValidUntil=true'], ['excludePastValidUntil=false', /Exclude enabled in the past/, 'excludePastValidUntil=true'],
['excludePastValidUntil=true', /Exclude enabled in the past/, 'excludePastValidUntil=false'], ['excludePastValidUntil=true', /Exclude enabled in the past/, 'excludePastValidUntil=false'],
])('allows to toggle filters through filtering dropdown', async (search, menuItemName, expectedQuery) => { ])('allows to toggle filters through filtering dropdown', async (search, menuItemName, expectedQuery) => {
const { user } = setUp(search, fromPartial({ version: '3.4.0' })); const { user } = setUp(search, true);
const toggleFilter = async (name: RegExp) => { const toggleFilter = async (name: RegExp) => {
await user.click(screen.getByRole('button', { name: 'Filters' })); await user.click(screen.getByRole('button', { name: 'Filters' }));
await waitFor(() => screen.findByRole('menu')); await waitFor(() => screen.findByRole('menu'));

View file

@ -1,13 +1,14 @@
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter, useNavigate } from 'react-router-dom'; import { MemoryRouter, useNavigate } from 'react-router-dom';
import type { SemVer } from '../../../src/utils/helpers/version';
import type { Settings } from '../../src'; import type { Settings } from '../../src';
import type { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub'; import type { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
import type { ShortUrlsOrder } from '../../src/short-urls/data'; import type { ShortUrlsOrder } from '../../src/short-urls/data';
import type { ShortUrlsList as ShortUrlsListModel } from '../../src/short-urls/reducers/shortUrlsList'; import type { ShortUrlsList as ShortUrlsListModel } from '../../src/short-urls/reducers/shortUrlsList';
import { ShortUrlsList as createShortUrlsList } from '../../src/short-urls/ShortUrlsList'; import { ShortUrlsList as createShortUrlsList } from '../../src/short-urls/ShortUrlsList';
import type { ShortUrlsTableType } from '../../src/short-urls/ShortUrlsTable'; import type { ShortUrlsTableType } from '../../src/short-urls/ShortUrlsTable';
import { FeaturesProvider } from '../../src/utils/features';
import { SettingsProvider } from '../../src/utils/settings';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
vi.mock('react-router-dom', async () => ({ vi.mock('react-router-dom', async () => ({
@ -35,15 +36,17 @@ describe('<ShortUrlsList />', () => {
}, },
}); });
const ShortUrlsList = createShortUrlsList(ShortUrlsTable, ShortUrlsFilteringBar); const ShortUrlsList = createShortUrlsList(ShortUrlsTable, ShortUrlsFilteringBar);
const setUp = (settings: Partial<Settings> = {}, version: SemVer = '3.0.0') => renderWithEvents( const setUp = (settings: Partial<Settings> = {}, excludeBotsOnShortUrls = true) => renderWithEvents(
<MemoryRouter> <MemoryRouter>
<ShortUrlsList <SettingsProvider value={fromPartial(settings)}>
{...fromPartial<MercureBoundProps>({ mercureInfo: { loading: true } })} <FeaturesProvider value={fromPartial({ excludeBotsOnShortUrls })}>
listShortUrls={listShortUrlsMock} <ShortUrlsList
shortUrlsList={shortUrlsList} {...fromPartial<MercureBoundProps>({ mercureInfo: { loading: true } })}
selectedServer={fromPartial({ id: '1', version })} listShortUrls={listShortUrlsMock}
settings={fromPartial(settings)} shortUrlsList={shortUrlsList}
/> />
</FeaturesProvider>
</SettingsProvider>
</MemoryRouter>, </MemoryRouter>,
); );
@ -93,26 +96,26 @@ describe('<ShortUrlsList />', () => {
shortUrlsList: { shortUrlsList: {
defaultOrdering: { field: 'visits', dir: 'ASC' }, defaultOrdering: { field: 'visits', dir: 'ASC' },
}, },
}), '3.3.0' as SemVer, { field: 'visits', dir: 'ASC' }], }), false, { field: 'visits', dir: 'ASC' }],
[fromPartial<Settings>({ [fromPartial<Settings>({
shortUrlsList: { shortUrlsList: {
defaultOrdering: { field: 'visits', dir: 'ASC' }, defaultOrdering: { field: 'visits', dir: 'ASC' },
}, },
visits: { excludeBots: true }, visits: { excludeBots: true },
}), '3.3.0' as SemVer, { field: 'visits', dir: 'ASC' }], }), false, { field: 'visits', dir: 'ASC' }],
[fromPartial<Settings>({ [fromPartial<Settings>({
shortUrlsList: { shortUrlsList: {
defaultOrdering: { field: 'visits', dir: 'ASC' }, defaultOrdering: { field: 'visits', dir: 'ASC' },
}, },
}), '3.4.0' as SemVer, { field: 'visits', dir: 'ASC' }], }), true, { field: 'visits', dir: 'ASC' }],
[fromPartial<Settings>({ [fromPartial<Settings>({
shortUrlsList: { shortUrlsList: {
defaultOrdering: { field: 'visits', dir: 'ASC' }, defaultOrdering: { field: 'visits', dir: 'ASC' },
}, },
visits: { excludeBots: true }, visits: { excludeBots: true },
}), '3.4.0' as SemVer, { field: 'nonBotVisits', dir: 'ASC' }], }), true, { field: 'nonBotVisits', dir: 'ASC' }],
])('parses order by based on server version and config', (settings, serverVersion, expectedOrderBy) => { ])('parses order by based on supported features version and config', (settings, excludeBotsOnShortUrls, expectedOrderBy) => {
setUp(settings, serverVersion); setUp(settings, excludeBotsOnShortUrls);
expect(listShortUrlsMock).toHaveBeenCalledWith(expect.objectContaining({ orderBy: expectedOrderBy })); expect(listShortUrlsMock).toHaveBeenCalledWith(expect.objectContaining({ orderBy: expectedOrderBy }));
}); });
}); });

View file

@ -1,6 +1,5 @@
import { fireEvent, screen } from '@testing-library/react'; import { fireEvent, screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import type { SelectedServer } from '../../../src/servers/data';
import type { ShortUrlsOrderableFields } from '../../src/short-urls/data'; import type { ShortUrlsOrderableFields } from '../../src/short-urls/data';
import { SHORT_URLS_ORDERABLE_FIELDS } from '../../src/short-urls/data'; import { SHORT_URLS_ORDERABLE_FIELDS } from '../../src/short-urls/data';
import type { ShortUrlsList } from '../../src/short-urls/reducers/shortUrlsList'; import type { ShortUrlsList } from '../../src/short-urls/reducers/shortUrlsList';
@ -11,8 +10,8 @@ describe('<ShortUrlsTable />', () => {
const shortUrlsList = fromPartial<ShortUrlsList>({}); const shortUrlsList = fromPartial<ShortUrlsList>({});
const orderByColumn = vi.fn(); const orderByColumn = vi.fn();
const ShortUrlsTable = shortUrlsTableCreator(() => <span>ShortUrlsRow</span>); const ShortUrlsTable = shortUrlsTableCreator(() => <span>ShortUrlsRow</span>);
const setUp = (server: SelectedServer = null) => renderWithEvents( const setUp = () => renderWithEvents(
<ShortUrlsTable shortUrlsList={shortUrlsList} selectedServer={server} orderByColumn={() => orderByColumn} />, <ShortUrlsTable shortUrlsList={shortUrlsList} orderByColumn={() => orderByColumn} />,
); );
it('should render inner table by default', () => { it('should render inner table by default', () => {
@ -54,7 +53,7 @@ describe('<ShortUrlsTable />', () => {
}); });
it('should render composed title column', () => { it('should render composed title column', () => {
setUp(fromPartial({ version: '2.0.0' })); setUp();
const { innerHTML } = screen.getAllByRole('columnheader')[2]; const { innerHTML } = screen.getAllByRole('columnheader')[2];

View file

@ -1,7 +1,6 @@
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import type { NotFoundServer, SelectedServer } from '../../../../src/servers/data';
import type { ShortUrl } from '../../../src/short-urls/data'; import type { ShortUrl } from '../../../src/short-urls/data';
import { ExportShortUrlsBtn as createExportShortUrlsBtn } from '../../../src/short-urls/helpers/ExportShortUrlsBtn'; import { ExportShortUrlsBtn as createExportShortUrlsBtn } from '../../../src/short-urls/helpers/ExportShortUrlsBtn';
import type { ReportExporter } from '../../../src/utils/services/ReportExporter'; import type { ReportExporter } from '../../../src/utils/services/ReportExporter';
@ -13,9 +12,9 @@ describe('<ExportShortUrlsBtn />', () => {
const exportShortUrls = vi.fn(); const exportShortUrls = vi.fn();
const reportExporter = fromPartial<ReportExporter>({ exportShortUrls }); const reportExporter = fromPartial<ReportExporter>({ exportShortUrls });
const ExportShortUrlsBtn = createExportShortUrlsBtn(buildShlinkApiClient, reportExporter); const ExportShortUrlsBtn = createExportShortUrlsBtn(buildShlinkApiClient, reportExporter);
const setUp = (amount?: number, selectedServer?: SelectedServer) => renderWithEvents( const setUp = (amount?: number) => renderWithEvents(
<MemoryRouter> <MemoryRouter>
<ExportShortUrlsBtn selectedServer={selectedServer ?? fromPartial({})} amount={amount} /> <ExportShortUrlsBtn amount={amount} />
</MemoryRouter>, </MemoryRouter>,
); );
@ -28,17 +27,6 @@ describe('<ExportShortUrlsBtn />', () => {
expect(screen.getByText(/Export/)).toHaveTextContent(`Export (${expectedAmount})`); expect(screen.getByText(/Export/)).toHaveTextContent(`Export (${expectedAmount})`);
}); });
it.each([
[null],
[fromPartial<NotFoundServer>({})],
])('does nothing on click if selected server is not reachable', async (selectedServer) => {
const { user } = setUp(0, selectedServer);
await user.click(screen.getByRole('button'));
expect(listShortUrls).not.toHaveBeenCalled();
expect(exportShortUrls).not.toHaveBeenCalled();
});
it.each([ it.each([
[10, 1], [10, 1],
[30, 2], [30, 2],
@ -48,7 +36,7 @@ describe('<ExportShortUrlsBtn />', () => {
[385, 20], [385, 20],
])('loads proper amount of pages based on the amount of results', async (amount, expectedPageLoads) => { ])('loads proper amount of pages based on the amount of results', async (amount, expectedPageLoads) => {
listShortUrls.mockResolvedValue({ data: [] }); listShortUrls.mockResolvedValue({ data: [] });
const { user } = setUp(amount, fromPartial({ id: '123' })); const { user } = setUp(amount);
await user.click(screen.getByRole('button')); await user.click(screen.getByRole('button'));
@ -63,7 +51,7 @@ describe('<ExportShortUrlsBtn />', () => {
tags: [], tags: [],
})], })],
}); });
const { user } = setUp(undefined, fromPartial({ id: '123' })); const { user } = setUp();
await user.click(screen.getByRole('button')); await user.click(screen.getByRole('button'));

View file

@ -1,6 +1,5 @@
import { fireEvent, screen } from '@testing-library/react'; import { fireEvent, screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import type { SemVer } from '../../../../src/utils/helpers/version';
import { QrCodeModal as createQrCodeModal } from '../../../src/short-urls/helpers/QrCodeModal'; import { QrCodeModal as createQrCodeModal } from '../../../src/short-urls/helpers/QrCodeModal';
import { renderWithEvents } from '../../__helpers__/setUpTest'; import { renderWithEvents } from '../../__helpers__/setUpTest';
@ -8,11 +7,10 @@ describe('<QrCodeModal />', () => {
const saveImage = vi.fn().mockReturnValue(Promise.resolve()); const saveImage = vi.fn().mockReturnValue(Promise.resolve());
const QrCodeModal = createQrCodeModal(fromPartial({ saveImage })); const QrCodeModal = createQrCodeModal(fromPartial({ saveImage }));
const shortUrl = 'https://s.test/abc123'; const shortUrl = 'https://s.test/abc123';
const setUp = (version: SemVer = '2.8.0') => renderWithEvents( const setUp = () => renderWithEvents(
<QrCodeModal <QrCodeModal
isOpen isOpen
shortUrl={fromPartial({ shortUrl })} shortUrl={fromPartial({ shortUrl })}
selectedServer={fromPartial({ version })}
toggle={() => {}} toggle={() => {}}
/>, />,
); );
@ -63,16 +61,14 @@ describe('<QrCodeModal />', () => {
}); });
it('shows expected components based on server version', () => { it('shows expected components based on server version', () => {
const { container } = setUp(); setUp();
const dropdowns = screen.getAllByRole('button'); const dropdowns = screen.getAllByRole('button');
const firstCol = container.parentNode?.querySelectorAll('.d-grid').item(0);
expect(dropdowns).toHaveLength(2 + 1); // Add one because of the close button expect(dropdowns).toHaveLength(2 + 2); // Add two because of the close and download buttons
expect(firstCol).toHaveClass('col-md-4');
}); });
it('saves the QR code image when clicking the Download button', async () => { it('saves the QR code image when clicking the Download button', async () => {
const { user } = setUp('2.9.0'); const { user } = setUp();
expect(saveImage).not.toHaveBeenCalled(); expect(saveImage).not.toHaveBeenCalled();
await user.click(screen.getByRole('button', { name: /^Download/ })); await user.click(screen.getByRole('button', { name: /^Download/ }));

View file

@ -1,23 +1,22 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import type { NotFoundServer, ReachableServer } from '../../../../src/servers/data';
import type { ShortUrl } from '../../../src/short-urls/data'; import type { ShortUrl } from '../../../src/short-urls/data';
import type { LinkSuffix } from '../../../src/short-urls/helpers/ShortUrlDetailLink'; import type { LinkSuffix } from '../../../src/short-urls/helpers/ShortUrlDetailLink';
import { ShortUrlDetailLink } from '../../../src/short-urls/helpers/ShortUrlDetailLink'; import { ShortUrlDetailLink } from '../../../src/short-urls/helpers/ShortUrlDetailLink';
import { RoutesPrefixProvider } from '../../../src/utils/routesPrefix';
describe('<ShortUrlDetailLink />', () => { describe('<ShortUrlDetailLink />', () => {
it.each([ it.each([
[undefined, undefined], [false, undefined],
[null, null], [false, null],
[fromPartial<ReachableServer>({ id: '1' }), null], [true, null],
[fromPartial<ReachableServer>({ id: '1' }), undefined], [true, undefined],
[fromPartial<NotFoundServer>({}), fromPartial<ShortUrl>({})], [false, fromPartial<ShortUrl>({})],
[null, fromPartial<ShortUrl>({})], [false, fromPartial<ShortUrl>({})],
[undefined, fromPartial<ShortUrl>({})], ])('only renders a plain span when either server or short URL are not set', (asLink, shortUrl) => {
])('only renders a plain span when either server or short URL are not set', (selectedServer, shortUrl) => {
render( render(
<ShortUrlDetailLink selectedServer={selectedServer} shortUrl={shortUrl} suffix="visits"> <ShortUrlDetailLink shortUrl={shortUrl} asLink={asLink} suffix="visits">
Something Something
</ShortUrlDetailLink>, </ShortUrlDetailLink>,
); );
@ -28,35 +27,37 @@ describe('<ShortUrlDetailLink />', () => {
it.each([ it.each([
[ [
fromPartial<ReachableServer>({ id: '1' }), '/server/1',
fromPartial<ShortUrl>({ shortCode: 'abc123' }), fromPartial<ShortUrl>({ shortCode: 'abc123' }),
'visits' as LinkSuffix, 'visits' as LinkSuffix,
'/server/1/short-code/abc123/visits', '/server/1/short-code/abc123/visits',
], ],
[ [
fromPartial<ReachableServer>({ id: '3' }), '/foobar',
fromPartial<ShortUrl>({ shortCode: 'def456', domain: 'example.com' }), fromPartial<ShortUrl>({ shortCode: 'def456', domain: 'example.com' }),
'visits' as LinkSuffix, 'visits' as LinkSuffix,
'/server/3/short-code/def456/visits?domain=example.com', '/foobar/short-code/def456/visits?domain=example.com',
], ],
[ [
fromPartial<ReachableServer>({ id: '1' }), '/server/1',
fromPartial<ShortUrl>({ shortCode: 'abc123' }), fromPartial<ShortUrl>({ shortCode: 'abc123' }),
'edit' as LinkSuffix, 'edit' as LinkSuffix,
'/server/1/short-code/abc123/edit', '/server/1/short-code/abc123/edit',
], ],
[ [
fromPartial<ReachableServer>({ id: '3' }), '/server/3',
fromPartial<ShortUrl>({ shortCode: 'def456', domain: 'example.com' }), fromPartial<ShortUrl>({ shortCode: 'def456', domain: 'example.com' }),
'edit' as LinkSuffix, 'edit' as LinkSuffix,
'/server/3/short-code/def456/edit?domain=example.com', '/server/3/short-code/def456/edit?domain=example.com',
], ],
])('renders link with expected query when', (selectedServer, shortUrl, suffix, expectedLink) => { ])('renders link with expected query when', (routesPrefix, shortUrl, suffix, expectedLink) => {
render( render(
<MemoryRouter> <MemoryRouter>
<ShortUrlDetailLink selectedServer={selectedServer} shortUrl={shortUrl} suffix={suffix}> <RoutesPrefixProvider value={routesPrefix}>
Something <ShortUrlDetailLink shortUrl={shortUrl} suffix={suffix} asLink>
</ShortUrlDetailLink> Something
</ShortUrlDetailLink>
</RoutesPrefixProvider>
</MemoryRouter>, </MemoryRouter>,
); );
expect(screen.getByRole('link')).toHaveProperty('href', expect.stringContaining(expectedLink)); expect(screen.getByRole('link')).toHaveProperty('href', expect.stringContaining(expectedLink));

View file

@ -1,7 +1,7 @@
import { render, screen, waitFor } from '@testing-library/react'; import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ShlinkVisitsSummary } from '../../../src/api/types'; import type { ShlinkVisitsSummary } from '../../../src/api-contract';
import type { ShortUrl, ShortUrlMeta } from '../../../src/short-urls/data'; import type { ShortUrl, ShortUrlMeta } from '../../../src/short-urls/data';
import { ShortUrlStatus } from '../../../src/short-urls/helpers/ShortUrlStatus'; import { ShortUrlStatus } from '../../../src/short-urls/helpers/ShortUrlStatus';

View file

@ -3,18 +3,17 @@ import { fromPartial } from '@total-typescript/shoehorn';
import { addDays, formatISO, subDays } from 'date-fns'; import { addDays, formatISO, subDays } from 'date-fns';
import { last } from 'ramda'; import { last } from 'ramda';
import { MemoryRouter, useLocation } from 'react-router-dom'; import { MemoryRouter, useLocation } from 'react-router-dom';
import type { ReachableServer } from '../../../../src/servers/data';
import type { TimeoutToggle } from '../../../../src/utils/helpers/hooks';
import type { OptionalString } from '../../../../src/utils/utils';
import type { Settings } from '../../../src'; import type { Settings } from '../../../src';
import type { ShortUrl, ShortUrlMeta } from '../../../src/short-urls/data'; import type { ShortUrl, ShortUrlMeta } from '../../../src/short-urls/data';
import { ShortUrlsRow as createShortUrlsRow } from '../../../src/short-urls/helpers/ShortUrlsRow'; import { ShortUrlsRow as createShortUrlsRow } from '../../../src/short-urls/helpers/ShortUrlsRow';
import { now, parseDate } from '../../../src/utils/dates/helpers/date'; import { now, parseDate } from '../../../src/utils/dates/helpers/date';
import type { TimeoutToggle } from '../../../src/utils/helpers/hooks';
import { SettingsProvider } from '../../../src/utils/settings';
import { renderWithEvents } from '../../__helpers__/setUpTest'; import { renderWithEvents } from '../../__helpers__/setUpTest';
import { colorGeneratorMock } from '../../utils/services/__mocks__/ColorGenerator.mock'; import { colorGeneratorMock } from '../../utils/services/__mocks__/ColorGenerator.mock';
interface SetUpOptions { interface SetUpOptions {
title?: OptionalString; title?: string | null;
tags?: string[]; tags?: string[];
meta?: ShortUrlMeta; meta?: ShortUrlMeta;
settings?: Partial<Settings>; settings?: Partial<Settings>;
@ -28,7 +27,6 @@ vi.mock('react-router-dom', async () => ({
describe('<ShortUrlsRow />', () => { describe('<ShortUrlsRow />', () => {
const timeoutToggle = vi.fn(() => true); const timeoutToggle = vi.fn(() => true);
const useTimeoutToggle = vi.fn(() => [false, timeoutToggle]) as TimeoutToggle; const useTimeoutToggle = vi.fn(() => [false, timeoutToggle]) as TimeoutToggle;
const server = fromPartial<ReachableServer>({ url: 'https://s.test' });
const shortUrl: ShortUrl = { const shortUrl: ShortUrl = {
shortCode: 'abc123', shortCode: 'abc123',
shortUrl: 'https://s.test/abc123', shortUrl: 'https://s.test/abc123',
@ -54,16 +52,16 @@ describe('<ShortUrlsRow />', () => {
(useLocation as any).mockReturnValue({ search }); (useLocation as any).mockReturnValue({ search });
return renderWithEvents( return renderWithEvents(
<MemoryRouter> <MemoryRouter>
<table> <SettingsProvider value={fromPartial(settings)}>
<tbody> <table>
<ShortUrlsRow <tbody>
selectedServer={server} <ShortUrlsRow
shortUrl={{ ...shortUrl, title, tags, meta: { ...shortUrl.meta, ...meta } }} shortUrl={{ ...shortUrl, title, tags, meta: { ...shortUrl.meta, ...meta } }}
onTagClick={() => null} onTagClick={() => null}
settings={fromPartial(settings)} />
/> </tbody>
</tbody> </table>
</table> </SettingsProvider>
</MemoryRouter>, </MemoryRouter>,
); );
}; };

View file

@ -1,21 +1,19 @@
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import type { ReachableServer } from '../../../../src/servers/data';
import type { ShortUrl } from '../../../src/short-urls/data'; import type { ShortUrl } from '../../../src/short-urls/data';
import { ShortUrlsRowMenu as createShortUrlsRowMenu } from '../../../src/short-urls/helpers/ShortUrlsRowMenu'; import { ShortUrlsRowMenu as createShortUrlsRowMenu } from '../../../src/short-urls/helpers/ShortUrlsRowMenu';
import { renderWithEvents } from '../../__helpers__/setUpTest'; import { renderWithEvents } from '../../__helpers__/setUpTest';
describe('<ShortUrlsRowMenu />', () => { describe('<ShortUrlsRowMenu />', () => {
const ShortUrlsRowMenu = createShortUrlsRowMenu(() => <i>DeleteShortUrlModal</i>, () => <i>QrCodeModal</i>); const ShortUrlsRowMenu = createShortUrlsRowMenu(() => <i>DeleteShortUrlModal</i>, () => <i>QrCodeModal</i>);
const selectedServer = fromPartial<ReachableServer>({ id: 'abc123' });
const shortUrl = fromPartial<ShortUrl>({ const shortUrl = fromPartial<ShortUrl>({
shortCode: 'abc123', shortCode: 'abc123',
shortUrl: 'https://s.test/abc123', shortUrl: 'https://s.test/abc123',
}); });
const setUp = () => renderWithEvents( const setUp = () => renderWithEvents(
<MemoryRouter> <MemoryRouter>
<ShortUrlsRowMenu selectedServer={selectedServer} shortUrl={shortUrl} /> <ShortUrlsRowMenu shortUrl={shortUrl} />
</MemoryRouter>, </MemoryRouter>,
); );

View file

@ -1,6 +1,5 @@
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ShlinkApiClient } from '../../../../src/api/services/ShlinkApiClient'; import type { ShlinkApiClient } from '../../../src/api-contract';
import type { ShlinkState } from '../../../../src/container/types';
import type { ShortUrl } from '../../../src/short-urls/data'; import type { ShortUrl } from '../../../src/short-urls/data';
import { import {
createShortUrl as createShortUrlCreator, createShortUrl as createShortUrlCreator,
@ -51,11 +50,10 @@ describe('shortUrlCreationReducer', () => {
describe('createShortUrl', () => { describe('createShortUrl', () => {
const dispatch = vi.fn(); const dispatch = vi.fn();
const getState = () => fromPartial<ShlinkState>({});
it('calls API on success', async () => { it('calls API on success', async () => {
createShortUrlCall.mockResolvedValue(shortUrl); createShortUrlCall.mockResolvedValue(shortUrl);
await createShortUrl({ longUrl: 'foo' })(dispatch, getState, {}); await createShortUrl({ longUrl: 'foo' })(dispatch, vi.fn(), {});
expect(createShortUrlCall).toHaveBeenCalledTimes(1); expect(createShortUrlCall).toHaveBeenCalledTimes(1);
expect(dispatch).toHaveBeenCalledTimes(2); expect(dispatch).toHaveBeenCalledTimes(2);

View file

@ -1,6 +1,6 @@
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ShlinkApiClient } from '../../../../src/api/services/ShlinkApiClient'; import type { ShlinkApiClient } from '../../../../src/api/services/ShlinkApiClient';
import type { ProblemDetailsError } from '../../../src/api/types/errors'; import type { ProblemDetailsError } from '../../../src/api-contract';
import { import {
deleteShortUrl as deleteShortUrlCreator, deleteShortUrl as deleteShortUrlCreator,
shortUrlDeletionReducerCreator, shortUrlDeletionReducerCreator,

View file

@ -1,6 +1,6 @@
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ShlinkApiClient } from '../../../../src/api/services/ShlinkApiClient'; import type { ShlinkApiClient } from '../../../src/api-contract';
import type { ShlinkState } from '../../../../src/container/types'; import type { RootState } from '../../../src/container/store';
import type { ShortUrl } from '../../../src/short-urls/data'; import type { ShortUrl } from '../../../src/short-urls/data';
import { shortUrlDetailReducerCreator } from '../../../src/short-urls/reducers/shortUrlDetail'; import { shortUrlDetailReducerCreator } from '../../../src/short-urls/reducers/shortUrlDetail';
import type { ShortUrlsList } from '../../../src/short-urls/reducers/shortUrlsList'; import type { ShortUrlsList } from '../../../src/short-urls/reducers/shortUrlsList';
@ -40,7 +40,7 @@ describe('shortUrlDetailReducer', () => {
describe('getShortUrlDetail', () => { describe('getShortUrlDetail', () => {
const dispatchMock = vi.fn(); const dispatchMock = vi.fn();
const buildGetState = (shortUrlsList?: ShortUrlsList) => () => fromPartial<ShlinkState>({ shortUrlsList }); const buildGetState = (shortUrlsList?: ShortUrlsList) => () => fromPartial<RootState>({ shortUrlsList });
it.each([ it.each([
[undefined], [undefined],

View file

@ -1,6 +1,4 @@
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ShlinkState } from '../../../../src/container/types';
import type { SelectedServer } from '../../../../src/servers/data';
import type { ShortUrl } from '../../../src/short-urls/data'; import type { ShortUrl } from '../../../src/short-urls/data';
import { import {
editShortUrl as editShortUrlCreator, editShortUrl as editShortUrlCreator,
@ -45,12 +43,9 @@ describe('shortUrlEditionReducer', () => {
describe('editShortUrl', () => { describe('editShortUrl', () => {
const dispatch = vi.fn(); const dispatch = vi.fn();
const createGetState = (selectedServer: SelectedServer = null) => () => fromPartial<ShlinkState>({
selectedServer,
});
it.each([[undefined], [null], ['example.com']])('dispatches short URL on success', async (domain) => { it.each([[undefined], [null], ['example.com']])('dispatches short URL on success', async (domain) => {
await editShortUrl({ shortCode, domain, data: { longUrl } })(dispatch, createGetState(), {}); await editShortUrl({ shortCode, domain, data: { longUrl } })(dispatch, vi.fn(), {});
expect(buildShlinkApiClient).toHaveBeenCalledTimes(1); expect(buildShlinkApiClient).toHaveBeenCalledTimes(1);
expect(updateShortUrl).toHaveBeenCalledTimes(1); expect(updateShortUrl).toHaveBeenCalledTimes(1);

View file

@ -1,6 +1,5 @@
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ShlinkApiClient } from '../../../../src/api/services/ShlinkApiClient'; import type { ShlinkApiClient, ShlinkShortUrlsResponse } from '../../../src/api-contract';
import type { ShlinkShortUrlsResponse } from '../../../src/api/types';
import type { ShortUrl } from '../../../src/short-urls/data'; import type { ShortUrl } from '../../../src/short-urls/data';
import { createShortUrl as createShortUrlCreator } from '../../../src/short-urls/reducers/shortUrlCreation'; import { createShortUrl as createShortUrlCreator } from '../../../src/short-urls/reducers/shortUrlCreation';
import { shortUrlDeleted } from '../../../src/short-urls/reducers/shortUrlDeletion'; import { shortUrlDeleted } from '../../../src/short-urls/reducers/shortUrlDeletion';
@ -187,7 +186,7 @@ describe('shortUrlsListReducer', () => {
describe('listShortUrls', () => { describe('listShortUrls', () => {
const dispatch = vi.fn(); const dispatch = vi.fn();
const getState = vi.fn().mockReturnValue({ selectedServer: {} }); const getState = vi.fn();
it('dispatches proper actions if API client request succeeds', async () => { it('dispatches proper actions if API client request succeeds', async () => {
listShortUrlsMock.mockResolvedValue({}); listShortUrlsMock.mockResolvedValue({});

View file

@ -5,20 +5,22 @@ import type { MercureBoundProps } from '../../src/mercure/helpers/boundToMercure
import type { TagsList } from '../../src/tags/reducers/tagsList'; import type { TagsList } from '../../src/tags/reducers/tagsList';
import type { TagsListProps } from '../../src/tags/TagsList'; import type { TagsListProps } from '../../src/tags/TagsList';
import { TagsList as createTagsList } from '../../src/tags/TagsList'; import { TagsList as createTagsList } from '../../src/tags/TagsList';
import { SettingsProvider } from '../../src/utils/settings';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
describe('<TagsList />', () => { describe('<TagsList />', () => {
const filterTags = vi.fn(); const filterTags = vi.fn();
const TagsListComp = createTagsList(({ sortedTags }) => <>TagsTable ({sortedTags.map((t) => t.visits).join(',')})</>); const TagsListComp = createTagsList(({ sortedTags }) => <>TagsTable ({sortedTags.map((t) => t.visits).join(',')})</>);
const setUp = (tagsList: Partial<TagsList>, excludeBots = false) => renderWithEvents( const setUp = (tagsList: Partial<TagsList>, excludeBots = false) => renderWithEvents(
<TagsListComp <SettingsProvider value={fromPartial({ visits: { excludeBots } })}>
{...fromPartial<TagsListProps>({})} <TagsListComp
{...fromPartial<MercureBoundProps>({ mercureInfo: {} })} {...fromPartial<TagsListProps>({})}
forceListTags={identity} {...fromPartial<MercureBoundProps>({ mercureInfo: {} })}
filterTags={filterTags} forceListTags={identity}
tagsList={fromPartial(tagsList)} filterTags={filterTags}
settings={fromPartial({ visits: { excludeBots } })} tagsList={fromPartial(tagsList)}
/>, />
</SettingsProvider>,
); );
it('shows a loading message when tags are being loaded', () => { it('shows a loading message when tags are being loaded', () => {

View file

@ -19,7 +19,6 @@ describe('<TagsTable />', () => {
return renderWithEvents( return renderWithEvents(
<TagsTable <TagsTable
sortedTags={sortedTags.map((tag) => fromPartial({ tag }))} sortedTags={sortedTags.map((tag) => fromPartial({ tag }))}
selectedServer={fromPartial({})}
currentOrder={{}} currentOrder={{}}
orderByColumn={() => orderByColumn} orderByColumn={() => orderByColumn}
/>, />,

View file

@ -1,7 +1,7 @@
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { TagsTableRow as createTagsTableRow } from '../../src/tags/TagsTableRow'; import { TagsTableRow as createTagsTableRow } from '../../src/tags/TagsTableRow';
import { RoutesPrefixProvider } from '../../src/utils/routesPrefix';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
import { colorGeneratorMock } from '../utils/services/__mocks__/ColorGenerator.mock'; import { colorGeneratorMock } from '../utils/services/__mocks__/ColorGenerator.mock';
@ -13,14 +13,15 @@ describe('<TagsTableRow />', () => {
); );
const setUp = (tagStats?: { visits?: number; shortUrls?: number }) => renderWithEvents( const setUp = (tagStats?: { visits?: number; shortUrls?: number }) => renderWithEvents(
<MemoryRouter> <MemoryRouter>
<table> <RoutesPrefixProvider value="/server/abc123">
<tbody> <table>
<TagsTableRow <tbody>
tag={{ tag: 'foo&bar', visits: tagStats?.visits ?? 0, shortUrls: tagStats?.shortUrls ?? 0 }} <TagsTableRow
selectedServer={fromPartial({ id: 'abc123' })} tag={{ tag: 'foo&bar', visits: tagStats?.visits ?? 0, shortUrls: tagStats?.shortUrls ?? 0 }}
/> />
</tbody> </tbody>
</table> </table>
</RoutesPrefixProvider>
</MemoryRouter>, </MemoryRouter>,
); );

View file

@ -2,6 +2,7 @@ import { screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { TagsSelector as createTagsSelector } from '../../../src/tags/helpers/TagsSelector'; import { TagsSelector as createTagsSelector } from '../../../src/tags/helpers/TagsSelector';
import type { TagsList } from '../../../src/tags/reducers/tagsList'; import type { TagsList } from '../../../src/tags/reducers/tagsList';
import { SettingsProvider } from '../../../src/utils/settings';
import { renderWithEvents } from '../../__helpers__/setUpTest'; import { renderWithEvents } from '../../__helpers__/setUpTest';
import { colorGeneratorMock } from '../../utils/services/__mocks__/ColorGenerator.mock'; import { colorGeneratorMock } from '../../utils/services/__mocks__/ColorGenerator.mock';
@ -11,13 +12,14 @@ describe('<TagsSelector />', () => {
const tags = ['foo', 'bar']; const tags = ['foo', 'bar'];
const tagsList = fromPartial<TagsList>({ tags: [...tags, 'baz'] }); const tagsList = fromPartial<TagsList>({ tags: [...tags, 'baz'] });
const setUp = () => renderWithEvents( const setUp = () => renderWithEvents(
<TagsSelector <SettingsProvider value={fromPartial({})}>
selectedTags={tags} <TagsSelector
tagsList={tagsList} selectedTags={tags}
settings={fromPartial({})} tagsList={tagsList}
listTags={vi.fn()} listTags={vi.fn()}
onChange={onChange} onChange={onChange}
/>, />
</SettingsProvider>,
); );
it('has an input for tags', () => { it('has an input for tags', () => {

View file

@ -1,6 +1,5 @@
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ShlinkApiClient } from '../../../../src/api/services/ShlinkApiClient'; import type { ShlinkApiClient } from '../../../src/api-contract';
import type { ShlinkState } from '../../../../src/container/types';
import { tagDeleted, tagDeleteReducerCreator } from '../../../src/tags/reducers/tagDelete'; import { tagDeleted, tagDeleteReducerCreator } from '../../../src/tags/reducers/tagDelete';
describe('tagDeleteReducer', () => { describe('tagDeleteReducer', () => {
@ -42,13 +41,12 @@ describe('tagDeleteReducer', () => {
describe('deleteTag', () => { describe('deleteTag', () => {
const dispatch = vi.fn(); const dispatch = vi.fn();
const getState = () => fromPartial<ShlinkState>({});
it('calls API on success', async () => { it('calls API on success', async () => {
const tag = 'foo'; const tag = 'foo';
deleteTagsCall.mockResolvedValue(undefined); deleteTagsCall.mockResolvedValue(undefined);
await deleteTag(tag)(dispatch, getState, {}); await deleteTag(tag)(dispatch, vi.fn(), {});
expect(deleteTagsCall).toHaveBeenCalledTimes(1); expect(deleteTagsCall).toHaveBeenCalledTimes(1);
expect(deleteTagsCall).toHaveBeenNthCalledWith(1, [tag]); expect(deleteTagsCall).toHaveBeenNthCalledWith(1, [tag]);

View file

@ -1,6 +1,5 @@
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ShlinkApiClient } from '../../../../src/api/services/ShlinkApiClient'; import type { ShlinkApiClient } from '../../../src/api-contract';
import type { ShlinkState } from '../../../../src/container/types';
import { editTag as editTagCreator, tagEdited, tagEditReducerCreator } from '../../../src/tags/reducers/tagEdit'; import { editTag as editTagCreator, tagEdited, tagEditReducerCreator } from '../../../src/tags/reducers/tagEdit';
import type { ColorGenerator } from '../../../src/utils/services/ColorGenerator'; import type { ColorGenerator } from '../../../src/utils/services/ColorGenerator';
@ -51,12 +50,11 @@ describe('tagEditReducer', () => {
describe('editTag', () => { describe('editTag', () => {
const dispatch = vi.fn(); const dispatch = vi.fn();
const getState = () => fromPartial<ShlinkState>({});
it('calls API on success', async () => { it('calls API on success', async () => {
editTagCall.mockResolvedValue(undefined); editTagCall.mockResolvedValue(undefined);
await editTag({ oldName, newName, color })(dispatch, getState, {}); await editTag({ oldName, newName, color })(dispatch, vi.fn(), {});
expect(editTagCall).toHaveBeenCalledTimes(1); expect(editTagCall).toHaveBeenCalledTimes(1);
expect(editTagCall).toHaveBeenCalledWith(oldName, newName); expect(editTagCall).toHaveBeenCalledWith(oldName, newName);

View file

@ -1,5 +1,5 @@
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ShlinkState } from '../../../../src/container/types'; import type { RootState } from '../../../src/container/store';
import type { ShortUrl } from '../../../src/short-urls/data'; import type { ShortUrl } from '../../../src/short-urls/data';
import { createShortUrl as createShortUrlCreator } from '../../../src/short-urls/reducers/shortUrlCreation'; import { createShortUrl as createShortUrlCreator } from '../../../src/short-urls/reducers/shortUrlCreation';
import { tagDeleted } from '../../../src/tags/reducers/tagDelete'; import { tagDeleted } from '../../../src/tags/reducers/tagDelete';
@ -195,11 +195,11 @@ describe('tagsListReducer', () => {
describe('listTags', () => { describe('listTags', () => {
const dispatch = vi.fn(); const dispatch = vi.fn();
const getState = vi.fn(() => fromPartial<ShlinkState>({})); const getState = vi.fn(() => fromPartial<RootState>({}));
const listTagsMock = vi.fn(); const listTagsMock = vi.fn();
const assertNoAction = async (tagsList: TagsList) => { const assertNoAction = async (tagsList: TagsList) => {
getState.mockReturnValue(fromPartial<ShlinkState>({ tagsList })); getState.mockReturnValue(fromPartial<RootState>({ tagsList }));
await listTagsCreator(buildShlinkApiClient, false)()(dispatch, getState, {}); await listTagsCreator(buildShlinkApiClient, false)()(dispatch, getState, {});
@ -218,7 +218,7 @@ describe('tagsListReducer', () => {
const tags = ['foo', 'bar', 'baz']; const tags = ['foo', 'bar', 'baz'];
listTagsMock.mockResolvedValue({ tags, stats: [] }); listTagsMock.mockResolvedValue({ tags, stats: [] });
buildShlinkApiClient.mockReturnValue({ listTags: listTagsMock }); buildShlinkApiClient.mockReturnValue({ tagsStats: listTagsMock });
await listTags()(dispatch, getState, {}); await listTags()(dispatch, getState, {});

View file

@ -1,23 +1,9 @@
import { addDays, formatISO, subDays } from 'date-fns'; import { addDays, formatISO, subDays } from 'date-fns';
import { formatDate, formatIsoDate, isBeforeOrEqual, isBetween, parseDate } from '../../../../src/utils/dates/helpers/date'; import { formatIsoDate, isBeforeOrEqual, isBetween, parseDate } from '../../../../src/utils/dates/helpers/date';
describe('date', () => { describe('date', () => {
const now = new Date(); const now = new Date();
describe('formatDate', () => {
it.each([
[parseDate('2020-03-05 10:00:10', 'yyyy-MM-dd HH:mm:ss'), 'dd/MM/yyyy', '05/03/2020'],
[parseDate('2020-03-05 10:00:10', 'yyyy-MM-dd HH:mm:ss'), 'yyyy-MM', '2020-03'],
[parseDate('2020-03-05 10:00:10', 'yyyy-MM-dd HH:mm:ss'), undefined, '2020-03-05'],
['2020-03-05 10:00:10', 'dd-MM-yyyy', '2020-03-05 10:00:10'],
['2020-03-05 10:00:10', undefined, '2020-03-05 10:00:10'],
[undefined, undefined, undefined],
[null, undefined, null],
])('formats date as expected', (date, format, expected) => {
expect(formatDate(format)(date)).toEqual(expected);
});
});
describe('formatIsoDate', () => { describe('formatIsoDate', () => {
it.each([ it.each([
[ [

View file

@ -1,7 +1,7 @@
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import type { LocalStorage } from '../../../../src/utils/services/LocalStorage';
import { MAIN_COLOR } from '../../../../src/utils/theme'; import { MAIN_COLOR } from '../../../../src/utils/theme';
import { ColorGenerator } from '../../../src/utils/services/ColorGenerator'; import { ColorGenerator } from '../../../src/utils/services/ColorGenerator';
import type { LocalStorage } from '../../../src/utils/services/LocalStorage';
describe('ColorGenerator', () => { describe('ColorGenerator', () => {
let colorGenerator: ColorGenerator; let colorGenerator: ColorGenerator;

View file

@ -1,5 +1,5 @@
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import type { OrderDir } from '../../../../shlink-frontend-kit/src/ordering/ordering'; import type { OrderDir } from '../../../../shlink-frontend-kit/src';
import { TableOrderIcon } from '../../../src/utils/table/TableOrderIcon'; import { TableOrderIcon } from '../../../src/utils/table/TableOrderIcon';
describe('<TableOrderIcon />', () => { describe('<TableOrderIcon />', () => {

View file

@ -3,6 +3,7 @@ import { fromPartial } from '@total-typescript/shoehorn';
import { formatISO } from 'date-fns'; import { formatISO } from 'date-fns';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import type { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub'; import type { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
import { SettingsProvider } from '../../src/utils/settings';
import { DomainVisits as createDomainVisits } from '../../src/visits/DomainVisits'; import { DomainVisits as createDomainVisits } from '../../src/visits/DomainVisits';
import type { DomainVisits } from '../../src/visits/reducers/domainVisits'; import type { DomainVisits } from '../../src/visits/reducers/domainVisits';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
@ -20,13 +21,14 @@ describe('<DomainVisits />', () => {
const DomainVisits = createDomainVisits(fromPartial({ exportVisits })); const DomainVisits = createDomainVisits(fromPartial({ exportVisits }));
const setUp = () => renderWithEvents( const setUp = () => renderWithEvents(
<MemoryRouter> <MemoryRouter>
<DomainVisits <SettingsProvider value={fromPartial({})}>
{...fromPartial<MercureBoundProps>({ mercureInfo: {} })} <DomainVisits
getDomainVisits={getDomainVisits} {...fromPartial<MercureBoundProps>({ mercureInfo: {} })}
cancelGetDomainVisits={cancelGetDomainVisits} getDomainVisits={getDomainVisits}
domainVisits={domainVisits} cancelGetDomainVisits={cancelGetDomainVisits}
settings={fromPartial({})} domainVisits={domainVisits}
/> />
</SettingsProvider>
</MemoryRouter>, </MemoryRouter>,
); );

View file

@ -3,6 +3,7 @@ import { fromPartial } from '@total-typescript/shoehorn';
import { formatISO } from 'date-fns'; import { formatISO } from 'date-fns';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import type { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub'; import type { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
import { SettingsProvider } from '../../src/utils/settings';
import { NonOrphanVisits as createNonOrphanVisits } from '../../src/visits/NonOrphanVisits'; import { NonOrphanVisits as createNonOrphanVisits } from '../../src/visits/NonOrphanVisits';
import type { VisitsInfo } from '../../src/visits/reducers/types'; import type { VisitsInfo } from '../../src/visits/reducers/types';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
@ -15,13 +16,14 @@ describe('<NonOrphanVisits />', () => {
const NonOrphanVisits = createNonOrphanVisits(fromPartial({ exportVisits })); const NonOrphanVisits = createNonOrphanVisits(fromPartial({ exportVisits }));
const setUp = () => renderWithEvents( const setUp = () => renderWithEvents(
<MemoryRouter> <MemoryRouter>
<NonOrphanVisits <SettingsProvider value={fromPartial({})}>
{...fromPartial<MercureBoundProps>({ mercureInfo: {} })} <NonOrphanVisits
getNonOrphanVisits={getNonOrphanVisits} {...fromPartial<MercureBoundProps>({ mercureInfo: {} })}
cancelGetNonOrphanVisits={cancelGetNonOrphanVisits} getNonOrphanVisits={getNonOrphanVisits}
nonOrphanVisits={nonOrphanVisits} cancelGetNonOrphanVisits={cancelGetNonOrphanVisits}
settings={fromPartial({})} nonOrphanVisits={nonOrphanVisits}
/> />
</SettingsProvider>
</MemoryRouter>, </MemoryRouter>,
); );

View file

@ -3,6 +3,7 @@ import { fromPartial } from '@total-typescript/shoehorn';
import { formatISO } from 'date-fns'; import { formatISO } from 'date-fns';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import type { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub'; import type { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
import { SettingsProvider } from '../../src/utils/settings';
import { OrphanVisits as createOrphanVisits } from '../../src/visits/OrphanVisits'; import { OrphanVisits as createOrphanVisits } from '../../src/visits/OrphanVisits';
import type { VisitsInfo } from '../../src/visits/reducers/types'; import type { VisitsInfo } from '../../src/visits/reducers/types';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
@ -14,13 +15,14 @@ describe('<OrphanVisits />', () => {
const OrphanVisits = createOrphanVisits(fromPartial({ exportVisits })); const OrphanVisits = createOrphanVisits(fromPartial({ exportVisits }));
const setUp = () => renderWithEvents( const setUp = () => renderWithEvents(
<MemoryRouter> <MemoryRouter>
<OrphanVisits <SettingsProvider value={fromPartial({})}>
{...fromPartial<MercureBoundProps>({ mercureInfo: {} })} <OrphanVisits
getOrphanVisits={getOrphanVisits} {...fromPartial<MercureBoundProps>({ mercureInfo: {} })}
orphanVisits={orphanVisits} getOrphanVisits={getOrphanVisits}
cancelGetOrphanVisits={vi.fn()} orphanVisits={orphanVisits}
settings={fromPartial({})} cancelGetOrphanVisits={vi.fn()}
/> />
</SettingsProvider>
</MemoryRouter>, </MemoryRouter>,
); );

View file

@ -4,6 +4,7 @@ import { formatISO } from 'date-fns';
import { identity } from 'ramda'; import { identity } from 'ramda';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import type { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub'; import type { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
import { SettingsProvider } from '../../src/utils/settings';
import type { ShortUrlVisits as ShortUrlVisitsState } from '../../src/visits/reducers/shortUrlVisits'; import type { ShortUrlVisits as ShortUrlVisitsState } from '../../src/visits/reducers/shortUrlVisits';
import type { ShortUrlVisitsProps } from '../../src/visits/ShortUrlVisits'; import type { ShortUrlVisitsProps } from '../../src/visits/ShortUrlVisits';
import { ShortUrlVisits as createShortUrlVisits } from '../../src/visits/ShortUrlVisits'; import { ShortUrlVisits as createShortUrlVisits } from '../../src/visits/ShortUrlVisits';
@ -16,16 +17,17 @@ describe('<ShortUrlVisits />', () => {
const ShortUrlVisits = createShortUrlVisits(fromPartial({ exportVisits })); const ShortUrlVisits = createShortUrlVisits(fromPartial({ exportVisits }));
const setUp = () => renderWithEvents( const setUp = () => renderWithEvents(
<MemoryRouter> <MemoryRouter>
<ShortUrlVisits <SettingsProvider value={fromPartial({})}>
{...fromPartial<ShortUrlVisitsProps>({})} <ShortUrlVisits
{...fromPartial<MercureBoundProps>({ mercureInfo: {} })} {...fromPartial<ShortUrlVisitsProps>({})}
getShortUrlDetail={identity} {...fromPartial<MercureBoundProps>({ mercureInfo: {} })}
getShortUrlVisits={getShortUrlVisitsMock} getShortUrlDetail={identity}
shortUrlVisits={shortUrlVisits} getShortUrlVisits={getShortUrlVisitsMock}
shortUrlDetail={fromPartial({})} shortUrlVisits={shortUrlVisits}
settings={fromPartial({})} shortUrlDetail={fromPartial({})}
cancelGetShortUrlVisits={() => {}} cancelGetShortUrlVisits={() => {}}
/> />
</SettingsProvider>
</MemoryRouter>, </MemoryRouter>,
); );

View file

@ -3,6 +3,7 @@ import { fromPartial } from '@total-typescript/shoehorn';
import { formatISO } from 'date-fns'; import { formatISO } from 'date-fns';
import { MemoryRouter } from 'react-router'; import { MemoryRouter } from 'react-router';
import type { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub'; import type { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
import { SettingsProvider } from '../../src/utils/settings';
import type { TagVisits as TagVisitsStats } from '../../src/visits/reducers/tagVisits'; import type { TagVisits as TagVisitsStats } from '../../src/visits/reducers/tagVisits';
import type { TagVisitsProps } from '../../src/visits/TagVisits'; import type { TagVisitsProps } from '../../src/visits/TagVisits';
import { TagVisits as createTagVisits } from '../../src/visits/TagVisits'; import { TagVisits as createTagVisits } from '../../src/visits/TagVisits';
@ -23,14 +24,15 @@ describe('<TagVisits />', () => {
); );
const setUp = () => renderWithEvents( const setUp = () => renderWithEvents(
<MemoryRouter> <MemoryRouter>
<TagVisits <SettingsProvider value={fromPartial({})}>
{...fromPartial<TagVisitsProps>({})} <TagVisits
{...fromPartial<MercureBoundProps>({ mercureInfo: {} })} {...fromPartial<TagVisitsProps>({})}
getTagVisits={getTagVisitsMock} {...fromPartial<MercureBoundProps>({ mercureInfo: {} })}
tagVisits={tagVisits} getTagVisits={getTagVisitsMock}
settings={fromPartial({})} tagVisits={tagVisits}
cancelGetTagVisits={() => {}} cancelGetTagVisits={() => {}}
/> />
</SettingsProvider>
</MemoryRouter>, </MemoryRouter>,
); );

View file

@ -3,6 +3,7 @@ import { fromPartial } from '@total-typescript/shoehorn';
import { createMemoryHistory } from 'history'; import { createMemoryHistory } from 'history';
import { Router } from 'react-router-dom'; import { Router } from 'react-router-dom';
import { rangeOf } from '../../src/utils/helpers'; import { rangeOf } from '../../src/utils/helpers';
import { SettingsProvider } from '../../src/utils/settings';
import type { VisitsInfo } from '../../src/visits/reducers/types'; import type { VisitsInfo } from '../../src/visits/reducers/types';
import type { Visit } from '../../src/visits/types'; import type { Visit } from '../../src/visits/types';
import { VisitsStats } from '../../src/visits/VisitsStats'; import { VisitsStats } from '../../src/visits/VisitsStats';
@ -20,13 +21,14 @@ describe('<VisitsStats />', () => {
history, history,
...renderWithEvents( ...renderWithEvents(
<Router location={history.location} navigator={history}> <Router location={history.location} navigator={history}>
<VisitsStats <SettingsProvider value={fromPartial({})}>
getVisits={getVisitsMock} <VisitsStats
visitsInfo={fromPartial(visitsInfo)} getVisits={getVisitsMock}
cancelGetVisits={() => {}} visitsInfo={fromPartial(visitsInfo)}
settings={fromPartial({})} cancelGetVisits={() => {}}
exportCsv={exportCsv} exportCsv={exportCsv}
/> />
</SettingsProvider>
</Router>, </Router>,
), ),
}; };

View file

@ -1,9 +1,8 @@
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { addDays, formatISO, subDays } from 'date-fns'; 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 { rangeOf } from '../../../../src/utils/utils';
import type { ShlinkVisits } from '../../../src/api/types'; import type { ShlinkApiClient, ShlinkVisits } from '../../../src/api-contract';
import type { RootState } from '../../../src/container/store';
import type { ShortUrl } from '../../../src/short-urls/data'; import type { ShortUrl } from '../../../src/short-urls/data';
import { formatIsoDate } from '../../../src/utils/dates/helpers/date'; import { formatIsoDate } from '../../../src/utils/dates/helpers/date';
import type { DateInterval } from '../../../src/utils/dates/helpers/dateIntervals'; import type { DateInterval } from '../../../src/utils/dates/helpers/dateIntervals';
@ -151,7 +150,7 @@ describe('domainVisitsReducer', () => {
describe('getDomainVisits', () => { describe('getDomainVisits', () => {
const dispatchMock = vi.fn(); const dispatchMock = vi.fn();
const getState = () => fromPartial<ShlinkState>({ const getState = () => fromPartial<RootState>({
domainVisits: { cancelLoad: false }, domainVisits: { cancelLoad: false },
}); });
const domain = 'foo.com'; const domain = 'foo.com';

View file

@ -1,9 +1,8 @@
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { addDays, formatISO, subDays } from 'date-fns'; 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 { rangeOf } from '../../../../src/utils/utils';
import type { ShlinkVisits } from '../../../src/api/types'; import type { ShlinkApiClient, ShlinkVisits } from '../../../src/api-contract';
import type { RootState } from '../../../src/container/store';
import { formatIsoDate } from '../../../src/utils/dates/helpers/date'; import { formatIsoDate } from '../../../src/utils/dates/helpers/date';
import type { DateInterval } from '../../../src/utils/dates/helpers/dateIntervals'; import type { DateInterval } from '../../../src/utils/dates/helpers/dateIntervals';
import { import {
@ -118,7 +117,7 @@ describe('nonOrphanVisitsReducer', () => {
describe('getNonOrphanVisits', () => { describe('getNonOrphanVisits', () => {
const dispatchMock = vi.fn(); const dispatchMock = vi.fn();
const getState = () => fromPartial<ShlinkState>({ const getState = () => fromPartial<RootState>({
orphanVisits: { cancelLoad: false }, orphanVisits: { cancelLoad: false },
}); });

View file

@ -1,9 +1,8 @@
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { addDays, formatISO, subDays } from 'date-fns'; 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 { rangeOf } from '../../../../src/utils/utils';
import type { ShlinkVisits } from '../../../src/api/types'; import type { ShlinkApiClient, ShlinkVisits } from '../../../src/api-contract';
import type { RootState } from '../../../src/container/store';
import { formatIsoDate } from '../../../src/utils/dates/helpers/date'; import { formatIsoDate } from '../../../src/utils/dates/helpers/date';
import type { DateInterval } from '../../../src/utils/dates/helpers/dateIntervals'; import type { DateInterval } from '../../../src/utils/dates/helpers/dateIntervals';
import { import {
@ -118,7 +117,7 @@ describe('orphanVisitsReducer', () => {
describe('getOrphanVisits', () => { describe('getOrphanVisits', () => {
const dispatchMock = vi.fn(); const dispatchMock = vi.fn();
const getState = () => fromPartial<ShlinkState>({ const getState = () => fromPartial<RootState>({
orphanVisits: { cancelLoad: false }, orphanVisits: { cancelLoad: false },
}); });

View file

@ -1,9 +1,8 @@
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { addDays, formatISO, subDays } from 'date-fns'; 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 { rangeOf } from '../../../../src/utils/utils';
import type { ShlinkVisits } from '../../../src/api/types'; import type { ShlinkApiClient, ShlinkVisits } from '../../../src/api-contract';
import type { RootState } from '../../../src/container/store';
import { formatIsoDate } from '../../../src/utils/dates/helpers/date'; import { formatIsoDate } from '../../../src/utils/dates/helpers/date';
import type { DateInterval } from '../../../src/utils/dates/helpers/dateIntervals'; import type { DateInterval } from '../../../src/utils/dates/helpers/dateIntervals';
import type { import type {
@ -142,7 +141,7 @@ describe('shortUrlVisitsReducer', () => {
describe('getShortUrlVisits', () => { describe('getShortUrlVisits', () => {
const dispatchMock = vi.fn(); const dispatchMock = vi.fn();
const getState = () => fromPartial<ShlinkState>({ const getState = () => fromPartial<RootState>({
shortUrlVisits: { cancelLoad: false }, shortUrlVisits: { cancelLoad: false },
}); });

View file

@ -1,9 +1,8 @@
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { addDays, formatISO, subDays } from 'date-fns'; 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 { rangeOf } from '../../../../src/utils/utils';
import type { ShlinkVisits } from '../../../src/api/types'; import type { ShlinkApiClient, ShlinkVisits } from '../../../src/api-contract';
import type { RootState } from '../../../src/container/store';
import { formatIsoDate } from '../../../src/utils/dates/helpers/date'; import { formatIsoDate } from '../../../src/utils/dates/helpers/date';
import type { DateInterval } from '../../../src/utils/dates/helpers/dateIntervals'; import type { DateInterval } from '../../../src/utils/dates/helpers/dateIntervals';
import type { import type {
@ -142,7 +141,7 @@ describe('tagVisitsReducer', () => {
describe('getTagVisits', () => { describe('getTagVisits', () => {
const dispatchMock = vi.fn(); const dispatchMock = vi.fn();
const getState = () => fromPartial<ShlinkState>({ const getState = () => fromPartial<RootState>({
tagVisits: { cancelLoad: false }, tagVisits: { cancelLoad: false },
}); });
const tag = 'foo'; const tag = 'foo';

View file

@ -1,7 +1,6 @@
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ShlinkApiClient } from '../../../../src/api/services/ShlinkApiClient'; import type { ShlinkApiClient, ShlinkVisitsOverview } from '../../../src/api-contract';
import type { ShlinkState } from '../../../../src/container/types'; import type { RootState } from '../../../src/container/store';
import type { ShlinkVisitsOverview } from '../../../src/api/types';
import { createNewVisits } from '../../../src/visits/reducers/visitCreation'; import { createNewVisits } from '../../../src/visits/reducers/visitCreation';
import type { import type {
PartialVisitsSummary, PartialVisitsSummary,
@ -25,7 +24,7 @@ describe('visitsOverviewReducer', () => {
it('returns loading on GET_OVERVIEW_START', () => { it('returns loading on GET_OVERVIEW_START', () => {
const { loading } = reducer( const { loading } = reducer(
state({ loading: false, error: false }), state({ loading: false, error: false }),
loadVisitsOverview.pending(''), loadVisitsOverview.pending('', {}),
); );
expect(loading).toEqual(true); expect(loading).toEqual(true);
@ -34,7 +33,7 @@ describe('visitsOverviewReducer', () => {
it('stops loading and returns error on GET_OVERVIEW_ERROR', () => { it('stops loading and returns error on GET_OVERVIEW_ERROR', () => {
const { loading, error } = reducer( const { loading, error } = reducer(
state({ loading: true, error: false }), state({ loading: true, error: false }),
loadVisitsOverview.rejected(null, ''), loadVisitsOverview.rejected(null, '', {}),
); );
expect(loading).toEqual(false); expect(loading).toEqual(false);
@ -44,7 +43,7 @@ describe('visitsOverviewReducer', () => {
it('return visits overview on GET_OVERVIEW', () => { it('return visits overview on GET_OVERVIEW', () => {
const action = loadVisitsOverview.fulfilled(fromPartial({ const action = loadVisitsOverview.fulfilled(fromPartial({
nonOrphanVisits: { total: 100 }, nonOrphanVisits: { total: 100 },
}), 'requestId'); }), 'requestId', {});
const { loading, error, nonOrphanVisits } = reducer(state({ loading: true, error: false }), action); const { loading, error, nonOrphanVisits } = reducer(state({ loading: true, error: false }), action);
expect(loading).toEqual(false); expect(loading).toEqual(false);
@ -127,7 +126,7 @@ describe('visitsOverviewReducer', () => {
describe('loadVisitsOverview', () => { describe('loadVisitsOverview', () => {
const dispatchMock = vi.fn(); const dispatchMock = vi.fn();
const getState = () => fromPartial<ShlinkState>({}); const getState = () => fromPartial<RootState>({});
it.each([ it.each([
[ [
@ -155,7 +154,7 @@ describe('visitsOverviewReducer', () => {
const resolvedOverview = fromPartial<ShlinkVisitsOverview>(serverResult); const resolvedOverview = fromPartial<ShlinkVisitsOverview>(serverResult);
getVisitsOverview.mockResolvedValue(resolvedOverview); getVisitsOverview.mockResolvedValue(resolvedOverview);
await loadVisitsOverview()(dispatchMock, getState, {}); await loadVisitsOverview(buildApiClientMock)(dispatchMock, getState, {});
expect(dispatchMock).toHaveBeenCalledTimes(2); expect(dispatchMock).toHaveBeenCalledTimes(2);
expect(dispatchMock).toHaveBeenNthCalledWith(2, expect.objectContaining({ payload: dispatchedPayload })); expect(dispatchMock).toHaveBeenNthCalledWith(2, expect.objectContaining({ payload: dispatchedPayload }));

View file

@ -1,5 +1,5 @@
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ShlinkVisitsParams } from '../../../src/api/types'; import type { ShlinkVisitsParams } from '../../../src/api-contract';
import { formatIsoDate, parseDate } from '../../../src/utils/dates/helpers/date'; import { formatIsoDate, parseDate } from '../../../src/utils/dates/helpers/date';
import type { CreateVisit, OrphanVisit, VisitsParams } from '../../../src/visits/types'; import type { CreateVisit, OrphanVisit, VisitsParams } from '../../../src/visits/types';
import type { GroupedNewVisits } from '../../../src/visits/types/helpers'; import type { GroupedNewVisits } from '../../../src/visits/types/helpers';

View file

@ -1,4 +1,4 @@
import { pipe } from 'ramda'; import { pipe, range } from 'ramda';
import type { SyntheticEvent } from 'react'; import type { SyntheticEvent } from 'react';
type Optional<T> = T | null | undefined; type Optional<T> = T | null | undefined;
@ -9,3 +9,6 @@ export const handleEventPreventingDefault = <T>(handler: () => T) => pipe(
(e: SyntheticEvent) => e.preventDefault(), (e: SyntheticEvent) => e.preventDefault(),
handler, handler,
); );
export const rangeOf = <T>(size: number, mappingFn: (value: number) => T, startAt = 1): T[] =>
range(startAt, size + 1).map(mappingFn);