Skip to the code
cf.

Breadcrumbs

Shoppers can get deep into the tea shop's pages, so each page shows where it is: Home › Shop › Teas › Green. Build Breadcrumbs, a named export of App.jsx, which takes a path like /shop/teas/green. The default export, App, is only for the Preview.

Draw a <nav> with aria-label="Breadcrumb", holding an ordered list (<ol>). The first item is always Home, for /. Then each part of the path gets an item, in order.

Every item except the last is a link to the path up to that part: Home to /, Shop to /shop, Teas to /shop/teas. The last item is the current page: plain text, not a link, with aria-current="page". Each <li> holds only its link or its text (draw any separators with CSS).

Labels are title-cased: hyphens become spaces, and each word starts with a capital letter, with the rest lower case. So loose-leaf is Loose Leaf, and EARL-GREY is Earl Grey. Links use the path parts as they are.

Empty parts are ignored, so a trailing slash or a doubled slash changes nothing: /shop//teas/ is the same as /shop/teas. For /, the only item is Home, as the current page.

<Breadcrumbs path="/shop/teas/green" />
 
// draws:
<nav aria-label="Breadcrumb">
  <ol>
    <li><a href="/">Home</a></li>
    <li><a href="/shop">Shop</a></li>
    <li><a href="/shop/teas">Teas</a></li>
    <li><span aria-current="page">Green</span></li>
  </ol>
</nav>

What the tests check

  1. is a navigation landmark called Breadcrumb
    render(<Breadcrumbs path="/shop/teas/green" />);
    expect(screen.getByRole("navigation", { name: "Breadcrumb" })).toBeInTheDocument();
    
  2. lists Home and each part of the path in an ordered list
    render(<Breadcrumbs path="/shop/teas/green" />);
    const list = within(screen.getByRole("navigation", { name: "Breadcrumb" })).getByRole("list");
    expect(list.tagName).toBe("OL");
    expect(within(list).getAllByRole("listitem").map((item) => item.textContent)).toEqual(["Home", "Shop", "Teas", "Green"]);
    
  3. links every level but the last to the path so far
    render(<Breadcrumbs path="/shop/teas/green" />);
    const links = screen.getAllByRole("link");
    expect(links.map((link) => link.textContent)).toEqual(["Home", "Shop", "Teas"]);
    expect(links.map((link) => link.getAttribute("href"))).toEqual(["/", "/shop", "/shop/teas"]);
    
  4. shows the last level as text marked as the current page
    render(<Breadcrumbs path="/shop/teas/green" />);
    expect(screen.queryByRole("link", { name: "Green" })).not.toBeInTheDocument();
    expect(screen.getByText("Green")).toHaveAttribute("aria-current", "page");
    
  5. title-cases labels and turns hyphens into spaces
    render(<Breadcrumbs path="/shop/loose-leaf/EARL-GREY" />);
    expect(screen.getByRole("link", { name: "Loose Leaf" })).toHaveAttribute("href", "/shop/loose-leaf");
    expect(screen.getByText("Earl Grey")).toHaveAttribute("aria-current", "page");
    
  6. ignores a trailing slash and doubled slashes
    render(<Breadcrumbs path="/shop//teas/" />);
    const items = screen.getAllByRole("listitem");
    expect(items.map((item) => item.textContent)).toEqual(["Home", "Shop", "Teas"]);
    expect(screen.getByRole("link", { name: "Shop" })).toHaveAttribute("href", "/shop");
    expect(screen.getByText("Teas")).toHaveAttribute("aria-current", "page");
    
  7. shows only Home, as the current page, for /
    render(<Breadcrumbs path="/" />);
    expect(screen.getAllByRole("listitem")).toHaveLength(1);
    expect(screen.queryByRole("link")).not.toBeInTheDocument();
    expect(screen.getByText("Home")).toHaveAttribute("aria-current", "page");
    
  8. follows a new path
    const { rerender } = render(<Breadcrumbs path="/shop/teas/green" />);
    rerender(<Breadcrumbs path="/about" />);
    expect(screen.getAllByRole("listitem").map((item) => item.textContent)).toEqual(["Home", "About"]);
    expect(screen.getByRole("link", { name: "Home" })).toHaveAttribute("href", "/");
    expect(screen.getByText("About")).toHaveAttribute("aria-current", "page");
    

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