Search code examples
javascriptreactjsjestjsreact-router-domreact-testing-library

Unit test for React App: useNavigate problem


I'm writing a test unit for my Login component, but I'm stuck in a useNavigate problem, I couldn't find a solution for it, I'm using react 18, so I couldn't use testing-library/react-hooks or enzyme.

here is my Login.jsx:

import React, { useEffect, useState } from "react";
import {
  Button,
  FormControl,
  Input,
  InputAdornment,
  InputLabel,
  Box,
  Alert,
} from "@mui/material";
import AccountCircle from "@mui/icons-material/AccountCircle";
import HttpsIcon from "@mui/icons-material/Https";
import logo from "../assets/logo_black_back.png";
import { ReactComponent as Svg } from "../assets/animation.svg";
import { PendingPage } from "../components";

import "./login.css";
import { Link, useNavigate } from "react-router-dom";
import { useLogin } from "../hooks/useLogin";
import { useUserContxt } from "../context/authenticationContext";

function Login() {
  const [email, setEmail] = useState();
  const [password, setPassword] = useState();
  const { login, error } = useLogin();
  const { user, isPending } = useUserContxt();
  const navigate = useNavigate();

  const submit = (e) => {
    e.preventDefault();
    login(email, password);
  };
  useEffect(() => {
    if (user && isPending === false) {
      navigate("/dashboard");
    }
  }, [user, isPending, navigate]);

  return !user && isPending === false ? (
    <Box
      sx={{
        width: "100%",
        height: "100vh",
        display: "flex",
        justifyContent: "center",
      }}
    >
      <Box
        sx={{
          flex: 1,
          display: { xs: "none", lg: "flex" },
          alignItems: "center",
          justifyContent: "center",
        }}
      >
        <Box>
          <Svg />
        </Box>
      </Box>
      <Box
        sx={{
          flex: 1,
          display: "flex",
          boxShadow: 1,
          borderRadius: "40px 0 0 40px",
          flexDirection: "column",
          p: 2,
          justifyContent: "flex-start",
          alignItems: "center",
        }}
      >
        <Box sx={{ width: 150, height: 150, m: 10, mb: 5 }}>
          <img src={logo} alt="logo" />
        </Box>
        <Box
          onSubmit={submit}
          component="form"
          sx={{ width: { xs: "350px", lg: "400px" } }}
        >
          <FormControl variant="standard" sx={{ width: "100%", mb: 5 }}>
            <InputLabel htmlFor="email">EMAIL</InputLabel>
            <Input
              onChange={(e) => setEmail(e.target.value)}
              id="email"
              startAdornment={
                <InputAdornment position="start">
                  <AccountCircle />
                </InputAdornment>
              }
            />
          </FormControl>
          <FormControl variant="standard" sx={{ width: "100%", mb: 5 }}>
            <InputLabel htmlFor="password">PASSWORD</InputLabel>
            <Input
              onChange={(e) => setPassword(e.target.value)}
              id="password"
              type="password"
              startAdornment={
                <InputAdornment position="start">
                  <HttpsIcon />
                </InputAdornment>
              }
            />
          </FormControl>
          <Box sx={{ display: "flex", justifyContent: "space-between" }}>
            <Button type="submit" variant="contained">
              LOGIN
            </Button>
            <Link to="/" style={{ textDecoration: "none" }}>
              <Button variant="text" color="error">
                BACK TO HOME
              </Button>
            </Link>
          </Box>
          {error && <Alert severity="warning">{error}</Alert>}
        </Box>
      </Box>
    </Box>
  ) : (
    <PendingPage />
  );
}

export default Login;

and here is the test code:

import React from 'react';
import '@testing-library/jest-dom';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import { useLogin } from '../hooks/useLogin';
import { useUserContxt } from '../context/authenticationContext';
import Login from './Login';

jest.mock('../hooks/useLogin');
jest.mock('../context/authenticationContext');

describe('Login', () => {
  const mockLogin = jest.fn();
  const mockDispatch = jest.fn();
  const mockedUsedNavigate = jest.fn();

  jest.mock("react-router-dom", () => ({
    ...jest.requireActual("react-router-dom"),
    useNavigate: () => mockedUsedNavigate
  }));

  beforeEach(() => {
    useLogin.mockReturnValue({
      login: mockLogin,
      error: null,
    });

    useUserContxt.mockReturnValue({
      user: null,
      isPending: false,
      dispatch: mockDispatch,
    });

    jest.spyOn(console, 'error').mockImplementation(() => {});
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  afterAll(() => {
    jest.restoreAllMocks();
  });

  it('should render Login component correctly', () => {
    render(
      <BrowserRouter>
        <Login />
      </BrowserRouter>
    );

    expect(screen.getByText('EMAIL')).toBeInTheDocument();
    expect(screen.getByText('PASSWORD')).toBeInTheDocument();
    expect(screen.getByText('LOGIN')).toBeInTheDocument();
    expect(screen.getByText('BACK TO HOME')).toBeInTheDocument();
  });

  it('should update email and password state when typing', async () => {
    render(
      <BrowserRouter>
        <Login />
      </BrowserRouter>
    );

    const emailInput = screen.getByLabelText('EMAIL');
    const passwordInput = screen.getByLabelText('PASSWORD');

    fireEvent.change(emailInput, { target: { value: 'test@test.com' } });
    fireEvent.change(passwordInput, { target: { value: 'password123' } });

    await waitFor(() => {
      expect(emailInput).toHaveValue('test@test.com');
      expect(passwordInput).toHaveValue('password123');
    });
  });

  it('should call login function when form is submitted', async () => {
    render(
      <BrowserRouter>
        <Login />
      </BrowserRouter>
    );

    const emailInput = screen.getByLabelText('EMAIL');
    const passwordInput = screen.getByLabelText('PASSWORD');
    const loginButton = screen.getByText('LOGIN');

    fireEvent.change(emailInput, { target: { value: 'test@test.com' } });
    fireEvent.change(passwordInput, { target: { value: 'password123' } });
    fireEvent.click(loginButton);

    await waitFor(() => {
      expect(mockLogin).toHaveBeenCalledWith('test@test.com', 'password123');
    });
  });
  it('should navigate to dashboard when user and isPending are false', async () => {
    useUserContxt.mockReturnValue({
      user: {},
      isPending: false,
      dispatch: mockDispatch,
    });

    render(
      <BrowserRouter>
        <Login />
      </BrowserRouter>
    );

    await waitFor(() => {
      expect(mockedUsedNavigate).toHaveBeenCalledWith('/dashboard');
    });
  });
});

the problem is in the last test "should navigate to dashboard when user and isPending are false"

I'm expecting the "/dashboard" path to be called.


Solution

  • I had a similar issue. I fixed it by moving the mock code to the global scope at the start of the file (prior to the test code).

    Specifically, for you, it would look like

    import Login from './Login';
    
    jest.mock('../hooks/useLogin');
    jest.mock('../context/authenticationContext');
    
    const mockedUsedNavigate = jest.fn();
    
    jest.mock("react-router-dom", () => ({
      ...jest.requireActual("react-router-dom"),
      useNavigate: () => mockedUsedNavigate
    }));
    
    describe('Login', () => {
      const mockLogin = jest.fn();
      const mockDispatch = jest.fn();
    
      beforeEach(() => {
        [...snip...]