diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2018-07-05 13:49:10 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2018-07-05 17:13:43 +0200 |
| commit | 7b5bc3ce5f182f83437aae244dd9772a8d51f072 (patch) | |
| tree | 76fed1c3d9f608e0ae6821b8eeccb112062ba1ee | |
| parent | 6403d3b357a247c7601edf40bcd4e06f5d85addf (diff) | |
| download | mullvadvpn-7b5bc3ce5f182f83437aae244dd9772a8d51f072.tar.xz mullvadvpn-7b5bc3ce5f182f83437aae244dd9772a8d51f072.zip | |
Add initial form data to Support form, save form data when failed to submit the problem report
| -rw-r--r-- | app/components/Support.js | 136 | ||||
| -rw-r--r-- | app/containers/SupportPage.js | 21 | ||||
| -rw-r--r-- | test/components/Support.spec.js | 152 |
3 files changed, 171 insertions, 138 deletions
diff --git a/app/components/Support.js b/app/components/Support.js index 7897f594ec..1f03d67375 100644 --- a/app/components/Support.js +++ b/app/components/Support.js @@ -6,14 +6,8 @@ import { Layout, Container } from './Layout'; import styles from './SupportStyles'; import Img from './Img'; -import type { AccountReduxState } from '../redux/account/reducers'; - -export type SupportReport = { - email: string, - message: string, - savedReport: ?string, -}; - +import type { AccountToken } from '../lib/daemon-rpc'; +import type { SupportReportForm } from '../redux/support/actions'; type SupportState = { email: string, message: string, @@ -22,11 +16,15 @@ type SupportState = { }; export type SupportProps = { - account: AccountReduxState, + defaultEmail: string, + defaultMessage: string, + accountHistory: Array<AccountToken>, onClose: () => void, - onViewLog: (string) => void, - onCollectLog: (Array<string>) => Promise<string>, - onSend: (email: string, message: string, savedReport: string) => void, + viewLog: (path: string) => void, + saveReportForm: (form: SupportReportForm) => void, + clearReportForm: () => void, + collectProblemReport: (accountsToRedact: Array<string>) => Promise<string>, + sendProblemReport: (email: string, message: string, savedReport: string) => Promise<void>, }; export default class Support extends Component<SupportProps, SupportState> { @@ -37,68 +35,102 @@ export default class Support extends Component<SupportProps, SupportState> { sendState: 'INITIAL', }; + _collectLogPromise: ?Promise<string>; + + constructor(props: SupportProps) { + super(props); + + // seed initial data from props + this.state.email = props.defaultEmail; + this.state.message = props.defaultMessage; + } + validate() { return this.state.message.trim().length > 0; } onChangeEmail = (email: string) => { - this.setState({ email: email }); + this.setState({ email: email }, () => { + this._saveFormData(); + }); }; onChangeDescription = (description: string) => { - this.setState({ message: description }); + this.setState({ message: description }, () => { + this._saveFormData(); + }); }; - onViewLog = () => { - this._getLog().then((path) => { - this.props.onViewLog(path); - }); + onViewLog = async (): Promise<void> => { + try { + const reportPath = await this._collectLog(); + this.props.viewLog(reportPath); + } catch (error) { + // TODO: handle error + } }; - _getLog(): Promise<string> { - const accountsToRedact = this.props.account.accountHistory; - const { savedReport } = this.state; - return savedReport - ? Promise.resolve(savedReport) - : this.props.onCollectLog(accountsToRedact).then((path) => { - return new Promise((resolve) => - this.setState({ savedReport: path }, () => resolve(path)), - ); + _saveFormData() { + this.props.saveReportForm({ + email: this.state.email, + message: this.state.message, + }); + } + + async _collectLog(): Promise<string> { + if (this._collectLogPromise) { + return this._collectLogPromise; + } else { + const collectPromise = this.props.collectProblemReport(this.props.accountHistory); + + // save promise to prevent subsequent requests + this._collectLogPromise = collectPromise; + + try { + const reportPath = await collectPromise; + return new Promise((resolve) => { + this.setState({ savedReport: reportPath }, () => resolve(reportPath)); }); + } catch (error) { + this._collectLogPromise = null; + + throw error; + } + } } - onSend = () => { + onSend = async (): Promise<void> => { if (this.state.sendState === 'INITIAL' && this.state.email.length === 0) { - this.setState({ - sendState: 'CONFIRM_NO_EMAIL', + return new Promise((resolve) => { + this.setState({ sendState: 'CONFIRM_NO_EMAIL' }, () => resolve()); }); } else { - this._sendProblemReport(); + try { + await this._sendReport(); + } catch (error) { + // No-op + } } }; - _sendProblemReport() { - this.setState( - { - sendState: 'LOADING', - }, - () => { - this._getLog() - .then((path) => { - return this.props.onSend(this.state.email, this.state.message, path); - }) - .then(() => { - this.setState({ - sendState: 'SUCCESS', - }); - }) - .catch(() => { - this.setState({ - sendState: 'FAILED', - }); + _sendReport(): Promise<void> { + return new Promise((resolve, reject) => { + this.setState({ sendState: 'LOADING' }, async () => { + try { + const { email, message } = this.state; + const reportPath = await this._collectLog(); + await this.props.sendProblemReport(email, message, reportPath); + this.props.clearReportForm(); + this.setState({ sendState: 'SUCCESS' }, () => { + resolve(); }); - }, - ); + } catch (error) { + this.setState({ sendState: 'FAILED' }, () => { + reject(error); + }); + } + }); + }); } render() { diff --git a/app/containers/SupportPage.js b/app/containers/SupportPage.js index d9ce8ad6ec..c80e9508de 100644 --- a/app/containers/SupportPage.js +++ b/app/containers/SupportPage.js @@ -8,26 +8,25 @@ import { collectProblemReport, sendProblemReport } from '../lib/problem-report'; import type { ReduxState, ReduxDispatch } from '../redux/store'; import type { SharedRouteProps } from '../routes'; +import supportActions from '../redux/support/actions'; const mapStateToProps = (state: ReduxState) => ({ - account: state.account, + defaultEmail: state.support.email, + defaultMessage: state.support.message, + accountHistory: state.account.accountHistory, }); const mapDispatchToProps = (dispatch: ReduxDispatch, _props: SharedRouteProps) => { + const { saveReportForm, clearReportForm } = bindActionCreators(supportActions, dispatch); const { push: pushHistory } = bindActionCreators({ push }, dispatch); return { onClose: () => pushHistory('/settings'), - - onCollectLog: (toRedact) => { - return collectProblemReport(toRedact); - }, - - onViewLog: (path) => openItem(path), - - onSend: (email, message, savedReport) => { - return sendProblemReport(email, message, savedReport); - }, + viewLog: (path) => openItem(path), + saveReportForm, + clearReportForm, + collectProblemReport, + sendProblemReport, }; }; diff --git a/test/components/Support.spec.js b/test/components/Support.spec.js index ce1efb2b75..59ddc25606 100644 --- a/test/components/Support.spec.js +++ b/test/components/Support.spec.js @@ -6,113 +6,115 @@ import { shallow } from 'enzyme'; import type { SupportProps } from '../../app/components/Support'; describe('components/Support', () => { - const makeProps = (mergeProps: $Shape<SupportProps> = {}): SupportProps => { - const defaultProps: SupportProps = { - account: { - accountToken: null, - accountHistory: [], - error: null, - expiry: null, - status: 'none', - }, - onClose: () => {}, - onViewLog: (_path) => {}, - onCollectLog: () => Promise.resolve('/tmp/mullvad_problem_report.log'), - onSend: (_report) => {}, - }; - return Object.assign({}, defaultProps, mergeProps); - }; + it('should call close callback', () => { + const props = makeProps({ onClose: spy() }); + const component = shallow(<Support {...props} />); + const closeButton = component.find({ testName: 'support__close' }); - it('should call close callback', (done) => { - const props = makeProps({ - onClose: () => done(), - }); - const component = getComponent(render(props), 'support__close'); - click(component); + click(closeButton); + expect(props.onClose).to.have.been.called.once; }); - it('should call view logs callback', (done) => { - const props = makeProps({ - onViewLog: (_path) => done(), - }); - const component = getComponent(render(props), 'support__view_logs'); - click(component); + it('should call view logs callback', async () => { + const props = makeProps({ viewLog: spy() }); + const component = shallow(<Support {...props} />); + const viewButton = component.find({ testName: 'support__view_logs' }); + + await click(viewButton); + expect(props.viewLog).to.have.been.called.once; }); - it('should call send callback when description filled in', (done) => { + it('should call send callback when description filled in', async () => { const props = makeProps({ - onSend: (_report) => done(), + defaultEmail: 'foo', + defaultMessage: 'abc', + sendProblemReport: spy((_report) => Promise.resolve()), }); + const component = shallow(<Support {...props} />); + const sendButton = component.find({ testName: 'support__send_logs' }); - const component = render(props); - component.setState({ message: 'abc', email: 'foo' }); - - const sendButton = getComponent(component, 'support__send_logs'); expect(sendButton.prop('disabled')).to.be.false; - click(sendButton); + await click(sendButton); + expect(props.sendProblemReport).to.have.been.called.once; }); it('should not call send callback when description is empty', () => { - const component = render(makeProps()); - component.setState({ message: '' }); + const props = makeProps({ defaultMessage: '' }); + const component = shallow(<Support {...props} />); + const sendButton = component.find({ testName: 'support__send_logs' }); - const sendButton = getComponent(render(makeProps()), 'support__send_logs'); expect(sendButton.prop('disabled')).to.be.true; }); - it('should not collect report twice', (done) => { - const collectCallback = spy(() => Promise.resolve('non-falsy')); + it('should not collect report twice', async () => { const props = makeProps({ - onCollectLog: collectCallback, + collectProblemReport: spy(() => Promise.resolve('/path/to/problem/report')), }); + const component = shallow(<Support {...props} />); + const viewButton = component.find({ testName: 'support__view_logs' }); - const viewLogButton = getComponent(render(props), 'support__view_logs'); - click(viewLogButton); + await Promise.all([click(viewButton), click(viewButton)]); + expect(props.collectProblemReport).to.have.been.called.once; + }); - setTimeout(() => { - click(viewLogButton); + it('should collect report on submission', async () => { + const props = makeProps({ + defaultMessage: '', + defaultEmail: 'foo', + collectProblemReport: spy(() => Promise.resolve('/path/to/problem/report')), + sendProblemReport: spy(() => Promise.resolve()), }); + const component = shallow(<Support {...props} />); + const sendButton = component.find({ testName: 'support__send_logs' }); - setTimeout(() => { - try { - expect(collectCallback).to.have.been.called.once; - done(); - } catch (e) { - done(e); - } - }); + await click(sendButton); + expect(props.collectProblemReport).to.have.been.called.once; + expect(props.sendProblemReport).to.have.been.called.once; }); - it('should collect report on submission', (done) => { - const collectCallback = spy(() => Promise.resolve('')); + it('should save the report form on change', () => { const props = makeProps({ - onCollectLog: collectCallback, - onSend: (_report) => { - try { - expect(collectCallback).to.have.been.called.once; - done(); - } catch (e) { - done(e); - } - }, + defaultEmail: 'email@domain', + defaultMessage: 'test message', + sendProblemReport: () => Promise.reject(new Error('Simulation')), + saveReportForm: spy(), }); + const component = shallow(<Support {...props} />); + const input = component.find({ testName: 'support__form_message' }); + input.simulate('changeText', 'new message'); + expect(props.saveReportForm).to.have.been.called.once; + }); - const component = render(props); - component.setState({ message: '', email: 'foo' }); + it('should clear the report form upon successful submission', async () => { + const props = makeProps({ + defaultEmail: 'email@domain', + defaultMessage: 'test message', + sendProblemReport: () => Promise.resolve(), + clearReportForm: spy(), + }); + const component = shallow(<Support {...props} />); + const sendButton = component.find({ testName: 'support__send_logs' }); - const sendButton = getComponent(component, 'support__send_logs'); - click(sendButton); + await click(sendButton); + expect(props.clearReportForm).to.have.been.called.once; }); }); -function render(props) { - return shallow(<Support {...props} />); -} - -function getComponent(container, testName) { - return container.findWhere((n) => n.prop('testName') === testName); +function makeProps(mergeProps: $Shape<SupportProps> = {}): SupportProps { + const defaultProps: SupportProps = { + defaultEmail: '', + defaultMessage: '', + accountHistory: [], + onClose: () => {}, + viewLog: (_path) => {}, + collectProblemReport: () => Promise.resolve('/path/to/problem/report'), + sendProblemReport: (_report) => Promise.resolve(), + saveReportForm: (_form) => {}, + clearReportForm: () => {}, + }; + return { ...defaultProps, ...mergeProps }; } function click(component) { - component.prop('onPress')(); + return component.prop('onPress')(); } |
