mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-03-15 03:53:51 +00:00
Move shlink-web-component tests to their own folder
This commit is contained in:
49
shlink-web-component/test/visits/DomainVisits.test.tsx
Normal file
49
shlink-web-component/test/visits/DomainVisits.test.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { formatISO } from 'date-fns';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import type { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
|
||||
import { DomainVisits as createDomainVisits } from '../../src/visits/DomainVisits';
|
||||
import type { DomainVisits } from '../../src/visits/reducers/domainVisits';
|
||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||
|
||||
vi.mock('react-router-dom', async () => ({
|
||||
...(await vi.importActual<any>('react-router-dom')),
|
||||
useParams: vi.fn().mockReturnValue({ domain: 'foo.com_DEFAULT' }),
|
||||
}));
|
||||
|
||||
describe('<DomainVisits />', () => {
|
||||
const exportVisits = vi.fn();
|
||||
const getDomainVisits = vi.fn();
|
||||
const cancelGetDomainVisits = vi.fn();
|
||||
const domainVisits = fromPartial<DomainVisits>({ visits: [{ date: formatISO(new Date()) }] });
|
||||
const DomainVisits = createDomainVisits(fromPartial({ exportVisits }));
|
||||
const setUp = () => renderWithEvents(
|
||||
<MemoryRouter>
|
||||
<DomainVisits
|
||||
{...fromPartial<MercureBoundProps>({ mercureInfo: {} })}
|
||||
getDomainVisits={getDomainVisits}
|
||||
cancelGetDomainVisits={cancelGetDomainVisits}
|
||||
domainVisits={domainVisits}
|
||||
settings={fromPartial({})}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
it('wraps visits stats and header', () => {
|
||||
setUp();
|
||||
expect(screen.getByRole('heading', { name: '"foo.com" visits' })).toBeInTheDocument();
|
||||
expect(getDomainVisits).toHaveBeenCalledWith(expect.objectContaining({ domain: 'DEFAULT' }));
|
||||
});
|
||||
|
||||
it('exports visits when clicking the button', async () => {
|
||||
const { user } = setUp();
|
||||
const btn = screen.getByRole('button', { name: 'Export (1)' });
|
||||
|
||||
expect(exportVisits).not.toHaveBeenCalled();
|
||||
expect(btn).toBeInTheDocument();
|
||||
|
||||
await user.click(btn);
|
||||
expect(exportVisits).toHaveBeenCalledWith('domain_foo.com_visits.csv', expect.anything());
|
||||
});
|
||||
});
|
||||
44
shlink-web-component/test/visits/NonOrphanVisits.test.tsx
Normal file
44
shlink-web-component/test/visits/NonOrphanVisits.test.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { formatISO } from 'date-fns';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import type { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
|
||||
import { NonOrphanVisits as createNonOrphanVisits } from '../../src/visits/NonOrphanVisits';
|
||||
import type { VisitsInfo } from '../../src/visits/reducers/types';
|
||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||
|
||||
describe('<NonOrphanVisits />', () => {
|
||||
const exportVisits = vi.fn();
|
||||
const getNonOrphanVisits = vi.fn();
|
||||
const cancelGetNonOrphanVisits = vi.fn();
|
||||
const nonOrphanVisits = fromPartial<VisitsInfo>({ visits: [{ date: formatISO(new Date()) }] });
|
||||
const NonOrphanVisits = createNonOrphanVisits(fromPartial({ exportVisits }));
|
||||
const setUp = () => renderWithEvents(
|
||||
<MemoryRouter>
|
||||
<NonOrphanVisits
|
||||
{...fromPartial<MercureBoundProps>({ mercureInfo: {} })}
|
||||
getNonOrphanVisits={getNonOrphanVisits}
|
||||
cancelGetNonOrphanVisits={cancelGetNonOrphanVisits}
|
||||
nonOrphanVisits={nonOrphanVisits}
|
||||
settings={fromPartial({})}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
it('wraps visits stats and header', () => {
|
||||
setUp();
|
||||
expect(screen.getByRole('heading', { name: 'Non-orphan visits' })).toBeInTheDocument();
|
||||
expect(getNonOrphanVisits).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('exports visits when clicking the button', async () => {
|
||||
const { user } = setUp();
|
||||
const btn = screen.getByRole('button', { name: 'Export (1)' });
|
||||
|
||||
expect(exportVisits).not.toHaveBeenCalled();
|
||||
expect(btn).toBeInTheDocument();
|
||||
|
||||
await user.click(btn);
|
||||
expect(exportVisits).toHaveBeenCalledWith('non_orphan_visits.csv', expect.anything());
|
||||
});
|
||||
});
|
||||
43
shlink-web-component/test/visits/OrphanVisits.test.tsx
Normal file
43
shlink-web-component/test/visits/OrphanVisits.test.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { formatISO } from 'date-fns';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import type { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
|
||||
import { OrphanVisits as createOrphanVisits } from '../../src/visits/OrphanVisits';
|
||||
import type { VisitsInfo } from '../../src/visits/reducers/types';
|
||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||
|
||||
describe('<OrphanVisits />', () => {
|
||||
const getOrphanVisits = vi.fn();
|
||||
const exportVisits = vi.fn();
|
||||
const orphanVisits = fromPartial<VisitsInfo>({ visits: [{ date: formatISO(new Date()) }] });
|
||||
const OrphanVisits = createOrphanVisits(fromPartial({ exportVisits }));
|
||||
const setUp = () => renderWithEvents(
|
||||
<MemoryRouter>
|
||||
<OrphanVisits
|
||||
{...fromPartial<MercureBoundProps>({ mercureInfo: {} })}
|
||||
getOrphanVisits={getOrphanVisits}
|
||||
orphanVisits={orphanVisits}
|
||||
cancelGetOrphanVisits={vi.fn()}
|
||||
settings={fromPartial({})}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
it('wraps visits stats and header', () => {
|
||||
setUp();
|
||||
expect(screen.getByRole('heading', { name: 'Orphan visits' })).toBeInTheDocument();
|
||||
expect(getOrphanVisits).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('exports visits when clicking the button', async () => {
|
||||
const { user } = setUp();
|
||||
const btn = screen.getByRole('button', { name: 'Export (1)' });
|
||||
|
||||
expect(exportVisits).not.toHaveBeenCalled();
|
||||
expect(btn).toBeInTheDocument();
|
||||
|
||||
await user.click(btn);
|
||||
expect(exportVisits).toHaveBeenCalledWith('orphan_visits.csv', expect.anything());
|
||||
});
|
||||
});
|
||||
48
shlink-web-component/test/visits/ShortUrlVisits.test.tsx
Normal file
48
shlink-web-component/test/visits/ShortUrlVisits.test.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { formatISO } from 'date-fns';
|
||||
import { identity } from 'ramda';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import type { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
|
||||
import type { ShortUrlVisits as ShortUrlVisitsState } from '../../src/visits/reducers/shortUrlVisits';
|
||||
import type { ShortUrlVisitsProps } from '../../src/visits/ShortUrlVisits';
|
||||
import { ShortUrlVisits as createShortUrlVisits } from '../../src/visits/ShortUrlVisits';
|
||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||
|
||||
describe('<ShortUrlVisits />', () => {
|
||||
const getShortUrlVisitsMock = vi.fn();
|
||||
const exportVisits = vi.fn();
|
||||
const shortUrlVisits = fromPartial<ShortUrlVisitsState>({ visits: [{ date: formatISO(new Date()) }] });
|
||||
const ShortUrlVisits = createShortUrlVisits(fromPartial({ exportVisits }));
|
||||
const setUp = () => renderWithEvents(
|
||||
<MemoryRouter>
|
||||
<ShortUrlVisits
|
||||
{...fromPartial<ShortUrlVisitsProps>({})}
|
||||
{...fromPartial<MercureBoundProps>({ mercureInfo: {} })}
|
||||
getShortUrlDetail={identity}
|
||||
getShortUrlVisits={getShortUrlVisitsMock}
|
||||
shortUrlVisits={shortUrlVisits}
|
||||
shortUrlDetail={fromPartial({})}
|
||||
settings={fromPartial({})}
|
||||
cancelGetShortUrlVisits={() => {}}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
it('wraps visits stats and header', () => {
|
||||
setUp();
|
||||
expect(screen.getAllByRole('heading')[0]).toHaveTextContent('Visits for');
|
||||
expect(getShortUrlVisitsMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('exports visits when clicking the button', async () => {
|
||||
const { user } = setUp();
|
||||
const btn = screen.getByRole('button', { name: 'Export (1)' });
|
||||
|
||||
expect(exportVisits).not.toHaveBeenCalled();
|
||||
expect(btn).toBeInTheDocument();
|
||||
|
||||
await user.click(btn);
|
||||
expect(exportVisits).toHaveBeenCalledWith('short-url_undefined_visits.csv', expect.anything());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { formatDistance, parseISO } from 'date-fns';
|
||||
import type { ShortUrlDetail } from '../../src/short-urls/reducers/shortUrlDetail';
|
||||
import type { ShortUrlVisits } from '../../src/visits/reducers/shortUrlVisits';
|
||||
import { ShortUrlVisitsHeader } from '../../src/visits/ShortUrlVisitsHeader';
|
||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||
|
||||
describe('<ShortUrlVisitsHeader />', () => {
|
||||
const dateCreated = '2018-01-01T10:00:00+00:00';
|
||||
const longUrl = 'https://foo.bar/bar/foo';
|
||||
const shortUrlVisits = fromPartial<ShortUrlVisits>({
|
||||
visits: [{}, {}, {}],
|
||||
});
|
||||
const goBack = vi.fn();
|
||||
const setUp = (title?: string | null) => {
|
||||
const shortUrlDetail = fromPartial<ShortUrlDetail>({
|
||||
shortUrl: {
|
||||
shortUrl: 'https://s.test/abc123',
|
||||
longUrl,
|
||||
dateCreated,
|
||||
title,
|
||||
},
|
||||
loading: false,
|
||||
});
|
||||
return renderWithEvents(
|
||||
<ShortUrlVisitsHeader shortUrlDetail={shortUrlDetail} shortUrlVisits={shortUrlVisits} goBack={goBack} />,
|
||||
);
|
||||
};
|
||||
|
||||
it('shows when the URL was created', async () => {
|
||||
const { user } = setUp();
|
||||
const dateElement = screen.getByText(`${formatDistance(new Date(), parseISO(dateCreated))} ago`);
|
||||
|
||||
expect(dateElement).toBeInTheDocument();
|
||||
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
|
||||
await user.hover(dateElement);
|
||||
await waitFor(() => expect(screen.getByRole('tooltip')).toHaveTextContent('2018-01-01 10:00'));
|
||||
});
|
||||
|
||||
it.each([
|
||||
[null, `Long URL: ${longUrl}`],
|
||||
[undefined, `Long URL: ${longUrl}`],
|
||||
['My cool title', 'Title: My cool title'],
|
||||
])('shows the long URL and title', (title, expectedContent) => {
|
||||
const { container } = setUp(title);
|
||||
|
||||
expect(container.querySelector('.long-url-container')).toHaveTextContent(expectedContent);
|
||||
expect(screen.getByRole('link', { name: title ?? longUrl })).toHaveAttribute('href', longUrl);
|
||||
});
|
||||
});
|
||||
53
shlink-web-component/test/visits/TagVisits.test.tsx
Normal file
53
shlink-web-component/test/visits/TagVisits.test.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { formatISO } from 'date-fns';
|
||||
import { MemoryRouter } from 'react-router';
|
||||
import type { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
|
||||
import type { TagVisits as TagVisitsStats } from '../../src/visits/reducers/tagVisits';
|
||||
import type { TagVisitsProps } from '../../src/visits/TagVisits';
|
||||
import { TagVisits as createTagVisits } from '../../src/visits/TagVisits';
|
||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||
|
||||
vi.mock('react-router-dom', async () => ({
|
||||
...(await vi.importActual<any>('react-router-dom')),
|
||||
useParams: vi.fn().mockReturnValue({ tag: 'foo' }),
|
||||
}));
|
||||
|
||||
describe('<TagVisits />', () => {
|
||||
const getTagVisitsMock = vi.fn();
|
||||
const exportVisits = vi.fn();
|
||||
const tagVisits = fromPartial<TagVisitsStats>({ visits: [{ date: formatISO(new Date()) }] });
|
||||
const TagVisits = createTagVisits(
|
||||
fromPartial({ isColorLightForKey: () => false, getColorForKey: () => 'red' }),
|
||||
fromPartial({ exportVisits }),
|
||||
);
|
||||
const setUp = () => renderWithEvents(
|
||||
<MemoryRouter>
|
||||
<TagVisits
|
||||
{...fromPartial<TagVisitsProps>({})}
|
||||
{...fromPartial<MercureBoundProps>({ mercureInfo: {} })}
|
||||
getTagVisits={getTagVisitsMock}
|
||||
tagVisits={tagVisits}
|
||||
settings={fromPartial({})}
|
||||
cancelGetTagVisits={() => {}}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
it('wraps visits stats and header', () => {
|
||||
setUp();
|
||||
expect(screen.getAllByRole('heading')[0]).toHaveTextContent('Visits for');
|
||||
expect(getTagVisitsMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('exports visits when clicking the button', async () => {
|
||||
const { user } = setUp();
|
||||
const btn = screen.getByRole('button', { name: 'Export (1)' });
|
||||
|
||||
expect(exportVisits).not.toHaveBeenCalled();
|
||||
expect(btn).toBeInTheDocument();
|
||||
|
||||
await user.click(btn);
|
||||
expect(exportVisits).toHaveBeenCalledWith('tag_foo_visits.csv', expect.anything());
|
||||
});
|
||||
});
|
||||
27
shlink-web-component/test/visits/TagVisitsHeader.test.tsx
Normal file
27
shlink-web-component/test/visits/TagVisitsHeader.test.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import type { ColorGenerator } from '../../src/utils/services/ColorGenerator';
|
||||
import type { TagVisits } from '../../src/visits/reducers/tagVisits';
|
||||
import { TagVisitsHeader } from '../../src/visits/TagVisitsHeader';
|
||||
|
||||
describe('<TagVisitsHeader />', () => {
|
||||
const tagVisits = fromPartial<TagVisits>({
|
||||
tag: 'foo',
|
||||
visits: [{}, {}, {}, {}],
|
||||
});
|
||||
const goBack = vi.fn();
|
||||
const colorGenerator = fromPartial<ColorGenerator>({ isColorLightForKey: () => false, getColorForKey: () => 'red' });
|
||||
const setUp = () => render(<TagVisitsHeader tagVisits={tagVisits} goBack={goBack} colorGenerator={colorGenerator} />);
|
||||
|
||||
it('shows expected visits', () => {
|
||||
const { container } = setUp();
|
||||
|
||||
expect(screen.getAllByText('Visits for')).toHaveLength(2);
|
||||
expect(container.querySelector('.badge:not(.tag)')).toHaveTextContent(`Visits: ${tagVisits.visits.length}`);
|
||||
});
|
||||
|
||||
it('shows title for tag', () => {
|
||||
const { container } = setUp();
|
||||
expect(container.querySelector('.badge.tag')).toHaveTextContent(tagVisits.tag);
|
||||
});
|
||||
});
|
||||
21
shlink-web-component/test/visits/VisitsHeader.test.tsx
Normal file
21
shlink-web-component/test/visits/VisitsHeader.test.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import type { Visit } from '../../src/visits/types';
|
||||
import { VisitsHeader } from '../../src/visits/VisitsHeader';
|
||||
|
||||
describe('<VisitsHeader />', () => {
|
||||
const visits: Visit[] = [fromPartial({}), fromPartial({}), fromPartial({})];
|
||||
const title = 'My header title';
|
||||
const goBack = vi.fn();
|
||||
const setUp = () => render(<VisitsHeader visits={visits} goBack={goBack} title={title} />);
|
||||
|
||||
it('shows the amount of visits', () => {
|
||||
const { container } = setUp();
|
||||
expect(container.querySelector('.badge')).toHaveTextContent(`Visits: ${visits.length}`);
|
||||
});
|
||||
|
||||
it('shows the title in two places', () => {
|
||||
setUp();
|
||||
expect(screen.getAllByText(title)).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
104
shlink-web-component/test/visits/VisitsStats.test.tsx
Normal file
104
shlink-web-component/test/visits/VisitsStats.test.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { rangeOf } from '../../src/utils/helpers';
|
||||
import type { VisitsInfo } from '../../src/visits/reducers/types';
|
||||
import type { Visit } from '../../src/visits/types';
|
||||
import { VisitsStats } from '../../src/visits/VisitsStats';
|
||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||
|
||||
describe('<VisitsStats />', () => {
|
||||
const visits = rangeOf(3, () => fromPartial<Visit>({ date: '2020-01-01' }));
|
||||
const getVisitsMock = vi.fn();
|
||||
const exportCsv = vi.fn();
|
||||
const setUp = (visitsInfo: Partial<VisitsInfo>, activeRoute = '/by-time') => {
|
||||
const history = createMemoryHistory();
|
||||
history.push(activeRoute);
|
||||
|
||||
return {
|
||||
history,
|
||||
...renderWithEvents(
|
||||
<Router location={history.location} navigator={history}>
|
||||
<VisitsStats
|
||||
getVisits={getVisitsMock}
|
||||
visitsInfo={fromPartial(visitsInfo)}
|
||||
cancelGetVisits={() => {}}
|
||||
settings={fromPartial({})}
|
||||
exportCsv={exportCsv}
|
||||
/>
|
||||
</Router>,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
it('renders a preloader when visits are loading', () => {
|
||||
setUp({ loading: true, visits: [] });
|
||||
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||
expect(screen.queryByText(/^This is going to take a while/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a warning and progress bar when loading large amounts of visits', () => {
|
||||
setUp({ loading: true, loadingLarge: true, visits: [], progress: 25 });
|
||||
|
||||
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
||||
expect(screen.getByText(/^This is going to take a while/)).toBeInTheDocument();
|
||||
expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '25');
|
||||
});
|
||||
|
||||
it('renders an error message when visits could not be loaded', () => {
|
||||
setUp({ loading: false, error: true, visits: [] });
|
||||
expect(screen.getByText('An error occurred while loading visits :(')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a message when visits are loaded but the list is empty', () => {
|
||||
setUp({ loading: false, error: false, visits: [] });
|
||||
expect(screen.getByText('There are no visits matching current filter')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['/by-time', 2],
|
||||
['/by-context', 4],
|
||||
['/by-location', 3],
|
||||
['/list', 1],
|
||||
])('renders expected amount of charts', (route, expectedCharts) => {
|
||||
const { container } = setUp({ loading: false, error: false, visits }, route);
|
||||
expect(container.querySelectorAll('.card')).toHaveLength(expectedCharts);
|
||||
});
|
||||
|
||||
it('holds the map button on cities chart header', () => {
|
||||
setUp({ loading: false, error: false, visits }, '/by-location');
|
||||
expect(
|
||||
screen.getAllByRole('img', { hidden: true }).some((icon) => icon.classList.contains('fa-map-location-dot')),
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
it('exports CSV when export btn is clicked', async () => {
|
||||
const { user } = setUp({ visits });
|
||||
|
||||
expect(exportCsv).not.toHaveBeenCalled();
|
||||
await user.click(screen.getByRole('button', { name: /Export/ }));
|
||||
expect(exportCsv).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sets filters in query string', async () => {
|
||||
const { history, user } = setUp({ visits });
|
||||
const expectSearchContains = (contains: string[]) => {
|
||||
expect(contains).not.toHaveLength(0);
|
||||
contains.forEach((entry) => expect(history.location.search).toContain(entry));
|
||||
};
|
||||
|
||||
expect(history.location.search).toEqual('');
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /Filters/ }));
|
||||
await waitFor(() => screen.getByRole('menu'));
|
||||
await user.click(screen.getByRole('menuitem', { name: 'Exclude potential bots' }));
|
||||
expectSearchContains(['excludeBots=true']);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /Last 30 days/ }));
|
||||
await waitFor(() => screen.getByRole('menu'));
|
||||
await user.click(screen.getByRole('menuitem', { name: /Last 180 days/ }));
|
||||
expectSearchContains(['startDate', 'endDate']);
|
||||
});
|
||||
});
|
||||
142
shlink-web-component/test/visits/VisitsTable.test.tsx
Normal file
142
shlink-web-component/test/visits/VisitsTable.test.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { rangeOf } from '../../src/utils/helpers';
|
||||
import type { NormalizedVisit } from '../../src/visits/types';
|
||||
import type { VisitsTableProps } from '../../src/visits/VisitsTable';
|
||||
import { VisitsTable } from '../../src/visits/VisitsTable';
|
||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||
|
||||
describe('<VisitsTable />', () => {
|
||||
const matchMedia = () => fromPartial<MediaQueryList>({ matches: false });
|
||||
const setSelectedVisits = vi.fn();
|
||||
const setUpFactory = (props: Partial<VisitsTableProps> = {}) => renderWithEvents(
|
||||
<VisitsTable
|
||||
visits={[]}
|
||||
{...props}
|
||||
matchMedia={matchMedia}
|
||||
setSelectedVisits={setSelectedVisits}
|
||||
/>,
|
||||
);
|
||||
const setUp = (visits: NormalizedVisit[], selectedVisits: NormalizedVisit[] = []) => setUpFactory(
|
||||
{ visits, selectedVisits },
|
||||
);
|
||||
const setUpForOrphanVisits = (isOrphanVisits: boolean) => setUpFactory({ isOrphanVisits });
|
||||
const setUpWithBots = () => setUpFactory({
|
||||
visits: [
|
||||
fromPartial({ potentialBot: false, date: '2022-05-05' }),
|
||||
fromPartial({ potentialBot: true, date: '2022-05-05' }),
|
||||
],
|
||||
});
|
||||
|
||||
it('renders expected amount of columns', () => {
|
||||
setUp([], []);
|
||||
expect(screen.getAllByRole('columnheader')).toHaveLength(8);
|
||||
});
|
||||
|
||||
it('shows warning when no visits are found', () => {
|
||||
setUp([]);
|
||||
expect(screen.getByText('No visits found with current filtering')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([
|
||||
[50, 5, 1],
|
||||
[21, 4, 1],
|
||||
[30, 4, 1],
|
||||
[60, 5, 1],
|
||||
[115, 7, 2], // This one will have ellipsis
|
||||
])('renders the expected amount of pages', (visitsCount, expectedAmountOfPageItems, expectedDisabledItems) => {
|
||||
const { container } = setUp(
|
||||
rangeOf(visitsCount, () => fromPartial<NormalizedVisit>({ browser: '', date: '2022-01-01', referer: '' })),
|
||||
);
|
||||
expect(container.querySelectorAll('.page-item')).toHaveLength(expectedAmountOfPageItems);
|
||||
expect(container.querySelectorAll('.disabled')).toHaveLength(expectedDisabledItems);
|
||||
});
|
||||
|
||||
it.each(
|
||||
rangeOf(20, (value) => [value]),
|
||||
)('does not render footer when there is only one page to render', (visitsCount) => {
|
||||
const { container } = setUp(
|
||||
rangeOf(visitsCount, () => fromPartial<NormalizedVisit>({ browser: '', date: '2022-01-01', referer: '' })),
|
||||
);
|
||||
|
||||
expect(container.querySelector('tfoot')).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText('pagination')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('selected rows are highlighted', async () => {
|
||||
const visits = rangeOf(10, () => fromPartial<NormalizedVisit>({ browser: '', date: '2022-01-01', referer: '' }));
|
||||
const { container, user } = setUp(visits, [visits[1], visits[2]]);
|
||||
|
||||
// Initial situation
|
||||
expect(container.querySelectorAll('.table-active')).toHaveLength(2);
|
||||
|
||||
// Select one extra
|
||||
await user.click(screen.getAllByRole('row')[5]);
|
||||
expect(setSelectedVisits).toHaveBeenCalledWith([visits[1], visits[2], visits[4]]);
|
||||
|
||||
// Deselect one
|
||||
await user.click(screen.getAllByRole('row')[3]);
|
||||
expect(setSelectedVisits).toHaveBeenCalledWith([visits[1]]);
|
||||
|
||||
// Select all
|
||||
await user.click(screen.getAllByRole('columnheader')[0]);
|
||||
expect(setSelectedVisits).toHaveBeenCalledWith(visits);
|
||||
});
|
||||
|
||||
it('orders visits when column is clicked', async () => {
|
||||
const { user } = setUp(rangeOf(9, (index) => fromPartial<NormalizedVisit>({
|
||||
browser: '',
|
||||
date: `2022-01-0${10 - index}`,
|
||||
referer: `${index}`,
|
||||
country: `Country_${index}`,
|
||||
})));
|
||||
const getFirstColumnValue = () => screen.getAllByRole('row')[2]?.querySelectorAll('td')[3]?.textContent;
|
||||
const clickColumn = async (index: number) => user.click(screen.getAllByRole('columnheader')[index]);
|
||||
|
||||
expect(getFirstColumnValue()).toContain('Country_1');
|
||||
await clickColumn(2); // Date column ASC
|
||||
expect(getFirstColumnValue()).toContain('Country_9');
|
||||
await clickColumn(7); // Referer column - ASC
|
||||
expect(getFirstColumnValue()).toContain('Country_1');
|
||||
await clickColumn(7); // Referer column - DESC
|
||||
expect(getFirstColumnValue()).toContain('Country_9');
|
||||
await clickColumn(7); // Referer column - reset
|
||||
expect(getFirstColumnValue()).toContain('Country_1');
|
||||
});
|
||||
|
||||
it('filters list when writing in search box', async () => {
|
||||
const { user } = setUp([
|
||||
...rangeOf(7, () => fromPartial<NormalizedVisit>({ browser: 'aaa', date: '2022-01-01', referer: 'aaa' })),
|
||||
...rangeOf(2, () => fromPartial<NormalizedVisit>({ browser: 'bbb', date: '2022-01-01', referer: 'bbb' })),
|
||||
]);
|
||||
const searchField = screen.getByPlaceholderText('Search...');
|
||||
const searchText = async (text: string) => {
|
||||
await user.clear(searchField);
|
||||
text.length > 0 && await user.type(searchField, text);
|
||||
};
|
||||
|
||||
expect(screen.getAllByRole('row')).toHaveLength(9 + 2);
|
||||
await searchText('aa');
|
||||
await waitFor(() => expect(screen.getAllByRole('row')).toHaveLength(7 + 2));
|
||||
await searchText('bb');
|
||||
await waitFor(() => expect(screen.getAllByRole('row')).toHaveLength(2 + 2));
|
||||
await searchText('');
|
||||
await waitFor(() => expect(screen.getAllByRole('row')).toHaveLength(9 + 2));
|
||||
});
|
||||
|
||||
it.each([
|
||||
[true, 9],
|
||||
[false, 8],
|
||||
])('displays proper amount of columns for orphan and non-orphan visits', (isOrphanVisits, expectedCols) => {
|
||||
setUpForOrphanVisits(isOrphanVisits);
|
||||
expect(screen.getAllByRole('columnheader')).toHaveLength(expectedCols);
|
||||
});
|
||||
|
||||
it('displays bots icon when a visit is a potential bot', () => {
|
||||
setUpWithBots();
|
||||
const [,, nonBotVisitRow, botVisitRow] = screen.getAllByRole('row');
|
||||
|
||||
expect(nonBotVisitRow.querySelectorAll('td')[1]).toBeEmptyDOMElement();
|
||||
expect(botVisitRow.querySelectorAll('td')[1]).not.toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
22
shlink-web-component/test/visits/charts/ChartCard.test.tsx
Normal file
22
shlink-web-component/test/visits/charts/ChartCard.test.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { ChartCard } from '../../../src/visits/charts/ChartCard';
|
||||
|
||||
describe('<ChartCard />', () => {
|
||||
const setUp = (title: Function | string = '', footer?: ReactNode) => render(
|
||||
<ChartCard title={title} footer={footer} />,
|
||||
);
|
||||
|
||||
it.each([
|
||||
['the title', 'the title'],
|
||||
[() => 'the title from func', 'the title from func'],
|
||||
])('properly renders title by parsing provided value', (title, expectedTitle) => {
|
||||
setUp(title);
|
||||
expect(screen.getByText(expectedTitle)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders footer only when provided', () => {
|
||||
setUp('', 'the footer');
|
||||
expect(screen.getByText('the footer')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import { DoughnutChart } from '../../../src/visits/charts/DoughnutChart';
|
||||
import { setUpCanvas } from '../../__helpers__/setUpTest';
|
||||
|
||||
describe('<DoughnutChart />', () => {
|
||||
const stats = {
|
||||
foo: 123,
|
||||
bar: 456,
|
||||
};
|
||||
|
||||
it('renders Doughnut with expected props', () => {
|
||||
const { events } = setUpCanvas(<DoughnutChart stats={stats} />);
|
||||
|
||||
expect(events).toBeTruthy();
|
||||
expect(events).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders expected legend', () => {
|
||||
setUpCanvas(<DoughnutChart stats={stats} />);
|
||||
|
||||
expect(screen.getByText('foo')).toBeInTheDocument();
|
||||
expect(screen.getByText('bar')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import type { Chart, ChartDataset } from 'chart.js';
|
||||
import { DoughnutChartLegend } from '../../../src/visits/charts/DoughnutChartLegend';
|
||||
|
||||
describe('<DoughnutChartLegend />', () => {
|
||||
const labels = ['foo', 'bar', 'baz', 'foo2', 'bar2'];
|
||||
const colors = ['green', 'blue', 'yellow'];
|
||||
const defaultColor = 'red';
|
||||
const datasets = [fromPartial<ChartDataset>({ backgroundColor: colors })];
|
||||
const chart = fromPartial<Chart>({
|
||||
config: {
|
||||
data: { labels, datasets },
|
||||
options: { defaultColor } as any,
|
||||
},
|
||||
});
|
||||
|
||||
it('renders the expected amount of items with expected colors and labels', () => {
|
||||
render(<DoughnutChartLegend chart={chart} />);
|
||||
|
||||
const items = screen.getAllByRole('listitem');
|
||||
|
||||
expect.assertions(labels.length * 2 + 1);
|
||||
expect(items).toHaveLength(labels.length);
|
||||
|
||||
labels.forEach((label, index) => {
|
||||
const item = items[index];
|
||||
|
||||
expect(item.querySelector('.doughnut-chart-legend__item-color')).toHaveAttribute(
|
||||
'style',
|
||||
`background-color: ${colors[index] ?? defaultColor};`,
|
||||
);
|
||||
expect(item.querySelector('.doughnut-chart-legend__item-text')).toHaveTextContent(label);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { HorizontalBarChartProps } from '../../../src/visits/charts/HorizontalBarChart';
|
||||
import { HorizontalBarChart } from '../../../src/visits/charts/HorizontalBarChart';
|
||||
import { setUpCanvas } from '../../__helpers__/setUpTest';
|
||||
|
||||
describe('<HorizontalBarChart />', () => {
|
||||
const setUp = (props: HorizontalBarChartProps) => setUpCanvas(<HorizontalBarChart {...props} />);
|
||||
|
||||
it.each([
|
||||
[{ foo: 123, bar: 456 }, undefined],
|
||||
[{ one: 999, two: 131313 }, { one: 30, two: 100 }],
|
||||
[{ one: 999, two: 131313, max: 3 }, { one: 30, two: 100 }],
|
||||
])('renders chart with expected canvas', (stats, highlightedStats) => {
|
||||
const { events } = setUp({ stats, highlightedStats });
|
||||
|
||||
expect(events).toBeTruthy();
|
||||
expect(events).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { formatISO, subDays, subMonths, subYears } from 'date-fns';
|
||||
import { LineChartCard } from '../../../src/visits/charts/LineChartCard';
|
||||
import type { NormalizedVisit } from '../../../src/visits/types';
|
||||
import { setUpCanvas } from '../../__helpers__/setUpTest';
|
||||
|
||||
describe('<LineChartCard />', () => {
|
||||
const setUp = (visits: NormalizedVisit[] = [], highlightedVisits: NormalizedVisit[] = []) => ({
|
||||
user: userEvent.setup(),
|
||||
...setUpCanvas(<LineChartCard title="Cool title" visits={visits} highlightedVisits={highlightedVisits} />),
|
||||
});
|
||||
|
||||
it('renders provided title', () => {
|
||||
setUp();
|
||||
expect(screen.getByRole('heading')).toHaveTextContent('Cool title');
|
||||
});
|
||||
|
||||
it.each([
|
||||
[[], 0],
|
||||
[[{ date: formatISO(subDays(new Date(), 1)) }], 3],
|
||||
[[{ date: formatISO(subDays(new Date(), 3)) }], 2],
|
||||
[[{ date: formatISO(subMonths(new Date(), 2)) }], 1],
|
||||
[[{ date: formatISO(subMonths(new Date(), 6)) }], 1],
|
||||
[[{ date: formatISO(subMonths(new Date(), 7)) }], 0],
|
||||
[[{ date: formatISO(subYears(new Date(), 1)) }], 0],
|
||||
])('renders group menu and selects proper grouping item based on visits dates', async (
|
||||
visits,
|
||||
expectedActiveIndex,
|
||||
) => {
|
||||
const { user } = setUp(visits.map((visit) => fromPartial(visit)));
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /Group by/ }));
|
||||
|
||||
const items = screen.getAllByRole('menuitem');
|
||||
|
||||
expect(items).toHaveLength(4);
|
||||
expect(items[0]).toHaveTextContent('Month');
|
||||
expect(items[1]).toHaveTextContent('Week');
|
||||
expect(items[2]).toHaveTextContent('Day');
|
||||
expect(items[3]).toHaveTextContent('Hour');
|
||||
expect(items[expectedActiveIndex]).toHaveAttribute('class', expect.stringContaining('active'));
|
||||
});
|
||||
|
||||
it.each([
|
||||
[undefined, undefined],
|
||||
[[], []],
|
||||
[[fromPartial<NormalizedVisit>({ date: '2016-04-01' })], []],
|
||||
[[fromPartial<NormalizedVisit>({ date: '2016-04-01' })], [fromPartial<NormalizedVisit>({ date: '2016-04-01' })]],
|
||||
])('renders chart with expected data', (visits, highlightedVisits) => {
|
||||
const { events } = setUp(visits, highlightedVisits);
|
||||
|
||||
expect(events).toBeTruthy();
|
||||
expect(events).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('includes stats for visits with no dates if selected', async () => {
|
||||
const { getEvents, user } = setUp([
|
||||
fromPartial({ date: '2016-04-01' }),
|
||||
fromPartial({ date: '2016-01-01' }),
|
||||
]);
|
||||
|
||||
const eventsBefore = getEvents();
|
||||
await user.click(screen.getByLabelText('Skip dates with no visits'));
|
||||
expect(eventsBefore).not.toEqual(getEvents());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import { range } from 'ramda';
|
||||
import type { ReactNode } from 'react';
|
||||
import { rangeOf } from '../../../src/utils/helpers';
|
||||
import { SortableBarChartCard } from '../../../src/visits/charts/SortableBarChartCard';
|
||||
import type { Stats } from '../../../src/visits/types';
|
||||
import { renderWithEvents } from '../../__helpers__/setUpTest';
|
||||
|
||||
describe('<SortableBarChartCard />', () => {
|
||||
const sortingItems = {
|
||||
name: 'Name',
|
||||
amount: 'Amount',
|
||||
};
|
||||
const stats = {
|
||||
Foo: 100,
|
||||
Bar: 50,
|
||||
};
|
||||
const setUp = (withPagination = false, extraStats = {}, extra?: (foo?: string[]) => ReactNode) => renderWithEvents(
|
||||
<SortableBarChartCard
|
||||
title="Foo"
|
||||
stats={{ ...stats, ...extraStats }}
|
||||
sortingItems={sortingItems}
|
||||
withPagination={withPagination}
|
||||
extraHeaderContent={extra}
|
||||
/>,
|
||||
);
|
||||
|
||||
it('renders stats unchanged when no ordering is set', () => {
|
||||
const { container } = setUp();
|
||||
|
||||
expect(container.firstChild).not.toBeNull();
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['Name', 1],
|
||||
['Amount', 1],
|
||||
['Name', 2],
|
||||
['Amount', 2],
|
||||
])('renders properly ordered stats when ordering is set', async (name, clicks) => {
|
||||
const { user } = setUp();
|
||||
|
||||
await user.click(screen.getByRole('button'));
|
||||
await Promise.all(rangeOf(clicks, async () => user.click(screen.getByRole('menuitem', { name }))));
|
||||
|
||||
expect(screen.getByRole('document')).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it.each([
|
||||
[0],
|
||||
[1],
|
||||
[2],
|
||||
[3],
|
||||
])('renders properly paginated stats when pagination is set', async (itemIndex) => {
|
||||
const { user } = setUp(true, range(1, 159).reduce<Stats>((accum, value) => {
|
||||
accum[`key_${value}`] = value;
|
||||
return accum;
|
||||
}, {}));
|
||||
|
||||
await user.click(screen.getAllByRole('button')[1]);
|
||||
await user.click(screen.getAllByRole('menuitem')[itemIndex]);
|
||||
|
||||
expect(screen.getByRole('document')).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders extra header content', () => {
|
||||
setUp(false, {}, () => (
|
||||
<span>
|
||||
<span className="foo-span">Foo in header</span>
|
||||
<span className="bar-span">Bar in header</span>
|
||||
</span>
|
||||
));
|
||||
|
||||
expect(screen.getByText('Foo in header')).toHaveClass('foo-span');
|
||||
expect(screen.getByText('Bar in header')).toHaveClass('bar-span');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<DoughnutChart /> > renders Doughnut with expected props 1`] = `
|
||||
[
|
||||
{
|
||||
"props": {
|
||||
"a": 1,
|
||||
"b": 0,
|
||||
"c": 0,
|
||||
"d": 1,
|
||||
"e": 0,
|
||||
"f": 0,
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "setTransform",
|
||||
},
|
||||
]
|
||||
`;
|
||||
@@ -0,0 +1,521 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<HorizontalBarChart /> > renders chart with expected canvas 1`] = `
|
||||
[
|
||||
{
|
||||
"props": {
|
||||
"a": 1,
|
||||
"b": 0,
|
||||
"c": 0,
|
||||
"d": 1,
|
||||
"e": 0,
|
||||
"f": 0,
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "setTransform",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"text": "foo",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "measureText",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"text": "bar",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "measureText",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"text": "0",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "measureText",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"text": "500",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "measureText",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`<HorizontalBarChart /> > renders chart with expected canvas 2`] = `
|
||||
[
|
||||
{
|
||||
"props": {
|
||||
"a": 1,
|
||||
"b": 0,
|
||||
"c": 0,
|
||||
"d": 1,
|
||||
"e": 0,
|
||||
"f": 0,
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "setTransform",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"text": "one",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "measureText",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"text": "two",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "measureText",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"text": "0",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "measureText",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"text": "200,000",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "measureText",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`<HorizontalBarChart /> > renders chart with expected canvas 3`] = `
|
||||
[
|
||||
{
|
||||
"props": {
|
||||
"a": 1,
|
||||
"b": 0,
|
||||
"c": 0,
|
||||
"d": 1,
|
||||
"e": 0,
|
||||
"f": 0,
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "setTransform",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"text": "one",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "measureText",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"text": "two",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "measureText",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"text": "max",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "measureText",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"text": "0",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "measureText",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"text": "200,000",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "measureText",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
]
|
||||
`;
|
||||
@@ -0,0 +1,461 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<LineChartCard /> > renders chart with expected data 1`] = `
|
||||
[
|
||||
{
|
||||
"props": {
|
||||
"a": 1,
|
||||
"b": 0,
|
||||
"c": 0,
|
||||
"d": 1,
|
||||
"e": 0,
|
||||
"f": 0,
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "setTransform",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"text": "0",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "measureText",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"text": "1",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "measureText",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`<LineChartCard /> > renders chart with expected data 2`] = `
|
||||
[
|
||||
{
|
||||
"props": {
|
||||
"a": 1,
|
||||
"b": 0,
|
||||
"c": 0,
|
||||
"d": 1,
|
||||
"e": 0,
|
||||
"f": 0,
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "setTransform",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"text": "0",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "measureText",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"text": "1",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "measureText",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`<LineChartCard /> > renders chart with expected data 3`] = `
|
||||
[
|
||||
{
|
||||
"props": {
|
||||
"a": 1,
|
||||
"b": 0,
|
||||
"c": 0,
|
||||
"d": 1,
|
||||
"e": 0,
|
||||
"f": 0,
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "setTransform",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"text": "0",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "measureText",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"text": "1",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "measureText",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"text": "2016-04",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "measureText",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`<LineChartCard /> > renders chart with expected data 4`] = `
|
||||
[
|
||||
{
|
||||
"props": {
|
||||
"a": 1,
|
||||
"b": 0,
|
||||
"c": 0,
|
||||
"d": 1,
|
||||
"e": 0,
|
||||
"f": 0,
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "setTransform",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"text": "0",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "measureText",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"text": "1",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "measureText",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"text": "2016-04",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "measureText",
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
|
||||
},
|
||||
"transform": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"type": "font",
|
||||
},
|
||||
]
|
||||
`;
|
||||
File diff suppressed because it is too large
Load Diff
28
shlink-web-component/test/visits/helpers/MapModal.test.tsx
Normal file
28
shlink-web-component/test/visits/helpers/MapModal.test.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MapModal } from '../../../src/visits/helpers/MapModal';
|
||||
import type { CityStats } from '../../../src/visits/types';
|
||||
|
||||
describe('<MapModal />', () => {
|
||||
const toggle = vi.fn();
|
||||
const zaragozaLat = 41.6563497;
|
||||
const zaragozaLong = -0.876566;
|
||||
const newYorkLat = 40.730610;
|
||||
const newYorkLong = -73.935242;
|
||||
const locations: CityStats[] = [
|
||||
{
|
||||
cityName: 'Zaragoza',
|
||||
count: 54,
|
||||
latLong: [zaragozaLat, zaragozaLong],
|
||||
},
|
||||
{
|
||||
cityName: 'New York',
|
||||
count: 7,
|
||||
latLong: [newYorkLat, newYorkLong],
|
||||
},
|
||||
];
|
||||
|
||||
it('renders expected map', () => {
|
||||
render(<MapModal toggle={toggle} isOpen title="Foobar" locations={locations} />);
|
||||
expect(screen.getByRole('dialog')).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { OpenMapModalBtn } from '../../../src/visits/helpers/OpenMapModalBtn';
|
||||
import type { CityStats } from '../../../src/visits/types';
|
||||
import { renderWithEvents } from '../../__helpers__/setUpTest';
|
||||
|
||||
describe('<OpenMapModalBtn />', () => {
|
||||
const title = 'Foo';
|
||||
const locations: CityStats[] = [
|
||||
fromPartial({ cityName: 'foo', count: 30, latLong: [5, 5] }),
|
||||
fromPartial({ cityName: 'bar', count: 45, latLong: [88, 88] }),
|
||||
];
|
||||
const setUp = (activeCities?: string[]) => renderWithEvents(
|
||||
<OpenMapModalBtn modalTitle={title} locations={locations} activeCities={activeCities} />,
|
||||
);
|
||||
|
||||
it('renders tooltip on button hover and opens modal on click', async () => {
|
||||
const { user } = setUp();
|
||||
|
||||
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole('button'));
|
||||
await waitFor(() => expect(screen.getByRole('tooltip')).toBeInTheDocument());
|
||||
await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument());
|
||||
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens dropdown instead of modal when a list of active cities has been provided', async () => {
|
||||
const { user } = setUp(['bar']);
|
||||
|
||||
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole('button'));
|
||||
|
||||
await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['Show all locations'],
|
||||
['Show locations in current page'],
|
||||
])('filters out non-active cities from list of locations', async (name) => {
|
||||
const { user } = setUp(['bar']);
|
||||
|
||||
await user.click(screen.getByRole('button'));
|
||||
await user.click(screen.getByRole('menuitem', { name }));
|
||||
|
||||
expect(await screen.findByRole('dialog')).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import { VisitsFilterDropdown } from '../../../src/visits/helpers/VisitsFilterDropdown';
|
||||
import type { OrphanVisitType, VisitsFilter } from '../../../src/visits/types';
|
||||
import { renderWithEvents } from '../../__helpers__/setUpTest';
|
||||
|
||||
describe('<VisitsFilterDropdown />', () => {
|
||||
const onChange = vi.fn();
|
||||
const setUp = (selected: VisitsFilter = {}, isOrphanVisits = true) => renderWithEvents(
|
||||
<VisitsFilterDropdown
|
||||
isOrphanVisits={isOrphanVisits}
|
||||
selected={selected}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
it('has expected text', () => {
|
||||
setUp();
|
||||
expect(screen.getByRole('button', { name: 'Filters' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([
|
||||
[false, 1, 1],
|
||||
[true, 4, 2],
|
||||
])('renders expected amount of items', async (isOrphanVisits, expectedItemsAmount, expectedHeadersAmount) => {
|
||||
const { user } = setUp({}, isOrphanVisits);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Filters' }));
|
||||
|
||||
expect(screen.getAllByRole('menuitem')).toHaveLength(expectedItemsAmount);
|
||||
expect(screen.getAllByRole('heading')).toHaveLength(expectedHeadersAmount);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['base_url' as OrphanVisitType, 1, 1],
|
||||
['invalid_short_url' as OrphanVisitType, 2, 1],
|
||||
['regular_404' as OrphanVisitType, 3, 1],
|
||||
[undefined, -1, 0],
|
||||
])('sets expected item as active', async (orphanVisitsType, expectedSelectedIndex, expectedActiveItems) => {
|
||||
const { user } = setUp({ orphanVisitsType });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Filters' }));
|
||||
|
||||
const items = screen.getAllByRole('menuitem');
|
||||
const activeItem = items.filter((item) => item.classList.contains('active'));
|
||||
|
||||
expect.assertions(expectedActiveItems + 1);
|
||||
expect(activeItem).toHaveLength(expectedActiveItems);
|
||||
items.forEach((item, index) => {
|
||||
if (item.classList.contains('active')) {
|
||||
expect(index).toEqual(expectedSelectedIndex);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
[0, { excludeBots: true }, {}],
|
||||
[1, { orphanVisitsType: 'base_url' }, {}],
|
||||
[2, { orphanVisitsType: 'invalid_short_url' }, {}],
|
||||
[3, { orphanVisitsType: 'regular_404' }, {}],
|
||||
[4, { orphanVisitsType: undefined, excludeBots: false }, { excludeBots: true }],
|
||||
])('invokes onChange with proper selection when an item is clicked', async (index, expectedSelection, selected) => {
|
||||
const { user } = setUp(selected);
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
await user.click(screen.getByRole('button', { name: 'Filters' }));
|
||||
await user.click(screen.getAllByRole('menuitem')[index]);
|
||||
expect(onChange).toHaveBeenCalledWith(expectedSelection);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,183 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<MapModal /> > renders expected map 1`] = `
|
||||
<div
|
||||
class="modal fade"
|
||||
role="dialog"
|
||||
style="display: block;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="modal-dialog map-modal__modal"
|
||||
role="document"
|
||||
>
|
||||
<div
|
||||
class="modal-content map-modal__modal-content"
|
||||
>
|
||||
<div
|
||||
class="map-modal__modal-body modal-body"
|
||||
>
|
||||
<h3
|
||||
class="map-modal__modal-title"
|
||||
>
|
||||
Foobar
|
||||
<button
|
||||
aria-label="Close"
|
||||
class="btn-close float-end"
|
||||
type="button"
|
||||
/>
|
||||
</h3>
|
||||
<div
|
||||
class="leaflet-container leaflet-touch leaflet-grab leaflet-touch-drag leaflet-touch-zoom"
|
||||
style="position: relative;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="leaflet-pane leaflet-map-pane"
|
||||
style="left: 0px; top: 0px;"
|
||||
>
|
||||
<div
|
||||
class="leaflet-pane leaflet-tile-pane"
|
||||
>
|
||||
<div
|
||||
class="leaflet-layer "
|
||||
style="z-index: 1;"
|
||||
>
|
||||
<div
|
||||
class="leaflet-tile-container leaflet-zoom-animated"
|
||||
style="z-index: 18; left: 0px; top: 0px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="leaflet-tile"
|
||||
src="https://a.tile.openstreetmap.org/0/0/0.png"
|
||||
style="width: 256px; height: 256px; left: -101px; top: -96px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="leaflet-pane leaflet-overlay-pane"
|
||||
/>
|
||||
<div
|
||||
class="leaflet-pane leaflet-shadow-pane"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="leaflet-marker-shadow leaflet-zoom-hide"
|
||||
src="marker-shadow.png"
|
||||
style="margin-left: -12px; margin-top: -41px; width: 41px; height: 41px; left: 26px; top: -1px;"
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
class="leaflet-marker-shadow leaflet-zoom-hide"
|
||||
src="marker-shadow.png"
|
||||
style="margin-left: -12px; margin-top: -41px; width: 41px; height: 41px; left: -26px; top: 0px;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="leaflet-pane leaflet-marker-pane"
|
||||
>
|
||||
<img
|
||||
alt="Marker"
|
||||
class="leaflet-marker-icon leaflet-zoom-hide leaflet-interactive"
|
||||
role="button"
|
||||
src="marker-icon.png"
|
||||
style="margin-left: -12px; margin-top: -41px; width: 25px; height: 41px; left: 26px; top: -1px; z-index: -1;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<img
|
||||
alt="Marker"
|
||||
class="leaflet-marker-icon leaflet-zoom-hide leaflet-interactive"
|
||||
role="button"
|
||||
src="marker-icon.png"
|
||||
style="margin-left: -12px; margin-top: -41px; width: 25px; height: 41px; left: -26px; top: 0px; z-index: 0;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="leaflet-pane leaflet-tooltip-pane"
|
||||
/>
|
||||
<div
|
||||
class="leaflet-pane leaflet-popup-pane"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="leaflet-control-container"
|
||||
>
|
||||
<div
|
||||
class="leaflet-top leaflet-left"
|
||||
>
|
||||
<div
|
||||
class="leaflet-control-zoom leaflet-bar leaflet-control"
|
||||
>
|
||||
<a
|
||||
aria-disabled="false"
|
||||
aria-label="Zoom in"
|
||||
class="leaflet-control-zoom-in"
|
||||
href="#"
|
||||
role="button"
|
||||
title="Zoom in"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
+
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
aria-disabled="true"
|
||||
aria-label="Zoom out"
|
||||
class="leaflet-control-zoom-out leaflet-disabled"
|
||||
href="#"
|
||||
role="button"
|
||||
title="Zoom out"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
−
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="leaflet-top leaflet-right"
|
||||
/>
|
||||
<div
|
||||
class="leaflet-bottom leaflet-left"
|
||||
/>
|
||||
<div
|
||||
class="leaflet-bottom leaflet-right"
|
||||
>
|
||||
<div
|
||||
class="leaflet-control-attribution leaflet-control"
|
||||
>
|
||||
<a
|
||||
href="https://leafletjs.com"
|
||||
title="A JavaScript library for interactive maps"
|
||||
>
|
||||
Leaflet
|
||||
</a>
|
||||
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
|
|
||||
</span>
|
||||
©
|
||||
<a
|
||||
href="https://osm.org/copyright"
|
||||
>
|
||||
OpenStreetMap
|
||||
</a>
|
||||
contributors
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,351 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<OpenMapModalBtn /> > filters out non-active cities from list of locations 1`] = `
|
||||
<div
|
||||
class="modal fade"
|
||||
role="dialog"
|
||||
style="display: block;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="modal-dialog map-modal__modal"
|
||||
role="document"
|
||||
>
|
||||
<div
|
||||
class="modal-content map-modal__modal-content"
|
||||
>
|
||||
<div
|
||||
class="map-modal__modal-body modal-body"
|
||||
>
|
||||
<h3
|
||||
class="map-modal__modal-title"
|
||||
>
|
||||
Foo
|
||||
<button
|
||||
aria-label="Close"
|
||||
class="btn-close float-end"
|
||||
type="button"
|
||||
/>
|
||||
</h3>
|
||||
<div
|
||||
class="leaflet-container leaflet-touch leaflet-grab leaflet-touch-drag leaflet-touch-zoom"
|
||||
style="position: relative;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="leaflet-pane leaflet-map-pane"
|
||||
style="left: 0px; top: 0px;"
|
||||
>
|
||||
<div
|
||||
class="leaflet-pane leaflet-tile-pane"
|
||||
>
|
||||
<div
|
||||
class="leaflet-layer "
|
||||
style="z-index: 1;"
|
||||
>
|
||||
<div
|
||||
class="leaflet-tile-container leaflet-zoom-animated"
|
||||
style="z-index: 18; left: 0px; top: 0px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="leaflet-tile"
|
||||
src="https://a.tile.openstreetmap.org/0/0/0.png"
|
||||
style="width: 256px; height: 256px; left: -161px; top: -62px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="leaflet-pane leaflet-overlay-pane"
|
||||
/>
|
||||
<div
|
||||
class="leaflet-pane leaflet-shadow-pane"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="leaflet-marker-shadow leaflet-zoom-hide"
|
||||
src="marker-shadow.png"
|
||||
style="margin-left: -12px; margin-top: -41px; width: 41px; height: 41px; left: -29px; top: 62px;"
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
class="leaflet-marker-shadow leaflet-zoom-hide"
|
||||
src="marker-shadow.png"
|
||||
style="margin-left: -12px; margin-top: -41px; width: 41px; height: 41px; left: 30px; top: -62px;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="leaflet-pane leaflet-marker-pane"
|
||||
>
|
||||
<img
|
||||
alt="Marker"
|
||||
class="leaflet-marker-icon leaflet-zoom-hide leaflet-interactive"
|
||||
role="button"
|
||||
src="marker-icon.png"
|
||||
style="margin-left: -12px; margin-top: -41px; width: 25px; height: 41px; left: -29px; top: 62px; z-index: 62;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<img
|
||||
alt="Marker"
|
||||
class="leaflet-marker-icon leaflet-zoom-hide leaflet-interactive"
|
||||
role="button"
|
||||
src="marker-icon.png"
|
||||
style="margin-left: -12px; margin-top: -41px; width: 25px; height: 41px; left: 30px; top: -62px; z-index: -62;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="leaflet-pane leaflet-tooltip-pane"
|
||||
/>
|
||||
<div
|
||||
class="leaflet-pane leaflet-popup-pane"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="leaflet-control-container"
|
||||
>
|
||||
<div
|
||||
class="leaflet-top leaflet-left"
|
||||
>
|
||||
<div
|
||||
class="leaflet-control-zoom leaflet-bar leaflet-control"
|
||||
>
|
||||
<a
|
||||
aria-disabled="false"
|
||||
aria-label="Zoom in"
|
||||
class="leaflet-control-zoom-in"
|
||||
href="#"
|
||||
role="button"
|
||||
title="Zoom in"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
+
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
aria-disabled="true"
|
||||
aria-label="Zoom out"
|
||||
class="leaflet-control-zoom-out leaflet-disabled"
|
||||
href="#"
|
||||
role="button"
|
||||
title="Zoom out"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
−
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="leaflet-top leaflet-right"
|
||||
/>
|
||||
<div
|
||||
class="leaflet-bottom leaflet-left"
|
||||
/>
|
||||
<div
|
||||
class="leaflet-bottom leaflet-right"
|
||||
>
|
||||
<div
|
||||
class="leaflet-control-attribution leaflet-control"
|
||||
>
|
||||
<a
|
||||
href="https://leafletjs.com"
|
||||
title="A JavaScript library for interactive maps"
|
||||
>
|
||||
Leaflet
|
||||
</a>
|
||||
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
|
|
||||
</span>
|
||||
©
|
||||
<a
|
||||
href="https://osm.org/copyright"
|
||||
>
|
||||
OpenStreetMap
|
||||
</a>
|
||||
contributors
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<OpenMapModalBtn /> > filters out non-active cities from list of locations 2`] = `
|
||||
<div
|
||||
class="modal fade"
|
||||
role="dialog"
|
||||
style="display: block;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="modal-dialog map-modal__modal"
|
||||
role="document"
|
||||
>
|
||||
<div
|
||||
class="modal-content map-modal__modal-content"
|
||||
>
|
||||
<div
|
||||
class="map-modal__modal-body modal-body"
|
||||
>
|
||||
<h3
|
||||
class="map-modal__modal-title"
|
||||
>
|
||||
Foo
|
||||
<button
|
||||
aria-label="Close"
|
||||
class="btn-close float-end"
|
||||
type="button"
|
||||
/>
|
||||
</h3>
|
||||
<div
|
||||
class="leaflet-container leaflet-touch leaflet-grab leaflet-touch-drag leaflet-touch-zoom"
|
||||
style="position: relative;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="leaflet-pane leaflet-map-pane"
|
||||
style="left: 0px; top: 0px;"
|
||||
>
|
||||
<div
|
||||
class="leaflet-pane leaflet-tile-pane"
|
||||
>
|
||||
<div
|
||||
class="leaflet-layer "
|
||||
style="z-index: 1;"
|
||||
>
|
||||
<div
|
||||
class="leaflet-tile-container leaflet-zoom-animated"
|
||||
style="z-index: 18; left: 0px; top: 0px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="leaflet-tile"
|
||||
src="https://a.tile.openstreetmap.org/10/762/0.png"
|
||||
style="width: 256px; height: 256px; left: -80px; top: 0px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="leaflet-pane leaflet-overlay-pane"
|
||||
/>
|
||||
<div
|
||||
class="leaflet-pane leaflet-shadow-pane"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="leaflet-marker-shadow leaflet-zoom-hide"
|
||||
src="marker-shadow.png"
|
||||
style="margin-left: -12px; margin-top: -41px; width: 41px; height: 41px; left: 0px; top: 0px;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="leaflet-pane leaflet-marker-pane"
|
||||
>
|
||||
<img
|
||||
alt="Marker"
|
||||
class="leaflet-marker-icon leaflet-zoom-hide leaflet-interactive"
|
||||
role="button"
|
||||
src="marker-icon.png"
|
||||
style="margin-left: -12px; margin-top: -41px; width: 25px; height: 41px; left: 0px; top: 0px; z-index: 0;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="leaflet-pane leaflet-tooltip-pane"
|
||||
/>
|
||||
<div
|
||||
class="leaflet-pane leaflet-popup-pane"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="leaflet-control-container"
|
||||
>
|
||||
<div
|
||||
class="leaflet-top leaflet-left"
|
||||
>
|
||||
<div
|
||||
class="leaflet-control-zoom leaflet-bar leaflet-control"
|
||||
>
|
||||
<a
|
||||
aria-disabled="false"
|
||||
aria-label="Zoom in"
|
||||
class="leaflet-control-zoom-in"
|
||||
href="#"
|
||||
role="button"
|
||||
title="Zoom in"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
+
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
aria-disabled="false"
|
||||
aria-label="Zoom out"
|
||||
class="leaflet-control-zoom-out"
|
||||
href="#"
|
||||
role="button"
|
||||
title="Zoom out"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
−
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="leaflet-top leaflet-right"
|
||||
/>
|
||||
<div
|
||||
class="leaflet-bottom leaflet-left"
|
||||
/>
|
||||
<div
|
||||
class="leaflet-bottom leaflet-right"
|
||||
>
|
||||
<div
|
||||
class="leaflet-control-attribution leaflet-control"
|
||||
>
|
||||
<a
|
||||
href="https://leafletjs.com"
|
||||
title="A JavaScript library for interactive maps"
|
||||
>
|
||||
Leaflet
|
||||
</a>
|
||||
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
|
|
||||
</span>
|
||||
©
|
||||
<a
|
||||
href="https://osm.org/copyright"
|
||||
>
|
||||
OpenStreetMap
|
||||
</a>
|
||||
contributors
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
218
shlink-web-component/test/visits/reducers/domainVisits.test.ts
Normal file
218
shlink-web-component/test/visits/reducers/domainVisits.test.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { addDays, formatISO, subDays } from 'date-fns';
|
||||
import type { ShlinkApiClient } from '../../../../src/api/services/ShlinkApiClient';
|
||||
import type { ShlinkState } from '../../../../src/container/types';
|
||||
import { rangeOf } from '../../../../src/utils/utils';
|
||||
import type { ShlinkVisits } from '../../../src/api/types';
|
||||
import type { ShortUrl } from '../../../src/short-urls/data';
|
||||
import { formatIsoDate } from '../../../src/utils/dates/helpers/date';
|
||||
import type { DateInterval } from '../../../src/utils/dates/helpers/dateIntervals';
|
||||
import type {
|
||||
DomainVisits, LoadDomainVisits,
|
||||
} from '../../../src/visits/reducers/domainVisits';
|
||||
import {
|
||||
DEFAULT_DOMAIN,
|
||||
domainVisitsReducerCreator,
|
||||
getDomainVisits as getDomainVisitsCreator,
|
||||
} from '../../../src/visits/reducers/domainVisits';
|
||||
import { createNewVisits } from '../../../src/visits/reducers/visitCreation';
|
||||
import type { Visit } from '../../../src/visits/types';
|
||||
|
||||
describe('domainVisitsReducer', () => {
|
||||
const now = new Date();
|
||||
const visitsMocks = rangeOf(2, () => fromPartial<Visit>({}));
|
||||
const getDomainVisitsCall = vi.fn();
|
||||
const buildApiClientMock = () => fromPartial<ShlinkApiClient>({ getDomainVisits: getDomainVisitsCall });
|
||||
const getDomainVisits = getDomainVisitsCreator(buildApiClientMock);
|
||||
const { reducer, cancelGetVisits: cancelGetDomainVisits } = domainVisitsReducerCreator(getDomainVisits);
|
||||
|
||||
describe('reducer', () => {
|
||||
const buildState = (data: Partial<DomainVisits>) => fromPartial<DomainVisits>(data);
|
||||
|
||||
it('returns loading on GET_DOMAIN_VISITS_START', () => {
|
||||
const { loading } = reducer(
|
||||
buildState({ loading: false }),
|
||||
getDomainVisits.pending('', fromPartial<LoadDomainVisits>({})),
|
||||
);
|
||||
expect(loading).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns loadingLarge on GET_DOMAIN_VISITS_LARGE', () => {
|
||||
const { loadingLarge } = reducer(buildState({ loadingLarge: false }), getDomainVisits.large());
|
||||
expect(loadingLarge).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns cancelLoad on GET_DOMAIN_VISITS_CANCEL', () => {
|
||||
const { cancelLoad } = reducer(buildState({ cancelLoad: false }), cancelGetDomainVisits());
|
||||
expect(cancelLoad).toEqual(true);
|
||||
});
|
||||
|
||||
it('stops loading and returns error on GET_DOMAIN_VISITS_ERROR', () => {
|
||||
const state = reducer(
|
||||
buildState({ loading: true, error: false }),
|
||||
getDomainVisits.rejected(null, '', fromPartial({})),
|
||||
);
|
||||
const { loading, error } = state;
|
||||
|
||||
expect(loading).toEqual(false);
|
||||
expect(error).toEqual(true);
|
||||
});
|
||||
|
||||
it('return visits on GET_DOMAIN_VISITS', () => {
|
||||
const actionVisits: Visit[] = [fromPartial({}), fromPartial({})];
|
||||
const { loading, error, visits } = reducer(
|
||||
buildState({ loading: true, error: false }),
|
||||
getDomainVisits.fulfilled({ visits: actionVisits }, '', fromPartial({})),
|
||||
);
|
||||
|
||||
expect(loading).toEqual(false);
|
||||
expect(error).toEqual(false);
|
||||
expect(visits).toEqual(actionVisits);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ domain: 'foo.com' }, 'foo.com', visitsMocks.length + 1],
|
||||
[{ domain: 'bar.com' }, 'foo.com', visitsMocks.length],
|
||||
[fromPartial<DomainVisits>({ domain: 'foo.com' }), 'foo.com', visitsMocks.length + 1],
|
||||
[fromPartial<DomainVisits>({ domain: DEFAULT_DOMAIN }), null, visitsMocks.length + 1],
|
||||
[
|
||||
fromPartial<DomainVisits>({
|
||||
domain: 'foo.com',
|
||||
query: { endDate: formatIsoDate(subDays(now, 1)) ?? undefined },
|
||||
}),
|
||||
'foo.com',
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
fromPartial<DomainVisits>({
|
||||
domain: 'foo.com',
|
||||
query: { startDate: formatIsoDate(addDays(now, 1)) ?? undefined },
|
||||
}),
|
||||
'foo.com',
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
fromPartial<DomainVisits>({
|
||||
domain: 'foo.com',
|
||||
query: {
|
||||
startDate: formatIsoDate(subDays(now, 5)) ?? undefined,
|
||||
endDate: formatIsoDate(subDays(now, 2)) ?? undefined,
|
||||
},
|
||||
}),
|
||||
'foo.com',
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
fromPartial<DomainVisits>({
|
||||
domain: 'foo.com',
|
||||
query: {
|
||||
startDate: formatIsoDate(subDays(now, 5)) ?? undefined,
|
||||
endDate: formatIsoDate(addDays(now, 3)) ?? undefined,
|
||||
},
|
||||
}),
|
||||
'foo.com',
|
||||
visitsMocks.length + 1,
|
||||
],
|
||||
[
|
||||
fromPartial<DomainVisits>({
|
||||
domain: 'bar.com',
|
||||
query: {
|
||||
startDate: formatIsoDate(subDays(now, 5)) ?? undefined,
|
||||
endDate: formatIsoDate(addDays(now, 3)) ?? undefined,
|
||||
},
|
||||
}),
|
||||
'foo.com',
|
||||
visitsMocks.length,
|
||||
],
|
||||
])('prepends new visits on CREATE_VISIT', (state, shortUrlDomain, expectedVisits) => {
|
||||
const shortUrl = fromPartial<ShortUrl>({ domain: shortUrlDomain });
|
||||
const { visits } = reducer(buildState({ ...state, visits: visitsMocks }), createNewVisits([
|
||||
fromPartial({ shortUrl, visit: { date: formatIsoDate(now) ?? undefined } }),
|
||||
]));
|
||||
|
||||
expect(visits).toHaveLength(expectedVisits);
|
||||
});
|
||||
|
||||
it('returns new progress on GET_DOMAIN_VISITS_PROGRESS_CHANGED', () => {
|
||||
const { progress } = reducer(undefined, getDomainVisits.progressChanged(85));
|
||||
expect(progress).toEqual(85);
|
||||
});
|
||||
|
||||
it('returns fallbackInterval on GET_DOMAIN_VISITS_FALLBACK_TO_INTERVAL', () => {
|
||||
const fallbackInterval: DateInterval = 'last30Days';
|
||||
const state = reducer(
|
||||
undefined,
|
||||
getDomainVisits.fallbackToInterval(fallbackInterval),
|
||||
);
|
||||
|
||||
expect(state).toEqual(expect.objectContaining({ fallbackInterval }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDomainVisits', () => {
|
||||
const dispatchMock = vi.fn();
|
||||
const getState = () => fromPartial<ShlinkState>({
|
||||
domainVisits: { cancelLoad: false },
|
||||
});
|
||||
const domain = 'foo.com';
|
||||
|
||||
it.each([
|
||||
[undefined],
|
||||
[{}],
|
||||
])('dispatches start and success when promise is resolved', async (query) => {
|
||||
const visits = visitsMocks;
|
||||
getDomainVisitsCall.mockResolvedValue({
|
||||
data: visitsMocks,
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
pagesCount: 1,
|
||||
totalItems: 1,
|
||||
},
|
||||
});
|
||||
|
||||
await getDomainVisits({ domain, query })(dispatchMock, getState, {});
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(2);
|
||||
expect(dispatchMock).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
payload: { visits, domain, query: query ?? {} },
|
||||
}));
|
||||
expect(getDomainVisitsCall).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
[fromPartial<Visit>({ date: formatISO(subDays(now, 20)) })],
|
||||
getDomainVisits.fallbackToInterval('last30Days'),
|
||||
3,
|
||||
],
|
||||
[
|
||||
[fromPartial<Visit>({ date: formatISO(subDays(now, 100)) })],
|
||||
getDomainVisits.fallbackToInterval('last180Days'),
|
||||
3,
|
||||
],
|
||||
[[], expect.objectContaining({ type: getDomainVisits.fulfilled.toString() }), 2],
|
||||
])('dispatches fallback interval when the list of visits is empty', async (
|
||||
lastVisits,
|
||||
expectedSecondDispatch,
|
||||
expectedDispatchCalls,
|
||||
) => {
|
||||
const buildVisitsResult = (data: Visit[] = []): ShlinkVisits => ({
|
||||
data,
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
pagesCount: 1,
|
||||
totalItems: 1,
|
||||
},
|
||||
});
|
||||
getDomainVisitsCall
|
||||
.mockResolvedValueOnce(buildVisitsResult())
|
||||
.mockResolvedValueOnce(buildVisitsResult(lastVisits));
|
||||
|
||||
await getDomainVisits({ domain, doIntervalFallback: true })(dispatchMock, getState, {});
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(expectedDispatchCalls);
|
||||
expect(dispatchMock).toHaveBeenNthCalledWith(2, expectedSecondDispatch);
|
||||
expect(getDomainVisitsCall).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,184 @@
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { addDays, formatISO, subDays } from 'date-fns';
|
||||
import type { ShlinkApiClient } from '../../../../src/api/services/ShlinkApiClient';
|
||||
import type { ShlinkState } from '../../../../src/container/types';
|
||||
import { rangeOf } from '../../../../src/utils/utils';
|
||||
import type { ShlinkVisits } from '../../../src/api/types';
|
||||
import { formatIsoDate } from '../../../src/utils/dates/helpers/date';
|
||||
import type { DateInterval } from '../../../src/utils/dates/helpers/dateIntervals';
|
||||
import {
|
||||
getNonOrphanVisits as getNonOrphanVisitsCreator,
|
||||
nonOrphanVisitsReducerCreator,
|
||||
} from '../../../src/visits/reducers/nonOrphanVisits';
|
||||
import type { VisitsInfo } from '../../../src/visits/reducers/types';
|
||||
import { createNewVisits } from '../../../src/visits/reducers/visitCreation';
|
||||
import type { Visit } from '../../../src/visits/types';
|
||||
|
||||
describe('nonOrphanVisitsReducer', () => {
|
||||
const now = new Date();
|
||||
const visitsMocks = rangeOf(2, () => fromPartial<Visit>({}));
|
||||
const getNonOrphanVisitsCall = vi.fn();
|
||||
const buildShlinkApiClient = () => fromPartial<ShlinkApiClient>({ getNonOrphanVisits: getNonOrphanVisitsCall });
|
||||
const getNonOrphanVisits = getNonOrphanVisitsCreator(buildShlinkApiClient);
|
||||
const { reducer, cancelGetVisits: cancelGetNonOrphanVisits } = nonOrphanVisitsReducerCreator(getNonOrphanVisits);
|
||||
|
||||
describe('reducer', () => {
|
||||
const buildState = (data: Partial<VisitsInfo>) => fromPartial<VisitsInfo>(data);
|
||||
|
||||
it('returns loading on GET_NON_ORPHAN_VISITS_START', () => {
|
||||
const { loading } = reducer(buildState({ loading: false }), getNonOrphanVisits.pending('', {}));
|
||||
expect(loading).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns loadingLarge on GET_NON_ORPHAN_VISITS_LARGE', () => {
|
||||
const { loadingLarge } = reducer(buildState({ loadingLarge: false }), getNonOrphanVisits.large());
|
||||
expect(loadingLarge).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns cancelLoad on GET_NON_ORPHAN_VISITS_CANCEL', () => {
|
||||
const { cancelLoad } = reducer(buildState({ cancelLoad: false }), cancelGetNonOrphanVisits());
|
||||
expect(cancelLoad).toEqual(true);
|
||||
});
|
||||
|
||||
it('stops loading and returns error on GET_NON_ORPHAN_VISITS_ERROR', () => {
|
||||
const { loading, error } = reducer(
|
||||
buildState({ loading: true, error: false }),
|
||||
getNonOrphanVisits.rejected(null, '', {}),
|
||||
);
|
||||
|
||||
expect(loading).toEqual(false);
|
||||
expect(error).toEqual(true);
|
||||
});
|
||||
|
||||
it('return visits on GET_NON_ORPHAN_VISITS', () => {
|
||||
const actionVisits: Visit[] = [fromPartial({}), fromPartial({})];
|
||||
const { loading, error, visits } = reducer(
|
||||
buildState({ loading: true, error: false }),
|
||||
getNonOrphanVisits.fulfilled({ visits: actionVisits }, '', {}),
|
||||
);
|
||||
|
||||
expect(loading).toEqual(false);
|
||||
expect(error).toEqual(false);
|
||||
expect(visits).toEqual(actionVisits);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{}, visitsMocks.length + 2],
|
||||
[
|
||||
fromPartial<VisitsInfo>({
|
||||
query: { endDate: formatIsoDate(subDays(now, 1)) ?? undefined },
|
||||
}),
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
fromPartial<VisitsInfo>({
|
||||
query: { startDate: formatIsoDate(addDays(now, 1)) ?? undefined },
|
||||
}),
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
fromPartial<VisitsInfo>({
|
||||
query: {
|
||||
startDate: formatIsoDate(subDays(now, 5)) ?? undefined,
|
||||
endDate: formatIsoDate(subDays(now, 2)) ?? undefined,
|
||||
},
|
||||
}),
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
fromPartial<VisitsInfo>({
|
||||
query: {
|
||||
startDate: formatIsoDate(subDays(now, 5)) ?? undefined,
|
||||
endDate: formatIsoDate(addDays(now, 3)) ?? undefined,
|
||||
},
|
||||
}),
|
||||
visitsMocks.length + 2,
|
||||
],
|
||||
])('prepends new visits on CREATE_VISIT', (state, expectedVisits) => {
|
||||
const prevState = buildState({ ...state, visits: visitsMocks });
|
||||
const visit = fromPartial<Visit>({ date: formatIsoDate(now) ?? undefined });
|
||||
|
||||
const { visits } = reducer(prevState, createNewVisits([{ visit }, { visit }]));
|
||||
|
||||
expect(visits).toHaveLength(expectedVisits);
|
||||
});
|
||||
|
||||
it('returns new progress on GET_NON_ORPHAN_VISITS_PROGRESS_CHANGED', () => {
|
||||
const { progress } = reducer(undefined, getNonOrphanVisits.progressChanged(85));
|
||||
expect(progress).toEqual(85);
|
||||
});
|
||||
|
||||
it('returns fallbackInterval on GET_NON_ORPHAN_VISITS_FALLBACK_TO_INTERVAL', () => {
|
||||
const fallbackInterval: DateInterval = 'last30Days';
|
||||
const state = reducer(undefined, getNonOrphanVisits.fallbackToInterval(fallbackInterval));
|
||||
|
||||
expect(state).toEqual(expect.objectContaining({ fallbackInterval }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNonOrphanVisits', () => {
|
||||
const dispatchMock = vi.fn();
|
||||
const getState = () => fromPartial<ShlinkState>({
|
||||
orphanVisits: { cancelLoad: false },
|
||||
});
|
||||
|
||||
it.each([
|
||||
[undefined],
|
||||
[{}],
|
||||
])('dispatches start and success when promise is resolved', async (query) => {
|
||||
const visits = visitsMocks.map((visit) => ({ ...visit, visitedUrl: '' }));
|
||||
getNonOrphanVisitsCall.mockResolvedValue({
|
||||
data: visits,
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
pagesCount: 1,
|
||||
totalItems: 1,
|
||||
},
|
||||
});
|
||||
|
||||
await getNonOrphanVisits({ query })(dispatchMock, getState, {});
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(2);
|
||||
expect(dispatchMock).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
payload: { visits, query: query ?? {} },
|
||||
}));
|
||||
expect(getNonOrphanVisitsCall).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
[fromPartial<Visit>({ date: formatISO(subDays(now, 5)) })],
|
||||
getNonOrphanVisits.fallbackToInterval('last7Days'),
|
||||
3,
|
||||
],
|
||||
[
|
||||
[fromPartial<Visit>({ date: formatISO(subDays(now, 200)) })],
|
||||
getNonOrphanVisits.fallbackToInterval('last365Days'),
|
||||
3,
|
||||
],
|
||||
[[], expect.objectContaining({ type: getNonOrphanVisits.fulfilled.toString() }), 2],
|
||||
])('dispatches fallback interval when the list of visits is empty', async (
|
||||
lastVisits,
|
||||
expectedSecondDispatch,
|
||||
expectedAmountOfDispatches,
|
||||
) => {
|
||||
const buildVisitsResult = (data: Visit[] = []): ShlinkVisits => ({
|
||||
data,
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
pagesCount: 1,
|
||||
totalItems: 1,
|
||||
},
|
||||
});
|
||||
getNonOrphanVisitsCall
|
||||
.mockResolvedValueOnce(buildVisitsResult())
|
||||
.mockResolvedValueOnce(buildVisitsResult(lastVisits));
|
||||
|
||||
await getNonOrphanVisits({ doIntervalFallback: true })(dispatchMock, getState, {});
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(expectedAmountOfDispatches);
|
||||
expect(dispatchMock).toHaveBeenNthCalledWith(2, expectedSecondDispatch);
|
||||
expect(getNonOrphanVisitsCall).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
184
shlink-web-component/test/visits/reducers/orphanVisits.test.ts
Normal file
184
shlink-web-component/test/visits/reducers/orphanVisits.test.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { addDays, formatISO, subDays } from 'date-fns';
|
||||
import type { ShlinkApiClient } from '../../../../src/api/services/ShlinkApiClient';
|
||||
import type { ShlinkState } from '../../../../src/container/types';
|
||||
import { rangeOf } from '../../../../src/utils/utils';
|
||||
import type { ShlinkVisits } from '../../../src/api/types';
|
||||
import { formatIsoDate } from '../../../src/utils/dates/helpers/date';
|
||||
import type { DateInterval } from '../../../src/utils/dates/helpers/dateIntervals';
|
||||
import {
|
||||
getOrphanVisits as getOrphanVisitsCreator,
|
||||
orphanVisitsReducerCreator,
|
||||
} from '../../../src/visits/reducers/orphanVisits';
|
||||
import type { VisitsInfo } from '../../../src/visits/reducers/types';
|
||||
import { createNewVisits } from '../../../src/visits/reducers/visitCreation';
|
||||
import type { Visit } from '../../../src/visits/types';
|
||||
|
||||
describe('orphanVisitsReducer', () => {
|
||||
const now = new Date();
|
||||
const visitsMocks = rangeOf(2, () => fromPartial<Visit>({}));
|
||||
const getOrphanVisitsCall = vi.fn();
|
||||
const buildShlinkApiClientMock = () => fromPartial<ShlinkApiClient>({ getOrphanVisits: getOrphanVisitsCall });
|
||||
const getOrphanVisits = getOrphanVisitsCreator(buildShlinkApiClientMock);
|
||||
const { reducer, cancelGetVisits: cancelGetOrphanVisits } = orphanVisitsReducerCreator(getOrphanVisits);
|
||||
|
||||
describe('reducer', () => {
|
||||
const buildState = (data: Partial<VisitsInfo>) => fromPartial<VisitsInfo>(data);
|
||||
|
||||
it('returns loading on GET_ORPHAN_VISITS_START', () => {
|
||||
const { loading } = reducer(buildState({ loading: false }), getOrphanVisits.pending('', {}));
|
||||
expect(loading).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns loadingLarge on GET_ORPHAN_VISITS_LARGE', () => {
|
||||
const { loadingLarge } = reducer(buildState({ loadingLarge: false }), getOrphanVisits.large());
|
||||
expect(loadingLarge).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns cancelLoad on GET_ORPHAN_VISITS_CANCEL', () => {
|
||||
const { cancelLoad } = reducer(buildState({ cancelLoad: false }), cancelGetOrphanVisits());
|
||||
expect(cancelLoad).toEqual(true);
|
||||
});
|
||||
|
||||
it('stops loading and returns error on GET_ORPHAN_VISITS_ERROR', () => {
|
||||
const { loading, error } = reducer(
|
||||
buildState({ loading: true, error: false }),
|
||||
getOrphanVisits.rejected(null, '', {}),
|
||||
);
|
||||
|
||||
expect(loading).toEqual(false);
|
||||
expect(error).toEqual(true);
|
||||
});
|
||||
|
||||
it('return visits on GET_ORPHAN_VISITS', () => {
|
||||
const actionVisits: Visit[] = [fromPartial({}), fromPartial({})];
|
||||
const { loading, error, visits } = reducer(
|
||||
buildState({ loading: true, error: false }),
|
||||
getOrphanVisits.fulfilled({ visits: actionVisits }, '', {}),
|
||||
);
|
||||
|
||||
expect(loading).toEqual(false);
|
||||
expect(error).toEqual(false);
|
||||
expect(visits).toEqual(actionVisits);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{}, visitsMocks.length + 2],
|
||||
[
|
||||
fromPartial<VisitsInfo>({
|
||||
query: { endDate: formatIsoDate(subDays(now, 1)) ?? undefined },
|
||||
}),
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
fromPartial<VisitsInfo>({
|
||||
query: { startDate: formatIsoDate(addDays(now, 1)) ?? undefined },
|
||||
}),
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
fromPartial<VisitsInfo>({
|
||||
query: {
|
||||
startDate: formatIsoDate(subDays(now, 5)) ?? undefined,
|
||||
endDate: formatIsoDate(subDays(now, 2)) ?? undefined,
|
||||
},
|
||||
}),
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
fromPartial<VisitsInfo>({
|
||||
query: {
|
||||
startDate: formatIsoDate(subDays(now, 5)) ?? undefined,
|
||||
endDate: formatIsoDate(addDays(now, 3)) ?? undefined,
|
||||
},
|
||||
}),
|
||||
visitsMocks.length + 2,
|
||||
],
|
||||
])('prepends new visits on CREATE_VISIT', (state, expectedVisits) => {
|
||||
const prevState = buildState({ ...state, visits: visitsMocks });
|
||||
const visit = fromPartial<Visit>({ date: formatIsoDate(now) ?? undefined });
|
||||
|
||||
const { visits } = reducer(prevState, createNewVisits([{ visit }, { visit }]));
|
||||
|
||||
expect(visits).toHaveLength(expectedVisits);
|
||||
});
|
||||
|
||||
it('returns new progress on GET_ORPHAN_VISITS_PROGRESS_CHANGED', () => {
|
||||
const { progress } = reducer(undefined, getOrphanVisits.progressChanged(85));
|
||||
expect(progress).toEqual(85);
|
||||
});
|
||||
|
||||
it('returns fallbackInterval on GET_ORPHAN_VISITS_FALLBACK_TO_INTERVAL', () => {
|
||||
const fallbackInterval: DateInterval = 'last30Days';
|
||||
const state = reducer(undefined, getOrphanVisits.fallbackToInterval(fallbackInterval));
|
||||
|
||||
expect(state).toEqual(expect.objectContaining({ fallbackInterval }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOrphanVisits', () => {
|
||||
const dispatchMock = vi.fn();
|
||||
const getState = () => fromPartial<ShlinkState>({
|
||||
orphanVisits: { cancelLoad: false },
|
||||
});
|
||||
|
||||
it.each([
|
||||
[undefined],
|
||||
[{}],
|
||||
])('dispatches start and success when promise is resolved', async (query) => {
|
||||
const visits = visitsMocks.map((visit) => ({ ...visit, visitedUrl: '' }));
|
||||
getOrphanVisitsCall.mockResolvedValue({
|
||||
data: visits,
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
pagesCount: 1,
|
||||
totalItems: 1,
|
||||
},
|
||||
});
|
||||
|
||||
await getOrphanVisits({ query })(dispatchMock, getState, {});
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(2);
|
||||
expect(dispatchMock).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
payload: { visits, query: query ?? {} },
|
||||
}));
|
||||
expect(getOrphanVisitsCall).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
[fromPartial<Visit>({ date: formatISO(subDays(now, 5)) })],
|
||||
getOrphanVisits.fallbackToInterval('last7Days'),
|
||||
3,
|
||||
],
|
||||
[
|
||||
[fromPartial<Visit>({ date: formatISO(subDays(now, 200)) })],
|
||||
getOrphanVisits.fallbackToInterval('last365Days'),
|
||||
3,
|
||||
],
|
||||
[[], expect.objectContaining({ type: getOrphanVisits.fulfilled.toString() }), 2],
|
||||
])('dispatches fallback interval when the list of visits is empty', async (
|
||||
lastVisits,
|
||||
expectedSecondDispatch,
|
||||
expectedDispatchCalls,
|
||||
) => {
|
||||
const buildVisitsResult = (data: Visit[] = []): ShlinkVisits => ({
|
||||
data,
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
pagesCount: 1,
|
||||
totalItems: 1,
|
||||
},
|
||||
});
|
||||
getOrphanVisitsCall
|
||||
.mockResolvedValueOnce(buildVisitsResult())
|
||||
.mockResolvedValueOnce(buildVisitsResult(lastVisits));
|
||||
|
||||
await getOrphanVisits({ doIntervalFallback: true })(dispatchMock, getState, {});
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(expectedDispatchCalls);
|
||||
expect(dispatchMock).toHaveBeenNthCalledWith(2, expectedSecondDispatch);
|
||||
expect(getOrphanVisitsCall).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
232
shlink-web-component/test/visits/reducers/shortUrlVisits.test.ts
Normal file
232
shlink-web-component/test/visits/reducers/shortUrlVisits.test.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { addDays, formatISO, subDays } from 'date-fns';
|
||||
import type { ShlinkApiClient } from '../../../../src/api/services/ShlinkApiClient';
|
||||
import type { ShlinkState } from '../../../../src/container/types';
|
||||
import { rangeOf } from '../../../../src/utils/utils';
|
||||
import type { ShlinkVisits } from '../../../src/api/types';
|
||||
import { formatIsoDate } from '../../../src/utils/dates/helpers/date';
|
||||
import type { DateInterval } from '../../../src/utils/dates/helpers/dateIntervals';
|
||||
import type {
|
||||
ShortUrlVisits } from '../../../src/visits/reducers/shortUrlVisits';
|
||||
import {
|
||||
getShortUrlVisits as getShortUrlVisitsCreator,
|
||||
shortUrlVisitsReducerCreator,
|
||||
} from '../../../src/visits/reducers/shortUrlVisits';
|
||||
import { createNewVisits } from '../../../src/visits/reducers/visitCreation';
|
||||
import type { Visit } from '../../../src/visits/types';
|
||||
|
||||
describe('shortUrlVisitsReducer', () => {
|
||||
const now = new Date();
|
||||
const visitsMocks = rangeOf(2, () => fromPartial<Visit>({}));
|
||||
const getShortUrlVisitsCall = vi.fn();
|
||||
const buildApiClientMock = () => fromPartial<ShlinkApiClient>({ getShortUrlVisits: getShortUrlVisitsCall });
|
||||
const getShortUrlVisits = getShortUrlVisitsCreator(buildApiClientMock);
|
||||
const { reducer, cancelGetVisits: cancelGetShortUrlVisits } = shortUrlVisitsReducerCreator(getShortUrlVisits);
|
||||
|
||||
describe('reducer', () => {
|
||||
const buildState = (data: Partial<ShortUrlVisits>) => fromPartial<ShortUrlVisits>(data);
|
||||
|
||||
it('returns loading on GET_SHORT_URL_VISITS_START', () => {
|
||||
const { loading } = reducer(buildState({ loading: false }), getShortUrlVisits.pending('', { shortCode: '' }));
|
||||
expect(loading).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns loadingLarge on GET_SHORT_URL_VISITS_LARGE', () => {
|
||||
const { loadingLarge } = reducer(buildState({ loadingLarge: false }), getShortUrlVisits.large());
|
||||
expect(loadingLarge).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns cancelLoad on GET_SHORT_URL_VISITS_CANCEL', () => {
|
||||
const { cancelLoad } = reducer(buildState({ cancelLoad: false }), cancelGetShortUrlVisits());
|
||||
expect(cancelLoad).toEqual(true);
|
||||
});
|
||||
|
||||
it('stops loading and returns error on GET_SHORT_URL_VISITS_ERROR', () => {
|
||||
const { loading, error } = reducer(
|
||||
buildState({ loading: true, error: false }),
|
||||
getShortUrlVisits.rejected(null, '', { shortCode: '' }),
|
||||
);
|
||||
|
||||
expect(loading).toEqual(false);
|
||||
expect(error).toEqual(true);
|
||||
});
|
||||
|
||||
it('return visits on GET_SHORT_URL_VISITS', () => {
|
||||
const actionVisits: Visit[] = [fromPartial({}), fromPartial({})];
|
||||
const { loading, error, visits } = reducer(
|
||||
buildState({ loading: true, error: false }),
|
||||
getShortUrlVisits.fulfilled({ visits: actionVisits }, '', { shortCode: '' }),
|
||||
);
|
||||
|
||||
expect(loading).toEqual(false);
|
||||
expect(error).toEqual(false);
|
||||
expect(visits).toEqual(actionVisits);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ shortCode: 'abc123' }, visitsMocks.length + 1],
|
||||
[{ shortCode: 'def456' }, visitsMocks.length],
|
||||
[
|
||||
fromPartial<ShortUrlVisits>({
|
||||
shortCode: 'abc123',
|
||||
query: { endDate: formatIsoDate(subDays(now, 1)) ?? undefined },
|
||||
}),
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
fromPartial<ShortUrlVisits>({
|
||||
shortCode: 'abc123',
|
||||
query: { startDate: formatIsoDate(addDays(now, 1)) ?? undefined },
|
||||
}),
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
fromPartial<ShortUrlVisits>({
|
||||
shortCode: 'abc123',
|
||||
query: {
|
||||
startDate: formatIsoDate(subDays(now, 5)) ?? undefined,
|
||||
endDate: formatIsoDate(subDays(now, 2)) ?? undefined,
|
||||
},
|
||||
}),
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
fromPartial<ShortUrlVisits>({
|
||||
shortCode: 'abc123',
|
||||
query: {
|
||||
startDate: formatIsoDate(subDays(now, 5)) ?? undefined,
|
||||
endDate: formatIsoDate(addDays(now, 3)) ?? undefined,
|
||||
},
|
||||
}),
|
||||
visitsMocks.length + 1,
|
||||
],
|
||||
[
|
||||
fromPartial<ShortUrlVisits>({
|
||||
shortCode: 'def456',
|
||||
query: {
|
||||
startDate: formatIsoDate(subDays(now, 5)) ?? undefined,
|
||||
endDate: formatIsoDate(addDays(now, 3)) ?? undefined,
|
||||
},
|
||||
}),
|
||||
visitsMocks.length,
|
||||
],
|
||||
])('prepends new visits on CREATE_VISIT', (state, expectedVisits) => {
|
||||
const shortUrl = {
|
||||
shortCode: 'abc123',
|
||||
};
|
||||
const prevState = buildState({
|
||||
...state,
|
||||
visits: visitsMocks,
|
||||
});
|
||||
|
||||
const { visits } = reducer(
|
||||
prevState,
|
||||
createNewVisits([fromPartial({ shortUrl, visit: { date: formatIsoDate(now) ?? undefined } })]),
|
||||
);
|
||||
|
||||
expect(visits).toHaveLength(expectedVisits);
|
||||
});
|
||||
|
||||
it('returns new progress on GET_SHORT_URL_VISITS_PROGRESS_CHANGED', () => {
|
||||
const { progress } = reducer(undefined, getShortUrlVisits.progressChanged(85));
|
||||
expect(progress).toEqual(85);
|
||||
});
|
||||
|
||||
it('returns fallbackInterval on GET_SHORT_URL_VISITS_FALLBACK_TO_INTERVAL', () => {
|
||||
const fallbackInterval: DateInterval = 'last30Days';
|
||||
const state = reducer(undefined, getShortUrlVisits.fallbackToInterval(fallbackInterval));
|
||||
|
||||
expect(state).toEqual(expect.objectContaining({ fallbackInterval }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getShortUrlVisits', () => {
|
||||
const dispatchMock = vi.fn();
|
||||
const getState = () => fromPartial<ShlinkState>({
|
||||
shortUrlVisits: { cancelLoad: false },
|
||||
});
|
||||
|
||||
it.each([
|
||||
[undefined, undefined],
|
||||
[{}, undefined],
|
||||
[{ domain: 'foobar.com' }, 'foobar.com'],
|
||||
])('dispatches start and success when promise is resolved', async (query, domain) => {
|
||||
const visits = visitsMocks;
|
||||
const shortCode = 'abc123';
|
||||
getShortUrlVisitsCall.mockResolvedValue({
|
||||
data: visitsMocks,
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
pagesCount: 1,
|
||||
totalItems: 1,
|
||||
},
|
||||
});
|
||||
|
||||
await getShortUrlVisits({ shortCode, query })(dispatchMock, getState, {});
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(2);
|
||||
expect(dispatchMock).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
payload: { visits, shortCode, domain, query: query ?? {} },
|
||||
}));
|
||||
expect(getShortUrlVisitsCall).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('performs multiple API requests when response contains more pages', async () => {
|
||||
const expectedRequests = 3;
|
||||
getShortUrlVisitsCall.mockImplementation(async (_, { page }) =>
|
||||
Promise.resolve({
|
||||
data: visitsMocks,
|
||||
pagination: {
|
||||
currentPage: page,
|
||||
pagesCount: expectedRequests,
|
||||
totalItems: 1,
|
||||
},
|
||||
}));
|
||||
|
||||
await getShortUrlVisits({ shortCode: 'abc123' })(dispatchMock, getState, {});
|
||||
|
||||
expect(getShortUrlVisitsCall).toHaveBeenCalledTimes(expectedRequests);
|
||||
expect(dispatchMock).toHaveBeenNthCalledWith(3, expect.objectContaining({
|
||||
payload: expect.objectContaining({
|
||||
visits: [...visitsMocks, ...visitsMocks, ...visitsMocks],
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
[fromPartial<Visit>({ date: formatISO(subDays(now, 5)) })],
|
||||
getShortUrlVisits.fallbackToInterval('last7Days'),
|
||||
3,
|
||||
],
|
||||
[
|
||||
[fromPartial<Visit>({ date: formatISO(subDays(now, 200)) })],
|
||||
getShortUrlVisits.fallbackToInterval('last365Days'),
|
||||
3,
|
||||
],
|
||||
[[], expect.objectContaining({ type: getShortUrlVisits.fulfilled.toString() }), 2],
|
||||
])('dispatches fallback interval when the list of visits is empty', async (
|
||||
lastVisits,
|
||||
expectedSecondDispatch,
|
||||
expectedDispatchCalls,
|
||||
) => {
|
||||
const buildVisitsResult = (data: Visit[] = []): ShlinkVisits => ({
|
||||
data,
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
pagesCount: 1,
|
||||
totalItems: 1,
|
||||
},
|
||||
});
|
||||
getShortUrlVisitsCall
|
||||
.mockResolvedValueOnce(buildVisitsResult())
|
||||
.mockResolvedValueOnce(buildVisitsResult(lastVisits));
|
||||
|
||||
await getShortUrlVisits({ shortCode: 'abc123', doIntervalFallback: true })(dispatchMock, getState, {});
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(expectedDispatchCalls);
|
||||
expect(dispatchMock).toHaveBeenNthCalledWith(2, expectedSecondDispatch);
|
||||
expect(getShortUrlVisitsCall).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
209
shlink-web-component/test/visits/reducers/tagVisits.test.ts
Normal file
209
shlink-web-component/test/visits/reducers/tagVisits.test.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { addDays, formatISO, subDays } from 'date-fns';
|
||||
import type { ShlinkApiClient } from '../../../../src/api/services/ShlinkApiClient';
|
||||
import type { ShlinkState } from '../../../../src/container/types';
|
||||
import { rangeOf } from '../../../../src/utils/utils';
|
||||
import type { ShlinkVisits } from '../../../src/api/types';
|
||||
import { formatIsoDate } from '../../../src/utils/dates/helpers/date';
|
||||
import type { DateInterval } from '../../../src/utils/dates/helpers/dateIntervals';
|
||||
import type {
|
||||
TagVisits } from '../../../src/visits/reducers/tagVisits';
|
||||
import {
|
||||
getTagVisits as getTagVisitsCreator,
|
||||
tagVisitsReducerCreator,
|
||||
} from '../../../src/visits/reducers/tagVisits';
|
||||
import { createNewVisits } from '../../../src/visits/reducers/visitCreation';
|
||||
import type { Visit } from '../../../src/visits/types';
|
||||
|
||||
describe('tagVisitsReducer', () => {
|
||||
const now = new Date();
|
||||
const visitsMocks = rangeOf(2, () => fromPartial<Visit>({}));
|
||||
const getTagVisitsCall = vi.fn();
|
||||
const buildShlinkApiClientMock = () => fromPartial<ShlinkApiClient>({ getTagVisits: getTagVisitsCall });
|
||||
const getTagVisits = getTagVisitsCreator(buildShlinkApiClientMock);
|
||||
const { reducer, cancelGetVisits: cancelGetTagVisits } = tagVisitsReducerCreator(getTagVisits);
|
||||
|
||||
describe('reducer', () => {
|
||||
const buildState = (data: Partial<TagVisits>) => fromPartial<TagVisits>(data);
|
||||
|
||||
it('returns loading on GET_TAG_VISITS_START', () => {
|
||||
const { loading } = reducer(buildState({ loading: false }), getTagVisits.pending('', { tag: '' }));
|
||||
expect(loading).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns loadingLarge on GET_TAG_VISITS_LARGE', () => {
|
||||
const { loadingLarge } = reducer(buildState({ loadingLarge: false }), getTagVisits.large());
|
||||
expect(loadingLarge).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns cancelLoad on GET_TAG_VISITS_CANCEL', () => {
|
||||
const { cancelLoad } = reducer(buildState({ cancelLoad: false }), cancelGetTagVisits());
|
||||
expect(cancelLoad).toEqual(true);
|
||||
});
|
||||
|
||||
it('stops loading and returns error on GET_TAG_VISITS_ERROR', () => {
|
||||
const { loading, error } = reducer(
|
||||
buildState({ loading: true, error: false }),
|
||||
getTagVisits.rejected(null, '', { tag: '' }),
|
||||
);
|
||||
|
||||
expect(loading).toEqual(false);
|
||||
expect(error).toEqual(true);
|
||||
});
|
||||
|
||||
it('return visits on GET_TAG_VISITS', () => {
|
||||
const actionVisits: Visit[] = [fromPartial({}), fromPartial({})];
|
||||
const { loading, error, visits } = reducer(
|
||||
buildState({ loading: true, error: false }),
|
||||
getTagVisits.fulfilled({ visits: actionVisits }, '', { tag: '' }),
|
||||
);
|
||||
|
||||
expect(loading).toEqual(false);
|
||||
expect(error).toEqual(false);
|
||||
expect(visits).toEqual(actionVisits);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ tag: 'foo' }, visitsMocks.length + 1],
|
||||
[{ tag: 'bar' }, visitsMocks.length],
|
||||
[
|
||||
fromPartial<TagVisits>({
|
||||
tag: 'foo',
|
||||
query: { endDate: formatIsoDate(subDays(now, 1)) ?? undefined },
|
||||
}),
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
fromPartial<TagVisits>({
|
||||
tag: 'foo',
|
||||
query: { startDate: formatIsoDate(addDays(now, 1)) ?? undefined },
|
||||
}),
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
fromPartial<TagVisits>({
|
||||
tag: 'foo',
|
||||
query: {
|
||||
startDate: formatIsoDate(subDays(now, 5)) ?? undefined,
|
||||
endDate: formatIsoDate(subDays(now, 2)) ?? undefined,
|
||||
},
|
||||
}),
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
fromPartial<TagVisits>({
|
||||
tag: 'foo',
|
||||
query: {
|
||||
startDate: formatIsoDate(subDays(now, 5)) ?? undefined,
|
||||
endDate: formatIsoDate(addDays(now, 3)) ?? undefined,
|
||||
},
|
||||
}),
|
||||
visitsMocks.length + 1,
|
||||
],
|
||||
[
|
||||
fromPartial<TagVisits>({
|
||||
tag: 'bar',
|
||||
query: {
|
||||
startDate: formatIsoDate(subDays(now, 5)) ?? undefined,
|
||||
endDate: formatIsoDate(addDays(now, 3)) ?? undefined,
|
||||
},
|
||||
}),
|
||||
visitsMocks.length,
|
||||
],
|
||||
])('prepends new visits on CREATE_VISIT', (state, expectedVisits) => {
|
||||
const shortUrl = {
|
||||
tags: ['foo', 'baz'],
|
||||
};
|
||||
const prevState = buildState({
|
||||
...state,
|
||||
visits: visitsMocks,
|
||||
});
|
||||
|
||||
const { visits } = reducer(
|
||||
prevState,
|
||||
createNewVisits([fromPartial({ shortUrl, visit: { date: formatIsoDate(now) ?? undefined } })]),
|
||||
);
|
||||
|
||||
expect(visits).toHaveLength(expectedVisits);
|
||||
});
|
||||
|
||||
it('returns new progress on GET_TAG_VISITS_PROGRESS_CHANGED', () => {
|
||||
const { progress } = reducer(undefined, getTagVisits.progressChanged(85));
|
||||
expect(progress).toEqual(85);
|
||||
});
|
||||
|
||||
it('returns fallbackInterval on GET_TAG_VISITS_FALLBACK_TO_INTERVAL', () => {
|
||||
const fallbackInterval: DateInterval = 'last30Days';
|
||||
const state = reducer(undefined, getTagVisits.fallbackToInterval(fallbackInterval));
|
||||
|
||||
expect(state).toEqual(expect.objectContaining({ fallbackInterval }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTagVisits', () => {
|
||||
const dispatchMock = vi.fn();
|
||||
const getState = () => fromPartial<ShlinkState>({
|
||||
tagVisits: { cancelLoad: false },
|
||||
});
|
||||
const tag = 'foo';
|
||||
|
||||
it.each([
|
||||
[undefined],
|
||||
[{}],
|
||||
])('dispatches start and success when promise is resolved', async (query) => {
|
||||
const visits = visitsMocks;
|
||||
getTagVisitsCall.mockResolvedValue({
|
||||
data: visitsMocks,
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
pagesCount: 1,
|
||||
totalItems: 1,
|
||||
},
|
||||
});
|
||||
|
||||
await getTagVisits({ tag, query })(dispatchMock, getState, {});
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(2);
|
||||
expect(dispatchMock).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
payload: { visits, tag, query: query ?? {} },
|
||||
}));
|
||||
expect(getTagVisitsCall).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
[fromPartial<Visit>({ date: formatISO(subDays(now, 20)) })],
|
||||
getTagVisits.fallbackToInterval('last30Days'),
|
||||
3,
|
||||
],
|
||||
[
|
||||
[fromPartial<Visit>({ date: formatISO(subDays(now, 100)) })],
|
||||
getTagVisits.fallbackToInterval('last180Days'),
|
||||
3,
|
||||
],
|
||||
[[], expect.objectContaining({ type: getTagVisits.fulfilled.toString() }), 2],
|
||||
])('dispatches fallback interval when the list of visits is empty', async (
|
||||
lastVisits,
|
||||
expectedSecondDispatch,
|
||||
expectedDispatchCalls,
|
||||
) => {
|
||||
const buildVisitsResult = (data: Visit[] = []): ShlinkVisits => ({
|
||||
data,
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
pagesCount: 1,
|
||||
totalItems: 1,
|
||||
},
|
||||
});
|
||||
getTagVisitsCall
|
||||
.mockResolvedValueOnce(buildVisitsResult())
|
||||
.mockResolvedValueOnce(buildVisitsResult(lastVisits));
|
||||
|
||||
await getTagVisits({ tag, doIntervalFallback: true })(dispatchMock, getState, {});
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(expectedDispatchCalls);
|
||||
expect(dispatchMock).toHaveBeenNthCalledWith(2, expectedSecondDispatch);
|
||||
expect(getTagVisitsCall).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import type { ShortUrl } from '../../../src/short-urls/data';
|
||||
import { createNewVisits } from '../../../src/visits/reducers/visitCreation';
|
||||
import type { Visit } from '../../../src/visits/types';
|
||||
|
||||
describe('visitCreationReducer', () => {
|
||||
describe('createNewVisits', () => {
|
||||
const shortUrl = fromPartial<ShortUrl>({});
|
||||
const visit = fromPartial<Visit>({});
|
||||
|
||||
it('just returns the action with proper type', () => {
|
||||
const { payload } = createNewVisits([{ shortUrl, visit }]);
|
||||
expect(payload).toEqual({ createdVisits: [{ shortUrl, visit }] });
|
||||
});
|
||||
});
|
||||
});
|
||||
165
shlink-web-component/test/visits/reducers/visitsOverview.test.ts
Normal file
165
shlink-web-component/test/visits/reducers/visitsOverview.test.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import type { ShlinkApiClient } from '../../../../src/api/services/ShlinkApiClient';
|
||||
import type { ShlinkState } from '../../../../src/container/types';
|
||||
import type { ShlinkVisitsOverview } from '../../../src/api/types';
|
||||
import { createNewVisits } from '../../../src/visits/reducers/visitCreation';
|
||||
import type {
|
||||
PartialVisitsSummary,
|
||||
VisitsOverview,
|
||||
} from '../../../src/visits/reducers/visitsOverview';
|
||||
import {
|
||||
loadVisitsOverview as loadVisitsOverviewCreator,
|
||||
visitsOverviewReducerCreator,
|
||||
} from '../../../src/visits/reducers/visitsOverview';
|
||||
import type { OrphanVisit } from '../../../src/visits/types';
|
||||
|
||||
describe('visitsOverviewReducer', () => {
|
||||
const getVisitsOverview = vi.fn();
|
||||
const buildApiClientMock = () => fromPartial<ShlinkApiClient>({ getVisitsOverview });
|
||||
const loadVisitsOverview = loadVisitsOverviewCreator(buildApiClientMock);
|
||||
const { reducer } = visitsOverviewReducerCreator(loadVisitsOverview);
|
||||
|
||||
describe('reducer', () => {
|
||||
const state = (payload: Partial<VisitsOverview> = {}) => fromPartial<VisitsOverview>(payload);
|
||||
|
||||
it('returns loading on GET_OVERVIEW_START', () => {
|
||||
const { loading } = reducer(
|
||||
state({ loading: false, error: false }),
|
||||
loadVisitsOverview.pending(''),
|
||||
);
|
||||
|
||||
expect(loading).toEqual(true);
|
||||
});
|
||||
|
||||
it('stops loading and returns error on GET_OVERVIEW_ERROR', () => {
|
||||
const { loading, error } = reducer(
|
||||
state({ loading: true, error: false }),
|
||||
loadVisitsOverview.rejected(null, ''),
|
||||
);
|
||||
|
||||
expect(loading).toEqual(false);
|
||||
expect(error).toEqual(true);
|
||||
});
|
||||
|
||||
it('return visits overview on GET_OVERVIEW', () => {
|
||||
const action = loadVisitsOverview.fulfilled(fromPartial({
|
||||
nonOrphanVisits: { total: 100 },
|
||||
}), 'requestId');
|
||||
const { loading, error, nonOrphanVisits } = reducer(state({ loading: true, error: false }), action);
|
||||
|
||||
expect(loading).toEqual(false);
|
||||
expect(error).toEqual(false);
|
||||
expect(nonOrphanVisits.total).toEqual(100);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[50, 53],
|
||||
[0, 3],
|
||||
])('returns updated amounts on CREATE_VISITS', (providedOrphanVisitsCount, expectedOrphanVisitsCount) => {
|
||||
const { nonOrphanVisits, orphanVisits } = reducer(
|
||||
state({
|
||||
nonOrphanVisits: { total: 100 },
|
||||
orphanVisits: { total: providedOrphanVisitsCount },
|
||||
}),
|
||||
createNewVisits([
|
||||
fromPartial({ visit: {} }),
|
||||
fromPartial({ visit: {} }),
|
||||
fromPartial({ visit: fromPartial<OrphanVisit>({ visitedUrl: '' }) }),
|
||||
fromPartial({ visit: fromPartial<OrphanVisit>({ visitedUrl: '' }) }),
|
||||
fromPartial({ visit: fromPartial<OrphanVisit>({ visitedUrl: '' }) }),
|
||||
]),
|
||||
);
|
||||
|
||||
expect(nonOrphanVisits.total).toEqual(102);
|
||||
expect(orphanVisits.total).toEqual(expectedOrphanVisitsCount);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
{} satisfies Omit<PartialVisitsSummary, 'total'>,
|
||||
{} satisfies Omit<PartialVisitsSummary, 'total'>,
|
||||
{ total: 103 } satisfies PartialVisitsSummary,
|
||||
{ total: 203 } satisfies PartialVisitsSummary,
|
||||
],
|
||||
[
|
||||
{ bots: 35 } satisfies Omit<PartialVisitsSummary, 'total'>,
|
||||
{ bots: 35 } satisfies Omit<PartialVisitsSummary, 'total'>,
|
||||
{ total: 103, bots: 37 } satisfies PartialVisitsSummary,
|
||||
{ total: 203, bots: 36 } satisfies PartialVisitsSummary,
|
||||
],
|
||||
[
|
||||
{ nonBots: 41, bots: 85 } satisfies Omit<PartialVisitsSummary, 'total'>,
|
||||
{ nonBots: 63, bots: 27 } satisfies Omit<PartialVisitsSummary, 'total'>,
|
||||
{ total: 103, nonBots: 42, bots: 87 } satisfies PartialVisitsSummary,
|
||||
{ total: 203, nonBots: 65, bots: 28 } satisfies PartialVisitsSummary,
|
||||
],
|
||||
[
|
||||
{ nonBots: 56 } satisfies Omit<PartialVisitsSummary, 'total'>,
|
||||
{ nonBots: 99 } satisfies Omit<PartialVisitsSummary, 'total'>,
|
||||
{ total: 103, nonBots: 57 } satisfies PartialVisitsSummary,
|
||||
{ total: 203, nonBots: 101 } satisfies PartialVisitsSummary,
|
||||
],
|
||||
])('takes bots and non-bots into consideration when creating visits', (
|
||||
initialNonOrphanVisits,
|
||||
initialOrphanVisits,
|
||||
expectedNonOrphanVisits,
|
||||
expectedOrphanVisits,
|
||||
) => {
|
||||
const { nonOrphanVisits, orphanVisits } = reducer(
|
||||
state({
|
||||
nonOrphanVisits: { total: 100, ...initialNonOrphanVisits },
|
||||
orphanVisits: { total: 200, ...initialOrphanVisits },
|
||||
}),
|
||||
createNewVisits([
|
||||
fromPartial({ visit: {} }),
|
||||
fromPartial({ visit: { potentialBot: true } }),
|
||||
fromPartial({ visit: { potentialBot: true } }),
|
||||
fromPartial({ visit: fromPartial<OrphanVisit>({ visitedUrl: '' }) }),
|
||||
fromPartial({ visit: fromPartial<OrphanVisit>({ visitedUrl: '' }) }),
|
||||
fromPartial({ visit: fromPartial<OrphanVisit>({ visitedUrl: '', potentialBot: true }) }),
|
||||
]),
|
||||
);
|
||||
|
||||
expect(nonOrphanVisits).toEqual(expectedNonOrphanVisits);
|
||||
expect(orphanVisits).toEqual(expectedOrphanVisits);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadVisitsOverview', () => {
|
||||
const dispatchMock = vi.fn();
|
||||
const getState = () => fromPartial<ShlinkState>({});
|
||||
|
||||
it.each([
|
||||
[
|
||||
// Shlink <3.5.0
|
||||
{ visitsCount: 50, orphanVisitsCount: 20 } satisfies ShlinkVisitsOverview,
|
||||
{
|
||||
nonOrphanVisits: { total: 50, nonBots: undefined, bots: undefined },
|
||||
orphanVisits: { total: 20, nonBots: undefined, bots: undefined },
|
||||
},
|
||||
],
|
||||
[
|
||||
// Shlink >=3.5.0
|
||||
{
|
||||
nonOrphanVisits: { total: 50, nonBots: 20, bots: 30 },
|
||||
orphanVisits: { total: 50, nonBots: 20, bots: 30 },
|
||||
visitsCount: 3,
|
||||
orphanVisitsCount: 3,
|
||||
} satisfies ShlinkVisitsOverview,
|
||||
{
|
||||
nonOrphanVisits: { total: 50, nonBots: 20, bots: 30 },
|
||||
orphanVisits: { total: 50, nonBots: 20, bots: 30 },
|
||||
},
|
||||
],
|
||||
])('dispatches start and success when promise is resolved', async (serverResult, dispatchedPayload) => {
|
||||
const resolvedOverview = fromPartial<ShlinkVisitsOverview>(serverResult);
|
||||
getVisitsOverview.mockResolvedValue(resolvedOverview);
|
||||
|
||||
await loadVisitsOverview()(dispatchMock, getState, {});
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(2);
|
||||
expect(dispatchMock).toHaveBeenNthCalledWith(2, expect.objectContaining({ payload: dispatchedPayload }));
|
||||
expect(getVisitsOverview).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
280
shlink-web-component/test/visits/services/VisitsParser.test.ts
Normal file
280
shlink-web-component/test/visits/services/VisitsParser.test.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { normalizeVisits, processStatsFromVisits } from '../../../src/visits/services/VisitsParser';
|
||||
import type { OrphanVisit, Visit, VisitsStats } from '../../../src/visits/types';
|
||||
|
||||
describe('VisitsParser', () => {
|
||||
const visits: Visit[] = [
|
||||
fromPartial({
|
||||
userAgent: 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0',
|
||||
referer: 'https://google.com',
|
||||
potentialBot: false,
|
||||
visitLocation: {
|
||||
countryName: 'Spain',
|
||||
cityName: 'Zaragoza',
|
||||
latitude: 123.45,
|
||||
longitude: -543.21,
|
||||
},
|
||||
}),
|
||||
fromPartial({
|
||||
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0',
|
||||
referer: 'https://google.com',
|
||||
potentialBot: false,
|
||||
visitLocation: {
|
||||
countryName: 'United States',
|
||||
cityName: 'New York',
|
||||
latitude: 1029,
|
||||
longitude: 6758,
|
||||
},
|
||||
}),
|
||||
fromPartial({
|
||||
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36',
|
||||
potentialBot: false,
|
||||
visitLocation: {
|
||||
countryName: 'Spain',
|
||||
cityName: '',
|
||||
},
|
||||
}),
|
||||
fromPartial({
|
||||
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36',
|
||||
referer: 'https://m.facebook.com',
|
||||
potentialBot: false,
|
||||
visitLocation: {
|
||||
countryName: 'Spain',
|
||||
cityName: 'Zaragoza',
|
||||
latitude: 123.45,
|
||||
longitude: -543.21,
|
||||
},
|
||||
}),
|
||||
fromPartial({
|
||||
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36 OPR/38.0.2220.41',
|
||||
potentialBot: true,
|
||||
}),
|
||||
];
|
||||
const orphanVisits: OrphanVisit[] = [
|
||||
fromPartial({
|
||||
type: 'base_url',
|
||||
visitedUrl: 'foo',
|
||||
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0',
|
||||
referer: 'https://google.com',
|
||||
potentialBot: false,
|
||||
visitLocation: {
|
||||
countryName: 'United States',
|
||||
cityName: 'New York',
|
||||
latitude: 1029,
|
||||
longitude: 6758,
|
||||
},
|
||||
}),
|
||||
fromPartial({
|
||||
type: 'regular_404',
|
||||
visitedUrl: 'bar',
|
||||
potentialBot: true,
|
||||
}),
|
||||
fromPartial({
|
||||
type: 'invalid_short_url',
|
||||
visitedUrl: 'bar',
|
||||
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36',
|
||||
referer: 'https://m.facebook.com',
|
||||
visitLocation: {
|
||||
countryName: 'Spain',
|
||||
cityName: 'Zaragoza',
|
||||
latitude: 123.45,
|
||||
longitude: -543.21,
|
||||
},
|
||||
potentialBot: false,
|
||||
}),
|
||||
];
|
||||
|
||||
describe('processStatsFromVisits', () => {
|
||||
let stats: VisitsStats;
|
||||
|
||||
beforeAll(() => {
|
||||
stats = processStatsFromVisits(normalizeVisits(visits));
|
||||
});
|
||||
|
||||
it('properly parses OS stats', () => {
|
||||
const { os } = stats;
|
||||
|
||||
expect(os).toEqual({
|
||||
Linux: 3,
|
||||
Windows: 1,
|
||||
macOS: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('properly parses browser stats', () => {
|
||||
const { browsers } = stats;
|
||||
|
||||
expect(browsers).toEqual({
|
||||
Firefox: 2,
|
||||
Chrome: 2,
|
||||
Opera: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('properly parses referrer stats', () => {
|
||||
const { referrers } = stats;
|
||||
|
||||
expect(referrers).toEqual({
|
||||
Direct: 2,
|
||||
'google.com': 2,
|
||||
'm.facebook.com': 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('properly parses countries stats', () => {
|
||||
const { countries } = stats;
|
||||
|
||||
expect(countries).toEqual({
|
||||
Spain: 3,
|
||||
'United States': 1,
|
||||
Unknown: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('properly parses cities stats', () => {
|
||||
const { cities } = stats;
|
||||
|
||||
expect(cities).toEqual({
|
||||
Zaragoza: 2,
|
||||
'New York': 1,
|
||||
Unknown: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('properly parses cities stats with lat and long', () => {
|
||||
const { citiesForMap } = stats;
|
||||
const zaragozaLat = 123.45;
|
||||
const zaragozaLong = -543.21;
|
||||
const newYorkLat = 1029;
|
||||
const newYorkLong = 6758;
|
||||
|
||||
expect(citiesForMap).toEqual({
|
||||
Zaragoza: {
|
||||
cityName: 'Zaragoza',
|
||||
count: 2,
|
||||
latLong: [zaragozaLat, zaragozaLong],
|
||||
},
|
||||
'New York': {
|
||||
cityName: 'New York',
|
||||
count: 1,
|
||||
latLong: [newYorkLat, newYorkLong],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('properly parses visited URL stats', () => {
|
||||
const { visitedUrls } = processStatsFromVisits(normalizeVisits(orphanVisits));
|
||||
|
||||
expect(visitedUrls).toEqual({
|
||||
foo: 1,
|
||||
bar: 2,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeVisits', () => {
|
||||
it('properly parses the list of visits', () => {
|
||||
expect(normalizeVisits(visits)).toEqual([
|
||||
{
|
||||
browser: 'Firefox',
|
||||
os: 'Windows',
|
||||
referer: 'google.com',
|
||||
country: 'Spain',
|
||||
city: 'Zaragoza',
|
||||
date: undefined,
|
||||
latitude: 123.45,
|
||||
longitude: -543.21,
|
||||
potentialBot: false,
|
||||
},
|
||||
{
|
||||
browser: 'Firefox',
|
||||
os: 'macOS',
|
||||
referer: 'google.com',
|
||||
country: 'United States',
|
||||
city: 'New York',
|
||||
date: undefined,
|
||||
latitude: 1029,
|
||||
longitude: 6758,
|
||||
potentialBot: false,
|
||||
},
|
||||
{
|
||||
browser: 'Chrome',
|
||||
os: 'Linux',
|
||||
referer: 'Direct',
|
||||
country: 'Spain',
|
||||
city: 'Unknown',
|
||||
date: undefined,
|
||||
latitude: undefined,
|
||||
longitude: undefined,
|
||||
potentialBot: false,
|
||||
},
|
||||
{
|
||||
browser: 'Chrome',
|
||||
os: 'Linux',
|
||||
referer: 'm.facebook.com',
|
||||
country: 'Spain',
|
||||
city: 'Zaragoza',
|
||||
date: undefined,
|
||||
latitude: 123.45,
|
||||
longitude: -543.21,
|
||||
potentialBot: false,
|
||||
},
|
||||
{
|
||||
browser: 'Opera',
|
||||
os: 'Linux',
|
||||
referer: 'Direct',
|
||||
country: 'Unknown',
|
||||
city: 'Unknown',
|
||||
date: undefined,
|
||||
latitude: undefined,
|
||||
longitude: undefined,
|
||||
potentialBot: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('properly parses the list of orphan visits', () => {
|
||||
expect(normalizeVisits(orphanVisits)).toEqual([
|
||||
{
|
||||
browser: 'Firefox',
|
||||
os: 'macOS',
|
||||
referer: 'google.com',
|
||||
country: 'United States',
|
||||
city: 'New York',
|
||||
date: undefined,
|
||||
latitude: 1029,
|
||||
longitude: 6758,
|
||||
type: 'base_url',
|
||||
visitedUrl: 'foo',
|
||||
potentialBot: false,
|
||||
},
|
||||
{
|
||||
type: 'regular_404',
|
||||
visitedUrl: 'bar',
|
||||
browser: 'Others',
|
||||
city: 'Unknown',
|
||||
country: 'Unknown',
|
||||
date: undefined,
|
||||
latitude: undefined,
|
||||
longitude: undefined,
|
||||
os: 'Others',
|
||||
referer: 'Direct',
|
||||
potentialBot: true,
|
||||
},
|
||||
{
|
||||
browser: 'Chrome',
|
||||
os: 'Linux',
|
||||
referer: 'm.facebook.com',
|
||||
country: 'Spain',
|
||||
city: 'Zaragoza',
|
||||
date: undefined,
|
||||
latitude: 123.45,
|
||||
longitude: -543.21,
|
||||
type: 'invalid_short_url',
|
||||
visitedUrl: 'bar',
|
||||
potentialBot: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
99
shlink-web-component/test/visits/types/helpers.test.ts
Normal file
99
shlink-web-component/test/visits/types/helpers.test.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import type { ShlinkVisitsParams } from '../../../src/api/types';
|
||||
import { formatIsoDate, parseDate } from '../../../src/utils/dates/helpers/date';
|
||||
import type { CreateVisit, OrphanVisit, VisitsParams } from '../../../src/visits/types';
|
||||
import type { GroupedNewVisits } from '../../../src/visits/types/helpers';
|
||||
import { groupNewVisitsByType, toApiParams } from '../../../src/visits/types/helpers';
|
||||
|
||||
describe('visitsTypeHelpers', () => {
|
||||
describe('groupNewVisitsByType', () => {
|
||||
it.each([
|
||||
[[], { orphanVisits: [], nonOrphanVisits: [] }],
|
||||
((): [CreateVisit[], GroupedNewVisits] => {
|
||||
const orphanVisits: CreateVisit[] = [
|
||||
fromPartial({ visit: fromPartial<OrphanVisit>({ visitedUrl: '' }) }),
|
||||
fromPartial({ visit: fromPartial<OrphanVisit>({ visitedUrl: '' }) }),
|
||||
];
|
||||
const nonOrphanVisits: CreateVisit[] = [
|
||||
fromPartial({ visit: {} }),
|
||||
fromPartial({ visit: {} }),
|
||||
fromPartial({ visit: {} }),
|
||||
fromPartial({ visit: {} }),
|
||||
fromPartial({ visit: {} }),
|
||||
];
|
||||
|
||||
return [
|
||||
[...orphanVisits, ...nonOrphanVisits],
|
||||
{ orphanVisits, nonOrphanVisits },
|
||||
];
|
||||
})(),
|
||||
((): [CreateVisit[], GroupedNewVisits] => {
|
||||
const orphanVisits: CreateVisit[] = [
|
||||
fromPartial({ visit: fromPartial<OrphanVisit>({ visitedUrl: '' }) }),
|
||||
fromPartial({ visit: fromPartial<OrphanVisit>({ visitedUrl: '' }) }),
|
||||
fromPartial({ visit: fromPartial<OrphanVisit>({ visitedUrl: '' }) }),
|
||||
];
|
||||
|
||||
return [orphanVisits, { orphanVisits, nonOrphanVisits: [] }];
|
||||
})(),
|
||||
((): [CreateVisit[], GroupedNewVisits] => {
|
||||
const nonOrphanVisits: CreateVisit[] = [
|
||||
fromPartial({ visit: {} }),
|
||||
fromPartial({ visit: {} }),
|
||||
fromPartial({ visit: {} }),
|
||||
];
|
||||
|
||||
return [nonOrphanVisits, { orphanVisits: [], nonOrphanVisits }];
|
||||
})(),
|
||||
])('groups new visits as expected', (createdVisits, expectedResult) => {
|
||||
expect(groupNewVisitsByType(createdVisits)).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toApiParams', () => {
|
||||
it.each([
|
||||
[{ page: 5, itemsPerPage: 100 } as VisitsParams, { page: 5, itemsPerPage: 100 } as ShlinkVisitsParams],
|
||||
[
|
||||
{
|
||||
page: 1,
|
||||
itemsPerPage: 30,
|
||||
filter: { excludeBots: true },
|
||||
} as VisitsParams,
|
||||
{ page: 1, itemsPerPage: 30, excludeBots: true } as ShlinkVisitsParams,
|
||||
],
|
||||
(() => {
|
||||
const endDate = parseDate('2020-05-05', 'yyyy-MM-dd');
|
||||
|
||||
return [
|
||||
{
|
||||
page: 20,
|
||||
itemsPerPage: 1,
|
||||
dateRange: { endDate },
|
||||
} as VisitsParams,
|
||||
{ page: 20, itemsPerPage: 1, endDate: formatIsoDate(endDate) } as ShlinkVisitsParams,
|
||||
];
|
||||
})(),
|
||||
(() => {
|
||||
const startDate = parseDate('2020-05-05', 'yyyy-MM-dd');
|
||||
const endDate = parseDate('2021-10-30', 'yyyy-MM-dd');
|
||||
|
||||
return [
|
||||
{
|
||||
page: 20,
|
||||
itemsPerPage: 1,
|
||||
dateRange: { startDate, endDate },
|
||||
filter: { excludeBots: false },
|
||||
} as VisitsParams,
|
||||
{
|
||||
page: 20,
|
||||
itemsPerPage: 1,
|
||||
startDate: formatIsoDate(startDate),
|
||||
endDate: formatIsoDate(endDate),
|
||||
} as ShlinkVisitsParams,
|
||||
];
|
||||
})(),
|
||||
])('converts param as expected', (visitsParams, expectedResult) => {
|
||||
expect(toApiParams(visitsParams)).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user