Skip to the code
cf.

Transfer list

When a customer builds a tasting box, they move teas from a list of what's available to a list of what they've chosen. Build App: it takes left and right, the starting lists of tea names (strings, unique across both lists). When they aren't given, as in the Preview, use a few names of your own.

Show two lists side by side: a <section> titled by an <h2> reading Available (the left list) and one titled Chosen (the right list), each section labelled by its heading through aria-labelledby. Each tea is an <li> with a <label> holding a checkbox and the tea's name. Every checkbox starts unchecked. A list with no teas shows No teas instead.

Between the lists are four buttons, named with aria-label: Move all right, Move selected right, Move selected left, and Move all left (their text can be », ›, ‹, «). The selected buttons move the checked teas on their side; the all buttons move every tea on their side. Moved teas go to the end of the other list, in the order they were in, and arrive unchecked. Checks on the other side stay as they were.

A button that would move nothing is disabled: Move selected right while no tea in Available is checked, Move all right while Available is empty, and the same for the left buttons with Chosen. Don't change the arrays you're given.

<App left={["Sencha", "Assam", "Oolong"]} right={["Chai"]} />
 
// check Oolong and Sencha, then press Move selected right:
// Available: Assam
// Chosen: Chai, Sencha, Oolong

What the tests check

  1. shows both lists with their teas, nothing checked
    const left = ["Sencha", "Assam", "Oolong", "Rooibos"];
    const right = ["Chai"];
    const teasIn = (name) => within(screen.getByRole("region", { name })).queryAllByRole("listitem").map((li) => li.textContent.trim());
    const button = (name) => screen.getByRole("button", { name });
    render(<App left={left} right={right} />);
    expect(teasIn("Available")).toEqual(["Sencha", "Assam", "Oolong", "Rooibos"]);
    expect(teasIn("Chosen")).toEqual(["Chai"]);
    expect(screen.getAllByRole("checkbox").some((input) => input.checked)).toBe(false);
    
  2. each Move selected button waits for a checked tea on its own side
    const left = ["Sencha", "Assam", "Oolong", "Rooibos"];
    const right = ["Chai"];
    const teasIn = (name) => within(screen.getByRole("region", { name })).queryAllByRole("listitem").map((li) => li.textContent.trim());
    const button = (name) => screen.getByRole("button", { name });
    render(<App left={left} right={right} />);
    expect(button("Move selected right")).toBeDisabled();
    expect(button("Move selected left")).toBeDisabled();
    await user.click(screen.getByRole("checkbox", { name: "Chai" }));
    expect(button("Move selected left")).toBeEnabled();
    expect(button("Move selected right")).toBeDisabled();
    await user.click(screen.getByRole("checkbox", { name: "Chai" }));
    await user.click(screen.getByRole("checkbox", { name: "Assam" }));
    expect(button("Move selected right")).toBeEnabled();
    expect(button("Move selected left")).toBeDisabled();
    
  3. moves the checked teas right, in order, to the end, unchecked
    const left = ["Sencha", "Assam", "Oolong", "Rooibos"];
    const right = ["Chai"];
    const teasIn = (name) => within(screen.getByRole("region", { name })).queryAllByRole("listitem").map((li) => li.textContent.trim());
    const button = (name) => screen.getByRole("button", { name });
    render(<App left={left} right={right} />);
    await user.click(screen.getByRole("checkbox", { name: "Oolong" }));
    await user.click(screen.getByRole("checkbox", { name: "Sencha" }));
    await user.click(button("Move selected right"));
    expect(teasIn("Available")).toEqual(["Assam", "Rooibos"]);
    expect(teasIn("Chosen")).toEqual(["Chai", "Sencha", "Oolong"]);
    expect(screen.getByRole("checkbox", { name: "Sencha" })).not.toBeChecked();
    expect(screen.getByRole("checkbox", { name: "Oolong" })).not.toBeChecked();
    expect(button("Move selected right")).toBeDisabled();
    expect(button("Move selected left")).toBeDisabled();
    
  4. moves the checked teas left
    const left = ["Sencha", "Assam", "Oolong", "Rooibos"];
    const right = ["Chai"];
    const teasIn = (name) => within(screen.getByRole("region", { name })).queryAllByRole("listitem").map((li) => li.textContent.trim());
    const button = (name) => screen.getByRole("button", { name });
    render(<App left={left} right={["Chai", "Mint", "Earl Grey"]} />);
    await user.click(screen.getByRole("checkbox", { name: "Earl Grey" }));
    await user.click(screen.getByRole("checkbox", { name: "Chai" }));
    await user.click(button("Move selected left"));
    expect(teasIn("Available")).toEqual(["Sencha", "Assam", "Oolong", "Rooibos", "Chai", "Earl Grey"]);
    expect(teasIn("Chosen")).toEqual(["Mint"]);
    
  5. keeps the checks on the other side when moving
    const left = ["Sencha", "Assam", "Oolong", "Rooibos"];
    const right = ["Chai"];
    const teasIn = (name) => within(screen.getByRole("region", { name })).queryAllByRole("listitem").map((li) => li.textContent.trim());
    const button = (name) => screen.getByRole("button", { name });
    render(<App left={left} right={right} />);
    await user.click(screen.getByRole("checkbox", { name: "Chai" }));
    await user.click(screen.getByRole("checkbox", { name: "Assam" }));
    await user.click(button("Move selected right"));
    expect(screen.getByRole("checkbox", { name: "Chai" })).toBeChecked();
    expect(screen.getByRole("checkbox", { name: "Assam" })).not.toBeChecked();
    await user.click(button("Move selected left"));
    expect(teasIn("Available")).toEqual(["Sencha", "Oolong", "Rooibos", "Chai"]);
    expect(teasIn("Chosen")).toEqual(["Assam"]);
    
  6. Move all moves every tea, and then disables itself
    const left = ["Sencha", "Assam", "Oolong", "Rooibos"];
    const right = ["Chai"];
    const teasIn = (name) => within(screen.getByRole("region", { name })).queryAllByRole("listitem").map((li) => li.textContent.trim());
    const button = (name) => screen.getByRole("button", { name });
    render(<App left={left} right={right} />);
    await user.click(screen.getByRole("checkbox", { name: "Rooibos" }));
    await user.click(button("Move all right"));
    expect(teasIn("Chosen")).toEqual(["Chai", "Sencha", "Assam", "Oolong", "Rooibos"]);
    expect(screen.getByRole("checkbox", { name: "Rooibos" })).not.toBeChecked();
    expect(button("Move all right")).toBeDisabled();
    expect(button("Move selected right")).toBeDisabled();
    await user.click(button("Move all left"));
    expect(teasIn("Available")).toEqual(["Chai", "Sencha", "Assam", "Oolong", "Rooibos"]);
    expect(teasIn("Chosen")).toEqual([]);
    expect(button("Move all left")).toBeDisabled();
    
  7. an empty list says No teas
    const left = ["Sencha", "Assam", "Oolong", "Rooibos"];
    const right = ["Chai"];
    const teasIn = (name) => within(screen.getByRole("region", { name })).queryAllByRole("listitem").map((li) => li.textContent.trim());
    const button = (name) => screen.getByRole("button", { name });
    render(<App left={left} right={[]} />);
    expect(within(screen.getByRole("region", { name: "Chosen" })).getByText("No teas")).toBeInTheDocument();
    expect(button("Move all left")).toBeDisabled();
    expect(button("Move all right")).toBeEnabled();
    await user.click(button("Move all right"));
    expect(within(screen.getByRole("region", { name: "Available" })).getByText("No teas")).toBeInTheDocument();
    expect(within(screen.getByRole("region", { name: "Chosen" })).queryByText("No teas")).not.toBeInTheDocument();
    
  8. doesn't change the arrays it's given
    const left = ["Sencha", "Assam", "Oolong", "Rooibos"];
    const right = ["Chai"];
    const teasIn = (name) => within(screen.getByRole("region", { name })).queryAllByRole("listitem").map((li) => li.textContent.trim());
    const button = (name) => screen.getByRole("button", { name });
    const frozenLeft = Object.freeze([...left]);
    const frozenRight = Object.freeze([...right]);
    render(<App left={frozenLeft} right={frozenRight} />);
    await user.click(screen.getByRole("checkbox", { name: "Assam" }));
    await user.click(button("Move selected right"));
    await user.click(button("Move all left"));
    expect(teasIn("Available")).toEqual(["Sencha", "Oolong", "Rooibos", "Chai", "Assam"]);
    expect(frozenLeft).toEqual(["Sencha", "Assam", "Oolong", "Rooibos"]);
    expect(frozenRight).toEqual(["Chai"]);
    

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