Skip to the code
cf.

Tag input

MediumForms

Build App: the tag box a tea shop uses to label its teas, with tags like green, smoky, and caffeine free.

There's a text box labelled Add a tag. Pressing Enter, or typing a comma, adds what's in the box as a tag, trimmed, and empties the box. The comma is never typed into the box.

If the trimmed text is empty, or is already a tag ignoring case (Green and green are the same tag), nothing is added and the box keeps its text.

The tags show in a list labelled Tags (<ul aria-label="Tags">), one list item per tag, in the order they were added. Each item shows the tag and a button whose accessible name is Remove and the tag, like Remove green tea. It removes that tag.

Pressing Backspace in the box while it's empty removes the last tag. With text in the box, Backspace deletes a character as usual.

<ul aria-label="Tags">
  <li>green <button aria-label="Remove green">×</button></li>
  <li>smoky <button aria-label="Remove smoky">×</button></li>
</ul>
<label for="tag">Add a tag</label>
<input id="tag" />

What the tests check

  1. Enter adds the trimmed text as a tag and empties the box
    render(<App />);
    const box = screen.getByLabelText("Add a tag");
    await user.type(box, "  green tea  {Enter}");
    const items = within(screen.getByRole("list", { name: "Tags" })).getAllByRole("listitem");
    expect(items).toHaveLength(1);
    expect(items[0]).toHaveTextContent("green tea");
    expect(screen.getByRole("button", { name: "Remove green tea" })).toBeInTheDocument();
    expect(box).toHaveValue("");
    
  2. a comma adds the tag, and never shows up in the box
    render(<App />);
    const box = screen.getByLabelText("Add a tag");
    await user.type(box, "smoky,");
    expect(screen.getByRole("button", { name: "Remove smoky" })).toBeInTheDocument();
    expect(box).toHaveValue("");
    
  3. blank text adds nothing
    render(<App />);
    await user.type(screen.getByLabelText("Add a tag"), "   {Enter},");
    expect(within(screen.getByRole("list", { name: "Tags" })).queryAllByRole("listitem")).toHaveLength(0);
    
  4. a tag that's already there, in any case, isn't added again, and the box keeps its text
    render(<App />);
    const box = screen.getByLabelText("Add a tag");
    await user.type(box, "Green{Enter}");
    await user.type(box, "green{Enter}");
    expect(within(screen.getByRole("list", { name: "Tags" })).getAllByRole("listitem")).toHaveLength(1);
    expect(box).toHaveValue("green");
    
  5. tags stay in the order they were added
    render(<App />);
    await user.type(screen.getByLabelText("Add a tag"), "oolong{Enter}smoky,floral{Enter}");
    const items = within(screen.getByRole("list", { name: "Tags" })).getAllByRole("listitem");
    expect(items).toHaveLength(3);
    expect(items[0]).toHaveTextContent("oolong");
    expect(items[1]).toHaveTextContent("smoky");
    expect(items[2]).toHaveTextContent("floral");
    
  6. Backspace in an empty box removes the last tag
    render(<App />);
    const box = screen.getByLabelText("Add a tag");
    await user.type(box, "green{Enter}black{Enter}");
    await user.keyboard("{Backspace}");
    expect(screen.queryByRole("button", { name: "Remove black" })).not.toBeInTheDocument();
    expect(screen.getByRole("button", { name: "Remove green" })).toBeInTheDocument();
    
  7. Backspace with text in the box deletes a character, not a tag
    render(<App />);
    const box = screen.getByLabelText("Add a tag");
    await user.type(box, "green{Enter}bla{Backspace}");
    expect(box).toHaveValue("bl");
    expect(screen.getByRole("button", { name: "Remove green" })).toBeInTheDocument();
    
  8. a tag's remove button removes just that tag
    render(<App />);
    await user.type(screen.getByLabelText("Add a tag"), "green{Enter}black{Enter}white{Enter}");
    await user.click(screen.getByRole("button", { name: "Remove black" }));
    const items = within(screen.getByRole("list", { name: "Tags" })).getAllByRole("listitem");
    expect(items).toHaveLength(2);
    expect(items[0]).toHaveTextContent("green");
    expect(items[1]).toHaveTextContent("white");
    
  9. a removed tag can be added again
    render(<App />);
    const box = screen.getByLabelText("Add a tag");
    await user.type(box, "chai{Enter}");
    await user.click(screen.getByRole("button", { name: "Remove chai" }));
    await user.type(box, "Chai{Enter}");
    expect(screen.getByRole("button", { name: "Remove Chai" })).toBeInTheDocument();
    expect(box).toHaveValue("");
    

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