import { render, screen, fireEvent } from "@testing-library/react";
import { test, expect, vi } from "vitest";
function DeleteButton({ onDelete }) {
return <button onClick={onDelete}>Delete</button>;
}
test("calls onDelete when clicked", () => {
const handleDelete = vi.fn(); // a mock/fake function that tracks its own calls
render(<DeleteButton onDelete={handleDelete} />);
fireEvent.click(screen.getByText("Delete"));
expect(handleDelete).toHaveBeenCalledTimes(1);
});vi.fn() creates a mock function specifically so the test can verify it was called, without needing any real deletion logic โ perfect for testing Chapter 17's callback-prop pattern in isolation from whatever the parent actually does with it.
test("shows an error for an invalid email", () => {
render(<SignupForm />);
const emailInput = screen.getByLabelText("Email");
fireEvent.change(emailInput, { target: { value: "not-an-email" } });
fireEvent.click(screen.getByText("Sign Up"));
expect(screen.getByText(/valid email/i)).toBeInTheDocument();
});This directly tests Chapter 15's validation logic: simulate typing invalid text with fireEvent.change, submit the form, and assert the expected error message actually appears โ exercising the real validation function through the UI, exactly as a user would trigger it.
test("shows a loading message before data arrives", () => {
global.fetch = vi.fn(() => new Promise(() => {})); // a Promise that never resolves
render(<Users />);
expect(screen.getByText("Loading...")).toBeInTheDocument();
});
test("shows an error message when the fetch fails", async () => {
global.fetch = vi.fn(() => Promise.reject(new Error("Network error")));
render(<Users />);
await waitFor(() => {
expect(screen.getByText(/error/i)).toBeInTheDocument();
});
});A never-resolving Promise is a simple, reliable trick to freeze a component in its loading state for testing. Rejecting the mocked fetch simulates a failed request, letting you verify Chapter 21's error-state handling actually works, without needing a real API to intentionally fail on command.
- A button component: verify its click handler fires correctly
- A counter: verify clicking updates the displayed count
- A form: verify submitting valid data calls the expected handler
- Form validation: verify invalid input shows the correct error message
- An API-driven list: verify loading, error, and success states each render correctly
Working through this checklist against a real project of your own is one of the fastest ways to genuinely internalize the testing patterns from Chapter 34.
A single sprawling test that renders a whole page and checks a dozen unrelated things is hard to debug when it fails โ you won't immediately know which specific behavior broke. Keep each test focused on one specific behavior (as in every example above), so a failing test's name immediately tells you what's actually wrong.
Write a test for a simple toggle button component that verifies its displayed text changes from "Show" to "Hide" after being clicked.
import { render, screen, fireEvent } from "@testing-library/react";
import { test, expect } from "vitest";
import { useState } from "react";
function ToggleButton() {
const [visible, setVisible] = useState(false);
return (
<button onClick={() => setVisible(!visible)}>
{visible ? "Hide" : "Show"}
</button>
);
}
test("toggles text between Show and Hide", () => {
render(<ToggleButton />);
const button = screen.getByText("Show");
fireEvent.click(button);
expect(screen.getByText("Hide")).toBeInTheDocument();
});
Q What does vi.fn() actually do?
It creates a 'mock' function โ a fake function that records how many times it was called and with what arguments, without needing real implementation logic. This lets a test verify a callback prop fired correctly without depending on what that callback actually does.
Q Why simulate a never-resolving Promise to test loading state?
Because a real fetch resolves too quickly to reliably 'catch' the component in its loading state during a test. A Promise that never resolves guarantees the component stays in loading mode for the entire duration of that specific test, making the loading UI reliably testable.