Merge pull request #640 from acelaya-forks/feature/more-rtl-tests

Feature/more rtl tests
This commit is contained in:
Alejandro Celaya 2022-05-09 19:53:46 +02:00 committed by GitHub
commit 686fe5abbe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 870 additions and 383 deletions

View file

@ -3,3 +3,5 @@ import 'jest-canvas-mock';
import ResizeObserver from 'resize-observer-polyfill'; import ResizeObserver from 'resize-observer-polyfill';
(global as any).ResizeObserver = ResizeObserver; (global as any).ResizeObserver = ResizeObserver;
(global as any).scrollTo = () => {};
(global as any).matchMedia = (media: string) => ({ matches: false, media });

View file

@ -9,10 +9,10 @@ module.exports = {
], ],
coverageThreshold: { coverageThreshold: {
global: { global: {
statements: 85, statements: 90,
branches: 80, branches: 80,
functions: 80, functions: 85,
lines: 85, lines: 90,
}, },
}, },
setupFiles: ['<rootDir>/config/jest/setupBeforeEnzyme.js', '<rootDir>/config/jest/setupEnzyme.js'], setupFiles: ['<rootDir>/config/jest/setupBeforeEnzyme.js', '<rootDir>/config/jest/setupEnzyme.js'],

View file

@ -18,7 +18,7 @@
"build:serve": "serve -p 5000 ./build", "build:serve": "serve -p 5000 ./build",
"test": "jest --env=jsdom --colors --verbose", "test": "jest --env=jsdom --colors --verbose",
"test:coverage": "npm run test -- --coverage --coverageReporters=text --coverageReporters=text-summary", "test:coverage": "npm run test -- --coverage --coverageReporters=text --coverageReporters=text-summary",
"test:ci": "npm run test:coverage -- --coverageReporters=clover", "test:ci": "npm run test:coverage -- --coverageReporters=clover --ci",
"test:pretty": "npm run test:coverage -- --coverageReporters=html", "test:pretty": "npm run test:coverage -- --coverageReporters=html",
"mutate": "./node_modules/.bin/stryker run --concurrency 4 --ignoreStatic" "mutate": "./node_modules/.bin/stryker run --concurrency 4 --ignoreStatic"
}, },

View file

@ -1,7 +1,7 @@
import { FC, PropsWithChildren, useEffect } from 'react'; import { FC, PropsWithChildren, useEffect } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
const ScrollToTop = (): FC<PropsWithChildren<unknown>> => ({ children }) => { export const ScrollToTop: FC<PropsWithChildren<unknown>> = ({ children }) => {
const location = useLocation(); const location = useLocation();
useEffect(() => { useEffect(() => {
@ -10,5 +10,3 @@ const ScrollToTop = (): FC<PropsWithChildren<unknown>> => ({ children }) => {
return <>{children}</>; return <>{children}</>;
}; };
export default ScrollToTop;

View file

@ -17,7 +17,9 @@ interface SimplePaginatorProps {
centered?: boolean; centered?: boolean;
} }
const SimplePaginator: FC<SimplePaginatorProps> = ({ pagesCount, currentPage, setCurrentPage, centered = true }) => { export const SimplePaginator: FC<SimplePaginatorProps> = (
{ pagesCount, currentPage, setCurrentPage, centered = true },
) => {
if (pagesCount < 2) { if (pagesCount < 2) {
return null; return null;
} }
@ -35,7 +37,9 @@ const SimplePaginator: FC<SimplePaginatorProps> = ({ pagesCount, currentPage, se
disabled={pageIsEllipsis(pageNumber)} disabled={pageIsEllipsis(pageNumber)}
active={currentPage === pageNumber} active={currentPage === pageNumber}
> >
<PaginationLink tag="span" onClick={onClick(pageNumber)}>{prettifyPageNumber(pageNumber)}</PaginationLink> <PaginationLink role="link" tag="span" onClick={onClick(pageNumber)}>
{prettifyPageNumber(pageNumber)}
</PaginationLink>
</PaginationItem> </PaginationItem>
))} ))}
<PaginationItem disabled={currentPage >= pagesCount}> <PaginationItem disabled={currentPage >= pagesCount}>
@ -44,5 +48,3 @@ const SimplePaginator: FC<SimplePaginatorProps> = ({ pagesCount, currentPage, se
</Pagination> </Pagination>
); );
}; };
export default SimplePaginator;

View file

@ -1,6 +1,6 @@
import axios from 'axios'; import axios from 'axios';
import Bottle from 'bottlejs'; import Bottle from 'bottlejs';
import ScrollToTop from '../ScrollToTop'; import { ScrollToTop } from '../ScrollToTop';
import { MainHeader } from '../MainHeader'; import { MainHeader } from '../MainHeader';
import { Home } from '../Home'; import { Home } from '../Home';
import { MenuLayout } from '../MenuLayout'; import { MenuLayout } from '../MenuLayout';
@ -23,7 +23,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.service('ReportExporter', ReportExporter, 'window', 'jsonToCsv'); bottle.service('ReportExporter', ReportExporter, 'window', 'jsonToCsv');
// Components // Components
bottle.serviceFactory('ScrollToTop', ScrollToTop); bottle.serviceFactory('ScrollToTop', () => ScrollToTop);
bottle.serviceFactory('MainHeader', MainHeader, 'ServersDropdown'); bottle.serviceFactory('MainHeader', MainHeader, 'ServersDropdown');

View file

@ -40,6 +40,7 @@ export const DomainSelector = ({ listDomains, value, domainsList, onChange }: Do
outline outline
type="button" type="button"
className="domains-dropdown__back-btn" className="domains-dropdown__back-btn"
aria-label="Back to domains list"
onClick={pipe(unselectDomain, hideInput)} onClick={pipe(unselectDomain, hideInput)}
> >
<FontAwesomeIcon icon={faUndo} /> <FontAwesomeIcon icon={faUndo} />

View file

@ -2,7 +2,7 @@ import { FC, useEffect, useRef } from 'react';
import { splitEvery } from 'ramda'; import { splitEvery } from 'ramda';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { SimpleCard } from '../utils/SimpleCard'; import { SimpleCard } from '../utils/SimpleCard';
import SimplePaginator from '../common/SimplePaginator'; import { SimplePaginator } from '../common/SimplePaginator';
import { useQueryState } from '../utils/helpers/hooks'; import { useQueryState } from '../utils/helpers/hooks';
import { parseQuery } from '../utils/helpers/query'; import { parseQuery } from '../utils/helpers/query';
import { TableOrderIcon } from '../utils/table/TableOrderIcon'; import { TableOrderIcon } from '../utils/table/TableOrderIcon';

View file

@ -4,7 +4,7 @@ import { min, splitEvery } from 'ramda';
import { faCheck as checkIcon, faRobot as botIcon } from '@fortawesome/free-solid-svg-icons'; import { faCheck as checkIcon, faRobot as botIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { UncontrolledTooltip } from 'reactstrap'; import { UncontrolledTooltip } from 'reactstrap';
import SimplePaginator from '../common/SimplePaginator'; import { SimplePaginator } from '../common/SimplePaginator';
import SearchField from '../utils/SearchField'; import SearchField from '../utils/SearchField';
import { determineOrderDir, Order, sortList } from '../utils/helpers/ordering'; import { determineOrderDir, Order, sortList } from '../utils/helpers/ordering';
import { prettify } from '../utils/helpers/numbers'; import { prettify } from '../utils/helpers/numbers';

View file

@ -2,7 +2,7 @@ import { FC, useState } from 'react';
import { fromPairs, pipe, reverse, sortBy, splitEvery, toLower, toPairs, type, zipObj } from 'ramda'; import { fromPairs, pipe, reverse, sortBy, splitEvery, toLower, toPairs, type, zipObj } from 'ramda';
import { rangeOf } from '../../utils/utils'; import { rangeOf } from '../../utils/utils';
import { Order } from '../../utils/helpers/ordering'; import { Order } from '../../utils/helpers/ordering';
import SimplePaginator from '../../common/SimplePaginator'; import { SimplePaginator } from '../../common/SimplePaginator';
import { roundTen } from '../../utils/helpers/numbers'; import { roundTen } from '../../utils/helpers/numbers';
import { OrderingDropdown } from '../../utils/OrderingDropdown'; import { OrderingDropdown } from '../../utils/OrderingDropdown';
import PaginationDropdown from '../../utils/PaginationDropdown'; import PaginationDropdown from '../../utils/PaginationDropdown';

View file

@ -1,47 +1,32 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { render, screen } from '@testing-library/react';
import { Link } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { NotFound } from '../../src/common/NotFound'; import { NotFound } from '../../src/common/NotFound';
import { SimpleCard } from '../../src/utils/SimpleCard';
describe('<NotFound />', () => { describe('<NotFound />', () => {
let wrapper: ShallowWrapper; const setUp = (props = {}) => render(<MemoryRouter><NotFound {...props} /></MemoryRouter>);
const createWrapper = (props = {}) => {
wrapper = shallow(<NotFound {...props} />).find(SimpleCard);
return wrapper;
};
afterEach(() => wrapper?.unmount());
it('shows expected error title', () => { it('shows expected error title', () => {
const wrapper = createWrapper(); setUp();
expect(screen.getByText('Oops! We could not find requested route.')).toBeInTheDocument();
expect(wrapper.contains('Oops! We could not find requested route.')).toEqual(true);
}); });
it('shows expected error message', () => { it('shows expected error message', () => {
const wrapper = createWrapper(); setUp();
expect(screen.getByText(
expect(wrapper.contains(
'Use your browser\'s back button to navigate to the page you have previously come from, or just press this button.', 'Use your browser\'s back button to navigate to the page you have previously come from, or just press this button.',
)).toEqual(true); )).toBeInTheDocument();
}); });
it('shows a link to the home', () => { it.each([
const wrapper = createWrapper(); [{}, '/', 'Home'],
const link = wrapper.find(Link); [{ to: '/foo/bar', children: 'Hello' }, '/foo/bar', 'Hello'],
[{ to: '/baz-bar', children: <>Foo</> }, '/baz-bar', 'Foo'],
])('shows expected link and text', (props, expectedLink, expectedText) => {
setUp(props);
const link = screen.getByRole('link');
expect(link.prop('to')).toEqual('/'); expect(link).toHaveAttribute('href', expectedLink);
expect(link.prop('className')).toEqual('btn btn-outline-primary btn-lg'); expect(link).toHaveTextContent(expectedText);
expect(link.prop('children')).toEqual('Home'); expect(link).toHaveAttribute('class', 'btn btn-outline-primary btn-lg');
});
it('shows a link with provided props', () => {
const wrapper = createWrapper({ to: '/foo/bar', children: 'Hello' });
const link = wrapper.find(Link);
expect(link.prop('to')).toEqual('/foo/bar');
expect(link.prop('className')).toEqual('btn btn-outline-primary btn-lg');
expect(link.prop('children')).toEqual('Hello');
}); });
}); });

View file

@ -1,21 +1,14 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { render, screen } from '@testing-library/react';
import createScrollToTop from '../../src/common/ScrollToTop'; import { MemoryRouter } from 'react-router-dom';
import { ScrollToTop } from '../../src/common/ScrollToTop';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn().mockReturnValue({}),
}));
describe('<ScrollToTop />', () => { describe('<ScrollToTop />', () => {
let wrapper: ShallowWrapper; it.each([
['Foobar'],
beforeEach(() => { ['Barfoo'],
const ScrollToTop = createScrollToTop(); ['Something'],
])('just renders children', (children) => {
wrapper = shallow(<ScrollToTop>Foobar</ScrollToTop>); render(<MemoryRouter><ScrollToTop>{children}</ScrollToTop></MemoryRouter>);
expect(screen.getByText(children)).toBeInTheDocument();
}); });
afterEach(() => wrapper.unmount());
it('just renders children', () => expect(wrapper.text()).toEqual('Foobar'));
}); });

View file

