diff --git a/__tests__/tests/LoginPage.test.jsx b/__tests__/tests/LoginPage.test.jsx index af79bcdd88..26af4924de 100644 --- a/__tests__/tests/LoginPage.test.jsx +++ b/__tests__/tests/LoginPage.test.jsx @@ -1,37 +1,185 @@ import React from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { shallow, mount } from 'enzyme'; +import { mount, shallow } from 'enzyme'; import LoginPage from '../../src/pages/Login'; import api from '../../src/api'; -import Dashboard from '../../src/pages/Dashboard'; + +const LOGIN_ERROR_MESSAGE = 'Invalid username or password. Please try again.'; describe('', () => { - let loginWrapper, usernameInput, passwordInput, errorTextArea, submitButton; + let loginWrapper; + let loginPage; + let loginForm; + let usernameInput; + let passwordInput; + let errorTextArea; + let submitButton; + let defaultLogo; - beforeEach(() => { - loginWrapper = mount(); + const findChildren = () => { + loginPage = loginWrapper.find('LoginPage'); + loginForm = loginWrapper.find('form.pf-c-form'); usernameInput = loginWrapper.find('.pf-c-form__group#username TextInput'); passwordInput = loginWrapper.find('.pf-c-form__group#password TextInput'); errorTextArea = loginWrapper.find('.pf-c-form__helper-text.pf-m-error'); submitButton = loginWrapper.find('Button[type="submit"]'); + defaultLogo = loginWrapper.find('TowerLogo'); + }; + + beforeEach(() => { + loginWrapper = mount(); + findChildren(); + }); + + afterEach(() => { + loginWrapper.unmount(); }); test('initially renders without crashing', () => { expect(loginWrapper.length).toBe(1); + expect(loginForm.length).toBe(1); expect(usernameInput.length).toBe(1); + expect(usernameInput.props().value).toBe(''); expect(passwordInput.length).toBe(1); + expect(passwordInput.props().value).toBe(''); expect(errorTextArea.length).toBe(1); + expect(loginPage.state().error).toBe(''); expect(submitButton.length).toBe(1); + expect(submitButton.props().isDisabled).toBe(false); + expect(defaultLogo.length).toBe(1); }); - // initially renders empty username and password fields, sets empty error message and makes submit button not disabled + test('custom logo renders Brand component', () => { + loginWrapper = mount(); + findChildren(); + expect(defaultLogo.length).toBe(0); + }); - // typing into username and password fields (if the component is not unmounting) will clear out any error message + test('state maps to un/pw input value props', () => { + loginPage.setState({ username: 'un', password: 'pw' }); + expect(loginPage.state().username).toBe('un'); + expect(loginPage.state().password).toBe('pw'); + findChildren(); + expect(usernameInput.props().value).toBe('un'); + expect(passwordInput.props().value).toBe('pw'); + }); - // when the submit Button is clicked, as long as it is not disabled state.loading is set to true - // api.login is called with param 1 username and param 2 password - // if api.login returns an error, the state.error should be set to LOGIN_ERROR_MESSAGE, if the error object returned has response.status set to 401. - // regardless of error or not, after api.login returns, state.loading should be set to false + test('updating un/pw clears out error', () => { + loginPage.setState({ error: 'error!' }); + usernameInput.instance().props.onChange('uname'); + expect(loginPage.state().username).toBe('uname'); + expect(loginPage.state().error).toBe(''); + loginPage.setState({ error: 'error!' }); + passwordInput.instance().props.onChange('pword'); + expect(loginPage.state().password).toBe('pword'); + expect(loginPage.state().error).toBe(''); + }); - // if api.isAuthenticated mock returns true, Redirect to / should be rendered -}); \ No newline at end of file + test('submit calls api.login successfully', (done) => { + api.login = jest.fn().mockImplementation(() => { + const loginPromise = Promise.resolve({}); + + // wrapped in timeout so this .then happens after state.loading is set to + // false in the actual .then handler + // + // would be improved by using .finally but that's not available for + // some reason + setTimeout(() => { + loginPromise.then(() => { + expect(loginPage.state().loading).toBe(false); + done(); + }); + }, 50); + + return loginPromise; + }); + expect(loginPage.state().loading).toBe(false); + loginPage.setState({ username: 'unamee', password: 'pwordd', loading: true }); + submitButton.simulate('submit'); + expect(api.login).toHaveBeenCalledTimes(0); + loginPage.setState({ loading: false }); + submitButton.simulate('submit'); + expect(api.login).toHaveBeenCalledTimes(1); + expect(api.login).toHaveBeenCalledWith('unamee', 'pwordd'); + expect(loginPage.state().loading).toBe(true); + }); + + test('submit calls api.login handles 401 error', (done) => { + api.login = jest.fn().mockImplementation(() => { + const err = { + response: { status: 401, message: 'problem' }, + }; + + const loginPromise = Promise.reject(err); + + // wrapped in timeout so this .then happens after state.loading is set to + // false in the actual .then handler + // + // would be improved by using .finally but that's not available for + // some reason + setTimeout(() => { + loginPromise.catch(() => { + expect(loginPage.state().loading).toBe(false); + expect(loginPage.state().error).toBe(LOGIN_ERROR_MESSAGE); + done(); + }); + }, 50); + + return loginPromise; + }); + expect(loginPage.state().loading).toBe(false); + loginPage.setState({ username: 'unamee', password: 'pwordd', loading: true }); + submitButton.simulate('submit'); + expect(api.login).toHaveBeenCalledTimes(0); + loginPage.setState({ loading: false }); + expect(loginPage.state().error).toBe(''); + submitButton.simulate('submit'); + expect(api.login).toHaveBeenCalledTimes(1); + expect(api.login).toHaveBeenCalledWith('unamee', 'pwordd'); + expect(loginPage.state().loading).toBe(true); + }); + + test('submit calls api.login handles 401 error', (done) => { + api.login = jest.fn().mockImplementation(() => { + const err = { + response: { status: 500, message: 'problem' }, + }; + + const loginPromise = Promise.reject(err); + + // wrapped in timeout so this .then happens after state.loading is set to + // false in the actual .then handler + // + // would be improved by using .finally but that's not available for + // some reason + setTimeout(() => { + loginPromise.catch(() => { + expect(loginPage.state().loading).toBe(false); + expect(loginPage.state().error).toBe(''); + done(); + }); + }, 50); + + return loginPromise; + }); + expect(loginPage.state().loading).toBe(false); + loginPage.setState({ username: 'unamee', password: 'pwordd', loading: true }); + submitButton.simulate('submit'); + expect(api.login).toHaveBeenCalledTimes(0); + loginPage.setState({ loading: false }); + expect(loginPage.state().error).toBe(''); + submitButton.simulate('submit'); + expect(api.login).toHaveBeenCalledTimes(1); + expect(api.login).toHaveBeenCalledWith('unamee', 'pwordd'); + expect(loginPage.state().loading).toBe(true); + }); + + test('render Redirect to / when already authenticated', () => { + api.isAuthenticated = jest.fn(); + api.isAuthenticated.mockReturnValue(true); + loginWrapper = shallow(); + const redirectElem = loginWrapper.find('Redirect'); + expect(redirectElem.length).toBe(1); + expect(redirectElem.props().to).toBe('/'); + }); +}); diff --git a/src/pages/Login.jsx b/src/pages/Login.jsx index 2a735dd70c..0ad82ef756 100644 --- a/src/pages/Login.jsx +++ b/src/pages/Login.jsx @@ -42,21 +42,24 @@ class LoginPage extends Component { handlePasswordChange = value => this.safeSetState({ password: value, error: '' }); handleSubmit = event => { - const { username, password } = this.state; + const { username, password, loading } = this.state; event.preventDefault(); - this.safeSetState({ loading: true }); + if (!loading) { + this.safeSetState({ loading: true }); - api.login(username, password) - .catch(error => { - if (error.response.status === 401) { - this.safeSetState({ error: LOGIN_ERROR_MESSAGE }); - } - }) - .finally(() => { - this.safeSetState({ loading: false }); - }); + api.login(username, password) + .then(() => { + this.safeSetState({ loading: false }); + }) + .catch(error => { + this.safeSetState({ loading: false }); + if (error.response.status === 401) { + this.safeSetState({ error: LOGIN_ERROR_MESSAGE }); + } + }); + } } render () {