// @vitest-environment jsdom // // The chip family (docs/DESIGN.md "One primitive per button family"). One geometry for a // filter, an applied narrowing, a tag and a suggestion, where five files had five. import { afterEach, describe, expect, it, vi } from "vitest"; import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { useState } from "react"; import { Chip, DismissibleChip } from "./Chip"; import { chipClass } from "./chipStyles"; afterEach(cleanup); describe("Chip", () => { it("is a pill on the 24px floor that never submits a form, and a toggle announces its state", () => { const onSubmit = vi.fn((e: { preventDefault: () => void }) => e.preventDefault()); render(
2024
, ); const chip = screen.getByRole("button", { name: "2024" }); expect(chip.className).toContain("rounded-full"); expect(chip.className).toContain("h-6"); expect(chip.className).toContain("touch-target"); expect(chip.getAttribute("aria-pressed")).toBe("false"); fireEvent.click(chip); expect(onSubmit).not.toHaveBeenCalled(); }); it("a removing chip is named by the removal and shows the ×", () => { const onClick = vi.fn(); render( Music , ); const chip = screen.getByRole("button", { name: "Remove Music" }); expect(chip.getAttribute("title")).toBe("Remove Music"); expect(chip.className).toContain("h-7"); expect(chip.querySelector("svg")).not.toBeNull(); fireEvent.click(chip); expect(onClick).toHaveBeenCalledOnce(); }); // The feed's and Plex's active-filter chips unmount with the filter they remove, // and a keyboard press left the focus on . The chip hands it on itself, for every caller. it("a removing chip pressed from the keyboard hands the focus to its neighbour", () => { function Row() { const [chips, setChips] = useState(["Music", "News"]); return (
{chips.map((c) => ( setChips((all) => all.filter((x) => x !== c))} > {c} ))}
); } render(); const music = screen.getByRole("button", { name: "Remove Music" }); act(() => music.focus()); fireEvent.click(music, { detail: 0 }); expect(screen.queryByRole("button", { name: "Remove Music" })).toBeNull(); expect(document.activeElement).toBe(screen.getByRole("button", { name: "Remove News" })); }); // The neighbour it hands the focus to can go in the same removal — the "Clear all" that // only shows beside enough filters. The focus then goes to the chip before, not to . it("a removing chip whose neighbour goes with it hands the focus to the chip before", async () => { function Row() { const [chips, setChips] = useState(["Music", "News", "Sport"]); return (
{chips.map((c) => ( setChips((all) => all.filter((x) => x !== c))} > {c} ))} {chips.length > 2 && }
); } render(); const sport = screen.getByRole("button", { name: "Remove Sport" }); act(() => sport.focus()); fireEvent.click(sport, { detail: 0 }); expect(screen.queryByRole("button", { name: "Clear all" })).toBeNull(); // the chip right before it — not the row's first control, which Music also is await waitFor(() => expect(document.activeElement).toBe(screen.getByRole("button", { name: "Remove News" })), ); }); it("the measuring mirror lays out exactly like the chip it measures", () => { render( tag , ); const cls = screen.getByRole("button", { name: "tag" }).className; for (const c of chipClass("tag", "xs").split(" ")) expect(cls).toContain(c); }); }); describe("DismissibleChip", () => { it("is two sibling controls at most, never a button in a button", () => { const onAccept = vi.fn(); const onDismiss = vi.fn(); const { container } = render( Music , ); expect(container.querySelector("button button")).toBeNull(); fireEvent.click(screen.getByRole("button", { name: "Add the tag Music" })); fireEvent.click(screen.getByRole("button", { name: "Not now" })); expect(onAccept).toHaveBeenCalledOnce(); expect(onDismiss).toHaveBeenCalledOnce(); }); it("with no body action the label is text, and only the × is a control", () => { render( {}} dismissLabel="Remove Year: 2024"> Year: 2024 , ); expect(screen.getAllByRole("button")).toHaveLength(1); expect(screen.getByText("Year: 2024").closest("button")).toBeNull(); }); });