diff options
| author | anderklander <anderklander@gmail.com> | 2018-01-20 10:01:26 +0100 |
|---|---|---|
| committer | anderklander <anderklander@gmail.com> | 2018-01-20 10:03:04 +0100 |
| commit | 13273ed34e3a237c4d71d7dd4a3be2f9f5c196f4 (patch) | |
| tree | b717edd56c10d3187eec0a7c86c4d0bd1890b9d3 | |
| parent | a11ed1b5c6490bfd5a8d855ab278dba6c9832f38 (diff) | |
| download | mullvadvpn-13273ed34e3a237c4d71d7dd4a3be2f9f5c196f4.tar.xz mullvadvpn-13273ed34e3a237c4d71d7dd4a3be2f9f5c196f4.zip | |
Support component in reactxp
| -rw-r--r-- | app/assets/css/style.css | 1 | ||||
| -rw-r--r-- | app/components/Support.css | 169 | ||||
| -rw-r--r-- | app/components/Support.js | 224 | ||||
| -rw-r--r-- | app/components/SupportStyles.js | 211 | ||||
| -rw-r--r-- | test/components/Support.spec.js | 71 |
5 files changed, 356 insertions, 320 deletions
diff --git a/app/assets/css/style.css b/app/assets/css/style.css index ac33356ce0..1cfed7d652 100644 --- a/app/assets/css/style.css +++ b/app/assets/css/style.css @@ -12,7 +12,6 @@ @import '../../components/Connect.css'; @import '../../components/AdvancedSettings.css'; @import '../../components/Account.css'; -@import '../../components/Support.css'; @import '../../components/SelectLocation.css'; @import '../../components/Layout.css'; @import '../../components/Switch.css'; diff --git a/app/components/Support.css b/app/components/Support.css deleted file mode 100644 index 8e8198742c..0000000000 --- a/app/components/Support.css +++ /dev/null @@ -1,169 +0,0 @@ -.support { - background: #192E45; - height: 100%; -} - -.support__container { - display: flex; - flex-direction: column; - height: 100%; -} - -.support__header { - flex: 0 0 auto; - padding: 40px 24px 24px; - position: relative; /* anchor for close button */ -} - -.support__close { - position: absolute; - display: flex; - align-items: center; - border: 0; - padding: 0; - margin: 0; - top: 24px; - left: 12px; - z-index: 1; /* part of .support__container covers the button */ -} - -.support__close-icon { - opacity: 0.6; - margin-right: 8px; -} - -.support__close-title { - font-family: "Open Sans"; - font-size: 13px; - font-weight: 600; - color: rgba(255, 255, 255, 0.6); -} - -.support__title { - font-family: DINPro; - font-size: 32px; - font-weight: 900; - line-height: 40px; - color: #FFFFFF; - margin-bottom: 16px; -} - -.support__subtitle { - font-family: "Open Sans"; - font-size: 13px; - font-weight: 600; - line-height: normal; - color: rgba(255,255,255,0.8); -} - -.support__content { - flex: 1 1 auto; - display: flex; - flex-direction: column; - justify-content: space-between; -} - -.support__form { - display: flex; - flex: 1 1 auto; - flex-direction: column; -} - -.support__form-row { - padding: 0 24px; -} - -.support__form-row + .support__form-row { - margin-top: 8px; -} - -.support__form-row-message { - display: flex; - flex: 1 1 auto; -} - -.support__form-email { - width: 100%; - border-radius: 4px; - border: 0; - overflow: hidden; - padding: 10px 12px 12px 12px; - font-family: "Open Sans"; - font-size: 13px; - font-weight: 600; - line-height: 26px; - color: #294D73; - background-color: #fff; -} - -.support__form-email::-webkit-input-placeholder { - color: rgba(41,77,115,0.4); -} - -.support__form-message-scroll-wrap { - width: 100%; - display: flex; - border-radius: 4px; - overflow: hidden; -} - -.support__form-message { - width: 100%; - border: 0; - overflow-y: scroll; - resize: none; - padding: 10px 12px 12px 12px; - font-family: "Open Sans"; - font-size: 13px; - font-weight: 600; - line-height: 1.4em; - color: #294D73; - background-color: #fff; -} - -.support__form-message::-webkit-input-placeholder { - color: rgba(41,77,115,0.4); -} - -.support__footer { - padding: 16px 24px 24px; -} - -.support__footer .button + .button { - margin-top: 16px; -} - -.support__sent-email { - display: inline; - font-weight: 900; - color: white; -} - -.support__status-security--secure { - font-family: "Open Sans"; - font-size: 16px; - font-weight: 800; - line-height: 22px; - margin-bottom: 4px; - color: #44AD4D; - text-transform: uppercase; -} - -.support__send-status { - font-family: DINPro; - font-size: 38px; - font-weight: 900; - line-height: 1.16em; - max-height: calc(1.16em * 2); - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - letter-spacing: -0.9px; - color: #FFFFFF; - margin-bottom: 4px; -} - -.support__status-icon { - text-align: center; - margin-bottom: 32px; -} diff --git a/app/components/Support.js b/app/components/Support.js index 04e6c7392d..a87e6eeee9 100644 --- a/app/components/Support.js +++ b/app/components/Support.js @@ -1,7 +1,9 @@ // @flow -import React, { Component } from 'react'; +import React from 'react'; +import { Component, Text, Button, View, TextInput } from 'reactxp'; import { Layout, Container, Header } from './Layout'; -import ExternalLinkSVG from '../assets/images/icon-extLink.svg'; +import styles from './SupportStyles'; +import Img from './Img'; import type { AccountReduxState } from '../redux/account/reducers'; @@ -38,20 +40,12 @@ export default class Support extends Component { return this.state.message.trim().length > 0; } - onChangeEmail = (e: Event) => { - const input = e.target; - if(!(input instanceof HTMLInputElement)) { - throw new Error('input must be an instance of HTMLInputElement'); - } - this.setState({ email: input.value }); + onChangeEmail = (email: string) => { + this.setState({ email: email }); } - onChangeDescription = (e: Event) => { - const input = e.target; - if(!(input instanceof HTMLTextAreaElement)) { - throw new Error('input must be an instance of HTMLTextAreaElement'); - } - this.setState({ message: input.value }); + onChangeDescription = (description: string) => { + this.setState({ message: description }); } onViewLog = () => { @@ -100,14 +94,13 @@ export default class Support extends Component { render() { - const header = <div className="support__header"> - <h2 className="support__title">Report a problem</h2> - { this.state.sendState === 'INITIAL' && <div className="support__subtitle"> - { `To help you more effectively, your app's log file will be attached to this message. - Your data will remain secure and private, as it is encrypted & anonymised before sending.` } - </div> + const header = <View style={styles.support__header}> + <Text style={styles.support__title}>Report a problem</Text> + { this.state.sendState === 'INITIAL' && <Text style={styles.support__subtitle}> + { 'To help you more effectively, your app\'s log file will be attached to this message. Your data will remain secure and private, as it is encrypted & anonymised before sending.' } + </Text> } - </div>; + </View>; const content = this._renderContent(); @@ -115,19 +108,19 @@ export default class Support extends Component { <Layout> <Header hidden={ true } style={ 'defaultDark' } /> <Container> - <div className="support"> - <div className="support__close" onClick={ this.props.onClose }> - <img className="support__close-icon" src="./assets/images/icon-back.svg" /> - <span className="support__close-title">Settings</span> - </div> - <div className="support__container"> + <View style={styles.support}> + <Button style={styles.support__close} onPress={ this.props.onClose } testName="support__close"> + <Img style={styles.support__close_icon} source="icon-back" /> + <Text style={styles.support__close_title}>Settings</Text> + </Button> + <View style={styles.support__container}> { header } { content } - </div> - </div> + </View> + </View> </Container> </Layout> ); @@ -149,106 +142,109 @@ export default class Support extends Component { } _renderForm() { - return <div className="support__content"> - <div className="support__form"> - <div className="support__form-row"> - <input className="support__form-email" - type="email" + return <View style={styles.support__content}> + <View style={styles.support__form}> + <View style={styles.support__form_row}> + <TextInput style={styles.support__form_email} placeholder="Your email" - value={ this.state.email } - onChange={ this.onChangeEmail } + defaultValue={ this.state.email } + onChangeText={ this.onChangeEmail } + keyboardType="email-address" autoFocus={ true } /> - </div> - <div className="support__form-row support__form-row-message"> - <div className="support__form-message-scroll-wrap"> - <textarea className="support__form-message" + </View> + <View style={styles.support__form_row_message}> + <View style={styles.support__form_message_scroll_wrap}> + <TextInput style={styles.support__form_message} placeholder="Describe your problem" - value={ this.state.message } - onChange={ this.onChangeDescription } /> - </div> - </div> - <div className="support__footer"> - <button type="button" - className="support__form-view-logs button button--primary" - onClick={ this.onViewLog }> - <span className="button-label">View app logs</span> - <ExternalLinkSVG className="button-icon button-icon--16" /> - </button> - <button type="button" - className="support__form-send button button--positive" - disabled={ !this.validate() } - onClick={ this.onSend }>Send</button> - </div> - </div> - </div>; + defaultValue={ this.state.message } + multiline={ true } + onChangeText={ this.onChangeDescription } + testName="support__form_message"/> + </View> + </View> + <View style={styles.support__footer}> + <Button onPress={ this.onViewLog } style={{'flex':1}} testName='support__view_logs'> + <View style={styles.support__form_view_logs}> + <View style={styles.support__open_icon}></View> + <Text style={styles.support__button_label}>View app logs</Text> + <Img source="icon-extLink" style={styles.support__open_icon} tintColor='currentColor'/> + </View> + </Button> + <Button style={styles.support__form_send} disabled={ !this.validate() } onPress={ this.onSend } testName='support__send_logs'> + <Text style={styles.support__button_label}>Send</Text> + </Button> + </View> + </View> + </View>; } _renderLoading() { - return <div className="support__content"> - - <div className="support__form"> - <div className="support__form-row"> - <div className="support__status-icon"> - <img src="./assets/images/icon-spinner.svg" alt="" /> - </div> - <div className="support__status-security--secure"> + return <View style={styles.support__content}> + <View style={styles.support__form}> + <View style={styles.support__form_row}> + <View style={styles.support__status_icon}> + <Img source="icon-spinner" alt="" /> + </View> + <View style={styles.support__status_security__secure}> Secure Connection - </div> - <div className="support__send-status"> - <span>Sending...</span> - </div> - </div> - </div> - </div>; + </View> + <Text style={styles.support__send_status}> + Sending... + </Text> + </View> + </View> + </View>; } _renderSent() { - return <div className="support__content"> - <div className="support__form"> - <div className="support__form-row"> - <div className="support__status-icon"> - <img src="./assets/images/icon-success.svg" alt="" /> - </div> - <div className="support__status-security--secure"> + return <View style={styles.support__content}> + <View style={styles.support__form}> + <View style={styles.support__form_row}> + <View style={styles.support__status_icon}> + <Img source="icon-success" alt="" /> + </View> + <Text style={styles.support__status_security__secure}> Secure Connection - </div> - <div className="support__send-status"> - <span>Sent</span> - </div> - <div className="support__subtitle"> + </Text> + <Text style={styles.support__send_status}> + Sent + </Text> + <Text style={styles.support__subtitle}> Thanks! We will look into this. If needed we will contact you on {'\u00A0'} - <div className="support__sent-email">{ this.state.email }</div> - </div> - </div> - </div> - </div>; + <Text style={styles.support__sent_email}>{ this.state.email }</Text> + </Text> + </View> + </View> + </View>; } _renderFailed() { - return <div className="support__content"> - <div className="support__form"> - <div className="support__form-row"> - <div className="support__status-icon"> - <img src="./assets/images/icon-fail.svg" alt="" /> - </div> - <div className="support__status-security--secure"> + return <View style={styles.support__content}> + <View style={styles.support__form}> + <View style={styles.support__form_row}> + <View style={styles.support__status_icon}> + <Img source="icon-fail" alt="" /> + </View> + <Text style={styles.support__status_security__secure}> Secure Connection - </div> - <div className="support__send-status"> - <span>Failed to send</span> - </div> - </div> - </div> - <div className="support__footer"> - <button type="button" - className="support__form-view-logs button button--primary" - onClick={ () => this.setState({ sendState: 'INITIAL' }) }> - <span className="button-label">Edit message</span> - </button> - <button type="button" - className="support__form-send button button--positive" - onClick={ this.onSend }>Try again</button> - </div> - </div>; + </Text> + <Text style={styles.support__send_status}> + Failed to send + </Text> + </View> + </View> + <View style={styles.support__footer}> + <Button onPress={ () => this.setState({ sendState: 'INITIAL' }) }> + <View style={styles.support__form_edit_logs}> + <Text style={styles.support__button_label}>Edit message</Text> + </View> + </Button> + <Button onPress={ this.onSend }> + <View style={styles.support__form_send}> + <Text style={styles.support__button_label}>Try again</Text> + </View> + </Button> + </View> + </View>; } } diff --git a/app/components/SupportStyles.js b/app/components/SupportStyles.js new file mode 100644 index 0000000000..e23f26bd6d --- /dev/null +++ b/app/components/SupportStyles.js @@ -0,0 +1,211 @@ +import { createViewStyles, createTextStyles } from '../lib/styles'; + +export default Object.assign(createViewStyles({ + support:{ + backgroundColor: '#192E45', + height: '100%', + }, + support__container:{ + display: 'flex', + flexDirection: 'column', + height: '100%', + }, + support__header:{ + flex: 0, + paddingTop: 12, + paddingBottom: 12, + paddingLeft: 24, + paddingRight: 24, + overflow: 'visible', + position: 'relative' /* anchor for close button */ + }, + support__close:{ + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'flex-start', + marginLeft: 12, + }, + support__close_icon:{ + width: 24, + height: 24, + flex: 0, + opacity: 0.6, + marginRight: 8, + }, + support__content:{ + flex: 1, + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', + paddingBottom: 24, + }, + support__form:{ + display: 'flex', + flex: 1, + flexDirection: 'column', + }, + support__form_row:{ + paddingTop: 0, + paddingBottom: 0, + paddingLeft: 24, + paddingRight: 24, + }, + support__form_row_message:{ + flex: 1, + paddingTop: 0, + paddingBottom: 0, + paddingLeft: 24, + paddingRight: 24, + marginTop: 8, + }, + support__form_message_scroll_wrap:{ + flex: 1, + display: 'flex', + borderRadius: 4, + overflow: 'hidden', + }, + support__footer:{ + paddingTop: 1, + paddingRight: 24, + paddingLeft: 24, + paddingBottom: 24, + marginTop: 12, + display: 'flex', + flexDirection: 'column', + flex: 0, + }, + support__form_view_logs:{ + backgroundColor: 'rgba(41,71,115,1)', + color: 'rgba(255,255,255,0.8)', + paddingTop: 7, + paddingLeft: 12, + paddingRight: 12, + paddingBottom: 9, + borderRadius: 4, + justifyContent: 'space-between', + alignItems: 'center', + flexDirection: 'row', + flex: 1, + }, + support__form_edit_logs:{ + backgroundColor: 'rgba(41,71,115,1)', + color: 'rgba(255,255,255,0.8)', + paddingTop: 7, + paddingLeft: 12, + paddingRight: 12, + paddingBottom: 9, + borderRadius: 4, + justifyContent: 'center', + alignItems: 'center', + flexDirection: 'row', + flex: 1, + }, + support__form_send:{ + backgroundColor: 'rgba(63,173,77,1)', + color: 'rgba(255,255,255,0.8)', + paddingTop: 7, + paddingLeft: 12, + paddingRight: 12, + paddingBottom: 9, + borderRadius: 4, + marginTop: 16, + justifyContent: 'space-between', + alignItems: 'center', + flex: 1, + }, + support__status_icon:{ + textAlign: 'center', + marginBottom: 32, + }, + support__open_icon:{ + color: 'rgba(255,255,255,0.8)', + marginLeft: 8, + width: 16, + height: 16, + flexGrow: 0, + flexShrink: 0, + flexBasis: 'auto', + alignItems: 'flex-end', + }, +}), createTextStyles({ + support__close_title:{ + fontFamily: 'Open Sans', + fontSize: 13, + fontWeight: '600', + color: 'rgba(255, 255, 255, 0.6)', + }, + support__title:{ + fontFamily: 'DINPro', + fontSize: 32, + fontWeight: '900', + lineHeight: 40, + color: '#FFFFFF', + marginBottom: 16, + }, + support__subtitle:{ + fontFamily: 'Open Sans', + fontSize: 13, + fontWeight: '600', + overflow: 'visible', + color: 'rgba(255,255,255,0.8)', + lineHeight: 20, + letterSpacing: -0.2, + }, + support__form_email:{ + width: '100%', + borderRadius: 4, + overflow: 'hidden', + paddingTop: 10, + paddingLeft: 12, + paddingRight: 12, + paddingBottom: 12, + fontFamily: 'Open Sans', + fontSize: 13, + fontWeight: '600', + lineHeight: 26, + color: '#294D73', + backgroundColor: '#fff', + }, + support__form_message:{ + paddingTop: 10, + paddingLeft: 12, + paddingRight: 12, + paddingBottom: 10, + fontFamily: 'Open Sans', + fontSize: 13, + fontWeight: '600', + color: '#294D73', + backgroundColor: '#fff', + flex: 1, + }, + support__sent_email:{ + fontWeight: '900', + color: 'white', + }, + support__status_security__secure:{ + fontFamily: 'Open Sans', + fontSize: 16, + fontWeight: '800', + lineHeight: 22, + marginBottom: 4, + color: '#44AD4D', + }, + support__send_status:{ + fontFamily: 'DINPro', + fontSize: 38, + fontWeight: '900', + maxHeight: 'calc(1.16em * 2)', + overflow: 'visible', + letterSpacing: -0.9, + color: '#FFFFFF', + marginBottom: 4, + }, + support__button_label:{ + fontFamily: 'DINPro', + fontSize: 20, + fontWeight: '900', + lineHeight: 26, + justifyContent: 'center', + alignItems: 'center', + }, +}));
\ No newline at end of file diff --git a/test/components/Support.spec.js b/test/components/Support.spec.js index 3251b08464..90b0c0d8fa 100644 --- a/test/components/Support.spec.js +++ b/test/components/Support.spec.js @@ -1,10 +1,11 @@ // @flow import { expect } from 'chai'; -import sinon from 'sinon'; import React from 'react'; -import ReactTestUtils, { Simulate } from 'react-dom/test-utils'; import Support from '../../app/components/Support'; +import { shallow } from 'enzyme'; +require('../setup/enzyme'); +import sinon from 'sinon'; import type { SupportProps } from '../../app/components/Support'; @@ -27,26 +28,20 @@ describe('components/Support', () => { return Object.assign({}, defaultProps, mergeProps); }; - const render = (props: SupportProps): Support => { - return ReactTestUtils.renderIntoDocument( - <Support { ...props } /> - ); - }; - it('should call close callback', (done) => { const props = makeProps({ onClose: () => done() }); - const domNode = ReactTestUtils.findRenderedDOMComponentWithClass(render(props), 'support__close'); - Simulate.click(domNode); + const component = getComponent(render(props), 'support__close'); + click(component); }); it('should call view logs callback', (done) => { const props = makeProps({ onViewLog: (_path) => done() }); - const domNode = ReactTestUtils.findRenderedDOMComponentWithClass(render(props), 'support__form-view-logs'); - Simulate.click(domNode); + const component = getComponent(render(props), 'support__view_logs'); + click(component); }); it('should call send callback when description filled in', (done) => { @@ -55,25 +50,19 @@ describe('components/Support', () => { }); const component = render(props); + component.setState({ message: 'abc' }); - const descriptionField = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'support__form-message'); - descriptionField.value = 'Lorem Ipsum'; - Simulate.change(descriptionField); - - const sendButton = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'support__form-send'); - expect(sendButton.disabled).to.be.false; - Simulate.click(sendButton); + const sendButton = getComponent(component, 'support__send_logs'); + expect(sendButton.prop('disabled')).to.be.false; + click(sendButton); }); it('should not call send callback when description is empty', () => { const component = render(makeProps()); + component.setState({ message: '' }); - const descriptionField = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'support__form-message'); - descriptionField.value = ''; - Simulate.change(descriptionField); - - const sendButton = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'support__form-send'); - expect(sendButton.disabled).to.be.true; + const sendButton = getComponent(render(makeProps()), 'support__send_logs'); + expect(sendButton.prop('disabled')).to.be.true; }); it('should not collect report twice', (done) => { @@ -82,12 +71,11 @@ describe('components/Support', () => { onCollectLog: collectCallback }); - const component = render(props); - const viewLogButton = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'support__form-view-logs'); - Simulate.click(viewLogButton); + const viewLogButton = getComponent(render(props), 'support__view_logs'); + click(viewLogButton); setTimeout(() => { - Simulate.click(viewLogButton); + click(viewLogButton); }); setTimeout(() => { @@ -114,14 +102,25 @@ describe('components/Support', () => { } }); - const component = render(props); - - const descriptionField = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'support__form-message'); - descriptionField.value = 'Lorem Ipsum'; - Simulate.change(descriptionField); + const component = render(makeProps()); + component.setState({ message: '' }); - const sendButton = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'support__form-send'); - Simulate.click(sendButton); + const sendButton = getComponent(render(props), 'support__send_logs'); + click(sendButton); }); }); + +function render(props) { + return shallow( + <Support {...props} /> + ); +} + +function getComponent(container, testName) { + return container.findWhere( n => n.prop('testName') === testName); +} + +function click(component) { + component.prop('onPress')(); +} |
