the assigned sources list is a rac checkboxgroup and safe search uses the same drawn checkbox, extracted to a shared ui component with grouped and standalone modes enforced by a discriminated union. the label carries a 44px pointer-target floor on both axes, the focus ring is driven from rac's focus-visible state and guarded by a test, and toggleSource is gone because the group hands back the whole set.
93 lines
3.4 KiB
TypeScript
93 lines
3.4 KiB
TypeScript
import { act, fireEvent, render, screen, within } from "@testing-library/react";
|
|
import Checkbox, { CheckboxGroup } from "./Checkbox";
|
|
|
|
/**
|
|
* The drawn box, which is the label's one child that does not hold the hidden
|
|
* input. StyleX compiles to class names and jsdom loads no stylesheet, so the
|
|
* class list is the only place the composed ring is observable.
|
|
*/
|
|
function indicator(input: HTMLElement): HTMLElement {
|
|
const label = input.closest("label") as HTMLElement;
|
|
const spans = [...label.querySelectorAll("span")];
|
|
const box = spans.find((span) => !span.contains(input));
|
|
if (box === undefined) throw new Error("the checkbox drew no indicator");
|
|
return box;
|
|
}
|
|
|
|
test("a standalone checkbox reports its state and reads its label as its name", () => {
|
|
const onChange = vi.fn();
|
|
render(
|
|
<Checkbox isSelected onChange={onChange}>
|
|
Safe search
|
|
</Checkbox>,
|
|
);
|
|
|
|
const box = screen.getByRole("checkbox", { name: "Safe search" }) as HTMLInputElement;
|
|
expect(box.checked).toBe(true);
|
|
|
|
fireEvent.click(box);
|
|
// The caller owns the state, so the box reports the value it would move to
|
|
// rather than moving there itself.
|
|
expect(onChange).toHaveBeenCalledWith(false);
|
|
});
|
|
|
|
test("a disabled checkbox keeps its state on screen but takes no input", () => {
|
|
render(
|
|
<Checkbox isSelected isDisabled onChange={vi.fn()}>
|
|
Safe search
|
|
</Checkbox>,
|
|
);
|
|
|
|
const box = screen.getByRole("checkbox", { name: "Safe search" }) as HTMLInputElement;
|
|
expect(box.checked).toBe(true);
|
|
// The disabled input is the whole guard: a browser fires no click on one, and
|
|
// it is out of the tab order. Clicking it here would prove nothing either way,
|
|
// because `fireEvent` dispatches straight at the node and skips that check.
|
|
expect(box.disabled).toBe(true);
|
|
});
|
|
|
|
test("keyboard focus composes the ring onto the drawn box", () => {
|
|
render(
|
|
<Checkbox isSelected={false} onChange={vi.fn()}>
|
|
Ads
|
|
</Checkbox>,
|
|
);
|
|
|
|
const input = screen.getByRole("checkbox", { name: "Ads" });
|
|
const box = indicator(input);
|
|
const idle = box.className.split(" ");
|
|
expect(input.closest("label")?.getAttribute("data-focus-visible")).toBeNull();
|
|
|
|
// React Aria only calls focus visible after the modality is keyboard, which
|
|
// is why a bare focus() is not enough to raise the ring.
|
|
act(() => {
|
|
fireEvent.keyDown(document.body, { key: "Tab" });
|
|
input.focus();
|
|
});
|
|
|
|
expect(input.closest("label")?.getAttribute("data-focus-visible")).toBe("true");
|
|
const ringed = box.className.split(" ");
|
|
// The box gained classes it did not have: the ring is composed onto it, not
|
|
// merely reported by React Aria on the root.
|
|
expect(ringed.length).toBeGreaterThan(idle.length);
|
|
expect(idle.every((name) => ringed.includes(name))).toBe(true);
|
|
});
|
|
|
|
test("inside a group the group owns the selection, and the group carries the name", () => {
|
|
const onChange = vi.fn();
|
|
render(
|
|
<CheckboxGroup aria-label="Assigned sources" value={["1"]} onChange={onChange}>
|
|
<Checkbox value="1">Ads</Checkbox>
|
|
<Checkbox value="2">Trackers</Checkbox>
|
|
</CheckboxGroup>,
|
|
);
|
|
|
|
const group = screen.getByRole("group", { name: "Assigned sources" });
|
|
expect((within(group).getByRole("checkbox", { name: "Ads" }) as HTMLInputElement).checked).toBe(true);
|
|
expect((within(group).getByRole("checkbox", { name: "Trackers" }) as HTMLInputElement).checked).toBe(false);
|
|
|
|
// The group reports the whole set, not the box that moved.
|
|
fireEvent.click(within(group).getByRole("checkbox", { name: "Trackers" }));
|
|
expect(onChange).toHaveBeenCalledWith(["1", "2"]);
|
|
});
|