mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 01:20:24 +03:00
commit
2d3363153a
20 changed files with 455 additions and 86 deletions
|
@ -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).
|
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
|
#### 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.
|
* [#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.
|
* [#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
|
#### Deprecated
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
const ScrollToTop = (window) => class ScrollToTop extends React.Component {
|
const ScrollToTop = ({ scrollTo }) => class ScrollToTop extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
location: PropTypes.object,
|
location: PropTypes.object,
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate({ location: prevLocation }) {
|
||||||
const { location } = this.props;
|
const { location } = this.props;
|
||||||
|
|
||||||
if (location !== prevProps.location) {
|
if (location !== prevLocation) {
|
||||||
window.scrollTo(0, 0);
|
scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,11 +13,12 @@ import provideUtilsServices from '../utils/services/provideServices';
|
||||||
const bottle = new Bottle();
|
const bottle = new Bottle();
|
||||||
const { container } = bottle;
|
const { container } = bottle;
|
||||||
|
|
||||||
|
const lazyService = (container, serviceName) => (...args) => container[serviceName](...args);
|
||||||
const mapActionService = (map, actionName) => ({
|
const mapActionService = (map, actionName) => ({
|
||||||
...map,
|
...map,
|
||||||
|
|
||||||
// Wrap actual action service in a function so that it is lazily created the first time it is called
|
// 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) =>
|
const connect = (propsFromState, actionServiceNames) =>
|
||||||
reduxConnect(
|
reduxConnect(
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { UncontrolledTooltip } from 'reactstrap';
|
import { UncontrolledTooltip } from 'reactstrap';
|
||||||
import { assoc } from 'ramda';
|
import { assoc, map } from 'ramda';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
@ -22,11 +22,16 @@ const ImportServersBtn = (serversImporter) => class ImportServersBtn extends Rea
|
||||||
render() {
|
render() {
|
||||||
const { importServersFromFile } = serversImporter;
|
const { importServersFromFile } = serversImporter;
|
||||||
const { onImport, createServers } = this.props;
|
const { onImport, createServers } = this.props;
|
||||||
const onChange = (e) =>
|
const assocId = (server) => assoc('id', uuid(), server);
|
||||||
importServersFromFile(e.target.files[0])
|
const onChange = ({ target }) =>
|
||||||
.then((servers) => servers.map((server) => assoc('id', uuid(), server)))
|
importServersFromFile(target.files[0])
|
||||||
|
.then(map(assocId))
|
||||||
.then(createServers)
|
.then(createServers)
|
||||||
.then(onImport);
|
.then(onImport)
|
||||||
|
.then(() => {
|
||||||
|
// Reset input after processing file
|
||||||
|
target.value = null;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
|
|
@ -14,20 +14,20 @@ export const listServers = (serversService) => () => ({
|
||||||
servers: serversService.listServers(),
|
servers: serversService.listServers(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createServer = (serversService) => (server) => {
|
export const createServer = (serversService, listServers) => (server) => {
|
||||||
serversService.createServer(server);
|
serversService.createServer(server);
|
||||||
|
|
||||||
return listServers(serversService)();
|
return listServers();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteServer = (serversService) => (server) => {
|
export const deleteServer = (serversService, listServers) => (server) => {
|
||||||
serversService.deleteServer(server);
|
serversService.deleteServer(server);
|
||||||
|
|
||||||
return listServers(serversService)();
|
return listServers();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createServers = (serversService) => (servers) => {
|
export const createServers = (serversService, listServers) => (servers) => {
|
||||||
serversService.createServers(servers);
|
serversService.createServers(servers);
|
||||||
|
|
||||||
return listServers(serversService)();
|
return listServers();
|
||||||
};
|
};
|
||||||
|
|
|
@ -35,9 +35,9 @@ const provideServices = (bottle, connect, withRouter) => {
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
bottle.serviceFactory('selectServer', selectServer, 'ServersService');
|
bottle.serviceFactory('selectServer', selectServer, 'ServersService');
|
||||||
bottle.serviceFactory('createServer', createServer, 'ServersService');
|
bottle.serviceFactory('createServer', createServer, 'ServersService', 'listServers');
|
||||||
bottle.serviceFactory('createServers', createServers, 'ServersService');
|
bottle.serviceFactory('createServers', createServers, 'ServersService', 'listServers');
|
||||||
bottle.serviceFactory('deleteServer', deleteServer, 'ServersService');
|
bottle.serviceFactory('deleteServer', deleteServer, 'ServersService', 'listServers');
|
||||||
bottle.serviceFactory('listServers', listServers, 'ServersService');
|
bottle.serviceFactory('listServers', listServers, 'ServersService');
|
||||||
|
|
||||||
bottle.serviceFactory('resetSelectedServer', () => resetSelectedServer);
|
bottle.serviceFactory('resetSelectedServer', () => resetSelectedServer);
|
||||||
|
|
|
@ -14,7 +14,7 @@ import './ShortUrlsList.scss';
|
||||||
const SORTABLE_FIELDS = {
|
const SORTABLE_FIELDS = {
|
||||||
dateCreated: 'Created at',
|
dateCreated: 'Created at',
|
||||||
shortCode: 'Short URL',
|
shortCode: 'Short URL',
|
||||||
originalUrl: 'Long URL',
|
longUrl: 'Long URL',
|
||||||
visits: 'Visits',
|
visits: 'Visits',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -142,9 +142,9 @@ const ShortUrlsList = (ShortUrlsRow) => class ShortUrlsList extends React.Compon
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
className="short-urls-list__header-cell short-urls-list__header-cell--with-action"
|
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
|
Long URL
|
||||||
</th>
|
</th>
|
||||||
<th className="short-urls-list__header-cell">Tags</th>
|
<th className="short-urls-list__header-cell">Tags</th>
|
||||||
|
|
|
@ -43,7 +43,6 @@ const ShortUrlsRow = (
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { shortUrl, selectedServer } = this.props;
|
const { shortUrl, selectedServer } = this.props;
|
||||||
const completeShortUrl = !selectedServer ? shortUrl.shortCode : `${selectedServer.url}/${shortUrl.shortCode}`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr className="short-urls-row">
|
<tr className="short-urls-row">
|
||||||
|
@ -51,10 +50,10 @@ const ShortUrlsRow = (
|
||||||
<Moment format="YYYY-MM-DD HH:mm">{shortUrl.dateCreated}</Moment>
|
<Moment format="YYYY-MM-DD HH:mm">{shortUrl.dateCreated}</Moment>
|
||||||
</td>
|
</td>
|
||||||
<td className="short-urls-row__cell" data-th="Short URL: ">
|
<td className="short-urls-row__cell" data-th="Short URL: ">
|
||||||
<ExternalLink href={completeShortUrl}>{completeShortUrl}</ExternalLink>
|
<ExternalLink href={shortUrl.shortUrl} />
|
||||||
</td>
|
</td>
|
||||||
<td className="short-urls-row__cell short-urls-row__cell--break" data-th="Long URL: ">
|
<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>
|
||||||
<td className="short-urls-row__cell" data-th="Tags: ">{this.renderTags(shortUrl.tags)}</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>
|
<td className="short-urls-row__cell text-md-right" data-th="Visits: ">{shortUrl.visitsCount}</td>
|
||||||
|
@ -66,7 +65,6 @@ const ShortUrlsRow = (
|
||||||
Copied short URL!
|
Copied short URL!
|
||||||
</small>
|
</small>
|
||||||
<ShortUrlsRowMenu
|
<ShortUrlsRowMenu
|
||||||
completeShortUrl={completeShortUrl}
|
|
||||||
selectedServer={selectedServer}
|
selectedServer={selectedServer}
|
||||||
shortUrl={shortUrl}
|
shortUrl={shortUrl}
|
||||||
onCopyToClipboard={() => stateFlagTimeout(this.setState.bind(this), 'copiedToClipboard')}
|
onCopyToClipboard={() => stateFlagTimeout(this.setState.bind(this), 'copiedToClipboard')}
|
||||||
|
|
|
@ -20,7 +20,6 @@ import './ShortUrlsRowMenu.scss';
|
||||||
|
|
||||||
const ShortUrlsRowMenu = (DeleteShortUrlModal, EditTagsModal) => class ShortUrlsRowMenu extends React.Component {
|
const ShortUrlsRowMenu = (DeleteShortUrlModal, EditTagsModal) => class ShortUrlsRowMenu extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
completeShortUrl: PropTypes.string,
|
|
||||||
onCopyToClipboard: PropTypes.func,
|
onCopyToClipboard: PropTypes.func,
|
||||||
selectedServer: serverType,
|
selectedServer: serverType,
|
||||||
shortUrl: shortUrlType,
|
shortUrl: shortUrlType,
|
||||||
|
@ -29,18 +28,18 @@ const ShortUrlsRowMenu = (DeleteShortUrlModal, EditTagsModal) => class ShortUrls
|
||||||
state = {
|
state = {
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
isQrModalOpen: false,
|
isQrModalOpen: false,
|
||||||
isPreviewOpen: false,
|
isPreviewModalOpen: false,
|
||||||
isTagsModalOpen: false,
|
isTagsModalOpen: false,
|
||||||
isDeleteModalOpen: false,
|
isDeleteModalOpen: false,
|
||||||
};
|
};
|
||||||
toggle = () => this.setState(({ isOpen }) => ({ isOpen: !isOpen }));
|
toggle = () => this.setState(({ isOpen }) => ({ isOpen: !isOpen }));
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { completeShortUrl, onCopyToClipboard, selectedServer, shortUrl } = this.props;
|
const { onCopyToClipboard, shortUrl, selectedServer: { id } } = this.props;
|
||||||
const serverId = selectedServer ? selectedServer.id : '';
|
const completeShortUrl = shortUrl && shortUrl.shortUrl ? shortUrl.shortUrl : '';
|
||||||
const toggleModal = (prop) => () => this.setState((prevState) => ({ [prop]: !prevState[prop] }));
|
const toggleModal = (prop) => () => this.setState((prevState) => ({ [prop]: !prevState[prop] }));
|
||||||
const toggleQrCode = toggleModal('isQrModalOpen');
|
const toggleQrCode = toggleModal('isQrModalOpen');
|
||||||
const togglePreview = toggleModal('isPreviewOpen');
|
const togglePreview = toggleModal('isPreviewModalOpen');
|
||||||
const toggleTags = toggleModal('isTagsModalOpen');
|
const toggleTags = toggleModal('isTagsModalOpen');
|
||||||
const toggleDelete = toggleModal('isDeleteModalOpen');
|
const toggleDelete = toggleModal('isDeleteModalOpen');
|
||||||
|
|
||||||
|
@ -50,7 +49,7 @@ const ShortUrlsRowMenu = (DeleteShortUrlModal, EditTagsModal) => class ShortUrls
|
||||||
<FontAwesomeIcon icon={menuIcon} />
|
<FontAwesomeIcon icon={menuIcon} />
|
||||||
</DropdownToggle>
|
</DropdownToggle>
|
||||||
<DropdownMenu right>
|
<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} /> Visit stats
|
<FontAwesomeIcon icon={pieChartIcon} /> Visit stats
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
|
||||||
|
@ -67,31 +66,19 @@ const ShortUrlsRowMenu = (DeleteShortUrlModal, EditTagsModal) => class ShortUrls
|
||||||
<DropdownItem className="short-urls-row-menu__dropdown-item--danger" onClick={toggleDelete}>
|
<DropdownItem className="short-urls-row-menu__dropdown-item--danger" onClick={toggleDelete}>
|
||||||
<FontAwesomeIcon icon={deleteIcon} /> Delete short URL
|
<FontAwesomeIcon icon={deleteIcon} /> Delete short URL
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DeleteShortUrlModal
|
<DeleteShortUrlModal shortUrl={shortUrl} isOpen={this.state.isDeleteModalOpen} toggle={toggleDelete} />
|
||||||
shortUrl={shortUrl}
|
|
||||||
isOpen={this.state.isDeleteModalOpen}
|
|
||||||
toggle={toggleDelete}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DropdownItem divider />
|
<DropdownItem divider />
|
||||||
|
|
||||||
<DropdownItem onClick={togglePreview}>
|
<DropdownItem onClick={togglePreview}>
|
||||||
<FontAwesomeIcon icon={pictureIcon} /> Preview
|
<FontAwesomeIcon icon={pictureIcon} /> Preview
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<PreviewModal
|
<PreviewModal url={completeShortUrl} isOpen={this.state.isPreviewModalOpen} toggle={togglePreview} />
|
||||||
url={completeShortUrl}
|
|
||||||
isOpen={this.state.isPreviewOpen}
|
|
||||||
toggle={togglePreview}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DropdownItem onClick={toggleQrCode}>
|
<DropdownItem onClick={toggleQrCode}>
|
||||||
<FontAwesomeIcon icon={qrIcon} /> QR code
|
<FontAwesomeIcon icon={qrIcon} /> QR code
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<QrCodeModal
|
<QrCodeModal url={completeShortUrl} isOpen={this.state.isQrModalOpen} toggle={toggleQrCode} />
|
||||||
url={completeShortUrl}
|
|
||||||
isOpen={this.state.isQrModalOpen}
|
|
||||||
toggle={toggleQrCode}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DropdownItem divider />
|
<DropdownItem divider />
|
||||||
|
|
||||||
|
|
|
@ -10,9 +10,10 @@ export const LIST_SHORT_URLS = 'shlink/shortUrlsList/LIST_SHORT_URLS';
|
||||||
/* eslint-enable padding-line-between-statements, newline-after-var */
|
/* eslint-enable padding-line-between-statements, newline-after-var */
|
||||||
|
|
||||||
export const shortUrlType = PropTypes.shape({
|
export const shortUrlType = PropTypes.shape({
|
||||||
tags: PropTypes.arrayOf(PropTypes.string),
|
|
||||||
shortCode: PropTypes.string,
|
shortCode: PropTypes.string,
|
||||||
originalUrl: PropTypes.string,
|
shortUrl: PropTypes.string,
|
||||||
|
longUrl: PropTypes.string,
|
||||||
|
tags: PropTypes.arrayOf(PropTypes.string),
|
||||||
});
|
});
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
|
|
|
@ -13,13 +13,12 @@ export default class DeleteTagConfirmModal extends React.Component {
|
||||||
tagDeleted: PropTypes.func,
|
tagDeleted: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
doDelete = () => {
|
doDelete = async () => {
|
||||||
const { tag, toggle, deleteTag } = this.props;
|
const { tag, toggle, deleteTag } = this.props;
|
||||||
|
|
||||||
return deleteTag(tag).then(() => {
|
await deleteTag(tag);
|
||||||
this.tagWasDeleted = true;
|
this.tagWasDeleted = true;
|
||||||
toggle();
|
toggle();
|
||||||
});
|
|
||||||
};
|
};
|
||||||
handleOnClosed = () => {
|
handleOnClosed = () => {
|
||||||
if (!this.tagWasDeleted) {
|
if (!this.tagWasDeleted) {
|
||||||
|
|
|
@ -34,13 +34,7 @@ export function VisitsHeader({ shortUrlDetail, shortUrlVisits }) {
|
||||||
Visit stats for <ExternalLink href={shortLink} />
|
Visit stats for <ExternalLink href={shortLink} />
|
||||||
</h2>
|
</h2>
|
||||||
<hr />
|
<hr />
|
||||||
{shortUrl.dateCreated && (
|
<div>Created: {renderDate()}</div>
|
||||||
<div>
|
|
||||||
Created:
|
|
||||||
|
|
||||||
{renderDate()}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div>
|
<div>
|
||||||
Long URL:
|
Long URL:
|
||||||
|
|
||||||
|
|
29
test/common/ScrollToTop.test.js
Normal file
29
test/common/ScrollToTop.test.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -13,6 +13,7 @@ describe('serverReducer', () => {
|
||||||
abc123: { id: 'abc123' },
|
abc123: { id: 'abc123' },
|
||||||
def456: { id: 'def456' },
|
def456: { id: 'def456' },
|
||||||
};
|
};
|
||||||
|
const expectedFetchServersResult = { type: FETCH_SERVERS, servers };
|
||||||
const ServersServiceMock = {
|
const ServersServiceMock = {
|
||||||
listServers: sinon.fake.returns(servers),
|
listServers: sinon.fake.returns(servers),
|
||||||
createServer: sinon.fake(),
|
createServer: sinon.fake(),
|
||||||
|
@ -40,38 +41,38 @@ describe('serverReducer', () => {
|
||||||
it('fetches servers and returns them as part of the action', () => {
|
it('fetches servers and returns them as part of the action', () => {
|
||||||
const result = listServers(ServersServiceMock)();
|
const result = listServers(ServersServiceMock)();
|
||||||
|
|
||||||
expect(result).toEqual({ type: FETCH_SERVERS, servers });
|
expect(result).toEqual(expectedFetchServersResult);
|
||||||
expect(ServersServiceMock.listServers.callCount).toEqual(1);
|
expect(ServersServiceMock.listServers.calledOnce).toEqual(true);
|
||||||
expect(ServersServiceMock.createServer.callCount).toEqual(0);
|
expect(ServersServiceMock.createServer.called).toEqual(false);
|
||||||
expect(ServersServiceMock.deleteServer.callCount).toEqual(0);
|
expect(ServersServiceMock.deleteServer.called).toEqual(false);
|
||||||
expect(ServersServiceMock.createServers.callCount).toEqual(0);
|
expect(ServersServiceMock.createServers.called).toEqual(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createServer', () => {
|
describe('createServer', () => {
|
||||||
it('adds new server and then fetches servers again', () => {
|
it('adds new server and then fetches servers again', () => {
|
||||||
const serverToCreate = { id: 'abc123' };
|
const serverToCreate = { id: 'abc123' };
|
||||||
const result = createServer(ServersServiceMock)(serverToCreate);
|
const result = createServer(ServersServiceMock, () => expectedFetchServersResult)(serverToCreate);
|
||||||
|
|
||||||
expect(result).toEqual({ type: FETCH_SERVERS, servers });
|
expect(result).toEqual(expectedFetchServersResult);
|
||||||
expect(ServersServiceMock.listServers.callCount).toEqual(1);
|
expect(ServersServiceMock.createServer.calledOnce).toEqual(true);
|
||||||
expect(ServersServiceMock.createServer.callCount).toEqual(1);
|
|
||||||
expect(ServersServiceMock.createServer.firstCall.calledWith(serverToCreate)).toEqual(true);
|
expect(ServersServiceMock.createServer.firstCall.calledWith(serverToCreate)).toEqual(true);
|
||||||
expect(ServersServiceMock.deleteServer.callCount).toEqual(0);
|
expect(ServersServiceMock.listServers.called).toEqual(false);
|
||||||
expect(ServersServiceMock.createServers.callCount).toEqual(0);
|
expect(ServersServiceMock.deleteServer.called).toEqual(false);
|
||||||
|
expect(ServersServiceMock.createServers.called).toEqual(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('deleteServer', () => {
|
describe('deleteServer', () => {
|
||||||
it('deletes a server and then fetches servers again', () => {
|
it('deletes a server and then fetches servers again', () => {
|
||||||
const serverToDelete = { id: 'abc123' };
|
const serverToDelete = { id: 'abc123' };
|
||||||
const result = deleteServer(ServersServiceMock)(serverToDelete);
|
const result = deleteServer(ServersServiceMock, () => expectedFetchServersResult)(serverToDelete);
|
||||||
|
|
||||||
expect(result).toEqual({ type: FETCH_SERVERS, servers });
|
expect(result).toEqual(expectedFetchServersResult);
|
||||||
expect(ServersServiceMock.listServers.callCount).toEqual(1);
|
expect(ServersServiceMock.listServers.called).toEqual(false);
|
||||||
expect(ServersServiceMock.createServer.callCount).toEqual(0);
|
expect(ServersServiceMock.createServer.called).toEqual(false);
|
||||||
expect(ServersServiceMock.createServers.callCount).toEqual(0);
|
expect(ServersServiceMock.createServers.called).toEqual(false);
|
||||||
expect(ServersServiceMock.deleteServer.callCount).toEqual(1);
|
expect(ServersServiceMock.deleteServer.calledOnce).toEqual(true);
|
||||||
expect(ServersServiceMock.deleteServer.firstCall.calledWith(serverToDelete)).toEqual(true);
|
expect(ServersServiceMock.deleteServer.firstCall.calledWith(serverToDelete)).toEqual(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -79,14 +80,14 @@ describe('serverReducer', () => {
|
||||||
describe('createServer', () => {
|
describe('createServer', () => {
|
||||||
it('creates multiple servers and then fetches servers again', () => {
|
it('creates multiple servers and then fetches servers again', () => {
|
||||||
const serversToCreate = values(servers);
|
const serversToCreate = values(servers);
|
||||||
const result = createServers(ServersServiceMock)(serversToCreate);
|
const result = createServers(ServersServiceMock, () => expectedFetchServersResult)(serversToCreate);
|
||||||
|
|
||||||
expect(result).toEqual({ type: FETCH_SERVERS, servers });
|
expect(result).toEqual(expectedFetchServersResult);
|
||||||
expect(ServersServiceMock.listServers.callCount).toEqual(1);
|
expect(ServersServiceMock.listServers.called).toEqual(false);
|
||||||
expect(ServersServiceMock.createServer.callCount).toEqual(0);
|
expect(ServersServiceMock.createServer.called).toEqual(false);
|
||||||
expect(ServersServiceMock.createServers.callCount).toEqual(1);
|
expect(ServersServiceMock.createServers.calledOnce).toEqual(true);
|
||||||
expect(ServersServiceMock.createServers.firstCall.calledWith(serversToCreate)).toEqual(true);
|
expect(ServersServiceMock.createServers.firstCall.calledWith(serversToCreate)).toEqual(true);
|
||||||
expect(ServersServiceMock.deleteServer.callCount).toEqual(0);
|
expect(ServersServiceMock.deleteServer.called).toEqual(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,7 +15,7 @@ describe('<CreateShortUrl />', () => {
|
||||||
const createShortUrl = sinon.spy();
|
const createShortUrl = sinon.spy();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const CreateShortUrl = createShortUrlsCreator(TagsSelector);
|
const CreateShortUrl = createShortUrlsCreator(TagsSelector, () => '');
|
||||||
|
|
||||||
wrapper = shallow(
|
wrapper = shallow(
|
||||||
<CreateShortUrl shortUrlCreationResult={shortUrlCreationResult} createShortUrl={createShortUrl} />
|
<CreateShortUrl shortUrlCreationResult={shortUrlCreationResult} createShortUrl={createShortUrl} />
|
||||||
|
|
108
test/short-urls/helpers/ShortUrlsRow.test.js
Normal file
108
test/short-urls/helpers/ShortUrlsRow.test.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
79
test/short-urls/helpers/ShortUrlsRowMenu.test.js
Normal file
79
test/short-urls/helpers/ShortUrlsRowMenu.test.js
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
87
test/tags/helpers/DeleteTagConfirmModal.test.js
Normal file
87
test/tags/helpers/DeleteTagConfirmModal.test.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
78
test/visits/SortableBarGraph.test.js
Normal file
78
test/visits/SortableBarGraph.test.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -8,6 +8,7 @@ describe('<VisitsHeader />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
const shortUrlDetail = {
|
const shortUrlDetail = {
|
||||||
shortUrl: {
|
shortUrl: {
|
||||||
|
shortUrl: 'https://doma.in/abc123',
|
||||||
longUrl: 'https://foo.bar/bar/foo',
|
longUrl: 'https://foo.bar/bar/foo',
|
||||||
dateCreated: '2018-01-01T10:00:00+01:00',
|
dateCreated: '2018-01-01T10:00:00+01:00',
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue