diff options
| author | Erik Larkö <erik@mullvad.net> | 2017-08-22 13:45:57 +0200 |
|---|---|---|
| committer | Erik Larkö <erik@mullvad.net> | 2017-08-22 13:45:57 +0200 |
| commit | da4f2903b14dbd09b088e44d6acf58d91d3df990 (patch) | |
| tree | cfbab4fd22712f82e5e778961fd444a86b351414 | |
| parent | 750d633006774154301fffaeb680b9eb4065c745 (diff) | |
| parent | 18ecd12a7472673c865f9bff777c3a776a92d1bb (diff) | |
| download | mullvadvpn-da4f2903b14dbd09b088e44d6acf58d91d3df990.tar.xz mullvadvpn-da4f2903b14dbd09b088e44d6acf58d91d3df990.zip | |
Merge branch 'login-refactor-and-tests'
| -rw-r--r-- | app/components/Login.js | 183 | ||||
| -rw-r--r-- | app/containers/LoginPage.js | 1 | ||||
| -rw-r--r-- | flow-typed/npm/chai_v4.x.x.js | 223 | ||||
| -rw-r--r-- | flow-typed/npm/sinon_vx.x.x.js | 333 | ||||
| -rw-r--r-- | package.json | 3 | ||||
| -rw-r--r-- | test/components/Login.spec.js | 107 | ||||
| -rw-r--r-- | yarn.lock | 57 |
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); } @@ -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" |
