From 0aed9f32116a5338ff15295169541d6dd7e09491 Mon Sep 17 00:00:00 2001 From: mrkvon Date: Sun, 18 Nov 2018 03:40:55 +0100 Subject: [PATCH] Improve UX and workflow, write tests --- modules/references/client/components/Info.js | 33 +++ .../client/components/Navigation.js | 4 +- .../components/ReferencesNew.component.js | 45 +++- .../client/components/references.api.js | 9 +- .../Navigation.client.component.tests.js | 9 - .../ReferencesNew.client.component.tests.js | 192 ++++++++++++++++++ .../client/config/users.client.routes.js | 2 +- 7 files changed, 279 insertions(+), 15 deletions(-) create mode 100644 modules/references/client/components/Info.js diff --git a/modules/references/client/components/Info.js b/modules/references/client/components/Info.js new file mode 100644 index 0000000000..37a5e23277 --- /dev/null +++ b/modules/references/client/components/Info.js @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +/** + * @TODO make these elements nicer + */ + +export function Self() { + return (
Self
); +} + +export function Loading() { + return (
Loading
); +} + +export function Duplicate() { + return (
Duplicate
); +} + +export function Submitted({ isReported, isPublic }) { + return ( +
+
Submitted
+ {(isReported) ?
Reported
: null} +
{(isPublic) ? '' : 'not '}public
+
+ ); +} + +Submitted.propTypes = { + isReported: PropTypes.boolean, + isPublic: PropTypes.boolean +}; diff --git a/modules/references/client/components/Navigation.js b/modules/references/client/components/Navigation.js index 09936cbc55..061a81b1d2 100644 --- a/modules/references/client/components/Navigation.js +++ b/modules/references/client/components/Navigation.js @@ -34,7 +34,7 @@ export default function Navigation(props) { className="btn btn-action btn-primary" aria-label="Submit reference" onClick={props.onSubmit} - disabled={props.tabDone < props.tabs - 1}> + disabled={props.tabDone < props.tabs - 1 || props.disabled}> Submit ); @@ -46,7 +46,6 @@ export default function Navigation(props) { {/* */} {(props.tab === props.tabs - 1) ? submit : null} - ); } @@ -54,6 +53,7 @@ Navigation.propTypes = { onBack: PropTypes.func, onNext: PropTypes.func, onSubmit: PropTypes.func, + disabled: PropTypes.boolean, tab: PropTypes.number, // current tab index - indexed from 0 tabs: PropTypes.number, // amount of tabs to display tabDone: PropTypes.number // which tab is already filled diff --git a/modules/references/client/components/ReferencesNew.component.js b/modules/references/client/components/ReferencesNew.component.js index 4b9044981b..f096ba8178 100644 --- a/modules/references/client/components/ReferencesNew.component.js +++ b/modules/references/client/components/ReferencesNew.component.js @@ -4,6 +4,7 @@ import * as references from './references.api'; import Navigation from './Navigation'; import Interaction from './Interaction'; import Recommend from './Recommend'; +import { Self, Loading, Duplicate, Submitted } from './Info'; const api = { references }; @@ -22,10 +23,26 @@ export default class ReferencesNew extends React.Component { recommend: null }, report: false, - reportMessage: '' + reportMessage: '', + isSelf: props.userFrom._id === props.userTo._id, + isLoading: true, + isSubmitting: false, + isDuplicate: false, + isSubmitted: false, + isPublic: false }; } + async componentDidMount() { + const reference = await api.references.read({ userFrom: this.props.userFrom._id, userTo: this.props.userTo._id }); + + const newState = { isLoading: false }; + + if (reference.length === 1) newState.isDuplicate = true; + + this.setState(newState); + } + handleTabSwitch(move) { this.setState({ tab: this.state.tab + move @@ -74,11 +91,21 @@ export default class ReferencesNew extends React.Component { reportMessage: this.state.reportMessage }; - await api.references.create({ ...data.reference, userTo: this.props.userTo._id }); + this.setState({ + isSubmitting: true + }); + + const savedReference = await api.references.create({ ...data.reference, userTo: this.props.userTo._id }); if (data.reference.recommend === 'no' && data.report) { await api.references.report(this.props.userTo, data.reportMessage); } + + this.setState({ + isSubmitting: false, + isSubmitted: true, + isPublic: savedReference.public + }); } render() { @@ -100,6 +127,18 @@ export default class ReferencesNew extends React.Component { const tabDone = (recommend) ? 1 : (hostedMe || hostedThem || met) ? 0 : -1; + if (this.state.isSelf) return ; + + if (this.state.isLoading) return ; + + if (this.state.isDuplicate) return ; + + if (this.state.isSubmitted) { + const isReported = this.state.reference.recommend === 'no' && this.state.report; + const isPublic = this.state.isPublic; + return ; + } + return (
@@ -109,6 +148,7 @@ export default class ReferencesNew extends React.Component { tab={this.state.tab} tabDone={tabDone} tabs={tabs.length} + disabled={this.state.isSubmitting} onBack={() => this.handleTabSwitch(-1)} onNext={() => this.handleTabSwitch(+1)} onSubmit={() => this.handleSubmit()} @@ -119,5 +159,6 @@ export default class ReferencesNew extends React.Component { } ReferencesNew.propTypes = { + userFrom: PropTypes.object, userTo: PropTypes.object }; diff --git a/modules/references/client/components/references.api.js b/modules/references/client/components/references.api.js index 7330ad4389..84d7fb0499 100644 --- a/modules/references/client/components/references.api.js +++ b/modules/references/client/components/references.api.js @@ -1,11 +1,18 @@ export async function create(reference) { - await fetch('/api/references', { + const response = await fetch('/api/references', { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8' }, body: JSON.stringify(reference) // eslint-disable-line angular/json-functions }); + + return await response.json(); +} + +export async function read({ userFrom, userTo }) { + const response = await fetch(`/api/references?userFrom=${userFrom}&userTo=${userTo}`); + return await response.json(); } export async function report(user, message) { diff --git a/modules/references/tests/client/Navigation.client.component.tests.js b/modules/references/tests/client/Navigation.client.component.tests.js index 05e57b9b9e..597b608718 100644 --- a/modules/references/tests/client/Navigation.client.component.tests.js +++ b/modules/references/tests/client/Navigation.client.component.tests.js @@ -22,15 +22,6 @@ Enzyme.configure({ adapter: new Adapter() }); }); describe('Navigation through 3 tabs', () => { - xit('test', () => { - const wrapper = shallow( {}} />); - console.log(wrapper); - console.log(); - console.log(wrapper.find('button'), wrapper.exists(), wrapper.name()); - expect(wrapper.find('button')).toBeDefined(); - expect(wrapper.props().tab).toBe(0); - }); - it('when tab is 0, there is no Back button and there is Next button', () => { const wrapper = shallow(); diff --git a/modules/references/tests/client/ReferencesNew.client.component.tests.js b/modules/references/tests/client/ReferencesNew.client.component.tests.js index e69de29bb2..d2524ebbbf 100644 --- a/modules/references/tests/client/ReferencesNew.client.component.tests.js +++ b/modules/references/tests/client/ReferencesNew.client.component.tests.js @@ -0,0 +1,192 @@ +'use strict'; + +import ReferencesNew from '../../client/components/ReferencesNew.component'; +import { Self, Loading, Duplicate, Submitted } from '../../client/components/Info'; +import Interaction from '../../client/components/Interaction'; +import Navigation from '../../client/components/Navigation'; +import Enzyme from 'enzyme'; +import { shallow } from 'enzyme'; +import React from 'react'; +import sinon from 'sinon'; +import Adapter from 'enzyme-adapter-react-16'; +import jasmineEnzyme from 'jasmine-enzyme'; +import * as api from '../../client/components/references.api'; + +Enzyme.configure({ adapter: new Adapter() }); + +/** + * This is a first React test suite with enzyme. + * The enzyme configuration can be moved elsewhere (before all tests ever). + */ + +(function () { + + describe('', () => { + beforeEach(() => { + jasmineEnzyme(); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should not be possible to leave a reference to self', () => { + const me = { + _id: '123456', + username: 'username' + }; + + const wrapper = shallow(); + expect(wrapper.find(Self)).toExist(); + }); + + it('check whether the reference exists at the beginning', () => { + const userFrom = { _id: '111111', username: 'userfrom' }; + const userTo = { _id: '222222', username: 'userto' }; + const stub = sinon.stub(api, 'read'); + stub.withArgs({ userFrom: userFrom._id, userTo: userTo._id }).returns(new Promise(() => {})); + + expect(stub.callCount).toBe(0); + const wrapper = shallow(); + expect(stub.callCount).toBe(1); + expect(wrapper.find(Loading)).toExist(); + }); + + it('can not leave a second reference', async () => { + const userFrom = { _id: '111111', username: 'userfrom' }; + const userTo = { _id: '222222', username: 'userto' }; + const stub = sinon.stub(api, 'read'); + stub.withArgs({ userFrom: userFrom._id, userTo: userTo._id }).resolves([{ + userFrom, userTo, public: false + }]); + + const wrapper = shallow(); + + expect(wrapper.find(Duplicate)).not.toExist(); + await null; + expect(wrapper.find(Duplicate)).toExist(); + expect(wrapper.find(Interaction)).not.toExist(); + }); + + it('can leave a reference (reference form is available)', async () => { + const userFrom = { _id: '111111', username: 'userfrom' }; + const userTo = { _id: '222222', username: 'userto' }; + const stub = sinon.stub(api, 'read'); + stub.withArgs({ userFrom: userFrom._id, userTo: userTo._id }).resolves([]); + + const wrapper = shallow(); + expect(wrapper.find(Interaction)).not.toExist(); + await null; + expect(wrapper.find(Interaction)).toExist(); + }); + + it('submit a reference', async () => { + const userFrom = { _id: '111111', username: 'userfrom' }; + const userTo = { _id: '222222', username: 'userto' }; + const spyCreate = sinon.spy(api, 'create'); + const stubRead = sinon.stub(api, 'read'); + stubRead.withArgs({ userFrom: userFrom._id, userTo: userTo._id }).resolves([]); + + const wrapper = shallow(); + + await null; + + expect(spyCreate.callCount).toBe(0); + wrapper.setState({ + reference: { + interactions: { + met: false, + hostedMe: true, + hostedThem: false + }, + recommend: 'yes' + } + }); + + const nav = wrapper.find(Navigation); + nav.props().onSubmit(); + + expect(spyCreate.callCount).toBe(1); + + expect(spyCreate.calledOnceWith({ + interactions: { + met: false, + hostedMe: true, + hostedThem: false + }, + recommend: 'yes', + userTo: userTo._id + })).toBe(true); + }); + + it('submit a report when recommend is no and user wants to send a report', async () => { + const userFrom = { _id: '111111', username: 'userfrom' }; + const userTo = { _id: '222222', username: 'userto' }; + const spyReport = sinon.spy(api, 'report'); + const stubRead = sinon.stub(api, 'read'); + const stubCreate = sinon.stub(api, 'create'); + stubRead.withArgs({ userFrom: userFrom._id, userTo: userTo._id }).resolves([]); + stubCreate.resolves(); + + const wrapper = shallow(); + + await null; + + expect(spyReport.callCount).toBe(0); + wrapper.setState({ + reference: { + interactions: { + met: false, + hostedMe: true, + hostedThem: false + }, + recommend: 'unknown' + }, + report: true, + reportMessage: 'asdf' + }); + + const nav = wrapper.find(Navigation); + + nav.props().onSubmit(); + + await null; + + expect(spyReport.callCount).toBe(0); + + wrapper.setState({ + reference: { + interactions: { + met: true, + hostedMe: true, + hostedThem: true + }, + recommend: 'no' + } + }); + + nav.props().onSubmit(); + + await null; + + expect(spyReport.callCount).toBe(1); + expect(spyReport.calledOnceWith(userTo, 'asdf')).toBe(true); + }); + + it('give the information that the reference was submitted', async () => { + const userFrom = { _id: '111111', username: 'userfrom' }; + const userTo = { _id: '222222', username: 'userto' }; + + const wrapper = shallow(); + + wrapper.setState({ + isLoading: false, + isSubmitted: true + }); + + wrapper.update(); + + expect(wrapper.find(Submitted)).toExist(); + }); + }); +}()); diff --git a/modules/users/client/config/users.client.routes.js b/modules/users/client/config/users.client.routes.js index 7d32a7eb45..9c6cd3e870 100644 --- a/modules/users/client/config/users.client.routes.js +++ b/modules/users/client/config/users.client.routes.js @@ -246,7 +246,7 @@ }). state('profile.references.new', { url: '/new', - template: '', + template: '', requiresAuth: true, noScrollingTop: true, data: {