Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 220
C-RAD Avatar asked Oct 21 '25 03:10

C-RAD


1 Answers

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.

like image 50
Giorgio Polvara - Gpx Avatar answered Oct 23 '25 16:10

Giorgio Polvara - Gpx



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!