@ -1,17 +1,10 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { render, screen } from '@testing-library/react';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import ShlinkVersions, { ShlinkVersionsProps } from '../../src/common/ShlinkVersions'; import ShlinkVersions, { ShlinkVersionsProps } from '../../src/common/ShlinkVersions';
import { NonReachableServer, NotFoundServer, ReachableServer } from '../../src/servers/data'; import { NonReachableServer, NotFoundServer, ReachableServer } from '../../src/servers/data';
describe('<ShlinkVersions />', () => { describe('<ShlinkVersions />', () => {
let wrapper: ShallowWrapper; const setUp = (props: ShlinkVersionsProps) => render(<ShlinkVersions {...props} />);
const createWrapper = (props: ShlinkVersionsProps) => {
wrapper = shallow(<ShlinkVersions {...props} />);
return wrapper;
};
afterEach(() => wrapper?.unmount());
it.each([ it.each([
['1.2.3', Mock.of<ReachableServer>({ version: '1.0.0', printableVersion: 'foo' }), 'v1.2.3', 'foo'], ['1.2.3', Mock.of<ReachableServer>({ version: '1.0.0', printableVersion: 'foo' }), 'v1.2.3', 'foo'],
@ -22,15 +15,19 @@ describe('<ShlinkVersions />', () => {
])( ])(
'displays expected versions when selected server is reachable', 'displays expected versions when selected server is reachable',
(clientVersion, selectedServer, expectedClientVersion, expectedServerVersion) => { (clientVersion, selectedServer, expectedClientVersion, expectedServerVersion) => {
const wrapper = createWrapper({ clientVersion, selectedServer }); setUp({ clientVersion, selectedServer });
const links = wrapper.find('VersionLink'); const [serverLink, clientLink] = screen.getAllByRole('link');
const serverLink = links.at(0);
const clientLink = links.at(1);
expect(serverLink.prop('project')).toEqual('shlink'); expect(serverLink).toHaveAttribute(
expect(serverLink.prop('version')).toEqual(expectedServerVersion); 'href',
expect(clientLink.prop('project')).toEqual('shlink-web-client'); `https://github.com/shlinkio/shlink/releases/${expectedServerVersion}`,
expect(clientLink.prop('version')).toEqual(expectedClientVersion); );
expect(serverLink).toHaveTextContent(expectedServerVersion);
expect(clientLink).toHaveAttribute(
'href',
`https://github.com/shlinkio/shlink-web-client/releases/${expectedClientVersion}`,
);
expect(clientLink).toHaveTextContent(expectedClientVersion);
}, },
); );
@ -39,10 +36,10 @@ describe('<ShlinkVersions />', () => {
['1.2.3', Mock.of<NotFoundServer>({ serverNotFound: true })], ['1.2.3', Mock.of<NotFoundServer>({ serverNotFound: true })],
['1.2.3', Mock.of<NonReachableServer>({ serverNotReachable: true })], ['1.2.3', Mock.of<NonReachableServer>({ serverNotReachable: true })],
])('displays only client version when selected server is not reachable', (clientVersion, selectedServer) => { ])('displays only client version when selected server is not reachable', (clientVersion, selectedServer) => {
const wrapper = createWrapper({ clientVersion, selectedServer }); setUp({ clientVersion, selectedServer });
const links = wrapper.find('VersionLink'); const links = screen.getAllByRole('link');
expect(links).toHaveLength(1); expect(links).toHaveLength(1);
expect(links.at(0).prop('project')).toEqual('shlink-web-client'); expect(links[0]).toHaveAttribute('href', 'https://github.com/shlinkio/shlink-web-client/releases/v1.2.3');
}); });
}); });

View file

@ -1,26 +1,19 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { render } from '@testing-library/react';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import ShlinkVersionsContainer from '../../src/common/ShlinkVersionsContainer'; import ShlinkVersionsContainer from '../../src/common/ShlinkVersionsContainer';
import { SelectedServer } from '../../src/servers/data'; import { SelectedServer } from '../../src/servers/data';
import { Sidebar } from '../../src/common/reducers/sidebar'; import { Sidebar } from '../../src/common/reducers/sidebar';
describe('<ShlinkVersionsContainer />', () => { describe('<ShlinkVersionsContainer />', () => {
let wrapper: ShallowWrapper; const setUp = (sidebar: Sidebar) => render(
<ShlinkVersionsContainer selectedServer={Mock.all<SelectedServer>()} sidebar={sidebar} />,
const createWrapper = (sidebar: Sidebar) => { );
wrapper = shallow(<ShlinkVersionsContainer selectedServer={Mock.all<SelectedServer>()} sidebar={sidebar} />);
return wrapper;
};
afterEach(() => wrapper?.unmount());
it.each([ it.each([
[{ sidebarPresent: false }, 'text-center'], [{ sidebarPresent: false }, 'text-center'],
[{ sidebarPresent: true }, 'text-center shlink-versions-container--with-sidebar'], [{ sidebarPresent: true }, 'text-center shlink-versions-container--with-sidebar'],
])('renders proper col classes based on sidebar status', (sidebar, expectedClasses) => { ])('renders proper col classes based on sidebar status', (sidebar, expectedClasses) => {
const wrapper = createWrapper(sidebar); const { container } = setUp(sidebar);
expect(container.firstChild).toHaveAttribute('class', `${expectedClasses}`);
expect(wrapper.find('div').prop('className')).toEqual(`${expectedClasses}`);
}); });
}); });

View file

