Skip to the code
cf.

To-do list with filters

Build App: a to-do list for the tea shop's morning jobs. It starts empty.

A text box labelled New task and an Add button add a task to the end of the list. Pressing Enter in the box adds it too. The text is trimmed before it's added, a box that's empty or only spaces adds nothing, and the box is emptied once a task is added.

Each task is a list item with a checkbox labelled by the task's text, which marks it done, and a Delete button that removes it. So that every Delete button has its own name, give it aria-label set to Delete and the task: aria-label="Delete Steep the oolong".

Three filter buttons, All, Active, and Done, choose which tasks the list shows: every task, the ones not done, or the ones done. The chosen filter has aria-pressed="true" and the other two "false". All is chosen at first.

Under the list, a line counts the tasks that aren't done, whatever the filter: 2 items left, 1 item left, 0 items left.

<form>
  <label>New task <input /></label>
  <button>Add</button>
</form>
<button aria-pressed="true">All</button>
<button aria-pressed="false">Active</button>
<button aria-pressed="false">Done</button>
<ul>
  <li>
    <label><input type="checkbox" /> Steep the oolong</label>
    <button aria-label="Delete Steep the oolong">Delete</button>
  </li>
</ul>
<p>1 item left</p>

What the tests check

  1. Add puts the task at the end of the list and empties the box
    render(<App />);
    const box = screen.getByLabelText("New task");
    await user.type(box, "Steep the oolong");
    await user.click(screen.getByRole("button", { name: "Add" }));
    await user.clear(box);
    await user.type(box, "Wash the pots");
    await user.click(screen.getByRole("button", { name: "Add" }));
    const items = screen.getAllByRole("listitem");
    expect(items).toHaveLength(2);
    expect(within(items[0]).getByRole("checkbox", { name: "Steep the oolong" })).not.toBeChecked();
    expect(within(items[1]).getByRole("checkbox", { name: "Wash the pots" })).toBeInTheDocument();
    expect(box).toHaveValue("");
    
  2. pressing Enter adds a task too
    render(<App />);
    await user.type(screen.getByLabelText("New task"), "Wash the pots{Enter}");
    expect(screen.getByRole("checkbox", { name: "Wash the pots" })).toBeInTheDocument();
    expect(screen.getByLabelText("New task")).toHaveValue("");
    
  3. trims the text and adds nothing for an empty box
    render(<App />);
    const box = screen.getByLabelText("New task");
    await user.type(box, "   Sweep the floor  {Enter}");
    expect(screen.getByRole("button", { name: "Delete Sweep the floor" })).toHaveAttribute("aria-label", "Delete Sweep the floor");
    await user.clear(box);
    await user.type(box, "    {Enter}");
    await user.click(screen.getByRole("button", { name: "Add" }));
    expect(screen.getAllByRole("listitem")).toHaveLength(1);
    
  4. ticking a task marks it done
    render(<App />);
    await user.type(screen.getByLabelText("New task"), "Steep the oolong{Enter}");
    const checkbox = screen.getByRole("checkbox", { name: "Steep the oolong" });
    await user.click(checkbox);
    expect(checkbox).toBeChecked();
    await user.click(checkbox);
    expect(checkbox).not.toBeChecked();
    
  5. counts the items left with the right word
    render(<App />);
    expect(screen.getByText("0 items left")).toBeInTheDocument();
    const box = screen.getByLabelText("New task");
    await user.type(box, "Steep the oolong{Enter}");
    expect(screen.getByText("1 item left")).toBeInTheDocument();
    await user.clear(box);
    await user.type(box, "Wash the pots{Enter}");
    expect(screen.getByText("2 items left")).toBeInTheDocument();
    await user.click(screen.getByRole("checkbox", { name: "Wash the pots" }));
    expect(screen.getByText("1 item left")).toBeInTheDocument();
    
  6. Delete removes only that task
    render(<App />);
    const box = screen.getByLabelText("New task");
    await user.type(box, "Steep the oolong{Enter}");
    await user.clear(box);
    await user.type(box, "Wash the pots{Enter}");
    await user.clear(box);
    await user.type(box, "Sweep the floor{Enter}");
    await user.click(screen.getByRole("checkbox", { name: "Sweep the floor" }));
    await user.click(screen.getByRole("button", { name: "Delete Wash the pots" }));
    const boxes = screen.getAllByRole("checkbox");
    expect(boxes).toHaveLength(2);
    expect(screen.getByRole("checkbox", { name: "Steep the oolong" })).not.toBeChecked();
    expect(screen.getByRole("checkbox", { name: "Sweep the floor" })).toBeChecked();
    expect(screen.getByText("1 item left")).toBeInTheDocument();
    
  7. Active and Done show only those tasks
    render(<App />);
    const box = screen.getByLabelText("New task");
    await user.type(box, "Steep the oolong{Enter}");
    await user.clear(box);
    await user.type(box, "Wash the pots{Enter}");
    await user.clear(box);
    await user.type(box, "Sweep the floor{Enter}");
    await user.click(screen.getByRole("checkbox", { name: "Wash the pots" }));
    const all = screen.getByRole("button", { name: "All" });
    const active = screen.getByRole("button", { name: "Active" });
    const done = screen.getByRole("button", { name: "Done" });
    expect(all).toHaveAttribute("aria-pressed", "true");
    await user.click(active);
    expect(active).toHaveAttribute("aria-pressed", "true");
    expect(all).toHaveAttribute("aria-pressed", "false");
    expect(screen.getAllByRole("checkbox")).toHaveLength(2);
    expect(screen.queryByRole("checkbox", { name: "Wash the pots" })).not.toBeInTheDocument();
    await user.click(done);
    expect(done).toHaveAttribute("aria-pressed", "true");
    expect(active).toHaveAttribute("aria-pressed", "false");
    expect(screen.getAllByRole("checkbox")).toHaveLength(1);
    expect(screen.getByRole("checkbox", { name: "Wash the pots" })).toBeChecked();
    await user.click(all);
    expect(screen.getAllByRole("checkbox")).toHaveLength(3);
    
  8. the count of items left ignores the filter
    render(<App />);
    const box = screen.getByLabelText("New task");
    await user.type(box, "Steep the oolong{Enter}");
    await user.clear(box);
    await user.type(box, "Wash the pots{Enter}");
    await user.click(screen.getByRole("checkbox", { name: "Wash the pots" }));
    await user.click(screen.getByRole("button", { name: "Done" }));
    expect(screen.getByText("1 item left")).toBeInTheDocument();
    
  9. a task ticked off under Active leaves the list
    render(<App />);
    const box = screen.getByLabelText("New task");
    await user.type(box, "Steep the oolong{Enter}");
    await user.clear(box);
    await user.type(box, "Wash the pots{Enter}");
    await user.click(screen.getByRole("button", { name: "Active" }));
    await user.click(screen.getByRole("checkbox", { name: "Steep the oolong" }));
    expect(screen.queryByRole("checkbox", { name: "Steep the oolong" })).not.toBeInTheDocument();
    expect(screen.getAllByRole("checkbox")).toHaveLength(1);
    expect(screen.getByText("1 item left")).toBeInTheDocument();
    
  10. a task added under Done waits under All
    render(<App />);
    await user.click(screen.getByRole("button", { name: "Done" }));
    await user.type(screen.getByLabelText("New task"), "Steep the oolong{Enter}");
    expect(screen.queryByRole("checkbox", { name: "Steep the oolong" })).not.toBeInTheDocument();
    expect(screen.getByText("1 item left")).toBeInTheDocument();
    await user.click(screen.getByRole("button", { name: "All" }));
    expect(screen.getByRole("checkbox", { name: "Steep the oolong" })).not.toBeChecked();
    

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).