diff --git a/__tests__/tests/App.test.jsx b/__tests__/tests/App.test.jsx index 8be9df0e6b..ef0e9fa5f0 100644 --- a/__tests__/tests/App.test.jsx +++ b/__tests__/tests/App.test.jsx @@ -34,4 +34,4 @@ describe('', () => { const login = appWrapper.find(Login); expect(login.length).toBe(0); }); -}); \ No newline at end of file +}); diff --git a/__tests__/tests/ConditionalRedirect.test.jsx b/__tests__/tests/ConditionalRedirect.test.jsx index af241810eb..c437ae6971 100644 --- a/__tests__/tests/ConditionalRedirect.test.jsx +++ b/__tests__/tests/ConditionalRedirect.test.jsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React from 'react'; import { Route, Redirect @@ -9,7 +9,11 @@ import ConditionalRedirect from '../../src/components/ConditionalRedirect'; describe('', () => { test('renders Redirect when shouldRedirect is passed truthy func', () => { const truthyFunc = () => true; - const shouldHaveRedirectChild = shallow( truthyFunc()} />); + const shouldHaveRedirectChild = shallow( + truthyFunc()} + /> + ); const redirectChild = shouldHaveRedirectChild.find(Redirect); expect(redirectChild.length).toBe(1); const routeChild = shouldHaveRedirectChild.find(Route); @@ -18,10 +22,14 @@ describe('', () => { test('renders Route when shouldRedirect is passed falsy func', () => { const falsyFunc = () => false; - const shouldHaveRouteChild = shallow( falsyFunc()} />); + const shouldHaveRouteChild = shallow( + falsyFunc()} + /> + ); const routeChild = shouldHaveRouteChild.find(Route); expect(routeChild.length).toBe(1); const redirectChild = shouldHaveRouteChild.find(Redirect); expect(redirectChild.length).toBe(0); }); -}); \ No newline at end of file +}); diff --git a/__tests__/tests/LoginPage.test.jsx b/__tests__/tests/LoginPage.test.jsx index af79bcdd88..63675a98ab 100644 --- a/__tests__/tests/LoginPage.test.jsx +++ b/__tests__/tests/LoginPage.test.jsx @@ -1,37 +1,141 @@ import React from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { shallow, mount } from 'enzyme'; +import { mount, shallow } from 'enzyme'; +import { asyncFlush } from '../../jest.setup'; 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('api.login not called when loading', () => { + api.login = jest.fn().mockImplementation(() => Promise.resolve({})); + expect(loginPage.state().loading).toBe(false); + loginPage.setState({ loading: true }); + submitButton.simulate('submit'); + expect(api.login).toHaveBeenCalledTimes(0); + }); + + test('submit calls api.login successfully', async () => { + api.login = jest.fn().mockImplementation(() => Promise.resolve({})); + expect(loginPage.state().loading).toBe(false); + loginPage.setState({ username: 'unamee', password: 'pwordd' }); + submitButton.simulate('submit'); + expect(api.login).toHaveBeenCalledTimes(1); + expect(api.login).toHaveBeenCalledWith('unamee', 'pwordd'); + expect(loginPage.state().loading).toBe(true); + await asyncFlush(); + expect(loginPage.state().loading).toBe(false); + }); + + test('submit calls api.login handles 401 error', async () => { + api.login = jest.fn().mockImplementation(() => { + const err = new Error('401 error'); + err.response = { status: 401, message: 'problem' }; + return Promise.reject(err); + }); + expect(loginPage.state().loading).toBe(false); + loginPage.setState({ username: 'unamee', password: 'pwordd' }); + submitButton.simulate('submit'); + expect(api.login).toHaveBeenCalledTimes(1); + expect(api.login).toHaveBeenCalledWith('unamee', 'pwordd'); + expect(loginPage.state().loading).toBe(true); + await asyncFlush(); + expect(loginPage.state().error).toBe(LOGIN_ERROR_MESSAGE); + expect(loginPage.state().loading).toBe(false); + }); + + test('submit calls api.login handles non-401 error', async () => { + api.login = jest.fn().mockImplementation(() => { + const err = new Error('500 error'); + err.response = { status: 500, message: 'problem' }; + return Promise.reject(err); + }); + expect(loginPage.state().loading).toBe(false); + loginPage.setState({ username: 'unamee', password: 'pwordd' }); + submitButton.simulate('submit'); + expect(api.login).toHaveBeenCalledTimes(1); + expect(api.login).toHaveBeenCalledWith('unamee', 'pwordd'); + expect(loginPage.state().loading).toBe(true); + await asyncFlush(); + expect(loginPage.state().error).toBe(''); + expect(loginPage.state().loading).toBe(false); + }); + + 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/jest.setup.js b/jest.setup.js index 570acf4cf7..62cb566c00 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1,5 +1,8 @@ require('@babel/polyfill'); +// eslint-disable-next-line import/prefer-default-export +export const asyncFlush = () => new Promise((resolve) => setImmediate(resolve)); + const enzyme = require('enzyme'); const Adapter = require('enzyme-adapter-react-16'); diff --git a/src/pages/Login.jsx b/src/pages/Login.jsx index 2a735dd70c..c806ef24b4 100644 --- a/src/pages/Login.jsx +++ b/src/pages/Login.jsx @@ -41,22 +41,24 @@ class LoginPage extends Component { handlePasswordChange = value => this.safeSetState({ password: value, error: '' }); - handleSubmit = event => { - const { username, password } = this.state; + handleSubmit = async event => { + const { username, password, loading } = this.state; event.preventDefault(); - this.safeSetState({ loading: true }); + if (!loading) { + this.safeSetState({ loading: true }); - api.login(username, password) - .catch(error => { + try { + await api.login(username, password); + } catch (error) { if (error.response.status === 401) { this.safeSetState({ error: LOGIN_ERROR_MESSAGE }); } - }) - .finally(() => { + } finally { this.safeSetState({ loading: false }); - }); + } + } } render () {