diff --git a/CHANGELOG.md b/CHANGELOG.md index 68e10e2d..b00c4c57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/common/ScrollToTop.js b/src/common/ScrollToTop.js index 19a34dfb..28e776b6 100644 --- a/src/common/ScrollToTop.js +++ b/src/common/ScrollToTop.js @@ -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); } } diff --git a/src/container/index.js b/src/container/index.js index ffe5de5d..3450796a 100644 --- a/src/container/index.js +++ b/src/container/index.js @@ -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( diff --git a/src/servers/helpers/ImportServersBtn.js b/src/servers/helpers/ImportServersBtn.js index 29fb88d8..4d687976 100644 --- a/src/servers/helpers/ImportServersBtn.js +++ b/src/servers/helpers/ImportServersBtn.js @@ -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 ( diff --git a/src/servers/reducers/server.js b/src/servers/reducers/server.js index 503b303e..44837d15 100644 --- a/src/servers/reducers/server.js +++ b/src/servers/reducers/server.js @@ -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(); }; diff --git a/src/servers/services/provideServices.js b/src/servers/services/provideServices.js index ff18b1b5..7821e168 100644 --- a/src/servers/services/provideServices.js +++ b/src/servers/services/provideServices.js @@ -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); diff --git a/src/short-urls/ShortUrlsList.js b/src/short-urls/ShortUrlsList.js index c037ebaf..660e559a 100644 --- a/src/short-urls/ShortUrlsList.js +++ b/src/short-urls/ShortUrlsList.js @@ -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 - {this.renderOrderIcon('originalUrl')} + {this.renderOrderIcon('longUrl')} Long URL Tags diff --git a/src/short-urls/helpers/ShortUrlsRow.js b/src/short-urls/helpers/ShortUrlsRow.js index 80bc60ef..a22f9ba2 100644 --- a/src/short-urls/helpers/ShortUrlsRow.js +++ b/src/short-urls/helpers/ShortUrlsRow.js @@ -43,7 +43,6 @@ const ShortUrlsRow = ( render() { const { shortUrl, selectedServer } = this.props; - const completeShortUrl = !selectedServer ? shortUrl.shortCode : `${selectedServer.url}/${shortUrl.shortCode}`; return ( @@ -51,10 +50,10 @@ const ShortUrlsRow = ( {shortUrl.dateCreated} - {completeShortUrl} + - {shortUrl.originalUrl} + {this.renderTags(shortUrl.tags)} {shortUrl.visitsCount} @@ -66,7 +65,6 @@ const ShortUrlsRow = ( Copied short URL! stateFlagTimeout(this.setState.bind(this), 'copiedToClipboard')} diff --git a/src/short-urls/helpers/ShortUrlsRowMenu.js b/src/short-urls/helpers/ShortUrlsRowMenu.js index e52419e4..dc5a3e50 100644 --- a/src/short-urls/helpers/ShortUrlsRowMenu.js +++ b/src/short-urls/helpers/ShortUrlsRowMenu.js @@ -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    - +  Visit stats @@ -67,31 +66,19 @@ const ShortUrlsRowMenu = (DeleteShortUrlModal, EditTagsModal) => class ShortUrls  Delete short URL - +  Preview - +  QR code - + diff --git a/src/short-urls/reducers/shortUrlsList.js b/src/short-urls/reducers/shortUrlsList.js index ad972cea..fa4d4fe5 100644 --- a/src/short-urls/reducers/shortUrlsList.js +++ b/src/short-urls/reducers/shortUrlsList.js @@ -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 = { diff --git a/src/tags/helpers/DeleteTagConfirmModal.js b/src/tags/helpers/DeleteTagConfirmModal.js index 4c5dadf8..460db160 100644 --- a/src/tags/helpers/DeleteTagConfirmModal.js +++ b/src/tags/helpers/DeleteTagConfirmModal.js @@ -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) { diff --git a/src/visits/VisitsHeader.js b/src/visits/VisitsHeader.js index 6213ced2..3103c2a4 100644 --- a/src/visits/VisitsHeader.js +++ b/src/visits/VisitsHeader.js @@ -34,13 +34,7 @@ export function VisitsHeader({ shortUrlDetail, shortUrlVisits }) { Visit stats for
- {shortUrl.dateCreated && ( -
- Created: -   - {renderDate()} -
- )} +
Created: {renderDate()}
Long URL:   diff --git a/test/common/ScrollToTop.test.js b/test/common/ScrollToTop.test.js new file mode 100644 index 00000000..77d428e5 --- /dev/null +++ b/test/common/ScrollToTop.test.js @@ -0,0 +1,29 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import * as sinon from 'sinon'; +import createScrollToTop from '../../src/common/ScrollToTop'; + +describe('', () => { + let wrapper; + const window = { + scrollTo: sinon.spy(), + }; + + beforeEach(() => { + const ScrollToTop = createScrollToTop(window); + + wrapper = shallow(Foobar); + }); + + 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); + }); +}); diff --git a/test/servers/reducers/server.test.js b/test/servers/reducers/server.test.js index 9e87236b..99cf30b7 100644 --- a/test/servers/reducers/server.test.js +++ b/test/servers/reducers/server.test.js @@ -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); }); }); }); diff --git a/test/short-urls/CreateShortUrl.test.js b/test/short-urls/CreateShortUrl.test.js index 0443369d..507e0f85 100644 --- a/test/short-urls/CreateShortUrl.test.js +++ b/test/short-urls/CreateShortUrl.test.js @@ -15,7 +15,7 @@ describe('', () => { const createShortUrl = sinon.spy(); beforeEach(() => { - const CreateShortUrl = createShortUrlsCreator(TagsSelector); + const CreateShortUrl = createShortUrlsCreator(TagsSelector, () => ''); wrapper = shallow( diff --git a/test/short-urls/helpers/ShortUrlsRow.test.js b/test/short-urls/helpers/ShortUrlsRow.test.js new file mode 100644 index 00000000..d8978788 --- /dev/null +++ b/test/short-urls/helpers/ShortUrlsRow.test.js @@ -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('', () => { + 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( + + ); + }); + 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'); + }); + + 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); + }); +}); diff --git a/test/short-urls/helpers/ShortUrlsRowMenu.test.js b/test/short-urls/helpers/ShortUrlsRowMenu.test.js new file mode 100644 index 00000000..6e73da0f --- /dev/null +++ b/test/short-urls/helpers/ShortUrlsRowMenu.test.js @@ -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('', () => { + 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( + + ); + }); + + 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(); + }); + }); +}); diff --git a/test/tags/helpers/DeleteTagConfirmModal.test.js b/test/tags/helpers/DeleteTagConfirmModal.test.js new file mode 100644 index 00000000..3c1edc88 --- /dev/null +++ b/test/tags/helpers/DeleteTagConfirmModal.test.js @@ -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('', () => { + let wrapper; + const tag = 'nodejs'; + const deleteTag = sinon.spy(); + const tagDeleted = sinon.spy(); + const createWrapper = (tagDelete) => { + wrapper = shallow( + ''} + 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 ${tag}?`); + 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); + }); +}); diff --git a/test/visits/SortableBarGraph.test.js b/test/visits/SortableBarGraph.test.js new file mode 100644 index 00000000..3dbcc337 --- /dev/null +++ b/test/visits/SortableBarGraph.test.js @@ -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('', () => { + let wrapper; + const sortingItems = { + name: 'Name', + amount: 'Amount', + }; + const stats = { + Foo: 100, + Bar: 50, + }; + const createWrapper = (extraHeaderContent = []) => { + wrapper = shallow( + + ); + + 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([ + () => Foo, + () => Bar, + ]); + + expect(wrapper.find('.foo-span')).toHaveLength(1); + expect(wrapper.find('.bar-span')).toHaveLength(1); + }); +}); diff --git a/test/visits/VisitsHeader.test.js b/test/visits/VisitsHeader.test.js index 5416d3cb..797f6a29 100644 --- a/test/visits/VisitsHeader.test.js +++ b/test/visits/VisitsHeader.test.js @@ -8,6 +8,7 @@ describe('', () => { let wrapper; const shortUrlDetail = { shortUrl: { + shortUrl: 'https://doma.in/abc123', longUrl: 'https://foo.bar/bar/foo', dateCreated: '2018-01-01T10:00:00+01:00', },