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 -1
View File
@@ -101,7 +101,7 @@ No `text-[9px]`. No off-scale sizes (`text-[28px]`); round to the neighbouring s
| A sheet from the bottom edge (phones) | `<SheetContent side="bottom">` or `ResponsiveSheet`, both of which render `BottomSheetContent` (`ui/bottom-sheet.tsx`): grip, swipe to close, X on the title row. `preview="58dvh"` for a sheet that opens part-way and pulls up to 90 dvh (its footer `data-sheet-footer`, its scroll body `data-sheet-body`); `draggable={false}` only for a sheet that fills the window | a hand-rolled grab handle or drag; a `fixed bottom-0` panel standing in for a sheet |
| Date / time entry | `DateField` / `TimeField` / `DateTimeField` | native `type="date | time | datetime-local"` inputs (browser-locale trap, v1.25.4) |
| Small text action inside a line (Open schedule, Select all, View all, Retry, Learn more) | `InlineAction` (`ui/inline-action.tsx`): tone `primary` / `muted` / `foreground`, size `xs` / `sm`, `asChild` for a `Link`; owns the focus ring and `pointer-coarse:min-h-11` | a hand-rolled underlined `<button>` / `<Link>` (`inline-action-guard.test.ts`) |
| Mutually exclusive choice on one track (range, zoom, cards vs table, recent vs all) | `Segmented` (`ui/segmented.tsx`): no track, the chosen segment filled in primary (the chart tabs' look; a muted track with a raised segment was tried in 1.43.8 and dropped in 1.43.10 as too heavy); `mode="radio"` (default, roving focus) or `"toggle"` (view switches, form sheets), `iconOnly` for icon segments, `size="sm"` for range rows in a chart header; 44 px per segment on a coarse pointer | a row of ghost/secondary Buttons, a fifth segmented variant (`segmented.test.tsx` holds the definition to one module) |
| Mutually exclusive choice on one track (range, zoom, cards vs table, recent vs all) | `Segmented` (`ui/segmented.tsx`): no track, the chosen segment filled in primary (the chart tabs' look; a muted track with a raised segment was tried in 1.43.8 and dropped in 1.43.10 as too heavy); `mode="radio"` (default, roving focus) or `"toggle"` (view switches, form sheets), `iconOnly` for icon segments, `size="sm"` for range rows in a chart header; 44 px per segment on a coarse pointer | a row of ghost/secondary Buttons, a fifth segmented variant (`segmented-single-implementation.test.ts` holds the definition to one module) |
| A grade beside a reading (confidence, score gain, band, lab range flag) | `ScorePill` (`ui/score-pill.tsx`): tone `success` / `info` / `warning` / `destructive` / `neutral`, `numeric` for figures; `CONFIDENCE_TONE` maps both confidence scales | a hand-rolled `rounded-full px-2 py-0.5 text-xs bg-*/15` chip (`score-pill-guard.test.ts`); `TagChip` (neutral label) and `Badge` (connection/account state) stay what they are |
| "This vital on these days vs those days" board | `MetricCrosstab` (`insights/metric-crosstab.tsx`): caller formats the numbers, the board owns row, neutral signed delta, confidence chip, dismissal | a fourth copy of the crosstab list |
| A trend / stat tile in a grid (KPI strip, derived tiles, their skeletons and building states) | `TrendTile` (`ui/trend-tile.tsx`) | the hand-spelled `bg-card border-border … p-4 md:p-6` frame |
@@ -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);
});
});
+5 -1
View File
@@ -2,7 +2,9 @@
* Setup for the `browser` project (`*.browser.test.tsx`, headless Chromium).
*
* - The app stylesheet, compiled by Vite through the project's PostCSS
* config, so components lay out and hide as they do in the app.
* config, so components lay out and hide as they do in the app, under the
* theme class the boot script sets for a first visit (`dark`, see
* `src/lib/pwa/theme-color.ts`); without one no colour token resolves.
* - The EN catalog, the fallback floor of every `t()` chain, loaded through
* the production loader. `renderWithApp` loads any other locale it is
* asked for.
@@ -19,6 +21,8 @@ import { loadMessages } from "@/lib/i18n/load-locale";
import { restoreFetch } from "./fetch-stub";
import { rememberAddress, restoreAddress } from "./url";
document.documentElement.classList.add("dark");
beforeAll(async () => {
await loadMessages("en");
});
+7
View File
@@ -129,6 +129,13 @@ export default defineConfig({
),
},
},
// Pre-bundle every dependency the browser files reach before the
// first one loads. Discovered late, a dependency makes Vite re-bundle
// and reload the page mid-run, which fails the file that was
// importing it; CI starts every run on an empty cache.
optimizeDeps: {
entries: ["src/**/*.browser.test.tsx", "tests/browser/**/*.{ts,tsx}"],
},
test: {
name: "browser",
include: ["src/**/*.browser.test.tsx"],