Skip to the code
cf.

Tea menu list

The shop's menu page lists today's teas. Build TeaMenu, a named export of App.jsx. It gets teas, an array of { id, name, price, soldOut } objects, and onAdd, a function to call when someone adds a tea to their order. The default export, App, is only for the Preview.

Show the teas in a <ul>, one <li> per tea, in the order they come. Each item shows the tea's name, its price with a dollar sign and two decimals (4 is $4.00, 3.5 is $3.50), and a button whose text is Add and the tea's name, like Add Sencha.

Clicking a button calls onAdd with that tea's id. When a tea's soldOut is true, its item also shows the text Sold out, and its button is disabled.

Give every item a key, so React logs no key warning. When teas is empty, show the paragraph No teas on the menu today. instead of the list.

<TeaMenu
  teas={[
    { id: "sencha", name: "Sencha", price: 4, soldOut: false },
    { id: "matcha", name: "Matcha", price: 12.25, soldOut: true },
  ]}
  onAdd={(id) => console.log("added", id)}
/>
 
// draws:
<ul>
  <li>Sencha $4.00 <button>Add Sencha</button></li>
  <li>Matcha $12.25 Sold out <button disabled>Add Matcha</button></li>
</ul>

What the tests check

  1. shows one list item per tea, in order
    const teas = [
      { id: "sencha", name: "Sencha", price: 4, soldOut: false },
      { id: "chai", name: "Masala chai", price: 3.5, soldOut: false },
      { id: "matcha", name: "Matcha", price: 12.25, soldOut: true },
    ];
    render(<TeaMenu teas={teas} onAdd={() => {}} />);
    const items = screen.getAllByRole("listitem");
    expect(items).toHaveLength(3);
    expect(items[0]).toHaveTextContent("Sencha");
    expect(items[1]).toHaveTextContent("Masala chai");
    expect(items[2]).toHaveTextContent("Matcha");
    
  2. shows prices with a dollar sign and two decimals
    const teas = [
      { id: "sencha", name: "Sencha", price: 4, soldOut: false },
      { id: "chai", name: "Masala chai", price: 3.5, soldOut: false },
      { id: "matcha", name: "Matcha", price: 12.25, soldOut: true },
      { id: "mint", name: "Mint", price: 0.5, soldOut: false },
    ];
    render(<TeaMenu teas={teas} onAdd={() => {}} />);
    const items = screen.getAllByRole("listitem");
    expect(items[0]).toHaveTextContent("$4.00");
    expect(items[1]).toHaveTextContent("$3.50");
    expect(items[2]).toHaveTextContent("$12.25");
    expect(items[3]).toHaveTextContent("$0.50");
    
  3. marks only the sold-out teas as Sold out
    const teas = [
      { id: "sencha", name: "Sencha", price: 4, soldOut: false },
      { id: "matcha", name: "Matcha", price: 12.25, soldOut: true },
      { id: "chai", name: "Masala chai", price: 3.5, soldOut: false },
    ];
    render(<TeaMenu teas={teas} onAdd={() => {}} />);
    const items = screen.getAllByRole("listitem");
    expect(items[0]).not.toHaveTextContent("Sold out");
    expect(items[1]).toHaveTextContent("Sold out");
    expect(items[2]).not.toHaveTextContent("Sold out");
    
  4. disables the Add button of a sold-out tea only
    const teas = [
      { id: "sencha", name: "Sencha", price: 4, soldOut: false },
      { id: "matcha", name: "Matcha", price: 12.25, soldOut: true },
    ];
    render(<TeaMenu teas={teas} onAdd={() => {}} />);
    expect(screen.getByRole("button", { name: "Add Sencha" })).toBeEnabled();
    expect(screen.getByRole("button", { name: "Add Matcha" })).toBeDisabled();
    
  5. calls onAdd with the tea's id
    const teas = [
      { id: "sencha", name: "Sencha", price: 4, soldOut: false },
      { id: "chai", name: "Masala chai", price: 3.5, soldOut: false },
    ];
    const onAdd = spy();
    render(<TeaMenu teas={teas} onAdd={onAdd} />);
    await user.click(screen.getByRole("button", { name: "Add Masala chai" }));
    expect(onAdd).toHaveBeenCalledTimes(1);
    expect(onAdd).toHaveBeenCalledWith("chai");
    
  6. clicking a sold-out tea's button doesn't call onAdd
    const teas = [{ id: "matcha", name: "Matcha", price: 12.25, soldOut: true }];
    const onAdd = spy();
    render(<TeaMenu teas={teas} onAdd={onAdd} />);
    await user.click(screen.getByRole("button", { name: "Add Matcha" }));
    expect(onAdd).not.toHaveBeenCalled();
    
  7. shows a message instead of a list when there are no teas
    render(<TeaMenu teas={[]} onAdd={() => {}} />);
    expect(screen.getByText("No teas on the menu today.")).toBeInTheDocument();
    expect(screen.queryByRole("list")).not.toBeInTheDocument();
    
  8. gives every item a key
    const teas = [
      { id: "sencha", name: "Sencha", price: 4, soldOut: false },
      { id: "chai", name: "Masala chai", price: 3.5, soldOut: false },
    ];
    render(<TeaMenu teas={teas} onAdd={() => {}} />);
    expect(logs.join("\n")).not.toMatch(/unique "key"/);
    

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