Skip to the code
cf.

Select all checkboxes

MediumForms

Build App({ items }): a tea shop's restock list. items is an array of { id, name }. Show a checkbox for each item, labelled with its name, and above them a checkbox labelled Select all. Nothing is checked at first. The starter gives items a default so the Preview has something to show; the tests pass their own.

Below the list, a line reads 2 of 4 selected: how many items are checked, out of how many there are.

Select all matches the items. It's checked when every item is checked, and unchecked when none are. When only some are, it's indeterminate: its indeterminate property is true and it isn't checked. HTML has no indeterminate attribute, so you set that DOM property through a ref.

Clicking Select all checks every item, unless every item is already checked, in which case it unchecks them all. With no items, Select all is disabled and the line reads 0 of 0 selected.

<App items={[{ id: 1, name: "Assam" }, { id: 2, name: "Sencha" }]} />

What the tests check

  1. starts with nothing selected
    const items = [{ id: 1, name: "Assam" }, { id: 2, name: "Darjeeling" }, { id: 3, name: "Sencha" }, { id: 4, name: "Oolong" }];
    render(<App items={items} />);
    const all = screen.getByRole("checkbox", { name: "Select all" });
    expect(all).not.toBeChecked();
    expect(all.indeterminate).toBe(false);
    for (const item of items) expect(screen.getByRole("checkbox", { name: item.name })).not.toBeChecked();
    expect(screen.getByText("0 of 4 selected")).toBeInTheDocument();
    
  2. checking one item counts it and makes Select all indeterminate
    const items = [{ id: 1, name: "Assam" }, { id: 2, name: "Darjeeling" }, { id: 3, name: "Sencha" }, { id: 4, name: "Oolong" }];
    render(<App items={items} />);
    await user.click(screen.getByRole("checkbox", { name: "Sencha" }));
    expect(screen.getByRole("checkbox", { name: "Sencha" })).toBeChecked();
    const all = screen.getByRole("checkbox", { name: "Select all" });
    expect(all.indeterminate).toBe(true);
    expect(all).not.toBeChecked();
    expect(screen.getByText("1 of 4 selected")).toBeInTheDocument();
    
  3. checking every item checks Select all
    const items = [{ id: 1, name: "Assam" }, { id: 2, name: "Darjeeling" }, { id: 3, name: "Sencha" }];
    render(<App items={items} />);
    for (const item of items) await user.click(screen.getByRole("checkbox", { name: item.name }));
    const all = screen.getByRole("checkbox", { name: "Select all" });
    expect(all).toBeChecked();
    expect(all.indeterminate).toBe(false);
    expect(screen.getByText("3 of 3 selected")).toBeInTheDocument();
    
  4. Select all checks every item when none are checked
    const items = [{ id: 1, name: "Assam" }, { id: 2, name: "Darjeeling" }, { id: 3, name: "Sencha" }, { id: 4, name: "Oolong" }];
    render(<App items={items} />);
    await user.click(screen.getByRole("checkbox", { name: "Select all" }));
    for (const item of items) expect(screen.getByRole("checkbox", { name: item.name })).toBeChecked();
    expect(screen.getByRole("checkbox", { name: "Select all" })).toBeChecked();
    expect(screen.getByText("4 of 4 selected")).toBeInTheDocument();
    
  5. Select all checks every item when only some are checked
    const items = [{ id: 1, name: "Assam" }, { id: 2, name: "Darjeeling" }, { id: 3, name: "Sencha" }, { id: 4, name: "Oolong" }];
    render(<App items={items} />);
    await user.click(screen.getByRole("checkbox", { name: "Assam" }));
    await user.click(screen.getByRole("checkbox", { name: "Oolong" }));
    await user.click(screen.getByRole("checkbox", { name: "Select all" }));
    for (const item of items) expect(screen.getByRole("checkbox", { name: item.name })).toBeChecked();
    const all = screen.getByRole("checkbox", { name: "Select all" });
    expect(all).toBeChecked();
    expect(all.indeterminate).toBe(false);
    expect(screen.getByText("4 of 4 selected")).toBeInTheDocument();
    
  6. Select all unchecks every item when all are checked
    const items = [{ id: 1, name: "Assam" }, { id: 2, name: "Darjeeling" }, { id: 3, name: "Sencha" }];
    render(<App items={items} />);
    const all = screen.getByRole("checkbox", { name: "Select all" });
    await user.click(all);
    await user.click(all);
    for (const item of items) expect(screen.getByRole("checkbox", { name: item.name })).not.toBeChecked();
    expect(all).not.toBeChecked();
    expect(all.indeterminate).toBe(false);
    expect(screen.getByText("0 of 3 selected")).toBeInTheDocument();
    
  7. unchecking one item after Select all makes it indeterminate again
    const items = [{ id: 1, name: "Assam" }, { id: 2, name: "Darjeeling" }, { id: 3, name: "Sencha" }];
    render(<App items={items} />);
    const all = screen.getByRole("checkbox", { name: "Select all" });
    await user.click(all);
    await user.click(screen.getByRole("checkbox", { name: "Darjeeling" }));
    expect(all.indeterminate).toBe(true);
    expect(all).not.toBeChecked();
    expect(screen.getByText("2 of 3 selected")).toBeInTheDocument();
    
  8. with no items, Select all is disabled and the count is 0 of 0
    render(<App items={[]} />);
    const all = screen.getByRole("checkbox", { name: "Select all" });
    expect(all).toBeDisabled();
    expect(all).not.toBeChecked();
    expect(screen.getByText("0 of 0 selected")).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).