summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authoranderklander <anderklander@gmail.com>2018-01-20 10:01:26 +0100
committeranderklander <anderklander@gmail.com>2018-01-20 10:03:04 +0100
commit13273ed34e3a237c4d71d7dd4a3be2f9f5c196f4 (patch)
treeb717edd56c10d3187eec0a7c86c4d0bd1890b9d3
parenta11ed1b5c6490bfd5a8d855ab278dba6c9832f38 (diff)
downloadmullvadvpn-13273ed34e3a237c4d71d7dd4a3be2f9f5c196f4.tar.xz
mullvadvpn-13273ed34e3a237c4d71d7dd4a3be2f9f5c196f4.zip
Support component in reactxp
-rw-r--r--app/assets/css/style.css1
-rw-r--r--app/components/Support.css169
-rw-r--r--app/components/Support.js224
-rw-r--r--app/components/SupportStyles.js211
-rw-r--r--test/components/Support.spec.js71
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')();
+}