summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorErik Larkö <erik@mullvad.net>2017-08-22 13:45:57 +0200
committerErik Larkö <erik@mullvad.net>2017-08-22 13:45:57 +0200
commitda4f2903b14dbd09b088e44d6acf58d91d3df990 (patch)
treecfbab4fd22712f82e5e778961fd444a86b351414
parent750d633006774154301fffaeb680b9eb4065c745 (diff)
parent18ecd12a7472673c865f9bff777c3a776a92d1bb (diff)
downloadmullvadvpn-da4f2903b14dbd09b088e44d6acf58d91d3df990.tar.xz
mullvadvpn-da4f2903b14dbd09b088e44d6acf58d91d3df990.zip
Merge branch 'login-refactor-and-tests'
-rw-r--r--app/components/Login.js183
-rw-r--r--app/containers/LoginPage.js1
-rw-r--r--flow-typed/npm/chai_v4.x.x.js223
-rw-r--r--flow-typed/npm/sinon_vx.x.x.js333
-rw-r--r--package.json3
-rw-r--r--test/components/Login.spec.js107
-rw-r--r--yarn.lock57
7 files changed, 799 insertions, 108 deletions
diff --git a/app/components/Login.js b/app/components/Login.js
index e09df8c3c7..ea727adc22 100644
--- a/app/components/Login.js
+++ b/app/components/Login.js
@@ -1,6 +1,5 @@
// @flow
import React, { Component } from 'react';
-import { If, Then } from 'react-if';
import { Layout, Container, Header } from './Layout';
import AccountInput from './AccountInput';
import ExternalLinkSVG from '../assets/images/icon-extLink.svg';
@@ -12,7 +11,6 @@ export type LoginPropTypes = {
account: AccountReduxState,
onLogin: (accountNumber: string) => void,
onSettings: ?(() => void),
- onChange: (input: string) => void,
onFirstChangeAfterFailure: () => void,
onExternalLink: (type: string) => void,
};
@@ -21,16 +19,20 @@ export default class Login extends Component {
props: LoginPropTypes;
state = {
notifyOnFirstChangeAfterFailure: false,
- isActive: false
- }
+ isActive: false,
+ unsubmittedAccountNumber: '',
+ };
onCreateAccount = () => this.props.onExternalLink('createAccount');
onFocus = () => this.setState({ isActive: true });
onBlur = () => this.setState({ isActive: false });
onLogin = () => {
- const { accountNumber } = this.props.account;
+ const accountNumber = this.state.unsubmittedAccountNumber;
if(accountNumber && accountNumber.length > 0) {
this.props.onLogin(accountNumber);
+ this.setState({
+ unsubmittedAccountNumber: '',
+ });
}
}
@@ -40,7 +42,9 @@ export default class Login extends Component {
this.setState({ notifyOnFirstChangeAfterFailure: false });
this.props.onFirstChangeAfterFailure();
}
- this.props.onChange(val);
+ this.setState({
+ unsubmittedAccountNumber: val,
+ });
}
formTitle(s: LoginState): string {
@@ -79,17 +83,6 @@ export default class Login extends Component {
return classes.join(' ');
}
- footerClass(s: LoginState): string {
- const classes = ['login-footer'];
- switch(s) {
- case 'ok':
- case 'logging in':
- classes.push('login-footer--invisible');
- break;
- }
- return classes.join(' ');
- }
-
submitClass(s: LoginState, accountNumber: ?string): string {
const classes = ['login-form__submit'];
@@ -114,28 +107,19 @@ export default class Login extends Component {
}
render(): React.Element<*> {
- const { accountNumber, status, error } = this.props.account;
+ const { status } = this.props.account;
const title = this.formTitle(status);
- const subtitle = this.formSubtitle(status, error);
- let isConnecting = false;
- let isFailed = false;
- let isLoggedIn = false;
- switch(status) {
- case 'logging in': isConnecting = true; break;
- case 'failed': isFailed = true; break;
- case 'ok': isLoggedIn = true; break;
- }
+ const shouldShowLoginForm = status !== 'ok';
+ const shouldShowFooter = status === 'none' || status === 'failed';
- const inputWrapClass = this.inputWrapClass(status);
- const footerClass = this.footerClass(status);
- const submitClass = this.submitClass(status, accountNumber);
+ const statusIcon = this._getStatusIcon();
- const autoFocusRef = input => {
- if(isFailed && input) {
- input.focus();
- }
- };
+ const loginFormClass = shouldShowLoginForm ? '' : 'login-form__fields--invisible';
+ const loginForm = this._createLoginForm();
+
+ const footerClass = shouldShowFooter ? '' : 'login-footer--invisible';
+ const footer = this._createFooter();
return (
<Layout>
@@ -143,64 +127,93 @@ export default class Login extends Component {
<Container>
<div className="login">
<div className="login-form">
- { /* show spinner when logging in */ }
- <If condition={ isConnecting }>
- <Then>
- <div className="login-form__status-icon">
- <img src="./assets/images/icon-spinner.svg" alt="" />
- </div>
- </Then>
- </If>
-
- { /* show error icon when failed */ }
- <If condition={ isFailed }>
- <Then>
- <div className="login-form__status-icon">
- <img src="./assets/images/icon-fail.svg" alt="" />
- </div>
- </Then>
- </If>
-
- { /* show tick when logged in */ }
- <If condition={ isLoggedIn }>
- <Then>
- <div className="login-form__status-icon">
- <img src="./assets/images/icon-success.svg" alt="" />
- </div>
- </Then>
- </If>
+ { statusIcon }
<div className="login-form__title">{ title }</div>
- <div className={ 'login-form__fields' + (isLoggedIn ? ' login-form__fields--invisible' : '') }>
- <div className="login-form__subtitle">{ subtitle }</div>
- <div className={ inputWrapClass }>
- <AccountInput className="login-form__input-field"
- type="text"
- placeholder="e.g 0000 0000 0000"
- onFocus={ this.onFocus }
- onBlur={ this.onBlur }
- onChange={ this.onInputChange }
- onEnter={ this.onLogin }
- value={ accountNumber || '' }
- disabled={ isConnecting }
- autoFocus={ true }
- ref={ autoFocusRef } />
- <button className={ submitClass } onClick={ this.onLogin }>
- <LoginArrowSVG className="login-form__submit-icon" />
- </button>
- </div>
+
+ <div className={ 'login-form__fields ' + loginFormClass }>
+ { loginForm }
</div>
</div>
- <div className={ footerClass }>
- <div className="login-footer__prompt">{ 'Don\'t have an account number?' }</div>
- <button className="button button--primary" onClick={ this.onCreateAccount }>
- <span className="button-label">Create account</span>
- <ExternalLinkSVG className="button-icon button-icon--16" />
- </button>
+
+ <div className={ 'login-footer ' + footerClass }>
+ { footer }
</div>
</div>
</Container>
</Layout>
);
}
+
+ _getStatusIcon(): React.Element<*> {
+ const statusIconPath = this._getStatusIconPath();
+
+ return <div className="login-form__status-icon">
+ <img src={ statusIconPath } alt="" />
+ </div>;
+ }
+
+ _getStatusIconPath(): ?string {
+ switch(this.props.account.status) {
+ case 'logging in':
+ return './assets/images/icon-spinner.svg';
+ case 'failed':
+ return './assets/images/icon-fail.svg';
+ case 'ok':
+ return './assets/images/icon-success.svg';
+ default:
+ return undefined;
+ }
+ }
+
+ _createLoginForm(): React.Element<*> {
+ const { status, error } = this.props.account;
+ const accountNumber = status === 'logging in'
+ ? this.props.account.accountNumber
+ : this.state.unsubmittedAccountNumber;
+
+ const inputDisabled = status === 'logging in';
+
+ const subtitle = this.formSubtitle(status, error);
+
+ const inputWrapClass = this.inputWrapClass(status);
+ const submitClass = this.submitClass(status, accountNumber);
+
+ const autoFocusRef = input => {
+ if(status === 'failed' && input) {
+ input.focus();
+ }
+ };
+
+ return <div>
+ <div className="login-form__subtitle">{ subtitle }</div>
+ <div className={ inputWrapClass }>
+ <AccountInput className="login-form__input-field"
+ type="text"
+ placeholder="e.g 0000 0000 0000"
+ onFocus={ this.onFocus }
+ onBlur={ this.onBlur }
+ onChange={ this.onInputChange }
+ onEnter={ this.onLogin }
+ value={ accountNumber || '' }
+ disabled={ inputDisabled }
+ autoFocus={ true }
+ ref={ autoFocusRef } />
+ <button className={ submitClass } onClick={ this.onLogin }>
+ <LoginArrowSVG className="login-form__submit-icon" />
+ </button>
+ </div>
+ </div>;
+ }
+
+ _createFooter(): React.Element<*> {
+ return <div>
+ <div className="login-footer__prompt">{ 'Don\'t have an account number?' }</div>
+ <button className="button button--primary" onClick={ this.onCreateAccount }>
+ <span className="button-label">Create account</span>
+ <ExternalLinkSVG className="button-icon button-icon--16" />
+ </button>
+ </div>;
+ }
}
+
diff --git a/app/containers/LoginPage.js b/app/containers/LoginPage.js
index c9dbe385f8..abee712be4 100644
--- a/app/containers/LoginPage.js
+++ b/app/containers/LoginPage.js
@@ -13,7 +13,6 @@ const mapDispatchToProps = (dispatch, props) => {
return {
onSettings: () => dispatch(push('/settings')),
onLogin: (account) => login(backend, account),
- onChange: (accountNumber) => loginChange({ accountNumber }),
onFirstChangeAfterFailure: () => loginChange({ status: 'none', error: null }),
onExternalLink: (type) => shell.openExternal(links[type])
};
diff --git a/flow-typed/npm/chai_v4.x.x.js b/flow-typed/npm/chai_v4.x.x.js
new file mode 100644
index 0000000000..56cc062e32
--- /dev/null
+++ b/flow-typed/npm/chai_v4.x.x.js
@@ -0,0 +1,223 @@
+// flow-typed signature: f4c38ee453c1a780b0ce642321a96131
+// flow-typed version: 147ab6243c/chai_v4.x.x/flow_>=v0.15.0
+
+declare module "chai" {
+
+ declare type ExpectChain<T> = {
+ and: ExpectChain<T>,
+ at: ExpectChain<T>,
+ be: ExpectChain<T>,
+ been: ExpectChain<T>,
+ have: ExpectChain<T>,
+ has: ExpectChain<T>,
+ is: ExpectChain<T>,
+ of: ExpectChain<T>,
+ same: ExpectChain<T>,
+ that: ExpectChain<T>,
+ to: ExpectChain<T>,
+ which: ExpectChain<T>,
+ with: ExpectChain<T>,
+
+ not: ExpectChain<T>,
+ deep: ExpectChain<T>,
+ any: ExpectChain<T>,
+ all: ExpectChain<T>,
+
+ a: ExpectChain<T> & (type: string) => ExpectChain<T>,
+ an: ExpectChain<T> & (type: string) => ExpectChain<T>,
+
+ include: ExpectChain<T> & (value: mixed) => ExpectChain<T>,
+ includes: ExpectChain<T> & (value: mixed) => ExpectChain<T>,
+ contain: ExpectChain<T> & (value: mixed) => ExpectChain<T>,
+ contains: ExpectChain<T> & (value: mixed) => ExpectChain<T>,
+
+ eql: (value: T) => ExpectChain<T>,
+ equal: (value: T) => ExpectChain<T>,
+ equals: (value: T) => ExpectChain<T>,
+
+ above: (value: T & number) => ExpectChain<T>,
+ least: (value: T & number) => ExpectChain<T>,
+ below: (value: T & number) => ExpectChain<T>,
+ most: (value: T & number) => ExpectChain<T>,
+ within: (start: T & number, finish: T & number) => ExpectChain<T>,
+
+ instanceof: (constructor: mixed) => ExpectChain<T>,
+ property: (
+ <P>(name: string, value?: P) => ExpectChain<P>
+ & (name: string) => ExpectChain<mixed>
+ ),
+
+ length: (value: number) => ExpectChain<T> | ExpectChain<number>,
+ lengthOf: (value: number) => ExpectChain<T>,
+
+ match: (regex: RegExp) => ExpectChain<T>,
+ string: (string: string) => ExpectChain<T>,
+
+ key: (key: string) => ExpectChain<T>,
+ keys: (key: string | Array<string>, ...keys: Array<string>) => ExpectChain<T>,
+
+ throw: <E>(
+ err?: Class<E> | Error | RegExp | string,
+ errMsgMatcher?: RegExp | string,
+ msg?: string) => ExpectChain<T>,
+
+ respondTo: (method: string) => ExpectChain<T>,
+ itself: ExpectChain<T>,
+
+ satisfy: (method: (value: T) => bool) => ExpectChain<T>,
+
+ closeTo: (expected: T & number, delta: number) => ExpectChain<T>,
+
+ members: (set: mixed) => ExpectChain<T>,
+ oneOf: (list: Array<T>) => ExpectChain<T>,
+
+ change: (obj: mixed, key: string) => ExpectChain<T>,
+ increase: (obj: mixed, key: string) => ExpectChain<T>,
+ decrease: (obj: mixed, key: string) => ExpectChain<T>,
+
+ // dirty-chai
+ ok: () => ExpectChain<T>,
+ true: () => ExpectChain<T>,
+ false: () => ExpectChain<T>,
+ null: () => ExpectChain<T>,
+ undefined: () => ExpectChain<T>,
+ exist: () => ExpectChain<T>,
+ empty: () => ExpectChain<T>,
+
+ extensible: () => ExpectChain<T>,
+ sealed: () => ExpectChain<T>,
+ frozen: () => ExpectChain<T>,
+
+ // chai-immutable
+ size: (n: number) => ExpectChain<T>,
+
+ // sinon-chai
+ called: () => ExpectChain<T>,
+ callCount: (n: number) => ExpectChain<T>,
+ calledOnce: () => ExpectChain<T>,
+ calledTwice: () => ExpectChain<T>,
+ calledThrice: () => ExpectChain<T>,
+ calledBefore: (spy: mixed) => ExpectChain<T>,
+ calledAfter: (spy: mixed) => ExpectChain<T>,
+ calledWith: (...args: Array<mixed>) => ExpectChain<T>,
+ calledWithMatch: (...args: Array<mixed>) => ExpectChain<T>,
+ calledWithExactly: (...args: Array<mixed>) => ExpectChain<T>,
+
+ // chai-as-promised
+ eventually: ExpectChain<T>,
+ resolvedWith: (value: mixed) => Promise<mixed> & ExpectChain<T>,
+ resolved: () => Promise<mixed> & ExpectChain<T>,
+ rejectedWith: (value: mixed) => Promise<mixed> & ExpectChain<T>,
+ rejected: () => Promise<mixed> & ExpectChain<T>,
+ notify: (callback: () => mixed) => ExpectChain<T>,
+ fulfilled: () => Promise<mixed> & ExpectChain<T>,
+
+ // chai-subset
+ containSubset: (obj: Object | Object[]) => ExpectChain<T>
+ };
+
+ declare function expect<T>(actual: T): ExpectChain<T>;
+
+ declare function use(plugin: (chai: Object, utils: Object) => void): void;
+
+ declare class assert {
+ static(expression: mixed, message?: string): void;
+ static fail(actual: mixed, expected: mixed, message?: string, operator?: string): void;
+
+ static isOk(object: mixed, message?: string): void;
+ static isNotOk(object: mixed, message?: string): void;
+
+ static equal(actual: mixed, expected: mixed, message?: string): void;
+ static notEqual(actual: mixed, expected: mixed, message?: string): void;
+
+ static strictEqual(act: mixed, exp: mixed, msg?: string): void;
+ static notStrictEqual(act: mixed, exp: mixed, msg?: string): void;
+
+ static deepEqual(act: mixed, exp: mixed, msg?: string): void;
+ static notDeepEqual(act: mixed, exp: mixed, msg?: string): void;
+
+ static ok(val: mixed, msg?: string): void;
+ static isTrue(val: mixed, msg?: string): void;
+ static isNotTrue(val: mixed, msg?: string): void;
+ static isFalse(val: mixed, msg?: string): void;
+ static isNotFalse(val: mixed, msg?: string): void;
+
+ static isNull(val: mixed, msg?: string): void;
+ static isNotNull(val: mixed, msg?: string): void;
+
+ static isUndefined(val: mixed, msg?: string): void;
+ static isDefined(val: mixed, msg?: string): void;
+
+ static isNaN(val: mixed, msg?: string): void;
+ static isNotNaN(val: mixed, msg?: string): void;
+
+ static isAbove(val: number, abv: number, msg?: string): void;
+ static isBelow(val: number, blw: number, msg?: string): void;
+
+ static isAtMost(val: number, atmst: number, msg?: string): void;
+ static isAtLeast(val: number, atlst: number, msg?: string): void;
+
+ static isFunction(val: mixed, msg?: string): void;
+ static isNotFunction(val: mixed, msg?: string): void;
+
+ static isObject(val: mixed, msg?: string): void;
+ static isNotObject(val: mixed, msg?: string): void;
+
+ static isArray(val: mixed, msg?: string): void;
+ static isNotArray(val: mixed, msg?: string): void;
+
+ static isString(val: mixed, msg?: string): void;
+ static isNotString(val: mixed, msg?: string): void;
+
+ static isNumber(val: mixed, msg?: string): void;
+ static isNotNumber(val: mixed, msg?: string): void;
+
+ static isBoolean(val: mixed, msg?: string): void;
+ static isNotBoolean(val: mixed, msg?: string): void;
+
+ static typeOf(val: mixed, type: string, msg?: string): void;
+ static notTypeOf(val: mixed, type: string, msg?: string): void;
+
+ static instanceOf(val: mixed, constructor: Function, msg?: string): void;
+ static notInstanceOf(val: mixed, constructor: Function, msg?: string): void;
+
+ static include(exp: string, inc: mixed, msg?: string): void;
+ static include<T>(exp: Array<T>, inc: T, msg?: string): void;
+
+ static notInclude(exp: string, inc: mixed, msg?: string): void;
+ static notInclude<T>(exp: Array<T>, inc: T, msg?: string): void;
+
+ static match(exp: mixed, re: RegExp, msg?: string): void;
+ static notMatch(exp: mixed, re: RegExp, msg?: string): void;
+
+ static property(obj: Object, prop: string, msg?: string): void;
+ static notProperty(obj: Object, prop: string, msg?: string): void;
+ static deepProperty(obj: Object, prop: string, msg?: string): void;
+ static notDeepProperty(obj: Object, prop: string, msg?: string): void;
+
+ static propertyVal(obj: Object, prop: string, val: mixed, msg?: string): void;
+ static propertyNotVal(obj: Object, prop: string, val: mixed, msg?: string): void;
+
+ static deepPropertyVal(obj: Object, prop: string, val: mixed, msg?: string): void;
+ static deepPropertyNotVal(obj: Object, prop: string, val: mixed, msg?: string): void;
+
+ static lengthOf(exp: mixed, len: number, msg?: string): void;
+
+ static throws<E>(
+ func: () => any,
+ err?: Class<E> | Error | RegExp | string,
+ errorMsgMatcher?: string | RegExp,
+ msg?: string): void;
+ static doesNotThrow<E>(
+ func: () => any,
+ err?: Class<E> | Error | RegExp | string,
+ errorMsgMatcher?: string | RegExp,
+ msg?: string): void;
+ }
+
+ declare var config: {
+ includeStack: boolean,
+ showDiff: boolean,
+ truncateThreshold: number
+ };
+}
diff --git a/flow-typed/npm/sinon_vx.x.x.js b/flow-typed/npm/sinon_vx.x.x.js
new file mode 100644
index 0000000000..1c51e0099c
--- /dev/null
+++ b/flow-typed/npm/sinon_vx.x.x.js
@@ -0,0 +1,333 @@
+// flow-typed signature: 0ff5f095b5512c194789f6356e524e5b
+// flow-typed version: <<STUB>>/sinon_v^3.2.1/flow_v0.50.0
+
+/**
+ * This is an autogenerated libdef stub for:
+ *
+ * 'sinon'
+ *
+ * Fill this stub out by replacing all the `any` types.
+ *
+ * Once filled out, we encourage you to share your work with the
+ * community by sending a pull request to:
+ * https://github.com/flowtype/flow-typed
+ */
+
+declare module 'sinon' {
+ declare module.exports: any;
+}
+
+/**
+ * We include stubs for each file inside this npm package in case you need to
+ * require those files directly. Feel free to delete any files that aren't
+ * needed.
+ */
+declare module 'sinon/lib/sinon' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/assert' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/behavior' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/blob' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/call' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/collect-own-methods' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/collection' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/color' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/default-behaviors' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/match' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/mock-expectation' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/mock' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/sandbox-stub' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/sandbox' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/spy-formatters' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/spy' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/stub-entire-object' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/stub-non-function-property' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/stub' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/throw-on-falsy-object' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/util/core/called-in-order' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/util/core/deep-equal' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/util/core/default-config' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/util/core/deprecated' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/util/core/every' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/util/core/extend' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/util/core/format' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/util/core/function-name' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/util/core/function-to-string' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/util/core/get-config' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/util/core/get-property-descriptor' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/util/core/iterable-to-string' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/util/core/order-by-first-call' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/util/core/restore' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/util/core/times-in-words' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/util/core/typeOf' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/util/core/value-to-string' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/util/core/walk' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/util/core/wrap-method' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/lib/sinon/util/fake_timers' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/pkg/sinon-3.2.1' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/pkg/sinon-no-sourcemaps-3.2.1' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/pkg/sinon-no-sourcemaps' {
+ declare module.exports: any;
+}
+
+declare module 'sinon/pkg/sinon' {
+ declare module.exports: any;
+}
+
+// Filename aliases
+declare module 'sinon/lib/sinon.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon'>;
+}
+declare module 'sinon/lib/sinon/assert.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/assert'>;
+}
+declare module 'sinon/lib/sinon/behavior.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/behavior'>;
+}
+declare module 'sinon/lib/sinon/blob.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/blob'>;
+}
+declare module 'sinon/lib/sinon/call.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/call'>;
+}
+declare module 'sinon/lib/sinon/collect-own-methods.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/collect-own-methods'>;
+}
+declare module 'sinon/lib/sinon/collection.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/collection'>;
+}
+declare module 'sinon/lib/sinon/color.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/color'>;
+}
+declare module 'sinon/lib/sinon/default-behaviors.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/default-behaviors'>;
+}
+declare module 'sinon/lib/sinon/match.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/match'>;
+}
+declare module 'sinon/lib/sinon/mock-expectation.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/mock-expectation'>;
+}
+declare module 'sinon/lib/sinon/mock.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/mock'>;
+}
+declare module 'sinon/lib/sinon/sandbox-stub.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/sandbox-stub'>;
+}
+declare module 'sinon/lib/sinon/sandbox.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/sandbox'>;
+}
+declare module 'sinon/lib/sinon/spy-formatters.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/spy-formatters'>;
+}
+declare module 'sinon/lib/sinon/spy.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/spy'>;
+}
+declare module 'sinon/lib/sinon/stub-entire-object.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/stub-entire-object'>;
+}
+declare module 'sinon/lib/sinon/stub-non-function-property.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/stub-non-function-property'>;
+}
+declare module 'sinon/lib/sinon/stub.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/stub'>;
+}
+declare module 'sinon/lib/sinon/throw-on-falsy-object.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/throw-on-falsy-object'>;
+}
+declare module 'sinon/lib/sinon/util/core/called-in-order.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/util/core/called-in-order'>;
+}
+declare module 'sinon/lib/sinon/util/core/deep-equal.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/util/core/deep-equal'>;
+}
+declare module 'sinon/lib/sinon/util/core/default-config.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/util/core/default-config'>;
+}
+declare module 'sinon/lib/sinon/util/core/deprecated.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/util/core/deprecated'>;
+}
+declare module 'sinon/lib/sinon/util/core/every.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/util/core/every'>;
+}
+declare module 'sinon/lib/sinon/util/core/extend.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/util/core/extend'>;
+}
+declare module 'sinon/lib/sinon/util/core/format.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/util/core/format'>;
+}
+declare module 'sinon/lib/sinon/util/core/function-name.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/util/core/function-name'>;
+}
+declare module 'sinon/lib/sinon/util/core/function-to-string.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/util/core/function-to-string'>;
+}
+declare module 'sinon/lib/sinon/util/core/get-config.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/util/core/get-config'>;
+}
+declare module 'sinon/lib/sinon/util/core/get-property-descriptor.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/util/core/get-property-descriptor'>;
+}
+declare module 'sinon/lib/sinon/util/core/iterable-to-string.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/util/core/iterable-to-string'>;
+}
+declare module 'sinon/lib/sinon/util/core/order-by-first-call.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/util/core/order-by-first-call'>;
+}
+declare module 'sinon/lib/sinon/util/core/restore.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/util/core/restore'>;
+}
+declare module 'sinon/lib/sinon/util/core/times-in-words.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/util/core/times-in-words'>;
+}
+declare module 'sinon/lib/sinon/util/core/typeOf.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/util/core/typeOf'>;
+}
+declare module 'sinon/lib/sinon/util/core/value-to-string.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/util/core/value-to-string'>;
+}
+declare module 'sinon/lib/sinon/util/core/walk.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/util/core/walk'>;
+}
+declare module 'sinon/lib/sinon/util/core/wrap-method.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/util/core/wrap-method'>;
+}
+declare module 'sinon/lib/sinon/util/fake_timers.js' {
+ declare module.exports: $Exports<'sinon/lib/sinon/util/fake_timers'>;
+}
+declare module 'sinon/pkg/sinon-3.2.1.js' {
+ declare module.exports: $Exports<'sinon/pkg/sinon-3.2.1'>;
+}
+declare module 'sinon/pkg/sinon-no-sourcemaps-3.2.1.js' {
+ declare module.exports: $Exports<'sinon/pkg/sinon-no-sourcemaps-3.2.1'>;
+}
+declare module 'sinon/pkg/sinon-no-sourcemaps.js' {
+ declare module.exports: $Exports<'sinon/pkg/sinon-no-sourcemaps'>;
+}
+declare module 'sinon/pkg/sinon.js' {
+ declare module.exports: $Exports<'sinon/pkg/sinon'>;
+}
diff --git a/package.json b/package.json
index 159de2f127..2f9571edc7 100644
--- a/package.json
+++ b/package.json
@@ -61,7 +61,8 @@
"npm-run-all": "^4.0.1",
"react-test-renderer": "^15.6.1",
"redux-mock-store": "^1.2.2",
- "rimraf": "^2.5.4"
+ "rimraf": "^2.5.4",
+ "sinon": "^3.2.1"
},
"scripts": {
"postinstall": "electron-builder install-app-deps",
diff --git a/test/components/Login.spec.js b/test/components/Login.spec.js
index 0f82102d4e..d0874d4b74 100644
--- a/test/components/Login.spec.js
+++ b/test/components/Login.spec.js
@@ -3,6 +3,8 @@
import { expect } from 'chai';
import React from 'react';
import { shallow } from 'enzyme';
+import sinon from 'sinon';
+
import Login from '../../app/components/Login';
import AccountInput from '../../app/components/AccountInput';
@@ -12,9 +14,9 @@ describe('components/Login', () => {
it('notifies on the first change after failure', () => {
- let cbCalled = false;
+ let callback = sinon.spy();
const props = {
- onFirstChangeAfterFailure: () => { cbCalled=true; },
+ onFirstChangeAfterFailure: callback,
};
const component = renderWithProps( props );
@@ -28,37 +30,104 @@ describe('components/Login', () => {
// Write something in the input field
setInputText(accountInput, 'foo');
- expect(cbCalled).to.be.true;
+ expect(callback.calledOnce).to.be.true;
// Reset the test state
- cbCalled = false;
+ callback.reset();
// Write some other thing in the input field
setInputText(accountInput, 'bar');
- expect(cbCalled).to.be.false;
+ expect(callback.calledOnce).to.be.false;
+ });
+
+ it('doesn\'t show the footer when logging in', () => {
+ const component = renderLoggingIn();
+
+ const footer = component.find('.login-footer');
+ expect(footer.hasClass('login-footer--invisible')).to.be.true;
+ });
+
+ it('shows the footer and account input when not logged in', () => {
+ const component = renderNotLoggedIn();
+
+ const footer = component.find('.login-footer');
+ expect(footer.hasClass('login-footer--invisible')).to.be.false;
+ expect(component.find(AccountInput).exists()).to.be.true;
});
+ it('doesn\'t show the footer nor account input when logged in', () => {
+ const component = renderLoggedIn();
+
+ const footer = component.find('.login-footer');
+ expect(footer.hasClass('login-footer--invisible')).to.be.true;
+ expect(component.find('.login-form__fields').hasClass('login-form__fields--invisible')).to.be.true;
+ });
+
+ it('logs in with the entered account number when clicking the login icon', (done) => {
+ const component = renderNotLoggedIn();
+ component.setProps({
+ onLogin: (an) => {
+ try {
+ expect(an).to.equal('12345');
+ done();
+ } catch (e) {
+ done(e);
+ }
+ },
+ });
+ const accountInput = component.find(AccountInput);
+ setInputText(accountInput, '12345');
+
+ component.find('.login-form__submit').simulate('click');
+ });
});
-function renderWithProps(customProps): ShallowWrapper {
- const defaultProps = {
- account: {accountNumber: null,
- paidUntil: null,
+const defaultAccount = {
+ accountNumber: null,
+ paidUntil: null,
+ status: 'none',
+ error: null,
+};
+
+const defaultProps = {
+ account: defaultAccount,
+ onLogin: () => {},
+ onSettings: () => {},
+ onChange: () => {},
+ onFirstChangeAfterFailure: () => {},
+ onExternalLink: () => {},
+};
+
+function renderLoggedIn() {
+ return renderWithProps({
+ account: Object.assign(defaultAccount, {
+ status: 'ok',
+ }),
+ });
+}
+
+function renderLoggingIn() {
+ return renderWithProps({
+ account: Object.assign(defaultAccount, {
+ status: 'logging in',
+ }),
+ });
+}
+
+function renderNotLoggedIn() {
+ return renderWithProps({
+ account: Object.assign(defaultAccount, {
status: 'none',
- error: null,
- },
- onLogin: () => {},
- onSettings: () => {},
- onChange: () => {},
- onFirstChangeAfterFailure: () => {},
- onExternalLink: () => {},
- };
- const props = Object.assign({}, defaultProps, customProps);
+ }),
+ });
+}
+function renderWithProps(customProps): ShallowWrapper {
+ const props = Object.assign({}, defaultProps, customProps);
return shallow( <Login { ...props } /> );
}
function setInputText(input: ShallowWrapper, text: string) {
- input.simulate('change', {target: {value: text}});
+ input.simulate('change', text);
}
diff --git a/yarn.lock b/yarn.lock
index 66543bbd91..7538f0d92f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1774,7 +1774,7 @@ dev-ip@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/dev-ip/-/dev-ip-1.0.1.tgz#a76a3ed1855be7a012bb8ac16cb80f3c00dc28f0"
-diff@3.2.0:
+diff@3.2.0, diff@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9"
@@ -2553,6 +2553,12 @@ form-data@~2.1.1:
combined-stream "^1.0.5"
mime-types "^2.1.12"
+formatio@1.2.0, formatio@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.2.0.tgz#f3b2167d9068c4698a8d51f4f760a39a54d818eb"
+ dependencies:
+ samsam "1.x"
+
formidable@1.0.x:
version "1.0.17"
resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.0.17.tgz#ef5491490f9433b705faa77249c99029ae348559"
@@ -3414,6 +3420,10 @@ jsx-ast-utils@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz#3867213e8dd79bf1e8f2300c0cfc1efb182c0df1"
+just-extend@^1.1.22:
+ version "1.1.22"
+ resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-1.1.22.tgz#3330af756cab6a542700c64b2e4e4aa062d52fff"
+
kdbush@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/kdbush/-/kdbush-1.0.1.tgz#3cbd03e9dead9c0f6f66ccdb96450e5cecc640e0"
@@ -3611,6 +3621,14 @@ lodash@^4.0.0, lodash@^4.15.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lod
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
+lolex@^1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.6.0.tgz#3a9a0283452a47d7439e72731b9e07d7386e49f6"
+
+lolex@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/lolex/-/lolex-2.1.2.tgz#2694b953c9ea4d013e5b8bfba891c991025b2629"
+
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
@@ -3863,6 +3881,10 @@ nan@^2.3.0:
version "2.6.2"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45"
+native-promise-only@^0.8.1:
+ version "0.8.1"
+ resolved "https://registry.yarnpkg.com/native-promise-only/-/native-promise-only-0.8.1.tgz#20a318c30cb45f71fe7adfbf7b21c99c1472ef11"
+
natives@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/natives/-/natives-1.1.0.tgz#e9ff841418a6b2ec7a495e939984f78f163e6e31"
@@ -3879,6 +3901,15 @@ netrc@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/netrc/-/netrc-0.1.4.tgz#6be94fcaca8d77ade0a9670dc460914c94472444"
+nise@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/nise/-/nise-1.0.1.tgz#0da92b10a854e97c0f496f6c2845a301280b3eef"
+ dependencies:
+ formatio "^1.2.0"
+ just-extend "^1.1.22"
+ lolex "^1.6.0"
+ path-to-regexp "^1.7.0"
+
node-emoji@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.5.1.tgz#fd918e412769bf8c448051238233840b2aff16a1"
@@ -4265,7 +4296,7 @@ path-parse@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1"
-path-to-regexp@^1.5.3:
+path-to-regexp@^1.5.3, path-to-regexp@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d"
dependencies:
@@ -4895,6 +4926,10 @@ safe-buffer@~5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7"
+samsam@1.x, samsam@^1.1.3:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.2.1.tgz#edd39093a3184370cb859243b2bdf255e7d8ea67"
+
sanitize-filename@^1.6.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.1.tgz#612da1c96473fa02dccda92dcd5b4ab164a6772a"
@@ -5015,6 +5050,20 @@ single-line-log@^1.1.2:
dependencies:
string-width "^1.0.1"
+sinon@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/sinon/-/sinon-3.2.1.tgz#d8adabd900730fd497788a027049c64b08be91c2"
+ dependencies:
+ diff "^3.1.0"
+ formatio "1.2.0"
+ lolex "^2.1.2"
+ native-promise-only "^0.8.1"
+ nise "^1.0.1"
+ path-to-regexp "^1.7.0"
+ samsam "^1.1.3"
+ text-encoding "0.6.4"
+ type-detect "^4.0.0"
+
slash@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
@@ -5361,6 +5410,10 @@ term-size@^0.1.0:
dependencies:
execa "^0.4.0"
+text-encoding@0.6.4:
+ version "0.6.4"
+ resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19"
+
text-table@~0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"