test(segmented): drive the control in a real browser

Roving focus, Home/End, wrap, toggle mode, disabled and the chosen
segment's fill are now asserted through key presses and computed styles
instead of markup greps. The static render test is deleted; its two
source sweeps move to segmented-single-implementation.test.ts.

The browser setup sets the default theme class so colour tokens
resolve, and pre-bundles the browser files' dependencies so a cold
cache does not reload the page mid-run.
This commit is contained in:
Marc-André Bombeck committed 2026-10-23 12:54:24 +02:00
1 parent 3d1692204b
commit ca1cceb3a9
7 files changed
+305 -243

No files matched your search

@@ -1,7 +1,7 @@
{
"$comment": "tests that read a source file by path, per test file under src/. Maintained by src/__tests__/test-realism-ratchet.test.ts.",
"count": 360,
"total": 360,
"count": 359,
"total": 359,
"entries": {
"__tests__/account-payload-consumer-guard.test.ts": 1,
"__tests__/admin-data-wipe-completeness.test.ts": 1,
@@ -264,7 +264,6 @@
"components/ui/__tests__/card-density.test.tsx": 1,
"components/ui/__tests__/card-header-rows.test.ts": 1,
"components/ui/__tests__/filter-bar.test.tsx": 1,
"components/ui/__tests__/segmented.test.tsx": 1,
"hooks/__tests__/use-insight-status.test.ts": 1,
"lib/__tests__/geo-offline-detection.test.ts": 1,
"lib/__tests__/query-keys.test.ts": 1,
@@ -0,0 +1,81 @@
/**
* Segmented is the one segmented control in the tree. Its behaviour (roving
* focus, toggle mode, disabled, the chosen segment's fill) is driven in a real
* browser by `segmented.browser.test.tsx`; this file holds the source sweeps
* that keep a second control from appearing beside it.
*/
import { describe, expect, it } from "vitest";
describe("one segmented control", () => {
it("is the only segmented-control component in the tree", async () => {
const { join } = await import("node:path");
const { readSourceText, walkSourceFiles } =
await import("@/__tests__/helpers/source-files");
const src = join(__dirname, "../../..");
const defining = walkSourceFiles(src, { floor: 2000 })
.filter((p) => !p.includes("__tests__/"))
.filter((p) =>
/export function (?:Segmented\w*|ViewToggle|\w*SegmentedToggle)\b/.test(
readSourceText(join(src, p)),
),
);
expect(defining).toEqual(["components/ui/segmented.tsx"]);
});
});
/**
* A row of buttons whose `aria-pressed` compares a state to each button's
* value is a segmented control spelled by hand: six of them sat beside
* `Segmented` (the narrative period, the lab result type, both admin filters,
* the chart range tabs, the mood chart's copy of them), each with its own
* size, fill and gap. New ones are refused; the ones that remain are not one
* choice on one track, and say why.
*/
describe("no hand-built segmented control", () => {
const PRESSED_BY_VALUE = /aria-pressed=\{[^}]*===[^}]*\}/g;
const NOT_A_SEGMENT: Record<string, string> = {
"components/settings/security-section/passkey-list-section.tsx":
"one method is disabled while the account has no passkey; Segmented disables the whole track",
"components/medications/units-per-dose-field.tsx":
"a chip row that opens a free-number field for any other count",
"components/body-sites/body-sites-section.tsx":
"wrapping side chips under a body-site picker, not a track",
"components/illness/log-day-sheet.tsx":
"a graded severity scale per symptom, one square per level",
"components/cycle/log-day-sheet.tsx":
"a graded severity scale per symptom, one circle per level",
"components/charts/chart-overlay-controls.tsx":
"baselines stay pressable while greyed, with a tooltip saying why there is no data",
"components/timeline/timeline-view.tsx":
"a single button that opens the range sheet, beside the zoom Segmented",
};
function pressedByValue(source: string): number {
return (source.match(PRESSED_BY_VALUE) ?? []).length;
}
it("finds none outside the reasoned exceptions", async () => {
const { join } = await import("node:path");
const { readSourceText, stripComments, walkSourceFiles } =
await import("@/__tests__/helpers/source-files");
const src = join(__dirname, "../../..");
const found = walkSourceFiles(src, { floor: 2000 })
.filter((p) => /^(components|app)\//.test(p))
.filter((p) => !p.includes("__tests__/"))
.filter((p) => p !== "components/ui/segmented.tsx")
.filter(
(p) => pressedByValue(stripComments(readSourceText(join(src, p)))) > 0,
)
.sort();
expect(found).toEqual(Object.keys(NOT_A_SEGMENT).sort());
});
it("counter-check: catches a hand-built pair, not an on/off toggle", () => {
expect(
pressedByValue(`<Button aria-pressed={filter === "all"}>All</Button>
<Button aria-pressed={filter === "failed"}>Failed</Button>`),
).toBe(2);
expect(pressedByValue(`<Button aria-pressed={open}>Show</Button>`)).toBe(0);
});
});
@@ -1,8 +1,12 @@
import { useState } from "react";
import { LayoutGrid, Table } from "lucide-react";
import { describe, expect, it, vi } from "vitest";
import { userEvent } from "vitest/browser";
import { renderWithApp } from "../../../../tests/browser/render";
import { Segmented } from "../segmented";
import { Segmented, type SegmentedProps } from "../segmented";
type Zoom = "day" | "week" | "month";
const OPTIONS = [
{ value: "day", label: "Day" },
@@ -10,22 +14,216 @@ const OPTIONS = [
{ value: "month", label: "Month" },
] as const;
describe("<Segmented> smoke", () => {
it("moves with the arrow key", async () => {
const onChange = vi.fn();
/**
* The control as a caller holds it: the value lives in the parent and moves
* only when `onChange` says so, which is what makes the roving tab stop
* follow the selection.
*/
function Controlled({
initial = "day",
onChange,
...props
}: Partial<Omit<SegmentedProps<Zoom>, "value">> & { initial?: Zoom }) {
const [value, setValue] = useState<Zoom>(initial);
return (
<>
<button type="button">Before</button>
<Segmented
options={OPTIONS}
label="Zoom"
slot="zoom"
{...props}
value={value}
onChange={(next) => {
onChange?.(next);
setValue(next);
}}
/>
<button type="button">After</button>
</>
);
}
async function mount(props: Parameters<typeof Controlled>[0] = {}) {
const onChange = vi.fn();
const { screen } = await renderWithApp(
<Controlled onChange={onChange} {...props} />,
);
const radio = (name: string) => screen.getByRole("radio", { name });
return { screen, onChange, radio };
}
describe("<Segmented> in radio mode", () => {
it("is one tab stop, on the chosen segment", async () => {
const { screen, radio } = await mount({ initial: "week" });
await expect
.element(screen.getByRole("radiogroup", { name: "Zoom" }))
.toBeInTheDocument();
await expect.element(radio("Week")).toBeChecked();
await expect.element(radio("Day")).not.toBeChecked();
await screen.getByRole("button", { name: "Before" }).click();
await userEvent.tab();
await expect.element(radio("Week")).toHaveFocus();
await userEvent.tab();
await expect
.element(screen.getByRole("button", { name: "After" }))
.toHaveFocus();
});
it("moves focus and selection together with the arrow keys", async () => {
const { onChange, radio } = await mount();
await radio("Day").click();
onChange.mockClear();
await userEvent.keyboard("{ArrowRight}");
await expect.element(radio("Week")).toHaveFocus();
await expect.element(radio("Week")).toBeChecked();
await userEvent.keyboard("{ArrowDown}");
await expect.element(radio("Month")).toBeChecked();
await userEvent.keyboard("{ArrowLeft}");
await expect.element(radio("Week")).toHaveFocus();
expect(onChange.mock.calls).toEqual([["week"], ["month"], ["week"]]);
});
it("wraps at both ends", async () => {
const { radio } = await mount({ initial: "month" });
await radio("Month").click();
await userEvent.keyboard("{ArrowRight}");
await expect.element(radio("Day")).toHaveFocus();
await expect.element(radio("Day")).toBeChecked();
await userEvent.keyboard("{ArrowUp}");
await expect.element(radio("Month")).toHaveFocus();
await expect.element(radio("Month")).toBeChecked();
});
it("jumps to the first and last segment with Home and End", async () => {
const { onChange, radio } = await mount({ initial: "week" });
await radio("Week").click();
await userEvent.keyboard("{End}");
await expect.element(radio("Month")).toHaveFocus();
await userEvent.keyboard("{Home}");
await expect.element(radio("Day")).toHaveFocus();
await expect.element(radio("Day")).toBeChecked();
expect(onChange).toHaveBeenLastCalledWith("day");
});
it("fires again on the chosen segment, which can open something", async () => {
const { onChange, radio } = await mount({ initial: "day" });
await radio("Day").click();
await radio("Day").click();
expect(onChange.mock.calls).toEqual([["day"], ["day"]]);
});
it("takes no press and no key while disabled", async () => {
const { screen, onChange, radio } = await mount({ disabled: true });
await expect
.element(screen.getByRole("radiogroup", { name: "Zoom" }))
.toHaveAttribute("aria-disabled", "true");
await expect.element(radio("Week")).toBeDisabled();
await radio("Week").click({ force: true });
await screen.getByRole("button", { name: "Before" }).click();
await userEvent.tab();
await userEvent.keyboard("{ArrowRight}{End}");
expect(onChange).not.toHaveBeenCalled();
await expect.element(radio("Day")).toBeChecked();
});
});
describe("<Segmented> in toggle mode", () => {
it("renders pressed buttons and ignores a press on the active one", async () => {
const { screen, onChange } = await mount({ mode: "toggle" });
await expect
.element(screen.getByRole("group", { name: "Zoom" }))
.toBeInTheDocument();
expect(screen.getByRole("radio").elements()).toHaveLength(0);
const day = screen.getByRole("button", { name: "Day" });
const week = screen.getByRole("button", { name: "Week" });
await expect.element(day).toHaveAttribute("aria-pressed", "true");
await day.click();
expect(onChange).not.toHaveBeenCalled();
await week.click();
expect(onChange).toHaveBeenCalledExactlyOnceWith("week");
await expect.element(week).toHaveAttribute("aria-pressed", "true");
await expect.element(day).toHaveAttribute("aria-pressed", "false");
});
it("leaves every segment its own tab stop and the arrows alone", async () => {
const { screen, onChange } = await mount({ mode: "toggle" });
await screen.getByRole("button", { name: "Day" }).click();
await userEvent.keyboard("{ArrowRight}");
expect(onChange).not.toHaveBeenCalled();
await userEvent.tab();
await expect
.element(screen.getByRole("button", { name: "Week" }))
.toHaveFocus();
});
});
describe("<Segmented> look", () => {
it("fills only the chosen segment in the primary colour, on no track", async () => {
const { screen, radio } = await mount({ initial: "week" });
const group = screen.getByRole("radiogroup").element();
const probe = document.createElement("span");
probe.className = "bg-primary";
document.body.append(probe);
const primary = getComputedStyle(probe).backgroundColor;
probe.remove();
const background = (name: string) =>
getComputedStyle(radio(name).element()).backgroundColor;
expect(background("Week")).toBe(primary);
expect(background("Day")).not.toBe(primary);
expect(getComputedStyle(group).backgroundColor).toBe("rgba(0, 0, 0, 0)");
});
it("sets the small size in smaller type and keeps the 36 px floor", async () => {
const { radio } = await mount();
const md = radio("Day").element();
expect(getComputedStyle(md).fontSize).toBe("14px");
expect(md.getBoundingClientRect().height).toBeGreaterThanOrEqual(36);
const { screen } = await renderWithApp(
<Segmented
options={OPTIONS}
value="day"
onChange={onChange}
label="Zoom"
onChange={() => {}}
label="Range"
size="sm"
/>,
);
await screen.getByRole("radio", { name: "Day" }).click();
await userEvent.keyboard("{ArrowRight}");
expect(onChange).toHaveBeenLastCalledWith("week");
const sm = screen
.getByRole("radiogroup", { name: "Range" })
.getByRole("radio", { name: "Day" })
.element();
expect(getComputedStyle(sm).fontSize).toBe("12px");
expect(sm.getBoundingClientRect().height).toBeGreaterThanOrEqual(44);
});
it("names icon-only segments without showing the label", async () => {
const onChange = vi.fn();
const { screen } = await renderWithApp(
<Segmented
mode="toggle"
iconOnly
options={[
{ value: "cards", label: "Cards", icon: LayoutGrid },
{ value: "table", label: "Table", icon: Table },
]}
value="cards"
onChange={onChange}
label="View"
optionSlot={(v) => `medications-view-${v}`}
/>,
);
const table = screen.getByRole("button", { name: "Table" });
await expect.element(table).toHaveAttribute("title", "Table");
await expect
.element(screen.getByRole("radio", { name: "Week" }))
.toHaveFocus();
.element(table)
.toHaveAttribute("data-slot", "medications-view-table");
await expect.element(screen.getByText("Cards")).not.toBeInTheDocument();
await table.click();
expect(onChange).toHaveBeenCalledWith("table");
});
});
@@ -1,227 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import { isValidElement, type ReactElement, type ReactNode } from "react";
import { LayoutGrid, Table } from "lucide-react";
import { Segmented, type SegmentedProps } from "../segmented";
const OPTIONS = [
{ value: "recent", label: "Last 90 days" },
{ value: "all", label: "All" },
] as const;
type Props = SegmentedProps<"recent" | "all">;
function html(props: Partial<Props> = {}) {
return renderToStaticMarkup(
<Segmented
options={OPTIONS}
value="recent"
onChange={() => {}}
label="History"
slot="history-view"
{...props}
/>,
);
}
/** The segment buttons of a rendered tree, for driving their handlers. */
function buttons(node: ReactNode): ReactElement<{ onClick: () => void }>[] {
const out: ReactElement<{ onClick: () => void }>[] = [];
const walk = (n: ReactNode) => {
if (Array.isArray(n)) return n.forEach(walk);
if (!isValidElement(n)) return;
const el = n as ReactElement<{ children?: ReactNode; onClick: () => void }>;
if (el.type === "button") out.push(el);
walk(el.props.children);
};
walk(node);
return out;
}
vi.mock("@/hooks/use-roving-radio-group", () => ({
useRovingRadioGroup: () => ({
getRadioProps: (i: number) => ({ tabIndex: i === 0 ? 0 : -1 }),
}),
}));
describe("<Segmented>", () => {
it("is a radiogroup with one checked radio by default", () => {
const out = html();
expect(out).toContain('role="radiogroup"');
expect(out).toContain('aria-label="History"');
expect(out.match(/role="radio"/g)?.length).toBe(2);
expect(out.match(/aria-checked="true"/g)?.length).toBe(1);
expect(out).toContain('data-slot="history-view-all"');
});
it("gives every segment the 44 px floor on a coarse pointer", () => {
const out = html();
expect(out.match(/pointer-coarse:min-h-11/g)?.length).toBe(2);
expect(out).toContain("min-h-9");
});
it("fills the chosen segment in the primary colour, on no track", () => {
const out = html();
const segments = out.match(/<button[^>]*>/g) ?? [];
expect(segments).toHaveLength(2);
const chosen = segments.filter((b) => b.includes('aria-checked="true"'));
expect(chosen).toHaveLength(1);
expect(chosen[0]).toContain("bg-primary text-primary-foreground");
const other = segments.find((b) => b.includes('aria-checked="false"'));
expect(other).not.toContain("bg-primary");
const group = out.match(/<div[^>]*role="radiogroup"[^>]*>/)?.[0];
expect(group).not.toContain("bg-muted");
});
it("sets a chart header's range row in the small type", () => {
expect(html()).toContain("text-sm");
const small = html({ size: "sm" });
expect(small).toContain("text-xs");
expect(small).not.toContain("text-sm");
expect(small.match(/min-h-11/g)?.length).toBe(2);
});
it("keeps a disabled chosen segment on the readable token pair", () => {
const chosen = html({ disabled: true })
.match(/<button[^>]*>/g)
?.find((b) => b.includes('aria-checked="true"'));
expect(chosen).toContain("disabled:bg-primary-disabled");
expect(chosen).toContain("disabled:opacity-100");
});
it("renders pressed buttons in toggle mode", () => {
const out = html({ mode: "toggle" });
expect(out).toContain('role="group"');
expect(out).not.toContain('role="radio"');
expect(out).toContain('aria-pressed="true"');
expect(out).toContain('aria-pressed="false"');
});
it("names icon-only segments and sizes them square", () => {
const out = renderToStaticMarkup(
<Segmented
mode="toggle"
iconOnly
options={[
{ value: "cards", label: "Cards", icon: LayoutGrid },
{ value: "table", label: "Table", icon: Table },
]}
value="cards"
onChange={() => {}}
label="View"
optionSlot={(v) => `medications-view-${v}`}
/>,
);
expect(out).toContain('aria-label="Cards"');
expect(out).toContain('title="Table"');
expect(out.match(/pointer-coarse:size-11/g)?.length).toBe(2);
expect(out).toContain('data-slot="medications-view-table"');
expect(out).not.toContain(">Cards<");
});
it("re-fires the chosen segment in radio mode, which can open something", () => {
const onChange = vi.fn();
const tree = Segmented({
options: OPTIONS,
value: "recent",
onChange,
label: "History",
});
buttons(tree)[0].props.onClick();
expect(onChange).toHaveBeenCalledWith("recent");
});
it("ignores a press on the active segment in toggle mode", () => {
const onChange = vi.fn();
const tree = Segmented({
options: OPTIONS,
value: "recent",
onChange,
label: "History",
mode: "toggle",
});
const [active, other] = buttons(tree);
active.props.onClick();
expect(onChange).not.toHaveBeenCalled();
other.props.onClick();
expect(onChange).toHaveBeenCalledWith("all");
});
});
describe("one segmented control", () => {
it("is the only segmented-control component in the tree", async () => {
const { readFileSync } = await import("node:fs");
const { join } = await import("node:path");
const { walkSourceFiles } =
await import("@/__tests__/helpers/source-files");
const src = join(__dirname, "../../..");
const defining = walkSourceFiles(src, { floor: 2000 })
.filter((p) => !p.includes("__tests__/"))
.filter((p) =>
/export function (?:Segmented\w*|ViewToggle|\w*SegmentedToggle)\b/.test(
readFileSync(join(src, p), "utf8"),
),
);
expect(defining).toEqual(["components/ui/segmented.tsx"]);
});
});
/**
* A row of buttons whose `aria-pressed` compares a state to each button's
* value is a segmented control spelled by hand: six of them sat beside
* `Segmented` (the narrative period, the lab result type, both admin filters,
* the chart range tabs, the mood chart's copy of them), each with its own
* size, fill and gap. New ones are refused; the ones that remain are not one
* choice on one track, and say why.
*/
describe("no hand-built segmented control", () => {
const PRESSED_BY_VALUE = /aria-pressed=\{[^}]*===[^}]*\}/g;
const NOT_A_SEGMENT: Record<string, string> = {
"components/settings/security-section/passkey-list-section.tsx":
"one method is disabled while the account has no passkey; Segmented disables the whole track",
"components/medications/units-per-dose-field.tsx":
"a chip row that opens a free-number field for any other count",
"components/body-sites/body-sites-section.tsx":
"wrapping side chips under a body-site picker, not a track",
"components/illness/log-day-sheet.tsx":
"a graded severity scale per symptom, one square per level",
"components/cycle/log-day-sheet.tsx":
"a graded severity scale per symptom, one circle per level",
"components/charts/chart-overlay-controls.tsx":
"baselines stay pressable while greyed, with a tooltip saying why there is no data",
"components/timeline/timeline-view.tsx":
"a single button that opens the range sheet, beside the zoom Segmented",
};
function pressedByValue(source: string): number {
return (source.match(PRESSED_BY_VALUE) ?? []).length;
}
it("finds none outside the reasoned exceptions", async () => {
const { readFileSync } = await import("node:fs");
const { join } = await import("node:path");
const { stripComments, walkSourceFiles } =
await import("@/__tests__/helpers/source-files");
const src = join(__dirname, "../../..");
const found = walkSourceFiles(src, { floor: 2000 })
.filter((p) => /^(components|app)\//.test(p))
.filter((p) => !p.includes("__tests__/"))
.filter((p) => p !== "components/ui/segmented.tsx")
.filter(
(p) =>
pressedByValue(stripComments(readFileSync(join(src, p), "utf8"))) > 0,
)
.sort();
expect(found).toEqual(Object.keys(NOT_A_SEGMENT).sort());
});
it("counter-check: catches a hand-built pair, not an on/off toggle", () => {
expect(
pressedByValue(`<Button aria-pressed={filter === "all"}>All</Button>
<Button aria-pressed={filter === "failed"}>Failed</Button>`),
).toBe(2);
expect(pressedByValue(`<Button aria-pressed={open}>Show</Button>`)).toBe(0);
});
});