import { resolve } from "node:path";
import { fireEvent, render } from "@testing-library/preact";
import { describe, expect, it, vi } from "vitest";
import { ChipSet } from "./ChipSet.js";
const ITEMS = [
{ id: "outdoors", label: "Outdoors" },
{ id: "making", label: "Making" },
{ id: "games", label: "Games" },
];
describe("ChipSet", () => {
it("is a named group of chips", () => {
const { container } = render();
const set = container.querySelector(".strand-chip-set");
expect(set?.getAttribute("role")).toBe("group");
expect(set?.getAttribute("aria-label")).toBe("Interests");
expect(container.querySelectorAll(".strand-chip-set__chip")).toHaveLength(3);
});
// "Any of these" and "one of these" are different promises. Painting
// them identically tells a screen reader nothing about which it is.
it("uses a radiogroup for single-select and a group for multi", () => {
const { container: multi } = render();
expect(multi.querySelector(".strand-chip-set")?.getAttribute("role")).toBe("group");
const { container: single } = render(
,
);
expect(single.querySelector(".strand-chip-set")?.getAttribute("role")).toBe("radiogroup");
expect(single.querySelector(".strand-chip-set__chip")?.getAttribute("role")).toBe("radio");
});
it("announces selection with the attribute that also styles it", () => {
const { container } = render(
,
);
const pressed = container.querySelectorAll('[aria-pressed="true"]');
expect(pressed).toHaveLength(1);
expect(pressed[0].textContent).toBe("Making");
});
it("uses aria-checked in single-select, not aria-pressed", () => {
const { container } = render(
,
);
expect(container.querySelectorAll('[aria-checked="true"]')).toHaveLength(1);
expect(container.querySelectorAll("[aria-pressed]")).toHaveLength(0);
});
it("adds a selection in multi-select and keeps the rest", () => {
const onSelectionChange = vi.fn();
const { container } = render(
,
);
fireEvent.click(container.querySelectorAll(".strand-chip-set__chip")[1]);
expect(onSelectionChange).toHaveBeenCalledWith(["outdoors", "making"]);
});
it("removes a selection in multi-select", () => {
const onSelectionChange = vi.fn();
const { container } = render(
,
);
fireEvent.click(container.querySelectorAll(".strand-chip-set__chip")[0]);
expect(onSelectionChange).toHaveBeenCalledWith(["making"]);
});
// The whole point of the mode: choosing replaces rather than adds.
it("replaces the selection in single-select", () => {
const onSelectionChange = vi.fn();
const { container } = render(
,
);
fireEvent.click(container.querySelectorAll(".strand-chip-set__chip")[2]);
expect(onSelectionChange).toHaveBeenCalledWith(["games"]);
});
// A filter strip above a dense list is drawn smaller than one that is the
// page's main control. Before `size`, a consumer wanting that had to override
// `.strand-chip-set__chip`, which the dogfood protocol forbids.
it("in single-select, keeps only the selected chip (or the first) in the tab order and moves the selection with the arrows", () => {
const onSelectionChange = vi.fn();
const { getAllByRole } = render();
const chips = getAllByRole("radio");
expect(chips.map((c) => c.tabIndex)).toEqual([-1, 0, -1]);
fireEvent.keyDown(chips[1], { key: "ArrowRight" });
expect(onSelectionChange).toHaveBeenLastCalledWith(["games"]);
expect(document.activeElement).toBe(chips[2]);
fireEvent.keyDown(chips[1], { key: "ArrowLeft" });
expect(onSelectionChange).toHaveBeenLastCalledWith(["outdoors"]);
fireEvent.keyDown(chips[1], { key: "End" });
expect(onSelectionChange).toHaveBeenLastCalledWith(["games"]);
fireEvent.keyDown(chips[1], { key: "Home" });
expect(onSelectionChange).toHaveBeenLastCalledWith(["outdoors"]);
});
it("in single-select, the arrows wrap around and nothing selected starts from the first chip", () => {
const onSelectionChange = vi.fn();
const { getAllByRole } = render();
const chips = getAllByRole("radio");
expect(chips.map((c) => c.tabIndex)).toEqual([0, -1, -1]);
fireEvent.keyDown(chips[0], { key: "ArrowLeft" });
expect(onSelectionChange).toHaveBeenLastCalledWith(["games"]);
});
it("in multi-select, every chip stays in the tab order and the arrows do nothing", () => {
const onSelectionChange = vi.fn();
const { getAllByRole } = render();
const chips = getAllByRole("button");
expect(chips.map((c) => c.tabIndex)).toEqual([0, 0, 0]);
fireEvent.keyDown(chips[0], { key: "ArrowRight" });
expect(onSelectionChange).not.toHaveBeenCalled();
});
});
import { snapshotFixtures } from "../../test/snapshot.js";
import { snapshotStylesheet } from "../../test/stylesheet.js";
import { fixtures } from "./ChipSet.fixtures.js";
snapshotFixtures(ChipSet, fixtures);
snapshotStylesheet(resolve(__dirname, "./ChipSet.css"));