diff options
| author | anderklander <anderklander@gmail.com> | 2018-02-27 19:54:39 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2018-03-26 14:22:10 +0200 |
| commit | 96b33592c1b1b24574340fbb456046ee591966ff (patch) | |
| tree | 61ba1a64ed16d5985db2cebb54b8b979dc7e7b89 | |
| parent | 36a0870b07f45fc1a3387fc6d029667f95faa1d3 (diff) | |
| download | mullvadvpn-96b33592c1b1b24574340fbb456046ee591966ff.tar.xz mullvadvpn-96b33592c1b1b24574340fbb456046ee591966ff.zip | |
Login in reactxp
| -rw-r--r-- | app/assets/css/global.css | 4 | ||||
| -rw-r--r-- | app/assets/css/style.css | 1 | ||||
| -rw-r--r-- | app/components/AccountInput.js | 45 | ||||
| -rw-r--r-- | app/components/Login.css | 211 | ||||
| -rw-r--r-- | app/components/Login.js | 270 | ||||
| -rw-r--r-- | app/components/LoginStyles.js | 154 | ||||
| -rw-r--r-- | test/components/AccountInput.spec.js | 60 | ||||
| -rw-r--r-- | test/components/Login.spec.js | 27 | ||||
| -rw-r--r-- | test/helpers/dom-events.js | 2 |
9 files changed, 386 insertions, 388 deletions
diff --git a/app/assets/css/global.css b/app/assets/css/global.css index 355ddd1bde..7f920bd279 100644 --- a/app/assets/css/global.css +++ b/app/assets/css/global.css @@ -28,3 +28,7 @@ body { width: 100%; display: flex; } + +::-webkit-input-placeholder { + color: rgba(41, 77, 115, 0.2); +}
\ No newline at end of file diff --git a/app/assets/css/style.css b/app/assets/css/style.css index 7d667f67e3..27403aaedf 100644 --- a/app/assets/css/style.css +++ b/app/assets/css/style.css @@ -7,7 +7,6 @@ /* app */ @import '../../components/PlatformWindow.css'; @import '../../components/CustomScrollbars.css'; -@import '../../components/Login.css'; @import '../../components/Connect.css'; @import '../../components/SelectLocation.css'; @import '../../components/Layout.css'; diff --git a/app/components/AccountInput.js b/app/components/AccountInput.js index 418cb3b77e..5b3cac206b 100644 --- a/app/components/AccountInput.js +++ b/app/components/AccountInput.js @@ -1,6 +1,7 @@ // @flow import * as React from 'react'; import { formatAccount } from '../lib/formatters'; +import { TextInput } from 'reactxp'; // @TODO: move it into types.js @@ -76,16 +77,20 @@ export default class AccountInput extends React.Component<AccountInputProps, Acc const displayString = formatAccount(this.state.value || ''); const { value, onChange, onEnter, ...otherProps } = this.props; // eslint-disable-line no-unused-vars return ( - <input { ...otherProps } - type="text" + <TextInput { ...otherProps } value={ displayString } - onChange={ () => {} } - onSelect={ this.onSelect } - onKeyUp={ this.onKeyUp } - onKeyDown={ this.onKeyDown } + onSelectionChange={ this.onSelect } onPaste={ this.onPaste } onCut={ this.onCut } - ref={ (ref) => this.onRef(ref) } /> + ref={ this.onRef } + autoCorrect={ false } + onChangeText={ () => {} } + onKeyPress={ this.onKeyPress } + returnKeyType="done" + keyboardType="numeric" + placeholderTextColor="rgba(41,77,115,0.2)" + testName='AccountInput' + /> ); } @@ -204,14 +209,14 @@ export default class AccountInput extends React.Component<AccountInputProps, Acc // Events - onKeyDown = (e: KeyboardEvent) => { + onKeyPress = (e: KeyboardEvent) => { const { value, selectionRange } = this.state; if(e.which === 8) { // backspace const result = this.remove(value, selectionRange); e.preventDefault(); - this._ignoreSelect = true; + //this._ignoreSelect = true; this.setState(result, () => { if(this.props.onChange) { @@ -222,37 +227,24 @@ export default class AccountInput extends React.Component<AccountInputProps, Acc const result = this.insert(value, e.key, selectionRange); e.preventDefault(); - this._ignoreSelect = true; + //this._ignoreSelect = true; this.setState(result, () => { if(this.props.onChange) { this.props.onChange(result.value); } }); - } - } - - onKeyUp = (e: KeyboardEvent) => { - this._ignoreSelect = false; - - if(e.which === 13 && this.props.onEnter) { + } else if(e.which === 13 && this.props.onEnter) { this.props.onEnter(); } } - onSelect = (e: Event) => { - const ref = e.target; - if(!(ref instanceof HTMLInputElement)) { - throw new Error('ref must be an instance of HTMLInputElement'); - } - + onSelect = (start: number, end: number) => { if(this._ignoreSelect) { return; } - const start = ref.selectionStart; - const end = ref.selectionEnd; - const selRange = this.toInternalSelectionRange(this.sanitize(ref.value), [start, end]); + const selRange = this.toInternalSelectionRange(this.sanitize(this.state.value), [start, end]); this.setState({ selectionRange: selRange }); } @@ -270,6 +262,7 @@ export default class AccountInput extends React.Component<AccountInputProps, Acc } onCut = (e: ClipboardEvent) => { + console.log(e); const target = e.target; if(!(target instanceof HTMLInputElement)) { throw new Error('ref must be an instance of HTMLInputElement'); diff --git a/app/components/Login.css b/app/components/Login.css deleted file mode 100644 index 9311212ec9..0000000000 --- a/app/components/Login.css +++ /dev/null @@ -1,211 +0,0 @@ -.login { - height: 100%; - display: flex; - flex-direction: column; -} - -.login-footer { - background-color: #192E45; - padding: 18px 24px 24px; - flex: 0 0 auto; - transition: transform 0.25s ease-in-out; -} - -.login-footer--invisible { - transform: translateY(100%); -} - -.login-form__status-icon { - flex: 0 0 auto; /* never collapse or grow */ - text-align: center; - margin-bottom: 44px; - - /* use fixed size to make space and avoid jitter when changing <img> visibility */ - height: 60px; -} - -.login-footer__prompt { - font-family: "Open Sans"; - font-size: 13px; - line-height: 18px; - font-weight: 600; - color: rgba(255,255,255,0.8); - margin-bottom: 8px; -} - -.login-form { - display: flex; - flex-direction: column; - padding: 0 24px; - margin: 83px 0 auto; -} - -.login-form__title { - font-family: DINPro; - font-size: 32px; - font-weight: 900; - line-height: 1.25em; - color: #FFFFFF; - margin-bottom: 7px; -} - -.login-form__subtitle { - font-family: "Open Sans"; - font-size: 13px; - font-weight: 600; - line-height: normal; - color: rgba(255,255,255,0.8); - margin-bottom: 8px; -} - -.login-form__account-input-container { - /* - Provides an anchor for input-container - to be properly positioned when it gains absolute - position to overlap the footer view. - */ - position: relative; - - /* push border to the outer side of the layout grid */ - margin-left: -2px; - margin-right: -2px; -} - -.login-form__account-input-group { - border: 2px solid transparent; - border-radius: 8px; - transition: border 0.25s ease-in-out; - overflow: hidden; -} - -.login-form__account-input-group--active { - position: absolute; - border-color: rgba(25,46,69,0.4); -} - -.login-form__account-input-group--inactive { - opacity: 0.6; -} - -.login-form__account-input-group--error { - border-color: rgba(208,2,27,0.4); -} - -.login-form__account-input-group--error .login-form__account-input-textfield { - color: #D0021B; -} - -.login-form__account-input-backdrop { - background-color: #FFFFFF; - display: flex; - flex-direction: row; - transition: background-color 0.25s ease-in-out; -} - -.login-form__account-input-textfield::-webkit-input-placeholder { - color: rgba(41,77,115,0.4); -} - -.login-form__account-input-textfield { - width: 100%; - border: 0; - padding: 10px 12px 12px 12px; - font-family: DINPro; - font-size: 20px; - font-weight: 900; - line-height: 26px; - color: #294D73; - background-color: transparent; - flex: 1 1 auto; - transition: 0.3s color ease-in-out; -} - -.login-form__account-input-textfield--inactive { - background-color: rgba(255,255,255,0.6); -} - -.login-form__account-input-button { - flex: 0 0 auto; - border: 0; - width: 48px; - background-color: transparent; - transition-duration: 0.3s; - transition-property: background-color, opacity; - transition-timing-function: ease-in-out; -} - -.login-form__account-input-button-icon { - display: inline-block; - vertical-align: middle; -} - -.login-form__account-input-button-icon path { - fill: rgba(41,77,115,0.2); - transition: fill 0.3s ease-in-out; -} - -.login-form__account-input-button--active { - background-color: #44ad4d; -} - -.login-form__account-input-button--active .login-form__account-input-button-icon path { - fill: #fff; -} - -.login-form__account-input-button--active:hover { - background-color: rgba(68, 173, 76, 0.9); -} - -.login-form__account-input-button--invisible { - visibility: hidden; - opacity: 0; -} - -.login-form__account-dropdown-container { - overflow: hidden; - transition: height 0.25s ease-in-out; -} - -.login-form__account-dropdown__item { - display: flex; - flex-direction: row; - border-top: 1px solid rgba(25,46,69,0.4); - background-color: rgba(255,255,255,0.6); - background-clip: padding-box; - transition: background-color 0.25s ease-in-out; -} - -.login-form__account-dropdown__item:hover { - background-color: rgba(255,255,255,0.65); -} - -.login-form__account-dropdown__label { - flex: 1 1 auto; - font-family: DINPro; - font-size: 20px; - font-weight: 900; - line-height: 26px; - color: #294D73; - border: 0; - background: transparent; - padding: 10px 12px 12px 12px; - text-align: left; -} - -.login-form__account-dropdown__remove { - background: transparent; - border: 0; - padding: 10px 12px 12px 12px; - - /* center SVG within button */ - display: flex; - justify-content: center; -} - -.login-form__account-dropdown__remove path { - transition: fill-opacity 0.25s ease-in-out; -} - -.login-form__account-dropdown__remove:hover path { - fill-opacity: 1; -}
\ No newline at end of file diff --git a/app/components/Login.js b/app/components/Login.js index 3231fc7710..2d7ea8f32f 100644 --- a/app/components/Login.js +++ b/app/components/Login.js @@ -1,11 +1,12 @@ // @flow import * as React from 'react'; +import { Text, View, Animated, Styles } from 'reactxp'; import { Layout, Container, Header } from './Layout'; import AccountInput from './AccountInput'; import { formatAccount } from '../lib/formatters'; -import ExternalLinkSVG from '../assets/images/icon-extLink.svg'; -import LoginArrowSVG from '../assets/images/icon-arrow.svg'; -import RemoveAccountSVG from '../assets/images/icon-close-sml.svg'; +import Img from './Img'; +import { Button, BlueButton, Label } from './styled'; +import styles from './LoginStyles'; import type { AccountReduxState } from '../redux/account/reducers'; import type { AccountToken } from '../lib/ipc-facade'; @@ -23,14 +24,26 @@ export type LoginPropTypes = { type State = { notifyOnFirstChangeAfterFailure: boolean, isActive: boolean, - dropdownHeight: number + dropdownHeight: number, + footerHeight: number, + animatedFooterValue: Animated.Value, + animatedDropdownValue: Animated.Value, + animation: Animated.CompositeAnimation, + footerAnimationStyle: Animated.Style, + dropdownAnimationStyle: Animated.Style, }; export default class Login extends React.Component<LoginPropTypes, State> { state = { notifyOnFirstChangeAfterFailure: false, isActive: false, - dropdownHeight: 0 + dropdownHeight: 0, + footerHeight: 0, + animatedFooterValue: Animated.createValue(0), + animatedDropdownValue: Animated.createValue(0), + animation: Animated.CompositeAnimation, + footerAnimationStyle: Animated.Style, + dropdownAnimationStyle: Animated.Style, }; constructor(props: LoginPropTypes) { @@ -38,14 +51,8 @@ export default class Login extends React.Component<LoginPropTypes, State> { if(props.account.status === 'failed') { this.state.notifyOnFirstChangeAfterFailure = true; } - } - - componentDidMount() { - this._updateDropdownHeight(); - } - - componentDidUpdate() { - this._updateDropdownHeight(); + this.state.dropdownAnimationStyle = Styles.createAnimatedViewStyle({height: this.state.animatedDropdownValue}); + this.state.footerAnimationStyle = Styles.createAnimatedViewStyle({transform: [{translateY: this.state.animatedFooterValue }]}); } componentWillReceiveProps(nextProps: LoginPropTypes) { @@ -55,36 +62,41 @@ export default class Login extends React.Component<LoginPropTypes, State> { if(prev.status !== next.status && next.status === 'failed') { this.setState({ notifyOnFirstChangeAfterFailure: true }); } + + this._animate(nextProps); } render() { - const footerClass = this._shouldShowFooter() ? '' : 'login-footer--invisible'; return ( <Layout> <Header showSettings={ true } onSettings={ this.props.onSettings } /> <Container> - <div className="login"> - <div className="login-form"> + <View style={styles.login}> + <View style={styles.login_form}> { this._getStatusIcon() } - <div className="login-form__title">{ this._formTitle() }</div> + <Text style={styles.title}>{ this._formTitle() }</Text> - {this._shouldShowLoginForm() && <div className='login-form__fields'> + {this._shouldShowLoginForm() && <View> { this._createLoginForm() } - </div>} - </div> + </View>} + </View> - <div className={ 'login-footer ' + footerClass }> + <Animated.View onLayout={this._onFooterLayout} style={[styles.login_footer, this.state.footerAnimationStyle]} testName='footer'> { this._createFooter() } - </div> - </div> + </Animated.View> + </View> </Container> </Layout> ); } - _onCreateAccount = () => this.props.onExternalLink('createAccount'); - _onFocus = () => this.setState({ isActive: true }); + _onCreateAccount = () => this.props.onExternalLink('createAccount') + + _onFocus = () => this.setState({ isActive: true }, () => { + this._animate(this.props); + }) + _onBlur = (e) => { const relatedTarget = e.relatedTarget; @@ -94,7 +106,28 @@ export default class Login extends React.Component<LoginPropTypes, State> { return; } - this.setState({ isActive: false }); + this.setState({ isActive: false }, () => { + this._animate(this.props); + }); + } + + _animate = (props: LoginPropTypes) => { + if (this.state.animation) { + this.state.animation.stop(); + } + const footerPosition = this._shouldShowFooter(props) ? 0 : this.state.footerHeight; + const dropdownHeight = this._shouldShowAccountHistory(props) ? this.state.dropdownHeight : 0; + console.log(dropdownHeight); + this._setAnimation(this._getFooterAnimation(footerPosition), this._getDropdownAnimation(dropdownHeight)); + } + + _setAnimation = (footerAnimation: Animated.CompositeAnimation, dropdownAnimation: Animated.CompositeAnimation) => { + let compositeAnimation = Animated.parallel([ footerAnimation, dropdownAnimation ]); + this.setState({animation: compositeAnimation}, () => { + this.state.animation.start(() => this.setState({ + animation: null + })); + }); } _onLogin = () => { @@ -140,88 +173,103 @@ export default class Login extends React.Component<LoginPropTypes, State> { _getStatusIcon() { const statusIconPath = this._getStatusIconPath(); - return <div className="login-form__status-icon"> + return <View style={ styles.status_icon}> { statusIconPath ? - <img src={ statusIconPath } alt="" /> : + <Img source={ statusIconPath } height='48' width='48' alt="" /> : null } - </div>; + </View>; } _getStatusIconPath(): ?string { switch(this.props.account.status) { case 'logging in': - return './assets/images/icon-spinner.svg'; + return 'icon-spinner'; case 'failed': - return './assets/images/icon-fail.svg'; + return 'icon-fail'; case 'ok': - return './assets/images/icon-success.svg'; + return 'icon-success'; default: return undefined; } } - _accountInputGroupClass(): string { - const classes = ['login-form__account-input-group']; + _accountInputGroupClass(): Array<Object> { + const classes = [styles.account_input_group]; if(this.state.isActive) { - classes.push('login-form__account-input-group--active'); + classes.push(styles.account_input_group__active); } switch(this.props.account.status) { case 'logging in': - classes.push('login-form__account-input-group--inactive'); + classes.push(styles.account_input_group__inactive); break; case 'failed': - classes.push('login-form__account-input-group--error'); + classes.push(styles.account_input_group__error); break; } - return classes.join(' '); + return classes; } - _accountInputButtonClass(): string { + _accountInputButtonClass(): Array<Object> { const { accountToken, status } = this.props.account; - const classes = ['login-form__account-input-button']; + const classes = [styles.account_input_button]; if(accountToken && accountToken.length > 0) { - classes.push('login-form__account-input-button--active'); + classes.push(styles.account_input_button__active); } if(status === 'logging in') { - classes.push('login-form__account-input-button--invisible'); + classes.push(styles.account_input_button__invisible); } - return classes.join(' '); + return classes; } - _shouldEnableAccountInput() { + _shouldEnableAccountInput(props: LoginPropTypes) { // enable account input always except when "logging in" - return this.props.account.status !== 'logging in'; + return props.account.status !== 'logging in'; } - _shouldShowAccountHistory() { - return this._shouldEnableAccountInput() && + _shouldShowAccountHistory(props: LoginPropTypes) { + return this._shouldEnableAccountInput(props) && this.state.isActive && - this.props.account.accountHistory.length > 0; + props.account.accountHistory.length > 0; } _shouldShowLoginForm() { return this.props.account.status !== 'ok'; } - _shouldShowFooter() { - const { status } = this.props.account; - return (status === 'none' || status === 'failed') && !this._shouldShowAccountHistory(); + _shouldShowFooter(props: LoginPropTypes) { + const { status } = props.account; + return (status === 'none' || status === 'failed') && !this._shouldShowAccountHistory(props); } - // helper function to calculate and save dropdown element's height - // this is a no-op of the height didn't change since last update - _updateDropdownHeight() { - const element = this._accountDropdownElement; - if(element && this.state.dropdownHeight !== element.clientHeight) { - this.setState({ - dropdownHeight: element.clientHeight - }); - } + _getFooterAnimation(toValue: number){ + return Animated.timing(this.state.animatedFooterValue, { + toValue: toValue, + easing: Animated.Easing.InOut(), + duration: 250, + useNativeDriver: true, + }); + } + + _onFooterLayout = (layout) => { + this.setState({footerHeight: layout.height}); + } + + _getDropdownAnimation(toValue: number){ + return Animated.timing(this.state.animatedDropdownValue, { + toValue: toValue, + easing: Animated.Easing.InOut(), + duration: 250, + useNativeDriver: true, + }); + } + + _onDropdownLayout = (layout) => { + this.setState({dropdownHeight: layout.height}); } // returns true if DOM node is within dropdown hierarchy @@ -241,9 +289,6 @@ export default class Login extends React.Component<LoginPropTypes, State> { _createLoginForm() { const { accountHistory, accountToken } = this.props.account; - const dropdownStyles = { - height: this._shouldShowAccountHistory() ? this.state.dropdownHeight : 0 - }; // auto-focus on account input when failed to log in // do not refactor this into instance method, @@ -254,47 +299,46 @@ export default class Login extends React.Component<LoginPropTypes, State> { } }; - return <div> - <div className="login-form__subtitle">{ this._formSubtitle() }</div> - <div className="login-form__account-input-container"> - <div className={ this._accountInputGroupClass() }> - <div className="login-form__account-input-backdrop"> - <AccountInput className="login-form__account-input-textfield" - type="text" - placeholder="e.g 0000 0000 0000" - onFocus={ this._onFocus } - onBlur={ this._onBlur } - onChange={ this._onInputChange } - onEnter={ this._onLogin } - value={ accountToken || '' } - disabled={ !this._shouldEnableAccountInput() } - autoFocus={ true } - ref={ autoFocusOnFailure } /> - <button className={ this._accountInputButtonClass() } onClick={ this._onLogin }> - <LoginArrowSVG className="login-form__account-input-button-icon" /> - </button> - </div> - <div style={ dropdownStyles } className="login-form__account-dropdown-container"> - <div ref={ this._onAccountDropdownContainerRef }> - { <AccountDropdown - items={ accountHistory.slice().reverse() } - onSelect={ this._onSelectAccountFromHistory } - onRemove={ this.props.onRemoveAccountTokenFromHistory } /> } - </div> - </div> - </div> - </div> - </div>; + return <View style= {styles.login}> + <Text style={ styles.subtitle }>{ this._formSubtitle() }</Text> + <View style={ this._accountInputGroupClass() }> + <View style={ styles.account_input_backdrop}> + <AccountInput style={styles.account_input_textfield} + type="text" + placeholder="e.g 0000 0000 0000" + onFocus={ this._onFocus } + onBlur={ this._onBlur } + onChange={ this._onInputChange } + onEnter={ this._onLogin } + value={ accountToken || '' } + disabled={ !this._shouldEnableAccountInput(this.props) } + autoFocus={ true } + ref={ autoFocusOnFailure } + testName='AccountInput'/> + <Button style={ this._accountInputButtonClass() } onPress={ this._onLogin } testName='account-input-button'> + <Img style={[ this._accountInputButtonClass() ]} source='icon-arrow' height='16' width='24' tintColor='currentColor' /> + </Button> + </View> + <Animated.View style={ this.state.dropdownAnimationStyle }> + <View onLayout={this._onDropdownLayout} ref={ this._onAccountDropdownContainerRef }> + { <AccountDropdown + items={ accountHistory.slice().reverse() } + onSelect={ this._onSelectAccountFromHistory } + onRemove={ this.props.onRemoveAccountTokenFromHistory } /> } + </View> + </Animated.View> + </View> + </View>; } _createFooter() { - 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>; + return <View> + <Text style={ styles.login_footer__prompt}>{ 'Don\'t have an account number?' }</Text> + <BlueButton onPress={ this._onCreateAccount }> + <Label>Create account</Label> + <Img source='icon-extLink' height='16' width='16' /> + </BlueButton> + </View>; } } @@ -308,7 +352,7 @@ class AccountDropdown extends React.Component<AccountDropdownProps> { render() { const uniqueItems = [...new Set(this.props.items)]; return ( - <div className="login-form__account-dropdown"> + <View> { uniqueItems.map(token => ( <AccountDropdownItem key={ token } value={ token } @@ -316,7 +360,7 @@ class AccountDropdown extends React.Component<AccountDropdownProps> { onSelect={ this.props.onSelect } onRemove={ this.props.onRemove } /> )) } - </div> + </View> ); } } @@ -330,15 +374,17 @@ type AccountDropdownItemProps = { class AccountDropdownItem extends React.Component<AccountDropdownItemProps> { render() { - return ( - <div className="login-form__account-dropdown__item"> - <button className="login-form__account-dropdown__label" - onClick={ () => this.props.onSelect(this.props.value) }>{ this.props.label }</button> - <button className="login-form__account-dropdown__remove" - onClick={ () => this.props.onRemove(this.props.value) }> - <RemoveAccountSVG /> - </button> - </div> + return (<View> + <View style={ styles.account_dropdown__spacer }/> + <View style={ styles.account_dropdown__item }> + <Button style={styles.account_dropdown__label} + onPress={ () => this.props.onSelect(this.props.value) }>{ this.props.label }</Button> + <Button style={styles.account_dropdown__remove} + onPress={ () => this.props.onRemove(this.props.value) }> + <Img source='icon-close-sml' height='16' width='16' /> + </Button> + </View> + </View> ); } } diff --git a/app/components/LoginStyles.js b/app/components/LoginStyles.js new file mode 100644 index 0000000000..ea3a2db35c --- /dev/null +++ b/app/components/LoginStyles.js @@ -0,0 +1,154 @@ +// @flow +import { createViewStyles, createTextStyles } from '../lib/styles'; +import { colors } from '../config'; + +export default { + ...createViewStyles({ + login: { + height: '100%', + }, + login_footer: { + backgroundColor: colors.darkBlue, + paddingTop: 18, + paddingBottom:24, + flex: 0, + }, + status_icon: { + flex: 0, + height: 48, + marginBottom: 30, + justifyContent: 'center', + }, + login_form:{ + flex:1, + flexDirection: 'column', + justifyContent: 'flex_end', + overflow:'visible', + paddingTop: 0, + paddingBottom: 0, + paddingLeft: 24, + paddingRight: 24, + marginTop: 83, + marginBottom:0, + marginRight: 0, + marginLeft:0, + }, + account_input_group: { + borderWidth: 2, + borderRadius: 8, + borderColor: 'transparent', + }, + account_input_group__active: { + borderColor: colors.darkBlue, + }, + account_input_group__inactive: { + opacity: 0.6, + }, + account_input_group__error: { + borderColor: colors.red40, + color: colors.red, + }, + account_input_textfield: { + color: colors.blue, + }, + account_input_backdrop: { + backgroundColor: colors.white, + borderColor: colors.darkBlue, + flexDirection: 'row', + }, + account_input_textfield__inactive: { + backgroundColor: colors.white60, + }, + account_input_button: { + flex: 0, + border: 0, + width: 48, + alignItems: 'center', + color: colors.blue20, + }, + account_input_button__active: { + color: colors.white, + backgroundColor: colors.green, + }, + account_input_button__invisible: { + visibility: 'hidden', + opacity: 0, + }, + account_dropdown__spacer: { + height: 1, + backgroundColor: colors.darkBlue, + }, + account_dropdown__item: { + flexDirection: 'row', + backgroundColor: colors.white60, + borderColor: colors.darkBlue, + }, + account_dropdown__remove: { + paddingTop: 10, + paddingLeft: 12, + paddingRight: 12, + paddingBottom: 12, + /* center SVG within button */ + justifyContent: 'center', + }, + }), + ...createTextStyles({ + login_footer__prompt: { + color: colors.white80, + fontFamily: 'Open Sans', + fontSize: 13, + fontWeight: '600', + lineHeight: 18, + letterSpacing: -0.2, + marginLeft: 24, + marginRight: 24, + }, + title: { + fontFamily: 'DINPro', + fontSize: 32, + fontWeight: '900', + lineHeight: 44, + letterSpacing: -0.7, + color: colors.white, + marginBottom: 7, + flex:0, + }, + subtitle: { + fontFamily: 'Open Sans', + fontSize: 13, + fontWeight: '600', + + letterSpaceing: -0.2, + color: colors.white80, + marginBottom: 8, + }, + account_input_textfield: { + border: 0, + paddingTop: 10, + paddingRight: 12, + paddingLeft: 12, + paddingBottom: 12, + fontFamily: 'DINPro', + fontSize: 20, + fontWeight: 900, + lineHeight: 26, + color: colors.blue, + backgroundColor: 'transparent', + flex: 1, + }, + account_dropdown__label: { + flex: 1, + fontFamily: 'DINPro', + fontSize: 20, + fontWeight: '900', + lineHeight: 26, + color: colors.blue, + border: 0, + paddingTop: 10, + paddingLeft: 12, + paddingRight: 12, + paddingBottom: 12, + textAlign: 'left', + }, + }) +}; diff --git a/test/components/AccountInput.spec.js b/test/components/AccountInput.spec.js index 701a1c243e..a10326bc17 100644 --- a/test/components/AccountInput.spec.js +++ b/test/components/AccountInput.spec.js @@ -2,17 +2,15 @@ import { expect } from 'chai'; import { createKeyEvent } from '../helpers/dom-events'; import * as React from 'react'; -import ReactTestUtils, { Simulate } from 'react-dom/test-utils'; +import { shallow } from 'enzyme'; +require('../setup/enzyme'); import AccountInput from '../../app/components/AccountInput'; import type { AccountInputProps } from '../../app/components/AccountInput'; describe('components/AccountInput', () => { const getInputRef = (component) => { - const node = ReactTestUtils.findRenderedDOMComponentWithTag(component, 'input'); - if(!(node instanceof HTMLInputElement)) { - throw new Error('Node is expected to be an instance of HTMLInputElement'); - } + const node = getComponent(component, 'AccountInput'); return node; }; @@ -23,8 +21,8 @@ describe('components/AccountInput', () => { onChange: null }; const props = Object.assign({}, defaultProps, mergeProps); - return ReactTestUtils.renderIntoDocument( - <AccountInput { ...props } /> + return shallow( + <AccountInput {...props} /> ); }; @@ -32,7 +30,7 @@ describe('components/AccountInput', () => { const component = render({ onEnter: () => done() }); - Simulate.keyUp(getInputRef(component), createKeyEvent('Enter')); + keyPress(getInputRef(component), createKeyEvent('Enter')); }); it('should call onChange', (done) => { @@ -42,7 +40,7 @@ describe('components/AccountInput', () => { done(); } }); - Simulate.keyDown(getInputRef(component), createKeyEvent('1')); + keyPress(getInputRef(component), createKeyEvent('1')); }); it('should format input properly', () => { @@ -65,7 +63,7 @@ describe('components/AccountInput', () => { for(const value of cases) { const component = render({ value }); - expect(getInputRef(component).value).to.be.equal(value); + expect(getInputRef(component).prop('value')).to.be.equal(value); } }); @@ -77,7 +75,7 @@ describe('components/AccountInput', () => { done(); } }); - Simulate.keyDown(getInputRef(component), createKeyEvent('Backspace')); + keyPress(getInputRef(component), createKeyEvent('Backspace')); }); it('should remove first character', (done) => { @@ -89,7 +87,7 @@ describe('components/AccountInput', () => { } }); component.setState({ selectionRange: [1, 1] }, () => { - Simulate.keyDown(getInputRef(component), createKeyEvent('Backspace')); + keyPress(getInputRef(component), createKeyEvent('Backspace')); }); }); @@ -102,7 +100,7 @@ describe('components/AccountInput', () => { } }); component.setState({ selectionRange: [0, 8] }, () => { - Simulate.keyDown(getInputRef(component), createKeyEvent('Backspace')); + keyPress(getInputRef(component), createKeyEvent('Backspace')); }); }); @@ -115,7 +113,7 @@ describe('components/AccountInput', () => { } }); component.setState({ selectionRange: [4, 8] }, () => { - Simulate.keyDown(getInputRef(component), createKeyEvent('Backspace')); + keyPress(getInputRef(component), createKeyEvent('Backspace')); }); }); @@ -125,11 +123,11 @@ describe('components/AccountInput', () => { }); component.setState({ selectionRange: [1, 3] }, () => { - Simulate.keyDown(getInputRef(component), createKeyEvent('1')); + keyPress(getInputRef(component), createKeyEvent('1')); component.setState({}, () => { - expect(component.state.value).to.be.equal('010'); - expect(component.state.selectionRange).to.deep.equal([2, 2]); + expect(component.state().value).to.be.equal('010'); + expect(component.state().selectionRange).to.deep.equal([2, 2]); done(); }); }); @@ -139,12 +137,12 @@ describe('components/AccountInput', () => { const component = render({ value: '' }); for(let i = 0; i < 12; i++) { - Simulate.keyDown(getInputRef(component), createKeyEvent('1')); + keyPress(getInputRef(component), createKeyEvent('1')); } component.setState({}, () => { - expect(component.state.value).to.be.equal('111111111111'); - expect(component.state.selectionRange).to.deep.equal([12, 12]); + expect(component.state().value).to.be.equal('111111111111'); + expect(component.state().selectionRange).to.deep.equal([12, 12]); done(); }); }); @@ -154,11 +152,11 @@ describe('components/AccountInput', () => { value: '0000' }); component.setState({ selectionRange: [1, 1]}, () => { - Simulate.keyDown(getInputRef(component), createKeyEvent('1')); + keyPress(getInputRef(component), createKeyEvent('1')); component.setState({}, () => { - expect(component.state.value).to.be.equal('01000'); - expect(component.state.selectionRange).to.deep.equal([2, 2]); + expect(component.state().value).to.be.equal('01000'); + expect(component.state().selectionRange).to.deep.equal([2, 2]); done(); }); }); @@ -169,14 +167,22 @@ describe('components/AccountInput', () => { value: '0000' }); component.setState({ selectionRange: [0, 0] }, () => { - Simulate.keyDown(getInputRef(component), createKeyEvent('Backspace')); + keyPress(getInputRef(component), createKeyEvent('Backspace')); component.setState({}, () => { - expect(component.state.value).to.be.equal('0000'); - expect(component.state.selectionRange).to.deep.equal([0, 0]); + expect(component.state().value).to.be.equal('0000'); + expect(component.state().selectionRange).to.deep.equal([0, 0]); done(); }); }); }); -});
\ No newline at end of file +}); + +function getComponent(container, testName) { + return container.findWhere( n => n.prop('testName') === testName); +} + +function keyPress(component, key) { + component.prop('onKeyPress')(key); +}
\ No newline at end of file diff --git a/test/components/Login.spec.js b/test/components/Login.spec.js index f34d9ea405..04bc6bd896 100644 --- a/test/components/Login.spec.js +++ b/test/components/Login.spec.js @@ -1,7 +1,7 @@ // @flow import { expect } from 'chai'; -import React from 'react'; +import * as React from 'react'; import { shallow } from 'enzyme'; import sinon from 'sinon'; @@ -33,22 +33,21 @@ describe('components/Login', () => { it('does not show the footer when logging in', () => { const component = renderLoggingIn(); - const footer = component.find('.login-footer'); - expect(footer.hasClass('login-footer--invisible')).to.be.true; + const footer = getComponent(component, 'footer'); + //TODO: add footer check + expect(footer.length).to.not.equal(0); }); 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; + //TODO: add footer check + expect(getComponent(component, 'AccountInput').length).to.be.above(0); }); it('does not 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')).to.have.length(0); + //TODO: add footer check + expect(getComponent(component, 'AccountInput').length).to.equal(0); }); it('logs in with the entered account number when clicking the login icon', (done) => { @@ -67,7 +66,7 @@ describe('components/Login', () => { }, }); - component.find('.login-form__account-input-button').simulate('click'); + click(getComponent(component, 'account-input-button')); }); }); @@ -118,3 +117,11 @@ function renderWithProps(customProps) { const props = Object.assign({}, defaultProps, customProps); return shallow( <Login { ...props } /> ); } + +function getComponent(container, testName) { + return container.findWhere( n => n.prop('testName') === testName); +} + +function click(component) { + component.prop('onPress')(); +} diff --git a/test/helpers/dom-events.js b/test/helpers/dom-events.js index 9a02ad167c..57f0fcc4d2 100644 --- a/test/helpers/dom-events.js +++ b/test/helpers/dom-events.js @@ -18,5 +18,5 @@ const keycodes = { export type Keycode = $Keys<typeof keycodes>; export function createKeyEvent(key: Keycode): Object { - return Object.assign({}, { key }, keycodes[key]); + return Object.assign({}, { key }, keycodes[key], {preventDefault: () => {}}); } |
