C-RAD
C-RAD

Reputation: 1031

App not re-rendering on history.push when run with jest

I'm trying to test my LoginForm component using jest and react-testing-library. When the login form is submitted successfully, my handleLoginSuccess function is supposed to set the 'user' item on localStorage and navigate the user back to the home page using history.push(). This works in my browser in the dev environment, but when I render the component using Jest and mock out the API, localStorage gets updated but the navigation to '/' doesn't happen.

I've tried setting localStorage before calling history.push(). I'm not sure what is responsible for re-rendering in this case, and why it works in dev but not test.

Login.test.jsx

import 'babel-polyfill'
import React from 'react'
import {withRouter} from 'react-router'
import {Router} from 'react-router-dom'
import {createMemoryHistory} from 'history'
import {render, fireEvent} from '@testing-library/react'
import Login from '../../pages/Login'
import API from '../../util/api'

jest.mock('../../util/api')


function renderWithRouter(
  ui,
  {route = '/', history = createMemoryHistory({initialEntries: [route]})} = {},
) {
  return {
    ...render(<Router history={history}>{ui}</Router>),
    // adding `history` to the returned utilities to allow us
    // to reference it in our tests (just try to avoid using
    // this to test implementation details).
    history,
  }
}

describe('When a user submits the login button', () => {
  test('it allows the user to login', async () => {
    const fakeUserResponse = {'status': 200, 'data': { 'user': 'Leo' } }

    API.mockImplementation(() => {
      return {
        post: () => {
          return Promise.resolve(fakeUserResponse)
        }
      }
    })

    const route = '/arbitrary-route'
    const {getByLabelText, getByText, findByText} = renderWithRouter(<Login />, {route})

    fireEvent.change(getByLabelText(/email/i), {target: {value: '[email protected] '}})
    fireEvent.change(getByLabelText(/password/i), {target: {value: 'Foobar123'}})
    fireEvent.click(getByText(/Log in/i))

    const logout = await findByText(/Log Out/i)

    expect(JSON.parse(window.localStorage.getItem('vector-user'))).toEqual(fakeUserResponse.data.user)
  })
})

relevant parts of LoginForm.jsx

class LoginForm extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      disableActions: false,
      formErrors: null,
    };
  }

  handleLoginSuccess = () => {
    const { loginSuccessCallback, redirectOnLogin, history } = { ...this.props };

    if (loginSuccessCallback) {
      loginSuccessCallback();
    } else {
      history.push('/');
    }
  }

  loginUser = ({ user }) => {
    localStorage.setItem('vector-user', JSON.stringify(user));
  }

  handleLoginResponse = (response) => {
    if (response.status !== 200) {
      this.handleResponseErrors(response.errors);
    } else {
      this.loginUser(response.data);
      this.handleLoginSuccess();
    }
  }

  handleLoginSubmit = (event) => {
    event.preventDefault();

    const {
      disableActions, email, password
    } = { ...this.state };

    if (disableActions === true) {
      return false;
    }

    const validator = new Validator();
    if (!validator.validateForm(event.target)) {
      this.handleResponseErrors(validator.errors);
      return false;
    }

    this.setState(prevState => ({ ...prevState, disableActions: true }));
    new API().post('login', { email, password }).then(this.handleLoginResponse);

    return true;
  }
}

Login.jsx

import React from 'react';
import { withRouter, Link } from 'react-router-dom';
import PropTypes from 'prop-types';

import LoginForm from '../components/LoginForm';

class Login extends React.Component {
  constructor({ location }) {
    super();

    const originalRequest = location.state && location.state.originalRequest;
    this.state = {
      originalRequest
    };
  }

  render() {
    const { originalRequest } = { ...this.state };

    return (
      <div>
        <h1>Login</h1>
        <LoginForm redirectOnLogin={originalRequest && originalRequest.pathname} />
        <Link to="/forgot">Forgot your password?</Link>
      </div>
    );
  }
}
Login.propTypes = {
  location: PropTypes.shape({
    state: PropTypes.shape({
      originalRequest: PropTypes.shape({
        pathname: PropTypes.string
      })
    })
  })
};

export default withRouter(Login);

Currently the await findByText() times out.

Upvotes: 4

Views: 2817

Answers (1)

Gio Polvara
Gio Polvara

Reputation: 26988

I think that's because in your tests you're not rendering any Route components. Without those react-router has no way to know what to render when the route changes. It will always render Login.

Upvotes: 5

Related Questions