Skip to content

Commit

Permalink
Auth: Run codemods; use enzyme, remove mixin injection from tests (#1…
Browse files Browse the repository at this point in the history
…8472)

* Auth: Prettify login.jsx
* Auth: Run i18n codemod on login.jsx
* Auth: Run react-create-class codemod on login.jsx
* Auth: Prettify login.jsx
* Auth: Simplify login.jsx
* Auth: Named export for un-localized Login component

* Framework: Upgrade enzyme to 2.9.1

2.5.0 supports passing callbacks to `setState`, which we require for `client/auth/test/login`.
Furthermore, there's a fix in 2.6.0 that's required for this to actually work
(enzymejs/enzyme#490)
Not updating to >=3.0.0 yet since chai-expect isn't compatible (yet).
Changelog: https://github.com/airbnb/enzyme/blob/master/CHANGELOG.md#252-november-9-2016

* Auth: Simplify FormTextInput ref (focus)
* Auth: Use enzyme to test login.jsx
* Auth: Remove obsolete refs from login.jsx (These were only used by tests.)
* Auth: Move SelfHostedInstructions and LostPassword to separate files
  • Loading branch information
ockham authored Oct 5, 2017
1 parent 58d602e commit 70aec10
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 155 deletions.
127 changes: 60 additions & 67 deletions client/auth/login.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
/** @format */
/**
* External dependencies
*/
import ReactDom from 'react-dom';
import React from 'react';
import createReactClass from 'create-react-class';
import { localize } from 'i18n-calypso';
import LinkedStateMixin from 'react-addons-linked-state-mixin';
import Gridicon from 'gridicons';

Expand All @@ -22,43 +24,10 @@ import * as AuthActions from 'lib/oauth-store/actions';
import eventRecorder from 'me/event-recorder';
import WordPressLogo from 'components/wordpress-logo';
import AuthCodeButton from './auth-code-button';
import { addLocaleToWpcomUrl, getLocaleSlug } from 'lib/i18n-utils';
import SelfHostedInstructions from './self-hosted-instructions';
import LostPassword from './lost-password';

const LostPassword = React.createClass( {
render: function() {
const url = addLocaleToWpcomUrl( 'https://wordpress.com/wp-login.php?action=lostpassword', getLocaleSlug() );
return (
<p className="auth__lost-password">
<a href={ url } target="_blank" rel="noopener noreferrer">
{ this.translate( 'Lost your password?' ) }
</a>
</p>
);
}
} );

const SelfHostedInstructions = React.createClass( {

render: function() {
return (
<div className="auth__self-hosted-instructions">
<a href="#" onClick={ this.props.onClickClose } className="auth__self-hosted-instructions-close"><Gridicon icon="cross" size={ 24 } /></a>

<h2>{ this.translate( 'Add self-hosted site' ) }</h2>
<p>{ this.translate( 'By default when you sign into the WordPress.com app, you can edit blogs and sites hosted at WordPress.com' ) }</p>
<p>{ this.translate( 'If you\'d like to edit your self-hosted WordPress blog or site, you can do that by following these instructions:' ) }</p>

<ol>
<li><strong>{ this.translate( 'Install the Jetpack plugin.' ) }</strong><br /><a href="http://jetpack.me/install/">{ this.translate( 'Please follow these instructions to install Jetpack' ) }</a>.</li>
<li>{ this.translate( 'Connect Jetpack to WordPress.com.' ) }</li>
<li>{ this.translate( 'Now you can sign in to the app using the WordPress.com account Jetpack is connected to, and you can find your self-hosted site under the "My Sites" section.' ) }</li>
</ol>
</div>
);
}
} );

module.exports = React.createClass( {
export const Login = createReactClass( {
displayName: 'Auth',

mixins: [ LinkedStateMixin, eventRecorder ],
Expand All @@ -75,18 +44,21 @@ module.exports = React.createClass( {
this.setState( AuthStore.get() );
},

componentDidUpdate() {
focusInput( input ) {
if ( this.state.requires2fa && this.state.inProgress === false ) {
ReactDom.findDOMNode( this.refs.auth_code ).focus();
input.focus();
}
},

getInitialState: function() {
return Object.assign( {
login: '',
password: '',
auth_code: ''
}, AuthStore.get() );
return Object.assign(
{
login: '',
password: '',
auth_code: '',
},
AuthStore.get()
);
},

submitForm: function( event ) {
Expand Down Expand Up @@ -119,12 +91,13 @@ module.exports = React.createClass( {
return this.hasLoginDetails();
},

toggleSelfHostedInstructions: function () {
var isShowing = !this.state.showInstructions;
toggleSelfHostedInstructions: function() {
const isShowing = ! this.state.showInstructions;
this.setState( { showInstructions: isShowing } );
},

render: function() {
const { translate } = this.props;
const { requires2fa, inProgress, errorMessage, errorLevel, showInstructions } = this.state;

return (
Expand All @@ -134,59 +107,79 @@ module.exports = React.createClass( {
<form className="auth__form" onSubmit={ this.submitForm }>
<FormFieldset>
<div className="auth__input-wrapper">
<Gridicon icon="user"/>
<Gridicon icon="user" />
<FormTextInput
name="login"
ref="login"
disabled={ requires2fa || inProgress }
placeholder={ this.translate( 'Username or email address' ) }
placeholder={ translate( 'Username or email address' ) }
onFocus={ this.recordFocusEvent( 'Username or email address' ) }
valueLink={ this.linkState( 'login' ) } />
valueLink={ this.linkState( 'login' ) }
/>
</div>
<div className="auth__input-wrapper">
<Gridicon icon="lock" />
<FormPasswordInput
name="password"
ref="password"
disabled={ requires2fa || inProgress }
placeholder={ this.translate( 'Password' ) }
placeholder={ translate( 'Password' ) }
onFocus={ this.recordFocusEvent( 'Password' ) }
hideToggle={ requires2fa }
submitting={ inProgress }
valueLink={ this.linkState( 'password' ) } />
valueLink={ this.linkState( 'password' ) }
/>
</div>
{ requires2fa &&
{ requires2fa && (
<FormFieldset>
<FormTextInput
name="auth_code"
type="number"
ref="auth_code"
ref={ this.focusInput }
disabled={ inProgress }
placeholder={ this.translate( 'Verification code' ) }
placeholder={ translate( 'Verification code' ) }
onFocus={ this.recordFocusEvent( 'Verification code' ) }
valueLink={ this.linkState( 'auth_code' ) } />
valueLink={ this.linkState( 'auth_code' ) }
/>
</FormFieldset>
}
) }
</FormFieldset>
<FormButtonsBar>
<FormButton disabled={ ! this.canSubmitForm() } onClick={ this.recordClickEvent( 'Sign in' ) } >
{ requires2fa ? this.translate( 'Verify' ) : this.translate( 'Sign in' ) }
<FormButton
disabled={ ! this.canSubmitForm() }
onClick={ this.recordClickEvent( 'Sign in' ) }
>
{ requires2fa ? translate( 'Verify' ) : translate( 'Sign in' ) }
</FormButton>
</FormButtonsBar>
{ ! requires2fa && <LostPassword /> }
{ errorMessage && <Notice text={ errorMessage } status={ errorLevel } showDismiss={ false } /> }
{ requires2fa && <AuthCodeButton username={ this.state.login } password={ this.state.password } /> }
{ errorMessage && (
<Notice text={ errorMessage } status={ errorLevel } showDismiss={ false } />
) }
{ requires2fa && (
<AuthCodeButton username={ this.state.login } password={ this.state.password } />
) }
</form>
<a className="auth__help" target="_blank" rel="noopener noreferrer" title={ this.translate( 'Visit the WordPress.com support site for help' ) } href="https://en.support.wordpress.com/">
<a
className="auth__help"
target="_blank"
rel="noopener noreferrer"
title={ translate( 'Visit the WordPress.com support site for help' ) }
href="https://en.support.wordpress.com/"
>
<Gridicon icon="help" />
</a>
<div className="auth__links">
<a href="#" onClick={ this.toggleSelfHostedInstructions }>{ this.translate( 'Add self-hosted site' ) }</a>
<a href={ config( 'signup_url' ) }>{ this.translate( 'Create account' ) }</a>
<a href="#" onClick={ this.toggleSelfHostedInstructions }>
{ translate( 'Add self-hosted site' ) }
</a>
<a href={ config( 'signup_url' ) }>{ translate( 'Create account' ) }</a>
</div>
{ showInstructions && <SelfHostedInstructions onClickClose={ this.toggleSelfHostedInstructions } /> }
{ showInstructions && (
<SelfHostedInstructions onClickClose={ this.toggleSelfHostedInstructions } />
) }
</div>
</Main>
);
}
},
} );

export default localize( Login );
26 changes: 26 additions & 0 deletions client/auth/lost-password.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* External dependencies
*/
import React from 'react';
import { localize } from 'i18n-calypso';

/**
* Internal dependencies
*/
import { addLocaleToWpcomUrl, getLocaleSlug } from 'lib/i18n-utils';

const LostPassword = ( { translate } ) => {
const url = addLocaleToWpcomUrl(
'https://wordpress.com/wp-login.php?action=lostpassword',
getLocaleSlug()
);
return (
<p className="auth__lost-password">
<a href={ url } target="_blank" rel="noopener noreferrer">
{ translate( 'Lost your password?' ) }
</a>
</p>
);
};

export default localize( LostPassword );
45 changes: 45 additions & 0 deletions client/auth/self-hosted-instructions.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* External dependencies
*/
import React from 'react';
import { localize } from 'i18n-calypso';
import Gridicon from 'gridicons';

const SelfHostedInstructions = ( { onClickClose, translate } ) => (
<div className="auth__self-hosted-instructions">
<a href="#" onClick={ onClickClose } className="auth__self-hosted-instructions-close">
<Gridicon icon="cross" size={ 24 } />
</a>

<h2>{ translate( 'Add self-hosted site' ) }</h2>
<p>
{ translate(
'By default when you sign into the WordPress.com app, you can edit blogs and sites hosted at WordPress.com'
) }
</p>
<p>
{ translate(
"If you'd like to edit your self-hosted WordPress blog or site, you can do that by following these instructions:"
) }
</p>

<ol>
<li>
<strong>{ translate( 'Install the Jetpack plugin.' ) }</strong>
<br />
<a href="http://jetpack.me/install/">
{ translate( 'Please follow these instructions to install Jetpack' ) }
</a>.
</li>
<li>{ translate( 'Connect Jetpack to WordPress.com.' ) }</li>
<li>
{ translate(
'Now you can sign in to the app using the WordPress.com account Jetpack is connected to, ' +
'and you can find your self-hosted site under the "My Sites" section.'
) }
</li>
</ol>
</div>
);

export default localize( SelfHostedInstructions );
37 changes: 13 additions & 24 deletions client/auth/test/login.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@
/**
* External dependencies
*/
import React from 'react';
import { expect } from 'chai';
import { identity } from 'lodash';
import { shallow } from 'enzyme';
import { identity, noop } from 'lodash';

/**
* Internal dependencies
*/
import { login as loginStub } from 'lib/oauth-store/actions';
import { Login } from '../login.jsx';
import FormButton from 'components/forms/form-button';

jest.mock( 'lib/oauth-store/actions', () => ( {
login: require( 'sinon' ).stub(),
Expand All @@ -24,56 +28,41 @@ jest.mock( 'lib/analytics', () => ( {
} ) );

describe( 'LoginTest', function() {
let Login, page, React, ReactDom, ReactClass, TestUtils;

before( () => {
React = require( 'react' );
ReactDom = require( 'react-dom' );
ReactClass = require( 'react/lib/ReactClass' );
TestUtils = require( 'react-addons-test-utils' );
ReactClass.injection.injectMixin( { translate: identity } );
Login = require( '../login.jsx' );

const container = document.createElement( 'div' );
page = ReactDom.render( <Login />, container );
} );
const page = shallow( <Login translate={ identity } /> );

it( 'OTP is not present on first render', function( done ) {
page.setState( { requires2fa: false }, function() {
expect( page.refs.auth_code ).to.be.undefined;
expect( page.find( { name: 'auth_code' } ) ).to.have.length( 0 );
done();
} );
} );

it( 'cannot submit until login details entered', function( done ) {
const submit = TestUtils.findRenderedDOMComponentWithTag( page, 'button' );

expect( page.find( FormButton ).props().disabled ).to.be.true;
page.setState( { login: 'test', password: 'test', inProgress: false }, function() {
expect( submit.disabled ).to.be.false;
expect( page.find( FormButton ).props().disabled ).to.be.false;
done();
} );
} );

it( 'shows OTP box with valid login', function( done ) {
page.setState( { login: 'test', password: 'test', requires2fa: true }, function() {
expect( page.refs.auth_code ).to.not.be.undefined;
expect( page.find( { name: 'auth_code' } ) ).to.have.length( 1 );
done();
} );
} );

it( 'prevents change of login when asking for OTP', function( done ) {
page.setState( { login: 'test', password: 'test', requires2fa: true }, function() {
expect( page.refs.login.props.disabled ).to.be.true;
expect( page.refs.password.props.disabled ).to.be.true;
expect( page.find( { name: 'login' } ).props().disabled ).to.be.true;
expect( page.find( { name: 'password' } ).props().disabled ).to.be.true;
done();
} );
} );

it( 'submits login form', function( done ) {
const submit = TestUtils.findRenderedDOMComponentWithTag( page, 'form' );

page.setState( { login: 'user', password: 'pass', auth_code: 'otp' }, function() {
TestUtils.Simulate.submit( submit );
page.find( 'form' ).simulate( 'submit', { preventDefault: noop, stopPropagation: noop } );

expect( loginStub ).to.have.been.calledOnce;
expect( loginStub.calledWith( 'user', 'pass', 'otp' ) ).to.be.true;
Expand Down
Loading

0 comments on commit 70aec10

Please sign in to comment.