@ -1,28 +1,23 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { render, screen } from '@testing-library/react';
import { identity } from 'ramda'; import { SimplePaginator } from '../../src/common/SimplePaginator';
import { PaginationItem } from 'reactstrap';
import SimplePaginator from '../../src/common/SimplePaginator';
import { ELLIPSIS } from '../../src/utils/helpers/pagination'; import { ELLIPSIS } from '../../src/utils/helpers/pagination';
describe('<SimplePaginator />', () => { describe('<SimplePaginator />', () => {
let wrapper: ShallowWrapper; const setUp = (pagesCount: number, currentPage = 1) => render(
const createWrapper = (pagesCount: number, currentPage = 1) => { <SimplePaginator pagesCount={pagesCount} currentPage={currentPage} setCurrentPage={jest.fn()} />,
wrapper = shallow(<SimplePaginator pagesCount={pagesCount} currentPage={currentPage} setCurrentPage={identity} />); );
return wrapper;
};
afterEach(() => wrapper?.unmount());
it.each([-3, -2, 0, 1])('renders empty when the amount of pages is smaller than 2', (pagesCount) => { it.each([-3, -2, 0, 1])('renders empty when the amount of pages is smaller than 2', (pagesCount) => {
expect(createWrapper(pagesCount).text()).toEqual(''); const { container } = setUp(pagesCount);
expect(container.firstChild).toEqual(null);
}); });
describe('ELLIPSIS are rendered where expected', () => { describe('ELLIPSIS are rendered where expected', () => {
const getItemsForPages = (pagesCount: number, currentPage: number) => { const getItemsForPages = (pagesCount: number, currentPage: number) => {
const paginator = createWrapper(pagesCount, currentPage); setUp(pagesCount, currentPage);
const items = paginator.find(PaginationItem);
const itemsWithEllipsis = items.filterWhere((item) => item?.key()?.includes(ELLIPSIS)); const items = screen.getAllByRole('link');
const itemsWithEllipsis = items.filter((item) => item.innerHTML.includes(ELLIPSIS));
return { items, itemsWithEllipsis }; return { items, itemsWithEllipsis };
}; };
@ -30,22 +25,22 @@ describe('<SimplePaginator />', () => {
it('renders first ELLIPSIS', () => { it('renders first ELLIPSIS', () => {
const { items, itemsWithEllipsis } = getItemsForPages(9, 7); const { items, itemsWithEllipsis } = getItemsForPages(9, 7);
expect(items.at(2).html()).toContain(ELLIPSIS); expect(items[1]).toHaveTextContent(ELLIPSIS);
expect(itemsWithEllipsis).toHaveLength(1); expect(itemsWithEllipsis).toHaveLength(1);
}); });
it('renders last ELLIPSIS', () => { it('renders last ELLIPSIS', () => {
const { items, itemsWithEllipsis } = getItemsForPages(9, 2); const { items, itemsWithEllipsis } = getItemsForPages(9, 2);
expect(items.at(items.length - 3).html()).toContain(ELLIPSIS); expect(items[items.length - 2]).toHaveTextContent(ELLIPSIS);
expect(itemsWithEllipsis).toHaveLength(1); expect(itemsWithEllipsis).toHaveLength(1);
}); });
it('renders both ELLIPSIS', () => { it('renders both ELLIPSIS', () => {
const { items, itemsWithEllipsis } = getItemsForPages(20, 9); const { items, itemsWithEllipsis } = getItemsForPages(20, 9);
expect(items.at(2).html()).toContain(ELLIPSIS); expect(items[1]).toHaveTextContent(ELLIPSIS);
expect(items.at(items.length - 3).html()).toContain(ELLIPSIS); expect(items[items.length - 2]).toHaveTextContent(ELLIPSIS);
expect(itemsWithEllipsis).toHaveLength(2); expect(itemsWithEllipsis).toHaveLength(2);
}); });
}); });

View file

@ -1,25 +1,17 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { render } from '@testing-library/react';
import { ShlinkLogo, ShlinkLogoProps } from '../../../src/common/img/ShlinkLogo'; import { ShlinkLogo, ShlinkLogoProps } from '../../../src/common/img/ShlinkLogo';
import { MAIN_COLOR } from '../../../src/utils/theme'; import { MAIN_COLOR } from '../../../src/utils/theme';
describe('<ShlinkLogo />', () => { describe('<ShlinkLogo />', () => {
let wrapper: ShallowWrapper; const setUp = (props: ShlinkLogoProps) => render(<ShlinkLogo {...props} />);
const createWrapper = (props: ShlinkLogoProps) => {
wrapper = shallow(<ShlinkLogo {...props} />);
return wrapper;
};
afterEach(() => wrapper?.unmount());
it.each([ it.each([
[undefined, MAIN_COLOR], [undefined, MAIN_COLOR],
['red', 'red'], ['red', 'red'],
['white', 'white'], ['white', 'white'],
])('renders expected color', (color, expectedColor) => { ])('renders expected color', (color, expectedColor) => {
const wrapper = createWrapper({ color }); const { container } = setUp({ color });
expect(container.querySelector('g')).toHaveAttribute('fill', expectedColor);
expect(wrapper.find('g').prop('fill')).toEqual(expectedColor);
}); });
it.each([ it.each([
@ -27,8 +19,12 @@ describe('<ShlinkLogo />', () => {
['foo', 'foo'], ['foo', 'foo'],
['bar', 'bar'], ['bar', 'bar'],
])('renders expected class', (className, expectedClassName) => { ])('renders expected class', (className, expectedClassName) => {
const wrapper = createWrapper({ className }); const { container } = setUp({ className });
expect(wrapper.prop('className')).toEqual(expectedClassName); if (expectedClassName) {
expect(container.firstChild).toHaveAttribute('class', expectedClassName);
} else {
expect(container.firstChild).not.toHaveAttribute('class');
}
}); });
}); });

View file

@ -1,4 +1,4 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { render, screen } from '@testing-library/react';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { ShlinkDomainRedirects } from '../../src/api/types'; import { ShlinkDomainRedirects } from '../../src/api/types';
import { DomainRow } from '../../src/domains/DomainRow'; import { DomainRow } from '../../src/domains/DomainRow';
@ -6,42 +6,71 @@ import { SelectedServer } from '../../src/servers/data';
import { Domain } from '../../src/domains/data'; import { Domain } from '../../src/domains/data';
describe('<DomainRow />', () => { describe('<DomainRow />', () => {
let wrapper: ShallowWrapper; const redirectsCombinations = [
const createWrapper = (domain: Domain, selectedServer = Mock.all<SelectedServer>()) => { [Mock.of<ShlinkDomainRedirects>({ baseUrlRedirect: 'foo' })],
wrapper = shallow( [Mock.of<ShlinkDomainRedirects>({ invalidShortUrlRedirect: 'bar' })],
<DomainRow [Mock.of<ShlinkDomainRedirects>({ baseUrlRedirect: 'baz', regular404Redirect: 'foo' })],
domain={domain}
selectedServer={selectedServer}
editDomainRedirects={jest.fn()}
checkDomainHealth={jest.fn()}
/>,
);
return wrapper;
};
afterEach(() => wrapper?.unmount());
it.each([
[undefined, 3],
[Mock.of<ShlinkDomainRedirects>(), 3],
[Mock.of<ShlinkDomainRedirects>({ baseUrlRedirect: 'foo' }), 2],
[Mock.of<ShlinkDomainRedirects>({ invalidShortUrlRedirect: 'foo' }), 2],
[Mock.of<ShlinkDomainRedirects>({ baseUrlRedirect: 'foo', regular404Redirect: 'foo' }), 1],
[ [
Mock.of<ShlinkDomainRedirects>( Mock.of<ShlinkDomainRedirects>(
{ baseUrlRedirect: 'foo', regular404Redirect: 'foo', invalidShortUrlRedirect: 'foo' }, { baseUrlRedirect: 'baz', regular404Redirect: 'bar', invalidShortUrlRedirect: 'foo' },
), ),
0,
], ],
])('shows expected redirects', (redirects, expectedNoRedirects) => { ];
const wrapper = createWrapper(Mock.of<Domain>({ domain: '', isDefault: true, redirects })); const setUp = (domain: Domain, defaultRedirects?: ShlinkDomainRedirects) => render(
const noRedirects = wrapper.find('Nr'); <DomainRow
const cells = wrapper.find('td'); domain={domain}
defaultRedirects={defaultRedirects}
selectedServer={Mock.all<SelectedServer>()}
editDomainRedirects={jest.fn()}
checkDomainHealth={jest.fn()}
/>,
);
expect(noRedirects).toHaveLength(expectedNoRedirects); it.each(redirectsCombinations)('shows expected redirects', (redirects) => {
redirects?.baseUrlRedirect && expect(cells.at(1).html()).toContain(redirects.baseUrlRedirect); setUp(Mock.of<Domain>({ domain: '', isDefault: true, redirects }));
redirects?.regular404Redirect && expect(cells.at(2).html()).toContain(redirects.regular404Redirect); const cells = screen.getAllByRole('cell');
redirects?.invalidShortUrlRedirect && expect(cells.at(3).html()).toContain(redirects.invalidShortUrlRedirect);
redirects?.baseUrlRedirect && expect(cells[1]).toHaveTextContent(redirects.baseUrlRedirect);
redirects?.regular404Redirect && expect(cells[2]).toHaveTextContent(redirects.regular404Redirect);
redirects?.invalidShortUrlRedirect && expect(cells[3]).toHaveTextContent(redirects.invalidShortUrlRedirect);
expect(screen.queryByText('(as fallback)')).not.toBeInTheDocument();
});
it.each([
[undefined],
[Mock.of<ShlinkDomainRedirects>()],
])('shows expected "no redirects"', (redirects) => {
setUp(Mock.of<Domain>({ domain: '', isDefault: true, redirects }));
const cells = screen.getAllByRole('cell');
expect(cells[1]).toHaveTextContent('No redirect');
expect(cells[2]).toHaveTextContent('No redirect');
expect(cells[3]).toHaveTextContent('No redirect');
expect(screen.queryByText('(as fallback)')).not.toBeInTheDocument();
});
it.each(redirectsCombinations)('shows expected fallback redirects', (fallbackRedirects) => {
setUp(Mock.of<Domain>({ domain: '', isDefault: true }), fallbackRedirects);
const cells = screen.getAllByRole('cell');
fallbackRedirects?.baseUrlRedirect && expect(cells[1]).toHaveTextContent(
`${fallbackRedirects.baseUrlRedirect} (as fallback)`,
);
fallbackRedirects?.regular404Redirect && expect(cells[2]).toHaveTextContent(
`${fallbackRedirects.regular404Redirect} (as fallback)`,
);
fallbackRedirects?.invalidShortUrlRedirect && expect(cells[3]).toHaveTextContent(
`${fallbackRedirects.invalidShortUrlRedirect} (as fallback)`,
);
});
it.each([[true], [false]])('shows icon on default domain only', (isDefault) => {
const { container } = setUp(Mock.of<Domain>({ domain: '', isDefault }));
if (isDefault) {
expect(container.querySelector('#defaultDomainIcon')).toBeInTheDocument();
} else {
expect(container.querySelector('#defaultDomainIcon')).not.toBeInTheDocument();
}
}); });
}); });

View file

@ -1,13 +1,10 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { DropdownItem, InputGroup } from 'reactstrap';
import { DomainSelector } from '../../src/domains/DomainSelector'; import { DomainSelector } from '../../src/domains/DomainSelector';
import { DomainsList } from '../../src/domains/reducers/domainsList'; import { DomainsList } from '../../src/domains/reducers/domainsList';
import { ShlinkDomain } from '../../src/api/types'; import { ShlinkDomain } from '../../src/api/types';
import { DropdownBtn } from '../../src/utils/DropdownBtn';
describe('<DomainSelector />', () => { describe('<DomainSelector />', () => {
let wrapper: ShallowWrapper;
const domainsList = Mock.of<DomainsList>({ const domainsList = Mock.of<DomainsList>({
domains: [ domains: [
Mock.of<ShlinkDomain>({ domain: 'default.com', isDefault: true }), Mock.of<ShlinkDomain>({ domain: 'default.com', isDefault: true }),
@ -15,51 +12,59 @@ describe('<DomainSelector />', () => {
Mock.of<ShlinkDomain>({ domain: 'bar.com' }), Mock.of<ShlinkDomain>({ domain: 'bar.com' }),
], ],
}); });
const createWrapper = (value = '') => { const setUp = (value = '') => render(
wrapper = shallow( <DomainSelector value={value} domainsList={domainsList} listDomains={jest.fn()} onChange={jest.fn()} />,
<DomainSelector value={value} domainsList={domainsList} listDomains={jest.fn()} onChange={jest.fn()} />, );
);
return wrapper;
};
afterEach(jest.clearAllMocks); afterEach(jest.clearAllMocks);
afterEach(() => wrapper.unmount());
it.each([ it.each([
['', 'Domain', 'domains-dropdown__toggle-btn'], ['', 'Domain', 'domains-dropdown__toggle-btn'],
['my-domain.com', 'Domain: my-domain.com', 'domains-dropdown__toggle-btn--active'], ['my-domain.com', 'Domain: my-domain.com', 'domains-dropdown__toggle-btn--active'],
])('shows dropdown by default', (value, expectedText, expectedClassName) => { ])('shows dropdown by default', async (value, expectedText, expectedClassName) => {
const wrapper = createWrapper(value); setUp(value);
const input = wrapper.find(InputGroup);
const dropdown = wrapper.find(DropdownBtn);
expect(input).toHaveLength(0); const btn = screen.getByRole('button', { name: expectedText });
expect(dropdown).toHaveLength(1);
expect(dropdown.find(DropdownItem)).toHaveLength(5); expect(screen.queryByPlaceholderText('Domain')).not.toBeInTheDocument();
expect(dropdown.prop('text')).toEqual(expectedText); expect(btn).toHaveAttribute(
expect(dropdown.prop('className')).toEqual(expectedClassName); 'class',
`dropdown-btn__toggle btn-block ${expectedClassName} dropdown-toggle btn btn-primary`,
);
fireEvent.click(btn);
await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
expect(screen.getAllByRole('menuitem')).toHaveLength(4);
}); });
it('allows toggling between dropdown and input', () => { it('allows toggling between dropdown and input', async () => {
const wrapper = createWrapper(); setUp();
wrapper.find(DropdownItem).last().simulate('click'); expect(screen.queryByPlaceholderText('Domain')).not.toBeInTheDocument();
expect(wrapper.find(InputGroup)).toHaveLength(1); expect(screen.getByRole('button', { name: 'Domain' })).toBeInTheDocument();
expect(wrapper.find(DropdownBtn)).toHaveLength(0);
wrapper.find('.domains-dropdown__back-btn').simulate('click'); fireEvent.click(screen.getByRole('button', { name: 'Domain' }));
expect(wrapper.find(InputGroup)).toHaveLength(0); fireEvent.click(await screen.findByText('New domain'));
expect(wrapper.find(DropdownBtn)).toHaveLength(1);
expect(screen.getByPlaceholderText('Domain')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Domain' })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Back to domains list' }));
expect(screen.queryByPlaceholderText('Domain')).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Domain' })).toBeInTheDocument();
}); });
it.each([ it.each([
[0, 'default.com<span class="float-end text-muted">default</span>'], [0, 'default.comdefault'],
[1, 'foo.com'], [1, 'foo.com'],
[2, 'bar.com'], [2, 'bar.com'],
])('shows expected content on every item', (index, expectedContent) => { ])('shows expected content on every item', async (index, expectedContent) => {
const item = createWrapper().find(DropdownItem).at(index); setUp();
expect(item.html()).toContain(expectedContent); fireEvent.click(screen.getByRole('button', { name: 'Domain' }));
const items = await screen.findAllByRole('menuitem');
expect(items[index]).toHaveTextContent(expectedContent);
}); });
}); });

View file

@ -1,108 +1,70 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { DomainsList } from '../../src/domains/reducers/domainsList'; import { DomainsList } from '../../src/domains/reducers/domainsList';
import { ManageDomains } from '../../src/domains/ManageDomains'; import { ManageDomains } from '../../src/domains/ManageDomains';
import Message from '../../src/utils/Message';
import { Result } from '../../src/utils/Result';
import SearchField from '../../src/utils/SearchField';
import { ProblemDetailsError, ShlinkDomain } from '../../src/api/types'; import { ProblemDetailsError, ShlinkDomain } from '../../src/api/types';
import { ShlinkApiError } from '../../src/api/ShlinkApiError';
import { DomainRow } from '../../src/domains/DomainRow';
import { SelectedServer } from '../../src/servers/data'; import { SelectedServer } from '../../src/servers/data';
describe('<ManageDomains />', () => { describe('<ManageDomains />', () => {
const listDomains = jest.fn(); const listDomains = jest.fn();
const filterDomains = jest.fn(); const filterDomains = jest.fn();
let wrapper: ShallowWrapper; const setUp = (domainsList: DomainsList) => render(
const createWrapper = (domainsList: DomainsList) => { <ManageDomains
wrapper = shallow( listDomains={listDomains}
<ManageDomains filterDomains={filterDomains}
listDomains={listDomains} editDomainRedirects={jest.fn()}
filterDomains={filterDomains} checkDomainHealth={jest.fn()}
editDomainRedirects={jest.fn()} domainsList={domainsList}
checkDomainHealth={jest.fn()} selectedServer={Mock.all<SelectedServer>()}
domainsList={domainsList} />,
selectedServer={Mock.all<SelectedServer>()} );
/>,
);
return wrapper;
};
afterEach(jest.clearAllMocks); afterEach(jest.clearAllMocks);
afterEach(() => wrapper?.unmount());
it('shows loading message while domains are loading', () => { it('shows loading message while domains are loading', () => {
const wrapper = createWrapper(Mock.of<DomainsList>({ loading: true, filteredDomains: [] })); setUp(Mock.of<DomainsList>({ loading: true, filteredDomains: [] }));
const message = wrapper.find(Message);
const searchField = wrapper.find(SearchField);
const result = wrapper.find(Result);
const apiError = wrapper.find(ShlinkApiError);
expect(message).toHaveLength(1); expect(screen.getByText('Loading...')).toBeInTheDocument();
expect(message.prop('loading')).toEqual(true); expect(screen.queryByText('Error loading domains :(')).not.toBeInTheDocument();
expect(searchField).toHaveLength(0);
expect(result).toHaveLength(0);
expect(apiError).toHaveLength(0);
}); });
it('shows error result when domains loading fails', () => { it.each([
const errorData = Mock.of<ProblemDetailsError>(); [undefined, 'Error loading domains :('],
const wrapper = createWrapper(Mock.of<DomainsList>( [Mock.of<ProblemDetailsError>(), 'Error loading domains :('],
{ loading: false, error: true, errorData, filteredDomains: [] }, [Mock.of<ProblemDetailsError>({ detail: 'Foo error!!' }), 'Foo error!!'],
)); ])('shows error result when domains loading fails', (errorData, expectedErrorMessage) => {
const message = wrapper.find(Message); setUp(Mock.of<DomainsList>({ loading: false, error: true, errorData, filteredDomains: [] }));
const searchField = wrapper.find(SearchField);
const result = wrapper.find(Result);
const apiError = wrapper.find(ShlinkApiError);
expect(result).toHaveLength(1); expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
expect(result.prop('type')).toEqual('error'); expect(screen.getByText(expectedErrorMessage)).toBeInTheDocument();
expect(apiError).toHaveLength(1);
expect(apiError.prop('errorData')).toEqual(errorData);
expect(searchField).toHaveLength(1);
expect(message).toHaveLength(0);
}); });
it('filters domains when SearchField changes', () => { it('filters domains when SearchField changes', async () => {
const wrapper = createWrapper(Mock.of<DomainsList>({ loading: false, error: false, filteredDomains: [] })); setUp(Mock.of<DomainsList>({ loading: false, error: false, filteredDomains: [] }));
const searchField = wrapper.find(SearchField);
expect(filterDomains).not.toHaveBeenCalled(); expect(filterDomains).not.toHaveBeenCalled();
searchField.simulate('change'); fireEvent.change(screen.getByPlaceholderText('Search...'), { target: { value: 'Foo' } });
expect(filterDomains).toHaveBeenCalledTimes(1); await waitFor(() => expect(filterDomains).toHaveBeenCalledTimes(1));
}); });
it('shows expected headers', () => { it('shows expected headers and one row when list of domains is empty', () => {
const wrapper = createWrapper(Mock.of<DomainsList>({ loading: false, error: false, filteredDomains: [] })); setUp(Mock.of<DomainsList>({ loading: false, error: false, filteredDomains: [] }));
const headerCells = wrapper.find('th');
expect(headerCells).toHaveLength(7); expect(screen.getAllByRole('columnheader')).toHaveLength(7);
expect(screen.getByText('No results found')).toBeInTheDocument();
}); });
it('one row when list of domains is empty', () => { it('has many rows if multiple domains are provided', () => {
const wrapper = createWrapper(Mock.of<DomainsList>({ loading: false, error: false, filteredDomains: [] }));
const tableBody = wrapper.find('tbody');
const regularRows = tableBody.find('tr');
const domainRows = tableBody.find(DomainRow);
expect(regularRows).toHaveLength(1);
expect(regularRows.html()).toContain('No results found');
expect(domainRows).toHaveLength(0);
});
it('as many DomainRows as domains are provided', () => {
const filteredDomains = [ const filteredDomains = [
Mock.of<ShlinkDomain>({ domain: 'foo' }), Mock.of<ShlinkDomain>({ domain: 'foo' }),
Mock.of<ShlinkDomain>({ domain: 'bar' }), Mock.of<ShlinkDomain>({ domain: 'bar' }),
Mock.of<ShlinkDomain>({ domain: 'baz' }), Mock.of<ShlinkDomain>({ domain: 'baz' }),
]; ];
const wrapper = createWrapper(Mock.of<DomainsList>({ loading: false, error: false, filteredDomains })); setUp(Mock.of<DomainsList>({ loading: false, error: false, filteredDomains }));
const tableBody = wrapper.find('tbody');
const regularRows = tableBody.find('tr');
const domainRows = tableBody.find(DomainRow);
expect(regularRows).toHaveLength(0); expect(screen.getAllByRole('row')).toHaveLength(filteredDomains.length + 1);
expect(domainRows).toHaveLength(filteredDomains.length); expect(screen.getByText('foo')).toBeInTheDocument();
expect(screen.getByText('bar')).toBeInTheDocument();
expect(screen.getByText('baz')).toBeInTheDocument();
}); });
}); });

View file

@ -1,73 +1,30 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { fireEvent, render, screen } from '@testing-library/react';
import { UncontrolledTooltip } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { faTimes, faCheck, faCircleNotch } from '@fortawesome/free-solid-svg-icons';
import { DomainStatus } from '../../../src/domains/data'; import { DomainStatus } from '../../../src/domains/data';
import { DomainStatusIcon } from '../../../src/domains/helpers/DomainStatusIcon'; import { DomainStatusIcon } from '../../../src/domains/helpers/DomainStatusIcon';
describe('<DomainStatusIcon />', () => { describe('<DomainStatusIcon />', () => {
const matchMedia = jest.fn().mockReturnValue(Mock.of<MediaQueryList>({ matches: false })); const matchMedia = jest.fn().mockReturnValue(Mock.of<MediaQueryList>({ matches: false }));
let wrapper: ShallowWrapper; const setUp = (status: DomainStatus) => render(<DomainStatusIcon status={status} matchMedia={matchMedia} />);
const createWrapper = (status: DomainStatus) => {
wrapper = shallow(<DomainStatusIcon status={status} matchMedia={matchMedia} />);
return wrapper;
};
beforeEach(jest.clearAllMocks); beforeEach(jest.clearAllMocks);
afterEach(() => wrapper?.unmount());
it('renders loading icon when status is "validating"', () => { it.each([
const wrapper = createWrapper('validating'); ['validating' as DomainStatus],
const tooltip = wrapper.find(UncontrolledTooltip); ['invalid' as DomainStatus],
const faIcon = wrapper.find(FontAwesomeIcon); ['valid' as DomainStatus],
])('renders expected icon and tooltip when status is not validating', (status) => {
expect(tooltip).toHaveLength(0); const { container } = setUp(status);
expect(faIcon).toHaveLength(1); expect(container.firstChild).toMatchSnapshot();
expect(faIcon.prop('icon')).toEqual(faCircleNotch);
expect(faIcon.prop('spin')).toEqual(true);
}); });
it.each([ it.each([
[ ['invalid' as DomainStatus],
'invalid' as DomainStatus, ['valid' as DomainStatus],
faTimes, ])('renders proper tooltip based on state', async (status) => {
'Oops! There is some missing configuration, and short URLs shared with this domain will not work.', const { container } = setUp(status);
],
['valid' as DomainStatus, faCheck, 'Congratulations! This domain is properly configured.'],
])('renders expected icon and tooltip when status is not validating', (status, expectedIcon, expectedText) => {
const wrapper = createWrapper(status);
const tooltip = wrapper.find(UncontrolledTooltip);
const faIcon = wrapper.find(FontAwesomeIcon);
const getTooltipText = (): string => {
const children = tooltip.prop('children');
if (typeof children === 'string') { container.firstChild && fireEvent.mouseOver(container.firstChild);
return children; expect(await screen.findByRole('tooltip')).toMatchSnapshot();
}
return tooltip.find('span').html();
};
expect(tooltip).toHaveLength(1);
expect(tooltip.prop('autohide')).toEqual(status === 'valid');
expect(getTooltipText()).toContain(expectedText);
expect(faIcon).toHaveLength(1);
expect(faIcon.prop('icon')).toEqual(expectedIcon);
expect(faIcon.prop('spin')).toEqual(false);
});
it.each([
[true, 'top-start'],
[false, 'left'],
])('places the tooltip properly based on query match', (isMobile, expectedPlacement) => {
matchMedia.mockReturnValue(Mock.of<MediaQueryList>({ matches: isMobile }));
const wrapper = createWrapper('valid');
const tooltip = wrapper.find(UncontrolledTooltip);
expect(tooltip).toHaveLength(1);
expect(tooltip.prop('placement')).toEqual(expectedPlacement);
}); });
}); });

View file

@ -0,0 +1,89 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<DomainStatusIcon /> renders expected icon and tooltip when status is not validating 1`] = `
<svg
aria-hidden="true"
class="svg-inline--fa fa-circle-notch fa-spin fa-fw "
data-icon="circle-notch"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M222.7 32.15C227.7 49.08 218.1 66.9 201.1 71.94C121.8 95.55 64 169.1 64 255.1C64 362 149.1 447.1 256 447.1C362 447.1 448 362 448 255.1C448 169.1 390.2 95.55 310.9 71.94C293.9 66.9 284.3 49.08 289.3 32.15C294.4 15.21 312.2 5.562 329.1 10.6C434.9 42.07 512 139.1 512 255.1C512 397.4 397.4 511.1 256 511.1C114.6 511.1 0 397.4 0 255.1C0 139.1 77.15 42.07 182.9 10.6C199.8 5.562 217.6 15.21 222.7 32.15V32.15z"
fill="currentColor"
/>
</svg>
`;
exports[`<DomainStatusIcon /> renders expected icon and tooltip when status is not validating 2`] = `
<span>
<svg
aria-hidden="true"
class="svg-inline--fa fa-xmark fa-fw text-danger"
data-icon="xmark"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 320 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z"
fill="currentColor"
/>
</svg>
</span>
`;
exports[`<DomainStatusIcon /> renders expected icon and tooltip when status is not validating 3`] = `
<span>
<svg
aria-hidden="true"
class="svg-inline--fa fa-check fa-fw text-muted"
data-icon="check"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 448 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M438.6 105.4C451.1 117.9 451.1 138.1 438.6 150.6L182.6 406.6C170.1 419.1 149.9 419.1 137.4 406.6L9.372 278.6C-3.124 266.1-3.124 245.9 9.372 233.4C21.87 220.9 42.13 220.9 54.63 233.4L159.1 338.7L393.4 105.4C405.9 92.88 426.1 92.88 438.6 105.4H438.6z"
fill="currentColor"
/>
</svg>
</span>
`;
exports[`<DomainStatusIcon /> renders proper tooltip based on state 1`] = `
<div
class="tooltip-inner"
role="tooltip"
>
<span>
Oops! There is some missing configuration, and short URLs shared with this domain will not work.
<br />
Check the
<a
href="https://slnk.to/multi-domain-docs"
rel="noopener noreferrer"
target="_blank"
>
documentation
</a>
in order to find out what is missing.
</span>
</div>
`;
exports[`<DomainStatusIcon /> renders proper tooltip based on state 2`] = `
<div
class="tooltip-inner"
role="tooltip"
>
Congratulations! This domain is properly configured.
</div>
`;

View file

@ -4,7 +4,7 @@ import { useLocation } from 'react-router-dom';
import { TagsTable as createTagsTable } from '../../src/tags/TagsTable'; import { TagsTable as createTagsTable } from '../../src/tags/TagsTable';
import { SelectedServer } from '../../src/servers/data'; import { SelectedServer } from '../../src/servers/data';
import { rangeOf } from '../../src/utils/utils'; import { rangeOf } from '../../src/utils/utils';
import SimplePaginator from '../../src/common/SimplePaginator'; import { SimplePaginator } from '../../src/common/SimplePaginator';
import { NormalizedTag } from '../../src/tags/data'; import { NormalizedTag } from '../../src/tags/data';
jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useLocation: jest.fn() })); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useLocation: jest.fn() }));

View file

@ -2,7 +2,7 @@ import { shallow, ShallowWrapper } from 'enzyme';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import VisitsTable, { VisitsTableProps } from '../../src/visits/VisitsTable'; import VisitsTable, { VisitsTableProps } from '../../src/visits/VisitsTable';
import { rangeOf } from '../../src/utils/utils'; import { rangeOf } from '../../src/utils/utils';
import SimplePaginator from '../../src/common/SimplePaginator'; import { SimplePaginator } from '../../src/common/SimplePaginator';
import SearchField from '../../src/utils/SearchField'; import SearchField from '../../src/utils/SearchField';
import { NormalizedVisit } from '../../src/visits/types'; import { NormalizedVisit } from '../../src/visits/types';
import { ReachableServer, SelectedServer } from '../../src/servers/data'; import { ReachableServer, SelectedServer } from '../../src/servers/data';

View file

@ -1,58 +1,20 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { render } from '@testing-library/react';
import { Bar } from 'react-chartjs-2'; import { HorizontalBarChart, HorizontalBarChartProps } from '../../../src/visits/charts/HorizontalBarChart';
import { prettify } from '../../../src/utils/helpers/numbers';
import { MAIN_COLOR, MAIN_COLOR_ALPHA } from '../../../src/utils/theme';
import { HorizontalBarChart } from '../../../src/visits/charts/HorizontalBarChart';
describe('<HorizontalBarChart />', () => { describe('<HorizontalBarChart />', () => {
let wrapper: ShallowWrapper; const setUp = (props: HorizontalBarChartProps) => {
const stats = { const { container } = render(<HorizontalBarChart {...props} />);
foo: 123, return container.querySelector('canvas')?.getContext('2d')?.__getEvents(); // eslint-disable-line no-underscore-dangle
bar: 456,
}; };
afterEach(() => wrapper?.unmount());
it('renders Bar with expected properties', () => {
wrapper = shallow(<HorizontalBarChart stats={stats} />);
const horizontal = wrapper.find(Bar);
expect(horizontal).toHaveLength(1);
const { datasets: [{ backgroundColor, borderColor }] } = horizontal.prop('data') as any;
const { plugins, scales } = (horizontal.prop('options') ?? {}) as any;
expect(backgroundColor).toEqual(MAIN_COLOR_ALPHA);
expect(borderColor).toEqual(MAIN_COLOR);
expect(plugins.legend).toEqual({ display: false });
expect(scales).toEqual({
x: {
beginAtZero: true,
stacked: true,
ticks: {
precision: 0,
callback: prettify,
},
},
y: { stacked: true },
});
});
it.each([ it.each([
[{ foo: 23 }, [100, 456], [23, 0]], [{ foo: 123, bar: 456 }, undefined],
[{ foo: 50 }, [73, 456], [50, 0]], [{ one: 999, two: 131313 }, { one: 30, two: 100 }],
[{ bar: 45 }, [123, 411], [0, 45]], [{ one: 999, two: 131313, max: 3 }, { one: 30, two: 100 }],
[{ bar: 20, foo: 13 }, [110, 436], [13, 20]], ])('renders chart with expected canvas', (stats, highlightedStats) => {
[undefined, [123, 456], undefined], const events = setUp({ stats, highlightedStats });
])('splits highlighted data from regular data', (highlightedStats, expectedData, expectedHighlightedData) => {
wrapper = shallow(<HorizontalBarChart stats={stats} highlightedStats={highlightedStats} />);
const horizontal = wrapper.find(Bar);
const { datasets: [{ data, label }, highlightedData] } = horizontal.prop('data') as any; expect(events).toBeTruthy();
expect(events).toMatchSnapshot();
expect(label).toEqual('Visits');
expect(data).toEqual(expectedData);
expectedHighlightedData && expect(highlightedData.data).toEqual(expectedHighlightedData);
!expectedHighlightedData && expect(highlightedData).toBeUndefined();
}); });
}); });

View file

@ -0,0 +1,521 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<HorizontalBarChart /> renders chart with expected canvas 1`] = `
Array [
Object {
"props": Object {
"a": 1,
"b": 0,
"c": 0,
"d": 1,
"e": 0,
"f": 0,
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "setTransform",
},
Object {
"props": Object {
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "font",
},
Object {
"props": Object {
"text": "foo",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "measureText",
},
Object {
"props": Object {
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "font",
},
Object {
"props": Object {
"text": "bar",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "measureText",
},
Object {
"props": Object {
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "font",
},
Object {
"props": Object {
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "font",
},
Object {
"props": Object {
"text": "0",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "measureText",
},
Object {
"props": Object {
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "font",
},
Object {
"props": Object {
"text": "500",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "measureText",
},
Object {
"props": Object {
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "font",
},
]
`;
exports[`<HorizontalBarChart /> renders chart with expected canvas 2`] = `
Array [
Object {
"props": Object {
"a": 1,
"b": 0,
"c": 0,
"d": 1,
"e": 0,
"f": 0,
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "setTransform",
},
Object {
"props": Object {
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "font",
},
Object {
"props": Object {
"text": "one",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "measureText",
},
Object {
"props": Object {
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "font",
},
Object {
"props": Object {
"text": "two",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "measureText",
},
Object {
"props": Object {
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "font",
},
Object {
"props": Object {
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "font",
},
Object {
"props": Object {
"text": "0",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "measureText",
},
Object {
"props": Object {
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "font",
},
Object {
"props": Object {
"text": "200,000",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "measureText",
},
Object {
"props": Object {
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "font",
},
]
`;
exports[`<HorizontalBarChart /> renders chart with expected canvas 3`] = `
Array [
Object {
"props": Object {
"a": 1,
"b": 0,
"c": 0,
"d": 1,
"e": 0,
"f": 0,
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "setTransform",
},
Object {
"props": Object {
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "font",
},
Object {
"props": Object {
"text": "one",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "measureText",
},
Object {
"props": Object {
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "font",
},
Object {
"props": Object {
"text": "two",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "measureText",
},
Object {
"props": Object {
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "font",
},
Object {
"props": Object {
"text": "max",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "measureText",
},
Object {
"props": Object {
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "font",
},
Object {
"props": Object {
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "font",
},
Object {
"props": Object {
"text": "0",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "measureText",
},
Object {
"props": Object {
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "font",
},
Object {
"props": Object {
"text": "200,000",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "measureText",
},
Object {
"props": Object {
"value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif",
},
"transform": Array [
1,
0,
0,
1,
0,
0,
],
"type": "font",
},
]
`;