Skip to the code
cf.

User profile fetch

Build UserProfile, the default export: the profile card for one member of a small tea shop's club. It takes an id prop (default 1) and fetches /api/users/ followed by the id, like /api/users/7. The answer is JSON like { id: 7, name: "Asha Rao", email: "asha@tea.shop" }.

While the user is loading, show only Loading… (with the single … character). Then show the name in a heading and the email in a paragraph.

If the request fails (fetch rejects, or the response isn't ok), show Couldn't load this user. and a Retry button that fetches the same user again.

When id changes, fetch the new user and show Loading… until it arrives. An answer for an id the card no longer shows must never appear, even if it arrives late.

<UserProfile id={7} />
 
// While it loads:
<p>Loading…</p>
 
// Then:
<h2>Asha Rao</h2>
<p>asha@tea.shop</p>

What the tests check

  1. fetches /api/users/:id for its id
    const fetch = mockFetch(() => ({ id: 7, name: "Asha Rao", email: "asha@tea.shop" }));
    render(<App id={7} />);
    expect(await screen.findByText("Asha Rao")).toBeInTheDocument();
    expect(fetch.calls.map((call) => call.url)).toEqual(["/api/users/7"]);
    
  2. shows Loading… until the user arrives
    const answer = deferred();
    mockFetch(() => answer.promise);
    render(<App id={7} />);
    expect(screen.getByText("Loading…")).toBeInTheDocument();
    answer.resolve({ id: 7, name: "Asha Rao", email: "asha@tea.shop" });
    expect(await screen.findByText("Asha Rao")).toBeInTheDocument();
    expect(screen.queryByText("Loading…")).not.toBeInTheDocument();
    
  3. shows the name in a heading and the email
    mockFetch(() => ({ id: 7, name: "Asha Rao", email: "asha@tea.shop" }));
    render(<App id={7} />);
    expect(await screen.findByRole("heading", { name: "Asha Rao" })).toBeInTheDocument();
    expect(screen.getByText("asha@tea.shop")).toBeInTheDocument();
    
  4. shows an error and Retry when the response isn't ok
    mockFetch(() => fail(500));
    render(<App id={7} />);
    expect(await screen.findByText("Couldn't load this user.")).toBeInTheDocument();
    expect(screen.getByRole("button", { name: "Retry" })).toBeInTheDocument();
    expect(screen.queryByText("Loading…")).not.toBeInTheDocument();
    
  5. shows the error when the request itself fails
    mockFetch(() => {
      throw new TypeError("Failed to fetch");
    });
    render(<App id={7} />);
    expect(await screen.findByText("Couldn't load this user.")).toBeInTheDocument();
    
  6. Retry fetches the same user again
    let attempts = 0;
    const fetch = mockFetch(() => (++attempts === 1 ? fail(500) : { id: 7, name: "Asha Rao", email: "asha@tea.shop" }));
    render(<App id={7} />);
    await user.click(await screen.findByRole("button", { name: "Retry" }));
    expect(await screen.findByText("Asha Rao")).toBeInTheDocument();
    expect(screen.queryByText("Couldn't load this user.")).not.toBeInTheDocument();
    expect(fetch.calls.map((call) => call.url)).toEqual(["/api/users/7", "/api/users/7"]);
    
  7. fetches the new user when id changes, showing Loading… meanwhile
    const later = deferred();
    const fetch = mockFetch((url) => (url === "/api/users/1" ? { id: 1, name: "Asha Rao", email: "asha@tea.shop" } : later.promise));
    const { rerender } = render(<App id={1} />);
    expect(await screen.findByText("Asha Rao")).toBeInTheDocument();
    rerender(<App id={2} />);
    expect(screen.getByText("Loading…")).toBeInTheDocument();
    expect(screen.queryByText("Asha Rao")).not.toBeInTheDocument();
    later.resolve({ id: 2, name: "Ben Okafor", email: "ben@tea.shop" });
    expect(await screen.findByText("Ben Okafor")).toBeInTheDocument();
    expect(fetch.calls.map((call) => call.url)).toEqual(["/api/users/1", "/api/users/2"]);
    
  8. never shows a user whose answer arrives after the id changed
    const first = deferred();
    mockFetch((url) => (url === "/api/users/1" ? first.promise : { id: 2, name: "Ben Okafor", email: "ben@tea.shop" }));
    const { rerender } = render(<App id={1} />);
    rerender(<App id={2} />);
    expect(await screen.findByText("Ben Okafor")).toBeInTheDocument();
    first.resolve({ id: 1, name: "Asha Rao", email: "asha@tea.shop" });
    await flush();
    expect(screen.queryByText("Asha Rao")).not.toBeInTheDocument();
    expect(screen.getByText("Ben Okafor")).toBeInTheDocument();
    

Learn more

App.jsxSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).