Skip to content
Advertisement

React Testing with Jest and useParams-hook, tried with MemoryRouter

How do I test a component which needs the parameter from react-router to work?

Carousel.js component

import { useParams } from "react-router-dom";
import { useState } from "react";
import './Carousel.css';

const Carousel = ({haustierArray}) => {
  const [active, setActive] = useState(0);
  const {id} = useParams();
  // const id = 1;
  const { images } = haustierArray.find(haustier => haustier.id === +id);
  const handleIndexClick = (event) => {
    setActive(+event.target.dataset.index)
  }
  return (
    <div className="carousel">
      <img src={images[active]} alt="animal" data-testid="hero" />
      <div className="carousel-small">
        {images.map((photo, index) => (
          <img
            data-testid={`thumbnail${index}`}
            data-index={index}
            src={photo}
            alt="animal thumbnail"
            key={photo}
            className={index === active ? "active" : ""}
            onClick={handleIndexClick}
          />
        ))}
      </div>
    </div>
  );
};

export default Carousel;

Carousel.test.js

import { render, waitFor } from "@testing-library/react";
import Carousel from "./Carousel.js";

it("lets users click on thumbnails to make them the hero image", async () => {
  const haustierArray = [
    {
      name: "Aristoteles",
      animal: "Meerschweinchen",
      breed: "Glatthaar",
      id: 1,
      images: ["0.jpg", "1.jpg", "2.jpg", "3.jpg"],
    },
  ];
  const carousel = render(<Carousel haustierArray={haustierArray} />);

  const hero = await carousel.findByTestId("hero");
  expect(hero.src).toContain(haustierArray[0].images[0]);

  for (let i = 0; i < haustierArray[0].images.length; i++) {
    const image = haustierArray[0].images[i];
    const thumb = await carousel.findByTestId(`thumbnail${i}`);
    thumb.click();
    await waitFor(() => {
      expect(hero.src).toContain(image);
      expect(thumb.classList).toContain("active");
    });
  }
});

The test works if I comment the line with useParams out and just declare const id = 1;.

Then I’ve tried to use MemoryRouter. Carousel.test.js v2

import { render, waitFor } from "@testing-library/react";
import Carousel from "./Carousel.js";
import { MemoryRouter, Routes, Route } from "react-router-dom";

it("lets users click on thumbnails to make them the hero image", async () => {
  const haustierArray = [
    {
      name: "Aristoteles",
      animal: "Meerschweinchen",
      breed: "Glatthaar",
      id: 1,
      images: ["0.jpg", "1.jpg", "2.jpg", "3.jpg"],
    },
    {
      name: "Leo",
      animal: "Katze",
      breed: "British",
      id: 2,
      images: ["0.png", "1.png"],
    },
    {
      name: "Lady",
      animal: "Hund",
      breed: "Dackel",
      id: 3,
      images: ["0.gif", "1.gif", "2.gif"],
    },
  ];
  for (let j = 0; j < haustierArray.length; j++) {
    const carousel = render(
      <MemoryRouter initialEntries={[`/pictures/${j + 1}`]}>
        <Routes>
          <Route path="/pictures/:id" element={<Carousel haustierArray={haustierArray} />} />
        </Routes>
      </MemoryRouter>
    );

    const hero = await carousel.findByTestId("hero");
    expect(hero.src).toContain(haustierArray[j].images[0]);

    for (let i = 0; i < haustierArray[j].images.length; i++) {
      const image = haustierArray[j].images[i];
      const thumb = await carousel.findByTestId(`thumbnail${i}`);
      thumb.click();
      await waitFor(() => {
        expect(hero.src).toContain(image);
        expect(thumb.classList).toContain("active");
      });
    }
  }
});

And the test failed with this output

