Merge pull request #489 from acelaya-forks/feature/coverage-80

Feature/coverage 80
This commit is contained in:
Alejandro Celaya 2021-09-20 22:13:18 +02:00 committed by GitHub
commit 691dabcfbc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 516 additions and 85 deletions

View file

@ -20,6 +20,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
### Changed
* [#408](https://github.com/shlinkio/shlink-web-client/issues/408) Updated to Chart.js 3.5
* [#486](https://github.com/shlinkio/shlink-web-client/issues/486) Refactored components used to render visits charts, making them easier to maintain and understand.
* [#409](https://github.com/shlinkio/shlink-web-client/issues/409) Increased required code coverage and added hard threshold check.
### Deprecated
* *Nothing*

View file

@ -1,13 +1,20 @@
module.exports = {
coverageDirectory: '<rootDir>/coverage',
collectCoverageFrom: [
'src/**/*.{js,ts,tsx}',
'!src/registerServiceWorker.js',
'!src/index.ts',
'src/**/*.{ts,tsx}',
'!src/*.{ts,tsx}',
'!src/reducers/index.ts',
'!src/**/provideServices.ts',
'!src/container/*.ts',
],
coverageThreshold: {
global: {
statements: 85,
branches: 75,
functions: 80,
lines: 85,
},
},
resolver: 'jest-pnp-resolver',
setupFiles: [
'react-app-polyfill/jsdom',

View file

@ -16,8 +16,9 @@
"serve:build": "serve ./build",
"build": "node scripts/build.js",
"test": "node scripts/test.js --env=jsdom --colors --verbose",
"test:ci": "npm run test -- --coverage --coverageReporters=text --coverageReporters=text-summary --coverageReporters=clover",
"test:pretty": "npm run test -- --coverage --coverageReporters=text --coverageReporters=text-summary --coverageReporters=html",
"test:coverage": "npm run test -- --coverage --coverageReporters=text --coverageReporters=text-summary",
"test:ci": "npm run test:coverage -- --coverageReporters=clover",
"test:pretty": "npm run test:coverage -- --coverageReporters=html",
"mutate": "./node_modules/.bin/stryker run --concurrency 4"
},
"dependencies": {

View file

@ -1,4 +1,3 @@
import qs from 'qs';
import { isEmpty, isNil, reject } from 'ramda';
import { AxiosInstance, AxiosResponse, Method } from 'axios';
import { ShortUrlsListParams } from '../../short-urls/reducers/shortUrlsListParams';
@ -19,6 +18,7 @@ import {
ShlinkEditDomainRedirects,
ShlinkDomainRedirects,
} from '../types';
import { stringifyQuery } from '../../utils/helpers/query';
const buildShlinkBaseUrl = (url: string, apiVersion: number) => url ? `${url}/rest/v${apiVersion}` : '';
const rejectNilProps = reject(isNil);
@ -123,7 +123,7 @@ export default class ShlinkApiClient {
headers: { 'X-Api-Key': this.apiKey },
params: rejectNilProps(query),
data: body,
paramsSerializer: (params) => qs.stringify(params, { arrayFormat: 'brackets' }),
paramsSerializer: stringifyQuery,
});
} catch (e) {
const { response } = e;

View file

@ -1,4 +1,4 @@
@import './utils/base';
@import '../utils/base';
.app-container {
height: 100%;

View file

@ -1,11 +1,11 @@
import { useEffect, FC } from 'react';
import { Route, Switch } from 'react-router-dom';
import NotFound from './common/NotFound';
import { ServersMap } from './servers/data';
import { Settings } from './settings/reducers/settings';
import { changeThemeInMarkup } from './utils/theme';
import { AppUpdateBanner } from './common/AppUpdateBanner';
import { forceUpdate } from './utils/helpers/sw';
import NotFound from '../common/NotFound';
import { ServersMap } from '../servers/data';
import { Settings } from '../settings/reducers/settings';
import { changeThemeInMarkup } from '../utils/theme';
import { AppUpdateBanner } from '../common/AppUpdateBanner';
import { forceUpdate } from '../utils/helpers/sw';
import './App.scss';
interface AppProps {

View file

@ -1,6 +1,6 @@
import Bottle from 'bottlejs';
import { appUpdateAvailable, resetAppUpdate } from '../reducers/appUpdates';
import App from '../../App';
import App from '../App';
import { ConnectDecorator } from '../../container/types';
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {

View file

@ -31,7 +31,7 @@ const MainHeader = (ServersDropdown: FC) => ({ location }: RouteComponentProps)
<Collapse navbar isOpen={isOpen}>
<Nav navbar className="ml-auto">
<NavItem>
<NavLink tag={Link} to={'/settings'} active={pathname === settingsPath}>
<NavLink tag={Link} to={settingsPath} active={pathname === settingsPath}>
<FontAwesomeIcon icon={cogsIcon} />&nbsp; Settings
</NavLink>
</NavItem>

View file

@ -34,7 +34,7 @@ const RealTimeUpdates = (
placeholder="Immediate"
disabled={!realTimeUpdates.enabled}
value={intervalValue(realTimeUpdates.interval)}
onChange={(e) => setRealTimeUpdatesInterval(Number(e.target.value))}
onChange={({ target }) => setRealTimeUpdatesInterval(Number(target.value))}
/>
{realTimeUpdates.enabled && (
<small className="form-text text-muted">

View file

@ -12,7 +12,7 @@ export const SET_SETTINGS = 'shlink/realTimeUpdates/SET_SETTINGS';
* optional, as old instances of the app will load partial objects from local storage until it is saved again.
*/
interface RealTimeUpdatesSettings {
export interface RealTimeUpdatesSettings {
enabled: boolean;
interval?: number;
}

View file

@ -1,5 +1,5 @@
import { useState } from 'react';
import { Modal, ModalBody, ModalFooter, ModalHeader, Popover } from 'reactstrap';
import { Button, Input, Modal, ModalBody, ModalFooter, ModalHeader, Popover } from 'reactstrap';
import { ChromePicker } from 'react-color';
import { faPalette as colorIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@ -25,10 +25,12 @@ const EditTagModal = ({ getColorForKey }: ColorGenerator) => (
const [ color, setColor ] = useState(getColorForKey(tag));
const [ showColorPicker, toggleColorPicker, , hideColorPicker ] = useToggle();
const { editing, error, errorData } = tagEdit;
const saveTag = handleEventPreventingDefault(async () => editTag(tag, newTagName, color)
.then(() => tagEdited(tag, newTagName, color))
.then(toggle)
.catch(() => {}));
const saveTag = handleEventPreventingDefault(
async () => editTag(tag, newTagName, color)
.then(() => tagEdited(tag, newTagName, color))
.then(toggle)
.catch(() => {}),
);
return (
<Modal isOpen={isOpen} toggle={toggle} centered onClosed={hideColorPicker}>
@ -47,13 +49,11 @@ const EditTagModal = ({ getColorForKey }: ColorGenerator) => (
<Popover isOpen={showColorPicker} toggle={toggleColorPicker} target="colorPickerBtn" placement="right">
<ChromePicker color={color} disableAlpha onChange={({ hex }) => setColor(hex)} />
</Popover>
<input
type="text"
<Input
value={newTagName}
placeholder="Tag"
required
className="form-control"
onChange={(e) => setNewTagName(e.target.value)}
onChange={({ target }) => setNewTagName(target.value)}
/>
</div>
@ -64,8 +64,8 @@ const EditTagModal = ({ getColorForKey }: ColorGenerator) => (
)}
</ModalBody>
<ModalFooter>
<button type="button" className="btn btn-link" onClick={toggle}>Cancel</button>
<button type="submit" className="btn btn-primary" disabled={editing}>{editing ? 'Saving...' : 'Save'}</button>
<Button type="button" color="link" onClick={toggle}>Cancel</Button>
<Button color="primary" disabled={editing}>{editing ? 'Saving...' : 'Save'}</Button>
</ModalFooter>
</form>
</Modal>

View file

@ -1,9 +1,9 @@
import { shallow, ShallowWrapper } from 'enzyme';
import { Route } from 'react-router-dom';
import { Mock } from 'ts-mockery';
import { Settings } from '../src/settings/reducers/settings';
import appFactory from '../src/App';
import { AppUpdateBanner } from '../src/common/AppUpdateBanner';
import { Settings } from '../../src/settings/reducers/settings';
import appFactory from '../../src/app/App';
import { AppUpdateBanner } from '../../src/common/AppUpdateBanner';
describe('<App />', () => {
let wrapper: ShallowWrapper;

View file

@ -17,13 +17,13 @@ describe('appUpdatesReducer', () => {
});
describe('appUpdateAvailable', () => {
test('creates expected action', () => {
it('creates expected action', () => {
expect(appUpdateAvailable()).toEqual({ type: APP_UPDATE_AVAILABLE });
});
});
describe('resetAppUpdate', () => {
test('creates expected action', () => {
it('creates expected action', () => {
expect(resetAppUpdate()).toEqual({ type: RESET_APP_UPDATE });
});
});

View file

@ -0,0 +1,62 @@
import { shallow, ShallowWrapper } from 'enzyme';
import { Mock } from 'ts-mockery';
import { match } from 'react-router';
import { History, Location } from 'history';
import { Collapse, NavbarToggler, NavLink } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import createMainHeader from '../../src/common/MainHeader';
describe('<MainHeader />', () => {
const ServersDropdown = () => null;
const MainHeader = createMainHeader(ServersDropdown);
let wrapper: ShallowWrapper;
const createWrapper = (pathname = '') => {
const location = Mock.of<Location>({ pathname });
wrapper = shallow(<MainHeader history={Mock.all<History>()} location={location} match={Mock.all<match>()} />);
return wrapper;
};
afterEach(() => wrapper?.unmount());
it('renders ServersDropdown', () => {
const wrapper = createWrapper();
expect(wrapper.find(ServersDropdown)).toHaveLength(1);
});
it.each([
[ '/foo', false ],
[ '/bar', false ],
[ '/settings', true ],
])('sets link to settings as active only when current path is settings', (currentPath, isActive) => {
const wrapper = createWrapper(currentPath);
const settingsLink = wrapper.find(NavLink);
expect(settingsLink.prop('active')).toEqual(isActive);
});
it('renders expected class based on the nav bar state', () => {
const wrapper = createWrapper();
expect(wrapper.find(NavbarToggler).find(FontAwesomeIcon).prop('className')).toEqual('main-header__toggle-icon');
wrapper.find(NavbarToggler).simulate('click');
expect(wrapper.find(NavbarToggler).find(FontAwesomeIcon).prop('className')).toEqual(
'main-header__toggle-icon main-header__toggle-icon--opened',
);
wrapper.find(NavbarToggler).simulate('click');
expect(wrapper.find(NavbarToggler).find(FontAwesomeIcon).prop('className')).toEqual('main-header__toggle-icon');
});
it('opens Collapse when clicking toggle', () => {
const wrapper = createWrapper();
expect(wrapper.find(Collapse).prop('isOpen')).toEqual(false);
wrapper.find(NavbarToggler).simulate('click');
expect(wrapper.find(Collapse).prop('isOpen')).toEqual(true);
wrapper.find(NavbarToggler).simulate('click');
expect(wrapper.find(Collapse).prop('isOpen')).toEqual(false);
});
});

View file

@ -14,7 +14,7 @@ describe('<ShlinkVersionsContainer />', () => {
afterEach(() => wrapper?.unmount());
test.each([
it.each([
[ null, 'text-center' ],
[ Mock.of<NotFoundServer>({ serverNotFound: true }), 'text-center' ],
[ Mock.of<NonReachableServer>({ serverNotReachable: true }), 'text-center' ],

View file

@ -0,0 +1,25 @@
import { Mock } from 'ts-mockery';
import { AxiosInstance } from 'axios';
import { ImageDownloader } from '../../../src/common/services/ImageDownloader';
import { windowMock } from '../../mocks/WindowMock';
describe('ImageDownloader', () => {
const get = jest.fn();
const axios = Mock.of<AxiosInstance>({ get });
let imageDownloader: ImageDownloader;
beforeEach(() => {
jest.clearAllMocks();
(global as any).URL = { createObjectURL: () => '' };
imageDownloader = new ImageDownloader(axios, windowMock);
});
it('calls URL with response type blob', async () => {
get.mockResolvedValue({ data: {} });
await imageDownloader.saveImage('/foo/bar.png', 'my-image.png');
expect(get).toHaveBeenCalledWith('/foo/bar.png', { responseType: 'blob' });
});
});

View file

@ -10,28 +10,40 @@ describe('<DomainSelector />', () => {
let wrapper: ShallowWrapper;
const domainsList = Mock.of<DomainsList>({
domains: [
Mock.of<ShlinkDomain>({ domain: 'default.com', isDefault: true }),
Mock.of<ShlinkDomain>({ domain: 'foo.com' }),
Mock.of<ShlinkDomain>({ domain: 'bar.com' }),
],
});
const createWrapper = (value = '') => {
wrapper = shallow(
<DomainSelector value={value} domainsList={domainsList} listDomains={jest.fn()} onChange={jest.fn()} />,
);
beforeEach(() => {
wrapper = shallow(<DomainSelector domainsList={domainsList} listDomains={jest.fn()} onChange={jest.fn()} />);
});
return wrapper;
};
afterEach(jest.clearAllMocks);
afterEach(() => wrapper.unmount());
it('shows dropdown by default', () => {
it.each([
[ '', 'Domain', 'domains-dropdown__toggle-btn' ],
[ 'my-domain.com', 'Domain: my-domain.com', 'domains-dropdown__toggle-btn--active' ],
])('shows dropdown by default', (value, expectedText, expectedClassName) => {
const wrapper = createWrapper(value);
const input = wrapper.find(InputGroup);
const dropdown = wrapper.find(DropdownBtn);
expect(input).toHaveLength(0);
expect(dropdown).toHaveLength(1);
expect(dropdown.find(DropdownItem)).toHaveLength(4);
expect(dropdown.find(DropdownItem)).toHaveLength(5);
expect(dropdown.prop('text')).toEqual(expectedText);
expect(dropdown.prop('className')).toEqual(expectedClassName);
});
it('allows to toggle between dropdown and input', () => {
it('allows toggling between dropdown and input', () => {
const wrapper = createWrapper();
wrapper.find(DropdownItem).last().simulate('click');
expect(wrapper.find(InputGroup)).toHaveLength(1);
expect(wrapper.find(DropdownBtn)).toHaveLength(0);
@ -40,4 +52,14 @@ describe('<DomainSelector />', () => {
expect(wrapper.find(InputGroup)).toHaveLength(0);
expect(wrapper.find(DropdownBtn)).toHaveLength(1);
});
it.each([
[ 0, 'default.com<span class="float-right text-muted">default</span>' ],
[ 1, 'foo.com' ],
[ 2, 'bar.com' ],
])('shows expected content on every item', (index, expectedContent) => {
const item = createWrapper().find(DropdownItem).at(index);
expect(item.html()).toContain(expectedContent);
});
});

18
test/mocks/WindowMock.ts Normal file
View file

@ -0,0 +1,18 @@
import { Mock } from 'ts-mockery';
const createLinkMock = () => ({
setAttribute: jest.fn(),
click: jest.fn(),
style: {},
});
export const appendChild = jest.fn();
export const removeChild = jest.fn();
export const windowMock = Mock.of<Window>({
document: {
createElement: jest.fn(createLinkMock),
body: { appendChild, removeChild },
},
});

View file

@ -2,21 +2,9 @@ import { Mock } from 'ts-mockery';
import { CsvJson } from 'csvjson';
import ServersExporter from '../../../src/servers/services/ServersExporter';
import LocalStorage from '../../../src/utils/services/LocalStorage';
import { appendChild, removeChild, windowMock } from '../../mocks/WindowMock';
describe('ServersExporter', () => {
const createLinkMock = () => ({
setAttribute: jest.fn(),
click: jest.fn(),
style: {},
});
const appendChild = jest.fn();
const removeChild = jest.fn();
const windowMock = Mock.of<Window>({
document: {
createElement: jest.fn(createLinkMock),
body: { appendChild, removeChild },
},
});
const storageMock = Mock.of<LocalStorage>({
get: jest.fn(() => ({
abc123: {

View file

@ -0,0 +1,104 @@
import { shallow, ShallowWrapper } from 'enzyme';
import { Mock } from 'ts-mockery';
import { Input } from 'reactstrap';
import { RealTimeUpdatesSettings, Settings } from '../../src/settings/reducers/settings';
import RealTimeUpdates from '../../src/settings/RealTimeUpdates';
import ToggleSwitch from '../../src/utils/ToggleSwitch';
describe('<RealTimeUpdates />', () => {
const toggleRealTimeUpdates = jest.fn();
const setRealTimeUpdatesInterval = jest.fn();
let wrapper: ShallowWrapper;
const createWrapper = (realTimeUpdates: Partial<RealTimeUpdatesSettings> = {}) => {
const settings = Mock.of<Settings>({ realTimeUpdates });
wrapper = shallow(
<RealTimeUpdates
settings={settings}
toggleRealTimeUpdates={toggleRealTimeUpdates}
setRealTimeUpdatesInterval={setRealTimeUpdatesInterval}
/>,
);
return wrapper;
};
afterEach(jest.clearAllMocks);
afterEach(() => wrapper?.unmount());
it('renders enabled real time updates as expected', () => {
const wrapper = createWrapper({ enabled: true });
const toggle = wrapper.find(ToggleSwitch);
const label = wrapper.find('label');
const input = wrapper.find(Input);
const small = wrapper.find('small');
expect(toggle.prop('checked')).toEqual(true);
expect(toggle.html()).toContain('processed');
expect(toggle.html()).not.toContain('ignored');
expect(label.prop('className')).toEqual('');
expect(input.prop('disabled')).toEqual(false);
expect(small).toHaveLength(2);
});
it('renders disabled real time updates as expected', () => {
const wrapper = createWrapper({ enabled: false });
const toggle = wrapper.find(ToggleSwitch);
const label = wrapper.find('label');
const input = wrapper.find(Input);
const small = wrapper.find('small');
expect(toggle.prop('checked')).toEqual(false);
expect(toggle.html()).not.toContain('processed');
expect(toggle.html()).toContain('ignored');
expect(label.prop('className')).toEqual('text-muted');
expect(input.prop('disabled')).toEqual(true);
expect(small).toHaveLength(1);
});
it.each([
[ 1, 'minute' ],
[ 2, 'minutes' ],
[ 10, 'minutes' ],
[ 100, 'minutes' ],
])('shows expected children when interval is greater than 0', (interval, minutesWord) => {
const wrapper = createWrapper({ enabled: true, interval });
const span = wrapper.find('span');
const input = wrapper.find(Input);
expect(span).toHaveLength(1);
expect(span.html()).toEqual(
`<span>Updates will be reflected in the UI every <b>${interval}</b> ${minutesWord}.</span>`,
);
expect(input.prop('value')).toEqual(`${interval}`);
});
it.each([[ undefined ], [ 0 ]])('shows expected children when interval is 0 or undefined', (interval) => {
const wrapper = createWrapper({ enabled: true, interval });
const span = wrapper.find('span');
const small = wrapper.find('small').at(1);
const input = wrapper.find(Input);
expect(span).toHaveLength(0);
expect(small.html()).toContain('Updates will be reflected in the UI as soon as they happen.');
expect(input.prop('value')).toEqual('');
});
it('updates real time updates on input change', () => {
const wrapper = createWrapper();
const input = wrapper.find(Input);
expect(setRealTimeUpdatesInterval).not.toHaveBeenCalled();
input.simulate('change', { target: { value: '10' } });
expect(setRealTimeUpdatesInterval).toHaveBeenCalledWith(10);
});
it('toggles real time updates on switch change', () => {
const wrapper = createWrapper();
const toggle = wrapper.find(ToggleSwitch);
expect(toggleRealTimeUpdates).not.toHaveBeenCalled();
toggle.simulate('change');
expect(toggleRealTimeUpdates).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,18 @@
import { shallow } from 'enzyme';
import createSettings from '../../src/settings/Settings';
import NoMenuLayout from '../../src/common/NoMenuLayout';
describe('<Settings />', () => {
const Component = () => null;
const Settings = createSettings(Component, Component, Component, Component);
it('renders a no-menu layout with the expected settings sections', () => {
const wrapper = shallow(<Settings />);
const layout = wrapper.find(NoMenuLayout);
const sections = wrapper.find('SettingsSections');
expect(layout).toHaveLength(1);
expect(sections).toHaveLength(1);
expect((sections.prop('items') as any[]).flat()).toHaveLength(4); // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion
});
});

View file

@ -4,7 +4,7 @@ import Checkbox from '../../../src/utils/Checkbox';
import { InfoTooltip } from '../../../src/utils/InfoTooltip';
describe('<ShortUrlFormCheckboxGroup />', () => {
test.each([
it.each([
[ undefined, '', 0 ],
[ 'This is the tooltip', 'mr-2', 1 ],
])('renders tooltip only when provided', (infoTooltip, expectedClassName, expectedAmountOfTooltips) => {

View file

@ -0,0 +1,109 @@
import { shallow, ShallowWrapper } from 'enzyme';
import { Mock } from 'ts-mockery';
import { Button, Input, Modal, ModalHeader, Popover } from 'reactstrap';
import { ChromePicker } from 'react-color';
import { TagEdition } from '../../../src/tags/reducers/tagEdit';
import createEditTagModal from '../../../src/tags/helpers/EditTagModal';
import ColorGenerator from '../../../src/utils/services/ColorGenerator';
import { Result } from '../../../src/utils/Result';
import { ProblemDetailsError } from '../../../src/api/types';
import { ShlinkApiError } from '../../../src/api/ShlinkApiError';
describe('<EditTagModal />', () => {
const EditTagModal = createEditTagModal(Mock.of<ColorGenerator>({ getColorForKey: jest.fn(() => 'red') }));
const editTag = jest.fn().mockReturnValue(Promise.resolve());
const tagEdited = jest.fn().mockReturnValue(Promise.resolve());
const toggle = jest.fn();
let wrapper: ShallowWrapper;
const createWrapper = (tagEdit: Partial<TagEdition> = {}) => {
const edition = Mock.of<TagEdition>(tagEdit);
wrapper = shallow(
<EditTagModal isOpen tag="foo" tagEdit={edition} editTag={editTag} tagEdited={tagEdited} toggle={toggle} />,
);
return wrapper;
};
afterEach(jest.clearAllMocks);
afterEach(() => wrapper?.unmount());
it('allows modal to be toggled with different mechanisms', () => {
const wrapper = createWrapper();
const modal = wrapper.find(Modal);
const modalHeader = wrapper.find(ModalHeader);
const cancelBtn = wrapper.find(Button).findWhere((btn) => btn.prop('type') === 'button');
expect(toggle).not.toHaveBeenCalled();
(modal.prop('toggle') as Function)();
(modalHeader.prop('toggle') as Function)();
cancelBtn.simulate('click');
expect(toggle).toHaveBeenCalledTimes(3);
expect(editTag).not.toHaveBeenCalled();
expect(tagEdited).not.toHaveBeenCalled();
});
it.each([
[ true, 'Saving...' ],
[ false, 'Save' ],
])('renders submit button in expected state', (editing, expectedText) => {
const wrapper = createWrapper({ editing });
const submitBtn = wrapper.find(Button).findWhere((btn) => btn.prop('color') === 'primary');
expect(submitBtn.html()).toContain(expectedText);
expect(submitBtn.prop('disabled')).toEqual(editing);
});
it.each([
[ true, 1 ],
[ false, 0 ],
])('displays error result in case of error', (error, expectedResultCount) => {
const wrapper = createWrapper({ error, errorData: Mock.all<ProblemDetailsError>() });
const result = wrapper.find(Result);
const apiError = wrapper.find(ShlinkApiError);
expect(result).toHaveLength(expectedResultCount);
expect(apiError).toHaveLength(expectedResultCount);
});
it('updates tag value when text changes', () => {
const wrapper = createWrapper();
expect(wrapper.find(Input).prop('value')).toEqual('foo');
wrapper.find(Input).simulate('change', { target: { value: 'bar' } });
expect(wrapper.find(Input).prop('value')).toEqual('bar');
});
it('invokes all functions on form submit', async () => {
const wrapper = createWrapper();
const form = wrapper.find('form');
expect(editTag).not.toHaveBeenCalled();
expect(tagEdited).not.toHaveBeenCalled();
await form.simulate('submit', { preventDefault: jest.fn() }); // eslint-disable-line @typescript-eslint/await-thenable
expect(editTag).toHaveBeenCalled();
expect(tagEdited).toHaveBeenCalled();
});
it('changes color when changing on color picker', () => {
const wrapper = createWrapper();
expect(wrapper.find(ChromePicker).prop('color')).toEqual('red');
wrapper.find(ChromePicker).simulate('change', { hex: 'blue' });
expect(wrapper.find(ChromePicker).prop('color')).toEqual('blue');
});
it('allows toggling popover with different mechanisms', () => {
const wrapper = createWrapper();
expect(wrapper.find(Popover).prop('isOpen')).toEqual(false);
(wrapper.find(Popover).prop('toggle') as Function)();
expect(wrapper.find(Popover).prop('isOpen')).toEqual(true);
wrapper.find('.input-group-prepend').simulate('click');
expect(wrapper.find(Popover).prop('isOpen')).toEqual(false);
});
});

View file

@ -0,0 +1,35 @@
import { shallow, ShallowWrapper } from 'enzyme';
import { DropdownItem } from 'reactstrap';
import PaginationDropdown from '../../src/utils/PaginationDropdown';
describe('<PaginationDropdown />', () => {
const setValue = jest.fn();
let wrapper: ShallowWrapper;
beforeEach(() => {
wrapper = shallow(<PaginationDropdown ranges={[ 10, 50, 100, 200 ]} value={50} setValue={setValue} />);
});
afterEach(jest.clearAllMocks);
afterEach(() => wrapper?.unmount());
it('renders expected amount of items', () => {
const items = wrapper.find(DropdownItem);
expect(items).toHaveLength(6);
});
it.each([
[ 0, 10 ],
[ 1, 50 ],
[ 2, 100 ],
[ 3, 200 ],
[ 5, Infinity ],
])('sets expected value when an item is clicked', (index, expectedValue) => {
const item = wrapper.find(DropdownItem).at(index);
expect(setValue).not.toHaveBeenCalled();
item.simulate('click');
expect(setValue).toHaveBeenCalledWith(expectedValue);
});
});

View file

@ -10,7 +10,7 @@ import { parseDate } from '../../../../src/utils/helpers/date';
describe('date-types', () => {
describe('dateRangeIsEmpty', () => {
test.each([
it.each([
[ undefined, true ],
[{}, true ],
[{ startDate: null }, true ],
@ -24,24 +24,24 @@ describe('date-types', () => {
[{ startDate: new Date() }, false ],
[{ endDate: new Date() }, false ],
[{ startDate: new Date(), endDate: new Date() }, false ],
])('proper result is returned', (dateRange, expectedResult) => {
])('returns proper result', (dateRange, expectedResult) => {
expect(dateRangeIsEmpty(dateRange)).toEqual(expectedResult);
});
});
describe('rangeIsInterval', () => {
test.each([
it.each([
[ undefined, false ],
[{}, false ],
[ 'today' as DateInterval, true ],
[ 'yesterday' as DateInterval, true ],
])('proper result is returned', (range, expectedResult) => {
])('returns proper result', (range, expectedResult) => {
expect(rangeIsInterval(range)).toEqual(expectedResult);
});
});
describe('rangeOrIntervalToString', () => {
test.each([
it.each([
[ undefined, undefined ],
[ 'today' as DateInterval, 'Today' ],
[ 'yesterday' as DateInterval, 'Yesterday' ],
@ -65,7 +65,7 @@ describe('date-types', () => {
{ startDate: parseDate('2020-01-01', 'yyyy-MM-dd'), endDate: parseDate('2021-02-02', 'yyyy-MM-dd') },
'2020-01-01 - 2021-02-02',
],
])('proper result is returned', (range, expectedValue) => {
])('returns proper result', (range, expectedValue) => {
expect(rangeOrIntervalToString(range)).toEqual(expectedValue);
});
});
@ -75,7 +75,7 @@ describe('date-types', () => {
const daysBack = (days: number) => subDays(new Date(), days);
const formatted = (date?: Date | null): string | undefined => !date ? undefined : format(date, 'yyyy-MM-dd');
test.each([
it.each([
[ undefined, undefined, undefined ],
[ 'today' as DateInterval, now(), now() ],
[ 'yesterday' as DateInterval, daysBack(1), daysBack(1) ],
@ -84,7 +84,7 @@ describe('date-types', () => {
[ 'last90Days' as DateInterval, daysBack(90), now() ],
[ 'last180days' as DateInterval, daysBack(180), now() ],
[ 'last365Days' as DateInterval, daysBack(365), now() ],
])('proper result is returned', (interval, expectedStartDate, expectedEndDate) => {
])('returns proper result', (interval, expectedStartDate, expectedEndDate) => {
const { startDate, endDate } = intervalToDateRange(interval);
expect(formatted(expectedStartDate)).toEqual(formatted(startDate));

View file

@ -2,7 +2,7 @@ import { buildQrCodeUrl, QrCodeFormat, QrErrorCorrection } from '../../../src/ut
describe('qrCodes', () => {
describe('buildQrCodeUrl', () => {
test.each([
it.each([
[
'foo.com',
{ size: 530, format: 'svg' as QrCodeFormat, margin: 0, errorCorrection: 'L' as QrErrorCorrection },

View file

@ -3,7 +3,7 @@ import { Doughnut } from 'react-chartjs-2';
import { keys, values } from 'ramda';
import { DoughnutChart } from '../../../src/visits/charts/DoughnutChart';
describe.skip('<DoughnutChart />', () => {
describe('<DoughnutChart />', () => {
let wrapper: ShallowWrapper;
const stats = {
foo: 123,

View file

@ -15,7 +15,7 @@ describe('<DoughnutChartLegend />', () => {
},
});
test('renders the expected amount of items with expected colors and labels', () => {
it('renders the expected amount of items with expected colors and labels', () => {
const wrapper = shallow(<DoughnutChartLegend chart={chart} />);
const items = wrapper.find('li');

View file

@ -4,7 +4,7 @@ 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.skip('<HorizontalBarChart />', () => {
describe('<HorizontalBarChart />', () => {
let wrapper: ShallowWrapper;
const stats = {
foo: 123,
@ -16,7 +16,6 @@ describe.skip('<HorizontalBarChart />', () => {
it('renders Bar with expected properties', () => {
wrapper = shallow(<HorizontalBarChart stats={stats} />);
const horizontal = wrapper.find(Bar);
const cols = wrapper.find('.col-sm-12');
expect(horizontal).toHaveLength(1);
@ -37,7 +36,6 @@ describe.skip('<HorizontalBarChart />', () => {
},
y: { stacked: true },
});
expect(cols).toHaveLength(1);
});
it.each([

View file

@ -43,9 +43,13 @@ describe('visitsOverviewReducer', () => {
expect(visitsCount).toEqual(100);
});
it('returns updated amounts on CREATE_VISITS', () => {
it.each([
[ 50, 53 ],
[ 0, 3 ],
[ undefined, 3 ],
])('returns updated amounts on CREATE_VISITS', (providedOrphanVisitsCount, expectedOrphanVisitsCount) => {
const { visitsCount, orphanVisitsCount } = reducer(
state({ visitsCount: 100, orphanVisitsCount: 50 }),
state({ visitsCount: 100, orphanVisitsCount: providedOrphanVisitsCount }),
{
type: CREATE_VISITS,
createdVisits: [
@ -65,7 +69,7 @@ describe('visitsOverviewReducer', () => {
);
expect(visitsCount).toEqual(102);
expect(orphanVisitsCount).toEqual(53);
expect(orphanVisitsCount).toEqual(expectedOrphanVisitsCount);
});
});

View file

@ -2,19 +2,9 @@ import { Mock } from 'ts-mockery';
import { CsvJson } from 'csvjson';
import { VisitsExporter } from '../../../src/visits/services/VisitsExporter';
import { NormalizedVisit } from '../../../src/visits/types';
import { windowMock } from '../../mocks/WindowMock';
describe('VisitsExporter', () => {
const createLinkMock = () => ({
setAttribute: jest.fn(),
click: jest.fn(),
style: {},
});
const windowMock = Mock.of<Window>({
document: {
createElement: jest.fn(createLinkMock),
body: { appendChild: jest.fn(), removeChild: jest.fn() },
},
});
const toCSV = jest.fn();
const csvToJsonMock = Mock.of<CsvJson>({ toCSV });
let exporter: VisitsExporter;

View file

@ -1,6 +1,8 @@
import { Mock } from 'ts-mockery';
import { GroupedNewVisits, groupNewVisitsByType } from '../../../src/visits/types/helpers';
import { CreateVisit, OrphanVisit, Visit } from '../../../src/visits/types';
import { GroupedNewVisits, groupNewVisitsByType, toApiParams } from '../../../src/visits/types/helpers';
import { CreateVisit, OrphanVisit, Visit, VisitsParams } from '../../../src/visits/types';
import { ShlinkVisitsParams } from '../../../src/api/types';
import { formatIsoDate, parseDate } from '../../../src/utils/helpers/date';
describe('visitsTypeHelpers', () => {
describe('groupNewVisitsByType', () => {
@ -56,4 +58,51 @@ describe('visitsTypeHelpers', () => {
expect(groupNewVisitsByType(createdVisits)).toEqual(expectedResult);
});
});
describe('toApiParams', () => {
it.each([
[ { page: 5, itemsPerPage: 100 } as VisitsParams, { page: 5, itemsPerPage: 100 } as ShlinkVisitsParams ],
[
{
page: 1,
itemsPerPage: 30,
filter: { excludeBots: true },
} as VisitsParams,
{ page: 1, itemsPerPage: 30, excludeBots: true } as ShlinkVisitsParams,
],
(() => {
const endDate = parseDate('2020-05-05', 'yyyy-MM-dd');
return [
{
page: 20,
itemsPerPage: 1,
dateRange: { endDate },
} as VisitsParams,
{ page: 20, itemsPerPage: 1, endDate: formatIsoDate(endDate) } as ShlinkVisitsParams,
];
})(),
(() => {
const startDate = parseDate('2020-05-05', 'yyyy-MM-dd');
const endDate = parseDate('2021-10-30', 'yyyy-MM-dd');
return [
{
page: 20,
itemsPerPage: 1,
dateRange: { startDate, endDate },
filter: { excludeBots: false },
} as VisitsParams,
{
page: 20,
itemsPerPage: 1,
startDate: formatIsoDate(startDate),
endDate: formatIsoDate(endDate),
} as ShlinkVisitsParams,
];
})(),
])('converts param as expected', (visitsParams, expectedResult) => {
expect(toApiParams(visitsParams)).toEqual(expectedResult);
});
});
});