Fixes login page after pf-react added LoginPage and LoginForm components

This commit is contained in:
mabashian
2018-11-29 16:50:17 -05:00
parent 7d2bc1c766
commit 58d6e586cd
2 changed files with 98 additions and 131 deletions

View File

@@ -2,33 +2,31 @@ import React from 'react';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { mount, shallow } from 'enzyme'; import { mount, shallow } from 'enzyme';
import { asyncFlush } from '../../jest.setup'; import { asyncFlush } from '../../jest.setup';
import LoginPage from '../../src/pages/Login'; import AtLogin from '../../src/pages/Login';
import api from '../../src/api'; import api from '../../src/api';
const LOGIN_ERROR_MESSAGE = 'Invalid username or password. Please try again.'; describe('<Login />', () => {
describe('<LoginPage />', () => {
let loginWrapper; let loginWrapper;
let atLogin;
let loginPage; let loginPage;
let loginForm; let loginForm;
let usernameInput; let usernameInput;
let passwordInput; let passwordInput;
let errorTextArea;
let submitButton; let submitButton;
let defaultLogo; let loginHeaderLogo;
const findChildren = () => { const findChildren = () => {
atLogin = loginWrapper.find('AtLogin');
loginPage = loginWrapper.find('LoginPage'); loginPage = loginWrapper.find('LoginPage');
loginForm = loginWrapper.find('form.pf-c-form'); loginForm = loginWrapper.find('LoginForm');
usernameInput = loginWrapper.find('.pf-c-form__group#username TextInput'); usernameInput = loginWrapper.find('input#pf-login-username-id');
passwordInput = loginWrapper.find('.pf-c-form__group#password TextInput'); passwordInput = loginWrapper.find('input#pf-login-password-id');
errorTextArea = loginWrapper.find('.pf-c-form__helper-text.pf-m-error');
submitButton = loginWrapper.find('Button[type="submit"]'); submitButton = loginWrapper.find('Button[type="submit"]');
defaultLogo = loginWrapper.find('TowerLogo'); loginHeaderLogo = loginWrapper.find('LoginHeaderBrand Brand');
}; };
beforeEach(() => { beforeEach(() => {
loginWrapper = mount(<MemoryRouter><LoginPage /></MemoryRouter>); loginWrapper = mount(<MemoryRouter><AtLogin /></MemoryRouter>);
findChildren(); findChildren();
}); });
@@ -38,62 +36,78 @@ describe('<LoginPage />', () => {
test('initially renders without crashing', () => { test('initially renders without crashing', () => {
expect(loginWrapper.length).toBe(1); expect(loginWrapper.length).toBe(1);
expect(loginPage.length).toBe(1);
expect(loginForm.length).toBe(1); expect(loginForm.length).toBe(1);
expect(usernameInput.length).toBe(1); expect(usernameInput.length).toBe(1);
expect(usernameInput.props().value).toBe(''); expect(usernameInput.props().value).toBe('');
expect(passwordInput.length).toBe(1); expect(passwordInput.length).toBe(1);
expect(passwordInput.props().value).toBe(''); expect(passwordInput.props().value).toBe('');
expect(errorTextArea.length).toBe(1); expect(atLogin.state().isValidPassword).toBe(true);
expect(loginPage.state().error).toBe('');
expect(submitButton.length).toBe(1); expect(submitButton.length).toBe(1);
expect(submitButton.props().isDisabled).toBe(false); expect(submitButton.props().isDisabled).toBe(false);
expect(defaultLogo.length).toBe(1); expect(loginHeaderLogo.length).toBe(1);
}); });
test('custom logo renders Brand component', () => { test('custom logo renders Brand component with correct src and alt', () => {
loginWrapper = mount(<MemoryRouter><LoginPage logo="hey" /></MemoryRouter>); loginWrapper = mount(<MemoryRouter><AtLogin logo="images/foo.jpg" alt="Foo Application" /></MemoryRouter>);
findChildren(); findChildren();
expect(defaultLogo.length).toBe(0); expect(loginHeaderLogo.length).toBe(1);
expect(loginHeaderLogo.props().src).toBe('data:image/jpeg;images/foo.jpg');
expect(loginHeaderLogo.props().alt).toBe('Foo Application');
});
test('default logo renders Brand component with correct src and alt', () => {
loginWrapper = mount(<MemoryRouter><AtLogin /></MemoryRouter>);
findChildren();
expect(loginHeaderLogo.length).toBe(1);
expect(loginHeaderLogo.props().src).toBe('tower-logo-header.svg');
expect(loginHeaderLogo.props().alt).toBe('Ansible Tower');
}); });
test('state maps to un/pw input value props', () => { test('state maps to un/pw input value props', () => {
loginPage.setState({ username: 'un', password: 'pw' }); atLogin.setState({ username: 'un', password: 'pw' });
expect(loginPage.state().username).toBe('un'); expect(atLogin.state().username).toBe('un');
expect(loginPage.state().password).toBe('pw'); expect(atLogin.state().password).toBe('pw');
findChildren(); findChildren();
expect(usernameInput.props().value).toBe('un'); expect(usernameInput.props().value).toBe('un');
expect(passwordInput.props().value).toBe('pw'); expect(passwordInput.props().value).toBe('pw');
}); });
test('updating un/pw clears out error', () => { test('updating un/pw clears out error', () => {
loginPage.setState({ error: 'error!' }); atLogin.setState({ isValidPassword: false });
usernameInput.instance().props.onChange('uname'); expect(loginWrapper.find('.pf-c-form__helper-text.pf-m-error').length).toBe(1);
expect(loginPage.state().username).toBe('uname'); usernameInput.instance().value = 'uname';
expect(loginPage.state().error).toBe(''); usernameInput.simulate('change');
loginPage.setState({ error: 'error!' }); expect(atLogin.state().username).toBe('uname');
passwordInput.instance().props.onChange('pword'); expect(atLogin.state().isValidPassword).toBe(true);
expect(loginPage.state().password).toBe('pword'); expect(loginWrapper.find('.pf-c-form__helper-text.pf-m-error').length).toBe(0);
expect(loginPage.state().error).toBe(''); atLogin.setState({ isValidPassword: false });
expect(loginWrapper.find('.pf-c-form__helper-text.pf-m-error').length).toBe(1);
passwordInput.instance().value = 'pword';
passwordInput.simulate('change');
expect(atLogin.state().password).toBe('pword');
expect(atLogin.state().isValidPassword).toBe(true);
expect(loginWrapper.find('.pf-c-form__helper-text.pf-m-error').length).toBe(0);
}); });
test('api.login not called when loading', () => { test('api.login not called when loading', () => {
api.login = jest.fn().mockImplementation(() => Promise.resolve({})); api.login = jest.fn().mockImplementation(() => Promise.resolve({}));
expect(loginPage.state().loading).toBe(false); expect(atLogin.state().loading).toBe(false);
loginPage.setState({ loading: true }); atLogin.setState({ loading: true });
submitButton.simulate('submit'); submitButton.simulate('click');
expect(api.login).toHaveBeenCalledTimes(0); expect(api.login).toHaveBeenCalledTimes(0);
}); });
test('submit calls api.login successfully', async () => { test('submit calls api.login successfully', async () => {
api.login = jest.fn().mockImplementation(() => Promise.resolve({})); api.login = jest.fn().mockImplementation(() => Promise.resolve({}));
expect(loginPage.state().loading).toBe(false); expect(atLogin.state().loading).toBe(false);
loginPage.setState({ username: 'unamee', password: 'pwordd' }); atLogin.setState({ username: 'unamee', password: 'pwordd' });
submitButton.simulate('submit'); submitButton.simulate('click');
expect(api.login).toHaveBeenCalledTimes(1); expect(api.login).toHaveBeenCalledTimes(1);
expect(api.login).toHaveBeenCalledWith('unamee', 'pwordd'); expect(api.login).toHaveBeenCalledWith('unamee', 'pwordd');
expect(loginPage.state().loading).toBe(true); expect(atLogin.state().loading).toBe(true);
await asyncFlush(); await asyncFlush();
expect(loginPage.state().loading).toBe(false); expect(atLogin.state().loading).toBe(false);
}); });
test('submit calls api.login handles 401 error', async () => { test('submit calls api.login handles 401 error', async () => {
@@ -102,15 +116,16 @@ describe('<LoginPage />', () => {
err.response = { status: 401, message: 'problem' }; err.response = { status: 401, message: 'problem' };
return Promise.reject(err); return Promise.reject(err);
}); });
expect(loginPage.state().loading).toBe(false); expect(atLogin.state().loading).toBe(false);
loginPage.setState({ username: 'unamee', password: 'pwordd' }); expect(atLogin.state().isValidPassword).toBe(true);
submitButton.simulate('submit'); atLogin.setState({ username: 'unamee', password: 'pwordd' });
submitButton.simulate('click');
expect(api.login).toHaveBeenCalledTimes(1); expect(api.login).toHaveBeenCalledTimes(1);
expect(api.login).toHaveBeenCalledWith('unamee', 'pwordd'); expect(api.login).toHaveBeenCalledWith('unamee', 'pwordd');
expect(loginPage.state().loading).toBe(true); expect(atLogin.state().loading).toBe(true);
await asyncFlush(); await asyncFlush();
expect(loginPage.state().error).toBe(LOGIN_ERROR_MESSAGE); expect(atLogin.state().isValidPassword).toBe(false);
expect(loginPage.state().loading).toBe(false); expect(atLogin.state().loading).toBe(false);
}); });
test('submit calls api.login handles non-401 error', async () => { test('submit calls api.login handles non-401 error', async () => {
@@ -119,21 +134,20 @@ describe('<LoginPage />', () => {
err.response = { status: 500, message: 'problem' }; err.response = { status: 500, message: 'problem' };
return Promise.reject(err); return Promise.reject(err);
}); });
expect(loginPage.state().loading).toBe(false); expect(atLogin.state().loading).toBe(false);
loginPage.setState({ username: 'unamee', password: 'pwordd' }); atLogin.setState({ username: 'unamee', password: 'pwordd' });
submitButton.simulate('submit'); submitButton.simulate('click');
expect(api.login).toHaveBeenCalledTimes(1); expect(api.login).toHaveBeenCalledTimes(1);
expect(api.login).toHaveBeenCalledWith('unamee', 'pwordd'); expect(api.login).toHaveBeenCalledWith('unamee', 'pwordd');
expect(loginPage.state().loading).toBe(true); expect(atLogin.state().loading).toBe(true);
await asyncFlush(); await asyncFlush();
expect(loginPage.state().error).toBe(''); expect(atLogin.state().loading).toBe(false);
expect(loginPage.state().loading).toBe(false);
}); });
test('render Redirect to / when already authenticated', () => { test('render Redirect to / when already authenticated', () => {
api.isAuthenticated = jest.fn(); api.isAuthenticated = jest.fn();
api.isAuthenticated.mockReturnValue(true); api.isAuthenticated.mockReturnValue(true);
loginWrapper = shallow(<LoginPage />); loginWrapper = shallow(<AtLogin />);
const redirectElem = loginWrapper.find('Redirect'); const redirectElem = loginWrapper.find('Redirect');
expect(redirectElem.length).toBe(1); expect(redirectElem.length).toBe(1);
expect(redirectElem.props().to).toBe('/'); expect(redirectElem.props().to).toBe('/');

View File

@@ -1,33 +1,22 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Redirect } from 'react-router-dom'; import { Redirect } from 'react-router-dom';
import { import {
Brand, LoginForm,
Button, LoginPage,
Level,
LevelItem,
Login,
LoginBox,
LoginBoxHeader,
LoginBoxBody,
LoginFooter,
LoginHeaderBrand,
TextInput,
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import TowerLogo from '../components/TowerLogo'; import towerLogo from '../../images/tower-logo-header.svg';
import api from '../api'; import api from '../api';
const LOGIN_ERROR_MESSAGE = 'Invalid username or password. Please try again.'; class AtLogin extends Component {
class LoginPage extends Component {
constructor (props) { constructor (props) {
super(props); super(props);
this.state = { this.state = {
username: '', username: '',
password: '', password: '',
error: '', isValidPassword: true,
loading: false, loading: false
}; };
} }
@@ -37,9 +26,9 @@ class LoginPage extends Component {
safeSetState = obj => !this.unmounting && this.setState(obj); safeSetState = obj => !this.unmounting && this.setState(obj);
handleUsernameChange = value => this.safeSetState({ username: value, error: '' }); handleUsernameChange = value => this.safeSetState({ username: value, isValidPassword: true });
handlePasswordChange = value => this.safeSetState({ password: value, error: '' }); handlePasswordChange = value => this.safeSetState({ password: value, isValidPassword: true });
handleSubmit = async event => { handleSubmit = async event => {
const { username, password, loading } = this.state; const { username, password, loading } = this.state;
@@ -53,7 +42,7 @@ class LoginPage extends Component {
await api.login(username, password); await api.login(username, password);
} catch (error) { } catch (error) {
if (error.response.status === 401) { if (error.response.status === 401) {
this.safeSetState({ error: LOGIN_ERROR_MESSAGE }); this.safeSetState({ isValidPassword: false });
} }
} finally { } finally {
this.safeSetState({ loading: false }); this.safeSetState({ loading: false });
@@ -62,75 +51,39 @@ class LoginPage extends Component {
} }
render () { render () {
const { username, password, loading, error } = this.state; const { username, password, isValidPassword } = this.state;
const { logo, loginInfo } = this.props; const { logo, alt } = this.props;
const logoSrc = logo ? `data:image/jpeg;${logo}` : towerLogo;
const logoAlt = alt || 'Ansible Tower';
const LOGIN_ERROR_MESSAGE = 'Invalid username or password. Please try again.';
const LOGIN_TITLE = 'Welcome to Ansible Tower! Please Sign In.';
const LOGIN_USERNAME = 'Username';
const LOGIN_PASSWORD = 'Password';
if (api.isAuthenticated()) { if (api.isAuthenticated()) {
return (<Redirect to="/" />); return (<Redirect to="/" />);
} }
return ( return (
<Login <LoginPage
header={( mainBrandImgSrc={logoSrc}
<LoginHeaderBrand> mainBrandImgAlt={logoAlt}
{logo ? <Brand src={`data:image/jpeg;${logo}`} alt="logo brand" /> : <TowerLogo />} loginTitle={LOGIN_TITLE}
</LoginHeaderBrand>
)}
footer={<LoginFooter>{ loginInfo }</LoginFooter>}
> >
<LoginBox> <LoginForm
<LoginBoxHeader> usernameLabel={LOGIN_USERNAME}
Welcome to Ansible Tower! Please Sign In. usernameValue={username}
</LoginBoxHeader> onChangeUsername={this.handleUsernameChange}
<LoginBoxBody> passwordLabel={LOGIN_PASSWORD}
<form className="pf-c-form" onSubmit={this.handleSubmit}> passwordValue={password}
<div className="pf-c-form__group" id="username"> onChangePassword={this.handlePasswordChange}
<label className="pf-c-form__label" htmlFor="username"> isValidPassword={isValidPassword}
Username passwordHelperTextInvalid={LOGIN_ERROR_MESSAGE}
<span className="pf-c-form__label__required" aria-hidden="true">&#42;</span> onLoginButtonClick={this.handleSubmit}
</label> />
<TextInput </LoginPage>
autoComplete="off"
aria-label="Username"
name="username"
type="text"
isDisabled={loading}
value={username}
onChange={this.handleUsernameChange}
/>
</div>
<div className="pf-c-form__group" id="password">
<label className="pf-c-form__label" htmlFor="pw">
Password
<span className="pf-c-form__label__required" aria-hidden="true">&#42;</span>
</label>
<TextInput
aria-label="Password"
name="password"
type="password"
isDisabled={loading}
value={password}
onChange={this.handlePasswordChange}
/>
</div>
<Level>
<LevelItem>
<p className="pf-c-form__helper-text pf-m-error" aria-live="polite">
{ error }
</p>
</LevelItem>
<LevelItem>
<Button type="submit" isDisabled={loading}>
Sign In
</Button>
</LevelItem>
</Level>
</form>
</LoginBoxBody>
</LoginBox>
</Login>
); );
} }
} }
export default LoginPage; export default AtLogin;