Files
siftlode/frontend/src/components/ui/Chip.dom.test.tsx
T
peter 81936e9f56 feat(design): every button on its family, the chip primitive, the lint rule (E28 S3b2)
The second half of the button sweep: the remaining hand-rolled buttons in
feature components, the design lab and the components/ui composites now
stand on Button, IconButton, Pressable, Chip or DragHandle.

- Chip + DismissibleChip (filter / token / tag / suggest / ghost, 24px
  floor, 44 on touch) replace five chip geometries; FilterChipGroup
  renders Chips; overflow lanes measure a mirror painted by chipClass.
- The Plex control row is IconButton's player rung (36, 20px glyph,
  round, overlay; 44 on touch); the YT modal bar is on the sm rung with
  pressed cycle toggles and names that survive a folded label.
- New variants: IconButton ghost-accent, danger-soft; Button warn on a
  new state-warn utility. The .btn-{tone} recipes and btnCls are gone.
- Fixed on the way: membership rows (collection editor, Plex playlist
  add) announce checked/mixed; a one-letter tag chip was 21px wide;
  descriptionLinks moved out of lib/ (it renders components).
- local/button-primitive: a raw <button>/<FocusSafeButton> outside the
  control-primitive files is a lint error (contract-tested, seen red).
2026-09-24 23:03:23 +02:00

87 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @vitest-environment jsdom
//
// E28 S3b: 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 { cleanup, fireEvent, render, screen } from "@testing-library/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(
<form onSubmit={onSubmit}>
<Chip pressed={false}>2024</Chip>
</form>,
);
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(
<Chip variant="token" size="md" removeLabel="Remove Music" onClick={onClick}>
Music
</Chip>,
);
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();
});
it("the measuring mirror lays out exactly like the chip it measures", () => {
render(
<Chip variant="tag" size="xs">
tag
</Chip>,
);
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(
<DismissibleChip
variant="suggest"
onClick={onAccept}
bodyLabel="Add the tag Music"
onDismiss={onDismiss}
dismissLabel="Not now"
>
Music
</DismissibleChip>,
);
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(
<DismissibleChip onDismiss={() => {}} dismissLabel="Remove Year: 2024">
Year: 2024
</DismissibleChip>,
);
expect(screen.getAllByRole("button")).toHaveLength(1);
expect(screen.getByText("Year: 2024").closest("button")).toBeNull();
});
});