mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-03-10 09:33:51 +00:00
Move shlink-web-component tests to their own folder
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
import { CopyToClipboardIcon } from '../../../src/utils/components/CopyToClipboardIcon';
|
||||
import { renderWithEvents } from '../../__helpers__/setUpTest';
|
||||
|
||||
describe('<CopyToClipboardIcon />', () => {
|
||||
const onCopy = vi.fn();
|
||||
const setUp = (text = 'foo') => renderWithEvents(<CopyToClipboardIcon text={text} onCopy={onCopy} />);
|
||||
|
||||
it('wraps expected components', () => {
|
||||
const { container } = setUp();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['text'],
|
||||
['bar'],
|
||||
['baz'],
|
||||
])('copies content to clipboard when clicked', async (text) => {
|
||||
const { user, container } = setUp(text);
|
||||
|
||||
expect(onCopy).not.toHaveBeenCalled();
|
||||
container.firstElementChild && await user.click(container.firstElementChild);
|
||||
expect(onCopy).toHaveBeenCalledWith(text, false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ExportBtn } from '../../../src/utils/components/ExportBtn';
|
||||
|
||||
describe('<ExportBtn />', () => {
|
||||
const setUp = (amount?: number, loading = false) => render(<ExportBtn amount={amount} loading={loading} />);
|
||||
|
||||
it.each([
|
||||
[true, 'Exporting...'],
|
||||
[false, 'Export (0)'],
|
||||
])('renders loading state when expected', async (loading, text) => {
|
||||
setUp(undefined, loading);
|
||||
const btn = await screen.findByRole('button');
|
||||
|
||||
expect(btn).toHaveTextContent(text);
|
||||
if (loading) {
|
||||
expect(btn).toHaveAttribute('disabled');
|
||||
} else {
|
||||
expect(btn).not.toHaveAttribute('disabled');
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
[undefined, '0'],
|
||||
[10, '10'],
|
||||
[10_000, '10,000'],
|
||||
[10_000_000, '10,000,000'],
|
||||
])('renders expected amount', async (amount, expectedRenderedAmount) => {
|
||||
setUp(amount);
|
||||
expect(await screen.findByRole('button')).toHaveTextContent(`Export (${expectedRenderedAmount})`);
|
||||
});
|
||||
|
||||
it('renders expected icon', () => {
|
||||
setUp();
|
||||
expect(screen.getByRole('img', { hidden: true })).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { IconProp } from '@fortawesome/fontawesome-svg-core';
|
||||
import { faAppleAlt, faCalendar, faTable } from '@fortawesome/free-solid-svg-icons';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { IconInput } from '../../../src/utils/components/IconInput';
|
||||
import { renderWithEvents } from '../../__helpers__/setUpTest';
|
||||
|
||||
describe('<IconInput />', () => {
|
||||
const setUp = (icon: IconProp, placeholder?: string) => renderWithEvents(
|
||||
<IconInput icon={icon} placeholder={placeholder} />,
|
||||
);
|
||||
|
||||
it.each([faCalendar, faAppleAlt, faTable])('displays provided icon', (icon) => {
|
||||
const { container } = setUp(icon);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('focuses input on icon click', async () => {
|
||||
const { user } = setUp(faCalendar, 'foo');
|
||||
|
||||
expect(screen.getByPlaceholderText('foo')).not.toHaveFocus();
|
||||
await user.click(screen.getByRole('img', { hidden: true }));
|
||||
expect(screen.getByPlaceholderText('foo')).toHaveFocus();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { Placement } from '@popperjs/core';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import type { InfoTooltipProps } from '../../../src/utils/components/InfoTooltip';
|
||||
import { InfoTooltip } from '../../../src/utils/components/InfoTooltip';
|
||||
import { renderWithEvents } from '../../__helpers__/setUpTest';
|
||||
|
||||
describe('<InfoTooltip />', () => {
|
||||
const setUp = (props: Partial<InfoTooltipProps> = {}) => renderWithEvents(
|
||||
<InfoTooltip placement="right" {...props} />,
|
||||
);
|
||||
|
||||
it.each([
|
||||
[undefined],
|
||||
['foo'],
|
||||
['bar'],
|
||||
])('renders expected className on span', (className) => {
|
||||
const { container } = setUp({ className });
|
||||
|
||||
if (className) {
|
||||
expect(container.firstChild).toHaveClass(className);
|
||||
} else {
|
||||
expect(container.firstChild).toHaveAttribute('class', '');
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
[<span key={1}>foo</span>, 'foo'],
|
||||
['Foo', 'Foo'],
|
||||
['Hello', 'Hello'],
|
||||
[['One', 'Two', <span key={3} />], 'OneTwo'],
|
||||
])('passes children down to the nested tooltip component', async (children, expectedContent) => {
|
||||
const { container, user } = setUp({ children });
|
||||
|
||||
container.firstElementChild && await user.hover(container.firstElementChild);
|
||||
await waitFor(() => expect(screen.getByRole('tooltip')).toBeInTheDocument());
|
||||
expect(screen.getByRole('tooltip')).toHaveTextContent(expectedContent);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['right' as Placement],
|
||||
['left' as Placement],
|
||||
['top' as Placement],
|
||||
['bottom' as Placement],
|
||||
])('places tooltip where requested', async (placement) => {
|
||||
const { container, user } = setUp({ placement });
|
||||
|
||||
container.firstElementChild && await user.hover(container.firstElementChild);
|
||||
await waitFor(() => expect(screen.getByRole('tooltip')).toBeInTheDocument());
|
||||
expect(screen.getByRole('tooltip').parentNode).toHaveAttribute('data-popper-placement', placement);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import { PaginationDropdown } from '../../../src/utils/components/PaginationDropdown';
|
||||
import { renderWithEvents } from '../../__helpers__/setUpTest';
|
||||
|
||||
describe('<PaginationDropdown />', () => {
|
||||
const setValue = vi.fn();
|
||||
const setUp = async () => {
|
||||
const result = renderWithEvents(<PaginationDropdown ranges={[10, 50, 100, 200]} value={50} setValue={setValue} />);
|
||||
const { user } = result;
|
||||
|
||||
await user.click(screen.getByRole('button'));
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
it('renders expected amount of items', async () => {
|
||||
await setUp();
|
||||
expect(screen.getAllByRole('menuitem')).toHaveLength(5);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[0, 10],
|
||||
[1, 50],
|
||||
[2, 100],
|
||||
[3, 200],
|
||||
])('sets expected value when an item is clicked', async (index, expectedValue) => {
|
||||
const { user } = await setUp();
|
||||
|
||||
expect(setValue).not.toHaveBeenCalled();
|
||||
await user.click(screen.getAllByRole('menuitem')[index]);
|
||||
expect(setValue).toHaveBeenCalledWith(expectedValue);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<CopyToClipboardIcon /> > wraps expected components 1`] = `
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="svg-inline--fa fa-clone ms-2 copy-to-clipboard-icon"
|
||||
data-icon="clone"
|
||||
data-prefix="far"
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M64 464H288c8.8 0 16-7.2 16-16V384h48v64c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V224c0-35.3 28.7-64 64-64h64v48H64c-8.8 0-16 7.2-16 16V448c0 8.8 7.2 16 16 16zM224 304H448c8.8 0 16-7.2 16-16V64c0-8.8-7.2-16-16-16H224c-8.8 0-16 7.2-16 16V288c0 8.8 7.2 16 16 16zm-64-16V64c0-35.3 28.7-64 64-64H448c35.3 0 64 28.7 64 64V288c0 35.3-28.7 64-64 64H224c-35.3 0-64-28.7-64-64z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,19 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<ExportBtn /> > renders expected icon 1`] = `
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="svg-inline--fa fa-file-csv "
|
||||
data-icon="file-csv"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 384 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M64 0C28.7 0 0 28.7 0 64V448c0 35.3 28.7 64 64 64H320c35.3 0 64-28.7 64-64V160H256c-17.7 0-32-14.3-32-32V0H64zM256 0V128H384L256 0zM80 224H96c22.1 0 40 17.9 40 40v8c0 8.8-7.2 16-16 16s-16-7.2-16-16v-8c0-4.4-3.6-8-8-8H80c-4.4 0-8 3.6-8 8v80c0 4.4 3.6 8 8 8H96c4.4 0 8-3.6 8-8v-8c0-8.8 7.2-16 16-16s16 7.2 16 16v8c0 22.1-17.9 40-40 40H80c-22.1 0-40-17.9-40-40V264c0-22.1 17.9-40 40-40zm72 46.4c0-25.6 20.8-46.4 46.4-46.4H216c8.8 0 16 7.2 16 16s-7.2 16-16 16H198.4c-7.9 0-14.4 6.4-14.4 14.4c0 5.2 2.8 9.9 7.2 12.5l25.4 14.5c14.4 8.3 23.4 23.6 23.4 40.3c0 25.6-20.8 46.4-46.4 46.4H168c-8.8 0-16-7.2-16-16s7.2-16 16-16h25.6c7.9 0 14.4-6.4 14.4-14.4c0-5.2-2.8-9.9-7.2-12.5l-25.4-14.5C160.9 302.4 152 287 152 270.4zM280 240v31.6c0 23 5.5 45.6 16 66c10.5-20.3 16-42.9 16-66V240c0-8.8 7.2-16 16-16s16 7.2 16 16v31.6c0 34.7-10.3 68.7-29.6 97.6l-5.1 7.7c-3 4.5-8 7.1-13.3 7.1s-10.3-2.7-13.3-7.1l-5.1-7.7c-19.3-28.9-29.6-62.9-29.6-97.6V240c0-8.8 7.2-16 16-16s16 7.2 16 16z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
@@ -0,0 +1,85 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<IconInput /> > displays provided icon 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="icon-input-container"
|
||||
>
|
||||
<input
|
||||
class="icon-input-container__input form-control"
|
||||
type="text"
|
||||
/>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="svg-inline--fa fa-calendar fa-fw icon-input-container__icon"
|
||||
data-icon="calendar"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 448 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M96 32V64H48C21.5 64 0 85.5 0 112v48H448V112c0-26.5-21.5-48-48-48H352V32c0-17.7-14.3-32-32-32s-32 14.3-32 32V64H160V32c0-17.7-14.3-32-32-32S96 14.3 96 32zM448 192H0V464c0 26.5 21.5 48 48 48H400c26.5 0 48-21.5 48-48V192z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<IconInput /> > displays provided icon 2`] = `
|
||||
<div>
|
||||
<div
|
||||
class="icon-input-container"
|
||||
>
|
||||
<input
|
||||
class="icon-input-container__input form-control"
|
||||
type="text"
|
||||
/>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="svg-inline--fa fa-apple-whole fa-fw icon-input-container__icon"
|
||||
data-icon="apple-whole"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 448 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M224 112c-8.8 0-16-7.2-16-16V80c0-44.2 35.8-80 80-80h16c8.8 0 16 7.2 16 16V32c0 44.2-35.8 80-80 80H224zM0 288c0-76.3 35.7-160 112-160c27.3 0 59.7 10.3 82.7 19.3c18.8 7.3 39.9 7.3 58.7 0c22.9-8.9 55.4-19.3 82.7-19.3c76.3 0 112 83.7 112 160c0 128-80 224-160 224c-16.5 0-38.1-6.6-51.5-11.3c-8.1-2.8-16.9-2.8-25 0c-13.4 4.7-35 11.3-51.5 11.3C80 512 0 416 0 288z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<IconInput /> > displays provided icon 3`] = `
|
||||
<div>
|
||||
<div
|
||||
class="icon-input-container"
|
||||
>
|
||||
<input
|
||||
class="icon-input-container__input form-control"
|
||||
type="text"
|
||||
/>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="svg-inline--fa fa-table fa-fw icon-input-container__icon"
|
||||
data-icon="table"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M64 256V160H224v96H64zm0 64H224v96H64V320zm224 96V320H448v96H288zM448 256H288V160H448v96zM64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
45
shlink-web-component/test/utils/dates/DateInput.test.tsx
Normal file
45
shlink-web-component/test/utils/dates/DateInput.test.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { parseISO } from 'date-fns';
|
||||
import type { DateInputProps } from '../../../src/utils/dates/DateInput';
|
||||
import { DateInput } from '../../../src/utils/dates/DateInput';
|
||||
import { renderWithEvents } from '../../__helpers__/setUpTest';
|
||||
|
||||
describe('<DateInput />', () => {
|
||||
const setUp = (props: Partial<DateInputProps> = {}) => renderWithEvents(
|
||||
<DateInput {...fromPartial<DateInputProps>(props)} />,
|
||||
);
|
||||
|
||||
it('shows calendar icon when input is not clearable', () => {
|
||||
setUp({ isClearable: false });
|
||||
expect(screen.getByRole('img', { hidden: true })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows calendar icon when input is clearable but selected value is nil', () => {
|
||||
setUp({ isClearable: true, selected: null });
|
||||
expect(screen.getByRole('img', { hidden: true })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show calendar icon when input is clearable', () => {
|
||||
setUp({ isClearable: true, selected: new Date() });
|
||||
expect(screen.queryByRole('img', { hidden: true })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows popper on element click', async () => {
|
||||
const { user, container } = setUp({ placeholderText: 'foo' });
|
||||
|
||||
expect(container.querySelector('.react-datepicker')).not.toBeInTheDocument();
|
||||
await user.click(screen.getByPlaceholderText('foo'));
|
||||
await waitFor(() => expect(container.querySelector('.react-datepicker')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it.each([
|
||||
[undefined, '2022-01-01'],
|
||||
['yyyy-MM-dd', '2022-01-01'],
|
||||
['yyyy-MM-dd HH:mm', '2022-01-01 15:18'],
|
||||
['HH:mm:ss', '15:18:36'],
|
||||
])('shows date in expected format', (dateFormat, expectedValue) => {
|
||||
setUp({ placeholderText: 'foo', selected: parseISO('2022-01-01T15:18:36'), dateFormat });
|
||||
expect(screen.getByPlaceholderText('foo')).toHaveValue(expectedValue);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import { DropdownBtn } from '../../../../shlink-frontend-kit/src';
|
||||
import { DateIntervalDropdownItems } from '../../../src/utils/dates/DateIntervalDropdownItems';
|
||||
import type { DateInterval } from '../../../src/utils/dates/helpers/dateIntervals';
|
||||
import { DATE_INTERVALS, rangeOrIntervalToString } from '../../../src/utils/dates/helpers/dateIntervals';
|
||||
import { renderWithEvents } from '../../__helpers__/setUpTest';
|
||||
|
||||
describe('<DateIntervalDropdownItems />', () => {
|
||||
const onChange = vi.fn();
|
||||
const setUp = async () => {
|
||||
const { user, ...renderResult } = renderWithEvents(
|
||||
<DropdownBtn text="text">
|
||||
<DateIntervalDropdownItems allText="All" active="last180Days" onChange={onChange} />
|
||||
</DropdownBtn>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button'));
|
||||
await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
|
||||
|
||||
return { user, ...renderResult };
|
||||
};
|
||||
|
||||
it('renders expected amount of items', async () => {
|
||||
await setUp();
|
||||
|
||||
expect(screen.getAllByRole('menuitem')).toHaveLength(DATE_INTERVALS.length + 1);
|
||||
expect(screen.getByRole('menuitem', { name: 'Last 180 days' })).toHaveClass('active');
|
||||
});
|
||||
|
||||
it('sets expected item as active', async () => {
|
||||
await setUp();
|
||||
const EXPECTED_ACTIVE_INDEX = 5;
|
||||
|
||||
DATE_INTERVALS.forEach((interval, index) => {
|
||||
const item = screen.getByRole('menuitem', { name: rangeOrIntervalToString(interval) });
|
||||
|
||||
if (index === EXPECTED_ACTIVE_INDEX) {
|
||||
expect(item).toHaveClass('active');
|
||||
} else {
|
||||
expect(item).not.toHaveClass('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
[3, 'last7Days' as DateInterval],
|
||||
[7, 'last365Days' as DateInterval],
|
||||
[2, 'yesterday' as DateInterval],
|
||||
])('triggers onChange callback when selecting an element', async (index, expectedInterval) => {
|
||||
const { user } = await setUp();
|
||||
|
||||
await user.click(screen.getAllByRole('menuitem')[index]);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(expectedInterval);
|
||||
});
|
||||
});
|
||||
32
shlink-web-component/test/utils/dates/DateRangeRow.test.tsx
Normal file
32
shlink-web-component/test/utils/dates/DateRangeRow.test.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import { DateRangeRow } from '../../../src/utils/dates/DateRangeRow';
|
||||
import { renderWithEvents } from '../../__helpers__/setUpTest';
|
||||
|
||||
describe('<DateRangeRow />', () => {
|
||||
const onEndDateChange = vi.fn();
|
||||
const onStartDateChange = vi.fn();
|
||||
const setUp = () => renderWithEvents(
|
||||
<DateRangeRow onEndDateChange={onEndDateChange} onStartDateChange={onStartDateChange} />,
|
||||
);
|
||||
|
||||
it('renders two date inputs', () => {
|
||||
setUp();
|
||||
expect(screen.getAllByRole('textbox')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('invokes start date callback when change event is triggered on first input', async () => {
|
||||
const { user } = setUp();
|
||||
|
||||
expect(onStartDateChange).not.toHaveBeenCalled();
|
||||
await user.type(screen.getByPlaceholderText('Since...'), '2020-05-05');
|
||||
expect(onStartDateChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('invokes end date callback when change event is triggered on second input', async () => {
|
||||
const { user } = setUp();
|
||||
|
||||
expect(onEndDateChange).not.toHaveBeenCalled();
|
||||
await user.type(screen.getByPlaceholderText('Until...'), '2022-05-05');
|
||||
expect(onEndDateChange).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import type { DateRangeSelectorProps } from '../../../src/utils/dates/DateRangeSelector';
|
||||
import { DateRangeSelector } from '../../../src/utils/dates/DateRangeSelector';
|
||||
import type { DateInterval } from '../../../src/utils/dates/helpers/dateIntervals';
|
||||
import { renderWithEvents } from '../../__helpers__/setUpTest';
|
||||
|
||||
describe('<DateRangeSelector />', () => {
|
||||
const onDatesChange = vi.fn();
|
||||
const setUp = async (props: Partial<DateRangeSelectorProps> = {}) => {
|
||||
const result = renderWithEvents(
|
||||
<DateRangeSelector
|
||||
{...fromPartial<DateRangeSelectorProps>(props)}
|
||||
defaultText="Default text"
|
||||
onDatesChange={onDatesChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
await result.user.click(screen.getByRole('button'));
|
||||
await waitFor(() => screen.getByRole('menu'));
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
it('renders proper amount of items', async () => {
|
||||
const { container } = await setUp();
|
||||
|
||||
expect(screen.getAllByRole('menuitem')).toHaveLength(8);
|
||||
expect(screen.getByRole('heading')).toHaveTextContent('Custom:');
|
||||
expect(container.querySelector('.dropdown-divider')).toBeInTheDocument();
|
||||
expect(container.querySelector('.dropdown-item-text')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([
|
||||
[undefined, 0],
|
||||
['all' as DateInterval, 1],
|
||||
['today' as DateInterval, 1],
|
||||
['yesterday' as DateInterval, 1],
|
||||
['last7Days' as DateInterval, 1],
|
||||
['last30Days' as DateInterval, 1],
|
||||
['last90Days' as DateInterval, 1],
|
||||
['last180Days' as DateInterval, 1],
|
||||
['last365Days' as DateInterval, 1],
|
||||
[{ startDate: new Date() }, 0],
|
||||
])('sets proper element as active based on provided date range', async (initialDateRange, expectedActiveItems) => {
|
||||
const { container } = await setUp({ initialDateRange });
|
||||
expect(container.querySelectorAll('.active')).toHaveLength(expectedActiveItems);
|
||||
});
|
||||
|
||||
it('triggers onDatesChange callback when selecting an element', async () => {
|
||||
const { user } = await setUp();
|
||||
|
||||
await user.click(screen.getByPlaceholderText('Since...'));
|
||||
await user.click(screen.getAllByRole('option')[0]);
|
||||
|
||||
await user.click(screen.getByPlaceholderText('Until...'));
|
||||
await user.click(screen.getAllByRole('option')[0]);
|
||||
|
||||
await user.click(screen.getAllByRole('menuitem')[0]);
|
||||
|
||||
expect(onDatesChange).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('propagates default text to DateIntervalDropdownItems', async () => {
|
||||
await setUp();
|
||||
expect(screen.getAllByText('Default text')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
23
shlink-web-component/test/utils/dates/Time.test.tsx
Normal file
23
shlink-web-component/test/utils/dates/Time.test.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { parseDate } from '../../../src/utils/dates/helpers/date';
|
||||
import type { TimeProps } from '../../../src/utils/dates/Time';
|
||||
import { Time } from '../../../src/utils/dates/Time';
|
||||
|
||||
describe('<Time />', () => {
|
||||
const setUp = (props: TimeProps) => render(<Time {...props} />);
|
||||
|
||||
it.each([
|
||||
[{ date: parseDate('2020-05-05', 'yyyy-MM-dd') }, '1588636800000', '2020-05-05 00:00'],
|
||||
[{ date: parseDate('2021-03-20', 'yyyy-MM-dd'), format: 'dd/MM/yyyy' }, '1616198400000', '20/03/2021'],
|
||||
])('includes expected dateTime and format', (props, expectedDateTime, expectedFormatted) => {
|
||||
const { container } = setUp(props);
|
||||
|
||||
expect(container.firstChild).toHaveAttribute('datetime', expectedDateTime);
|
||||
expect(container.firstChild).toHaveTextContent(expectedFormatted);
|
||||
});
|
||||
|
||||
it('renders relative times when requested', () => {
|
||||
const { container } = setUp({ date: new Date(), relative: true });
|
||||
expect(container.firstChild).toHaveTextContent(' ago');
|
||||
});
|
||||
});
|
||||
63
shlink-web-component/test/utils/dates/helpers/date.test.ts
Normal file
63
shlink-web-component/test/utils/dates/helpers/date.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { addDays, formatISO, subDays } from 'date-fns';
|
||||
import { formatDate, formatIsoDate, isBeforeOrEqual, isBetween, parseDate } from '../../../../src/utils/dates/helpers/date';
|
||||
|
||||
describe('date', () => {
|
||||
const now = new Date();
|
||||
|
||||
describe('formatDate', () => {
|
||||
it.each([
|
||||
[parseDate('2020-03-05 10:00:10', 'yyyy-MM-dd HH:mm:ss'), 'dd/MM/yyyy', '05/03/2020'],
|
||||
[parseDate('2020-03-05 10:00:10', 'yyyy-MM-dd HH:mm:ss'), 'yyyy-MM', '2020-03'],
|
||||
[parseDate('2020-03-05 10:00:10', 'yyyy-MM-dd HH:mm:ss'), undefined, '2020-03-05'],
|
||||
['2020-03-05 10:00:10', 'dd-MM-yyyy', '2020-03-05 10:00:10'],
|
||||
['2020-03-05 10:00:10', undefined, '2020-03-05 10:00:10'],
|
||||
[undefined, undefined, undefined],
|
||||
[null, undefined, null],
|
||||
])('formats date as expected', (date, format, expected) => {
|
||||
expect(formatDate(format)(date)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatIsoDate', () => {
|
||||
it.each([
|
||||
[
|
||||
parseDate('2020-03-05 10:00:10', 'yyyy-MM-dd HH:mm:ss'),
|
||||
formatISO(parseDate('2020-03-05 10:00:10', 'yyyy-MM-dd HH:mm:ss')),
|
||||
],
|
||||
['2020-03-05 10:00:10', '2020-03-05 10:00:10'],
|
||||
['foo', 'foo'],
|
||||
[undefined, undefined],
|
||||
[null, null],
|
||||
])('formats date as expected', (date, expected) => {
|
||||
expect(formatIsoDate(date)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isBetween', () => {
|
||||
it.each([
|
||||
[now, undefined, undefined, true],
|
||||
[now, subDays(now, 1), undefined, true],
|
||||
[now, now, undefined, true],
|
||||
[now, undefined, addDays(now, 1), true],
|
||||
[now, undefined, now, true],
|
||||
[now, subDays(now, 1), addDays(now, 1), true],
|
||||
[now, now, now, true],
|
||||
[now, addDays(now, 1), undefined, false],
|
||||
[now, undefined, subDays(now, 1), false],
|
||||
[now, subDays(now, 3), subDays(now, 1), false],
|
||||
[now, addDays(now, 1), addDays(now, 3), false],
|
||||
])('returns true when a date is between provided range', (date, start, end, expectedResult) => {
|
||||
expect(isBetween(date, start, end)).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isBeforeOrEqual', () => {
|
||||
it.each([
|
||||
[now, now, true],
|
||||
[now, addDays(now, 1), true],
|
||||
[now, subDays(now, 1), false],
|
||||
])('returns true when the date before or equal to provided one', (date, dateToCompare, expectedResult) => {
|
||||
expect(isBeforeOrEqual(date, dateToCompare)).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
import { endOfDay, format, formatISO, startOfDay, subDays } from 'date-fns';
|
||||
import { now, parseDate } from '../../../../src/utils/dates/helpers/date';
|
||||
import type {
|
||||
DateInterval } from '../../../../src/utils/dates/helpers/dateIntervals';
|
||||
import {
|
||||
dateRangeIsEmpty,
|
||||
dateToMatchingInterval,
|
||||
intervalToDateRange,
|
||||
rangeIsInterval,
|
||||
rangeOrIntervalToString,
|
||||
toDateRange,
|
||||
} from '../../../../src/utils/dates/helpers/dateIntervals';
|
||||
|
||||
describe('date-types', () => {
|
||||
const daysBack = (days: number) => subDays(now(), days);
|
||||
|
||||
describe('dateRangeIsEmpty', () => {
|
||||
it.each([
|
||||
[undefined, true],
|
||||
[{}, true],
|
||||
[{ startDate: null }, true],
|
||||
[{ endDate: null }, true],
|
||||
[{ startDate: null, endDate: null }, true],
|
||||
[{ startDate: undefined }, true],
|
||||
[{ endDate: undefined }, true],
|
||||
[{ startDate: undefined, endDate: undefined }, true],
|
||||
[{ startDate: undefined, endDate: null }, true],
|
||||
[{ startDate: null, endDate: undefined }, true],
|
||||
[{ startDate: now() }, false],
|
||||
[{ endDate: now() }, false],
|
||||
[{ startDate: now(), endDate: now() }, false],
|
||||
])('returns proper result', (dateRange, expectedResult) => {
|
||||
expect(dateRangeIsEmpty(dateRange)).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rangeIsInterval', () => {
|
||||
it.each([
|
||||
[undefined, false],
|
||||
[{}, false],
|
||||
['today' as DateInterval, true],
|
||||
['yesterday' as DateInterval, true],
|
||||
])('returns proper result', (range, expectedResult) => {
|
||||
expect(rangeIsInterval(range)).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rangeOrIntervalToString', () => {
|
||||
it.each([
|
||||
[undefined, undefined],
|
||||
['today' as DateInterval, 'Today'],
|
||||
['yesterday' as DateInterval, 'Yesterday'],
|
||||
['last7Days' as DateInterval, 'Last 7 days'],
|
||||
['last30Days' as DateInterval, 'Last 30 days'],
|
||||
['last90Days' as DateInterval, 'Last 90 days'],
|
||||
['last180Days' as DateInterval, 'Last 180 days'],
|
||||
['last365Days' as DateInterval, 'Last 365 days'],
|
||||
[{}, undefined],
|
||||
[{ startDate: null }, undefined],
|
||||
[{ endDate: null }, undefined],
|
||||
[{ startDate: null, endDate: null }, undefined],
|
||||
[{ startDate: undefined }, undefined],
|
||||
[{ endDate: undefined }, undefined],
|
||||
[{ startDate: undefined, endDate: undefined }, undefined],
|
||||
[{ startDate: undefined, endDate: null }, undefined],
|
||||
[{ startDate: null, endDate: undefined }, undefined],
|
||||
[{ startDate: parseDate('2020-01-01', 'yyyy-MM-dd') }, 'Since 2020-01-01'],
|
||||
[{ endDate: parseDate('2020-01-01', 'yyyy-MM-dd') }, 'Until 2020-01-01'],
|
||||
[
|
||||
{ startDate: parseDate('2020-01-01', 'yyyy-MM-dd'), endDate: parseDate('2021-02-02', 'yyyy-MM-dd') },
|
||||
'2020-01-01 - 2021-02-02',
|
||||
],
|
||||
])('returns proper result', (range, expectedValue) => {
|
||||
expect(rangeOrIntervalToString(range)).toEqual(expectedValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('intervalToDateRange', () => {
|
||||
const formatted = (date?: Date | null): string | undefined => (!date ? undefined : format(date, 'yyyy-MM-dd'));
|
||||
|
||||
it.each([
|
||||
[undefined, undefined, undefined],
|
||||
['today' as DateInterval, now(), now()],
|
||||
['yesterday' as DateInterval, daysBack(1), daysBack(1)],
|
||||
['last7Days' as DateInterval, daysBack(7), now()],
|
||||
['last30Days' as DateInterval, daysBack(30), now()],
|
||||
['last90Days' as DateInterval, daysBack(90), now()],
|
||||
['last180Days' as DateInterval, daysBack(180), now()],
|
||||
['last365Days' as DateInterval, daysBack(365), now()],
|
||||
])('returns proper result', (interval, expectedStartDate, expectedEndDate) => {
|
||||
const { startDate, endDate } = intervalToDateRange(interval);
|
||||
|
||||
expect(formatted(expectedStartDate)).toEqual(formatted(startDate));
|
||||
expect(formatted(expectedEndDate)).toEqual(formatted(endDate));
|
||||
});
|
||||
});
|
||||
|
||||
describe('dateToMatchingInterval', () => {
|
||||
it.each([
|
||||
[startOfDay(now()), 'today'],
|
||||
[now(), 'today'],
|
||||
[formatISO(now()), 'today'],
|
||||
[daysBack(1), 'yesterday'],
|
||||
[endOfDay(daysBack(1)), 'yesterday'],
|
||||
[daysBack(2), 'last7Days'],
|
||||
[daysBack(7), 'last7Days'],
|
||||
[startOfDay(daysBack(7)), 'last7Days'],
|
||||
[daysBack(18), 'last30Days'],
|
||||
[daysBack(29), 'last30Days'],
|
||||
[daysBack(58), 'last90Days'],
|
||||
[startOfDay(daysBack(90)), 'last90Days'],
|
||||
[daysBack(120), 'last180Days'],
|
||||
[daysBack(250), 'last365Days'],
|
||||
[daysBack(366), 'all'],
|
||||
[formatISO(daysBack(500)), 'all'],
|
||||
])('returns the first interval which contains provided date', (date, expectedInterval) => {
|
||||
expect(dateToMatchingInterval(date)).toEqual(expectedInterval);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toDateRange', () => {
|
||||
it.each([
|
||||
['today' as DateInterval, intervalToDateRange('today')],
|
||||
['yesterday' as DateInterval, intervalToDateRange('yesterday')],
|
||||
['last7Days' as DateInterval, intervalToDateRange('last7Days')],
|
||||
['last30Days' as DateInterval, intervalToDateRange('last30Days')],
|
||||
['last90Days' as DateInterval, intervalToDateRange('last90Days')],
|
||||
['last180Days' as DateInterval, intervalToDateRange('last180Days')],
|
||||
['last365Days' as DateInterval, intervalToDateRange('last365Days')],
|
||||
['all' as DateInterval, intervalToDateRange('all')],
|
||||
[{}, {}],
|
||||
[{ startDate: now() }, { startDate: now() }],
|
||||
[{ endDate: now() }, { endDate: now() }],
|
||||
[{ startDate: daysBack(10), endDate: now() }, { startDate: daysBack(10), endDate: now() }],
|
||||
])('returns properly parsed interval or range', (rangeOrInterval, expectedResult) => {
|
||||
expect(toDateRange(rangeOrInterval)).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
});
|
||||
65
shlink-web-component/test/utils/helpers/index.test.ts
Normal file
65
shlink-web-component/test/utils/helpers/index.test.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
nonEmptyValueOrNull,
|
||||
parseBooleanToString,
|
||||
parseOptionalBooleanToString,
|
||||
rangeOf,
|
||||
} from '../../../src/utils/helpers';
|
||||
|
||||
describe('utils', () => {
|
||||
describe('rangeOf', () => {
|
||||
const func = (i: number) => `result_${i}`;
|
||||
const size = 5;
|
||||
|
||||
it('builds a range of specified size invike provided function', () => {
|
||||
expect(rangeOf(size, func)).toEqual([
|
||||
'result_1',
|
||||
'result_2',
|
||||
'result_3',
|
||||
'result_4',
|
||||
'result_5',
|
||||
]);
|
||||
});
|
||||
|
||||
it('builds a range starting at provided pos', () => {
|
||||
const startAt = 3;
|
||||
|
||||
expect(rangeOf(size, func, startAt)).toEqual([
|
||||
'result_3',
|
||||
'result_4',
|
||||
'result_5',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('nonEmptyValueOrNull', () => {
|
||||
it.each([
|
||||
['', null],
|
||||
['Hello', 'Hello'],
|
||||
[[], null],
|
||||
[[1, 2, 3], [1, 2, 3]],
|
||||
[{}, null],
|
||||
[{ foo: 'bar' }, { foo: 'bar' }],
|
||||
])('returns expected value based on input', (value, expected) => {
|
||||
expect(nonEmptyValueOrNull(value)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseBooleanToString', () => {
|
||||
it.each([
|
||||
[true, 'true'],
|
||||
[false, 'false'],
|
||||
])('parses value as expected', (value, expectedResult) => {
|
||||
expect(parseBooleanToString(value)).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseOptionalBooleanToString', () => {
|
||||
it.each([
|
||||
[undefined, undefined],
|
||||
[true, 'true'],
|
||||
[false, 'false'],
|
||||
])('parses value as expected', (value, expectedResult) => {
|
||||
expect(parseOptionalBooleanToString(value)).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
});
|
||||
20
shlink-web-component/test/utils/helpers/numbers.test.ts
Normal file
20
shlink-web-component/test/utils/helpers/numbers.test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { roundTen } from '../../../src/utils/helpers/numbers';
|
||||
|
||||
describe('numbers', () => {
|
||||
describe('roundTen', () => {
|
||||
it('rounds provided number to the next multiple of ten', () => {
|
||||
const expectationsPairs = [
|
||||
[10, 10],
|
||||
[12, 20],
|
||||
[158, 160],
|
||||
[5, 10],
|
||||
[-42, -40],
|
||||
];
|
||||
|
||||
expect.assertions(expectationsPairs.length);
|
||||
expectationsPairs.forEach(([number, expected]) => {
|
||||
expect(roundTen(number)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
36
shlink-web-component/test/utils/helpers/qrCodes.test.ts
Normal file
36
shlink-web-component/test/utils/helpers/qrCodes.test.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { QrCodeFormat, QrErrorCorrection } from '../../../src/utils/helpers/qrCodes';
|
||||
import { buildQrCodeUrl } from '../../../src/utils/helpers/qrCodes';
|
||||
|
||||
describe('qrCodes', () => {
|
||||
describe('buildQrCodeUrl', () => {
|
||||
it.each([
|
||||
[
|
||||
'bar.io',
|
||||
{ size: 870, format: 'svg' as QrCodeFormat, margin: 0, errorCorrection: 'L' as QrErrorCorrection },
|
||||
'bar.io/qr-code?size=870&format=svg&errorCorrection=L',
|
||||
],
|
||||
[
|
||||
'bar.io',
|
||||
{ size: 200, format: 'svg' as QrCodeFormat, margin: 0, errorCorrection: 'L' as QrErrorCorrection },
|
||||
'bar.io/qr-code?size=200&format=svg&errorCorrection=L',
|
||||
],
|
||||
[
|
||||
'shlink.io',
|
||||
{ size: 456, format: 'png' as QrCodeFormat, margin: 10, errorCorrection: 'L' as QrErrorCorrection },
|
||||
'shlink.io/qr-code?size=456&format=png&errorCorrection=L&margin=10',
|
||||
],
|
||||
[
|
||||
'shlink.io',
|
||||
{ size: 456, format: 'png' as QrCodeFormat, margin: 0, errorCorrection: 'H' as QrErrorCorrection },
|
||||
'shlink.io/qr-code?size=456&format=png&errorCorrection=H',
|
||||
],
|
||||
[
|
||||
'shlink.io',
|
||||
{ size: 999, format: 'png' as QrCodeFormat, margin: 20, errorCorrection: 'Q' as QrErrorCorrection },
|
||||
'shlink.io/qr-code?size=999&format=png&errorCorrection=Q&margin=20',
|
||||
],
|
||||
])('builds expected URL based in params', (shortUrl, options, expectedUrl) => {
|
||||
expect(buildQrCodeUrl(shortUrl, options)).toEqual(expectedUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
25
shlink-web-component/test/utils/helpers/query.test.ts
Normal file
25
shlink-web-component/test/utils/helpers/query.test.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { parseQuery, stringifyQuery } from '../../../src/utils/helpers/query';
|
||||
|
||||
describe('query', () => {
|
||||
describe('parseQuery', () => {
|
||||
it.each([
|
||||
['', {}],
|
||||
['foo=bar', { foo: 'bar' }],
|
||||
['?foo=bar', { foo: 'bar' }],
|
||||
['?foo=bar&baz=123', { foo: 'bar', baz: '123' }],
|
||||
])('parses query string as expected', (queryString, expectedResult) => {
|
||||
expect(parseQuery(queryString)).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stringifyQuery', () => {
|
||||
it.each([
|
||||
[{}, ''],
|
||||
[{ foo: 'bar' }, 'foo=bar'],
|
||||
[{ foo: 'bar', baz: '123' }, 'foo=bar&baz=123'],
|
||||
[{ bar: 'foo', list: ['one', 'two'] }, encodeURI('bar=foo&list[]=one&list[]=two')],
|
||||
])('stringifies query as expected', (queryObj, expectedResult) => {
|
||||
expect(stringifyQuery(queryObj)).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import type { LocalStorage } from '../../../../src/utils/services/LocalStorage';
|
||||
import { MAIN_COLOR } from '../../../../src/utils/theme';
|
||||
import { ColorGenerator } from '../../../src/utils/services/ColorGenerator';
|
||||
|
||||
describe('ColorGenerator', () => {
|
||||
let colorGenerator: ColorGenerator;
|
||||
const storageMock = fromPartial<LocalStorage>({
|
||||
set: vi.fn(),
|
||||
get: vi.fn().mockImplementation(() => undefined),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
colorGenerator = new ColorGenerator(storageMock);
|
||||
});
|
||||
|
||||
it('sets a color in the storage and makes it available after that', () => {
|
||||
const color = '#ff0000';
|
||||
|
||||
colorGenerator.setColorForKey('foo', color);
|
||||
|
||||
expect(colorGenerator.getColorForKey('foo')).toEqual(color);
|
||||
expect(storageMock.set).toHaveBeenCalledTimes(1);
|
||||
expect(storageMock.get).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('generates a random color when none is available for requested key', () => {
|
||||
expect(colorGenerator.getColorForKey('bar')).toEqual(expect.stringMatching(/^#(?:[0-9a-fA-F]{6})$/));
|
||||
expect(storageMock.set).toHaveBeenCalledTimes(1);
|
||||
expect(storageMock.get).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('trims and lower cases keys before trying to match', () => {
|
||||
const color = '#ff0000';
|
||||
|
||||
colorGenerator.setColorForKey('foo', color);
|
||||
|
||||
expect(colorGenerator.getColorForKey(' foo')).toEqual(color);
|
||||
expect(colorGenerator.getColorForKey('foO')).toEqual(color);
|
||||
expect(colorGenerator.getColorForKey('FoO')).toEqual(color);
|
||||
expect(colorGenerator.getColorForKey('FOO')).toEqual(color);
|
||||
expect(colorGenerator.getColorForKey('FOO ')).toEqual(color);
|
||||
expect(colorGenerator.getColorForKey(' FoO ')).toEqual(color);
|
||||
expect(storageMock.set).toHaveBeenCalledTimes(1);
|
||||
expect(storageMock.get).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('isColorLightForKey', () => {
|
||||
it.each([
|
||||
[MAIN_COLOR, true],
|
||||
['#8A661C', false],
|
||||
['#F7BE05', true],
|
||||
['#5A02D8', false],
|
||||
['#202786', false],
|
||||
])('returns that the color for a key is light based on the color assigned to that key', (color, isLight) => {
|
||||
colorGenerator.setColorForKey('foo', color);
|
||||
|
||||
expect(isLight).toEqual(colorGenerator.isColorLightForKey('foo'));
|
||||
expect(isLight).toEqual(colorGenerator.isColorLightForKey('foo')); // To cover when color is already calculated
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ImageDownloader } from '../../../src/utils/services/ImageDownloader';
|
||||
import { windowMock } from '../../__mocks__/Window.mock';
|
||||
|
||||
describe('ImageDownloader', () => {
|
||||
const fetch = vi.fn();
|
||||
let imageDownloader: ImageDownloader;
|
||||
|
||||
beforeEach(() => {
|
||||
(global as any).URL = { createObjectURL: () => '' };
|
||||
|
||||
imageDownloader = new ImageDownloader(fetch, windowMock);
|
||||
});
|
||||
|
||||
it('calls URL with response type blob', async () => {
|
||||
fetch.mockResolvedValue({ blob: () => new Blob() });
|
||||
|
||||
await imageDownloader.saveImage('/foo/bar.png', 'my-image.png');
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith('/foo/bar.png');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { LocalStorage } from '../../../src/utils/services/LocalStorage';
|
||||
|
||||
describe('LocalStorage', () => {
|
||||
const getItem = vi.fn((key) => (key === 'shlink.foo' ? JSON.stringify({ foo: 'bar' }) : null));
|
||||
const setItem = vi.fn();
|
||||
const localStorageMock = fromPartial<Storage>({ getItem, setItem });
|
||||
let storage: LocalStorage;
|
||||
|
||||
beforeEach(() => {
|
||||
storage = new LocalStorage(localStorageMock);
|
||||
});
|
||||
|
||||
describe('set', () => {
|
||||
it('writes an stringified representation of provided value in local storage', () => {
|
||||
const value = { bar: 'baz' };
|
||||
|
||||
storage.set('foo', value);
|
||||
|
||||
expect(setItem).toHaveBeenCalledTimes(1);
|
||||
expect(setItem).toHaveBeenCalledWith('shlink.foo', JSON.stringify(value));
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('fetches item from local storage', () => {
|
||||
storage.get('foo');
|
||||
expect(getItem).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('returns parsed value when requested value is found in local storage', () => {
|
||||
const value = storage.get('foo');
|
||||
|
||||
expect(value).toEqual({ foo: 'bar' });
|
||||
});
|
||||
|
||||
it('returns undefined when requested value is not found in local storage', () => {
|
||||
const value = storage.get('bar');
|
||||
|
||||
expect(value).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { ExportableShortUrl } from '../../../src/short-urls/data';
|
||||
import { ReportExporter } from '../../../src/utils/services/ReportExporter';
|
||||
import type { NormalizedVisit } from '../../../src/visits/types';
|
||||
import { windowMock } from '../../__mocks__/Window.mock';
|
||||
|
||||
describe('ReportExporter', () => {
|
||||
const jsonToCsv = vi.fn();
|
||||
let exporter: ReportExporter;
|
||||
|
||||
beforeEach(() => {
|
||||
(global as any).Blob = class Blob {};
|
||||
(global as any).URL = { createObjectURL: () => '' };
|
||||
|
||||
exporter = new ReportExporter(windowMock, jsonToCsv);
|
||||
});
|
||||
|
||||
describe('exportVisits', () => {
|
||||
it('parses provided visits to CSV', () => {
|
||||
const visits: NormalizedVisit[] = [
|
||||
{
|
||||
browser: 'browser',
|
||||
city: 'city',
|
||||
country: 'country',
|
||||
date: 'date',
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
os: 'os',
|
||||
referer: 'referer',
|
||||
potentialBot: false,
|
||||
},
|
||||
];
|
||||
|
||||
exporter.exportVisits('my_visits.csv', visits);
|
||||
|
||||
expect(jsonToCsv).toHaveBeenCalledWith(visits);
|
||||
});
|
||||
|
||||
it('skips execution when list of visits is empty', () => {
|
||||
exporter.exportVisits('my_visits.csv', []);
|
||||
|
||||
expect(jsonToCsv).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('exportShortUrls', () => {
|
||||
it('parses provided short URLs to CSV', () => {
|
||||
const shortUrls: ExportableShortUrl[] = [
|
||||
{
|
||||
shortUrl: 'shortUrl',
|
||||
visits: 10,
|
||||
title: '',
|
||||
createdAt: '',
|
||||
longUrl: '',
|
||||
tags: '',
|
||||
shortCode: '',
|
||||
},
|
||||
];
|
||||
|
||||
exporter.exportShortUrls(shortUrls);
|
||||
|
||||
expect(jsonToCsv).toHaveBeenCalledWith(shortUrls);
|
||||
});
|
||||
|
||||
it('skips execution when list of visits is empty', () => {
|
||||
exporter.exportShortUrls([]);
|
||||
|
||||
expect(jsonToCsv).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import type { ColorGenerator } from '../../../../src/utils/services/ColorGenerator';
|
||||
|
||||
export const colorGeneratorMock = fromPartial<ColorGenerator>({
|
||||
getColorForKey: vi.fn(() => 'red'),
|
||||
setColorForKey: vi.fn(),
|
||||
isColorLightForKey: vi.fn(() => false),
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import type { OrderDir } from '../../../../shlink-frontend-kit/src/ordering/ordering';
|
||||
import { TableOrderIcon } from '../../../src/utils/table/TableOrderIcon';
|
||||
|
||||
describe('<TableOrderIcon />', () => {
|
||||
const setUp = (field: string, currentDir?: OrderDir, className?: string) => render(
|
||||
<TableOrderIcon currentOrder={{ dir: currentDir, field: 'foo' }} field={field} className={className} />,
|
||||
);
|
||||
|
||||
it.each([
|
||||
['foo', undefined],
|
||||
['bar', 'DESC' as OrderDir],
|
||||
['bar', 'ASC' as OrderDir],
|
||||
])('renders empty when not all conditions are met', (field, dir) => {
|
||||
const { container } = setUp(field, dir);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['DESC' as OrderDir],
|
||||
['ASC' as OrderDir],
|
||||
])('renders an icon when all conditions are met', (dir) => {
|
||||
const { container } = setUp('foo', dir);
|
||||
|
||||
expect(container.firstChild).not.toBeNull();
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it.each([
|
||||
[undefined, 'ms-1'],
|
||||
['foo', 'foo'],
|
||||
['bar', 'bar'],
|
||||
])('renders expected classname', (className, expectedClassName) => {
|
||||
const { container } = setUp('foo', 'ASC', className);
|
||||
expect(container.firstChild).toHaveClass(expectedClassName);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<TableOrderIcon /> > renders an icon when all conditions are met 1`] = `
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="svg-inline--fa fa-caret-down ms-1"
|
||||
data-icon="caret-down"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 320 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M137.4 374.6c12.5 12.5 32.8 12.5 45.3 0l128-128c9.2-9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8L32 192c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9l128 128z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
exports[`<TableOrderIcon /> > renders an icon when all conditions are met 2`] = `
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="svg-inline--fa fa-caret-up ms-1"
|
||||
data-icon="caret-up"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 320 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M182.6 137.4c-12.5-12.5-32.8-12.5-45.3 0l-128 128c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8H288c12.9 0 24.6-7.8 29.6-19.8s2.2-25.7-6.9-34.9l-128-128z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
Reference in New Issue
Block a user