summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2017-10-19 14:17:44 +0200
committerAndrej Mihajlov <and@mullvad.net>2017-10-19 14:17:44 +0200
commit934f9287c6639ca6ffa761d7322666c85cec07f7 (patch)
tree74267c2e8d8c7dae1c991b99dcab8ea29640e6f8
parent47a585cc0bec626836c53538b6fa17701d182af5 (diff)
parentd92d943bdef837b8f1bd638d76ba037ef718b7ba (diff)
downloadmullvadvpn-934f9287c6639ca6ffa761d7322666c85cec07f7.tar.xz
mullvadvpn-934f9287c6639ca6ffa761d7322666c85cec07f7.zip
Merge branch 'problem-report'
-rw-r--r--app/assets/css/buttons.css21
-rw-r--r--app/assets/css/style.css1
-rw-r--r--app/assets/images/icon-email.svg6
-rw-r--r--app/components/Account.css7
-rw-r--r--app/components/SelectLocation.css2
-rw-r--r--app/components/Settings.css3
-rw-r--r--app/components/Settings.js22
-rw-r--r--app/components/Support.css134
-rw-r--r--app/components/Support.js108
-rw-r--r--app/containers/AccountPage.js1
-rw-r--r--app/containers/SettingsPage.js1
-rw-r--r--app/containers/SupportPage.js17
-rw-r--r--app/routes.js6
-rw-r--r--app/transitions.js101
-rw-r--r--test/components/Settings.spec.js14
-rw-r--r--test/components/Support.spec.js70
16 files changed, 425 insertions, 89 deletions
diff --git a/app/assets/css/buttons.css b/app/assets/css/buttons.css
index 7c4f0203c1..805d9e9a74 100644
--- a/app/assets/css/buttons.css
+++ b/app/assets/css/buttons.css
@@ -10,6 +10,11 @@
line-height: 26px;
justify-content: center;
align-items: center;
+ transition: 0.25s opacity;
+}
+
+.button:disabled {
+ opacity: 0.5;
}
.button-label {
@@ -34,12 +39,12 @@
fill: rgba(255,255,255,0.8);
}
-.button--primary:hover {
+.button--primary:not(:disabled):hover {
background-color: rgba(41,71,115,0.9);
color: rgba(255,255,255,1);
}
-.button--primary:hover .button-icon path {
+.button--primary:not(:disabled):hover .button-icon path {
fill: rgba(255,255,255,1);
}
@@ -52,7 +57,7 @@
color: rgba(255,255,255,0.6);
}
-.button--secondary:hover {
+.button--secondary:not(:disabled):hover {
background-color: rgba(41,71,115,0.5);
color: rgba(255,255,255,0.8);
}
@@ -66,7 +71,7 @@
color: rgba(255,255,255,0.8);
}
-.button--negative:hover {
+.button--negative:not(:disabled):hover {
background-color: rgba(208,2,27,0.95);
color: rgba(255,255,255,1);
}
@@ -80,7 +85,7 @@
color: rgba(255,255,255,0.6);
}
-.button--negative-light:hover {
+.button--negative-light:not(:disabled):hover {
background-color: rgba(208,2,27,0.45);
color: rgba(255,255,255,0.8);
}
@@ -98,12 +103,12 @@
fill: rgba(255,255,255,0.8);
}
-.button--positive:hover {
+.button--positive:not(:disabled):hover {
background-color: rgba(63,173,77,0.9);
color: rgba(255,255,255,1);
}
-.button--positive:hover .button-icon path {
+.button--positive:not(:disabled):hover .button-icon path {
fill: rgba(255,255,255,1);
}
@@ -116,7 +121,7 @@
color: rgba(255,255,255,0.8);
}
-.button--neutral:hover {
+.button--neutral:not(:disabled):hover {
background-color: rgba(255,255,255,0.25);
color: rgba(255,255,255,1);
}
diff --git a/app/assets/css/style.css b/app/assets/css/style.css
index 95e601a060..8b0bd30f23 100644
--- a/app/assets/css/style.css
+++ b/app/assets/css/style.css
@@ -12,6 +12,7 @@
@import '../../components/Connect.css';
@import '../../components/Settings.css';
@import '../../components/Account.css';
+@import '../../components/Support.css';
@import '../../components/SelectLocation.css';
@import '../../components/HeaderBar.css';
@import '../../components/Layout.css';
diff --git a/app/assets/images/icon-email.svg b/app/assets/images/icon-email.svg
deleted file mode 100644
index 5a8a8c0945..0000000000
--- a/app/assets/images/icon-email.svg
+++ /dev/null
@@ -1,6 +0,0 @@
-<svg width="16px" height="12px" viewBox="0 0 16 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
- <title>path</title>
- <desc>Mullvad VPN app</desc>
- <defs></defs>
- <path d="M3.80277564,2 L8,4.79814957 L8,4.79814957 L12.1972244,2 L3.80277564,2 Z M14,3.20185043 L8.57398104,6.8191964 C8.40423955,6.93806521 8.20219416,7.00036557 7.98908928,7.00003642 C7.7891813,6.9976758 7.59213144,6.93552384 7.42601896,6.8191964 L2,3.20185043 L2,10 L14,10 L14,3.20185043 Z M0,1.00247329 C0,0.448822582 0.444630861,0 1.00087166,0 L14.9991283,0 C15.5518945,0 16,0.455760956 16,1.00247329 L16,10.9975267 C16,11.5511774 15.5553691,12 14.9991283,12 L1.00087166,12 C0.448105505,12 0,11.544239 0,10.9975267 L0,1.00247329 Z" id="path" fill="#FFFFFF" fill-rule="evenodd"></path>
-</svg> \ No newline at end of file
diff --git a/app/components/Account.css b/app/components/Account.css
index 21aefef292..ab94518034 100644
--- a/app/components/Account.css
+++ b/app/components/Account.css
@@ -24,8 +24,7 @@
margin: 0;
top: 24px;
left: 12px;
- z-index: 1; /* part of .account__container convers the button */
-
+ z-index: 1; /* part of .account__container covers the button */
}
.account__close-icon {
@@ -35,8 +34,8 @@
.account__close-title {
font-family: "Open Sans";
- font-size: 13px;
- font-weight: 600;
+ font-size: 13px;
+ font-weight: 600;
color: rgba(255, 255, 255, 0.6);
}
diff --git a/app/components/SelectLocation.css b/app/components/SelectLocation.css
index 72684711a1..cb63f61115 100644
--- a/app/components/SelectLocation.css
+++ b/app/components/SelectLocation.css
@@ -28,7 +28,7 @@
background-color: transparent;
background-image: url(../assets/images/icon-close.svg);
opacity: 0.6;
- z-index: 1; /* part of .select-location__container convers the button */
+ z-index: 1; /* part of .select-location__container covers the button */
}
.select-location__title {
diff --git a/app/components/Settings.css b/app/components/Settings.css
index 4bd2de5929..7bb3dfcec7 100644
--- a/app/components/Settings.css
+++ b/app/components/Settings.css
@@ -35,7 +35,7 @@
background-color: transparent;
background-image: url(../assets/images/icon-close.svg);
opacity: 0.6;
- z-index: 1; /* part of .settings__container convers the button */
+ z-index: 1; /* part of .settings__container covers the button */
}
.settings__title {
@@ -95,6 +95,7 @@
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;
}
diff --git a/app/components/Settings.js b/app/components/Settings.js
index 9dddb2267d..4c906a9dc1 100644
--- a/app/components/Settings.js
+++ b/app/components/Settings.js
@@ -14,21 +14,15 @@ export type SettingsProps = {
onQuit: () => void,
onClose: () => void,
onViewAccount: () => void,
- onExternalLink: (type: string) => void,
- onUpdateSettings: (update: $Shape<SettingsReduxState>) => void
+ onViewSupport: () => void,
+ onExternalLink: (type: string) => void
};
export default class Settings extends Component {
props: SettingsProps;
- onClose = () => this.props.onClose();
-
- onExternalLink(type: string) {
- this.props.onExternalLink(type);
- }
-
- render(): React.Element<*> {
+ render() {
const isLoggedIn = this.props.account.status === 'ok';
let isOutOfTime = false, formattedExpiry = '';
let expiryIso = this.props.account.expiry;
@@ -44,7 +38,7 @@ export default class Settings extends Component {
<Header hidden={ true } style={ 'defaultDark' } />
<Container>
<div className="settings">
- <button className="settings__close" onClick={ this.onClose } />
+ <button className="settings__close" onClick={ this.props.onClose } />
<div className="settings__container">
<div className="settings__header">
<h2 className="settings__title">Settings</h2>
@@ -80,17 +74,17 @@ export default class Settings extends Component {
</If>
<div className="settings__external">
- <div className="settings__cell settings__cell--active" onClick={ this.onExternalLink.bind(this, 'faq') }>
+ <div className="settings__cell settings__cell--active" onClick={ this.props.onExternalLink.bind(this, 'faq') }>
<div className="settings__cell-label">FAQs</div>
<img className="settings__cell-icon" src="./assets/images/icon-extLink.svg" />
</div>
- <div className="settings__cell settings__cell--active" onClick={ this.onExternalLink.bind(this, 'guides') }>
+ <div className="settings__cell settings__cell--active" onClick={ this.props.onExternalLink.bind(this, 'guides') }>
<div className="settings__cell-label">Guides</div>
<img className="settings__cell-icon" src="./assets/images/icon-extLink.svg" />
</div>
- <div className="settings__cell settings__cell--active" onClick={ this.onExternalLink.bind(this, 'supportEmail') }>
+ <div className="settings__view-support settings__cell settings__cell--active" onClick={ this.props.onViewSupport }>
<div className="settings__cell-label">Contact support</div>
- <img className="settings__cell-icon" src="./assets/images/icon-email.svg" />
+ <img className="settings__cell-disclosure" src="assets/images/icon-chevron.svg" />
</div>
</div>
</div>
diff --git a/app/components/Support.css b/app/components/Support.css
new file mode 100644
index 0000000000..58f4df3a20
--- /dev/null
+++ b/app/components/Support.css
@@ -0,0 +1,134 @@
+.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--description {
+ 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-description-scroll-wrap {
+ width: 100%;
+ display: flex;
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.support__form-description {
+ 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-description::-webkit-input-placeholder {
+ color: rgba(41,77,115,0.4);
+}
+
+.support__footer {
+ padding: 16px 24px 24px;
+}
+
+.support__footer .button + .button {
+ margin-top: 16px;
+} \ No newline at end of file
diff --git a/app/components/Support.js b/app/components/Support.js
new file mode 100644
index 0000000000..783133568f
--- /dev/null
+++ b/app/components/Support.js
@@ -0,0 +1,108 @@
+// @flow
+import React, { Component } from 'react';
+import { Layout, Container, Header } from './Layout';
+import ExternalLinkSVG from '../assets/images/icon-extLink.svg';
+
+export type SupportReport = {
+ email: string,
+ description: string
+};
+
+export type SupportState = SupportReport;
+export type SupportProps = {
+ onClose: () => void;
+ onViewLogs: () => void;
+ onSend: (report: SupportReport) => void;
+};
+
+export default class Support extends Component {
+ props: SupportProps;
+ state: SupportState = {
+ email: '',
+ description: ''
+ }
+
+ validate() {
+ return this.state.description.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 });
+ }
+
+ onChangeDescription = (e: Event) => {
+ const input = e.target;
+ if(!(input instanceof HTMLTextAreaElement)) {
+ throw new Error('input must be an instance of HTMLTextAreaElement');
+ }
+ this.setState({ description: input.value });
+ }
+
+ onSend = () => {
+ this.props.onSend(this.state);
+ }
+
+ render() {
+ return (
+ <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">
+
+ <div className="support__header">
+ <h2 className="support__title">Contact support</h2>
+ <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>
+ </div>
+
+ <div className="support__content">
+ <div className="support__form">
+ <div className="support__form-row">
+ <input className="support__form-email"
+ type="email"
+ placeholder="Your email"
+ value={ this.state.email }
+ onChange={ this.onChangeEmail }
+ autoFocus={ true } />
+ </div>
+ <div className="support__form-row support__form-row--description">
+ <div className="support__form-description-scroll-wrap">
+ <textarea className="support__form-description"
+ placeholder="Describe your problem"
+ value={ this.state.description }
+ onChange={ this.onChangeDescription } />
+ </div>
+ </div>
+ <div className="support__footer">
+ <button type="button"
+ className="button button--primary"
+ onClick={ this.props.onViewLogs }>
+ <span className="support__form-view-logs 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>
+
+ </div>
+ </div>
+ </Container>
+ </Layout>
+ );
+ }
+}
diff --git a/app/containers/AccountPage.js b/app/containers/AccountPage.js
index b68ce4f84c..1727c1e5be 100644
--- a/app/containers/AccountPage.js
+++ b/app/containers/AccountPage.js
@@ -15,7 +15,6 @@ const mapDispatchToProps = (dispatch, props) => {
return {
onLogout: () => logout(props.backend),
onClose: () => dispatch(push('/settings')),
- onViewAccount: () => dispatch(push('/settings/account')),
onBuyMore: () => shell.openExternal(links['purchase'])
};
};
diff --git a/app/containers/SettingsPage.js b/app/containers/SettingsPage.js
index 4f5ef06968..520f27ce6e 100644
--- a/app/containers/SettingsPage.js
+++ b/app/containers/SettingsPage.js
@@ -13,6 +13,7 @@ const mapDispatchToProps = (dispatch, _props) => {
onQuit: () => remote.app.quit(),
onClose: () => dispatch(push('/connect')),
onViewAccount: () => dispatch(push('/settings/account')),
+ onViewSupport: () => dispatch(push('/settings/support')),
onExternalLink: (type) => shell.openExternal(links[type]),
};
};
diff --git a/app/containers/SupportPage.js b/app/containers/SupportPage.js
new file mode 100644
index 0000000000..fe579395f6
--- /dev/null
+++ b/app/containers/SupportPage.js
@@ -0,0 +1,17 @@
+import { connect } from 'react-redux';
+import { push } from 'react-router-redux';
+import Support from '../components/Support';
+
+const mapStateToProps = (state) => {
+ return state;
+};
+
+const mapDispatchToProps = (dispatch, _props) => {
+ return {
+ onClose: () => dispatch(push('/settings')),
+ onViewLogs: () => {},
+ onSend: (_report) => {}
+ };
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(Support);
diff --git a/app/routes.js b/app/routes.js
index 0770785ba4..5736e7f37a 100644
--- a/app/routes.js
+++ b/app/routes.js
@@ -8,6 +8,7 @@ import LoginPage from './containers/LoginPage';
import ConnectPage from './containers/ConnectPage';
import SettingsPage from './containers/SettingsPage';
import AccountPage from './containers/AccountPage';
+import SupportPage from './containers/SupportPage';
import SelectLocationPage from './containers/SelectLocationPage';
import { getTransitionProps } from './transitions';
@@ -94,8 +95,9 @@ export default function makeRoutes(getState: ReduxGetState, componentProps: Shar
<LoginRoute exact path="/" component={ LoginPage } />
<PrivateRoute exact path="/connect" component={ ConnectPage } />
<PublicRoute exact path="/settings" component={ SettingsPage } />
- <PrivateRoute path="/settings/account" component={ AccountPage } />
- <PrivateRoute path="/select-location" component={ SelectLocationPage } />
+ <PrivateRoute exact path="/settings/account" component={ AccountPage } />
+ <PublicRoute exact path="/settings/support" component={ SupportPage } />
+ <PrivateRoute exact path="/select-location" component={ SelectLocationPage } />
</Switch>
</CSSTransitionGroup>
</WindowChrome>
diff --git a/app/transitions.js b/app/transitions.js
index 5bdc76471c..0ce94e2d16 100644
--- a/app/transitions.js
+++ b/app/transitions.js
@@ -18,12 +18,49 @@ type TransitionMap = {
};
/**
+ * Transition descriptors
+ */
+const transitions: TransitionMap = {
+ slide: {
+ forward: {
+ name: 'slide-up-transition',
+ duration: 450
+ },
+ backward: {
+ name: 'slide-down-transition',
+ duration: 450
+ }
+ },
+ push: {
+ forward: {
+ name: 'push-transition',
+ duration: 450
+ },
+ backward: {
+ name: 'pop-transition',
+ duration: 450
+ }
+ }
+};
+
+/**
+ * Transition rules
+ * (null) is used to indicate any route.
+ */
+const transitionRules = [
+ r('/settings', '/settings/account', transitions.push),
+ r('/settings', '/settings/support', transitions.push),
+ r(null, '/settings', transitions.slide),
+ r(null, '/select-location', transitions.slide)
+];
+
+/**
* Calculate CSSTransitionGroup props.
*
* @param {string} [fromRoute] - source route
* @param {string} toRoute - target route
*/
-export const getTransitionProps = (fromRoute: ?string, toRoute: string): CSSTransitionGroupProps => {
+export function getTransitionProps(fromRoute: ?string, toRoute: string): CSSTransitionGroupProps {
// ignore initial transition and transition between the same routes
if(!fromRoute || fromRoute === toRoute) {
return noTransitionProps();
@@ -37,13 +74,13 @@ export const getTransitionProps = (fromRoute: ?string, toRoute: string): CSSTran
}
return noTransitionProps();
-};
+}
/**
* Integrate TransitionDescriptor into CSSTransitionGroupProps
* @param {TransitionDescriptor} descriptor
*/
-const toCSSTransitionGroupProps = (descriptor: TransitionDescriptor): CSSTransitionGroupProps => {
+function toCSSTransitionGroupProps(descriptor: TransitionDescriptor): CSSTransitionGroupProps {
const {name, duration} = descriptor;
return {
transitionName: name,
@@ -52,58 +89,24 @@ const toCSSTransitionGroupProps = (descriptor: TransitionDescriptor): CSSTransit
transitionEnter: true,
transitionLeave: true
};
-};
+}
/**
* Returns default props with animations disabled
*/
-const noTransitionProps = (): CSSTransitionGroupProps => ({
- transitionName: '',
- transitionEnterTimeout: 0,
- transitionLeaveTimeout: 0,
- transitionEnter: false,
- transitionLeave: false
-});
-
-/**
- * Transition descriptors
- */
-const transitions: TransitionMap = {
- slide: {
- forward: {
- name: 'slide-up-transition',
- duration: 450
- },
- backward: {
- name: 'slide-down-transition',
- duration: 450
- }
- },
- push: {
- forward: {
- name: 'push-transition',
- duration: 450
- },
- backward: {
- name: 'pop-transition',
- duration: 450
- }
- }
-};
+function noTransitionProps(): CSSTransitionGroupProps {
+ return {
+ transitionName: '',
+ transitionEnterTimeout: 0,
+ transitionLeaveTimeout: 0,
+ transitionEnter: false,
+ transitionLeave: false
+ };
+}
/**
* Shortcut to create TransitionRule
*/
-const r = (from: ?string, to: string, fork: TransitionFork): TransitionRule => {
+function r(from: ?string, to: string, fork: TransitionFork): TransitionRule {
return new TransitionRule(from, to, fork);
-};
-
-/**
- * Transition rules
- * (null) is used to indicate any route.
- */
-const transitionRules = [
- r('/settings', '/settings/account', transitions.push),
- r(null, '/settings', transitions.slide),
- r(null, '/select-location', transitions.slide)
-]; \ No newline at end of file
+}
diff --git a/test/components/Settings.spec.js b/test/components/Settings.spec.js
index 5655e1e5fa..f90d658f9b 100644
--- a/test/components/Settings.spec.js
+++ b/test/components/Settings.spec.js
@@ -43,8 +43,8 @@ describe('components/Settings', () => {
onQuit: () => {},
onClose: () => {},
onViewAccount: () => {},
- onExternalLink: (_type) => {},
- onUpdateSettings: (_update) => {}
+ onViewSupport: () => {},
+ onExternalLink: (_type) => {}
};
return Object.assign({}, defaultProps, mergeProps);
};
@@ -128,6 +128,14 @@ describe('components/Settings', () => {
Simulate.click(domNode);
});
+ 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);
+ });
+
it('should call external links callback', () => {
let collectedExternalLinkTypes: Array<string> = [];
const props = makeProps(loggedOutAccountState, settingsState, {
@@ -140,7 +148,7 @@ describe('components/Settings', () => {
.filter((elm: HTMLElement) => elm.classList.contains('settings__cell'))
.forEach((elm) => Simulate.click(elm));
- expect(collectedExternalLinkTypes).to.include.ordered.members(['faq', 'guides', 'supportEmail']);
+ expect(collectedExternalLinkTypes).to.include.ordered.members(['faq', 'guides']);
});
});
diff --git a/test/components/Support.spec.js b/test/components/Support.spec.js
new file mode 100644
index 0000000000..bcd871855b
--- /dev/null
+++ b/test/components/Support.spec.js
@@ -0,0 +1,70 @@
+// @flow
+
+import { expect } from 'chai';
+import React from 'react';
+import ReactTestUtils, { Simulate } from 'react-dom/test-utils';
+import Support from '../../app/components/Support';
+
+import type { SupportProps } from '../../app/components/Support';
+
+describe('components/Support', () => {
+
+ const makeProps = (mergeProps: $Shape<SupportProps> = {}): SupportProps => {
+ const defaultProps: SupportProps = {
+ onClose: () => {},
+ onViewLogs: () => {},
+ onSend: (_report) => {}
+ };
+ 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);
+ });
+
+ it('should call view logs callback', (done) => {
+ const props = makeProps({
+ onViewLogs: () => done()
+ });
+ const domNode = ReactTestUtils.findRenderedDOMComponentWithClass(render(props), 'support__form-view-logs');
+ Simulate.click(domNode);
+ });
+
+ it('should call send callback when description filled in', (done) => {
+ const props = makeProps({
+ onSend: (_report) => done()
+ });
+
+ const component = render(props);
+
+ const descriptionField = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'support__form-description');
+ descriptionField.value = 'Lorem Ipsum';
+ Simulate.change(descriptionField);
+
+ const sendButton = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'support__form-send');
+ expect(sendButton.disabled).to.be.false;
+ Simulate.click(sendButton);
+ });
+
+ it('should not call send callback when description is empty', () => {
+ const component = render(makeProps());
+
+ const descriptionField = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'support__form-description');
+ descriptionField.value = '';
+ Simulate.change(descriptionField);
+
+ const sendButton = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'support__form-send');
+ expect(sendButton.disabled).to.be.true;
+ });
+
+});