Found multiple elements by: [data-testid="hero"]

    Here are the matching elements:

    Ignored nodes: comments, script, style
    <img
      alt="animal"
      data-testid="hero"
      src="3.jpg"
    />

    Ignored nodes: comments, script, style
    <img
      alt="animal"
      data-testid="hero"
      src="0.png"
    />

    (If this is intentional, then use the `*AllBy*` variant of the query (like `queryAllByText`, `getAllByText`, or `findAllByText`)).

    Ignored nodes: comments, script, style
    <body>
      <div>
        <div
          class="carousel"
        >
          <img
            alt="animal"
            data-testid="hero"
            src="3.jpg"
          />
          <div
            class="carousel-small"
          >
            <img
              alt="animal thumbnail"
              class=""
              data-index="0"
              data-testid="thumbnail0"
              src="0.jpg"
            />
            <img
              alt="animal thumbnail"
              class=""
              data-index="1"
              data-testid="thumbnail1"
              src="1.jpg"
            />
            <img
              alt="animal thumbnail"
              class=""
              data-index="2"
              data-testid="thumbnail2"
              src="2.jpg"
            />
            <img
              alt="animal thumbnail"
              class="active"
              data-index="3"
              data-testid="thumbnail3"
              src="3.jpg"
            />
          </div>
        </div>
      </div>
      <div>
        <div
          class="carousel"
        >
          <img
            alt="animal"
            data-testid="hero"
            src="0.png"
          />
          <div
            class="carousel-small"
          >
            <img
              alt="animal thumbnail"
              class="active"
              data-index="0"
              data-testid="thumbnail0"
              src="0.png"
            />
            <img
              alt="animal thumbnail"
              class=""
              data-index="1"
              data-testid="thumbnail1"
              src="1.png"
            />
          </div>
        </div>
      </div>
    </body>

      40 |     );
      41 |
    > 42 |     const hero = await carousel.findByTestId("hero");
         |                                 ^
      43 |     expect(hero.src).toContain(haustierArray[j].images[0]);
      44 |
      45 |     for (let i = 0; i < haustierArray[j].images.length; i++) {

      at waitForWrapper (node_modules/@testing-library/dom/dist/wait-for.js:187:27)
      at findByTestId (node_modules/@testing-library/dom/dist/query-helpers.js:101:33)
      at Object.<anonymous> (src/components/Carousel.test.js:42:33)

It seems that it worked for one inner loop cycle, but when the outer loop runs the second iteration, 3.jpg from last iteration is somehow still there. How do I fix it?

Advertisement

Answer

The @testing-library/react render by default creates a div and append that div to the document.body. Since you’re rendering multiple times in one test, then it would only be apparent that the entire JSX render would be appended multiple times. If you don’t want this to happen, I suggest you perform a cleanup after each rendered loop.

import { render, waitFor, cleanup } from "@testing-library/react";
import Carousel from "./Carousel.js";
import { MemoryRouter, Routes, Route } from "react-router-dom";

it("lets users click on thumbnails to make them the hero image", async () => {
  const haustierArray = [
    {
      name: "Aristoteles",
      animal: "Meerschweinchen",
      breed: "Glatthaar",
      id: 1,
      images: ["0.jpg", "1.jpg", "2.jpg", "3.jpg"],
    },
    {
      name: "Leo",
      animal: "Katze",
      breed: "British",
      id: 2,
      images: ["0.png", "1.png"],
    },
    {
      name: "Lady",
      animal: "Hund",
      breed: "Dackel",
      id: 3,
      images: ["0.gif", "1.gif", "2.gif"],
    },
  ];
  for (let j = 0; j < haustierArray.length; j++) {
    const carousel = render(
      <MemoryRouter initialEntries={[`/pictures/${j + 1}`]}>
        <Routes>
          <Route path="/pictures/:id" element={<Carousel haustierArray={haustierArray} />} />
        </Routes>
      </MemoryRouter>
    );

    const hero = await carousel.findByTestId("hero");
    expect(hero.src).toContain(haustierArray[j].images[0]);

    for (let i = 0; i < haustierArray[j].images.length; i++) {
      const image = haustierArray[j].images[i];
      const thumb = await carousel.findByTestId(`thumbnail${i}`);
      thumb.click();
      await waitFor(() => {
        expect(hero.src).toContain(image);
        expect(thumb.classList).toContain("active");
      });
    }

    // unmounts the react trees that were mounted with `render`.
    cleanup();

  }
});
User contributions licensed under: CC BY-SA
7 People found this is helpful
Advertisement