summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorErik Larkö <erik@mullvad.net>2017-12-27 10:44:40 +0100
committerErik Larkö <erik@mullvad.net>2017-12-27 10:44:40 +0100
commit40ef5effa822a2681117fdf105f091866a3351cb (patch)
tree0b216fb938d93c1713c0c6887a1ebcf296891cec
parent44b813ea88ba30f35e58df196945f76ddf34000b (diff)
parent185fb6f3cfc081b31d0e3c78d6e338854a61e299 (diff)
downloadmullvadvpn-40ef5effa822a2681117fdf105f091866a3351cb.tar.xz
mullvadvpn-40ef5effa822a2681117fdf105f091866a3351cb.zip
Merge branch 'reactxp-settings'
-rw-r--r--app/assets/css/style.css1
-rw-r--r--app/components/CustomScrollbars.css3
-rw-r--r--app/components/Settings.css125
-rw-r--r--app/components/Settings.js157
-rw-r--r--app/components/SettingsStyles.js121
-rw-r--r--app/lib/styles.js17
-rw-r--r--test/components/Settings.spec.js78
7 files changed, 267 insertions, 235 deletions
diff --git a/app/assets/css/style.css b/app/assets/css/style.css
index eb4353b67e..d3da3124a0 100644
--- a/app/assets/css/style.css
+++ b/app/assets/css/style.css
@@ -10,7 +10,6 @@
@import '../../components/CustomScrollbars.css';
@import '../../components/Login.css';
@import '../../components/Connect.css';
-@import '../../components/Settings.css';
@import '../../components/AdvancedSettings.css';
@import '../../components/Account.css';
@import '../../components/Support.css';
diff --git a/app/components/CustomScrollbars.css b/app/components/CustomScrollbars.css
index fb00d1b7cb..4604b1e413 100644
--- a/app/components/CustomScrollbars.css
+++ b/app/components/CustomScrollbars.css
@@ -20,10 +20,11 @@
transition: height 0.25s ease-in-out, opacity 0.25s ease-in-out;
pointer-events: none;
opacity: 0;
+ z-index: 99;
}
.custom-scrollbars__thumb--visible {
/* thumb appears without animation */
transition: height 0.25s ease-in-out;
opacity: 1;
-} \ No newline at end of file
+}
diff --git a/app/components/Settings.css b/app/components/Settings.css
deleted file mode 100644
index 9e49c814f9..0000000000
--- a/app/components/Settings.css
+++ /dev/null
@@ -1,125 +0,0 @@
-.settings {
- background: #192E45;
- height: 100%;
-}
-
-.settings__container {
- display: flex;
- flex-direction: column;
- height: 100%;
-}
-
-.settings__header {
- flex: 0 0 auto;
- padding: 40px 24px 24px;
- position: relative; /* anchor for close button */
-}
-
-.settings__content {
- flex: 1 1 auto;
- display: flex;
- flex-direction: column;
- justify-content: space-between;
-}
-
-.settings__close {
- position: absolute;
- display: block;
- border: 0;
- padding: 0;
- margin: 0;
- width: 24px;
- height: 24px;
- top: 24px;
- left: 12px;
- background-color: transparent;
- background-image: url(../assets/images/icon-close.svg);
- opacity: 0.6;
- z-index: 1; /* part of .settings__container covers the button */
-}
-
-.settings__title {
- font-family: DINPro;
- font-size: 32px;
- font-weight: 900;
- line-height: 40px;
- color: #FFFFFF;
-}
-
-.settings__cell {
- background-color:rgba(41,71,115,1);
- padding: 15px 24px;
- display: flex;
- flex-direction: row;
- align-items: center;
-}
-
-.settings__cell-disclosure {
- display: block;
- margin-left: 8px;
- color: rgba(255, 255, 255, 0.8);
-}
-
-.settings__cell-spacer {
- height: 24px;
-}
-
-.settings__cell--selected,
-.settings__cell--selected:hover {
- background-color: #44AD4D;
-}
-
-.settings__cell--active:hover {
- background-color: rgba(41,71,115,0.9);
-}
-
-.settings__cell + .settings__cell {
- margin-top: 1px;
-}
-
-.settings__cell-label {
- font-family: DINPro;
- font-size: 20px;
- font-weight: 900;
- line-height: 26px;
- color: #FFFFFF;
- flex: 1 0 auto;
-}
-
-.settings__cell-icon {
- width: 16px;
- flex: 0 0 auto;
- color: rgba(255, 255, 255, 0.8);
-}
-
-.settings__cell-value {
- flex: 0 0 auto;
-}
-
-.settings__account-paid-until-label {
- font-family: "Open Sans";
- font-size: 13px;
- font-weight: 800;
- line-height: 26px; /* matches .cell-label */
- color: rgba(255, 255, 255, 0.8);
- text-transform: uppercase;
-}
-
-.settings__account-paid-until-label--error {
- color: #d0021b;
-}
-
-.settings__cell-footer {
- padding: 8px 24px 24px;
- font-family: "Open Sans";
- font-size: 13px;
- font-weight: 600;
- line-height: 20px;
- color: rgba(255,255,255,0.8);
-}
-
-.settings__footer {
- padding: 24px;
-}
-
-.settings__footer .button + .button { margin-top: 16px; }
diff --git a/app/components/Settings.js b/app/components/Settings.js
index 25a3e6df47..effd366ba7 100644
--- a/app/components/Settings.js
+++ b/app/components/Settings.js
@@ -1,9 +1,10 @@
// @flow
import moment from 'moment';
-import React, { Component } from 'react';
-import { If, Then, Else } from 'react-if';
+import React from 'react';
+import { Component, Text, Button, View } from 'reactxp';
import { Layout, Container, Header } from './Layout';
import CustomScrollbars from './CustomScrollbars';
+import styles from './SettingsStyles';
import Img from './Img';
import type { AccountReduxState } from '../redux/account/reducers';
@@ -39,88 +40,92 @@ export default class Settings extends Component {
<Layout>
<Header hidden={ true } style={ 'defaultDark' } />
<Container>
- <div className="settings">
- <button className="settings__close" onClick={ this.props.onClose } />
- <div className="settings__container">
- <div className="settings__header">
- <h2 className="settings__title">Settings</h2>
- </div>
+ <View style={styles.settings}>
+ <Button style={styles.settings__close} onPress={ this.props.onClose } testName='settings__close'>
+ <Img style={styles.settings__close_icon} source='icon-close' tintColor='currentColor'/>
+ </Button>
+
+ <View style={styles.settings__container}>
+ <View style={styles.settings__header}>
+ <Text style={styles.settings__title}>Settings</Text>
+ </View>
+
<CustomScrollbars autoHide={ true }>
- <div className="settings__content">
- <div className="settings__main">
- { /* show account options when logged in */ }
- <If condition={ isLoggedIn }>
- <Then>
- <div className="settings__account">
+ <View style={styles.settings__content}>
+ <View style={styles.settings__main}>
+ {/* show account options when logged in */}
+ {isLoggedIn ? (
+ <View style={styles.settings_account} testName='settings__account'>
+ <Button onPress={ this.props.onViewAccount } testName='settings__view_account'>
+ <View style={styles.settings__cell}>
+ <Text style={styles.settings__cell_label}>Account</Text>
+ <View style={styles.settings__cell_value}>
+ {isOutOfTime ? (
+ <Text style={styles.settings__account_paid_until_label__error} testName='settings__account_paid_until_label'>OUT OF TIME</Text>
+ ) : (
+ <Text style={styles.settings__account_paid_until_label} testName='settings__account_paid_until_label'>{formattedExpiry}</Text>
+ )}
+ </View>
+ {/* HERE */}
+ <Img style={styles.settings__cell_disclosure} source='icon-chevron'/>
+ </View>
+ </Button>
+ <View style={styles.settings__cell_spacer} />
+ </View>
+ ) : null}
+
+ {isLoggedIn ? (
+ <Button onPress={ this.props.onViewAdvancedSettings }>
+ <View style={styles.settings__cell}>
+ <Text style={styles.settings__cell_label}>Advanced</Text>
+ <Img style={styles.settings__cell_disclosure} source='icon-chevron' tintColor='currentColor' />
+ </View>
+ <View style={styles.settings__cell_spacer}></View>
+ </Button>
+ ) : null}
- <div className="settings__view-account settings__cell settings__cell--active" onClick={ this.props.onViewAccount }>
- <div className="settings__cell-label">Account</div>
- <div className="settings__cell-value">
- <If condition={ isOutOfTime }>
- <Then>
- <span className="settings__account-paid-until-label settings__account-paid-until-label--error">OUT OF TIME</span>
- </Then>
- <Else>
- <span className="settings__account-paid-until-label">{ formattedExpiry }</span>
- </Else>
- </If>
- </div>
- <div className="settings__cell-disclosure">
- <Img source="icon-chevron" tintColor="currentColor" />
- </div>
- </div>
- <div className="settings__cell-spacer"></div>
- </div>
- </Then>
- </If>
+ {isLoggedIn ? (
+ <Button onPress={ this.props.onViewAdvancedSettings }>
+ <View style={styles.settings__cell}>
+ <Text style={styles.settings__cell_label}>Advanced</Text>
+ <Img style={styles.settings__cell_disclosure} source='icon-chevron' tintColor='currentColor'/>
+ </View>
+ <View style={styles.settings__cell_spacer}></View>
+ </Button>
+ ) : null}
- <If condition={ isLoggedIn }>
- <Then>
- <div className="settings__advanced">
- <div className="settings__cell settings__cell--active" onClick={ this.props.onViewAdvancedSettings }>
- <div className="settings__cell-label">Advanced</div>
- <div className="settings__cell-value">
- <div className="settings__cell-disclosure">
- <Img source="icon-chevron" tintColor="currentColor" />
- </div>
- </div>
- </div>
- <div className="settings__cell-spacer"></div>
- </div>
- </Then>
- </If>
+ <Button onPress={ this.props.onExternalLink.bind(this, 'faq') } testName='settings__external_link'>
+ <View style={styles.settings__cell}>
+ <Text style={styles.settings__cell_label}>FAQs</Text>
+ <Img style={styles.settings__cell_icon} source='icon-extLink' tintColor='currentColor'/>
+ </View>
+ </Button>
- <div className="settings__external">
- <div className="settings__cell settings__cell--active" onClick={ this.props.onExternalLink.bind(this, 'faq') }>
- <div className="settings__cell-label">FAQs</div>
- <div className="settings__cell-icon">
- <Img source="icon-extLink" tintColor="currentColor" />
- </div>
- </div>
- <div className="settings__cell settings__cell--active" onClick={ this.props.onExternalLink.bind(this, 'guides') }>
- <div className="settings__cell-label">Guides</div>
- <div className="settings__cell-icon">
- <Img source="icon-extLink" tintColor="currentColor" />
- </div>
- </div>
- <div className="settings__view-support settings__cell settings__cell--active" onClick={ this.props.onViewSupport }>
- <div className="settings__cell-label">Report a problem</div>
- <div className="settings__cell-disclosure">
- <Img source="icon-extLink" tintColor="currentColor" />
- </div>
- </div>
- </div>
- </div>
+ <Button onPress={ this.props.onExternalLink.bind(this, 'guides') } testName='settings__external_link'>
+ <View style={styles.settings__cell}>
+ <Text style={styles.settings__cell_label}>Guides</Text>
+ <Img style={styles.settings__cell_icon} source='icon-extLink' tintColor='currentColor'/>
+ </View>
+ </Button>
- <div className="settings__footer">
- <button className="settings__quit button button--negative" onClick={ this.props.onQuit }>Quit app</button>
- </div>
+ <Button onPress={ this.props.onViewSupport } testName='settings__view_support'>
+ <View style={styles.settings__cell}>
+ <Text style={styles.settings__cell_label}>Contact support</Text>
+ <Img style={styles.settings__cell_icon} source='icon-chevron' tintColor='currentColor'/>
+ </View>
+ </Button>
+ </View>
- </div>
+ <View style={styles.settings__footer}>
+ <Button style={styles.settings__footer_button} onPress={this.props.onQuit} testName='settings__quit'>
+ <Text style={styles.settings__footer_button_label}>Quit app</Text>
+ </Button>
+ </View>
+ </View>
</CustomScrollbars>
- </div>
- </div>
+ </View>
+ </View>
</Container>
</Layout>
);
diff --git a/app/components/SettingsStyles.js b/app/components/SettingsStyles.js
new file mode 100644
index 0000000000..d5daef85b5
--- /dev/null
+++ b/app/components/SettingsStyles.js
@@ -0,0 +1,121 @@
+import { createViewStyles, createTextStyles } from '../lib/styles';
+
+export default Object.assign(createViewStyles({
+ settings: {
+ backgroundColor: '#192E45',
+ height: '100%'
+ },
+ settings__container:{
+ flexDirection: 'column',
+ height: '100%'
+ },
+ settings__header:{
+ flex: 0,
+ paddingTop: 12,
+ paddingBottom: 12,
+ paddingLeft: 24,
+ paddingRight: 24,
+ position: 'relative' /* anchor for close button */
+ },
+ settings__content:{
+ flexDirection: 'column',
+ flex: 1,
+ },
+ settings__close:{
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'flex-start',
+ marginTop: 0,
+ marginLeft: 12,
+ },
+ settings__close_icon:{
+ width: 24,
+ height: 24,
+ flex: 0,
+ opacity: 0.6,
+ marginRight: 8,
+ },
+ settings__cell:{
+ backgroundColor: 'rgba(41,71,115,1)',
+ paddingTop: 15,
+ paddingBottom: 15,
+ paddingLeft: 24,
+ paddingRight: 24,
+ marginLeft: -6, //Because of button.css, when removed remove this
+ marginRight: -6, //Because of button.css, when removed remove this
+ marginTop: -1, //Because of button.css, when removed remove this
+ marginBottom: 0, //Because of button.css, when removed remove this
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between'
+ },
+ settings__cell_disclosure:{
+ marginLeft: 8,
+ color: 'rgba(255, 255, 255, 0.8)',
+ },
+ settings__cell_spacer:{
+ height: 24,
+ flex: 0
+ },
+ settings__cell__active_hover:{
+ backgroundColor: 'rgba(41,71,115,0.9)'
+ },
+ settings__cell_icon:{
+ width: 16,
+ height: 16,
+ flex: 0,
+ color: 'rgba(255, 255, 255, 0.8)',
+ },
+ settings__account_paid_until_label__error:{
+ color: '#d0021b'
+ },
+ settings__footer_button:{
+ backgroundColor: 'rgba(208,2,27,1)',
+ paddingTop: 7,
+ paddingLeft: 12,
+ paddingRight: 12,
+ paddingBottom: 9,
+ borderRadius: 4,
+ justifyContent: 'center',
+ alignItems: 'center',
+ width: '100%',
+ },
+ settings__footer:{
+ width: '100%',
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingTop: 24,
+ paddingLeft: 24,
+ paddingRight: 24,
+ paddingBottom: 24,
+ },
+}), createTextStyles({
+ settings__title:{
+ fontFamily: 'DINPro',
+ fontSize: 32,
+ fontWeight: '900',
+ lineHeight: 40,
+ color: '#FFFFFF'
+ },
+ settings__cell_label:{
+ fontFamily: 'DINPro',
+ fontSize: 20,
+ fontWeight: '900',
+ lineHeight: 26,
+ color: '#FFFFFF'
+ },
+ settings__footer_button_label:{
+ fontFamily: 'DINPro',
+ fontSize: 20,
+ fontWeight: '900',
+ lineHeight: 26,
+ color: 'rgba(255,255,255,0.8)'
+ },
+ settings__account_paid_until_label:{
+ fontFamily: 'Open Sans',
+ fontSize: 13,
+ fontWeight: '800',
+ color: 'rgba(255, 255, 255, 0.8)'
+ },
+}));
diff --git a/app/lib/styles.js b/app/lib/styles.js
new file mode 100644
index 0000000000..c75d73236b
--- /dev/null
+++ b/app/lib/styles.js
@@ -0,0 +1,17 @@
+import { Styles } from 'reactxp';
+
+export function createViewStyles(styles: { [string]: Object }) {
+ const viewStyles = {};
+ for (const style of Object.keys(styles)) {
+ viewStyles[style] = Styles.createViewStyle(styles[style]);
+ }
+ return viewStyles;
+}
+
+export function createTextStyles(styles: { [string]: Object }) {
+ const textStyles = {};
+ for (const style of Object.keys(styles)) {
+ textStyles[style] = Styles.createTextStyle(styles[style]);
+ }
+ return textStyles;
+} \ No newline at end of file
diff --git a/test/components/Settings.spec.js b/test/components/Settings.spec.js
index cc38c54616..7f949cb6c3 100644
--- a/test/components/Settings.spec.js
+++ b/test/components/Settings.spec.js
@@ -2,9 +2,11 @@
import { expect } from 'chai';
import React from 'react';
-import ReactTestUtils, { Simulate } from 'react-dom/test-utils';
import Settings from '../../app/components/Settings';
+import { shallow } from 'enzyme';
+require('../setup/enzyme');
+
import type { AccountReduxState } from '../../app/redux/account/reducers';
import type { SettingsReduxState } from '../../app/redux/settings/reducers';
import type { SettingsProps } from '../../app/components/Settings';
@@ -59,91 +61,90 @@ describe('components/Settings', () => {
return Object.assign({}, defaultProps, mergeProps);
};
- const render = (props: SettingsProps): Settings => {
- return ReactTestUtils.renderIntoDocument(
- <Settings { ...props } />
- );
- };
-
it('should show quit button when logged out', () => {
const props = makeProps(loggedOutAccountState, settingsState);
- ReactTestUtils.findRenderedDOMComponentWithClass(render(props), 'settings__quit');
+ const component = getComponent(render(props), 'settings__quit');
+ expect(component).to.have.length(1);
});
it('should show quit button when logged in', () => {
const props = makeProps(loggedInAccountState, settingsState);
- ReactTestUtils.findRenderedDOMComponentWithClass(render(props), 'settings__quit');
+ const component = getComponent(render(props), 'settings__quit');
+ expect(component).to.have.length(1);
});
it('should show external links when logged out', () => {
const props = makeProps(loggedOutAccountState, settingsState);
- ReactTestUtils.findRenderedDOMComponentWithClass(render(props), 'settings__external');
+ const component = getComponent(render(props), 'settings__external_link');
+ expect(component.length).to.be.above(0);
});
it('should show external links when logged in', () => {
const props = makeProps(loggedInAccountState, settingsState);
- ReactTestUtils.findRenderedDOMComponentWithClass(render(props), 'settings__external');
+ const component = getComponent(render(props), 'settings__external_link');
+ expect(component.length).to.be.above(0);
});
it('should show account section when logged in', () => {
const props = makeProps(loggedInAccountState, settingsState);
- ReactTestUtils.findRenderedDOMComponentWithClass(render(props), 'settings__account');
+ const component = getComponent(render(props), 'settings__account');
+ expect(component).to.have.length(1);
});
it('should hide account section when logged out', () => {
const props = makeProps(loggedOutAccountState, settingsState);
- const elements = ReactTestUtils.scryRenderedDOMComponentsWithClass(render(props), 'settings__account');
- expect(elements).to.be.empty;
+ const elements = getComponent(render(props), 'settings__account');
+ expect(elements).to.have.length(0);
});
it('should hide account link when not logged in', () => {
const props = makeProps(loggedOutAccountState, settingsState);
- const elements = ReactTestUtils.scryRenderedDOMComponentsWithClass(render(props), 'settings__view-account');
- expect(elements).to.be.empty;
+ const elements = getComponent(render(props), 'settings__view_account');
+ expect(elements).to.have.length(0);
});
it('should show out-of-time message for unpaid account', () => {
const props = makeProps(unpaidAccountState, settingsState);
- const domNode = ReactTestUtils.findRenderedDOMComponentWithClass(render(props), 'settings__account-paid-until-label');
- expect(domNode.textContent).to.contain('OUT OF TIME');
+ const component = getComponent(render(props), 'settings__account_paid_until_label');
+ expect(component.children().text()).to.equal('OUT OF TIME');
});
it('should hide out-of-time message for paid account', () => {
const props = makeProps(loggedInAccountState, settingsState);
- const domNode = ReactTestUtils.findRenderedDOMComponentWithClass(render(props), 'settings__account-paid-until-label');
- expect(domNode.textContent).to.not.contain('OUT OF TIME');
+ const component = getComponent(render(props), 'settings__account_paid_until_label');
+ expect(component.children().text()).not.to.equal('OUT OF TIME');
});
it('should call close callback', (done) => {
const props = makeProps(loggedOutAccountState, settingsState, {
onClose: () => done()
});
- const domNode = ReactTestUtils.findRenderedDOMComponentWithClass(render(props), 'settings__close');
- Simulate.click(domNode);
+ const component = getComponent(render(props), 'settings__close');
+ click(component);
});
it('should call quit callback', (done) => {
const props = makeProps(loggedOutAccountState, settingsState, {
onQuit: () => done()
});
- const domNode = ReactTestUtils.findRenderedDOMComponentWithClass(render(props), 'settings__quit');
- Simulate.click(domNode);
+ const component = getComponent(render(props), 'settings__quit');
+ click(component);
});
it('should call account callback', (done) => {
const props = makeProps(loggedInAccountState, settingsState, {
onViewAccount: () => done()
});
- const domNode = ReactTestUtils.findRenderedDOMComponentWithClass(render(props), 'settings__view-account');
- Simulate.click(domNode);
+ const component = getComponent(render(props), 'settings__view_account');
+ click(component);
});
it('should call support callback', (done) => {
const props = makeProps(loggedInAccountState, settingsState, {
onViewSupport: () => done()
});
- const domNode = ReactTestUtils.findRenderedDOMComponentWithClass(render(props), 'settings__view-support');
- Simulate.click(domNode);
+ const component = getComponent(render(props), 'settings__view_support');
+ click(component);
});
it('should call external links callback', () => {
@@ -153,12 +154,25 @@ describe('components/Settings', () => {
collectedExternalLinkTypes.push(type);
}
});
- const container = ReactTestUtils.findRenderedDOMComponentWithClass(render(props), 'settings__external');
- Array.from(container.childNodes)
- .filter((elm: HTMLElement) => elm.classList.contains('settings__cell'))
- .forEach((elm) => Simulate.click(elm));
+ const container = getComponent(render(props), 'settings__external_link');
+ container.find({ testName: 'settings__external_link' })
+ .forEach((element) => click(element));
expect(collectedExternalLinkTypes).to.include.ordered.members(['faq', 'guides']);
});
});
+
+function render(props) {
+ return shallow(
+ <Settings {...props} />
+ );
+}
+
+function getComponent(container, testName) {
+ return container.findWhere( n => n.prop('testName') === testName);
+}
+
+function click(component) {
+ component.prop('onPress')();
+}