Merge pull request #100 from acelaya/feature/tests

Feature/tests
This commit is contained in:
Alejandro Celaya 2019-01-13 23:38:01 +01:00 committed by GitHub
commit 2d3363153a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 455 additions and 86 deletions

View file

@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
## [Unreleased]
## 2.0.0 - 2019-01-13
#### Added
@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
* [#87](https://github.com/shlinkio/shlink-web-client/issues/87) and [#89](https://github.com/shlinkio/shlink-web-client/issues/89) Updated all dependencies to latest major versions.
* [#96](https://github.com/shlinkio/shlink-web-client/issues/96) Updated visits page to load visits in multiple paginated requests of `5000` visits when used shlink server supports it. This will prevent shlink to hang when trying to load big amounts of visits.
* [#71](https://github.com/shlinkio/shlink-web-client/issues/71) Improved tests and increased code coverage.
#### Deprecated

View file

@ -1,17 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';
const ScrollToTop = (window) => class ScrollToTop extends React.Component {
const ScrollToTop = ({ scrollTo }) => class ScrollToTop extends React.Component {
static propTypes = {
location: PropTypes.object,
children: PropTypes.node,
};
componentDidUpdate(prevProps) {
componentDidUpdate({ location: prevLocation }) {
const { location } = this.props;
if (location !== prevProps.location) {
window.scrollTo(0, 0);
if (location !== prevLocation) {
scrollTo(0, 0);
}
}

View file

@ -13,11 +13,12 @@ import provideUtilsServices from '../utils/services/provideServices';
const bottle = new Bottle();
const { container } = bottle;
const lazyService = (container, serviceName) => (...args) => container[serviceName](...args);
const mapActionService = (map, actionName) => ({
...map,
// Wrap actual action service in a function so that it is lazily created the first time it is called
[actionName]: (...args) => container[actionName](...args),
[actionName]: lazyService(container, actionName),
});
const connect = (propsFromState, actionServiceNames) =>
reduxConnect(

View file

@ -1,6 +1,6 @@
import React from 'react';
import { UncontrolledTooltip } from 'reactstrap';
import { assoc } from 'ramda';
import { assoc, map } from 'ramda';
import { v4 as uuid } from 'uuid';
import PropTypes from 'prop-types';
@ -22,11 +22,16 @@ const ImportServersBtn = (serversImporter) => class ImportServersBtn extends Rea
render() {
const { importServersFromFile } = serversImporter;
const { onImport, createServers } = this.props;
const onChange = (e) =>
importServersFromFile(e.target.files[0])
.then((servers) => servers.map((server) => assoc('id', uuid(), server)))
const assocId = (server) => assoc('id', uuid(), server);
const onChange = ({ target }) =>
importServersFromFile(target.files[0])
.then(map(assocId))
.then(createServers)
.then(onImport);
.then(onImport)
.then(() => {
// Reset input after processing file
target.value = null;
});
return (
<React.Fragment>

View file

@ -14,20 +14,20 @@ export const listServers = (serversService) => () => ({
servers: serversService.listServers(),
});
export const createServer = (serversService) => (server) => {
export const createServer = (serversService, listServers) => (server) => {
serversService.createServer(server);
return listServers(serversService)();
return listServers();
};
export const deleteServer = (serversService) => (server) => {
export const deleteServer = (serversService, listServers) => (server) => {
serversService.deleteServer(server);
return listServers(serversService)();
return listServers();
};
export const createServers = (serversService) => (servers) => {
export const createServers = (serversService, listServers) => (servers) => {
serversService.createServers(servers);
return listServers(serversService)();
return listServers();
};

View file

@ -35,9 +35,9 @@ const provideServices = (bottle, connect, withRouter) => {
// Actions
bottle.serviceFactory('selectServer', selectServer, 'ServersService');
bottle.serviceFactory('createServer', createServer, 'ServersService');
bottle.serviceFactory('createServers', createServers, 'ServersService');
bottle.serviceFactory('deleteServer', deleteServer, 'ServersService');
bottle.serviceFactory('createServer', createServer, 'ServersService', 'listServers');
bottle.serviceFactory('createServers', createServers, 'ServersService', 'listServers');
bottle.serviceFactory('deleteServer', deleteServer, 'ServersService', 'listServers');
bottle.serviceFactory('listServers', listServers, 'ServersService');
bottle.serviceFactory('resetSelectedServer', () => resetSelectedServer);

View file

@ -14,7 +14,7 @@ import './ShortUrlsList.scss';
const SORTABLE_FIELDS = {
dateCreated: 'Created at',
shortCode: 'Short URL',
originalUrl: 'Long URL',
longUrl: 'Long URL',
visits: 'Visits',
};
@ -142,9 +142,9 @@ const ShortUrlsList = (ShortUrlsRow) => class ShortUrlsList extends React.Compon
</th>
<th
className="short-urls-list__header-cell short-urls-list__header-cell--with-action"
onClick={this.orderByColumn('originalUrl')}
onClick={this.orderByColumn('longUrl')}
>
{this.renderOrderIcon('originalUrl')}
{this.renderOrderIcon('longUrl')}
Long URL
</th>
<th className="short-urls-list__header-cell">Tags</th>

View file

@ -43,7 +43,6 @@ const ShortUrlsRow = (
render() {
const { shortUrl, selectedServer } = this.props;
const completeShortUrl = !selectedServer ? shortUrl.shortCode : `${selectedServer.url}/${shortUrl.shortCode}`;
return (
<tr className="short-urls-row">
@ -51,10 +50,10 @@ const ShortUrlsRow = (
<Moment format="YYYY-MM-DD HH:mm">{shortUrl.dateCreated}</Moment>
</td>
<td className="short-urls-row__cell" data-th="Short URL: ">
<ExternalLink href={completeShortUrl}>{completeShortUrl}</ExternalLink>
<ExternalLink href={shortUrl.shortUrl} />
</td>
<td className="short-urls-row__cell short-urls-row__cell--break" data-th="Long URL: ">
<ExternalLink href={shortUrl.originalUrl}>{shortUrl.originalUrl}</ExternalLink>
<ExternalLink href={shortUrl.longUrl} />
</td>
<td className="short-urls-row__cell" data-th="Tags: ">{this.renderTags(shortUrl.tags)}</td>
<td className="short-urls-row__cell text-md-right" data-th="Visits: ">{shortUrl.visitsCount}</td>
@ -66,7 +65,6 @@ const ShortUrlsRow = (
Copied short URL!
</small>
<ShortUrlsRowMenu
completeShortUrl={completeShortUrl}
selectedServer={selectedServer}
shortUrl={shortUrl}
onCopyToClipboard={() => stateFlagTimeout(this.setState.bind(this), 'copiedToClipboard')}

View file

@ -20,7 +20,6 @@ import './ShortUrlsRowMenu.scss';
const ShortUrlsRowMenu = (DeleteShortUrlModal, EditTagsModal) => class ShortUrlsRowMenu extends React.Component {
static propTypes = {
completeShortUrl: PropTypes.string,
onCopyToClipboard: PropTypes.func,
selectedServer: serverType,
shortUrl: shortUrlType,
@ -29,18 +28,18 @@ const ShortUrlsRowMenu = (DeleteShortUrlModal, EditTagsModal) => class ShortUrls
state = {
isOpen: false,
isQrModalOpen: false,
isPreviewOpen: false,
isPreviewModalOpen: false,
isTagsModalOpen: false,
isDeleteModalOpen: false,
};
toggle = () => this.setState(({ isOpen }) => ({ isOpen: !isOpen }));
render() {
const { completeShortUrl, onCopyToClipboard, selectedServer, shortUrl } = this.props;
const serverId = selectedServer ? selectedServer.id : '';
const { onCopyToClipboard, shortUrl, selectedServer: { id } } = this.props;
const completeShortUrl = shortUrl && shortUrl.shortUrl ? shortUrl.shortUrl : '';
const toggleModal = (prop) => () => this.setState((prevState) => ({ [prop]: !prevState[prop] }));
const toggleQrCode = toggleModal('isQrModalOpen');
const togglePreview = toggleModal('isPreviewOpen');
const togglePreview = toggleModal('isPreviewModalOpen');
const toggleTags = toggleModal('isTagsModalOpen');
const toggleDelete = toggleModal('isDeleteModalOpen');
@ -50,7 +49,7 @@ const ShortUrlsRowMenu = (DeleteShortUrlModal, EditTagsModal) => class ShortUrls
&nbsp;<FontAwesomeIcon icon={menuIcon} />&nbsp;
</DropdownToggle>
<DropdownMenu right>
<DropdownItem tag={Link} to={`/server/${serverId}/short-code/${shortUrl.shortCode}/visits`}>
<DropdownItem tag={Link} to={`/server/${id}/short-code/${shortUrl.shortCode}/visits`}>
<FontAwesomeIcon icon={pieChartIcon} /> &nbsp;Visit stats
</DropdownItem>
@ -67,31 +66,19 @@ const ShortUrlsRowMenu = (DeleteShortUrlModal, EditTagsModal) => class ShortUrls
<DropdownItem className="short-urls-row-menu__dropdown-item--danger" onClick={toggleDelete}>
<FontAwesomeIcon icon={deleteIcon} /> &nbsp;Delete short URL
</DropdownItem>
<DeleteShortUrlModal
shortUrl={shortUrl}
isOpen={this.state.isDeleteModalOpen}
toggle={toggleDelete}
/>
<DeleteShortUrlModal shortUrl={shortUrl} isOpen={this.state.isDeleteModalOpen} toggle={toggleDelete} />
<DropdownItem divider />
<DropdownItem onClick={togglePreview}>
<FontAwesomeIcon icon={pictureIcon} /> &nbsp;Preview
</DropdownItem>
<PreviewModal
url={completeShortUrl}
isOpen={this.state.isPreviewOpen}
toggle={togglePreview}
/>
<PreviewModal url={completeShortUrl} isOpen={this.state.isPreviewModalOpen} toggle={togglePreview} />
<DropdownItem onClick={toggleQrCode}>
<FontAwesomeIcon icon={qrIcon} /> &nbsp;QR code
</DropdownItem>
<QrCodeModal
url={completeShortUrl}
isOpen={this.state.isQrModalOpen}
toggle={toggleQrCode}
/>
<QrCodeModal url={completeShortUrl} isOpen={this.state.isQrModalOpen} toggle={toggleQrCode} />
<DropdownItem divider />

View file

@ -10,9 +10,10 @@ export const LIST_SHORT_URLS = 'shlink/shortUrlsList/LIST_SHORT_URLS';
/* eslint-enable padding-line-between-statements, newline-after-var */
export const shortUrlType = PropTypes.shape({
tags: PropTypes.arrayOf(PropTypes.string),
shortCode: PropTypes.string,
originalUrl: PropTypes.string,
shortUrl: PropTypes.string,
longUrl: PropTypes.string,
tags: PropTypes.arrayOf(PropTypes.string),
});
const initialState = {

View file

@ -13,13 +13,12 @@ export default class DeleteTagConfirmModal extends React.Component {
tagDeleted: PropTypes.func,
};
doDelete = () => {
doDelete = async () => {
const { tag, toggle, deleteTag } = this.props;
return deleteTag(tag).then(() => {
this.tagWasDeleted = true;
toggle();
});
await deleteTag(tag);
this.tagWasDeleted = true;
toggle();
};
handleOnClosed = () => {
if (!this.tagWasDeleted) {

View file

@ -34,13 +34,7 @@ export function VisitsHeader({ shortUrlDetail, shortUrlVisits }) {
Visit stats for <ExternalLink href={shortLink} />
</h2>
<hr />
{shortUrl.dateCreated && (
<div>
Created:
&nbsp;
{renderDate()}
</div>
)}
<div>Created: {renderDate()}</div>
<div>
Long URL:
&nbsp;

View file

@ -0,0 +1,29 @@
import React from 'react';
import { shallow } from 'enzyme';
import * as sinon from 'sinon';
import createScrollToTop from '../../src/common/ScrollToTop';
describe('<ScrollToTop />', () => {
let wrapper;
const window = {
scrollTo: sinon.spy(),
};
beforeEach(() => {
const ScrollToTop = createScrollToTop(window);
wrapper = shallow(<ScrollToTop locaction={{ href: 'foo' }}>Foobar</ScrollToTop>);
});
afterEach(() => {
wrapper.unmount();
window.scrollTo.resetHistory();
});
it('just renders children', () => expect(wrapper.text()).toEqual('Foobar'));
it('scrolls to top when location changes', () => {
wrapper.instance().componentDidUpdate({ location: { href: 'bar' } });
expect(window.scrollTo.calledOnce).toEqual(true);
});
});

View file

@ -13,6 +13,7 @@ describe('serverReducer', () => {
abc123: { id: 'abc123' },
def456: { id: 'def456' },
};
const expectedFetchServersResult = { type: FETCH_SERVERS, servers };
const ServersServiceMock = {
listServers: sinon.fake.returns(servers),
createServer: sinon.fake(),
@ -40,38 +41,38 @@ describe('serverReducer', () => {
it('fetches servers and returns them as part of the action', () => {
const result = listServers(ServersServiceMock)();
expect(result).toEqual({ type: FETCH_SERVERS, servers });
expect(ServersServiceMock.listServers.callCount).toEqual(1);
expect(ServersServiceMock.createServer.callCount).toEqual(0);
expect(ServersServiceMock.deleteServer.callCount).toEqual(0);
expect(ServersServiceMock.createServers.callCount).toEqual(0);
expect(result).toEqual(expectedFetchServersResult);
expect(ServersServiceMock.listServers.calledOnce).toEqual(true);
expect(ServersServiceMock.createServer.called).toEqual(false);
expect(ServersServiceMock.deleteServer.called).toEqual(false);
expect(ServersServiceMock.createServers.called).toEqual(false);
});
});
describe('createServer', () => {
it('adds new server and then fetches servers again', () => {
const serverToCreate = { id: 'abc123' };
const result = createServer(ServersServiceMock)(serverToCreate);
const result = createServer(ServersServiceMock, () => expectedFetchServersResult)(serverToCreate);
expect(result).toEqual({ type: FETCH_SERVERS, servers });
expect(ServersServiceMock.listServers.callCount).toEqual(1);
expect(ServersServiceMock.createServer.callCount).toEqual(1);
expect(result).toEqual(expectedFetchServersResult);
expect(ServersServiceMock.createServer.calledOnce).toEqual(true);
expect(ServersServiceMock.createServer.firstCall.calledWith(serverToCreate)).toEqual(true);
expect(ServersServiceMock.deleteServer.callCount).toEqual(0);
expect(ServersServiceMock.createServers.callCount).toEqual(0);
expect(ServersServiceMock.listServers.called).toEqual(false);
expect(ServersServiceMock.deleteServer.called).toEqual(false);
expect(ServersServiceMock.createServers.called).toEqual(false);
});
});
describe('deleteServer', () => {
it('deletes a server and then fetches servers again', () => {
const serverToDelete = { id: 'abc123' };
const result = deleteServer(ServersServiceMock)(serverToDelete);
const result = deleteServer(ServersServiceMock, () => expectedFetchServersResult)(serverToDelete);
expect(result).toEqual({ type: FETCH_SERVERS, servers });
expect(ServersServiceMock.listServers.callCount).toEqual(1);
expect(ServersServiceMock.createServer.callCount).toEqual(0);
expect(ServersServiceMock.createServers.callCount).toEqual(0);
expect(ServersServiceMock.deleteServer.callCount).toEqual(1);
expect(result).toEqual(expectedFetchServersResult);
expect(ServersServiceMock.listServers.called).toEqual(false);
expect(ServersServiceMock.createServer.called).toEqual(false);
expect(ServersServiceMock.createServers.called).toEqual(false);
expect(ServersServiceMock.deleteServer.calledOnce).toEqual(true);
expect(ServersServiceMock.deleteServer.firstCall.calledWith(serverToDelete)).toEqual(true);
});
});
@ -79,14 +80,14 @@ describe('serverReducer', () => {
describe('createServer', () => {
it('creates multiple servers and then fetches servers again', () => {
const serversToCreate = values(servers);
const result = createServers(ServersServiceMock)(serversToCreate);
const result = createServers(ServersServiceMock, () => expectedFetchServersResult)(serversToCreate);
expect(result).toEqual({ type: FETCH_SERVERS, servers });
expect(ServersServiceMock.listServers.callCount).toEqual(1);
expect(ServersServiceMock.createServer.callCount).toEqual(0);
expect(ServersServiceMock.createServers.callCount).toEqual(1);
expect(result).toEqual(expectedFetchServersResult);
expect(ServersServiceMock.listServers.called).toEqual(false);
expect(ServersServiceMock.createServer.called).toEqual(false);
expect(ServersServiceMock.createServers.calledOnce).toEqual(true);
expect(ServersServiceMock.createServers.firstCall.calledWith(serversToCreate)).toEqual(true);
expect(ServersServiceMock.deleteServer.callCount).toEqual(0);
expect(ServersServiceMock.deleteServer.called).toEqual(false);
});
});
});

View file

@ -15,7 +15,7 @@ describe('<CreateShortUrl />', () => {
const createShortUrl = sinon.spy();
beforeEach(() => {
const CreateShortUrl = createShortUrlsCreator(TagsSelector);
const CreateShortUrl = createShortUrlsCreator(TagsSelector, () => '');
wrapper = shallow(
<CreateShortUrl shortUrlCreationResult={shortUrlCreationResult} createShortUrl={createShortUrl} />

View file

@ -0,0 +1,108 @@
import React from 'react';
import { shallow } from 'enzyme';
import moment from 'moment';
import Moment from 'react-moment';
import { assoc, toString } from 'ramda';
import * as sinon from 'sinon';
import createShortUrlsRow from '../../../src/short-urls/helpers/ShortUrlsRow';
import ExternalLink from '../../../src/utils/ExternalLink';
import Tag from '../../../src/tags/helpers/Tag';
describe('<ShortUrlsRow />', () => {
let wrapper;
const mockFunction = () => '';
const ShortUrlsRowMenu = mockFunction;
const stateFlagTimeout = sinon.spy();
const colorGenerator = {
getColorForKey: mockFunction,
setColorForKey: mockFunction,
};
const server = {
url: 'https://doma.in',
};
const shortUrl = {
shortCode: 'abc123',
shortUrl: 'http://doma.in/abc123',
longUrl: 'http://foo.com/bar',
dateCreated: moment('2018-05-23 18:30:41').format(),
tags: [ 'nodejs', 'reactjs' ],
visitsCount: 45,
};
beforeEach(() => {
const ShortUrlsRow = createShortUrlsRow(ShortUrlsRowMenu, colorGenerator, stateFlagTimeout);
wrapper = shallow(
<ShortUrlsRow shortUrlsListParams={{}} refreshList={mockFunction} selecrtedServer={server} shortUrl={shortUrl} />
);
});
afterEach(() => wrapper.unmount());
it('renders date in first column', () => {
const col = wrapper.find('td').first();
const moment = col.find(Moment);
expect(moment.html()).toContain('>2018-05-23 18:30</time>');
});
it('renders short URL in second row', () => {
const col = wrapper.find('td').at(1);
const link = col.find(ExternalLink);
expect(link.prop('href')).toEqual(shortUrl.shortUrl);
});
it('renders long URL in third row', () => {
const col = wrapper.find('td').at(2); // eslint-disable-line no-magic-numbers
const link = col.find(ExternalLink);
expect(link.prop('href')).toEqual(shortUrl.longUrl);
});
describe('renders list of tags in fourth row', () => {
it('with tags', () => {
const col = wrapper.find('td').at(3); // eslint-disable-line no-magic-numbers
const tags = col.find(Tag);
expect(tags).toHaveLength(shortUrl.tags.length);
shortUrl.tags.forEach((tagValue, index) => {
const tag = tags.at(index);
expect(tag.prop('text')).toEqual(tagValue);
});
});
it('without tags', () => {
wrapper.setProps({ shortUrl: assoc('tags', [], shortUrl) });
const col = wrapper.find('td').at(3); // eslint-disable-line no-magic-numbers
expect(col.text()).toContain('No tags');
});
});
it('renders visits count in fifth row', () => {
const col = wrapper.find('td').at(4); // eslint-disable-line no-magic-numbers
expect(col.text()).toEqual(toString(shortUrl.visitsCount));
});
it('updates state when copied to clipboard', () => {
const col = wrapper.find('td').at(5); // eslint-disable-line no-magic-numbers
const menu = col.find(ShortUrlsRowMenu);
expect(menu).toHaveLength(1);
expect(stateFlagTimeout.called).toEqual(false);
menu.simulate('copyToClipboard');
expect(stateFlagTimeout.calledOnce).toEqual(true);
});
it('shows copy hint when state prop is true', () => {
// eslint-disable-next-line no-magic-numbers
const isHidden = () => wrapper.find('td').at(5).find('.short-urls-row__copy-hint').prop('hidden');
expect(isHidden()).toEqual(true);
wrapper.setState({ copiedToClipboard: true });
expect(isHidden()).toEqual(false);
});
});

View file

@ -0,0 +1,79 @@
import React from 'react';
import { shallow } from 'enzyme';
import * as sinon from 'sinon';
import { ButtonDropdown, DropdownItem } from 'reactstrap';
import createShortUrlsRowMenu from '../../../src/short-urls/helpers/ShortUrlsRowMenu';
import PreviewModal from '../../../src/short-urls/helpers/PreviewModal';
import QrCodeModal from '../../../src/short-urls/helpers/QrCodeModal';
describe('<ShortUrlsRowMenu />', () => {
let wrapper;
const DeleteShortUrlModal = () => '';
const EditTagsModal = () => '';
const onCopyToClipboard = sinon.spy();
const selectedServer = { id: 'abc123' };
const shortUrl = {
shortCode: 'abc123',
shortUrl: 'https://doma.in/abc123',
};
beforeEach(() => {
const ShortUrlsRowMenu = createShortUrlsRowMenu(DeleteShortUrlModal, EditTagsModal);
wrapper = shallow(
<ShortUrlsRowMenu selectedServer={selectedServer} shortUrl={shortUrl} onCopyToClipboard={onCopyToClipboard} />
);
});
afterEach(() => wrapper.unmount());
it('renders modal windows', () => {
const deleteShortUrlModal = wrapper.find(DeleteShortUrlModal);
const editTagsModal = wrapper.find(EditTagsModal);
const previewModal = wrapper.find(PreviewModal);
const qrCodeModal = wrapper.find(QrCodeModal);
expect(deleteShortUrlModal).toHaveLength(1);
expect(editTagsModal).toHaveLength(1);
expect(previewModal).toHaveLength(1);
expect(qrCodeModal).toHaveLength(1);
});
it('renders correct amount of menu items', () => {
const items = wrapper.find(DropdownItem);
const expectedNonDividerItems = 6;
const expectedDividerItems = 2;
expect(items).toHaveLength(expectedNonDividerItems + expectedDividerItems);
expect(items.find('[divider]')).toHaveLength(expectedDividerItems);
});
describe('toggles state when toggling modal windows', () => {
const assert = (modalComponent, stateProp, done) => {
const modal = wrapper.find(modalComponent);
expect(wrapper.state(stateProp)).toEqual(false);
modal.prop('toggle')();
setImmediate(() => {
expect(wrapper.state(stateProp)).toEqual(true);
done();
});
};
it('DeleteShortUrlModal', (done) => assert(DeleteShortUrlModal, 'isDeleteModalOpen', done));
it('EditTagsModal', (done) => assert(EditTagsModal, 'isTagsModalOpen', done));
it('PreviewModal', (done) => assert(PreviewModal, 'isPreviewModalOpen', done));
it('QrCodeModal', (done) => assert(QrCodeModal, 'isQrModalOpen', done));
});
it('toggles dropdown state when toggling dropdown', (done) => {
const dropdown = wrapper.find(ButtonDropdown);
expect(wrapper.state('isOpen')).toEqual(false);
dropdown.prop('toggle')();
setImmediate(() => {
expect(wrapper.state('isOpen')).toEqual(true);
done();
});
});
});

View file

@ -0,0 +1,87 @@
import React from 'react';
import { shallow } from 'enzyme';
import * as sinon from 'sinon';
import { Modal, ModalBody, ModalFooter } from 'reactstrap';
import DeleteTagConfirmModal from '../../../src/tags/helpers/DeleteTagConfirmModal';
describe('<DeleteTagConfirmModal />', () => {
let wrapper;
const tag = 'nodejs';
const deleteTag = sinon.spy();
const tagDeleted = sinon.spy();
const createWrapper = (tagDelete) => {
wrapper = shallow(
<DeleteTagConfirmModal
tag={tag}
toggle={() => ''}
isOpen
deleteTag={deleteTag}
tagDeleted={tagDeleted}
tagDelete={tagDelete}
/>
);
return wrapper;
};
afterEach(() => {
wrapper && wrapper.unmount();
deleteTag.resetHistory();
tagDeleted.resetHistory();
});
it('asks confirmation for provided tag to be deleted', () => {
wrapper = createWrapper({ error: false, deleting: false });
const body = wrapper.find(ModalBody);
const footer = wrapper.find(ModalFooter);
const delBtn = footer.find('.btn-danger');
expect(body.html()).toContain(`Are you sure you want to delete tag <b>${tag}</b>?`);
expect(delBtn.prop('disabled')).toEqual(false);
expect(delBtn.text()).toEqual('Delete tag');
});
it('shows error message when deletion failed', () => {
wrapper = createWrapper({ error: true, deleting: false });
const body = wrapper.find(ModalBody);
expect(body.html()).toContain('Something went wrong while deleting the tag :(');
});
it('shows loading status while deleting', () => {
wrapper = createWrapper({ error: false, deleting: true });
const footer = wrapper.find(ModalFooter);
const delBtn = footer.find('.btn-danger');
expect(delBtn.prop('disabled')).toEqual(true);
expect(delBtn.text()).toEqual('Deleting tag...');
});
it('deletes tag modal when btn is clicked', () => {
wrapper = createWrapper({ error: false, deleting: true });
const footer = wrapper.find(ModalFooter);
const delBtn = footer.find('.btn-danger');
delBtn.simulate('click');
expect(deleteTag.calledOnce).toEqual(true);
expect(deleteTag.calledWith(tag)).toEqual(true);
});
it('does no further actions when modal is closed without deleting tag', () => {
wrapper = createWrapper({ error: false, deleting: false });
const modal = wrapper.find(Modal);
modal.simulate('closed');
expect(tagDeleted.called).toEqual(false);
});
it('notifies tag to be deleted when modal is closed after deleting tag', () => {
wrapper = createWrapper({ error: false, deleting: false });
const modal = wrapper.find(Modal);
wrapper.instance().tagWasDeleted = true;
modal.simulate('closed');
expect(tagDeleted.calledOnce).toEqual(true);
expect(tagDeleted.calledWith(tag)).toEqual(true);
});
});

View file

@ -0,0 +1,78 @@
import React from 'react';
import { shallow } from 'enzyme';
import { keys, values } from 'ramda';
import SortableBarGraph from '../../src/visits/SortableBarGraph';
import GraphCard from '../../src/visits/GraphCard';
import SortingDropdown from '../../src/utils/SortingDropdown';
describe('<SortableBarGraph />', () => {
let wrapper;
const sortingItems = {
name: 'Name',
amount: 'Amount',
};
const stats = {
Foo: 100,
Bar: 50,
};
const createWrapper = (extraHeaderContent = []) => {
wrapper = shallow(
<SortableBarGraph title="Foo" stats={stats} sortingItems={sortingItems} extraHeaderContent={extraHeaderContent} />
);
return wrapper;
};
afterEach(() => wrapper && wrapper.unmount());
it('renders stats unchanged when no ordering is set', () => {
const wrapper = createWrapper();
const graphCard = wrapper.find(GraphCard);
expect(graphCard.prop('stats')).toEqual(stats);
});
describe('renders properly ordered stats when ordering is set', () => {
let assert;
beforeEach(() => {
const wrapper = createWrapper();
const dropdown = wrapper.find(SortingDropdown);
assert = (sortName, sortDir, expectedKeys, expectedValues, done) => {
dropdown.prop('onChange')(sortName, sortDir);
setImmediate(() => {
const graphCard = wrapper.find(GraphCard);
const statsKeys = keys(graphCard.prop('stats'));
const statsValues = values(graphCard.prop('stats'));
expect(statsKeys).toEqual(expectedKeys);
expect(statsValues).toEqual(expectedValues);
done();
});
};
});
// eslint-disable-next-line no-magic-numbers
it('name - ASC', (done) => assert('name', 'ASC', [ 'Bar', 'Foo' ], [ 50, 100 ], done));
// eslint-disable-next-line no-magic-numbers
it('name - DESC', (done) => assert('name', 'DESC', [ 'Foo', 'Bar' ], [ 100, 50 ], done));
// eslint-disable-next-line no-magic-numbers
it('value - ASC', (done) => assert('value', 'ASC', [ 'Bar', 'Foo' ], [ 50, 100 ], done));
// eslint-disable-next-line no-magic-numbers
it('value - DESC', (done) => assert('value', 'DESC', [ 'Foo', 'Bar' ], [ 100, 50 ], done));
});
it('renders extra header functions', () => {
const wrapper = createWrapper([
() => <span className="foo-span">Foo</span>,
() => <span className="bar-span">Bar</span>,
]);
expect(wrapper.find('.foo-span')).toHaveLength(1);
expect(wrapper.find('.bar-span')).toHaveLength(1);
});
});

View file

@ -8,6 +8,7 @@ describe('<VisitsHeader />', () => {
let wrapper;
const shortUrlDetail = {
shortUrl: {
shortUrl: 'https://doma.in/abc123',
longUrl: 'https://foo.bar/bar/foo',
dateCreated: '2018-01-01T10:00:00+01:00',
},