reactjsreact-testing-library

Testing library throws error with finding input element with slight change


I am testing my Input element by 2 way.

  1. adding value and click on button to "todo list" works fine
  2. adding value and enter the "input' field, but throws error.

any one clarify the issue here: my test code:

import { describe, vi, it, expect } from "vitest";
import { AddTodo } from "./AddItem";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

const updateTodo = vi.fn();

describe("Add Item component", () => {
  it("should contain a input with button element", () => {
    render(<AddTodo updateTodo={updateTodo} />);
    expect(screen.getByRole("textbox")).toBeInTheDocument();
    expect(screen.getByRole("button")).toBeInTheDocument();
  });
  it("should add a todo item into screen", async () => {
    render(<AddTodo updateTodo={updateTodo} />);
    const inputBox = await screen.findByRole("textbox");
    const button = await screen.findByRole("button", { name: "+" });
    await userEvent.type(inputBox, "First todo");
    expect(inputBox).toHaveValue("First todo");
    await userEvent.click(button);
    expect(updateTodo).toHaveBeenCalledWith(
      expect.objectContaining({ text: "First todo" })
    );
  });
  it("should add a todo item into screen on enter", async () => { //fails
    render(<AddTodo updateTodo={updateTodo} />);
    const inputBox = await screen.findByTestId("todo-input");
    await userEvent.type(inputBox, "Second todo");
    await userEvent.keyboard("{enter}");
    expect(updateTodo).toHaveBeenCalledWith(
      expect.objectContaining({ text: "First todo" })
    );
  });
});

Live code

thanks in advance

failing case

it("should not call the updateTodo when input not provided", async () => {
    const user = userEvent.setup();
    const { findByTestId } = render(<AddTodo updateTodo={updateTodo} />);
    const inputBox = await findByTestId("todo-input");
    user.type(inputBox, "");
    await user.keyboard("{enter}");
    expect(updateTodo).not.toHaveBeenCalled();
  });

error AssertionError: expected "spy" to not be called at all, but actually been called 1 times


Solution

  • You forgot to cleanup the render after Each tests. When running the tests on each test vitest was adding multple todo lists to the dom.

    afterEach(cleanup);
    

    Adding a cleanup after each tests will make everything green.

    import { describe, vi, it, expect, afterEach } from "vitest";
    import { AddTodo } from "./AddItem";
    import { cleanup, render, screen } from "@testing-library/react";
    import userEvent from "@testing-library/user-event";
    
    const updateTodo = vi.fn();
    afterEach(cleanup);
    describe("Add Item component", () => {
      it("should contain a input with button element", () => {
        render(<AddTodo updateTodo={updateTodo} />);
        expect(screen.getByRole("textbox")).toBeInTheDocument();
        expect(screen.getByRole("button")).toBeInTheDocument();
      });
      it("should add a todo item into screen", async () => {
        const user = userEvent.setup();
        render(<AddTodo updateTodo={updateTodo} />);
        const inputBox = await screen.findByRole("textbox");
        const button = await screen.findByRole("button", { name: "+" });
        await user.type(inputBox, "First todo");
        expect(inputBox).toHaveValue("First todo");
        await user.click(button);
        expect(updateTodo).toHaveBeenCalledWith(
          expect.objectContaining({ text: "First todo" })
        );
      });
      it("should add a todo item into screen on enter", async () => {
        const user = userEvent.setup();
        render(<AddTodo updateTodo={updateTodo} />);
        const inputBox = await screen.findByRole("textbox");
        await user.type(inputBox, "Second todo");
        await user.keyboard("{enter}");
        expect(updateTodo).toHaveBeenCalledWith(
          expect.objectContaining({ text: "First todo" })
        );
      });
    });
    

    I have made a few more changes. Additionally reading elements from render is a best practice. (by doing that it will not read entire screen)

    here is an example - https://codesandbox.io/p/devbox/react-test-forked-7ykncc

    Edit

    here is the example reading elements from render itself

    import { describe, vi, it, expect, afterEach } from "vitest";
    import { AddTodo } from "./AddItem";
    import { cleanup, render } from "@testing-library/react";
    import userEvent from "@testing-library/user-event";
    
    const updateTodo = vi.fn();
    afterEach(cleanup);
    describe("Add Item component", () => {
      it("should contain a input with button element", () => {
        const { getByRole } = render(<AddTodo updateTodo={updateTodo} />);
        expect(getByRole("textbox")).toBeInTheDocument();
        expect(getByRole("button")).toBeInTheDocument();
      });
      it("should add a todo item into screen", async () => {
        const user = userEvent.setup();
        const { findByRole } = render(<AddTodo updateTodo={updateTodo} />);
        const inputBox = await findByRole("textbox");
        const button = await findByRole("button", { name: "+" });
        await user.type(inputBox, "First todo");
        expect(inputBox).toHaveValue("First todo");
        await user.click(button);
        expect(updateTodo).toHaveBeenCalledWith(
          expect.objectContaining({ text: "First todo" })
        );
      });
      it("should add a todo item into screen on enter", async () => {
        const user = userEvent.setup();
        const { findByRole } = render(<AddTodo updateTodo={updateTodo} />);
        const inputBox = await findByRole("textbox");
        await user.type(inputBox, "Second todo");
        await user.keyboard("{enter}");
        expect(updateTodo).toHaveBeenCalledWith(
          expect.objectContaining({ text: "First todo" })
        );
      });
    });