diff --git a/.agents/skills/codeatlas-ui/API.md b/.agents/skills/codeatlas-ui/API.md index d9aad9d6..598808c3 100644 --- a/.agents/skills/codeatlas-ui/API.md +++ b/.agents/skills/codeatlas-ui/API.md @@ -627,6 +627,8 @@ Data attributes: `data-disabled`, `data-state` (`checked` `indeterminate`) Parts: `ca-chip` `ca-chip--brand` `ca-chip--error` `ca-chip--info` `ca-chip--interactive` `ca-chip--selected` `ca-chip--sm` `ca-chip--success` `ca-chip--warning` `ca-chip__delete` `ca-chip__icon` `ca-chip__label` +Data attributes: `data-theme-scope` (`dark` `light`) + ## ChipGroup `import { ChipGroup } from "@codeatlasdev/ui"` diff --git a/.changeset/island-scope-surfaces.md b/.changeset/island-scope-surfaces.md new file mode 100644 index 00000000..1faa843c --- /dev/null +++ b/.changeset/island-scope-surfaces.md @@ -0,0 +1,5 @@ +--- +"@codeatlasdev/ui": patch +--- + +Surfaces and chips inside a `ThemeIsland` (and a `PreviewStage` with a `scheme` or `preset`) follow the island, not the page around it. The layer tokens (`--ca-overlay-bg`, `--ca-panel-bg`, `--ca-chrome-bg`, `--ca-upload-bg`) and the solid style's tint overrides are re-declared on `[data-theme-scope]`, so a dark island on a light page no longer inherits the page's resolved light surface (a `FileUpload` zone, an `AppForm` footer) under dark text. The `success`, `error`, `warning` and `info` chip text follows `[data-theme-scope]` as well as `.dark`. diff --git a/.changeset/scroll-focusable-subtree.md b/.changeset/scroll-focusable-subtree.md new file mode 100644 index 00000000..45eb2317 --- /dev/null +++ b/.changeset/scroll-focusable-subtree.md @@ -0,0 +1,5 @@ +--- +"@codeatlasdev/ui": patch +--- + +`scrollFocusable` re-measures when content is added at any depth, not only as a direct child of the scroller. A fixed-height wrapper never resizes when the content it holds grows, so a lazily loaded demo inside a `PreviewStage` frame left its scrollable region without a tab stop. diff --git a/.changeset/scroll-regions-focusable.md b/.changeset/scroll-regions-focusable.md new file mode 100644 index 00000000..e2d8d922 --- /dev/null +++ b/.changeset/scroll-regions-focusable.md @@ -0,0 +1,5 @@ +--- +"@codeatlasdev/ui": patch +--- + +`PreviewStage` frames and `ResizablePanel`s that scroll are keyboard-reachable while their content overflows (`scrollFocusable`, WCAG 2.1.1): a demo taller than the stage, or a narrow panel whose content is wider than it, no longer traps its content out of keyboard reach. Without overflow they add no tab stop. diff --git a/.changeset/seg-touch-width.md b/.changeset/seg-touch-width.md new file mode 100644 index 00000000..b2e8ab0c --- /dev/null +++ b/.changeset/seg-touch-width.md @@ -0,0 +1,5 @@ +--- +"@codeatlasdev/ui": patch +--- + +`SegmentedControl` items are at least 44px wide as well as tall under a coarse pointer: in a narrow slot the equal shrinkable columns squeezed short labels (a "390" preset) to 38px, under the touch target. diff --git a/AGENTS.md b/AGENTS.md index ed4f73a1..85652a89 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,42 +42,51 @@ site/ → Astro site (landing + MDX docs + templates) at ui.code - **Triage before a batch ("fix all issues"):** never start coding first. (1) `bun run issue:triage` creates the labels and writes every open issue with comments, linked PRs and the commits on `main` that close it (closing keyword or `(#issue) (#pr)` subject; a bare `(#n)` is a PR number) to `.cache/triage.md`. (2) Read all of them and check each against current `main` source (issues usually quote an older `dist/`). (3) Already fixed: prove it as in **Check first** (a passing test on `main`), comment the evidence and close (`gh issue close --reason completed`); duplicate: close as `duplicate` pointing to the survivor; not planned: `not planned` with the reason. (4) Every remaining issue gets exactly one type (`bug` / `enhancement` / `documentation`) and one or more `area:*` labels. (5) Link related issues with a comment `Related: #a, #b (same file/root cause)` on each side; when one must land first, label `blocked` and comment `Blocked by #n`. (6) Order the work: blockers, then bugs, then enhancements and docs; fix issues sharing a file in the same worktree sequence, never in parallel worktrees. Tooling edits go through a worktree and PR too, never on the main checkout. - **Check first:** `bun run issue:status`. Skip issues labeled `in progress` by someone else. Re-read the issue against current `main`: if it is already fixed, prove it with a passing test, comment the evidence and close it. - **Test first** (critical rule 0): failing regression test, then the fix, then the passing run. Both outputs go in the PR body. A test you did not run proves nothing: run every test you add or change (Playwright included) before pushing. -- **CI is paused** (pre-stable): `ci.yml` is disabled and `CI ok` is not required on `main`; the pre-push `validate` is the gate, and run the browser suites locally when you touch them. Visual baselines are rendered locally in Docker (`bun run test:vrt:update`, `bun run test:vrt:check`; see Tests), never on the host. Resume: `gh workflow enable ci.yml` + re-add `CI ok` to the `main` ruleset. +- **CI is paused** (pre-stable): `ci.yml` is disabled and `CI ok` is not required on `main`; the pre-push `validate` is the gate, and run the browser suites locally when you touch them. Visual baselines are rendered natively on your machine (`bun run test:vrt:update`, `bun run test:vrt`; see Tests), never in Docker. Resume: `gh workflow enable ci.yml` + re-add `CI ok` to the `main` ruleset. - **Gates are real:** never push with `--no-verify` or `LEFTHOOK=0`. A gate that fails only under load is a bug to fix (#73 was exponential regex backtracking; #75, #76, #81 were real races), not to bypass. Open an issue for it. -- **Done:** `bun run validate`, changeset when the package changes, docs when behavior changes; leave the PR as draft. A maintainer lands it with `sh scripts/issue-land.sh `: rebase on `main` (generated files are rebuilt, real conflicts stop it), validate, `test:vrt:check` in Docker (a mismatch stops it: review and commit `test:vrt:update`, then rerun), squash-merge, `issue:done`. +- **Done:** `bun run validate`, changeset when the package changes, docs when behavior changes; leave the PR as draft. A maintainer lands it with `sh scripts/issue-land.sh `: rebase on `main` (generated files are rebuilt, real conflicts stop it), `test-changed --land` (the tests and screenshots the diff affects plus 5 canary routes, 3 minute budget; a screenshot mismatch stops it: review and commit `test:vrt:update`, then rerun), `validate` on the push (90 s budget), squash-merge, `issue:done`. --- ## Tests -Unit tests run on `bun test` (repo root, `bunfig.toml`); component and visual tests need a real Chromium and stay on Vitest. Use `bun run test` (everything) or the scripts below. A bare `bun test` at the root runs only the unit tests. +Unit tests run on `bun test` (repo root, `bunfig.toml`); component and visual tests need a real Chromium and stay on Vitest. **A command is a time budget, not a coverage promise**: day-to-day `bun run test` runs only what the diff against `origin/main` affects and takes seconds; the whole suite runs at release, as two commands (`test:full`, then `test:vrt`) so that none takes five minutes. A run over its budget fails (below), so slowness is caught like any other regression. -| Command | What it runs | -|---|---| -| `bun run test` | unit + browser + fixtures + E2E | -| `bun run test:unit` | `bun test` on `packages/ui/tests/*.test.ts` (fails any test without an `expect`) | -| `bun run test:browser` | Vitest `browser` project (real Chromium, axe) | -| `bun run test:fixtures` | `packages/ui/tests/*.e2e.ts`: builds a real fixture app (Astro, Kit) and drives it in Chromium (e.g. `caUI` + ``). Never name these `*.test.ts`: the unit suite runs in `validate` and in the release job, which have no browsers (`tests/unit-suite.test.ts` guards it) | -| `bun run test:e2e` / `test:a11y` | Playwright against the static site (build + preview) | -| `bun run lighthouse` | Lighthouse (SEO and accessibility must be 100, performance floor) plus the installability contract (standalone manifest with `id`, 192/512/maskable icons, `twitter:title`, `og:image`, `lang`) on the built site served locally, offline. Not in `validate`: it needs a production build and a Chrome, is CPU-heavy and its performance score moves with machine load, which AGENTS.md forbids as a gate. Run it when you touch SEO, the manifest, `` or `init` output; `E2E_PREBUILT=1` skips the build | -| `bun run test:vrt:check` | both visual suites (package Vitest `vrt` + site `visual-desktop`/`visual-mobile`) against the committed baselines, inside Docker (`mcr.microsoft.com/playwright:v<@playwright/test>-noble`, `--platform linux/amd64`, DejaVu fonts added and FreeFont removed so every font lookup resolves like the old `ubuntu-24.04` runner (otherwise code blocks reflow ~27px after the first capture), container-local `node_modules` volumes), the same steps as the `visual` job of `ci.yml`. Needs Docker; takes several minutes (amd64 emulation on Apple Silicon) | -| `bun run test:vrt:update` | same, rewriting every baseline in the worktree (`scripts/vrt-docker.sh --update`). Review the changed images against an intended change before committing; run `test:vrt:check` twice afterwards to prove they are stable | -| `bun run test:status` | live and dead test runs of this repo, with their browser count | -| `bun run test:reap` | kill the browsers of runs whose runner died (this repo only) | +| Command | What it runs | Budget | Measured | +|---|---|---|---| +| `bun run test` = `test:changed` | what the diff affects: `bun test --changed` (unit); the component tests whose imports reach a changed file (`scripts/test-graph.ts`: a static import graph with the barrel flattened, because Vitest's own `--changed` selects every file in browser mode); the screenshots of those components; the docs routes mapped by `scripts/route-impact.ts`. Prints what it selected and why. Every component test runs when a shared file changed (styles, tokens, theme, stores, primitives, a test setup file). The site part needs a fresh `site/dist` (a build is ~75 s, over the budget) and says so when it skips | 60 s | 28 s for a one-component edit with a fresh `dist` (unit 0.4, components 4.6, screenshots 6.7, routes 16.6; load ~15) | +| `bun scripts/test-changed.ts --land` | the same plus the package and site builds and 5 canary routes (home, recipes, app-shell, templates/app, theme). This is the gate `issue-land.sh` runs; `validate` runs on the push. **When a shared file changed** (styles, tokens, theme, stores, primitives, a test setup file, the site layout, or a file the route map cannot place) it runs every component test and every route and **no screenshots**, under the larger budget; `--print-budget` says which one applies | 180 s; 360 s when a shared file changed | 104 s for a one-component edit (builds 17 + 58, routes 17; load ~15). Shared case at load 10: unit 5 + components 109 + builds 99 + routes 132 = 346 s; its sweep audits the gallery through 6 representative items (`E2E_GALLERY=representatives`), `test:full` audits all ~400 | +| `bun run validate` | themes, token lint, Biome, unit tests, both type checks, build, size | 90 s | 51 s at load 11 | +| `bun run test:full` | unit, every component test, every route (layout, axe, dist-html, every project including WebKit), fixtures. Release gate, part 1 of 2 | 300 s | components 109 s (load 25) + routes 168 s (load 29) + unit 40 s + fixtures 7 s: over budget on a busy machine, not measured on a quiet one | +| `bun run test:unit` | `bun test` on `packages/ui/tests/*.test.ts` (fails any test without an `expect`) | | 30 to 40 s | +| `bun run test:browser` | Vitest `browser` project (real Chromium, axe), every component test | | 109 s (load 25) | +| `bun run test:fixtures` | `packages/ui/tests/*.e2e.ts`: builds a real fixture app (Astro, Kit) and drives it in Chromium (e.g. `caUI` + ``). Never name these `*.test.ts`: the unit suite runs in `validate` and in the release job, which have no browsers (`tests/unit-suite.test.ts` guards it) | | 7 s | +| `bun run test:e2e` / `test:a11y` | Playwright against the static site (build + preview): every project | | route sweep 168 s (load 30) | +| `bun run test:vrt` | every screenshot against the committed baselines of the platform you are on: package Vitest `vrt` (405) and the site's `visual-desktop`/`visual-mobile` (landing, templates, theme at the viewport). Release gate, part 2 of 2; also the way to check screenshots after a shared change, which the land gate leaves out | 180 s | components 105 to 137 s + site 7 to 11 s | +| `bun run test:vrt:update` | the same, rewriting the baselines of your platform. Review the changed images against an intended change before committing; run `test:vrt` twice afterwards to prove they are stable | | components 133 s, site 70 s | +| `bun run lighthouse` | Lighthouse (SEO and accessibility must be 100, performance floor) plus the installability contract (standalone manifest with `id`, 192/512/maskable icons, `twitter:title`, `og:image`, `lang`) on the built site served locally, offline. Not in `validate`: it needs a production build and a Chrome, is CPU-heavy and its performance score moves with machine load, which AGENTS.md forbids as a gate. Run it when you touch SEO, the manifest, `` or `init` output; `E2E_PREBUILT=1` skips the build | | | +| `bun run test:status` | live and dead test runs of this repo, the test lock and its holder, and the browser count | | | +| `bun run test:reap` | kill the browsers of runs whose runner died (this repo only) | | | + +"Measured" is wall time on the owner's M4 Pro (12 cores) with the load average noted; other agents and projects share that machine, so a number is only comparable with another taken in the same window. Budgets live in one table, `BUDGETS` in `scripts/test-run.ts`, and are raised only in their own commit with the measured reason. + +**Budgets are enforced.** `scripts/test-run.ts --budget=` starts the clock once it holds the lock, prints the 10 slowest tests when the run ends (runners append to `CA_TEST_TIMINGS`: `tests/timing-reporter.ts` for Playwright, `packages/ui/tests/timing-reporter.ts` for Vitest) and **fails** a run over its budget. A budget is wall time, so on an oversubscribed machine (load average above the core count) the verdict is printed as `OVER BUDGET, NOT ENFORCED` and does not fail the run: it would only measure the other processes. Rerun when the load is below the core count. + +**One suite at a time.** A run takes a machine-wide lock (`$TMPDIR/ca-test-lock`, shared by every worktree and checkout; `CA_TEST_LOCK_DIR` overrides it) and a second run waits, printing who holds it, instead of starting more browsers. A holder that died is taken over, and a nested `test-run` (the commands above call each other) reuses its parent's lock. **Never call `vitest` / `playwright test` directly for browser tests: always go through the scripts above.** They run under `scripts/test-run.ts`, which: - records each run in `.cache/test-runs/.json` and tags every browser it starts with `--ca-test-run=`; - tears down the whole test process group on exit, Ctrl+C, SIGTERM or SIGHUP; - at the start of every run, kills browsers left by runs whose runner died (SIGKILL, closed terminal, killed agent job). -Ownership is explicit: only processes carrying the tag of a dead run of this repo are touched. Other projects on the machine (`../ac`, …) share `~/Library/Caches/ms-playwright`; never kill Playwright browsers by name or path. To pass extra args: `bun run test:browser -- src/lib/components/Button`. A browser test that must hold on a loaded machine gets its own `*.throttled.svelte.test.ts` (CDP `Emulation.setCPUThrottlingRate`, see `CommandPalette.throttled`); a full run under parallel CPU load (#544) must stay green. Multi-step keyboard flows take `SLOW_FLOW` from `tests/settled.ts` as their timeout; a condition is awaited with `expect.poll`/`expect.element`, never a fixed `setTimeout` deadline. Local runs use a quarter of the cores (`maxWorkers`, Playwright `workers: "25%"`, also when a shell exports `CI=true`: only `GITHUB_ACTIONS` gets every core, #539); behavior tests use the headless shell. VRT only runs on Linux (the `vrt` project excludes everything else): on a Mac use `test:vrt:check`/`test:vrt:update`, never a bare `test:vrt`. +Ownership is explicit: only processes carrying the tag of a dead run of this repo are touched. Other projects on the machine (`../ac`, …) share `~/Library/Caches/ms-playwright`; never kill Playwright browsers by name or path. To pass extra args: `bun run test:browser -- src/lib/components/Button`. A browser test that must hold on a loaded machine gets its own `*.throttled.svelte.test.ts` (CDP `Emulation.setCPUThrottlingRate`, see `CommandPalette.throttled`); a full run under parallel CPU load (#544) must stay green. Multi-step keyboard flows take `SLOW_FLOW` from `tests/settled.ts` as their timeout; a condition is awaited with `expect.poll`/`expect.element`, never a fixed `setTimeout` deadline. Local runs use a quarter of the cores (`maxWorkers`, Playwright `workers: "25%"`, also when a shell exports `CI=true`: only `GITHUB_ACTIONS` gets every core, #539); behavior tests use the headless shell. **Visual baselines are rendered natively and never in Docker.** The platform is part of every baseline's file name (`Name-dark-glass-chromium-darwin.png` for Vitest, `home-dark-darwin.webp` for Playwright), so a platform is only ever compared with its own baselines and the linux and darwin sets sit side by side. The `vrt` project runs on darwin and linux and compares nothing elsewhere. The Vitest screenshot setup loads the same self-hosted typefaces as the site (`@fontsource-variable`), so text does not depend on the machine's fonts. On a platform with no baselines, run `bun run test:vrt:update` and review the images before committing. The linux set is what a future CI would use, or regenerate; **CI stays disabled** (`ci.yml` is untouched), so nothing but the local commands above renders or compares pixels. The site visual project captures the visible viewport of five pages (landing, templates, theme): the docs pages are a stack of component demos that the component screenshots already compare one by one, and the layout audit checks their geometry, so a tall full-page docs screenshot duplicated both and was the flaky one. Naming conventions: - `*.test.ts` in `packages/ui/tests/`: unit test on `bun:test` (`mock`/`spyOn`/`jest.useFakeTimers`; DOM only through happy-dom's `GlobalRegistrator` inside that file; fresh module via `import("…?fresh=n")`). Types checked by `bun --check`. - `*.svelte.test.ts`: component in the browser, next to the component. - `*.vrt.test.ts`: per-component screenshot; matrix harness `NameMatrix.test.svelte`. - `*.test.svelte`: harness for snippets/`bind:` (not shipped in `dist`). -- `site/tests/*.spec.ts` (desktop+mobile+iphone), `*.a11y.spec.ts`, `*.visual.spec.ts`. +- `site/tests/*.spec.ts` (desktop+mobile+iphone), `*.axe.spec.ts` (axe over every route, its own `axe` project), `*.a11y.spec.ts`, `*.visual.spec.ts`. A new route-level check goes into `routes.spec.ts` (it already loads every route once), never into a spec that loads every route again. - Keep test titles short: `describe` + `it` combined at most ~80 characters. Vitest names the trace attachment with path + title + hash, and names over 255 bytes fail with `ENAMETOOLONG`, losing the trace exactly when the test fails. Rule: every new component needs a `.svelte.test.ts` with `expect(await checkA11y(screen.container)).toHaveNoViolations()` in each state (open/closed). `render` from `vitest-browser-svelte` is async. `a11y_*` warnings are errors: `bun run check` uses `--fail-on-warnings` and the site build fails via `onwarn` (`site/svelte.config.js`). (`svelte-package` does not compile `.svelte`, so in the package it is `check` that blocks.) @@ -87,9 +96,9 @@ Accessibility rules (package and apps): - Screen reader announcements go through `announce(msg, politeness)` (one shared live region); do not create a new local `role="status"`. In SvelteKit, mount `RouteAnnouncer` (`@codeatlasdev/ui/kit`) once in the root layout. - An `aria-label` on a button must contain the visible text (WCAG 2.5.3); compose with `aria-labelledby` as `Combobox` does with `ariaLabel`. - New surface CSS that blurs, adds a shadow or sets `border: none` needs an entry in the `@media (forced-colors: active)` block of `materials.css` (`CanvasText` border). Contrast tokens (`prefers-contrast: more`) come from `themeCSS`/`presetCSS`; `tests/contrast.test.ts` measures 7:1 for text and 3:1 for outlines in every preset. -- `site/tests/routes.a11y.spec.ts` runs axe in dark, light and `forcedColors: "active"` on every route, and ensures at most one `#main`, one `
` and one skip link per page. Its timeout is sized per route from the page's node count (axe walks every node, three passes), never a hand-kept list of slow routes; `A11Y_CPU_THROTTLE=` (CDP CPU throttling) reproduces a loaded machine deterministically. +- Every prerendered route is checked by three specs that each load it ONCE per project, never again: `site/tests/routes.spec.ts` (desktop 1280x800 and mobile 360x780: 200, a title, exactly one `
`, at most one `#main` and one skip link, no console or page errors, and the layout audit in `layout-audit.ts`: no sideways scroll, no overlap, no clipped text, mobile 44px targets); `site/tests/routes.axe.spec.ts` (the `axe` project: axe in dark, light and forced colors on every route, `color-contrast` included in dark and light, `
` excluded because Shiki's palette is proven by `CodeSnippet.svelte.test.ts`); and `site/tests/dist-html.spec.ts` (page rules read from `site/dist` with a DOM parser, no browser; it also pins the known duplicate heading ids so a new one fails, #548). `makeAxe(page)` runs `axe.run` in the page: `@axe-core/playwright`'s builder re-injects axe and opens a blank page on every `analyze()` and was 4 to 7 times slower on the same page and rules (568 against 155 ms on a 1,027-node page, 3,661 against 514 ms on `/gallery/`), with identical findings on real and deliberately broken pages (low contrast, missing `alt`, a button without a name, same-origin and `srcdoc` frames). The in-between widths (768, 1024, 844x390, resized inside the same load) run only on routes that render layout primitives, on composed pages (templates, landing, theme, gallery) and on routes the diff touches. Hydration is awaited with Astro's own `astro:hydrate` event (`waitIslands`), and `settleDom` (fonts, finite animations, shiki swap) replaces the 500 ms height-stability window that only screenshots pay (`settle`). `scripts/route-impact.ts` maps a changed file to its routes and **fails safe**: a shared path (styles, tokens, theme, primitives, the site layout or shell, a package component the shell renders) or any file it does not recognise selects every route, and says which file and why. The axe timeout is sized per route from the page's node count, never a hand-kept list of slow routes; `A11Y_CPU_THROTTLE=` (CDP CPU throttling) reproduces a loaded machine deterministically.
 
-The browser setup (`tests/setup.browser.ts`) parks the Playwright pointer outside the viewport before every test: the pointer is page state, so without it a test that hovers leaves `:hover` active on whatever the next file renders at the top-left (a filled Button reads its hover fill, a tooltip is already open) and results depend on file order. A test that needs a pointer moves it itself (`userEvent.hover`); `src/lib/testing/pointer-reset.svelte.test.ts` guards the hook.
+The browser setup (`tests/setup.browser.ts`) parks the Playwright pointer outside the viewport before a test that follows a pointer event (and before the first test of a file; the CDP call costs ~17 ms and almost no test moves the pointer): the pointer is page state, so without it a test that hovers leaves `:hover` active on whatever the next file renders at the top-left (a filled Button reads its hover fill, a tooltip is already open) and results depend on file order. A test that needs a pointer moves it itself (`userEvent.hover`); `src/lib/testing/pointer-reset.svelte.test.ts` guards the hook. `checkA11y` settles by jumping running animations to their end state (`settled()` in `@codeatlasdev/ui/testing`), not by sitting through a 150 ms entrance transition on each of ~430 checks. Component tests keep the default per-file isolation: `isolate: false` was tried on the browser project and failed 775 of 2095 tests (every file shares one page) in a run that took 642 s, so it is not an option here; `vitest doctor` stops on this suite when any test flakes under load, which is why the comparison was run by hand.
 
 Reading failures: locators fail with an ARIA snapshot; traces are in `__traces__/` (Vitest) and `site/test-results//trace.zip` (`bunx playwright trace open `); visual diffs are in `packages/ui/.vitest/attachments` and `site/playwright-report`. For lean output: `bunx vitest run --reporter=minimal`. Optional: `npx @playwright/mcp` to inspect the site.
 
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b00494d3..4a51a1a7 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -66,7 +66,7 @@ Then, in the worktree (use paths inside it; your editor or agent must not edit t
 1. **Understand and reproduce.** Write the regression test first and run it: it must fail on the current code. Paste that output in the PR.
 2. **Fix** the root cause until the test passes. No workarounds for a symptom. Run every test you added or changed, Playwright included.
 3. **Before review:** `bun run validate`, a changeset if the package changes, docs if behavior changed. Never bypass the pre-push hook (`--no-verify`, `LEFTHOOK=0`): a gate that fails only under load is a bug; open an issue for it.
-4. **Landing (maintainers):** `sh scripts/issue-land.sh 47` rebases on `main`, rebuilds generated files (lockfile, API.md, components.json, css-api), validates, checks the visual baselines locally in Docker (`bun run test:vrt:check`) and squash-merges (a mismatch stops it: review and commit `bun run test:vrt:update`, then rerun). GitHub closes the issue through `Closes #47`; the script then runs `issue:done` (worktree, branch and `in progress` label cleanup).
+4. **Landing (maintainers):** `sh scripts/issue-land.sh 47` rebases on `main`, rebuilds generated files (lockfile, API.md, components.json, css-api), runs the land gate (`scripts/test-changed.ts --land`: the tests and screenshots the diff affects plus 5 canary routes, 3 minute budget; a screenshot mismatch stops it: review and commit `bun run test:vrt:update`, then rerun), pushes (the pre-push hook runs `bun run validate`, 90 second budget) and squash-merges. GitHub closes the issue through `Closes #47`; the script then runs `issue:done` (worktree, branch and `in progress` label cleanup).
 
 Issues filed by an AI agent through the `report_issue` MCP tool use the **AI report** form (`ai-report` label) and are sanitized before posting (no project names, paths, URLs, env, secrets or data). Treat them like any bug report; when you close one, `bunx @codeatlasdev/ui workarounds` is how its reporter finds the workaround to remove. See [Reporting problems](https://ui.codeatlas.com.br/docs/getting-started/reporting/).
 
@@ -91,7 +91,7 @@ If you find the issue is already fixed or obsolete, prove it with a test that pa
 
 **Test first.** A bug fix starts with a regression test that reproduces the problem: run it, watch it fail on the current code, then fix until it passes, and put that fail-before/pass-after evidence in the PR. Tests prove behavior; tests that only pad coverage (wiring, copies, mock echoes, incidental defaults) are rejected.
 
-> **CI is paused** while the project is pre-stable: the `CI` workflow is disabled (`gh workflow disable ci.yml`) and `CI ok` is no longer a required check on `main`. The local gates are the gate: the pre-push hook runs `bun run validate`; run the browser suites (`bun run test:browser`, `test:e2e`, `test:vrt:check`) yourself when a change touches them. PR titles are still checked (`pr-title.yml`), and releases still run on tags. To turn CI back on: `gh workflow enable ci.yml` and add `CI ok` back as a required status check in the `main` ruleset.
+> **CI is paused** while the project is pre-stable: the `CI` workflow is disabled (`gh workflow disable ci.yml`) and `CI ok` is no longer a required check on `main`. The local gates are the gate: the pre-push hook runs `bun run validate`; `bun run test` runs what your change affects (seconds); `bun run test:full` and `bun run test:vrt` run everything and are what a release runs. PR titles are still checked (`pr-title.yml`), and releases still run on tags. To turn CI back on: `gh workflow enable ci.yml` and add `CI ok` back as a required status check in the `main` ruleset.
 
 CI (`.github/workflows/ci.yml`, when enabled) runs the same gates. Locally, both must exit with code 0:
 
@@ -102,11 +102,11 @@ bun run test       # unit (bun test) + component tests (Chromium) + site E2E/a11
 
 Unit tests run on `bun test` (`bunfig.toml` points it at `packages/ui/tests`); component and visual tests need a real browser and stay on Vitest, so prefer `bun run test` for the full suite. Type checking is split by who understands the files: `bun --check` (the native checker, config in the root `tsconfig.json`) covers scripts, unit tests and Playwright specs, `svelte-check` covers `packages/ui` and `astro check` covers `site`. Bun >= 1.4.3 and Node >= 22.17 are required.
 
-Browser tests always run through the package scripts (`bun run test:browser`, `test:e2e`, …), never a bare `vitest` or `playwright test`. The scripts go through `scripts/test-run.ts`, which tags this repo's browsers, tears them down on exit or Ctrl+C, and cleans up after runs that were killed hard. `bun run test:status` shows runs, `bun run test:reap` cleans dead ones. It never touches other projects' Playwright browsers.
+Browser tests always run through the package scripts (`bun run test`, `test:browser`, `test:e2e`, …), never a bare `vitest` or `playwright test`. The scripts go through `scripts/test-run.ts`, which tags this repo's browsers, tears them down on exit or Ctrl+C, cleans up after runs that were killed hard, holds a machine-wide lock so a second suite waits (and says who holds the lock) instead of starting more browsers, and enforces a time budget per command: `bun run test` 60 s, the land gate 180 s (300 s when a shared file changed: every component test and route, no screenshots), `validate` 90 s, `test:full` 300 s and `test:vrt` 180 s (a release runs both). A run over budget fails and prints its 10 slowest tests (on an oversubscribed machine the verdict is advisory, see AGENTS.md). `bun run test:status` shows runs and the lock holder, `bun run test:reap` cleans dead ones. It never touches other projects' Playwright browsers.
 
-Git hooks (lefthook, installed by `bun install`): pre-commit formats staged files with Biome, commit-msg checks the message, pre-push runs `bun run validate`. Browser suites (`bun run test:browser`, `test:e2e`) run on CI and on demand.
+Git hooks (lefthook, installed by `bun install`): pre-commit formats staged files with Biome, commit-msg checks the message, pre-push runs `bun run validate`. `bun run test` is what to run while you work: it selects the unit tests, component tests, screenshots and docs routes your diff reaches and prints what it chose and why. A change to a shared file (styles, tokens, theme, primitives, a test setup file) runs every component test, and a change it cannot map to a route runs every route.
 
-Visual regression baselines are rendered in Docker (`linux/amd64`, the Playwright `noble` image matching `@playwright/test`, with the fonts adjusted to resolve like the old `ubuntu-24.04` CI runner), never on the host, so the pixels are identical for everyone. `bun run test:vrt:check` compares both suites (components and site) against the committed baselines; `bun run test:vrt:update` rewrites them in your worktree. Docker is required. Review the changed images before committing them, and run `test:vrt:check` again to confirm they are stable.
+Visual regression baselines are rendered natively on your machine, never in Docker, and the platform is part of every baseline's file name (`-chromium-darwin.png`, `-darwin.webp`), so macOS and Linux baselines sit side by side and a platform is only compared with its own. `bun run test:vrt` compares both suites (components, and the landing, templates and theme at the viewport) against the committed baselines; `bun run test:vrt:update` rewrites those of your platform in your worktree. Review the changed images before committing them, and run `test:vrt` twice to confirm they are stable. On a platform with no baselines yet, update first. CI stays disabled, so these local commands are the only visual check.
 
 Fix any errors before opening a pull request. PRs with failing checks will not be merged.
 
diff --git a/bun.lock b/bun.lock
index fc8510b6..6096f032 100644
--- a/bun.lock
+++ b/bun.lock
@@ -134,6 +134,7 @@
         "@astrojs/check": "0.9.10",
         "@axe-core/playwright": "4.13.0",
         "@playwright/test": "1.64.0",
+        "happy-dom": "21.0.1",
         "typescript": "6.0.3",
         "wrangler": "4.149.0",
       },
diff --git a/package.json b/package.json
index 3019a94c..7f797f17 100644
--- a/package.json
+++ b/package.json
@@ -29,9 +29,11 @@
 		"release:check": "node scripts/release.js",
 		"clean": "rm -rf packages/ui/dist site/dist site/.astro",
 		"lint:fix": "biome check --write .",
-		"validate": "bun scripts/validate.ts",
-		"postinstall": "node scripts/fix-ts-symlink.js && node scripts/install-hooks.js && bunx playwright install chromium",
-		"test": "bun run test:unit && bun run test:browser && bun run test:fixtures && bun run test:e2e",
+		"validate": "bun scripts/test-run.ts --budget=validate bun scripts/validate.ts",
+		"postinstall": "node scripts/fix-ts-symlink.js && node scripts/install-hooks.js && bunx playwright install chromium webkit",
+		"test": "bun scripts/test-run.ts --budget=changed bun scripts/test-changed.ts",
+		"test:changed": "bun scripts/test-run.ts --budget=changed bun scripts/test-changed.ts",
+		"test:full": "bun scripts/test-run.ts --budget=full sh -c 'bun run test:unit && bun run test:browser && bun run test:fixtures && bun run test:e2e'",
 		"test:unit": "bun run --cwd packages/ui test:unit && bun run --cwd site test:unit",
 		"test:browser": "bun run --cwd packages/ui test:browser",
 		"test:fixtures": "bun run --cwd packages/ui test:e2e:bun",
@@ -43,8 +45,8 @@
 		"issue:status": "bun scripts/issue.ts status",
 		"issue:triage": "bun scripts/issue.ts triage",
 		"issue:done": "bun scripts/issue.ts done",
-		"test:vrt:check": "sh scripts/vrt-docker.sh --check",
-		"test:vrt:update": "sh scripts/vrt-docker.sh --update",
+		"test:vrt": "bun scripts/test-run.ts --budget=vrt sh -c 'bun run --cwd packages/ui test:vrt && bun run --cwd site test:vrt'",
+		"test:vrt:update": "bun scripts/test-run.ts sh -c 'bun run --cwd packages/ui test:vrt:update && bun run --cwd site test:vrt:update'",
 		"test:reap": "bun scripts/test-run.ts --reap",
 		"test:status": "bun scripts/test-run.ts --status"
 	},
diff --git a/packages/ui/src/lib/actions/ScrollFocusableHarness.test.svelte b/packages/ui/src/lib/actions/ScrollFocusableHarness.test.svelte
new file mode 100644
index 00000000..c629ef2e
--- /dev/null
+++ b/packages/ui/src/lib/actions/ScrollFocusableHarness.test.svelte
@@ -0,0 +1,26 @@
+
+
+
+
+
+ {#if show}
Mounted after the first paint
{/if} +
+
+ + diff --git a/packages/ui/src/lib/actions/scroll-focusable.svelte.test.ts b/packages/ui/src/lib/actions/scroll-focusable.svelte.test.ts new file mode 100644 index 00000000..aacb515e --- /dev/null +++ b/packages/ui/src/lib/actions/scroll-focusable.svelte.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; +import { render } from "vitest-browser-svelte"; +import Harness from "./ScrollFocusableHarness.test.svelte"; + +describe("scrollFocusable", () => { + it("content that overflows after mount, below a direct child, makes the scroller focusable", async () => { + const screen = await render(Harness, { later: true }); + const el = screen.getByTestId("scroller").element() as HTMLElement; + expect(el.hasAttribute("tabindex")).toBe(false); + await expect.poll(() => el.scrollHeight > el.clientHeight + 1).toBe(true); + await expect.poll(() => el.tabIndex, { timeout: 2000 }).toBe(0); + }); + + it("adds no tab stop while nothing overflows", async () => { + const screen = await render(Harness, { later: false }); + const el = screen.getByTestId("scroller").element() as HTMLElement; + expect(el.hasAttribute("tabindex")).toBe(false); + }); +}); diff --git a/packages/ui/src/lib/actions/scroll-focusable.ts b/packages/ui/src/lib/actions/scroll-focusable.ts index abdd4fcd..788d0dd8 100644 --- a/packages/ui/src/lib/actions/scroll-focusable.ts +++ b/packages/ui/src/lib/actions/scroll-focusable.ts @@ -18,13 +18,13 @@ export const scrollFocusable: Action = (node) => { ro.observe(node); for (const child of node.children) ro.observe(child); }; - // Children rendered after mount (hydration, async data, route content) change the - // overflow too: watch them as they arrive. + // Content rendered after mount (hydration, async data, a lazily loaded demo) changes the + // overflow too, at any depth: a fixed-height wrapper never resizes when what it holds grows. const mo = new MutationObserver(() => { watch(); update(); }); - mo.observe(node, { childList: true }); + mo.observe(node, { childList: true, subtree: true }); watch(); update(); return { diff --git a/packages/ui/src/lib/components/Accordion/__screenshots__/Accordion.vrt.test.ts/Accordion-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Accordion/__screenshots__/Accordion.vrt.test.ts/Accordion-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..62456275 Binary files /dev/null and b/packages/ui/src/lib/components/Accordion/__screenshots__/Accordion.vrt.test.ts/Accordion-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Accordion/__screenshots__/Accordion.vrt.test.ts/Accordion-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Accordion/__screenshots__/Accordion.vrt.test.ts/Accordion-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..366c4964 Binary files /dev/null and b/packages/ui/src/lib/components/Accordion/__screenshots__/Accordion.vrt.test.ts/Accordion-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Accordion/__screenshots__/Accordion.vrt.test.ts/Accordion-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Accordion/__screenshots__/Accordion.vrt.test.ts/Accordion-light-glass-chromium-darwin.png new file mode 100644 index 00000000..c481db51 Binary files /dev/null and b/packages/ui/src/lib/components/Accordion/__screenshots__/Accordion.vrt.test.ts/Accordion-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Accordion/__screenshots__/Accordion.vrt.test.ts/Accordion-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Accordion/__screenshots__/Accordion.vrt.test.ts/Accordion-light-solid-chromium-darwin.png new file mode 100644 index 00000000..5ac5b44a Binary files /dev/null and b/packages/ui/src/lib/components/Accordion/__screenshots__/Accordion.vrt.test.ts/Accordion-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Alert/__screenshots__/Alert.vrt.test.ts/Alert-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Alert/__screenshots__/Alert.vrt.test.ts/Alert-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..ceae284a Binary files /dev/null and b/packages/ui/src/lib/components/Alert/__screenshots__/Alert.vrt.test.ts/Alert-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Alert/__screenshots__/Alert.vrt.test.ts/Alert-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Alert/__screenshots__/Alert.vrt.test.ts/Alert-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..f06cddb8 Binary files /dev/null and b/packages/ui/src/lib/components/Alert/__screenshots__/Alert.vrt.test.ts/Alert-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Alert/__screenshots__/Alert.vrt.test.ts/Alert-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Alert/__screenshots__/Alert.vrt.test.ts/Alert-light-glass-chromium-darwin.png new file mode 100644 index 00000000..8501ac46 Binary files /dev/null and b/packages/ui/src/lib/components/Alert/__screenshots__/Alert.vrt.test.ts/Alert-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Alert/__screenshots__/Alert.vrt.test.ts/Alert-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Alert/__screenshots__/Alert.vrt.test.ts/Alert-light-solid-chromium-darwin.png new file mode 100644 index 00000000..28ab17a6 Binary files /dev/null and b/packages/ui/src/lib/components/Alert/__screenshots__/Alert.vrt.test.ts/Alert-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AlertDialog/__screenshots__/AlertDialog.vrt.test.ts/AlertDialog-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/AlertDialog/__screenshots__/AlertDialog.vrt.test.ts/AlertDialog-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..4f55d369 Binary files /dev/null and b/packages/ui/src/lib/components/AlertDialog/__screenshots__/AlertDialog.vrt.test.ts/AlertDialog-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AlertDialog/__screenshots__/AlertDialog.vrt.test.ts/AlertDialog-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/AlertDialog/__screenshots__/AlertDialog.vrt.test.ts/AlertDialog-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..5bd928e2 Binary files /dev/null and b/packages/ui/src/lib/components/AlertDialog/__screenshots__/AlertDialog.vrt.test.ts/AlertDialog-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AlertDialog/__screenshots__/AlertDialog.vrt.test.ts/AlertDialog-destructive-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/AlertDialog/__screenshots__/AlertDialog.vrt.test.ts/AlertDialog-destructive-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..65642261 Binary files /dev/null and b/packages/ui/src/lib/components/AlertDialog/__screenshots__/AlertDialog.vrt.test.ts/AlertDialog-destructive-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AlertDialog/__screenshots__/AlertDialog.vrt.test.ts/AlertDialog-destructive-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/AlertDialog/__screenshots__/AlertDialog.vrt.test.ts/AlertDialog-destructive-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..ec9441f8 Binary files /dev/null and b/packages/ui/src/lib/components/AlertDialog/__screenshots__/AlertDialog.vrt.test.ts/AlertDialog-destructive-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AlertDialog/__screenshots__/AlertDialog.vrt.test.ts/AlertDialog-destructive-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/AlertDialog/__screenshots__/AlertDialog.vrt.test.ts/AlertDialog-destructive-light-glass-chromium-darwin.png new file mode 100644 index 00000000..61be1b11 Binary files /dev/null and b/packages/ui/src/lib/components/AlertDialog/__screenshots__/AlertDialog.vrt.test.ts/AlertDialog-destructive-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AlertDialog/__screenshots__/AlertDialog.vrt.test.ts/AlertDialog-destructive-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/AlertDialog/__screenshots__/AlertDialog.vrt.test.ts/AlertDialog-destructive-light-solid-chromium-darwin.png new file mode 100644 index 00000000..b31306ab Binary files /dev/null and b/packages/ui/src/lib/components/AlertDialog/__screenshots__/AlertDialog.vrt.test.ts/AlertDialog-destructive-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AlertDialog/__screenshots__/AlertDialog.vrt.test.ts/AlertDialog-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/AlertDialog/__screenshots__/AlertDialog.vrt.test.ts/AlertDialog-light-glass-chromium-darwin.png new file mode 100644 index 00000000..753386e0 Binary files /dev/null and b/packages/ui/src/lib/components/AlertDialog/__screenshots__/AlertDialog.vrt.test.ts/AlertDialog-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AlertDialog/__screenshots__/AlertDialog.vrt.test.ts/AlertDialog-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/AlertDialog/__screenshots__/AlertDialog.vrt.test.ts/AlertDialog-light-solid-chromium-darwin.png new file mode 100644 index 00000000..626d4b1c Binary files /dev/null and b/packages/ui/src/lib/components/AlertDialog/__screenshots__/AlertDialog.vrt.test.ts/AlertDialog-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AppForm/__screenshots__/AppForm.vrt.test.ts/AppForm-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/AppForm/__screenshots__/AppForm.vrt.test.ts/AppForm-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..8526e4f4 Binary files /dev/null and b/packages/ui/src/lib/components/AppForm/__screenshots__/AppForm.vrt.test.ts/AppForm-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AppForm/__screenshots__/AppForm.vrt.test.ts/AppForm-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/AppForm/__screenshots__/AppForm.vrt.test.ts/AppForm-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..90e56257 Binary files /dev/null and b/packages/ui/src/lib/components/AppForm/__screenshots__/AppForm.vrt.test.ts/AppForm-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AppForm/__screenshots__/AppForm.vrt.test.ts/AppForm-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/AppForm/__screenshots__/AppForm.vrt.test.ts/AppForm-light-glass-chromium-darwin.png new file mode 100644 index 00000000..bcff4c5d Binary files /dev/null and b/packages/ui/src/lib/components/AppForm/__screenshots__/AppForm.vrt.test.ts/AppForm-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AppForm/__screenshots__/AppForm.vrt.test.ts/AppForm-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/AppForm/__screenshots__/AppForm.vrt.test.ts/AppForm-light-solid-chromium-darwin.png new file mode 100644 index 00000000..356c0aa6 Binary files /dev/null and b/packages/ui/src/lib/components/AppForm/__screenshots__/AppForm.vrt.test.ts/AppForm-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AppForm/__screenshots__/AppForm.vrt.test.ts/AppForm-mobile-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/AppForm/__screenshots__/AppForm.vrt.test.ts/AppForm-mobile-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..e6e9b2d7 Binary files /dev/null and b/packages/ui/src/lib/components/AppForm/__screenshots__/AppForm.vrt.test.ts/AppForm-mobile-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AppForm/__screenshots__/AppForm.vrt.test.ts/AppForm-mobile-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/AppForm/__screenshots__/AppForm.vrt.test.ts/AppForm-mobile-light-glass-chromium-darwin.png new file mode 100644 index 00000000..71bb232a Binary files /dev/null and b/packages/ui/src/lib/components/AppForm/__screenshots__/AppForm.vrt.test.ts/AppForm-mobile-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AppList/__screenshots__/AppList.vrt.test.ts/AppList-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/AppList/__screenshots__/AppList.vrt.test.ts/AppList-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..a823f794 Binary files /dev/null and b/packages/ui/src/lib/components/AppList/__screenshots__/AppList.vrt.test.ts/AppList-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AppList/__screenshots__/AppList.vrt.test.ts/AppList-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/AppList/__screenshots__/AppList.vrt.test.ts/AppList-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..102e600f Binary files /dev/null and b/packages/ui/src/lib/components/AppList/__screenshots__/AppList.vrt.test.ts/AppList-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AppList/__screenshots__/AppList.vrt.test.ts/AppList-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/AppList/__screenshots__/AppList.vrt.test.ts/AppList-light-glass-chromium-darwin.png new file mode 100644 index 00000000..e7734bed Binary files /dev/null and b/packages/ui/src/lib/components/AppList/__screenshots__/AppList.vrt.test.ts/AppList-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AppList/__screenshots__/AppList.vrt.test.ts/AppList-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/AppList/__screenshots__/AppList.vrt.test.ts/AppList-light-solid-chromium-darwin.png new file mode 100644 index 00000000..bf8947f2 Binary files /dev/null and b/packages/ui/src/lib/components/AppList/__screenshots__/AppList.vrt.test.ts/AppList-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AppList/__screenshots__/AppList.vrt.test.ts/AppList-mobile-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/AppList/__screenshots__/AppList.vrt.test.ts/AppList-mobile-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..a8f6a065 Binary files /dev/null and b/packages/ui/src/lib/components/AppList/__screenshots__/AppList.vrt.test.ts/AppList-mobile-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AppList/__screenshots__/AppList.vrt.test.ts/AppList-mobile-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/AppList/__screenshots__/AppList.vrt.test.ts/AppList-mobile-light-glass-chromium-darwin.png new file mode 100644 index 00000000..6bf37218 Binary files /dev/null and b/packages/ui/src/lib/components/AppList/__screenshots__/AppList.vrt.test.ts/AppList-mobile-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AppSettings/__screenshots__/AppSettings.vrt.test.ts/AppSettings-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/AppSettings/__screenshots__/AppSettings.vrt.test.ts/AppSettings-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..8b2024ae Binary files /dev/null and b/packages/ui/src/lib/components/AppSettings/__screenshots__/AppSettings.vrt.test.ts/AppSettings-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AppSettings/__screenshots__/AppSettings.vrt.test.ts/AppSettings-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/AppSettings/__screenshots__/AppSettings.vrt.test.ts/AppSettings-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..d924c800 Binary files /dev/null and b/packages/ui/src/lib/components/AppSettings/__screenshots__/AppSettings.vrt.test.ts/AppSettings-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AppSettings/__screenshots__/AppSettings.vrt.test.ts/AppSettings-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/AppSettings/__screenshots__/AppSettings.vrt.test.ts/AppSettings-light-glass-chromium-darwin.png new file mode 100644 index 00000000..aa871617 Binary files /dev/null and b/packages/ui/src/lib/components/AppSettings/__screenshots__/AppSettings.vrt.test.ts/AppSettings-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AppSettings/__screenshots__/AppSettings.vrt.test.ts/AppSettings-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/AppSettings/__screenshots__/AppSettings.vrt.test.ts/AppSettings-light-solid-chromium-darwin.png new file mode 100644 index 00000000..2571e428 Binary files /dev/null and b/packages/ui/src/lib/components/AppSettings/__screenshots__/AppSettings.vrt.test.ts/AppSettings-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AppSettings/__screenshots__/AppSettings.vrt.test.ts/AppSettings-mobile-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/AppSettings/__screenshots__/AppSettings.vrt.test.ts/AppSettings-mobile-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..5519855e Binary files /dev/null and b/packages/ui/src/lib/components/AppSettings/__screenshots__/AppSettings.vrt.test.ts/AppSettings-mobile-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AppSettings/__screenshots__/AppSettings.vrt.test.ts/AppSettings-mobile-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/AppSettings/__screenshots__/AppSettings.vrt.test.ts/AppSettings-mobile-light-glass-chromium-darwin.png new file mode 100644 index 00000000..73876c61 Binary files /dev/null and b/packages/ui/src/lib/components/AppSettings/__screenshots__/AppSettings.vrt.test.ts/AppSettings-mobile-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AppShell/__screenshots__/AppShellLayout.vrt.test.ts/AppShellLayout-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/AppShell/__screenshots__/AppShellLayout.vrt.test.ts/AppShellLayout-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..4816b46f Binary files /dev/null and b/packages/ui/src/lib/components/AppShell/__screenshots__/AppShellLayout.vrt.test.ts/AppShellLayout-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AppShell/__screenshots__/AppShellLayout.vrt.test.ts/AppShellLayout-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/AppShell/__screenshots__/AppShellLayout.vrt.test.ts/AppShellLayout-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..ed9fe348 Binary files /dev/null and b/packages/ui/src/lib/components/AppShell/__screenshots__/AppShellLayout.vrt.test.ts/AppShellLayout-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AppShell/__screenshots__/AppShellLayout.vrt.test.ts/AppShellLayout-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/AppShell/__screenshots__/AppShellLayout.vrt.test.ts/AppShellLayout-light-glass-chromium-darwin.png new file mode 100644 index 00000000..3824a6a0 Binary files /dev/null and b/packages/ui/src/lib/components/AppShell/__screenshots__/AppShellLayout.vrt.test.ts/AppShellLayout-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AppShell/__screenshots__/AppShellLayout.vrt.test.ts/AppShellLayout-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/AppShell/__screenshots__/AppShellLayout.vrt.test.ts/AppShellLayout-light-solid-chromium-darwin.png new file mode 100644 index 00000000..71debb82 Binary files /dev/null and b/packages/ui/src/lib/components/AppShell/__screenshots__/AppShellLayout.vrt.test.ts/AppShellLayout-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AppShell/__screenshots__/AppShellLayout.vrt.test.ts/AppShellLayout-mobile-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/AppShell/__screenshots__/AppShellLayout.vrt.test.ts/AppShellLayout-mobile-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..5a6fd08a Binary files /dev/null and b/packages/ui/src/lib/components/AppShell/__screenshots__/AppShellLayout.vrt.test.ts/AppShellLayout-mobile-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AppShell/__screenshots__/AppShellLayout.vrt.test.ts/AppShellLayout-mobile-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/AppShell/__screenshots__/AppShellLayout.vrt.test.ts/AppShellLayout-mobile-light-glass-chromium-darwin.png new file mode 100644 index 00000000..252bb3a0 Binary files /dev/null and b/packages/ui/src/lib/components/AppShell/__screenshots__/AppShellLayout.vrt.test.ts/AppShellLayout-mobile-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AuthLayout/__screenshots__/AuthCard.vrt.test.ts/AuthCard-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/AuthLayout/__screenshots__/AuthCard.vrt.test.ts/AuthCard-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..c5822552 Binary files /dev/null and b/packages/ui/src/lib/components/AuthLayout/__screenshots__/AuthCard.vrt.test.ts/AuthCard-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AuthLayout/__screenshots__/AuthCard.vrt.test.ts/AuthCard-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/AuthLayout/__screenshots__/AuthCard.vrt.test.ts/AuthCard-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..c994d592 Binary files /dev/null and b/packages/ui/src/lib/components/AuthLayout/__screenshots__/AuthCard.vrt.test.ts/AuthCard-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AuthLayout/__screenshots__/AuthCard.vrt.test.ts/AuthCard-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/AuthLayout/__screenshots__/AuthCard.vrt.test.ts/AuthCard-light-glass-chromium-darwin.png new file mode 100644 index 00000000..8b48fd79 Binary files /dev/null and b/packages/ui/src/lib/components/AuthLayout/__screenshots__/AuthCard.vrt.test.ts/AuthCard-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AuthLayout/__screenshots__/AuthCard.vrt.test.ts/AuthCard-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/AuthLayout/__screenshots__/AuthCard.vrt.test.ts/AuthCard-light-solid-chromium-darwin.png new file mode 100644 index 00000000..e10779c2 Binary files /dev/null and b/packages/ui/src/lib/components/AuthLayout/__screenshots__/AuthCard.vrt.test.ts/AuthCard-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AuthLayout/__screenshots__/AuthLayout.vrt.test.ts/AuthLayout-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/AuthLayout/__screenshots__/AuthLayout.vrt.test.ts/AuthLayout-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..a165b48d Binary files /dev/null and b/packages/ui/src/lib/components/AuthLayout/__screenshots__/AuthLayout.vrt.test.ts/AuthLayout-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AuthLayout/__screenshots__/AuthLayout.vrt.test.ts/AuthLayout-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/AuthLayout/__screenshots__/AuthLayout.vrt.test.ts/AuthLayout-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..a165b48d Binary files /dev/null and b/packages/ui/src/lib/components/AuthLayout/__screenshots__/AuthLayout.vrt.test.ts/AuthLayout-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AuthLayout/__screenshots__/AuthLayout.vrt.test.ts/AuthLayout-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/AuthLayout/__screenshots__/AuthLayout.vrt.test.ts/AuthLayout-light-glass-chromium-darwin.png new file mode 100644 index 00000000..59afdfa3 Binary files /dev/null and b/packages/ui/src/lib/components/AuthLayout/__screenshots__/AuthLayout.vrt.test.ts/AuthLayout-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AuthLayout/__screenshots__/AuthLayout.vrt.test.ts/AuthLayout-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/AuthLayout/__screenshots__/AuthLayout.vrt.test.ts/AuthLayout-light-solid-chromium-darwin.png new file mode 100644 index 00000000..59afdfa3 Binary files /dev/null and b/packages/ui/src/lib/components/AuthLayout/__screenshots__/AuthLayout.vrt.test.ts/AuthLayout-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AuthLayout/__screenshots__/AuthLayout.vrt.test.ts/AuthLayout-mobile-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/AuthLayout/__screenshots__/AuthLayout.vrt.test.ts/AuthLayout-mobile-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..6a302edb Binary files /dev/null and b/packages/ui/src/lib/components/AuthLayout/__screenshots__/AuthLayout.vrt.test.ts/AuthLayout-mobile-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/AuthLayout/__screenshots__/AuthLayout.vrt.test.ts/AuthLayout-mobile-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/AuthLayout/__screenshots__/AuthLayout.vrt.test.ts/AuthLayout-mobile-light-glass-chromium-darwin.png new file mode 100644 index 00000000..c810898e Binary files /dev/null and b/packages/ui/src/lib/components/AuthLayout/__screenshots__/AuthLayout.vrt.test.ts/AuthLayout-mobile-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Avatar/__screenshots__/Avatar.vrt.test.ts/Avatar-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Avatar/__screenshots__/Avatar.vrt.test.ts/Avatar-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..f1e10ad7 Binary files /dev/null and b/packages/ui/src/lib/components/Avatar/__screenshots__/Avatar.vrt.test.ts/Avatar-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Avatar/__screenshots__/Avatar.vrt.test.ts/Avatar-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Avatar/__screenshots__/Avatar.vrt.test.ts/Avatar-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..ffd54706 Binary files /dev/null and b/packages/ui/src/lib/components/Avatar/__screenshots__/Avatar.vrt.test.ts/Avatar-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Avatar/__screenshots__/Avatar.vrt.test.ts/Avatar-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Avatar/__screenshots__/Avatar.vrt.test.ts/Avatar-light-glass-chromium-darwin.png new file mode 100644 index 00000000..923fe0ec Binary files /dev/null and b/packages/ui/src/lib/components/Avatar/__screenshots__/Avatar.vrt.test.ts/Avatar-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Avatar/__screenshots__/Avatar.vrt.test.ts/Avatar-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Avatar/__screenshots__/Avatar.vrt.test.ts/Avatar-light-solid-chromium-darwin.png new file mode 100644 index 00000000..833b5bf9 Binary files /dev/null and b/packages/ui/src/lib/components/Avatar/__screenshots__/Avatar.vrt.test.ts/Avatar-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Badge/__screenshots__/Badge.vrt.test.ts/Badge-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Badge/__screenshots__/Badge.vrt.test.ts/Badge-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..df90e20f Binary files /dev/null and b/packages/ui/src/lib/components/Badge/__screenshots__/Badge.vrt.test.ts/Badge-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Badge/__screenshots__/Badge.vrt.test.ts/Badge-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Badge/__screenshots__/Badge.vrt.test.ts/Badge-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..c931fb46 Binary files /dev/null and b/packages/ui/src/lib/components/Badge/__screenshots__/Badge.vrt.test.ts/Badge-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Badge/__screenshots__/Badge.vrt.test.ts/Badge-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Badge/__screenshots__/Badge.vrt.test.ts/Badge-light-glass-chromium-darwin.png new file mode 100644 index 00000000..6351f64d Binary files /dev/null and b/packages/ui/src/lib/components/Badge/__screenshots__/Badge.vrt.test.ts/Badge-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Badge/__screenshots__/Badge.vrt.test.ts/Badge-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Badge/__screenshots__/Badge.vrt.test.ts/Badge-light-solid-chromium-darwin.png new file mode 100644 index 00000000..1c3b3e32 Binary files /dev/null and b/packages/ui/src/lib/components/Badge/__screenshots__/Badge.vrt.test.ts/Badge-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Breadcrumb/__screenshots__/Breadcrumb.vrt.test.ts/Breadcrumb-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Breadcrumb/__screenshots__/Breadcrumb.vrt.test.ts/Breadcrumb-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..6260fd12 Binary files /dev/null and b/packages/ui/src/lib/components/Breadcrumb/__screenshots__/Breadcrumb.vrt.test.ts/Breadcrumb-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Breadcrumb/__screenshots__/Breadcrumb.vrt.test.ts/Breadcrumb-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Breadcrumb/__screenshots__/Breadcrumb.vrt.test.ts/Breadcrumb-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..ad150b43 Binary files /dev/null and b/packages/ui/src/lib/components/Breadcrumb/__screenshots__/Breadcrumb.vrt.test.ts/Breadcrumb-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Breadcrumb/__screenshots__/Breadcrumb.vrt.test.ts/Breadcrumb-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Breadcrumb/__screenshots__/Breadcrumb.vrt.test.ts/Breadcrumb-light-glass-chromium-darwin.png new file mode 100644 index 00000000..21973aac Binary files /dev/null and b/packages/ui/src/lib/components/Breadcrumb/__screenshots__/Breadcrumb.vrt.test.ts/Breadcrumb-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Breadcrumb/__screenshots__/Breadcrumb.vrt.test.ts/Breadcrumb-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Breadcrumb/__screenshots__/Breadcrumb.vrt.test.ts/Breadcrumb-light-solid-chromium-darwin.png new file mode 100644 index 00000000..751b9933 Binary files /dev/null and b/packages/ui/src/lib/components/Breadcrumb/__screenshots__/Breadcrumb.vrt.test.ts/Breadcrumb-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Button/__screenshots__/Button.vrt.test.ts/Button-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Button/__screenshots__/Button.vrt.test.ts/Button-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..2e0eb1fc Binary files /dev/null and b/packages/ui/src/lib/components/Button/__screenshots__/Button.vrt.test.ts/Button-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Button/__screenshots__/Button.vrt.test.ts/Button-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Button/__screenshots__/Button.vrt.test.ts/Button-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..746ac4ca Binary files /dev/null and b/packages/ui/src/lib/components/Button/__screenshots__/Button.vrt.test.ts/Button-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Button/__screenshots__/Button.vrt.test.ts/Button-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Button/__screenshots__/Button.vrt.test.ts/Button-light-glass-chromium-darwin.png new file mode 100644 index 00000000..983e891d Binary files /dev/null and b/packages/ui/src/lib/components/Button/__screenshots__/Button.vrt.test.ts/Button-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Button/__screenshots__/Button.vrt.test.ts/Button-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Button/__screenshots__/Button.vrt.test.ts/Button-light-solid-chromium-darwin.png new file mode 100644 index 00000000..c836b69a Binary files /dev/null and b/packages/ui/src/lib/components/Button/__screenshots__/Button.vrt.test.ts/Button-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Carousel/Carousel.svelte.test.ts b/packages/ui/src/lib/components/Carousel/Carousel.svelte.test.ts index f5641e85..79aa14e3 100644 --- a/packages/ui/src/lib/components/Carousel/Carousel.svelte.test.ts +++ b/packages/ui/src/lib/components/Carousel/Carousel.svelte.test.ts @@ -14,6 +14,10 @@ const btn = (c: HTMLElement, name: RegExp) => name.test(b.getAttribute("aria-label") ?? b.textContent ?? "") ) as HTMLButtonElement; const settle = () => new Promise((r) => setTimeout(r, 450)); +// The autoplay chain is plain setTimeout: a fake clock installed BEFORE render, advanced past several +// of its intervals, proves "nothing rotates" with no wall time (a clock faked after render would not +// own the component's timer and prove nothing). +const fakeClock = () => vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] }); beforeEach(() => { vi.useRealTimers(); @@ -135,30 +139,38 @@ describe("Carousel controls", () => { describe("Carousel autoplay (opt in, always stoppable)", () => { it("does not rotate and shows no play control unless autoplay is set", async () => { + fakeClock(); const screen = await render(Harness, {}); expect(q(screen.container, ".ca-carousel").querySelector(".ca-carousel__play")).toBeNull(); - await new Promise((r) => setTimeout(r, 700)); + await vi.advanceTimersByTimeAsync(700); expect(dots(screen.container)[0].getAttribute("aria-current")).toBe("true"); }); it("rotates on its own, offers Pause, and stops when paused", async () => { + fakeClock(); const screen = await render(Harness, { autoplay: 300, loop: true }); const play = q(screen.container, ".ca-carousel__play"); expect(play.getAttribute("aria-label")).toBe("Pause autoplay"); - await expect.poll(() => dots(screen.container)[1].getAttribute("aria-current")).toBe("true"); - await play.click(); + await vi.advanceTimersByTimeAsync(300); + expect(dots(screen.container)[1].getAttribute("aria-current")).toBe("true"); + // Dispatch the click directly: the clock stays the one that owns the autoplay timer, and a + // Playwright click (its own round trips) would need real timers. + play.click(); + await vi.advanceTimersByTimeAsync(0); expect(play.getAttribute("aria-label")).toBe("Start autoplay"); const at = dots(screen.container).findIndex((d) => d.hasAttribute("aria-current")); - await new Promise((r) => setTimeout(r, 900)); + await vi.advanceTimersByTimeAsync(900); expect(dots(screen.container).findIndex((d) => d.hasAttribute("aria-current"))).toBe(at); + vi.useRealTimers(); expect(await checkA11y(screen.container)).toHaveNoViolations(); }); it("pauses while focus is inside and while the pointer is over it", async () => { + fakeClock(); const screen = await render(Harness, { autoplay: 300, loop: true }); btn(screen.container, /slide 1 action/i).focus(); const at = dots(screen.container).findIndex((d) => d.hasAttribute("aria-current")); - await new Promise((r) => setTimeout(r, 900)); + await vi.advanceTimersByTimeAsync(900); expect(dots(screen.container).findIndex((d) => d.hasAttribute("aria-current"))).toBe(at); }); @@ -184,10 +196,12 @@ describe("Carousel reduced motion", () => { } as unknown as MediaQueryList) : real(q)) as typeof window.matchMedia; try { + fakeClock(); const screen = await render(Harness, { autoplay: 200, loop: true }); expect(screen.container.querySelector(".ca-carousel__play")).toBeNull(); - await new Promise((r) => setTimeout(r, 700)); + await vi.advanceTimersByTimeAsync(700); expect(dots(screen.container)[0].getAttribute("aria-current")).toBe("true"); + vi.useRealTimers(); expect(await checkA11y(screen.container)).toHaveNoViolations(); } finally { window.matchMedia = real; diff --git a/packages/ui/src/lib/components/Checkbox/__screenshots__/Checkbox.vrt.test.ts/Checkbox-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Checkbox/__screenshots__/Checkbox.vrt.test.ts/Checkbox-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..c863939e Binary files /dev/null and b/packages/ui/src/lib/components/Checkbox/__screenshots__/Checkbox.vrt.test.ts/Checkbox-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Checkbox/__screenshots__/Checkbox.vrt.test.ts/Checkbox-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Checkbox/__screenshots__/Checkbox.vrt.test.ts/Checkbox-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..134bb23c Binary files /dev/null and b/packages/ui/src/lib/components/Checkbox/__screenshots__/Checkbox.vrt.test.ts/Checkbox-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Checkbox/__screenshots__/Checkbox.vrt.test.ts/Checkbox-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Checkbox/__screenshots__/Checkbox.vrt.test.ts/Checkbox-light-glass-chromium-darwin.png new file mode 100644 index 00000000..c67009dc Binary files /dev/null and b/packages/ui/src/lib/components/Checkbox/__screenshots__/Checkbox.vrt.test.ts/Checkbox-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Checkbox/__screenshots__/Checkbox.vrt.test.ts/Checkbox-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Checkbox/__screenshots__/Checkbox.vrt.test.ts/Checkbox-light-solid-chromium-darwin.png new file mode 100644 index 00000000..77cba5fa Binary files /dev/null and b/packages/ui/src/lib/components/Checkbox/__screenshots__/Checkbox.vrt.test.ts/Checkbox-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Chip/Chip.svelte b/packages/ui/src/lib/components/Chip/Chip.svelte index df60b879..654e0488 100644 --- a/packages/ui/src/lib/components/Chip/Chip.svelte +++ b/packages/ui/src/lib/components/Chip/Chip.svelte @@ -168,10 +168,20 @@ .ca-chip--brand { background: color-mix(in oklab, var(--ca-brand-main) 14%, transparent); color: var(--ca-brand-text); border-color: color-mix(in oklab, var(--ca-brand-main) 30%, transparent); } /* Dark mode variants: raise L for legibility */ - :global(.dark) .ca-chip--success { color: var(--ca-semantic-success); border-color: color-mix(in oklab, var(--ca-semantic-success) 35%, transparent); } - :global(.dark) .ca-chip--error { color: var(--ca-semantic-error); border-color: color-mix(in oklab, var(--ca-semantic-error) 35%, transparent); } - :global(.dark) .ca-chip--warning { color: var(--ca-semantic-warning); border-color: color-mix(in oklab, var(--ca-semantic-warning) 35%, transparent); } - :global(.dark) .ca-chip--info { color: var(--ca-semantic-info); border-color: color-mix(in oklab, var(--ca-semantic-info) 35%, transparent); } + :global(.dark) .ca-chip--success, + :global([data-theme-scope='dark']) .ca-chip--success { color: var(--ca-semantic-success); border-color: color-mix(in oklab, var(--ca-semantic-success) 35%, transparent); } + :global(.dark) .ca-chip--error, + :global([data-theme-scope='dark']) .ca-chip--error { color: var(--ca-semantic-error); border-color: color-mix(in oklab, var(--ca-semantic-error) 35%, transparent); } + :global(.dark) .ca-chip--warning, + :global([data-theme-scope='dark']) .ca-chip--warning { color: var(--ca-semantic-warning); border-color: color-mix(in oklab, var(--ca-semantic-warning) 35%, transparent); } + :global(.dark) .ca-chip--info, + :global([data-theme-scope='dark']) .ca-chip--info { color: var(--ca-semantic-info); border-color: color-mix(in oklab, var(--ca-semantic-info) 35%, transparent); } + + /* A light island inside a dark page: back to the light-mode text of the variants above. */ + :global([data-theme-scope='light']) .ca-chip--success { color: color-mix(in oklab, var(--ca-semantic-success) 65%, black); } + :global([data-theme-scope='light']) .ca-chip--error { color: color-mix(in oklab, var(--ca-semantic-error) 65%, black); } + :global([data-theme-scope='light']) .ca-chip--warning { color: color-mix(in oklab, var(--ca-semantic-warning) 65%, black); } + :global([data-theme-scope='light']) .ca-chip--info { color: color-mix(in oklab, var(--ca-semantic-info) 65%, black); } /* Size */ .ca-chip--sm { font-size: var(--ca-font-size-micro); height: var(--ca-space-5); padding: 0 var(--ca-space-2); } diff --git a/packages/ui/src/lib/components/Chip/__screenshots__/Chip.vrt.test.ts/Chip-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Chip/__screenshots__/Chip.vrt.test.ts/Chip-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..3ee6fb36 Binary files /dev/null and b/packages/ui/src/lib/components/Chip/__screenshots__/Chip.vrt.test.ts/Chip-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Chip/__screenshots__/Chip.vrt.test.ts/Chip-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Chip/__screenshots__/Chip.vrt.test.ts/Chip-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..ca8ed872 Binary files /dev/null and b/packages/ui/src/lib/components/Chip/__screenshots__/Chip.vrt.test.ts/Chip-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Chip/__screenshots__/Chip.vrt.test.ts/Chip-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Chip/__screenshots__/Chip.vrt.test.ts/Chip-light-glass-chromium-darwin.png new file mode 100644 index 00000000..a43c4900 Binary files /dev/null and b/packages/ui/src/lib/components/Chip/__screenshots__/Chip.vrt.test.ts/Chip-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Chip/__screenshots__/Chip.vrt.test.ts/Chip-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Chip/__screenshots__/Chip.vrt.test.ts/Chip-light-solid-chromium-darwin.png new file mode 100644 index 00000000..ccf53b3e Binary files /dev/null and b/packages/ui/src/lib/components/Chip/__screenshots__/Chip.vrt.test.ts/Chip-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/CodeSnippet/CodeSnippet.svelte.test.ts b/packages/ui/src/lib/components/CodeSnippet/CodeSnippet.svelte.test.ts index 33ae6f36..3edc096f 100644 --- a/packages/ui/src/lib/components/CodeSnippet/CodeSnippet.svelte.test.ts +++ b/packages/ui/src/lib/components/CodeSnippet/CodeSnippet.svelte.test.ts @@ -4,6 +4,7 @@ import { render } from "vitest-browser-svelte"; import { checkA11y } from "../../../../tests/a11y"; import { settled } from "../../../../tests/settled"; import { setLocale } from "../../i18n/index.js"; +import { withTheme } from "../../testing"; import CodeSnippet from "./CodeSnippet.svelte"; const CODE = "const answer: number = 42;\nconsole.log(answer);"; @@ -49,6 +50,26 @@ describe("CodeSnippet", () => { await expect.element(screen.getByRole("button", { name: "Copiar código" })).toBeInTheDocument(); }); + // The docs site checks contrast outside
 only (a page holds thousands of token spans), so the
+	// highlighted palette is proven here, once, in both schemes.
+	it.each(["dark", "light"] as const)(
+		"highlighted tokens keep their contrast in %s",
+		async (scheme) => {
+			const restore = withTheme({ scheme });
+			try {
+				const screen = await render(CodeSnippet, { code: CODE, language: "typescript" });
+				await vi.waitFor(() => expect(screen.container.querySelector("pre.shiki")).not.toBeNull(), {
+					timeout: 15_000,
+				});
+				expect(
+					await checkA11y(screen.container, { runOnly: ["color-contrast"] })
+				).toHaveNoViolations();
+			} finally {
+				restore();
+			}
+		}
+	);
+
 	it("token colours follow the colour scheme of their context without re-highlighting", async () => {
 		const screen = await render(CodeSnippet, { code: CODE, language: "typescript" });
 		await vi.waitFor(() => expect(screen.container.querySelector("pre.shiki")).not.toBeNull(), {
diff --git a/packages/ui/src/lib/components/Collapsible/__screenshots__/Collapsible.vrt.test.ts/Collapsible-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Collapsible/__screenshots__/Collapsible.vrt.test.ts/Collapsible-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..05625e69
Binary files /dev/null and b/packages/ui/src/lib/components/Collapsible/__screenshots__/Collapsible.vrt.test.ts/Collapsible-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Collapsible/__screenshots__/Collapsible.vrt.test.ts/Collapsible-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Collapsible/__screenshots__/Collapsible.vrt.test.ts/Collapsible-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..9cf2f685
Binary files /dev/null and b/packages/ui/src/lib/components/Collapsible/__screenshots__/Collapsible.vrt.test.ts/Collapsible-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Collapsible/__screenshots__/Collapsible.vrt.test.ts/Collapsible-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Collapsible/__screenshots__/Collapsible.vrt.test.ts/Collapsible-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..f193dd84
Binary files /dev/null and b/packages/ui/src/lib/components/Collapsible/__screenshots__/Collapsible.vrt.test.ts/Collapsible-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Collapsible/__screenshots__/Collapsible.vrt.test.ts/Collapsible-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Collapsible/__screenshots__/Collapsible.vrt.test.ts/Collapsible-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..a3451eef
Binary files /dev/null and b/packages/ui/src/lib/components/Collapsible/__screenshots__/Collapsible.vrt.test.ts/Collapsible-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Combobox/__screenshots__/Combobox.vrt.test.ts/Combobox-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Combobox/__screenshots__/Combobox.vrt.test.ts/Combobox-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..01318267
Binary files /dev/null and b/packages/ui/src/lib/components/Combobox/__screenshots__/Combobox.vrt.test.ts/Combobox-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Combobox/__screenshots__/Combobox.vrt.test.ts/Combobox-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Combobox/__screenshots__/Combobox.vrt.test.ts/Combobox-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..f3c89b37
Binary files /dev/null and b/packages/ui/src/lib/components/Combobox/__screenshots__/Combobox.vrt.test.ts/Combobox-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Combobox/__screenshots__/Combobox.vrt.test.ts/Combobox-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Combobox/__screenshots__/Combobox.vrt.test.ts/Combobox-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..49c43cae
Binary files /dev/null and b/packages/ui/src/lib/components/Combobox/__screenshots__/Combobox.vrt.test.ts/Combobox-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Combobox/__screenshots__/Combobox.vrt.test.ts/Combobox-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Combobox/__screenshots__/Combobox.vrt.test.ts/Combobox-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..a38fc3cb
Binary files /dev/null and b/packages/ui/src/lib/components/Combobox/__screenshots__/Combobox.vrt.test.ts/Combobox-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/CommandPalette/CommandPalette.throttled.svelte.test.ts b/packages/ui/src/lib/components/CommandPalette/CommandPalette.throttled.svelte.test.ts
index e0ebfe4e..b3bed87c 100644
--- a/packages/ui/src/lib/components/CommandPalette/CommandPalette.throttled.svelte.test.ts
+++ b/packages/ui/src/lib/components/CommandPalette/CommandPalette.throttled.svelte.test.ts
@@ -36,9 +36,13 @@ describe("CommandPalette under 40x CPU throttle — minQueryLength (#97)", () =>
 });
 
 // ArrowDown: both 40x and 150x exercise the selection-state race.
-// Wall time at 150x scales with throttle; 30 s gives 2× margin over the 40x median × (150/40).
-describe.each([40, 150])("CommandPalette under %dx CPU throttle — ArrowDown (#97)", (rate) => {
-	const budget = rate >= 150 ? 30_000 : 15_000;
+// Wall time at 150x scales with throttle (see the budget below).
+// A short title: Vitest names the trace file with the full title and a name over 255 bytes fails with
+// ENAMETOOLONG, losing the trace exactly when the test fails.
+describe.each([40, 150])("CommandPalette %dx throttle, ArrowDown (#97)", (rate) => {
+	// The limit is a hang detector, not an assertion: at 150x this test takes ~19 s on a quiet machine and 26 s
+	// at load 45, so 30 s failed it whenever other suites shared the machine (#546).
+	const budget = rate >= 150 ? 90_000 : 30_000;
 
 	beforeEach(async () => {
 		await cdp().send("Emulation.setCPUThrottlingRate", { rate });
@@ -48,7 +52,7 @@ describe.each([40, 150])("CommandPalette under %dx CPU throttle — ArrowDown (#
 	});
 
 	it(
-		"ArrowDown/ArrowUp move aria-selected under load (#97)",
+		"ArrowDown/ArrowUp move aria-selected",
 		async () => {
 			// Minimal: one Down + one Up proves selection moves without rAF race.
 			// Full clamping coverage lives in CommandPalette.svelte.test.ts (no throttle).
diff --git a/packages/ui/src/lib/components/CommandPalette/__screenshots__/CommandPalette.vrt.test.ts/CommandPalette-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/CommandPalette/__screenshots__/CommandPalette.vrt.test.ts/CommandPalette-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..97158519
Binary files /dev/null and b/packages/ui/src/lib/components/CommandPalette/__screenshots__/CommandPalette.vrt.test.ts/CommandPalette-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/CommandPalette/__screenshots__/CommandPalette.vrt.test.ts/CommandPalette-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/CommandPalette/__screenshots__/CommandPalette.vrt.test.ts/CommandPalette-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..3c79173c
Binary files /dev/null and b/packages/ui/src/lib/components/CommandPalette/__screenshots__/CommandPalette.vrt.test.ts/CommandPalette-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/CommandPalette/__screenshots__/CommandPalette.vrt.test.ts/CommandPalette-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/CommandPalette/__screenshots__/CommandPalette.vrt.test.ts/CommandPalette-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..004c0c5e
Binary files /dev/null and b/packages/ui/src/lib/components/CommandPalette/__screenshots__/CommandPalette.vrt.test.ts/CommandPalette-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/CommandPalette/__screenshots__/CommandPalette.vrt.test.ts/CommandPalette-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/CommandPalette/__screenshots__/CommandPalette.vrt.test.ts/CommandPalette-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..d6152d8a
Binary files /dev/null and b/packages/ui/src/lib/components/CommandPalette/__screenshots__/CommandPalette.vrt.test.ts/CommandPalette-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/CopyButton/__screenshots__/CopyButton.vrt.test.ts/CopyButton-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/CopyButton/__screenshots__/CopyButton.vrt.test.ts/CopyButton-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..49f29aad
Binary files /dev/null and b/packages/ui/src/lib/components/CopyButton/__screenshots__/CopyButton.vrt.test.ts/CopyButton-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/CopyButton/__screenshots__/CopyButton.vrt.test.ts/CopyButton-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/CopyButton/__screenshots__/CopyButton.vrt.test.ts/CopyButton-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..b91a444b
Binary files /dev/null and b/packages/ui/src/lib/components/CopyButton/__screenshots__/CopyButton.vrt.test.ts/CopyButton-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/CopyButton/__screenshots__/CopyButton.vrt.test.ts/CopyButton-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/CopyButton/__screenshots__/CopyButton.vrt.test.ts/CopyButton-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..094ff649
Binary files /dev/null and b/packages/ui/src/lib/components/CopyButton/__screenshots__/CopyButton.vrt.test.ts/CopyButton-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/CopyButton/__screenshots__/CopyButton.vrt.test.ts/CopyButton-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/CopyButton/__screenshots__/CopyButton.vrt.test.ts/CopyButton-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..60b947ab
Binary files /dev/null and b/packages/ui/src/lib/components/CopyButton/__screenshots__/CopyButton.vrt.test.ts/CopyButton-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Date/__screenshots__/Calendar.vrt.test.ts/Calendar-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Date/__screenshots__/Calendar.vrt.test.ts/Calendar-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..b91912aa
Binary files /dev/null and b/packages/ui/src/lib/components/Date/__screenshots__/Calendar.vrt.test.ts/Calendar-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Date/__screenshots__/Calendar.vrt.test.ts/Calendar-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Date/__screenshots__/Calendar.vrt.test.ts/Calendar-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..5e581a8d
Binary files /dev/null and b/packages/ui/src/lib/components/Date/__screenshots__/Calendar.vrt.test.ts/Calendar-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Date/__screenshots__/Calendar.vrt.test.ts/Calendar-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Date/__screenshots__/Calendar.vrt.test.ts/Calendar-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..404a1f13
Binary files /dev/null and b/packages/ui/src/lib/components/Date/__screenshots__/Calendar.vrt.test.ts/Calendar-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Date/__screenshots__/Calendar.vrt.test.ts/Calendar-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Date/__screenshots__/Calendar.vrt.test.ts/Calendar-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..53f52c46
Binary files /dev/null and b/packages/ui/src/lib/components/Date/__screenshots__/Calendar.vrt.test.ts/Calendar-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Date/__screenshots__/DateField.vrt.test.ts/DateField-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Date/__screenshots__/DateField.vrt.test.ts/DateField-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..8531f74f
Binary files /dev/null and b/packages/ui/src/lib/components/Date/__screenshots__/DateField.vrt.test.ts/DateField-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Date/__screenshots__/DateField.vrt.test.ts/DateField-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Date/__screenshots__/DateField.vrt.test.ts/DateField-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..4dbd0235
Binary files /dev/null and b/packages/ui/src/lib/components/Date/__screenshots__/DateField.vrt.test.ts/DateField-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Date/__screenshots__/DateField.vrt.test.ts/DateField-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Date/__screenshots__/DateField.vrt.test.ts/DateField-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..62122b5b
Binary files /dev/null and b/packages/ui/src/lib/components/Date/__screenshots__/DateField.vrt.test.ts/DateField-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Date/__screenshots__/DateField.vrt.test.ts/DateField-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Date/__screenshots__/DateField.vrt.test.ts/DateField-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..dd7d76a5
Binary files /dev/null and b/packages/ui/src/lib/components/Date/__screenshots__/DateField.vrt.test.ts/DateField-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Date/__screenshots__/DatePicker.vrt.test.ts/DatePicker-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Date/__screenshots__/DatePicker.vrt.test.ts/DatePicker-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..829071a9
Binary files /dev/null and b/packages/ui/src/lib/components/Date/__screenshots__/DatePicker.vrt.test.ts/DatePicker-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Date/__screenshots__/DatePicker.vrt.test.ts/DatePicker-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Date/__screenshots__/DatePicker.vrt.test.ts/DatePicker-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..e2948b19
Binary files /dev/null and b/packages/ui/src/lib/components/Date/__screenshots__/DatePicker.vrt.test.ts/DatePicker-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Date/__screenshots__/DatePicker.vrt.test.ts/DatePicker-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Date/__screenshots__/DatePicker.vrt.test.ts/DatePicker-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..5774b204
Binary files /dev/null and b/packages/ui/src/lib/components/Date/__screenshots__/DatePicker.vrt.test.ts/DatePicker-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Date/__screenshots__/DatePicker.vrt.test.ts/DatePicker-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Date/__screenshots__/DatePicker.vrt.test.ts/DatePicker-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..8e7e35a1
Binary files /dev/null and b/packages/ui/src/lib/components/Date/__screenshots__/DatePicker.vrt.test.ts/DatePicker-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Date/__screenshots__/DateRangePicker.vrt.test.ts/DateRangePicker-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Date/__screenshots__/DateRangePicker.vrt.test.ts/DateRangePicker-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..647514cd
Binary files /dev/null and b/packages/ui/src/lib/components/Date/__screenshots__/DateRangePicker.vrt.test.ts/DateRangePicker-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Date/__screenshots__/DateRangePicker.vrt.test.ts/DateRangePicker-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Date/__screenshots__/DateRangePicker.vrt.test.ts/DateRangePicker-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..4e2e7b7a
Binary files /dev/null and b/packages/ui/src/lib/components/Date/__screenshots__/DateRangePicker.vrt.test.ts/DateRangePicker-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Date/__screenshots__/DateRangePicker.vrt.test.ts/DateRangePicker-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Date/__screenshots__/DateRangePicker.vrt.test.ts/DateRangePicker-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..a22d1c14
Binary files /dev/null and b/packages/ui/src/lib/components/Date/__screenshots__/DateRangePicker.vrt.test.ts/DateRangePicker-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Date/__screenshots__/DateRangePicker.vrt.test.ts/DateRangePicker-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Date/__screenshots__/DateRangePicker.vrt.test.ts/DateRangePicker-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..984ef6d8
Binary files /dev/null and b/packages/ui/src/lib/components/Date/__screenshots__/DateRangePicker.vrt.test.ts/DateRangePicker-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Date/__screenshots__/RangeCalendar.vrt.test.ts/RangeCalendar-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Date/__screenshots__/RangeCalendar.vrt.test.ts/RangeCalendar-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..50c4dd71
Binary files /dev/null and b/packages/ui/src/lib/components/Date/__screenshots__/RangeCalendar.vrt.test.ts/RangeCalendar-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Date/__screenshots__/RangeCalendar.vrt.test.ts/RangeCalendar-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Date/__screenshots__/RangeCalendar.vrt.test.ts/RangeCalendar-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..30c6f1b9
Binary files /dev/null and b/packages/ui/src/lib/components/Date/__screenshots__/RangeCalendar.vrt.test.ts/RangeCalendar-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Date/__screenshots__/RangeCalendar.vrt.test.ts/RangeCalendar-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Date/__screenshots__/RangeCalendar.vrt.test.ts/RangeCalendar-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..b1c98181
Binary files /dev/null and b/packages/ui/src/lib/components/Date/__screenshots__/RangeCalendar.vrt.test.ts/RangeCalendar-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Date/__screenshots__/RangeCalendar.vrt.test.ts/RangeCalendar-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Date/__screenshots__/RangeCalendar.vrt.test.ts/RangeCalendar-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..43c12703
Binary files /dev/null and b/packages/ui/src/lib/components/Date/__screenshots__/RangeCalendar.vrt.test.ts/RangeCalendar-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Date/__screenshots__/TimeField.vrt.test.ts/TimeField-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Date/__screenshots__/TimeField.vrt.test.ts/TimeField-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..545ff9ab
Binary files /dev/null and b/packages/ui/src/lib/components/Date/__screenshots__/TimeField.vrt.test.ts/TimeField-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Date/__screenshots__/TimeField.vrt.test.ts/TimeField-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Date/__screenshots__/TimeField.vrt.test.ts/TimeField-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..69a487ba
Binary files /dev/null and b/packages/ui/src/lib/components/Date/__screenshots__/TimeField.vrt.test.ts/TimeField-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Date/__screenshots__/TimeField.vrt.test.ts/TimeField-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Date/__screenshots__/TimeField.vrt.test.ts/TimeField-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..2e4e1e8d
Binary files /dev/null and b/packages/ui/src/lib/components/Date/__screenshots__/TimeField.vrt.test.ts/TimeField-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Date/__screenshots__/TimeField.vrt.test.ts/TimeField-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Date/__screenshots__/TimeField.vrt.test.ts/TimeField-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..5ca4ada9
Binary files /dev/null and b/packages/ui/src/lib/components/Date/__screenshots__/TimeField.vrt.test.ts/TimeField-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-chromeless-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-chromeless-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..a99baf4f
Binary files /dev/null and b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-chromeless-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-chromeless-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-chromeless-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..793d5013
Binary files /dev/null and b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-chromeless-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-chromeless-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-chromeless-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..cb759512
Binary files /dev/null and b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-chromeless-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-chromeless-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-chromeless-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..f166c208
Binary files /dev/null and b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-chromeless-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-lg-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-lg-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..a1ff3321
Binary files /dev/null and b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-lg-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-lg-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-lg-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..7fc4f251
Binary files /dev/null and b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-lg-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-lg-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-lg-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..63fe60c8
Binary files /dev/null and b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-lg-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-lg-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-lg-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..14beedfe
Binary files /dev/null and b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-lg-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-md-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-md-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..05399dea
Binary files /dev/null and b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-md-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-md-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-md-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..b26e319a
Binary files /dev/null and b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-md-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-md-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-md-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..d04ad55e
Binary files /dev/null and b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-md-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-md-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-md-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..de87e8ed
Binary files /dev/null and b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-md-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-sm-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-sm-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..f051c57c
Binary files /dev/null and b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-sm-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-sm-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-sm-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..cf64e11e
Binary files /dev/null and b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-sm-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-sm-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-sm-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..ccfa7e40
Binary files /dev/null and b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-sm-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-sm-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-sm-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..075e4934
Binary files /dev/null and b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-sm-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-split-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-split-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..faf633b1
Binary files /dev/null and b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-split-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-split-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-split-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..33d89e30
Binary files /dev/null and b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-split-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-split-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-split-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..89687c87
Binary files /dev/null and b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-split-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-split-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-split-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..83f08ac2
Binary files /dev/null and b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-split-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-split-mobile-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-split-mobile-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..4114ecff
Binary files /dev/null and b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-split-mobile-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-split-mobile-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-split-mobile-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..d63b45a5
Binary files /dev/null and b/packages/ui/src/lib/components/Dialog/__screenshots__/Dialog.vrt.test.ts/Dialog-split-mobile-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Divider/__screenshots__/Divider.vrt.test.ts/Divider-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Divider/__screenshots__/Divider.vrt.test.ts/Divider-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..20288e3b
Binary files /dev/null and b/packages/ui/src/lib/components/Divider/__screenshots__/Divider.vrt.test.ts/Divider-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Divider/__screenshots__/Divider.vrt.test.ts/Divider-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Divider/__screenshots__/Divider.vrt.test.ts/Divider-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..7e9a88f1
Binary files /dev/null and b/packages/ui/src/lib/components/Divider/__screenshots__/Divider.vrt.test.ts/Divider-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Divider/__screenshots__/Divider.vrt.test.ts/Divider-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Divider/__screenshots__/Divider.vrt.test.ts/Divider-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..f8f0701f
Binary files /dev/null and b/packages/ui/src/lib/components/Divider/__screenshots__/Divider.vrt.test.ts/Divider-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Divider/__screenshots__/Divider.vrt.test.ts/Divider-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Divider/__screenshots__/Divider.vrt.test.ts/Divider-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..015cbf6e
Binary files /dev/null and b/packages/ui/src/lib/components/Divider/__screenshots__/Divider.vrt.test.ts/Divider-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Docs/__screenshots__/Callout.vrt.test.ts/Callout-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Docs/__screenshots__/Callout.vrt.test.ts/Callout-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..d26a283d
Binary files /dev/null and b/packages/ui/src/lib/components/Docs/__screenshots__/Callout.vrt.test.ts/Callout-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Docs/__screenshots__/Callout.vrt.test.ts/Callout-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Docs/__screenshots__/Callout.vrt.test.ts/Callout-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..53d50e47
Binary files /dev/null and b/packages/ui/src/lib/components/Docs/__screenshots__/Callout.vrt.test.ts/Callout-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Docs/__screenshots__/Callout.vrt.test.ts/Callout-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Docs/__screenshots__/Callout.vrt.test.ts/Callout-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..78db199e
Binary files /dev/null and b/packages/ui/src/lib/components/Docs/__screenshots__/Callout.vrt.test.ts/Callout-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Docs/__screenshots__/Callout.vrt.test.ts/Callout-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Docs/__screenshots__/Callout.vrt.test.ts/Callout-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..e439e6ca
Binary files /dev/null and b/packages/ui/src/lib/components/Docs/__screenshots__/Callout.vrt.test.ts/Callout-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Docs/__screenshots__/CardGrid.vrt.test.ts/CardGrid-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Docs/__screenshots__/CardGrid.vrt.test.ts/CardGrid-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..747c1888
Binary files /dev/null and b/packages/ui/src/lib/components/Docs/__screenshots__/CardGrid.vrt.test.ts/CardGrid-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Docs/__screenshots__/CardGrid.vrt.test.ts/CardGrid-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Docs/__screenshots__/CardGrid.vrt.test.ts/CardGrid-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..ed424fc1
Binary files /dev/null and b/packages/ui/src/lib/components/Docs/__screenshots__/CardGrid.vrt.test.ts/CardGrid-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Docs/__screenshots__/CardGrid.vrt.test.ts/CardGrid-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Docs/__screenshots__/CardGrid.vrt.test.ts/CardGrid-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..490c398d
Binary files /dev/null and b/packages/ui/src/lib/components/Docs/__screenshots__/CardGrid.vrt.test.ts/CardGrid-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Docs/__screenshots__/CardGrid.vrt.test.ts/CardGrid-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Docs/__screenshots__/CardGrid.vrt.test.ts/CardGrid-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..98053562
Binary files /dev/null and b/packages/ui/src/lib/components/Docs/__screenshots__/CardGrid.vrt.test.ts/CardGrid-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Docs/__screenshots__/DocsNav.vrt.test.ts/DocsNav-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Docs/__screenshots__/DocsNav.vrt.test.ts/DocsNav-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..767815ee
Binary files /dev/null and b/packages/ui/src/lib/components/Docs/__screenshots__/DocsNav.vrt.test.ts/DocsNav-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Docs/__screenshots__/DocsNav.vrt.test.ts/DocsNav-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Docs/__screenshots__/DocsNav.vrt.test.ts/DocsNav-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..a03a552c
Binary files /dev/null and b/packages/ui/src/lib/components/Docs/__screenshots__/DocsNav.vrt.test.ts/DocsNav-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Docs/__screenshots__/DocsNav.vrt.test.ts/DocsNav-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Docs/__screenshots__/DocsNav.vrt.test.ts/DocsNav-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..4dd8b8c7
Binary files /dev/null and b/packages/ui/src/lib/components/Docs/__screenshots__/DocsNav.vrt.test.ts/DocsNav-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Docs/__screenshots__/DocsNav.vrt.test.ts/DocsNav-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Docs/__screenshots__/DocsNav.vrt.test.ts/DocsNav-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..2de42563
Binary files /dev/null and b/packages/ui/src/lib/components/Docs/__screenshots__/DocsNav.vrt.test.ts/DocsNav-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Docs/__screenshots__/DocsShell.vrt.test.ts/DocsShell-desktop-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Docs/__screenshots__/DocsShell.vrt.test.ts/DocsShell-desktop-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..1fa2d307
Binary files /dev/null and b/packages/ui/src/lib/components/Docs/__screenshots__/DocsShell.vrt.test.ts/DocsShell-desktop-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Docs/__screenshots__/DocsShell.vrt.test.ts/DocsShell-desktop-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Docs/__screenshots__/DocsShell.vrt.test.ts/DocsShell-desktop-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..feb425bc
Binary files /dev/null and b/packages/ui/src/lib/components/Docs/__screenshots__/DocsShell.vrt.test.ts/DocsShell-desktop-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Docs/__screenshots__/DocsShell.vrt.test.ts/DocsShell-desktop-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Docs/__screenshots__/DocsShell.vrt.test.ts/DocsShell-desktop-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..414c8420
Binary files /dev/null and b/packages/ui/src/lib/components/Docs/__screenshots__/DocsShell.vrt.test.ts/DocsShell-desktop-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Docs/__screenshots__/DocsShell.vrt.test.ts/DocsShell-desktop-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Docs/__screenshots__/DocsShell.vrt.test.ts/DocsShell-desktop-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..f30042e7
Binary files /dev/null and b/packages/ui/src/lib/components/Docs/__screenshots__/DocsShell.vrt.test.ts/DocsShell-desktop-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Docs/__screenshots__/DocsShell.vrt.test.ts/DocsShell-mobile-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Docs/__screenshots__/DocsShell.vrt.test.ts/DocsShell-mobile-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..01d16039
Binary files /dev/null and b/packages/ui/src/lib/components/Docs/__screenshots__/DocsShell.vrt.test.ts/DocsShell-mobile-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Docs/__screenshots__/DocsShell.vrt.test.ts/DocsShell-mobile-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Docs/__screenshots__/DocsShell.vrt.test.ts/DocsShell-mobile-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..a0d79c8c
Binary files /dev/null and b/packages/ui/src/lib/components/Docs/__screenshots__/DocsShell.vrt.test.ts/DocsShell-mobile-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Docs/__screenshots__/MethodBadge.vrt.test.ts/MethodBadge-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Docs/__screenshots__/MethodBadge.vrt.test.ts/MethodBadge-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..14d996e6
Binary files /dev/null and b/packages/ui/src/lib/components/Docs/__screenshots__/MethodBadge.vrt.test.ts/MethodBadge-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Docs/__screenshots__/MethodBadge.vrt.test.ts/MethodBadge-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Docs/__screenshots__/MethodBadge.vrt.test.ts/MethodBadge-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..3daaa69c
Binary files /dev/null and b/packages/ui/src/lib/components/Docs/__screenshots__/MethodBadge.vrt.test.ts/MethodBadge-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Docs/__screenshots__/MethodBadge.vrt.test.ts/MethodBadge-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Docs/__screenshots__/MethodBadge.vrt.test.ts/MethodBadge-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..f8204717
Binary files /dev/null and b/packages/ui/src/lib/components/Docs/__screenshots__/MethodBadge.vrt.test.ts/MethodBadge-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Docs/__screenshots__/MethodBadge.vrt.test.ts/MethodBadge-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Docs/__screenshots__/MethodBadge.vrt.test.ts/MethodBadge-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..cd16a08b
Binary files /dev/null and b/packages/ui/src/lib/components/Docs/__screenshots__/MethodBadge.vrt.test.ts/MethodBadge-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Docs/__screenshots__/Steps.vrt.test.ts/Steps-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Docs/__screenshots__/Steps.vrt.test.ts/Steps-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..83e34568
Binary files /dev/null and b/packages/ui/src/lib/components/Docs/__screenshots__/Steps.vrt.test.ts/Steps-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Docs/__screenshots__/Steps.vrt.test.ts/Steps-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Docs/__screenshots__/Steps.vrt.test.ts/Steps-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..f91a69bb
Binary files /dev/null and b/packages/ui/src/lib/components/Docs/__screenshots__/Steps.vrt.test.ts/Steps-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Docs/__screenshots__/Steps.vrt.test.ts/Steps-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Docs/__screenshots__/Steps.vrt.test.ts/Steps-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..a7d76c4d
Binary files /dev/null and b/packages/ui/src/lib/components/Docs/__screenshots__/Steps.vrt.test.ts/Steps-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Docs/__screenshots__/Steps.vrt.test.ts/Steps-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Docs/__screenshots__/Steps.vrt.test.ts/Steps-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..30a6e80d
Binary files /dev/null and b/packages/ui/src/lib/components/Docs/__screenshots__/Steps.vrt.test.ts/Steps-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Docs/__screenshots__/TableOfContents.vrt.test.ts/TableOfContents-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Docs/__screenshots__/TableOfContents.vrt.test.ts/TableOfContents-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..312aa333
Binary files /dev/null and b/packages/ui/src/lib/components/Docs/__screenshots__/TableOfContents.vrt.test.ts/TableOfContents-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Docs/__screenshots__/TableOfContents.vrt.test.ts/TableOfContents-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Docs/__screenshots__/TableOfContents.vrt.test.ts/TableOfContents-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..99116eca
Binary files /dev/null and b/packages/ui/src/lib/components/Docs/__screenshots__/TableOfContents.vrt.test.ts/TableOfContents-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Docs/__screenshots__/TableOfContents.vrt.test.ts/TableOfContents-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Docs/__screenshots__/TableOfContents.vrt.test.ts/TableOfContents-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..577c2a35
Binary files /dev/null and b/packages/ui/src/lib/components/Docs/__screenshots__/TableOfContents.vrt.test.ts/TableOfContents-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Docs/__screenshots__/TableOfContents.vrt.test.ts/TableOfContents-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Docs/__screenshots__/TableOfContents.vrt.test.ts/TableOfContents-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..72d11224
Binary files /dev/null and b/packages/ui/src/lib/components/Docs/__screenshots__/TableOfContents.vrt.test.ts/TableOfContents-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-bottom-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-bottom-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..e344b1fb
Binary files /dev/null and b/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-bottom-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-bottom-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-bottom-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..bf785e5c
Binary files /dev/null and b/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-bottom-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-bottom-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-bottom-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..b70b228e
Binary files /dev/null and b/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-bottom-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-bottom-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-bottom-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..d5cdd7ae
Binary files /dev/null and b/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-bottom-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-left-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-left-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..80901c4c
Binary files /dev/null and b/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-left-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-left-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-left-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..7ef016a0
Binary files /dev/null and b/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-left-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-left-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-left-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..6091600f
Binary files /dev/null and b/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-left-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-left-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-left-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..e77367f9
Binary files /dev/null and b/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-left-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-right-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-right-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..b325c0fd
Binary files /dev/null and b/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-right-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-right-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-right-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..cbc7d305
Binary files /dev/null and b/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-right-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-right-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-right-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..fcc4c2bd
Binary files /dev/null and b/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-right-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-right-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-right-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..0c584dca
Binary files /dev/null and b/packages/ui/src/lib/components/Drawer/__screenshots__/Drawer.vrt.test.ts/Drawer-right-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/DropdownMenu/__screenshots__/DropdownMenu.vrt.test.ts/DropdownMenu-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/DropdownMenu/__screenshots__/DropdownMenu.vrt.test.ts/DropdownMenu-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..0839c4c1
Binary files /dev/null and b/packages/ui/src/lib/components/DropdownMenu/__screenshots__/DropdownMenu.vrt.test.ts/DropdownMenu-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/DropdownMenu/__screenshots__/DropdownMenu.vrt.test.ts/DropdownMenu-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/DropdownMenu/__screenshots__/DropdownMenu.vrt.test.ts/DropdownMenu-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..b2fb2feb
Binary files /dev/null and b/packages/ui/src/lib/components/DropdownMenu/__screenshots__/DropdownMenu.vrt.test.ts/DropdownMenu-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/DropdownMenu/__screenshots__/DropdownMenu.vrt.test.ts/DropdownMenu-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/DropdownMenu/__screenshots__/DropdownMenu.vrt.test.ts/DropdownMenu-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..9c6b8b90
Binary files /dev/null and b/packages/ui/src/lib/components/DropdownMenu/__screenshots__/DropdownMenu.vrt.test.ts/DropdownMenu-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/DropdownMenu/__screenshots__/DropdownMenu.vrt.test.ts/DropdownMenu-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/DropdownMenu/__screenshots__/DropdownMenu.vrt.test.ts/DropdownMenu-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..69b1f235
Binary files /dev/null and b/packages/ui/src/lib/components/DropdownMenu/__screenshots__/DropdownMenu.vrt.test.ts/DropdownMenu-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/EmptyState/__screenshots__/EmptyState.vrt.test.ts/EmptyState-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/EmptyState/__screenshots__/EmptyState.vrt.test.ts/EmptyState-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..52e4d1ea
Binary files /dev/null and b/packages/ui/src/lib/components/EmptyState/__screenshots__/EmptyState.vrt.test.ts/EmptyState-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/EmptyState/__screenshots__/EmptyState.vrt.test.ts/EmptyState-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/EmptyState/__screenshots__/EmptyState.vrt.test.ts/EmptyState-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..e670d47a
Binary files /dev/null and b/packages/ui/src/lib/components/EmptyState/__screenshots__/EmptyState.vrt.test.ts/EmptyState-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/EmptyState/__screenshots__/EmptyState.vrt.test.ts/EmptyState-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/EmptyState/__screenshots__/EmptyState.vrt.test.ts/EmptyState-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..f9f9644b
Binary files /dev/null and b/packages/ui/src/lib/components/EmptyState/__screenshots__/EmptyState.vrt.test.ts/EmptyState-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/EmptyState/__screenshots__/EmptyState.vrt.test.ts/EmptyState-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/EmptyState/__screenshots__/EmptyState.vrt.test.ts/EmptyState-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..5593a6ab
Binary files /dev/null and b/packages/ui/src/lib/components/EmptyState/__screenshots__/EmptyState.vrt.test.ts/EmptyState-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/ErrorPage/__screenshots__/ErrorPage.vrt.test.ts/ErrorPage-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/ErrorPage/__screenshots__/ErrorPage.vrt.test.ts/ErrorPage-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..2b68c860
Binary files /dev/null and b/packages/ui/src/lib/components/ErrorPage/__screenshots__/ErrorPage.vrt.test.ts/ErrorPage-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/ErrorPage/__screenshots__/ErrorPage.vrt.test.ts/ErrorPage-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/ErrorPage/__screenshots__/ErrorPage.vrt.test.ts/ErrorPage-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..893da18d
Binary files /dev/null and b/packages/ui/src/lib/components/ErrorPage/__screenshots__/ErrorPage.vrt.test.ts/ErrorPage-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/ErrorPage/__screenshots__/ErrorPage.vrt.test.ts/ErrorPage-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/ErrorPage/__screenshots__/ErrorPage.vrt.test.ts/ErrorPage-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..6993eac6
Binary files /dev/null and b/packages/ui/src/lib/components/ErrorPage/__screenshots__/ErrorPage.vrt.test.ts/ErrorPage-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/ErrorPage/__screenshots__/ErrorPage.vrt.test.ts/ErrorPage-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/ErrorPage/__screenshots__/ErrorPage.vrt.test.ts/ErrorPage-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..3b818a2d
Binary files /dev/null and b/packages/ui/src/lib/components/ErrorPage/__screenshots__/ErrorPage.vrt.test.ts/ErrorPage-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/ErrorPage/__screenshots__/ErrorPage.vrt.test.ts/ErrorPage-mobile-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/ErrorPage/__screenshots__/ErrorPage.vrt.test.ts/ErrorPage-mobile-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..2554e037
Binary files /dev/null and b/packages/ui/src/lib/components/ErrorPage/__screenshots__/ErrorPage.vrt.test.ts/ErrorPage-mobile-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/ErrorPage/__screenshots__/ErrorPage.vrt.test.ts/ErrorPage-mobile-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/ErrorPage/__screenshots__/ErrorPage.vrt.test.ts/ErrorPage-mobile-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..8fec0be0
Binary files /dev/null and b/packages/ui/src/lib/components/ErrorPage/__screenshots__/ErrorPage.vrt.test.ts/ErrorPage-mobile-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/FileUpload/__screenshots__/FileUpload.vrt.test.ts/FileUpload-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/FileUpload/__screenshots__/FileUpload.vrt.test.ts/FileUpload-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..e1c5836b
Binary files /dev/null and b/packages/ui/src/lib/components/FileUpload/__screenshots__/FileUpload.vrt.test.ts/FileUpload-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/FileUpload/__screenshots__/FileUpload.vrt.test.ts/FileUpload-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/FileUpload/__screenshots__/FileUpload.vrt.test.ts/FileUpload-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..c6283437
Binary files /dev/null and b/packages/ui/src/lib/components/FileUpload/__screenshots__/FileUpload.vrt.test.ts/FileUpload-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/FileUpload/__screenshots__/FileUpload.vrt.test.ts/FileUpload-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/FileUpload/__screenshots__/FileUpload.vrt.test.ts/FileUpload-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..d37614b1
Binary files /dev/null and b/packages/ui/src/lib/components/FileUpload/__screenshots__/FileUpload.vrt.test.ts/FileUpload-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/FileUpload/__screenshots__/FileUpload.vrt.test.ts/FileUpload-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/FileUpload/__screenshots__/FileUpload.vrt.test.ts/FileUpload-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..8bbf20a6
Binary files /dev/null and b/packages/ui/src/lib/components/FileUpload/__screenshots__/FileUpload.vrt.test.ts/FileUpload-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/FilterGroup/__screenshots__/FilterGroup.vrt.test.ts/FilterGroup-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/FilterGroup/__screenshots__/FilterGroup.vrt.test.ts/FilterGroup-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..66f109d3
Binary files /dev/null and b/packages/ui/src/lib/components/FilterGroup/__screenshots__/FilterGroup.vrt.test.ts/FilterGroup-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/FilterGroup/__screenshots__/FilterGroup.vrt.test.ts/FilterGroup-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/FilterGroup/__screenshots__/FilterGroup.vrt.test.ts/FilterGroup-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..6f825eda
Binary files /dev/null and b/packages/ui/src/lib/components/FilterGroup/__screenshots__/FilterGroup.vrt.test.ts/FilterGroup-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/FilterGroup/__screenshots__/FilterGroup.vrt.test.ts/FilterGroup-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/FilterGroup/__screenshots__/FilterGroup.vrt.test.ts/FilterGroup-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..0fa26516
Binary files /dev/null and b/packages/ui/src/lib/components/FilterGroup/__screenshots__/FilterGroup.vrt.test.ts/FilterGroup-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/FilterGroup/__screenshots__/FilterGroup.vrt.test.ts/FilterGroup-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/FilterGroup/__screenshots__/FilterGroup.vrt.test.ts/FilterGroup-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..9cc2064f
Binary files /dev/null and b/packages/ui/src/lib/components/FilterGroup/__screenshots__/FilterGroup.vrt.test.ts/FilterGroup-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/GlassAppBar/__screenshots__/GlassAppBar.vrt.test.ts/GlassAppBar-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/GlassAppBar/__screenshots__/GlassAppBar.vrt.test.ts/GlassAppBar-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..ae40c34e
Binary files /dev/null and b/packages/ui/src/lib/components/GlassAppBar/__screenshots__/GlassAppBar.vrt.test.ts/GlassAppBar-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/GlassAppBar/__screenshots__/GlassAppBar.vrt.test.ts/GlassAppBar-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/GlassAppBar/__screenshots__/GlassAppBar.vrt.test.ts/GlassAppBar-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..3be09823
Binary files /dev/null and b/packages/ui/src/lib/components/GlassAppBar/__screenshots__/GlassAppBar.vrt.test.ts/GlassAppBar-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/GlassAppBar/__screenshots__/GlassAppBar.vrt.test.ts/GlassAppBar-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/GlassAppBar/__screenshots__/GlassAppBar.vrt.test.ts/GlassAppBar-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..8e7c1d9a
Binary files /dev/null and b/packages/ui/src/lib/components/GlassAppBar/__screenshots__/GlassAppBar.vrt.test.ts/GlassAppBar-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/GlassAppBar/__screenshots__/GlassAppBar.vrt.test.ts/GlassAppBar-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/GlassAppBar/__screenshots__/GlassAppBar.vrt.test.ts/GlassAppBar-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..d9ad2446
Binary files /dev/null and b/packages/ui/src/lib/components/GlassAppBar/__screenshots__/GlassAppBar.vrt.test.ts/GlassAppBar-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/GlassCard/__screenshots__/GlassCard.vrt.test.ts/GlassCard-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/GlassCard/__screenshots__/GlassCard.vrt.test.ts/GlassCard-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..8caaa2ab
Binary files /dev/null and b/packages/ui/src/lib/components/GlassCard/__screenshots__/GlassCard.vrt.test.ts/GlassCard-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/GlassCard/__screenshots__/GlassCard.vrt.test.ts/GlassCard-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/GlassCard/__screenshots__/GlassCard.vrt.test.ts/GlassCard-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..122aaac6
Binary files /dev/null and b/packages/ui/src/lib/components/GlassCard/__screenshots__/GlassCard.vrt.test.ts/GlassCard-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/GlassCard/__screenshots__/GlassCard.vrt.test.ts/GlassCard-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/GlassCard/__screenshots__/GlassCard.vrt.test.ts/GlassCard-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..0c7c4c26
Binary files /dev/null and b/packages/ui/src/lib/components/GlassCard/__screenshots__/GlassCard.vrt.test.ts/GlassCard-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/GlassCard/__screenshots__/GlassCard.vrt.test.ts/GlassCard-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/GlassCard/__screenshots__/GlassCard.vrt.test.ts/GlassCard-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..a5b4dd9d
Binary files /dev/null and b/packages/ui/src/lib/components/GlassCard/__screenshots__/GlassCard.vrt.test.ts/GlassCard-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/GlassSurface/__screenshots__/GlassSurface.vrt.test.ts/GlassSurface-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/GlassSurface/__screenshots__/GlassSurface.vrt.test.ts/GlassSurface-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..272ce2e0
Binary files /dev/null and b/packages/ui/src/lib/components/GlassSurface/__screenshots__/GlassSurface.vrt.test.ts/GlassSurface-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/GlassSurface/__screenshots__/GlassSurface.vrt.test.ts/GlassSurface-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/GlassSurface/__screenshots__/GlassSurface.vrt.test.ts/GlassSurface-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..82320b2a
Binary files /dev/null and b/packages/ui/src/lib/components/GlassSurface/__screenshots__/GlassSurface.vrt.test.ts/GlassSurface-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/GlassSurface/__screenshots__/GlassSurface.vrt.test.ts/GlassSurface-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/GlassSurface/__screenshots__/GlassSurface.vrt.test.ts/GlassSurface-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..bfc29a3b
Binary files /dev/null and b/packages/ui/src/lib/components/GlassSurface/__screenshots__/GlassSurface.vrt.test.ts/GlassSurface-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/GlassSurface/__screenshots__/GlassSurface.vrt.test.ts/GlassSurface-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/GlassSurface/__screenshots__/GlassSurface.vrt.test.ts/GlassSurface-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..890789ed
Binary files /dev/null and b/packages/ui/src/lib/components/GlassSurface/__screenshots__/GlassSurface.vrt.test.ts/GlassSurface-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Grid/__screenshots__/Grid.vrt.test.ts/Grid-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Grid/__screenshots__/Grid.vrt.test.ts/Grid-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..a2c40d8a
Binary files /dev/null and b/packages/ui/src/lib/components/Grid/__screenshots__/Grid.vrt.test.ts/Grid-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Grid/__screenshots__/Grid.vrt.test.ts/Grid-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Grid/__screenshots__/Grid.vrt.test.ts/Grid-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..d88f7700
Binary files /dev/null and b/packages/ui/src/lib/components/Grid/__screenshots__/Grid.vrt.test.ts/Grid-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Grid/__screenshots__/Grid.vrt.test.ts/Grid-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Grid/__screenshots__/Grid.vrt.test.ts/Grid-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..51a2a503
Binary files /dev/null and b/packages/ui/src/lib/components/Grid/__screenshots__/Grid.vrt.test.ts/Grid-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Grid/__screenshots__/Grid.vrt.test.ts/Grid-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Grid/__screenshots__/Grid.vrt.test.ts/Grid-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..706bc306
Binary files /dev/null and b/packages/ui/src/lib/components/Grid/__screenshots__/Grid.vrt.test.ts/Grid-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/InstallPrompt/__screenshots__/InstallPrompt.vrt.test.ts/InstallPrompt-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/InstallPrompt/__screenshots__/InstallPrompt.vrt.test.ts/InstallPrompt-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..cab23841
Binary files /dev/null and b/packages/ui/src/lib/components/InstallPrompt/__screenshots__/InstallPrompt.vrt.test.ts/InstallPrompt-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/InstallPrompt/__screenshots__/InstallPrompt.vrt.test.ts/InstallPrompt-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/InstallPrompt/__screenshots__/InstallPrompt.vrt.test.ts/InstallPrompt-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..5ca68c71
Binary files /dev/null and b/packages/ui/src/lib/components/InstallPrompt/__screenshots__/InstallPrompt.vrt.test.ts/InstallPrompt-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/InstallPrompt/__screenshots__/InstallPrompt.vrt.test.ts/InstallPrompt-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/InstallPrompt/__screenshots__/InstallPrompt.vrt.test.ts/InstallPrompt-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..9f5df7fc
Binary files /dev/null and b/packages/ui/src/lib/components/InstallPrompt/__screenshots__/InstallPrompt.vrt.test.ts/InstallPrompt-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/InstallPrompt/__screenshots__/InstallPrompt.vrt.test.ts/InstallPrompt-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/InstallPrompt/__screenshots__/InstallPrompt.vrt.test.ts/InstallPrompt-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..096550b2
Binary files /dev/null and b/packages/ui/src/lib/components/InstallPrompt/__screenshots__/InstallPrompt.vrt.test.ts/InstallPrompt-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Kbd/__screenshots__/Kbd.vrt.test.ts/Kbd-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Kbd/__screenshots__/Kbd.vrt.test.ts/Kbd-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..73949cc4
Binary files /dev/null and b/packages/ui/src/lib/components/Kbd/__screenshots__/Kbd.vrt.test.ts/Kbd-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Kbd/__screenshots__/Kbd.vrt.test.ts/Kbd-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Kbd/__screenshots__/Kbd.vrt.test.ts/Kbd-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..a44ccf30
Binary files /dev/null and b/packages/ui/src/lib/components/Kbd/__screenshots__/Kbd.vrt.test.ts/Kbd-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Kbd/__screenshots__/Kbd.vrt.test.ts/Kbd-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Kbd/__screenshots__/Kbd.vrt.test.ts/Kbd-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..89762849
Binary files /dev/null and b/packages/ui/src/lib/components/Kbd/__screenshots__/Kbd.vrt.test.ts/Kbd-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Kbd/__screenshots__/Kbd.vrt.test.ts/Kbd-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Kbd/__screenshots__/Kbd.vrt.test.ts/Kbd-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..c04706a4
Binary files /dev/null and b/packages/ui/src/lib/components/Kbd/__screenshots__/Kbd.vrt.test.ts/Kbd-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/KpiCard/__screenshots__/KpiCard.vrt.test.ts/KpiCard-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/KpiCard/__screenshots__/KpiCard.vrt.test.ts/KpiCard-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..d79aba14
Binary files /dev/null and b/packages/ui/src/lib/components/KpiCard/__screenshots__/KpiCard.vrt.test.ts/KpiCard-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/KpiCard/__screenshots__/KpiCard.vrt.test.ts/KpiCard-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/KpiCard/__screenshots__/KpiCard.vrt.test.ts/KpiCard-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..6cb52afe
Binary files /dev/null and b/packages/ui/src/lib/components/KpiCard/__screenshots__/KpiCard.vrt.test.ts/KpiCard-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/KpiCard/__screenshots__/KpiCard.vrt.test.ts/KpiCard-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/KpiCard/__screenshots__/KpiCard.vrt.test.ts/KpiCard-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..c52145a4
Binary files /dev/null and b/packages/ui/src/lib/components/KpiCard/__screenshots__/KpiCard.vrt.test.ts/KpiCard-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/KpiCard/__screenshots__/KpiCard.vrt.test.ts/KpiCard-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/KpiCard/__screenshots__/KpiCard.vrt.test.ts/KpiCard-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..af432f8d
Binary files /dev/null and b/packages/ui/src/lib/components/KpiCard/__screenshots__/KpiCard.vrt.test.ts/KpiCard-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/KpiGrid/__screenshots__/KpiGrid.vrt.test.ts/KpiGrid-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/KpiGrid/__screenshots__/KpiGrid.vrt.test.ts/KpiGrid-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..b645f2b4
Binary files /dev/null and b/packages/ui/src/lib/components/KpiGrid/__screenshots__/KpiGrid.vrt.test.ts/KpiGrid-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/KpiGrid/__screenshots__/KpiGrid.vrt.test.ts/KpiGrid-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/KpiGrid/__screenshots__/KpiGrid.vrt.test.ts/KpiGrid-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..84d34e07
Binary files /dev/null and b/packages/ui/src/lib/components/KpiGrid/__screenshots__/KpiGrid.vrt.test.ts/KpiGrid-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/KpiGrid/__screenshots__/KpiGrid.vrt.test.ts/KpiGrid-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/KpiGrid/__screenshots__/KpiGrid.vrt.test.ts/KpiGrid-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..6b481aff
Binary files /dev/null and b/packages/ui/src/lib/components/KpiGrid/__screenshots__/KpiGrid.vrt.test.ts/KpiGrid-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/KpiGrid/__screenshots__/KpiGrid.vrt.test.ts/KpiGrid-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/KpiGrid/__screenshots__/KpiGrid.vrt.test.ts/KpiGrid-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..d0128016
Binary files /dev/null and b/packages/ui/src/lib/components/KpiGrid/__screenshots__/KpiGrid.vrt.test.ts/KpiGrid-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/LiquidGlass/__screenshots__/LiquidGlass.vrt.test.ts/LiquidGlass-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/LiquidGlass/__screenshots__/LiquidGlass.vrt.test.ts/LiquidGlass-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..61af9787
Binary files /dev/null and b/packages/ui/src/lib/components/LiquidGlass/__screenshots__/LiquidGlass.vrt.test.ts/LiquidGlass-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/LiquidGlass/__screenshots__/LiquidGlass.vrt.test.ts/LiquidGlass-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/LiquidGlass/__screenshots__/LiquidGlass.vrt.test.ts/LiquidGlass-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..5573e714
Binary files /dev/null and b/packages/ui/src/lib/components/LiquidGlass/__screenshots__/LiquidGlass.vrt.test.ts/LiquidGlass-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/LiquidGlass/__screenshots__/LiquidGlass.vrt.test.ts/LiquidGlass-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/LiquidGlass/__screenshots__/LiquidGlass.vrt.test.ts/LiquidGlass-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..a0d839e7
Binary files /dev/null and b/packages/ui/src/lib/components/LiquidGlass/__screenshots__/LiquidGlass.vrt.test.ts/LiquidGlass-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/LiquidGlass/__screenshots__/LiquidGlass.vrt.test.ts/LiquidGlass-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/LiquidGlass/__screenshots__/LiquidGlass.vrt.test.ts/LiquidGlass-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..617e192e
Binary files /dev/null and b/packages/ui/src/lib/components/LiquidGlass/__screenshots__/LiquidGlass.vrt.test.ts/LiquidGlass-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/LoadingBoundary/LoadingBoundary.svelte.test.ts b/packages/ui/src/lib/components/LoadingBoundary/LoadingBoundary.svelte.test.ts
index 70351fc8..1c43881f 100644
--- a/packages/ui/src/lib/components/LoadingBoundary/LoadingBoundary.svelte.test.ts
+++ b/packages/ui/src/lib/components/LoadingBoundary/LoadingBoundary.svelte.test.ts
@@ -41,11 +41,26 @@ describe("LoadingBoundary", () => {
 			props: { delay: 20, minDuration: 400, stamps, stall: 120 },
 		});
 		await expect.poll(() => stamps.mounted).toBeDefined();
+		// Observe the region instead of polling it: on a stalled machine a poll can first run after the
+		// 400 ms hold is over and read "ready", which says nothing about the hold. The observer callback
+		// runs at the DOM change itself, so what it records is what the user would have seen.
+		const states: string[] = [];
+		let atHolding: { placeholderGone: boolean; contentVisible: boolean } | undefined;
+		const observer = new MutationObserver(() => {
+			const state = busy(screen.container)?.getAttribute("data-state") ?? "ready";
+			if (states.at(-1) !== state) states.push(state);
+			if (state === "holding" && !atHolding)
+				atHolding = {
+					placeholderGone: stamps.unmounted !== undefined,
+					contentVisible: content(screen.container)?.checkVisibility() ?? false,
+				};
+		});
+		observer.observe(screen.container, { subtree: true, childList: true, attributes: true });
 		release(screen);
 		// Resolved already, but the boundary holds: no flash of content.
-		await expect.poll(() => busy(screen.container)?.getAttribute("data-state")).toBe("holding");
-		expect(stamps.unmounted).toBeUndefined();
-		expect(content(screen.container)?.checkVisibility()).toBe(false);
+		await expect.poll(() => states.includes("holding")).toBe(true);
+		observer.disconnect();
+		expect(atHolding).toEqual({ placeholderGone: false, contentVisible: false });
 		await expect.poll(() => stamps.unmounted, { timeout: 4000 }).toBeDefined();
 		expect((stamps.unmounted ?? 0) - (stamps.mounted ?? 0)).toBeGreaterThanOrEqual(400);
 		await expect.poll(() => content(screen.container)?.checkVisibility()).toBe(true);
diff --git a/packages/ui/src/lib/components/NotificationBell/__screenshots__/NotificationBell.vrt.test.ts/NotificationBell-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/NotificationBell/__screenshots__/NotificationBell.vrt.test.ts/NotificationBell-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..e57b749d
Binary files /dev/null and b/packages/ui/src/lib/components/NotificationBell/__screenshots__/NotificationBell.vrt.test.ts/NotificationBell-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/NotificationBell/__screenshots__/NotificationBell.vrt.test.ts/NotificationBell-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/NotificationBell/__screenshots__/NotificationBell.vrt.test.ts/NotificationBell-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..0b033a26
Binary files /dev/null and b/packages/ui/src/lib/components/NotificationBell/__screenshots__/NotificationBell.vrt.test.ts/NotificationBell-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/NotificationBell/__screenshots__/NotificationBell.vrt.test.ts/NotificationBell-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/NotificationBell/__screenshots__/NotificationBell.vrt.test.ts/NotificationBell-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..b4ecca42
Binary files /dev/null and b/packages/ui/src/lib/components/NotificationBell/__screenshots__/NotificationBell.vrt.test.ts/NotificationBell-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/NotificationBell/__screenshots__/NotificationBell.vrt.test.ts/NotificationBell-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/NotificationBell/__screenshots__/NotificationBell.vrt.test.ts/NotificationBell-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..6d5ed6d5
Binary files /dev/null and b/packages/ui/src/lib/components/NotificationBell/__screenshots__/NotificationBell.vrt.test.ts/NotificationBell-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/NumberField/__screenshots__/NumberField.vrt.test.ts/NumberField-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/NumberField/__screenshots__/NumberField.vrt.test.ts/NumberField-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..36fa8643
Binary files /dev/null and b/packages/ui/src/lib/components/NumberField/__screenshots__/NumberField.vrt.test.ts/NumberField-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/NumberField/__screenshots__/NumberField.vrt.test.ts/NumberField-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/NumberField/__screenshots__/NumberField.vrt.test.ts/NumberField-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..98625457
Binary files /dev/null and b/packages/ui/src/lib/components/NumberField/__screenshots__/NumberField.vrt.test.ts/NumberField-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/NumberField/__screenshots__/NumberField.vrt.test.ts/NumberField-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/NumberField/__screenshots__/NumberField.vrt.test.ts/NumberField-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..1aa42dea
Binary files /dev/null and b/packages/ui/src/lib/components/NumberField/__screenshots__/NumberField.vrt.test.ts/NumberField-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/NumberField/__screenshots__/NumberField.vrt.test.ts/NumberField-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/NumberField/__screenshots__/NumberField.vrt.test.ts/NumberField-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..2759884b
Binary files /dev/null and b/packages/ui/src/lib/components/NumberField/__screenshots__/NumberField.vrt.test.ts/NumberField-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/PageShell/__screenshots__/PageShell.vrt.test.ts/PageShell-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/PageShell/__screenshots__/PageShell.vrt.test.ts/PageShell-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..b890b586
Binary files /dev/null and b/packages/ui/src/lib/components/PageShell/__screenshots__/PageShell.vrt.test.ts/PageShell-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/PageShell/__screenshots__/PageShell.vrt.test.ts/PageShell-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/PageShell/__screenshots__/PageShell.vrt.test.ts/PageShell-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..8b351504
Binary files /dev/null and b/packages/ui/src/lib/components/PageShell/__screenshots__/PageShell.vrt.test.ts/PageShell-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/PageShell/__screenshots__/PageShell.vrt.test.ts/PageShell-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/PageShell/__screenshots__/PageShell.vrt.test.ts/PageShell-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..800aa8c4
Binary files /dev/null and b/packages/ui/src/lib/components/PageShell/__screenshots__/PageShell.vrt.test.ts/PageShell-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/PageShell/__screenshots__/PageShell.vrt.test.ts/PageShell-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/PageShell/__screenshots__/PageShell.vrt.test.ts/PageShell-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..cdc84536
Binary files /dev/null and b/packages/ui/src/lib/components/PageShell/__screenshots__/PageShell.vrt.test.ts/PageShell-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/PageShell/__screenshots__/PageShell.vrt.test.ts/PageShell-mobile-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/PageShell/__screenshots__/PageShell.vrt.test.ts/PageShell-mobile-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..a4af1a55
Binary files /dev/null and b/packages/ui/src/lib/components/PageShell/__screenshots__/PageShell.vrt.test.ts/PageShell-mobile-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/PageShell/__screenshots__/PageShell.vrt.test.ts/PageShell-mobile-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/PageShell/__screenshots__/PageShell.vrt.test.ts/PageShell-mobile-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..f07c1356
Binary files /dev/null and b/packages/ui/src/lib/components/PageShell/__screenshots__/PageShell.vrt.test.ts/PageShell-mobile-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Pagination/__screenshots__/Pagination.vrt.test.ts/Pagination-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Pagination/__screenshots__/Pagination.vrt.test.ts/Pagination-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..cb8d39b8
Binary files /dev/null and b/packages/ui/src/lib/components/Pagination/__screenshots__/Pagination.vrt.test.ts/Pagination-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Pagination/__screenshots__/Pagination.vrt.test.ts/Pagination-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Pagination/__screenshots__/Pagination.vrt.test.ts/Pagination-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..0df8b67d
Binary files /dev/null and b/packages/ui/src/lib/components/Pagination/__screenshots__/Pagination.vrt.test.ts/Pagination-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Pagination/__screenshots__/Pagination.vrt.test.ts/Pagination-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Pagination/__screenshots__/Pagination.vrt.test.ts/Pagination-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..3d47d14b
Binary files /dev/null and b/packages/ui/src/lib/components/Pagination/__screenshots__/Pagination.vrt.test.ts/Pagination-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Pagination/__screenshots__/Pagination.vrt.test.ts/Pagination-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Pagination/__screenshots__/Pagination.vrt.test.ts/Pagination-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..cc11c05d
Binary files /dev/null and b/packages/ui/src/lib/components/Pagination/__screenshots__/Pagination.vrt.test.ts/Pagination-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/PinInput/__screenshots__/PinInput.vrt.test.ts/PinInput-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/PinInput/__screenshots__/PinInput.vrt.test.ts/PinInput-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..17b74270
Binary files /dev/null and b/packages/ui/src/lib/components/PinInput/__screenshots__/PinInput.vrt.test.ts/PinInput-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/PinInput/__screenshots__/PinInput.vrt.test.ts/PinInput-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/PinInput/__screenshots__/PinInput.vrt.test.ts/PinInput-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..96d83f6a
Binary files /dev/null and b/packages/ui/src/lib/components/PinInput/__screenshots__/PinInput.vrt.test.ts/PinInput-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/PinInput/__screenshots__/PinInput.vrt.test.ts/PinInput-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/PinInput/__screenshots__/PinInput.vrt.test.ts/PinInput-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..617d73a9
Binary files /dev/null and b/packages/ui/src/lib/components/PinInput/__screenshots__/PinInput.vrt.test.ts/PinInput-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/PinInput/__screenshots__/PinInput.vrt.test.ts/PinInput-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/PinInput/__screenshots__/PinInput.vrt.test.ts/PinInput-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..1b15ce1c
Binary files /dev/null and b/packages/ui/src/lib/components/PinInput/__screenshots__/PinInput.vrt.test.ts/PinInput-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Popover/__screenshots__/Popover.vrt.test.ts/Popover-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Popover/__screenshots__/Popover.vrt.test.ts/Popover-dark-glass-chromium-darwin.png
new file mode 100644
index 00000000..aaf83d8d
Binary files /dev/null and b/packages/ui/src/lib/components/Popover/__screenshots__/Popover.vrt.test.ts/Popover-dark-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Popover/__screenshots__/Popover.vrt.test.ts/Popover-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Popover/__screenshots__/Popover.vrt.test.ts/Popover-dark-solid-chromium-darwin.png
new file mode 100644
index 00000000..bc761e62
Binary files /dev/null and b/packages/ui/src/lib/components/Popover/__screenshots__/Popover.vrt.test.ts/Popover-dark-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Popover/__screenshots__/Popover.vrt.test.ts/Popover-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Popover/__screenshots__/Popover.vrt.test.ts/Popover-light-glass-chromium-darwin.png
new file mode 100644
index 00000000..04d9ab6a
Binary files /dev/null and b/packages/ui/src/lib/components/Popover/__screenshots__/Popover.vrt.test.ts/Popover-light-glass-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/Popover/__screenshots__/Popover.vrt.test.ts/Popover-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Popover/__screenshots__/Popover.vrt.test.ts/Popover-light-solid-chromium-darwin.png
new file mode 100644
index 00000000..723eeafe
Binary files /dev/null and b/packages/ui/src/lib/components/Popover/__screenshots__/Popover.vrt.test.ts/Popover-light-solid-chromium-darwin.png differ
diff --git a/packages/ui/src/lib/components/PreviewStage/PreviewStage.svelte b/packages/ui/src/lib/components/PreviewStage/PreviewStage.svelte
index ae9dc22f..c6adfd13 100644
--- a/packages/ui/src/lib/components/PreviewStage/PreviewStage.svelte
+++ b/packages/ui/src/lib/components/PreviewStage/PreviewStage.svelte
@@ -4,6 +4,7 @@
   import { useMessages } from '../../i18n/state.svelte.js';
   import { previewStage } from '../../i18n/groups/previewStage.js';
   import { colorScheme } from '../../stores/color-scheme.svelte.js';
+  import { scrollFocusable } from '../../actions/scroll-focusable.js';
   import { reducedMotion } from '../../stores/reduced-motion.svelte.js';
   import SegmentedControl from '../ToggleGroup/SegmentedControl.svelte';
   import ThemeIsland from '../ThemeIsland/ThemeIsland.svelte';
@@ -153,7 +154,7 @@
 
   
-
+
{#if children} {#if scheme === 'inherit' && !preset}
{@render children()}
diff --git a/packages/ui/src/lib/components/PreviewStage/PreviewStage.svelte.test.ts b/packages/ui/src/lib/components/PreviewStage/PreviewStage.svelte.test.ts index fe912b00..4c1a40a7 100644 --- a/packages/ui/src/lib/components/PreviewStage/PreviewStage.svelte.test.ts +++ b/packages/ui/src/lib/components/PreviewStage/PreviewStage.svelte.test.ts @@ -48,6 +48,16 @@ describe("PreviewStage width", () => { }); }); +describe("PreviewStage scrolling", () => { + it("a demo taller than the stage scrolls by keyboard, and axe passes", async () => { + const screen = await render(Harness, { tall: true }); + const f = frame(screen.container); + expect(f.scrollHeight).toBeGreaterThan(f.clientHeight); + await expect.poll(() => f.tabIndex).toBe(0); + expect(await checkA11y(screen.container)).toHaveNoViolations(); + }); +}); + describe("PreviewStage theming", () => { it("a scheme scopes the content to its own island", async () => { const screen = await render(Harness, { scheme: "dark", preset: "ocean" }); diff --git a/packages/ui/src/lib/components/PreviewStage/PreviewStageHarness.test.svelte b/packages/ui/src/lib/components/PreviewStage/PreviewStageHarness.test.svelte index 87c89370..efb4bf88 100644 --- a/packages/ui/src/lib/components/PreviewStage/PreviewStageHarness.test.svelte +++ b/packages/ui/src/lib/components/PreviewStage/PreviewStageHarness.test.svelte @@ -6,11 +6,13 @@ scheme = "inherit", preset, src, + tall = false, }: { width?: number | "fill"; scheme?: "light" | "dark" | "inherit"; preset?: string; src?: string; + tall?: boolean; } = $props(); @@ -20,6 +22,7 @@ {:else} + {#if tall}
Taller than the stage
{/if}
{/if} {width} @@ -27,4 +30,5 @@ diff --git a/packages/ui/src/lib/components/Progress/__screenshots__/Progress.vrt.test.ts/Progress-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Progress/__screenshots__/Progress.vrt.test.ts/Progress-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..31fe3906 Binary files /dev/null and b/packages/ui/src/lib/components/Progress/__screenshots__/Progress.vrt.test.ts/Progress-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Progress/__screenshots__/Progress.vrt.test.ts/Progress-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Progress/__screenshots__/Progress.vrt.test.ts/Progress-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..c616273f Binary files /dev/null and b/packages/ui/src/lib/components/Progress/__screenshots__/Progress.vrt.test.ts/Progress-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Progress/__screenshots__/Progress.vrt.test.ts/Progress-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Progress/__screenshots__/Progress.vrt.test.ts/Progress-light-glass-chromium-darwin.png new file mode 100644 index 00000000..1429b448 Binary files /dev/null and b/packages/ui/src/lib/components/Progress/__screenshots__/Progress.vrt.test.ts/Progress-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Progress/__screenshots__/Progress.vrt.test.ts/Progress-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Progress/__screenshots__/Progress.vrt.test.ts/Progress-light-solid-chromium-darwin.png new file mode 100644 index 00000000..4a667cfd Binary files /dev/null and b/packages/ui/src/lib/components/Progress/__screenshots__/Progress.vrt.test.ts/Progress-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/RadioGroup/__screenshots__/RadioGroup.vrt.test.ts/RadioGroup-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/RadioGroup/__screenshots__/RadioGroup.vrt.test.ts/RadioGroup-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..6e353a8a Binary files /dev/null and b/packages/ui/src/lib/components/RadioGroup/__screenshots__/RadioGroup.vrt.test.ts/RadioGroup-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/RadioGroup/__screenshots__/RadioGroup.vrt.test.ts/RadioGroup-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/RadioGroup/__screenshots__/RadioGroup.vrt.test.ts/RadioGroup-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..78415f35 Binary files /dev/null and b/packages/ui/src/lib/components/RadioGroup/__screenshots__/RadioGroup.vrt.test.ts/RadioGroup-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/RadioGroup/__screenshots__/RadioGroup.vrt.test.ts/RadioGroup-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/RadioGroup/__screenshots__/RadioGroup.vrt.test.ts/RadioGroup-light-glass-chromium-darwin.png new file mode 100644 index 00000000..6a5ba6ce Binary files /dev/null and b/packages/ui/src/lib/components/RadioGroup/__screenshots__/RadioGroup.vrt.test.ts/RadioGroup-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/RadioGroup/__screenshots__/RadioGroup.vrt.test.ts/RadioGroup-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/RadioGroup/__screenshots__/RadioGroup.vrt.test.ts/RadioGroup-light-solid-chromium-darwin.png new file mode 100644 index 00000000..9b515535 Binary files /dev/null and b/packages/ui/src/lib/components/RadioGroup/__screenshots__/RadioGroup.vrt.test.ts/RadioGroup-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/RecordList/__screenshots__/RecordList.vrt.test.ts/RecordList-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/RecordList/__screenshots__/RecordList.vrt.test.ts/RecordList-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..051d902a Binary files /dev/null and b/packages/ui/src/lib/components/RecordList/__screenshots__/RecordList.vrt.test.ts/RecordList-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/RecordList/__screenshots__/RecordList.vrt.test.ts/RecordList-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/RecordList/__screenshots__/RecordList.vrt.test.ts/RecordList-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..552bce59 Binary files /dev/null and b/packages/ui/src/lib/components/RecordList/__screenshots__/RecordList.vrt.test.ts/RecordList-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/RecordList/__screenshots__/RecordList.vrt.test.ts/RecordList-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/RecordList/__screenshots__/RecordList.vrt.test.ts/RecordList-light-glass-chromium-darwin.png new file mode 100644 index 00000000..829b73a2 Binary files /dev/null and b/packages/ui/src/lib/components/RecordList/__screenshots__/RecordList.vrt.test.ts/RecordList-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/RecordList/__screenshots__/RecordList.vrt.test.ts/RecordList-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/RecordList/__screenshots__/RecordList.vrt.test.ts/RecordList-light-solid-chromium-darwin.png new file mode 100644 index 00000000..00f75fa3 Binary files /dev/null and b/packages/ui/src/lib/components/RecordList/__screenshots__/RecordList.vrt.test.ts/RecordList-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/ResizablePanels/ResizablePanel.svelte b/packages/ui/src/lib/components/ResizablePanels/ResizablePanel.svelte index e97c32bf..fc4e06a0 100644 --- a/packages/ui/src/lib/components/ResizablePanels/ResizablePanel.svelte +++ b/packages/ui/src/lib/components/ResizablePanels/ResizablePanel.svelte @@ -2,6 +2,7 @@ import { getContext, onMount } from 'svelte'; import type { Snippet } from 'svelte'; import type { HTMLAttributes } from 'svelte/elements'; + import { scrollFocusable } from '../../actions/scroll-focusable.js'; import { SPLITTER_KEY, type SplitterContext } from './splitter.js'; /** One panel of a `ResizablePanels` group. Sizes are percentages of the group. */ @@ -41,6 +42,7 @@ data-panel-id={id} class="ca-resizable__panel {className}" style:--ca-panel-size={size} + use:scrollFocusable > {@render children?.()}
diff --git a/packages/ui/src/lib/components/ResizablePanels/ResizablePanels.svelte.test.ts b/packages/ui/src/lib/components/ResizablePanels/ResizablePanels.svelte.test.ts index a8902937..047941fa 100644 --- a/packages/ui/src/lib/components/ResizablePanels/ResizablePanels.svelte.test.ts +++ b/packages/ui/src/lib/components/ResizablePanels/ResizablePanels.svelte.test.ts @@ -44,6 +44,18 @@ describe("ResizablePanels layout", () => { }); }); +describe("ResizablePanels scrolling", () => { + it("a panel whose content overflows scrolls by keyboard, and axe passes", async () => { + const screen = await render(Harness, { wide: true, min: 5 }); + const panel = q(screen.container, ".ca-resizable__panel"); + panel.style.width = "40px"; + panel.style.flex = "none"; + expect(panel.scrollWidth).toBeGreaterThan(panel.clientWidth); + await expect.poll(() => panel.tabIndex).toBe(0); + expect(await checkA11y(screen.container)).toHaveNoViolations(); + }); +}); + describe("ResizablePanels window splitter (APG)", () => { it("separator exposes the size of the panel before it, with min and max", async () => { const screen = await render(Harness, { min: 20, max: 80 }); diff --git a/packages/ui/src/lib/components/ResizablePanels/ResizablePanelsHarness.test.svelte b/packages/ui/src/lib/components/ResizablePanels/ResizablePanelsHarness.test.svelte index c5ec1dba..4b280511 100644 --- a/packages/ui/src/lib/components/ResizablePanels/ResizablePanelsHarness.test.svelte +++ b/packages/ui/src/lib/components/ResizablePanels/ResizablePanelsHarness.test.svelte @@ -10,6 +10,7 @@ three = false, min = 20, max = 80, + wide = false, }: { direction?: 'horizontal' | 'vertical'; persist?: string; @@ -17,12 +18,13 @@ three?: boolean; min?: number; max?: number; + wide?: boolean; } = $props();
- A + {wide ? 'A very long label that overflows a narrow panel' : 'A'} B {#if three} diff --git a/packages/ui/src/lib/components/Row/__screenshots__/Row.vrt.test.ts/Row-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Row/__screenshots__/Row.vrt.test.ts/Row-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..3a808a85 Binary files /dev/null and b/packages/ui/src/lib/components/Row/__screenshots__/Row.vrt.test.ts/Row-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Row/__screenshots__/Row.vrt.test.ts/Row-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Row/__screenshots__/Row.vrt.test.ts/Row-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..a45c1e52 Binary files /dev/null and b/packages/ui/src/lib/components/Row/__screenshots__/Row.vrt.test.ts/Row-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Row/__screenshots__/Row.vrt.test.ts/Row-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Row/__screenshots__/Row.vrt.test.ts/Row-light-glass-chromium-darwin.png new file mode 100644 index 00000000..dfdcd665 Binary files /dev/null and b/packages/ui/src/lib/components/Row/__screenshots__/Row.vrt.test.ts/Row-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Row/__screenshots__/Row.vrt.test.ts/Row-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Row/__screenshots__/Row.vrt.test.ts/Row-light-solid-chromium-darwin.png new file mode 100644 index 00000000..6a9d5cdc Binary files /dev/null and b/packages/ui/src/lib/components/Row/__screenshots__/Row.vrt.test.ts/Row-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/ScrollArea/__screenshots__/ScrollArea.vrt.test.ts/ScrollArea-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/ScrollArea/__screenshots__/ScrollArea.vrt.test.ts/ScrollArea-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..fa7fa9f8 Binary files /dev/null and b/packages/ui/src/lib/components/ScrollArea/__screenshots__/ScrollArea.vrt.test.ts/ScrollArea-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/ScrollArea/__screenshots__/ScrollArea.vrt.test.ts/ScrollArea-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/ScrollArea/__screenshots__/ScrollArea.vrt.test.ts/ScrollArea-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..86484ed0 Binary files /dev/null and b/packages/ui/src/lib/components/ScrollArea/__screenshots__/ScrollArea.vrt.test.ts/ScrollArea-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/ScrollArea/__screenshots__/ScrollArea.vrt.test.ts/ScrollArea-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/ScrollArea/__screenshots__/ScrollArea.vrt.test.ts/ScrollArea-light-glass-chromium-darwin.png new file mode 100644 index 00000000..6e14f7b0 Binary files /dev/null and b/packages/ui/src/lib/components/ScrollArea/__screenshots__/ScrollArea.vrt.test.ts/ScrollArea-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/ScrollArea/__screenshots__/ScrollArea.vrt.test.ts/ScrollArea-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/ScrollArea/__screenshots__/ScrollArea.vrt.test.ts/ScrollArea-light-solid-chromium-darwin.png new file mode 100644 index 00000000..b822050b Binary files /dev/null and b/packages/ui/src/lib/components/ScrollArea/__screenshots__/ScrollArea.vrt.test.ts/ScrollArea-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Select/__screenshots__/Select.vrt.test.ts/Select-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Select/__screenshots__/Select.vrt.test.ts/Select-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..8edc509c Binary files /dev/null and b/packages/ui/src/lib/components/Select/__screenshots__/Select.vrt.test.ts/Select-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Select/__screenshots__/Select.vrt.test.ts/Select-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Select/__screenshots__/Select.vrt.test.ts/Select-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..f85ca281 Binary files /dev/null and b/packages/ui/src/lib/components/Select/__screenshots__/Select.vrt.test.ts/Select-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Select/__screenshots__/Select.vrt.test.ts/Select-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Select/__screenshots__/Select.vrt.test.ts/Select-light-glass-chromium-darwin.png new file mode 100644 index 00000000..54db81a4 Binary files /dev/null and b/packages/ui/src/lib/components/Select/__screenshots__/Select.vrt.test.ts/Select-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Select/__screenshots__/Select.vrt.test.ts/Select-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Select/__screenshots__/Select.vrt.test.ts/Select-light-solid-chromium-darwin.png new file mode 100644 index 00000000..624c2f1b Binary files /dev/null and b/packages/ui/src/lib/components/Select/__screenshots__/Select.vrt.test.ts/Select-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Select/__screenshots__/Select.vrt.test.ts/Select-open-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Select/__screenshots__/Select.vrt.test.ts/Select-open-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..f45e5572 Binary files /dev/null and b/packages/ui/src/lib/components/Select/__screenshots__/Select.vrt.test.ts/Select-open-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Select/__screenshots__/Select.vrt.test.ts/Select-open-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Select/__screenshots__/Select.vrt.test.ts/Select-open-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..1be40012 Binary files /dev/null and b/packages/ui/src/lib/components/Select/__screenshots__/Select.vrt.test.ts/Select-open-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Select/__screenshots__/Select.vrt.test.ts/Select-open-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Select/__screenshots__/Select.vrt.test.ts/Select-open-light-glass-chromium-darwin.png new file mode 100644 index 00000000..4615443c Binary files /dev/null and b/packages/ui/src/lib/components/Select/__screenshots__/Select.vrt.test.ts/Select-open-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Select/__screenshots__/Select.vrt.test.ts/Select-open-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Select/__screenshots__/Select.vrt.test.ts/Select-open-light-solid-chromium-darwin.png new file mode 100644 index 00000000..d5ed6d03 Binary files /dev/null and b/packages/ui/src/lib/components/Select/__screenshots__/Select.vrt.test.ts/Select-open-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/SettingItem/__screenshots__/SettingItem.vrt.test.ts/SettingItem-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/SettingItem/__screenshots__/SettingItem.vrt.test.ts/SettingItem-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..72a971c6 Binary files /dev/null and b/packages/ui/src/lib/components/SettingItem/__screenshots__/SettingItem.vrt.test.ts/SettingItem-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/SettingItem/__screenshots__/SettingItem.vrt.test.ts/SettingItem-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/SettingItem/__screenshots__/SettingItem.vrt.test.ts/SettingItem-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..ef052d77 Binary files /dev/null and b/packages/ui/src/lib/components/SettingItem/__screenshots__/SettingItem.vrt.test.ts/SettingItem-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/SettingItem/__screenshots__/SettingItem.vrt.test.ts/SettingItem-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/SettingItem/__screenshots__/SettingItem.vrt.test.ts/SettingItem-light-glass-chromium-darwin.png new file mode 100644 index 00000000..12f34277 Binary files /dev/null and b/packages/ui/src/lib/components/SettingItem/__screenshots__/SettingItem.vrt.test.ts/SettingItem-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/SettingItem/__screenshots__/SettingItem.vrt.test.ts/SettingItem-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/SettingItem/__screenshots__/SettingItem.vrt.test.ts/SettingItem-light-solid-chromium-darwin.png new file mode 100644 index 00000000..be67b46d Binary files /dev/null and b/packages/ui/src/lib/components/SettingItem/__screenshots__/SettingItem.vrt.test.ts/SettingItem-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Sheet/__screenshots__/Sheet.vrt.test.ts/Sheet-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Sheet/__screenshots__/Sheet.vrt.test.ts/Sheet-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..23771508 Binary files /dev/null and b/packages/ui/src/lib/components/Sheet/__screenshots__/Sheet.vrt.test.ts/Sheet-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Sheet/__screenshots__/Sheet.vrt.test.ts/Sheet-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Sheet/__screenshots__/Sheet.vrt.test.ts/Sheet-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..3e3ace6d Binary files /dev/null and b/packages/ui/src/lib/components/Sheet/__screenshots__/Sheet.vrt.test.ts/Sheet-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Sheet/__screenshots__/Sheet.vrt.test.ts/Sheet-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Sheet/__screenshots__/Sheet.vrt.test.ts/Sheet-light-glass-chromium-darwin.png new file mode 100644 index 00000000..833347f9 Binary files /dev/null and b/packages/ui/src/lib/components/Sheet/__screenshots__/Sheet.vrt.test.ts/Sheet-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Sheet/__screenshots__/Sheet.vrt.test.ts/Sheet-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Sheet/__screenshots__/Sheet.vrt.test.ts/Sheet-light-solid-chromium-darwin.png new file mode 100644 index 00000000..3879ae87 Binary files /dev/null and b/packages/ui/src/lib/components/Sheet/__screenshots__/Sheet.vrt.test.ts/Sheet-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Sheet/__screenshots__/Sheet.vrt.test.ts/Sheet-mobile-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Sheet/__screenshots__/Sheet.vrt.test.ts/Sheet-mobile-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..7087db61 Binary files /dev/null and b/packages/ui/src/lib/components/Sheet/__screenshots__/Sheet.vrt.test.ts/Sheet-mobile-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Sheet/__screenshots__/Sheet.vrt.test.ts/Sheet-mobile-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Sheet/__screenshots__/Sheet.vrt.test.ts/Sheet-mobile-light-glass-chromium-darwin.png new file mode 100644 index 00000000..ecbc0de0 Binary files /dev/null and b/packages/ui/src/lib/components/Sheet/__screenshots__/Sheet.vrt.test.ts/Sheet-mobile-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Skeleton/__screenshots__/Skeleton.vrt.test.ts/Skeleton-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Skeleton/__screenshots__/Skeleton.vrt.test.ts/Skeleton-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..a2d6e2e6 Binary files /dev/null and b/packages/ui/src/lib/components/Skeleton/__screenshots__/Skeleton.vrt.test.ts/Skeleton-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Skeleton/__screenshots__/Skeleton.vrt.test.ts/Skeleton-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Skeleton/__screenshots__/Skeleton.vrt.test.ts/Skeleton-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..e7984960 Binary files /dev/null and b/packages/ui/src/lib/components/Skeleton/__screenshots__/Skeleton.vrt.test.ts/Skeleton-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Skeleton/__screenshots__/Skeleton.vrt.test.ts/Skeleton-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Skeleton/__screenshots__/Skeleton.vrt.test.ts/Skeleton-light-glass-chromium-darwin.png new file mode 100644 index 00000000..76f49ec7 Binary files /dev/null and b/packages/ui/src/lib/components/Skeleton/__screenshots__/Skeleton.vrt.test.ts/Skeleton-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Skeleton/__screenshots__/Skeleton.vrt.test.ts/Skeleton-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Skeleton/__screenshots__/Skeleton.vrt.test.ts/Skeleton-light-solid-chromium-darwin.png new file mode 100644 index 00000000..2ba13c65 Binary files /dev/null and b/packages/ui/src/lib/components/Skeleton/__screenshots__/Skeleton.vrt.test.ts/Skeleton-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Slider/__screenshots__/Slider.vrt.test.ts/Slider-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Slider/__screenshots__/Slider.vrt.test.ts/Slider-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..f7d20926 Binary files /dev/null and b/packages/ui/src/lib/components/Slider/__screenshots__/Slider.vrt.test.ts/Slider-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Slider/__screenshots__/Slider.vrt.test.ts/Slider-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Slider/__screenshots__/Slider.vrt.test.ts/Slider-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..07006696 Binary files /dev/null and b/packages/ui/src/lib/components/Slider/__screenshots__/Slider.vrt.test.ts/Slider-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Slider/__screenshots__/Slider.vrt.test.ts/Slider-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Slider/__screenshots__/Slider.vrt.test.ts/Slider-light-glass-chromium-darwin.png new file mode 100644 index 00000000..01129944 Binary files /dev/null and b/packages/ui/src/lib/components/Slider/__screenshots__/Slider.vrt.test.ts/Slider-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Slider/__screenshots__/Slider.vrt.test.ts/Slider-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Slider/__screenshots__/Slider.vrt.test.ts/Slider-light-solid-chromium-darwin.png new file mode 100644 index 00000000..4f71f9c5 Binary files /dev/null and b/packages/ui/src/lib/components/Slider/__screenshots__/Slider.vrt.test.ts/Slider-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Spinner/__screenshots__/Spinner.vrt.test.ts/Spinner-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Spinner/__screenshots__/Spinner.vrt.test.ts/Spinner-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..5130392f Binary files /dev/null and b/packages/ui/src/lib/components/Spinner/__screenshots__/Spinner.vrt.test.ts/Spinner-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Spinner/__screenshots__/Spinner.vrt.test.ts/Spinner-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Spinner/__screenshots__/Spinner.vrt.test.ts/Spinner-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..6ab456fe Binary files /dev/null and b/packages/ui/src/lib/components/Spinner/__screenshots__/Spinner.vrt.test.ts/Spinner-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Spinner/__screenshots__/Spinner.vrt.test.ts/Spinner-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Spinner/__screenshots__/Spinner.vrt.test.ts/Spinner-light-glass-chromium-darwin.png new file mode 100644 index 00000000..b8debb3d Binary files /dev/null and b/packages/ui/src/lib/components/Spinner/__screenshots__/Spinner.vrt.test.ts/Spinner-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Spinner/__screenshots__/Spinner.vrt.test.ts/Spinner-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Spinner/__screenshots__/Spinner.vrt.test.ts/Spinner-light-solid-chromium-darwin.png new file mode 100644 index 00000000..9fe623fc Binary files /dev/null and b/packages/ui/src/lib/components/Spinner/__screenshots__/Spinner.vrt.test.ts/Spinner-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Stack/__screenshots__/Stack.vrt.test.ts/Stack-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Stack/__screenshots__/Stack.vrt.test.ts/Stack-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..396dd265 Binary files /dev/null and b/packages/ui/src/lib/components/Stack/__screenshots__/Stack.vrt.test.ts/Stack-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Stack/__screenshots__/Stack.vrt.test.ts/Stack-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Stack/__screenshots__/Stack.vrt.test.ts/Stack-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..f1c47add Binary files /dev/null and b/packages/ui/src/lib/components/Stack/__screenshots__/Stack.vrt.test.ts/Stack-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Stack/__screenshots__/Stack.vrt.test.ts/Stack-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Stack/__screenshots__/Stack.vrt.test.ts/Stack-light-glass-chromium-darwin.png new file mode 100644 index 00000000..cdb59ff9 Binary files /dev/null and b/packages/ui/src/lib/components/Stack/__screenshots__/Stack.vrt.test.ts/Stack-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Stack/__screenshots__/Stack.vrt.test.ts/Stack-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Stack/__screenshots__/Stack.vrt.test.ts/Stack-light-solid-chromium-darwin.png new file mode 100644 index 00000000..d0fed8d2 Binary files /dev/null and b/packages/ui/src/lib/components/Stack/__screenshots__/Stack.vrt.test.ts/Stack-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/StatusChip/__screenshots__/StatusChip.vrt.test.ts/StatusChip-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/StatusChip/__screenshots__/StatusChip.vrt.test.ts/StatusChip-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..5decc612 Binary files /dev/null and b/packages/ui/src/lib/components/StatusChip/__screenshots__/StatusChip.vrt.test.ts/StatusChip-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/StatusChip/__screenshots__/StatusChip.vrt.test.ts/StatusChip-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/StatusChip/__screenshots__/StatusChip.vrt.test.ts/StatusChip-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..11e99ef3 Binary files /dev/null and b/packages/ui/src/lib/components/StatusChip/__screenshots__/StatusChip.vrt.test.ts/StatusChip-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/StatusChip/__screenshots__/StatusChip.vrt.test.ts/StatusChip-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/StatusChip/__screenshots__/StatusChip.vrt.test.ts/StatusChip-light-glass-chromium-darwin.png new file mode 100644 index 00000000..e7f73460 Binary files /dev/null and b/packages/ui/src/lib/components/StatusChip/__screenshots__/StatusChip.vrt.test.ts/StatusChip-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/StatusChip/__screenshots__/StatusChip.vrt.test.ts/StatusChip-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/StatusChip/__screenshots__/StatusChip.vrt.test.ts/StatusChip-light-solid-chromium-darwin.png new file mode 100644 index 00000000..f5cf74e6 Binary files /dev/null and b/packages/ui/src/lib/components/StatusChip/__screenshots__/StatusChip.vrt.test.ts/StatusChip-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Stepper/__screenshots__/Stepper.vrt.test.ts/Stepper-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Stepper/__screenshots__/Stepper.vrt.test.ts/Stepper-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..64119fa3 Binary files /dev/null and b/packages/ui/src/lib/components/Stepper/__screenshots__/Stepper.vrt.test.ts/Stepper-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Stepper/__screenshots__/Stepper.vrt.test.ts/Stepper-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Stepper/__screenshots__/Stepper.vrt.test.ts/Stepper-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..39fe3039 Binary files /dev/null and b/packages/ui/src/lib/components/Stepper/__screenshots__/Stepper.vrt.test.ts/Stepper-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Stepper/__screenshots__/Stepper.vrt.test.ts/Stepper-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Stepper/__screenshots__/Stepper.vrt.test.ts/Stepper-light-glass-chromium-darwin.png new file mode 100644 index 00000000..a717124e Binary files /dev/null and b/packages/ui/src/lib/components/Stepper/__screenshots__/Stepper.vrt.test.ts/Stepper-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Stepper/__screenshots__/Stepper.vrt.test.ts/Stepper-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Stepper/__screenshots__/Stepper.vrt.test.ts/Stepper-light-solid-chromium-darwin.png new file mode 100644 index 00000000..7e1f4a56 Binary files /dev/null and b/packages/ui/src/lib/components/Stepper/__screenshots__/Stepper.vrt.test.ts/Stepper-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Switch/__screenshots__/Switch.vrt.test.ts/Switch-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Switch/__screenshots__/Switch.vrt.test.ts/Switch-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..946ae76e Binary files /dev/null and b/packages/ui/src/lib/components/Switch/__screenshots__/Switch.vrt.test.ts/Switch-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Switch/__screenshots__/Switch.vrt.test.ts/Switch-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Switch/__screenshots__/Switch.vrt.test.ts/Switch-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..58869cb3 Binary files /dev/null and b/packages/ui/src/lib/components/Switch/__screenshots__/Switch.vrt.test.ts/Switch-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Switch/__screenshots__/Switch.vrt.test.ts/Switch-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Switch/__screenshots__/Switch.vrt.test.ts/Switch-light-glass-chromium-darwin.png new file mode 100644 index 00000000..4e9b1164 Binary files /dev/null and b/packages/ui/src/lib/components/Switch/__screenshots__/Switch.vrt.test.ts/Switch-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Switch/__screenshots__/Switch.vrt.test.ts/Switch-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Switch/__screenshots__/Switch.vrt.test.ts/Switch-light-solid-chromium-darwin.png new file mode 100644 index 00000000..e0ffb2cb Binary files /dev/null and b/packages/ui/src/lib/components/Switch/__screenshots__/Switch.vrt.test.ts/Switch-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Table/__screenshots__/SimpleTable.vrt.test.ts/SimpleTable-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Table/__screenshots__/SimpleTable.vrt.test.ts/SimpleTable-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..98442864 Binary files /dev/null and b/packages/ui/src/lib/components/Table/__screenshots__/SimpleTable.vrt.test.ts/SimpleTable-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Table/__screenshots__/SimpleTable.vrt.test.ts/SimpleTable-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Table/__screenshots__/SimpleTable.vrt.test.ts/SimpleTable-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..12bb87a8 Binary files /dev/null and b/packages/ui/src/lib/components/Table/__screenshots__/SimpleTable.vrt.test.ts/SimpleTable-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Table/__screenshots__/SimpleTable.vrt.test.ts/SimpleTable-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Table/__screenshots__/SimpleTable.vrt.test.ts/SimpleTable-light-glass-chromium-darwin.png new file mode 100644 index 00000000..34e5574a Binary files /dev/null and b/packages/ui/src/lib/components/Table/__screenshots__/SimpleTable.vrt.test.ts/SimpleTable-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Table/__screenshots__/SimpleTable.vrt.test.ts/SimpleTable-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Table/__screenshots__/SimpleTable.vrt.test.ts/SimpleTable-light-solid-chromium-darwin.png new file mode 100644 index 00000000..5a3799fc Binary files /dev/null and b/packages/ui/src/lib/components/Table/__screenshots__/SimpleTable.vrt.test.ts/SimpleTable-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Table/__screenshots__/SimpleTable.vrt.test.ts/SimpleTable-mobile-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Table/__screenshots__/SimpleTable.vrt.test.ts/SimpleTable-mobile-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..8813739a Binary files /dev/null and b/packages/ui/src/lib/components/Table/__screenshots__/SimpleTable.vrt.test.ts/SimpleTable-mobile-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Table/__screenshots__/SimpleTable.vrt.test.ts/SimpleTable-mobile-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Table/__screenshots__/SimpleTable.vrt.test.ts/SimpleTable-mobile-light-glass-chromium-darwin.png new file mode 100644 index 00000000..8dc5275c Binary files /dev/null and b/packages/ui/src/lib/components/Table/__screenshots__/SimpleTable.vrt.test.ts/SimpleTable-mobile-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Tabs/__screenshots__/Tabs.vrt.test.ts/Tabs-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Tabs/__screenshots__/Tabs.vrt.test.ts/Tabs-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..239b1d5b Binary files /dev/null and b/packages/ui/src/lib/components/Tabs/__screenshots__/Tabs.vrt.test.ts/Tabs-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Tabs/__screenshots__/Tabs.vrt.test.ts/Tabs-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Tabs/__screenshots__/Tabs.vrt.test.ts/Tabs-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..c9993534 Binary files /dev/null and b/packages/ui/src/lib/components/Tabs/__screenshots__/Tabs.vrt.test.ts/Tabs-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Tabs/__screenshots__/Tabs.vrt.test.ts/Tabs-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Tabs/__screenshots__/Tabs.vrt.test.ts/Tabs-light-glass-chromium-darwin.png new file mode 100644 index 00000000..30518a1e Binary files /dev/null and b/packages/ui/src/lib/components/Tabs/__screenshots__/Tabs.vrt.test.ts/Tabs-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Tabs/__screenshots__/Tabs.vrt.test.ts/Tabs-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Tabs/__screenshots__/Tabs.vrt.test.ts/Tabs-light-solid-chromium-darwin.png new file mode 100644 index 00000000..ff30d18b Binary files /dev/null and b/packages/ui/src/lib/components/Tabs/__screenshots__/Tabs.vrt.test.ts/Tabs-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/TextArea/__screenshots__/TextArea.vrt.test.ts/TextArea-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/TextArea/__screenshots__/TextArea.vrt.test.ts/TextArea-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..320d15a4 Binary files /dev/null and b/packages/ui/src/lib/components/TextArea/__screenshots__/TextArea.vrt.test.ts/TextArea-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/TextArea/__screenshots__/TextArea.vrt.test.ts/TextArea-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/TextArea/__screenshots__/TextArea.vrt.test.ts/TextArea-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..1230c813 Binary files /dev/null and b/packages/ui/src/lib/components/TextArea/__screenshots__/TextArea.vrt.test.ts/TextArea-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/TextArea/__screenshots__/TextArea.vrt.test.ts/TextArea-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/TextArea/__screenshots__/TextArea.vrt.test.ts/TextArea-light-glass-chromium-darwin.png new file mode 100644 index 00000000..7401abe5 Binary files /dev/null and b/packages/ui/src/lib/components/TextArea/__screenshots__/TextArea.vrt.test.ts/TextArea-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/TextArea/__screenshots__/TextArea.vrt.test.ts/TextArea-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/TextArea/__screenshots__/TextArea.vrt.test.ts/TextArea-light-solid-chromium-darwin.png new file mode 100644 index 00000000..189f5380 Binary files /dev/null and b/packages/ui/src/lib/components/TextArea/__screenshots__/TextArea.vrt.test.ts/TextArea-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/TextField/__screenshots__/TextField.vrt.test.ts/TextField-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/TextField/__screenshots__/TextField.vrt.test.ts/TextField-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..0f6ced7b Binary files /dev/null and b/packages/ui/src/lib/components/TextField/__screenshots__/TextField.vrt.test.ts/TextField-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/TextField/__screenshots__/TextField.vrt.test.ts/TextField-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/TextField/__screenshots__/TextField.vrt.test.ts/TextField-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..eded9875 Binary files /dev/null and b/packages/ui/src/lib/components/TextField/__screenshots__/TextField.vrt.test.ts/TextField-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/TextField/__screenshots__/TextField.vrt.test.ts/TextField-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/TextField/__screenshots__/TextField.vrt.test.ts/TextField-light-glass-chromium-darwin.png new file mode 100644 index 00000000..1516ceac Binary files /dev/null and b/packages/ui/src/lib/components/TextField/__screenshots__/TextField.vrt.test.ts/TextField-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/TextField/__screenshots__/TextField.vrt.test.ts/TextField-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/TextField/__screenshots__/TextField.vrt.test.ts/TextField-light-solid-chromium-darwin.png new file mode 100644 index 00000000..79092d94 Binary files /dev/null and b/packages/ui/src/lib/components/TextField/__screenshots__/TextField.vrt.test.ts/TextField-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/ThemeIsland/ThemeIsland.svelte.test.ts b/packages/ui/src/lib/components/ThemeIsland/ThemeIsland.svelte.test.ts index 921335d3..f293a75d 100644 --- a/packages/ui/src/lib/components/ThemeIsland/ThemeIsland.svelte.test.ts +++ b/packages/ui/src/lib/components/ThemeIsland/ThemeIsland.svelte.test.ts @@ -3,6 +3,7 @@ import { render } from "vitest-browser-svelte"; import { checkA11y } from "../../../../tests/a11y"; import { settled } from "../../../../tests/settled"; import Harness from "./ThemeIsland.test.svelte"; +import Surfaces from "./ThemeIslandSurfaces.test.svelte"; const html = document.documentElement; const bg = (el: Element) => getComputedStyle(el).backgroundColor; @@ -69,4 +70,25 @@ describe("ThemeIsland (#352)", () => { expect(html.getAttribute("data-preset")).toBe("amber"); expect(html.hasAttribute("data-theme-scope")).toBe(false); }); + + it.each([ + ["dark", false], + ["light", true], + ] as const)( + "a %s island on the opposite page keeps AA and its own surfaces", + async (scheme, pageDark) => { + html.classList.toggle("dark", pageDark); + const screen = await render(Surfaces, { scheme }); + const island = screen.container.querySelector(".ca-theme-island") as HTMLElement; + const zone = screen.container.querySelector(".ca-file-upload__zone") as HTMLElement; + // The default style is solid: the zone is the raised surface of the island, not the page's. + const probe = document.createElement("div"); + probe.style.backgroundColor = "var(--ca-surface-raised)"; + island.append(probe); + const raised = bg(probe); + probe.remove(); + expect(bg(zone)).toBe(raised); + expect(await checkA11y(screen.container)).toHaveNoViolations(); + } + ); }); diff --git a/packages/ui/src/lib/components/ThemeIsland/ThemeIslandSurfaces.test.svelte b/packages/ui/src/lib/components/ThemeIsland/ThemeIslandSurfaces.test.svelte new file mode 100644 index 00000000..604957e5 --- /dev/null +++ b/packages/ui/src/lib/components/ThemeIsland/ThemeIslandSurfaces.test.svelte @@ -0,0 +1,14 @@ + + + + + + + diff --git a/packages/ui/src/lib/components/ThemeToggle/__screenshots__/ThemeToggle.vrt.test.ts/ThemeToggle-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/ThemeToggle/__screenshots__/ThemeToggle.vrt.test.ts/ThemeToggle-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..026b1c83 Binary files /dev/null and b/packages/ui/src/lib/components/ThemeToggle/__screenshots__/ThemeToggle.vrt.test.ts/ThemeToggle-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/ThemeToggle/__screenshots__/ThemeToggle.vrt.test.ts/ThemeToggle-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/ThemeToggle/__screenshots__/ThemeToggle.vrt.test.ts/ThemeToggle-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..64f86d71 Binary files /dev/null and b/packages/ui/src/lib/components/ThemeToggle/__screenshots__/ThemeToggle.vrt.test.ts/ThemeToggle-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/ThemeToggle/__screenshots__/ThemeToggle.vrt.test.ts/ThemeToggle-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/ThemeToggle/__screenshots__/ThemeToggle.vrt.test.ts/ThemeToggle-light-glass-chromium-darwin.png new file mode 100644 index 00000000..475fbbdc Binary files /dev/null and b/packages/ui/src/lib/components/ThemeToggle/__screenshots__/ThemeToggle.vrt.test.ts/ThemeToggle-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/ThemeToggle/__screenshots__/ThemeToggle.vrt.test.ts/ThemeToggle-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/ThemeToggle/__screenshots__/ThemeToggle.vrt.test.ts/ThemeToggle-light-solid-chromium-darwin.png new file mode 100644 index 00000000..19a2aa5d Binary files /dev/null and b/packages/ui/src/lib/components/ThemeToggle/__screenshots__/ThemeToggle.vrt.test.ts/ThemeToggle-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Timeline/__screenshots__/Timeline.vrt.test.ts/Timeline-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Timeline/__screenshots__/Timeline.vrt.test.ts/Timeline-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..13490867 Binary files /dev/null and b/packages/ui/src/lib/components/Timeline/__screenshots__/Timeline.vrt.test.ts/Timeline-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Timeline/__screenshots__/Timeline.vrt.test.ts/Timeline-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Timeline/__screenshots__/Timeline.vrt.test.ts/Timeline-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..1f0e703e Binary files /dev/null and b/packages/ui/src/lib/components/Timeline/__screenshots__/Timeline.vrt.test.ts/Timeline-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Timeline/__screenshots__/Timeline.vrt.test.ts/Timeline-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Timeline/__screenshots__/Timeline.vrt.test.ts/Timeline-light-glass-chromium-darwin.png new file mode 100644 index 00000000..d93746bc Binary files /dev/null and b/packages/ui/src/lib/components/Timeline/__screenshots__/Timeline.vrt.test.ts/Timeline-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Timeline/__screenshots__/Timeline.vrt.test.ts/Timeline-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Timeline/__screenshots__/Timeline.vrt.test.ts/Timeline-light-solid-chromium-darwin.png new file mode 100644 index 00000000..2b896313 Binary files /dev/null and b/packages/ui/src/lib/components/Timeline/__screenshots__/Timeline.vrt.test.ts/Timeline-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/ToggleGroup/SegmentedControl.svelte b/packages/ui/src/lib/components/ToggleGroup/SegmentedControl.svelte index fe696f1e..2ecf0a59 100644 --- a/packages/ui/src/lib/components/ToggleGroup/SegmentedControl.svelte +++ b/packages/ui/src/lib/components/ToggleGroup/SegmentedControl.svelte @@ -189,6 +189,7 @@ @media (pointer: coarse) { :global(.ca-seg__item) { min-height: var(--ca-touch-target); + min-width: var(--ca-touch-target); } } } diff --git a/packages/ui/src/lib/components/ToggleGroup/SegmentedControl.svelte.test.ts b/packages/ui/src/lib/components/ToggleGroup/SegmentedControl.svelte.test.ts index 61b3c398..e2ade264 100644 --- a/packages/ui/src/lib/components/ToggleGroup/SegmentedControl.svelte.test.ts +++ b/packages/ui/src/lib/components/ToggleGroup/SegmentedControl.svelte.test.ts @@ -4,6 +4,7 @@ import { render } from "vitest-browser-svelte"; import { checkA11y } from "../../../../tests/a11y"; import { reducedMotion } from "../../stores/reduced-motion.svelte"; import SegmentedControlHarness from "./SegmentedControlHarness.test.svelte"; +import SegmentedControlNarrow from "./SegmentedControlNarrow.test.svelte"; const segIndex = (el: Element) => (el as HTMLElement).style.getPropertyValue("--seg-index"); @@ -74,4 +75,19 @@ describe("SegmentedControl", () => { }); } }); + + it("every item is at least 44px wide and tall under a coarse pointer", async () => { + await cdp().send("Emulation.setTouchEmulationEnabled", { enabled: true, maxTouchPoints: 5 }); + try { + const screen = await render(SegmentedControlNarrow); + expect(matchMedia("(pointer: coarse)").matches).toBe(true); + for (const item of screen.container.querySelectorAll(".ca-seg__item")) { + const r = item.getBoundingClientRect(); + expect(r.width, "item width").toBeGreaterThanOrEqual(44); + expect(r.height, "item height").toBeGreaterThanOrEqual(44); + } + } finally { + await cdp().send("Emulation.setTouchEmulationEnabled", { enabled: false }); + } + }); }); diff --git a/packages/ui/src/lib/components/ToggleGroup/SegmentedControlNarrow.test.svelte b/packages/ui/src/lib/components/ToggleGroup/SegmentedControlNarrow.test.svelte new file mode 100644 index 00000000..8f47e40a --- /dev/null +++ b/packages/ui/src/lib/components/ToggleGroup/SegmentedControlNarrow.test.svelte @@ -0,0 +1,20 @@ + + +
+ +
diff --git a/packages/ui/src/lib/components/ToggleGroup/__screenshots__/SegmentedControl.vrt.test.ts/SegmentedControl-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/ToggleGroup/__screenshots__/SegmentedControl.vrt.test.ts/SegmentedControl-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..beea6791 Binary files /dev/null and b/packages/ui/src/lib/components/ToggleGroup/__screenshots__/SegmentedControl.vrt.test.ts/SegmentedControl-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/ToggleGroup/__screenshots__/SegmentedControl.vrt.test.ts/SegmentedControl-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/ToggleGroup/__screenshots__/SegmentedControl.vrt.test.ts/SegmentedControl-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..3bad06f1 Binary files /dev/null and b/packages/ui/src/lib/components/ToggleGroup/__screenshots__/SegmentedControl.vrt.test.ts/SegmentedControl-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/ToggleGroup/__screenshots__/SegmentedControl.vrt.test.ts/SegmentedControl-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/ToggleGroup/__screenshots__/SegmentedControl.vrt.test.ts/SegmentedControl-light-glass-chromium-darwin.png new file mode 100644 index 00000000..8c02416d Binary files /dev/null and b/packages/ui/src/lib/components/ToggleGroup/__screenshots__/SegmentedControl.vrt.test.ts/SegmentedControl-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/ToggleGroup/__screenshots__/SegmentedControl.vrt.test.ts/SegmentedControl-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/ToggleGroup/__screenshots__/SegmentedControl.vrt.test.ts/SegmentedControl-light-solid-chromium-darwin.png new file mode 100644 index 00000000..8e421e3c Binary files /dev/null and b/packages/ui/src/lib/components/ToggleGroup/__screenshots__/SegmentedControl.vrt.test.ts/SegmentedControl-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/ToggleGroup/__screenshots__/ToggleGroup.vrt.test.ts/ToggleGroup-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/ToggleGroup/__screenshots__/ToggleGroup.vrt.test.ts/ToggleGroup-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..710a9d33 Binary files /dev/null and b/packages/ui/src/lib/components/ToggleGroup/__screenshots__/ToggleGroup.vrt.test.ts/ToggleGroup-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/ToggleGroup/__screenshots__/ToggleGroup.vrt.test.ts/ToggleGroup-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/ToggleGroup/__screenshots__/ToggleGroup.vrt.test.ts/ToggleGroup-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..bdad6601 Binary files /dev/null and b/packages/ui/src/lib/components/ToggleGroup/__screenshots__/ToggleGroup.vrt.test.ts/ToggleGroup-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/ToggleGroup/__screenshots__/ToggleGroup.vrt.test.ts/ToggleGroup-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/ToggleGroup/__screenshots__/ToggleGroup.vrt.test.ts/ToggleGroup-light-glass-chromium-darwin.png new file mode 100644 index 00000000..99515ba3 Binary files /dev/null and b/packages/ui/src/lib/components/ToggleGroup/__screenshots__/ToggleGroup.vrt.test.ts/ToggleGroup-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/ToggleGroup/__screenshots__/ToggleGroup.vrt.test.ts/ToggleGroup-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/ToggleGroup/__screenshots__/ToggleGroup.vrt.test.ts/ToggleGroup-light-solid-chromium-darwin.png new file mode 100644 index 00000000..cd403e77 Binary files /dev/null and b/packages/ui/src/lib/components/ToggleGroup/__screenshots__/ToggleGroup.vrt.test.ts/ToggleGroup-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Tooltip/__screenshots__/Tooltip.vrt.test.ts/Tooltip-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/Tooltip/__screenshots__/Tooltip.vrt.test.ts/Tooltip-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..7b24c30a Binary files /dev/null and b/packages/ui/src/lib/components/Tooltip/__screenshots__/Tooltip.vrt.test.ts/Tooltip-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Tooltip/__screenshots__/Tooltip.vrt.test.ts/Tooltip-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/Tooltip/__screenshots__/Tooltip.vrt.test.ts/Tooltip-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..862ff0e7 Binary files /dev/null and b/packages/ui/src/lib/components/Tooltip/__screenshots__/Tooltip.vrt.test.ts/Tooltip-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Tooltip/__screenshots__/Tooltip.vrt.test.ts/Tooltip-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/Tooltip/__screenshots__/Tooltip.vrt.test.ts/Tooltip-light-glass-chromium-darwin.png new file mode 100644 index 00000000..c53386c0 Binary files /dev/null and b/packages/ui/src/lib/components/Tooltip/__screenshots__/Tooltip.vrt.test.ts/Tooltip-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/Tooltip/__screenshots__/Tooltip.vrt.test.ts/Tooltip-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/Tooltip/__screenshots__/Tooltip.vrt.test.ts/Tooltip-light-solid-chromium-darwin.png new file mode 100644 index 00000000..f96b5f96 Binary files /dev/null and b/packages/ui/src/lib/components/Tooltip/__screenshots__/Tooltip.vrt.test.ts/Tooltip-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/templates/__screenshots__/FormSection.vrt.test.ts/FormSection-dark-glass-chromium-darwin.png b/packages/ui/src/lib/components/templates/__screenshots__/FormSection.vrt.test.ts/FormSection-dark-glass-chromium-darwin.png new file mode 100644 index 00000000..a1665240 Binary files /dev/null and b/packages/ui/src/lib/components/templates/__screenshots__/FormSection.vrt.test.ts/FormSection-dark-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/templates/__screenshots__/FormSection.vrt.test.ts/FormSection-dark-solid-chromium-darwin.png b/packages/ui/src/lib/components/templates/__screenshots__/FormSection.vrt.test.ts/FormSection-dark-solid-chromium-darwin.png new file mode 100644 index 00000000..7915174a Binary files /dev/null and b/packages/ui/src/lib/components/templates/__screenshots__/FormSection.vrt.test.ts/FormSection-dark-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/templates/__screenshots__/FormSection.vrt.test.ts/FormSection-light-glass-chromium-darwin.png b/packages/ui/src/lib/components/templates/__screenshots__/FormSection.vrt.test.ts/FormSection-light-glass-chromium-darwin.png new file mode 100644 index 00000000..5edc89d7 Binary files /dev/null and b/packages/ui/src/lib/components/templates/__screenshots__/FormSection.vrt.test.ts/FormSection-light-glass-chromium-darwin.png differ diff --git a/packages/ui/src/lib/components/templates/__screenshots__/FormSection.vrt.test.ts/FormSection-light-solid-chromium-darwin.png b/packages/ui/src/lib/components/templates/__screenshots__/FormSection.vrt.test.ts/FormSection-light-solid-chromium-darwin.png new file mode 100644 index 00000000..82cb1f1e Binary files /dev/null and b/packages/ui/src/lib/components/templates/__screenshots__/FormSection.vrt.test.ts/FormSection-light-solid-chromium-darwin.png differ diff --git a/packages/ui/src/lib/styles/materials.css b/packages/ui/src/lib/styles/materials.css index d8f57438..3fa1d8d9 100644 --- a/packages/ui/src/lib/styles/materials.css +++ b/packages/ui/src/lib/styles/materials.css @@ -243,7 +243,8 @@ * --ca-overlay-blur: none; * } * ─────────────────────────────────────────────────────────────────────────── */ -:root { +:root, +[data-theme-scope] { /* ── Overlay (Popover, DropdownMenu, pickers) ── * Carries primary text, so it is near-opaque by construction: the contrast of its labels must not * depend on the page behind. The blur stays for the glass look. --ca-overlay-bg-glass is the old @@ -349,7 +350,8 @@ * .glass-glow, .glass-active, .glass-btn-prominent and the nav pills lose their halo without a * component edit. Selected states keep their brand bar. ── */ [data-style="solid"], -:root:not([data-style]) { +:root:not([data-style]), +:root:not([data-style]) [data-theme-scope] { --ca-glow-primary: transparent; --ca-glow-primary-strong: transparent; --ca-glass-rim: transparent; @@ -399,6 +401,7 @@ /* ── Opaque material styles: no blur anywhere, overlays on solid surfaces ── */ [data-style="solid"], :root:not([data-style]), +:root:not([data-style]) [data-theme-scope], [data-style="flat"], [data-style="outlined"], [data-style="neumorphic"], diff --git a/packages/ui/src/lib/testing/helpers.svelte.test.ts b/packages/ui/src/lib/testing/helpers.svelte.test.ts index 3334103d..eba90a71 100644 --- a/packages/ui/src/lib/testing/helpers.svelte.test.ts +++ b/packages/ui/src/lib/testing/helpers.svelte.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it } from "vitest"; -import { coarsePointer, measure, withTheme } from "./index"; +import { coarsePointer, measure, settled, withTheme } from "./index"; const mounted: HTMLElement[] = []; function mount(html: string): HTMLElement { @@ -115,3 +115,33 @@ describe("coarsePointer", () => { expect(seen).toEqual([true]); }); }); + +describe("settled", () => { + // axe must read the colours a transition ends on. It used to sit through the transition in real + // time (150 ms on every check, a 5 s one hit the 3 s deadline); it now jumps to the end state. + it("resolves at once, with a running transition at its end state", async () => { + const el = mount( + '

faded in

' + ); + const p = el.querySelector("#t") as HTMLElement; + p.getBoundingClientRect(); + p.style.opacity = "1"; + expect(document.getAnimations().length).toBeGreaterThan(0); + const start = performance.now(); + await settled(); + expect(performance.now() - start).toBeLessThan(1_000); + expect(getComputedStyle(p).opacity).toBe("1"); + expect(document.getAnimations().filter((a) => a.playState === "running")).toHaveLength(0); + }); + + it("leaves an infinite animation alone and still resolves", async () => { + const el = mount( + 'o' + ); + await settled(); + expect(getComputedStyle(el.querySelector("#s") as HTMLElement).animationIterationCount).toBe( + "infinite" + ); + expect(document.getAnimations().some((a) => a.playState === "running")).toBe(true); + }); +}); diff --git a/packages/ui/src/lib/testing/index.ts b/packages/ui/src/lib/testing/index.ts index dfdfc749..8b98f3ec 100644 --- a/packages/ui/src/lib/testing/index.ts +++ b/packages/ui/src/lib/testing/index.ts @@ -27,7 +27,10 @@ export const A11Y_EXCEPTIONS: NonNullable = { /** * Resolves once no CSS animation or transition is running, so axe reads final colours (overlays * fade in; contrast measured mid-fade is a false positive). Infinite animations (spinners, - * shimmers) have no final state and are ignored. + * shimmers) have no final state and are ignored. Each running animation is jumped to its end + * state rather than waited out: the end state is what is checked, and sitting through a 150 ms + * entrance transition in real time tells axe nothing more (it was ~150 ms per check, on the + * ~430 checks of the component suite). */ export async function settled(timeout = 3_000): Promise { const frame = () => { @@ -38,12 +41,18 @@ export async function settled(timeout = 3_000): Promise { const running = () => document .getAnimations() - .filter((a) => a.playState === "running" && a.effect?.getTiming().iterations !== Infinity) - .length; + .filter((a) => a.playState === "running" && a.effect?.getTiming().iterations !== Infinity); await frame(); const deadline = Date.now() + timeout; - while (running() > 0) { - if (Date.now() > deadline) throw new Error(`${running()} animation(s) still running`); + for (let now = running(); now.length > 0; now = running()) { + if (Date.now() > deadline) throw new Error(`${now.length} animation(s) still running`); + for (const animation of now) { + try { + animation.finish(); + } catch { + // not finishable (zero playback rate): fall through to the next frame and the deadline + } + } await frame(); } } diff --git a/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-apple-dark-chromium-darwin.png b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-apple-dark-chromium-darwin.png new file mode 100644 index 00000000..2968f5cc Binary files /dev/null and b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-apple-dark-chromium-darwin.png differ diff --git a/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-apple-light-chromium-darwin.png b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-apple-light-chromium-darwin.png new file mode 100644 index 00000000..8f1ebfd7 Binary files /dev/null and b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-apple-light-chromium-darwin.png differ diff --git a/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-bento-dark-chromium-darwin.png b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-bento-dark-chromium-darwin.png new file mode 100644 index 00000000..a543ec38 Binary files /dev/null and b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-bento-dark-chromium-darwin.png differ diff --git a/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-bento-light-chromium-darwin.png b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-bento-light-chromium-darwin.png new file mode 100644 index 00000000..88a37f07 Binary files /dev/null and b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-bento-light-chromium-darwin.png differ diff --git a/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-brutalism-dark-chromium-darwin.png b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-brutalism-dark-chromium-darwin.png new file mode 100644 index 00000000..6f3b7c34 Binary files /dev/null and b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-brutalism-dark-chromium-darwin.png differ diff --git a/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-brutalism-light-chromium-darwin.png b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-brutalism-light-chromium-darwin.png new file mode 100644 index 00000000..2822b023 Binary files /dev/null and b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-brutalism-light-chromium-darwin.png differ diff --git a/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-claymorphism-dark-chromium-darwin.png b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-claymorphism-dark-chromium-darwin.png new file mode 100644 index 00000000..fed13fb8 Binary files /dev/null and b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-claymorphism-dark-chromium-darwin.png differ diff --git a/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-claymorphism-light-chromium-darwin.png b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-claymorphism-light-chromium-darwin.png new file mode 100644 index 00000000..0674d40a Binary files /dev/null and b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-claymorphism-light-chromium-darwin.png differ diff --git a/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-default-dark-chromium-darwin.png b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-default-dark-chromium-darwin.png new file mode 100644 index 00000000..f693ed8f Binary files /dev/null and b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-default-dark-chromium-darwin.png differ diff --git a/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-default-light-chromium-darwin.png b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-default-light-chromium-darwin.png new file mode 100644 index 00000000..13643423 Binary files /dev/null and b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-default-light-chromium-darwin.png differ diff --git a/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-editorial-dark-chromium-darwin.png b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-editorial-dark-chromium-darwin.png new file mode 100644 index 00000000..a8560520 Binary files /dev/null and b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-editorial-dark-chromium-darwin.png differ diff --git a/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-editorial-light-chromium-darwin.png b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-editorial-light-chromium-darwin.png new file mode 100644 index 00000000..4d14319b Binary files /dev/null and b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-editorial-light-chromium-darwin.png differ diff --git a/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-fluent-dark-chromium-darwin.png b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-fluent-dark-chromium-darwin.png new file mode 100644 index 00000000..dca325f6 Binary files /dev/null and b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-fluent-dark-chromium-darwin.png differ diff --git a/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-fluent-light-chromium-darwin.png b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-fluent-light-chromium-darwin.png new file mode 100644 index 00000000..8305d0db Binary files /dev/null and b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-fluent-light-chromium-darwin.png differ diff --git a/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-glassmorphism-dark-chromium-darwin.png b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-glassmorphism-dark-chromium-darwin.png new file mode 100644 index 00000000..dcc095d1 Binary files /dev/null and b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-glassmorphism-dark-chromium-darwin.png differ diff --git a/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-glassmorphism-light-chromium-darwin.png b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-glassmorphism-light-chromium-darwin.png new file mode 100644 index 00000000..1e4fe790 Binary files /dev/null and b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-glassmorphism-light-chromium-darwin.png differ diff --git a/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-liquid-dark-chromium-darwin.png b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-liquid-dark-chromium-darwin.png new file mode 100644 index 00000000..b138d0ea Binary files /dev/null and b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-liquid-dark-chromium-darwin.png differ diff --git a/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-liquid-light-chromium-darwin.png b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-liquid-light-chromium-darwin.png new file mode 100644 index 00000000..2e0fd515 Binary files /dev/null and b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-liquid-light-chromium-darwin.png differ diff --git a/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-material-dark-chromium-darwin.png b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-material-dark-chromium-darwin.png new file mode 100644 index 00000000..e4301f25 Binary files /dev/null and b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-material-dark-chromium-darwin.png differ diff --git a/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-material-light-chromium-darwin.png b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-material-light-chromium-darwin.png new file mode 100644 index 00000000..9ed12cea Binary files /dev/null and b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-material-light-chromium-darwin.png differ diff --git a/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-neumorphism-dark-chromium-darwin.png b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-neumorphism-dark-chromium-darwin.png new file mode 100644 index 00000000..94409d1e Binary files /dev/null and b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-neumorphism-dark-chromium-darwin.png differ diff --git a/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-neumorphism-light-chromium-darwin.png b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-neumorphism-light-chromium-darwin.png new file mode 100644 index 00000000..a21b5bc9 Binary files /dev/null and b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-neumorphism-light-chromium-darwin.png differ diff --git a/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-sketchbook-dark-chromium-darwin.png b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-sketchbook-dark-chromium-darwin.png new file mode 100644 index 00000000..cd97e436 Binary files /dev/null and b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-sketchbook-dark-chromium-darwin.png differ diff --git a/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-sketchbook-light-chromium-darwin.png b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-sketchbook-light-chromium-darwin.png new file mode 100644 index 00000000..cc0c18fa Binary files /dev/null and b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-sketchbook-light-chromium-darwin.png differ diff --git a/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-swiss-dark-chromium-darwin.png b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-swiss-dark-chromium-darwin.png new file mode 100644 index 00000000..99c1dba4 Binary files /dev/null and b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-swiss-dark-chromium-darwin.png differ diff --git a/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-swiss-light-chromium-darwin.png b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-swiss-light-chromium-darwin.png new file mode 100644 index 00000000..3316ba7f Binary files /dev/null and b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-swiss-light-chromium-darwin.png differ diff --git a/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-terminal-dark-chromium-darwin.png b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-terminal-dark-chromium-darwin.png new file mode 100644 index 00000000..0de8c4d7 Binary files /dev/null and b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-terminal-dark-chromium-darwin.png differ diff --git a/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-terminal-light-chromium-darwin.png b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-terminal-light-chromium-darwin.png new file mode 100644 index 00000000..5acde514 Binary files /dev/null and b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-terminal-light-chromium-darwin.png differ diff --git a/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-y2k-dark-chromium-darwin.png b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-y2k-dark-chromium-darwin.png new file mode 100644 index 00000000..efef00e0 Binary files /dev/null and b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-y2k-dark-chromium-darwin.png differ diff --git a/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-y2k-light-chromium-darwin.png b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-y2k-light-chromium-darwin.png new file mode 100644 index 00000000..966419e2 Binary files /dev/null and b/packages/ui/src/lib/theme/__screenshots__/DesignStyles.vrt.test.ts/style-y2k-light-chromium-darwin.png differ diff --git a/packages/ui/tests/demos-catalog.test.ts b/packages/ui/tests/demos-catalog.test.ts index 834b507d..35b68b6b 100644 --- a/packages/ui/tests/demos-catalog.test.ts +++ b/packages/ui/tests/demos-catalog.test.ts @@ -111,7 +111,7 @@ describe("demo meta sidecars", () => { expect(files[`en/e/${d.id}.txt`]).toContain(`When to use: ${d.use}`); expect(files[`en/p/${d.id}.txt`]).toContain(`Spec:\n- ${d.spec[0]}`); expect(files[`en/p/${d.id}.txt`]).not.toContain(".."); - }); + }, 60_000); }); describe("motion sidecar fields", () => { @@ -187,7 +187,7 @@ describe("motion sidecar fields", () => { expect(files["en/catalog.tsv"].split("\n")[0]).toBe( "id\tgroup\tpage\ttitle\tnames\ttags\tsummary" ); - }); + }, 60_000); }); describe("demo CSS isolation", () => { diff --git a/packages/ui/tests/route-impact.test.ts b/packages/ui/tests/route-impact.test.ts new file mode 100644 index 00000000..c1ab727e --- /dev/null +++ b/packages/ui/tests/route-impact.test.ts @@ -0,0 +1,192 @@ +import { describe, expect, it } from "bun:test"; +import { buildImpact, lockReachesRuntime, select } from "../../../scripts/route-impact"; + +// scripts/route-impact.ts decides which docs routes a diff has to retest. A route it misses is a hole +// nobody sees, so the contract is: map what it knows, and for anything else select EVERY route. +const impact = buildImpact(); +const pick = (...files: string[]) => select(files, impact); + +describe("route-impact", () => { + it("maps a demo and its MDX to the routes that render them", () => { + // The demo renders on its docs page and on its own gallery item. + expect(pick("site/src/demos/inputs/button/Sizes.svelte").routes).toEqual([ + "/docs/inputs/button/", + "/gallery/004/", + ]); + expect(pick("site/src/content/docs/inputs/button.mdx").routes).toEqual([ + "/docs/inputs/button/", + ]); + }); + + it("maps a component to every route whose demos use it", () => { + // Not Button: the site chrome renders it on every page, which is the next test. + expect(impact.globalFolders.has("Avatar")).toBe(false); + const s = pick("packages/ui/src/lib/components/Avatar/Avatar.svelte"); + expect(s.all).toBe(false); + expect(s.routes).toContain("/docs/display/avatar/"); + expect([...s.folders]).toEqual(["Avatar"]); + }); + + it("maps a subpath barrel to the routes that import it", () => { + const s = pick("packages/ui/src/lib/charts/theme.ts"); + expect(s.all).toBe(false); + expect(s.routes).toContain("/docs/charts/"); + }); + + it.each([ + ["a stylesheet", "packages/ui/src/lib/styles/base.css"], + ["a token", "packages/ui/src/lib/tokens/colors.ts"], + ["the theme", "packages/ui/src/lib/theme/index.ts"], + ["a primitive", "packages/ui/src/lib/primitives/presence.svelte.ts"], + ["the site layout", "site/src/layouts/Base.astro"], + ["the route harness", "site/tests/routes.spec.ts"], + ["the lockfile", "bun.lock"], + ])("%s selects every route", (_what, file) => { + const s = pick(file); + expect(s.all).toBe(true); + expect(s.reason).toContain(file); + }); + + it("selects every route for a file it does not know, and names it", () => { + const s = pick("site/public/some-new-asset.svg"); + expect(s.all).toBe(true); + expect(s.reason).toContain("site/public/some-new-asset.svg"); + expect(pick("packages/ui/src/lib/new-top-level-module.ts").all).toBe(true); + expect(pick("site/src/templates/_shared/brand-new.ts").all).toBe(true); + }); + + it("selects every route for a component the site shell renders on every page", () => { + // Which components the shell renders changes with the site (#556 moved Button out of it): read it + // from the map rather than naming one. + expect(impact.globalFolders.size).toBeGreaterThan(0); + for (const folder of impact.globalFolders) { + const s = pick(`packages/ui/src/lib/components/${folder}/${folder}.svelte`); + expect(s.all).toBe(true); + expect(s.reason).toContain("every page"); + } + }); + + it("ignores files that cannot change a rendered route", () => { + for (const f of [ + "README.md", + "AGENTS.md", + ".changeset/brave-lions-run.md", + "scripts/release.ts", + "packages/ui/tests/a11y.ts", + "packages/ui/src/lib/components/Button/Button.svelte.test.ts", + "packages/ui/src/lib/components/Button/Button.vrt.test.ts", + "site/tests/dialog-modal.spec.ts", + "site/functions/changelog.ts", + ]) { + const s = pick(f); + expect(s.all).toBe(false); + expect(s.routes).toEqual([]); + } + }); + + it("a test or harness in a shared directory is not itself shared", () => { + for (const f of [ + "packages/ui/src/lib/actions/scroll-focusable.svelte.test.ts", + "packages/ui/src/lib/actions/ScrollFocusableHarness.test.svelte", + "packages/ui/src/lib/styles/default-style.svelte.test.ts", + "packages/ui/src/lib/stores/color-scheme.svelte.test.ts", + ]) { + const s = pick(f); + expect(s.all).toBe(false); + expect(s.routes).toEqual([]); + } + }); + + it("a changed test file next to a component still records the component folder for the visual run", () => { + const s = pick("packages/ui/src/lib/components/Button/Button.vrt.test.ts"); + expect([...s.folders]).toEqual(["Button"]); + }); + + // The gallery has one page per item (`/gallery//`, ~400). Every page is the same viewer chrome + // around a different demo, so the map must say which items a diff touches, not "all" or "none". + describe("gallery items", () => { + const items = (r: string[]) => r.filter((x) => /^\/gallery\/[^/]+\/$/.test(x)); + + it("a demo selects its own item page and its docs route, not the other items", () => { + const s = pick("site/src/demos/inputs/button/Sizes.svelte"); + expect(s.routes).toContain("/docs/inputs/button/"); + expect(items(s.routes)).toEqual(["/gallery/004/"]); + }); + + it("the gallery routes are real routes, never the template literal", () => { + const s = pick("site/src/components/gallery/GalleryViewer.svelte"); + expect(s.routes.some((r) => r.includes("["))).toBe(false); + expect(items(s.routes).length).toBeGreaterThan(0); + }); + + it("a change to the viewer chrome selects a fixed handful of items, one per kind", () => { + for (const f of [ + "site/src/components/gallery/GalleryViewer.svelte", + "packages/ui/src/lib/components/PreviewStage/PreviewStage.svelte", + "packages/ui/src/lib/components/ToggleGroup/SegmentedControl.svelte", + ]) { + const got = items(pick(f).routes); + expect(got.length).toBeGreaterThanOrEqual(3); + expect(got.length).toBeLessThanOrEqual(8); + // A static demo, a motion demo and a whole page. + expect(got.some((r) => r.startsWith("/gallery/page-"))).toBe(true); + expect(got.some((r) => /^\/gallery\/\d{3}\/$/.test(r))).toBe(true); + } + }); + + it("a component selects the items whose demo uses it, capped at the representatives", () => { + const got = items(pick("packages/ui/src/lib/components/Avatar/Avatar.svelte").routes); + expect(got.length).toBeGreaterThan(0); + expect(got.length).toBeLessThanOrEqual(8); + }); + }); + + // bun.lock is shared by every page because a runtime dependency can change what a page renders. A + // lock change that only touches a workspace's devDependencies (a test tool) cannot. + describe("lockfile", () => { + const lock = (dev: string[], run = ['"svelte": "5.57.2"']) => `{ + "workspaces": { + "site": { + "name": "site", + "dependencies": { + ${run.join(",\n ")}, + }, + "devDependencies": { + ${dev.join(",\n ")}, + }, + }, + }, + "packages": { + "svelte": ["svelte@5.57.2", "", {}, "sha-a"], + }, +} +`; + const before = lock(['"typescript": "6.0.3"']); + + it("a devDependency added to a workspace does not reach a page", () => { + const after = lock(['"happy-dom": "21.0.1"', '"typescript": "6.0.3"']); + expect(lockReachesRuntime(before, after)).toBe(false); + }); + + it("a runtime dependency added, bumped or removed does", () => { + expect( + lockReachesRuntime(before, lock(['"typescript": "6.0.3"'], ['"svelte": "5.58.0"'])) + ).toBe(true); + expect( + lockReachesRuntime( + before, + lock(['"typescript": "6.0.3"'], ['"svelte": "5.57.2"', '"clsx": "2.1.1"']) + ) + ).toBe(true); + }); + + it("a changed resolved package entry does", () => { + const after = before.replace("sha-a", "sha-b"); + expect(lockReachesRuntime(before, after)).toBe(true); + }); + + it("an unreadable lock counts as reaching (fail safe)", () => { + expect(lockReachesRuntime(before, "not a lockfile")).toBe(true); + }); + }); +}); diff --git a/packages/ui/tests/settled.ts b/packages/ui/tests/settled.ts index aa43180a..e5088fa5 100644 --- a/packages/ui/tests/settled.ts +++ b/packages/ui/tests/settled.ts @@ -1,19 +1,5 @@ -import { expect } from "vitest"; - -/** Resolves once no CSS animation/transition is running (overlays fade in; axe must see final colours). Polls playState: `Animation.finished` can hang on already-finished animations. */ -export async function settled(): Promise { - await new Promise((r) => requestAnimationFrame(() => r(null))); - await expect - .poll( - () => - document - .getAnimations() - // Spinners, shimmers and indeterminate bars loop forever: they have no final state. - .filter((a) => a.playState === "running" && a.effect?.getTiming().iterations !== Infinity) - .length - ) - .toBe(0); -} +// The public helper: one implementation for the suite and for apps (`@codeatlasdev/ui/testing`). +export { settled } from "../src/lib/testing"; /** * Test-level budget for multi-step keyboard/typing flows. Every Playwright step (fill, key press, diff --git a/packages/ui/tests/setup.browser.ts b/packages/ui/tests/setup.browser.ts index d600ef0c..729fdd0c 100644 --- a/packages/ui/tests/setup.browser.ts +++ b/packages/ui/tests/setup.browser.ts @@ -19,7 +19,15 @@ beforeEach(async () => { // The Playwright pointer is page state: a test that hovers or clicks leaves it where it ended, and // the next test renders at the top-left, so `:hover` rules (filled Button fill, tooltips that open // on hover) would start active and results would depend on file order (#405). Park it outside the -// viewport before every test; a test that needs a pointer moves it itself. +// viewport before a test; a test that needs a pointer moves it itself. The CDP round trip costs +// ~17 ms and almost no test moves the pointer, so it is only paid when a pointer event was seen since +// the last park. A new file starts "moved": the previous file may have left the pointer anywhere. +let pointerMoved = true; +for (const type of ["pointermove", "pointerdown", "pointerup", "mousemove"]) + window.addEventListener(type, () => (pointerMoved = true), { capture: true, passive: true }); beforeEach(async () => { + if (!pointerMoved) return; await cdp().send("Input.dispatchMouseEvent", { type: "mouseMoved", x: -1, y: -1 }); + // the park itself fires no event in the page (it is outside the viewport), so this stays false + pointerMoved = false; }); diff --git a/packages/ui/tests/test-graph.test.ts b/packages/ui/tests/test-graph.test.ts new file mode 100644 index 00000000..905fd75f --- /dev/null +++ b/packages/ui/tests/test-graph.test.ts @@ -0,0 +1,115 @@ +import { describe, expect, it } from "bun:test"; +import { affected, buildGraph, isSharedComponent, landBudget } from "../../../scripts/test-graph"; + +// scripts/test-graph.ts selects component tests by a static import graph, because Vitest's `--changed` +// selects every file in browser mode. Too wide a selection defeats it; too narrow hides a regression. +const graph = buildGraph(); +const pick = (...changed: string[]) => [ + ...affected(graph, [...graph.tests.browser, ...graph.tests.vrt], changed).keys(), +]; +const dir = "packages/ui/src/lib/components"; + +describe("test-graph", () => { + it("finds every test file", () => { + expect(graph.tests.browser.length).toBeGreaterThan(150); + expect(graph.tests.vrt.length).toBeGreaterThan(60); + }); + + it("a component reaches its own tests and its harnesses, and nothing unrelated", () => { + const hit = pick(`${dir}/Avatar/Avatar.svelte`); + expect(hit).toContain(`${dir}/Avatar/Avatar.svelte.test.ts`); + expect(hit).toContain(`${dir}/Avatar/Avatar.vrt.test.ts`); + expect(hit.some((t) => t.includes("/CommandPalette/"))).toBe(false); + expect(hit.length).toBeLessThan(25); + }); + + it("reaches the tests of a component that renders the changed one", () => { + // Button is rendered by many components; a change must reach their tests too + const hit = pick(`${dir}/Button/Button.svelte`); + expect(hit).toContain(`${dir}/Button/Button.svelte.test.ts`); + expect(hit.length).toBeGreaterThan(20); + }); + + it("a stylesheet reaches the component that imports it", () => { + const hit = pick(`${dir}/Dialog/dialog.css`); + expect(hit).toContain(`${dir}/Dialog/Dialog.svelte.test.ts`); + }); + + it("a harness reaches only the tests that import it", () => { + const hit = pick(`${dir}/Tabs/TabsMatrix.test.svelte`); + expect(hit).toContain(`${dir}/Tabs/Tabs.vrt.test.ts`); + expect(hit.length).toBeLessThan(5); + }); + + it("a changed screenshot reaches the test that took it", () => { + const hit = pick( + `${dir}/Tabs/__screenshots__/Tabs.vrt.test.ts/Tabs-dark-glass-chromium-darwin.png` + ); + expect(hit).toEqual([`${dir}/Tabs/Tabs.vrt.test.ts`]); + }); + + it("a changed test file reaches itself", () => { + expect(pick(`${dir}/Tabs/Tabs.svelte.test.ts`)).toEqual([`${dir}/Tabs/Tabs.svelte.test.ts`]); + }); + + it("an import cycle does not hide a dependent", () => { + const cyc = { + deps: new Map([ + ["a.ts", new Set(["b.ts"])], + ["b.ts", new Set(["a.ts", "c.ts"])], + ["t.svelte.test.ts", new Set(["a.ts"])], + ]), + tests: { browser: ["t.svelte.test.ts"], vrt: [], ssr: [] }, + }; + expect([...affected(cyc, cyc.tests.browser, ["c.ts"]).keys()]).toEqual(["t.svelte.test.ts"]); + }); + + it("keeps the barrel edge when a name cannot be traced, so the selection only grows", () => { + // the package barrel is imported by name from a test or component: those names resolve to files + expect(graph.deps.get("packages/ui/src/lib/index.ts")?.size ?? 0).toBeGreaterThan(50); + }); +}); + +// `issue-land.sh` picks the land budget from this: a shared change runs every component test and every route +// (360 s, no screenshots), anything else the affected ones plus the canaries (180 s). +describe("landBudget", () => { + it("a component edit is under the 180 s budget", () => { + expect(landBudget([`${dir}/Avatar/Avatar.svelte`])).toBe("land"); + expect(landBudget([`${dir}/Avatar/Avatar.svelte.test.ts`, "AGENTS.md"])).toBe("land"); + expect(landBudget([])).toBe("land"); + }); + + it.each([ + "packages/ui/src/lib/styles/base.css", + "packages/ui/src/lib/tokens/colors.ts", + "packages/ui/src/lib/theme/index.ts", + "packages/ui/src/lib/primitives/presence.svelte.ts", + "packages/ui/tests/setup.browser.ts", + "packages/ui/vite.config.ts", + "site/src/layouts/Base.astro", + ])("%s is shared: the 360 s budget", (file) => { + expect(landBudget([file])).toBe("landShared"); + }); + + it("one shared file among ordinary ones is enough, and a file the route map does not know is shared", () => { + expect(landBudget([`${dir}/Avatar/Avatar.svelte`, "packages/ui/src/lib/styles/base.css"])).toBe( + "landShared" + ); + expect(landBudget(["site/public/some-new-asset.svg"])).toBe("landShared"); + }); + + it("isSharedComponent only flags what every component test renders through", () => { + expect(isSharedComponent("packages/ui/src/lib/styles/base.css")).toBe(true); + expect(isSharedComponent(`${dir}/Avatar/Avatar.svelte`)).toBe(false); + }); + + it("a test or harness in a shared directory is not itself shared", () => { + for (const f of [ + "packages/ui/src/lib/actions/scroll-focusable.svelte.test.ts", + "packages/ui/src/lib/actions/ScrollFocusableHarness.test.svelte", + "packages/ui/src/lib/styles/default-style.svelte.test.ts", + ]) + expect(isSharedComponent(f)).toBe(false); + expect(isSharedComponent("packages/ui/src/lib/actions/scroll-focusable.ts")).toBe(true); + }); +}); diff --git a/packages/ui/tests/test-run-budget.test.ts b/packages/ui/tests/test-run-budget.test.ts new file mode 100644 index 00000000..04296b59 --- /dev/null +++ b/packages/ui/tests/test-run-budget.test.ts @@ -0,0 +1,68 @@ +import { afterAll, describe, expect, it, spyOn } from "bun:test"; +import { writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { BUDGETS, report, slowest } from "../../../scripts/test-run"; +import { cleanTmp, tmpDir } from "./tmp"; + +// scripts/test-run.ts gives each command a wall-time budget: over it the run FAILS and prints the +// slowest tests, so slowness is a regression like any other. A budget is raised only in its own commit. +afterAll(cleanTmp); + +const timings = (rows: { name: string; ms: number }[]) => { + const file = join(tmpDir("ca-budget-"), "t.jsonl"); + writeFileSync(file, `${rows.map((r) => JSON.stringify(r)).join("\n")}\ngarbage line\n`); + return file; +}; + +describe("test-run budgets", () => { + it("the budgets are the agreed ones", () => { + expect(BUDGETS).toEqual({ + changed: 60, + validate: 90, + land: 180, + landShared: 360, + full: 300, + vrt: 180, + }); + }); + + it("slowest returns the n slowest, ignores unreadable lines", () => { + const file = timings(Array.from({ length: 15 }, (_, i) => ({ name: `t${i}`, ms: i * 100 }))); + const top = slowest(file, 10); + expect(top).toHaveLength(10); + expect(top[0]).toEqual({ name: "t14", ms: 1400 }); + expect(top[9].name).toBe("t5"); + expect(slowest(join(tmpDir("ca-budget-"), "missing.jsonl"))).toEqual([]); + }); + + it("within budget passes quietly, over budget fails and lists the slowest tests", () => { + const err = spyOn(console, "error").mockImplementation(() => {}); + try { + const file = timings([ + { name: "slow one", ms: 9000 }, + { name: "fast one", ms: 5 }, + ]); + expect(report("changed", 12, file, 3, 12)).toBe(0); + expect(err.mock.calls.flat().join("\n")).toContain("within budget: changed 12.0s of 60s"); + err.mockClear(); + + expect(report("changed", 61, file, 3, 12)).toBe(1); + const out = err.mock.calls.flat().join("\n"); + expect(out).toContain("OVER BUDGET: changed 61.0s of 60s"); + expect(out).toContain("slow one"); + // the boundary: exactly on budget is within it + expect(report("validate", 90, file, 3, 12)).toBe(0); + expect(report("full", 300.1, file, 3, 12)).toBe(1); + // no budget, no verdict + expect(report(undefined, 9999, file, 3, 12)).toBe(0); + + // an oversubscribed machine: reported loudly, not failed + err.mockClear(); + expect(report("changed", 61, file, 40, 12)).toBe(0); + expect(err.mock.calls.flat().join("\n")).toContain("OVER BUDGET, NOT ENFORCED"); + expect(report("changed", 61, file, 12, 12)).toBe(1); + } finally { + err.mockRestore(); + } + }); +}); diff --git a/packages/ui/tests/test-run-lock.test.ts b/packages/ui/tests/test-run-lock.test.ts new file mode 100644 index 00000000..ffce04b5 --- /dev/null +++ b/packages/ui/tests/test-run-lock.test.ts @@ -0,0 +1,106 @@ +import { afterAll, describe, expect, it } from "bun:test"; +import { existsSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { cleanTmp, tmpDir } from "./tmp"; + +// scripts/test-run.ts keeps one suite at a time per machine: a second run waits and says who holds +// the lock instead of starting more browsers on top of the first. These tests drive the real CLI. +const SCRIPT = join(import.meta.dir, "../../../scripts/test-run.ts"); +afterAll(cleanTmp); + +// Every process a test starts is killed when the file ends, pass or fail: a failed assertion would +// otherwise leave a holder running (its `sh` loop never sees the marker file). +const started: ReturnType[] = []; +afterAll(() => { + for (const p of started) p.kill(); +}); + +// `bun run validate` runs this suite under test-run.ts itself, which exports the markers of ITS lock and +// timings file. The CLIs started here must behave like a top-level run: inheriting CA_TEST_LOCK_HELD makes +// them reuse that lock instead of waiting on the one under test. +const { CA_TEST_LOCK_HELD, CA_TEST_TIMINGS, CA_TEST_RUN, ...inherited } = process.env; + +const run = (lockDir: string, args: string[]) => { + const p = Bun.spawn([process.execPath, SCRIPT, ...args], { + env: { ...inherited, CA_TEST_LOCK_DIR: lockDir }, + stdout: "pipe", + stderr: "pipe", + }); + started.push(p); + return p; +}; + +const until = async (what: string, ok: () => boolean, ms = 10_000) => { + const end = Date.now() + ms; + while (!ok()) { + if (Date.now() > end) throw new Error(`timed out waiting for ${what}`); + await Bun.sleep(25); + } +}; + +describe("test-run lock", () => { + it("a second run waits for the first and names the holder", async () => { + const dir = join(tmpDir("ca-lock-"), "lock"); + const marker = join(tmpDir("ca-lock-m-"), "go"); + // First run: holds the lock until the marker file appears. + const first = run(dir, [ + "sh", + "-c", + `while [ ! -f ${JSON.stringify(marker)} ]; do sleep 0.05; done`, + ]); + await until("first run to take the lock", () => existsSync(join(dir, "holder.json"))); + + const second = run(dir, ["sh", "-c", "echo second-ran"]); + // The signal that it is waiting is the message itself: read stderr until it appears. + let err = ""; + const reader = second.stderr.getReader(); + while (!err.includes("waiting for the test lock held by")) { + const { value, done } = await reader.read(); + if (done) break; + err += new TextDecoder().decode(value); + } + expect(err).toContain(`pid ${first.pid}`); + expect(second.exitCode).toBeNull(); // still waiting, the command has not run + + writeFileSync(marker, ""); + expect(await first.exited).toBe(0); + expect(await second.exited).toBe(0); + expect(await new Response(second.stdout).text()).toContain("second-ran"); + expect(existsSync(dir)).toBe(false); + }, 30_000); + + it("takes over a lock whose holder died", async () => { + const dir = join(tmpDir("ca-lock-"), "lock"); + // A pid that is gone: spawn and reap a short process. + const dead = Bun.spawn(["true"]); + await dead.exited; + Bun.spawnSync(["mkdir", "-p", dir]); + writeFileSync( + join(dir, "holder.json"), + JSON.stringify({ pid: dead.pid, started: new Date().toISOString(), cwd: "/x", cmd: ["gone"] }) + ); + const p = run(dir, ["sh", "-c", "echo took-over"]); + expect(await p.exited).toBe(0); + expect(await new Response(p.stdout).text()).toContain("took-over"); + }, 20_000); + + it("--status reports the holder, --reap never waits for it", async () => { + const dir = join(tmpDir("ca-lock-"), "lock"); + const marker = join(tmpDir("ca-lock-m-"), "go"); + const first = run(dir, [ + "sh", + "-c", + `while [ ! -f ${JSON.stringify(marker)} ]; do sleep 0.05; done`, + ]); + await until("first run to take the lock", () => existsSync(join(dir, "holder.json"))); + + const status = run(dir, ["--status"]); + await status.exited; + expect(await new Response(status.stdout).text()).toContain(`test lock: pid ${first.pid}`); + const reap = run(dir, ["--reap"]); + expect(await reap.exited).toBe(0); + + writeFileSync(marker, ""); + await first.exited; + }, 20_000); +}); diff --git a/packages/ui/tests/timing-reporter.ts b/packages/ui/tests/timing-reporter.ts new file mode 100644 index 00000000..677053d5 --- /dev/null +++ b/packages/ui/tests/timing-reporter.ts @@ -0,0 +1,14 @@ +import { appendFileSync } from "node:fs"; +import { relative } from "node:path"; +import type { Reporter, TestCase } from "vitest/node"; + +/** Appends `{name, ms}` per test to `CA_TEST_TIMINGS` so scripts/test-run.ts can print the slowest ones. */ +export default class TimingReporter implements Reporter { + onTestCaseResult(testCase: TestCase) { + const file = process.env.CA_TEST_TIMINGS; + const ms = testCase.diagnostic()?.duration; + if (!file || ms === undefined) return; + const where = relative(process.cwd(), testCase.module.moduleId); + appendFileSync(file, `${JSON.stringify({ name: `${where} > ${testCase.fullName}`, ms })}\n`); + } +} diff --git a/packages/ui/vite.config.ts b/packages/ui/vite.config.ts index d076fa4d..46a828c6 100644 --- a/packages/ui/vite.config.ts +++ b/packages/ui/vite.config.ts @@ -1,8 +1,26 @@ +import { realpathSync } from "node:fs"; import { availableParallelism } from "node:os"; import { fileURLToPath } from "node:url"; import { svelte } from "@sveltejs/vite-plugin-svelte"; import { playwright } from "@vitest/browser-playwright"; +import { searchForWorkspaceRoot } from "vite"; import { defaultExclude, defineConfig } from "vitest/config"; +import TimingReporter from "./tests/timing-reporter.ts"; + +// The self-hosted typefaces of the screenshot setup (tests/setup.vrt.ts). A package manager that links +// `node_modules` into a store outside the workspace (bun's global cache) puts their .woff2 files outside +// the directories Vite serves, and every screenshot test then fails with a NetworkError: allow them. +const fontDirs = ["inter", "jetbrains-mono"].flatMap((name) => { + try { + return [ + realpathSync( + fileURLToPath(new URL(`./node_modules/@fontsource-variable/${name}`, import.meta.url)) + ), + ]; + } catch { + return []; + } +}); const browserPattern = "src/**/*.svelte.test.ts"; const vrtPattern = "src/**/*.vrt.test.ts"; @@ -25,6 +43,7 @@ const browserBase = (channel?: "chromium") => ({ export default defineConfig({ plugins: [svelte()], + server: { fs: { allow: [searchForWorkspaceRoot(process.cwd()), ...fontDirs] } }, // Pre-bundle lazily imported deps so the first browser run doesn't trigger an optimizer reload. optimizeDeps: { include: ["echarts", "@tanstack/svelte-table", "shiki", "axe-core"] }, test: { @@ -32,8 +51,8 @@ export default defineConfig({ // Locally: a quarter of the cores (each worker is a browser tab in one Chromium). CI keeps the default. maxWorkers: process.env.CI ? undefined : Math.max(2, Math.floor(availableParallelism() / 4)), reporters: process.env.CI - ? ["default", "github-actions", ["html", { singleFile: true }]] - : ["default"], + ? ["default", "github-actions", ["html", { singleFile: true }], new TimingReporter()] + : ["default", new TimingReporter()], projects: [ { // SvelteKit-only modules used by kit/*.svelte (`$app/navigation`, `$app/state`); tests override it with vi.mock. @@ -76,9 +95,11 @@ export default defineConfig({ test: { name: "vrt", include: [vrtPattern], - // Baselines are rendered only on CI (ubuntu-24.04). An empty `include` would fall - // back to Vitest's defaults, so exclude everything elsewhere instead. - exclude: process.platform === "linux" ? defaultExclude : ["**/*"], + // Baselines are rendered natively and carry the platform in their name + // (`Name-dark-glass-chromium-darwin.png`), so a platform is only ever compared with its own. + // An empty `include` would fall back to Vitest's defaults, so exclude everything on a + // platform without baselines instead. + exclude: ["darwin", "linux"].includes(process.platform) ? defaultExclude : ["**/*"], setupFiles: ["./tests/setup.browser.ts", "./tests/setup.vrt.ts"], browser: { ...browserBase("chromium"), diff --git a/scripts/issue-land.sh b/scripts/issue-land.sh index ac99045a..0edb2cf0 100755 --- a/scripts/issue-land.sh +++ b/scripts/issue-land.sh @@ -1,6 +1,6 @@ #!/usr/bin/env sh # Land an issue PR: rebase its worktree on origin/main, regenerate generated files, validate, -# check the visual baselines locally in Docker, mark ready, squash-merge, clean up. +# run the land gate (affected tests, canaries and changed screenshots, 3 min budget; validate runs on the push), mark ready, squash-merge, clean up. # sh scripts/issue-land.sh set -e N="$1" @@ -43,13 +43,19 @@ for f in $(git status --porcelain --untracked-files=no | awk '{print $2}'); do done git diff --cached --quiet || git commit -qm "chore: regenerate lockfile and manifest after rebase" [ -z "$(git status --porcelain --untracked-files=no)" ] || { git status --short; echo "uncommitted changes in $WT" >&2; exit 1; } -bun run validate +# Budgeted gate, chosen by what the diff touches: the tests and screenshots it affects plus the route canaries +# (`land`, 3 minutes), or, when a SHARED file changed (tokens, styles, theme, primitives, a test setup file), +# every component test and every route without screenshots (`landShared`, 6 minutes). The screenshots of a +# shared change run at release (`bun run test:vrt`) and on demand. `validate` (90 s) runs on the push below, so +# it is not run twice. The release gate is `test:full` plus `test:vrt`. +BUDGET=$(bun scripts/test-changed.ts --land --print-budget) +bun scripts/test-run.ts --budget="$BUDGET" bun scripts/test-changed.ts --land git push -q --force-with-lease gh pr ready "$PR" >/dev/null 2>&1 || true -# CI is paused (ci.yml disabled, see AGENTS.md): validate above plus the local visual check are the gate. -# A mismatch means the change moved pixels: run `bun run test:vrt:update` in $WT, review the images, -# commit them and rerun this script. -sh "$WT/scripts/vrt-docker.sh" --check +# CI is paused (ci.yml disabled, see AGENTS.md): the budgeted gate above and `validate` on the push are the +# gate. Unless a shared file changed, the gate includes the screenshots of the changed components and pages, +# rendered natively. A mismatch means the change moved pixels: run `bun run test:vrt:update` in $WT, review the images, commit them and +# rerun this script. # No --delete-branch: gh would switch this worktree to main (checked out in $ROOT) and delete the # local branch (checked out here), failing after the merge (#103). `issue.ts done` removes both. # GitHub deletes the remote branch on merge (repo setting); the API call covers it being turned off. diff --git a/scripts/release.ts b/scripts/release.ts index 6ccaa34e..f92c71ca 100644 --- a/scripts/release.ts +++ b/scripts/release.ts @@ -98,6 +98,19 @@ if (sh(["git", "rev-parse", "HEAD"]) !== sh(["git", "rev-parse", "origin/main"]) fail("HEAD differs from origin/main: pull first"); } +// The whole suite runs before anything is versioned or tagged, as two commands so each stays under its own +// budget (5 minutes): `test:full` (unit, every component test, every route, fixtures) and `test:vrt` (every +// component and site screenshot). A red run stops here with the tree untouched. `--dry-run` only reports. +if (!dry) + for (const script of ["test:full", "test:vrt"]) { + console.log(`▶ bun run ${script}`); + if ( + Bun.spawnSync(["bun", "run", script], { stdio: ["inherit", "inherit", "inherit"] }) + .exitCode !== 0 + ) + fail(`bun run ${script} failed: nothing was versioned or tagged`); + } + const pending = readdirSync(".changeset").filter((f) => f.endsWith(".md") && f !== "README.md"); const current = `${PREFIX}${version()}`; if (pending.length === 0) { diff --git a/scripts/route-impact.ts b/scripts/route-impact.ts new file mode 100644 index 00000000..84c9cedd --- /dev/null +++ b/scripts/route-impact.ts @@ -0,0 +1,400 @@ +/** + * Which docs-site routes does a source file affect? Used by the Playwright route spec (viewport + * sweep only on routes that render layout primitives or changed) and by `bun run test:changed`. + * + * The map is derived from source, never hand-kept: + * route <- docs MDX (`site/src/content/docs/.mdx` -> `/docs//`) or an Astro page + * MDX <- imports demos (`~/demos///.svelte`) + * demo <- imports symbols from `@codeatlasdev/ui[/subpath]` + * symbol <- exported from a component folder (`packages/ui/src/lib/index.ts` and subpath barrels) + * Anything that every page depends on (styles, tokens, theme, stores, the shared layout) selects all routes. + */ +import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; +import { dirname, join, relative } from "node:path"; +import { fileURLToPath } from "node:url"; + +export const ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); +const SITE = join(ROOT, "site/src"); +const LIB = join(ROOT, "packages/ui/src/lib"); + +/** Components whose job is layout: a page that renders one is audited at every in-between width. */ +export const LAYOUT_PRIMITIVES = new Set([ + "Stack", + "Flex", + "Grid", + "Box", + "Container", + "Section", + "PageShell", + "PageFrame", + "AppShellLayout", + "AppList", + "AppForm", + "AppSettings", + "AuthLayout", + "MobilePane", + "StickyActions", + "ResizablePanels", + "KpiGrid", + "CardGrid", + "MediaGrid", + "FormSection", + "DocsShell", +]); + +/** Changes under these paths can alter every page: select all routes. */ +const GLOBAL = [ + /^packages\/ui\/src\/lib\/(styles|tokens|theme|stores|utils|actions|primitives)\//, + /^packages\/ui\/src\/lib\/(index|theme-script|vite-plugin|astro|brand|types)\.ts$/, + /^packages\/ui\/package\.json$/, + /^site\/(astro\.config\.mjs|package\.json|svelte\.config\.js|playwright\.config\.ts)$/, + /^site\/src\/(layouts|styles|lib|theme\.ts|content\.config\.ts|components\/(docs|site))\//, + /^site\/src\/(theme|content\.config)\.ts$/, + /^site\/tests\/(fixtures|routes|routes\.spec|routes\.axe\.spec|layout-audit|dist-html\.spec|timing-reporter|screenshot)\.(ts|css)$/, + /^scripts\/route-impact\.ts$/, + /^bun\.lock$/, +]; + +interface Lock { + workspaces?: Record>; + packages?: Record; +} + +/** + * Does the change from lockfile text `before` to `after` reach a page? `bun.lock` is in GLOBAL because a + * runtime dependency can change what every page renders; a change that only adds, bumps or drops a + * workspace's `devDependencies` (a test tool) and leaves every other entry as it was cannot. Anything this + * cannot read counts as reaching: a wrong "no" is a hole, a wrong "yes" is only a full sweep. + */ +export function lockReachesRuntime(before: string, after: string): boolean { + const parse = (text: string): Lock | undefined => { + try { + // bun.lock is JSON with trailing commas. + return JSON.parse(text.replace(/,(\s*[}\]])/g, "$1")) as Lock; + } catch { + return undefined; + } + }; + const a = parse(before); + const b = parse(after); + if (!a?.workspaces || !b?.workspaces) return true; + const strip = (l: Lock) => + JSON.stringify( + Object.fromEntries( + Object.entries(l.workspaces ?? {}).map(([ws, def]) => [ + ws, + Object.fromEntries(Object.entries(def).filter(([k]) => k !== "devDependencies")), + ]) + ) + ); + if (strip(a) !== strip(b)) return true; + // Resolved packages: nothing may change or go away (a bumped runtime dependency changes its entry); + // a dev tool only adds entries. + const pa = a.packages ?? {}; + const pb = b.packages ?? {}; + return Object.entries(pa).some( + ([name, entry]) => JSON.stringify(pb[name]) !== JSON.stringify(entry) + ); +} + +/** + * Files that cannot change what a docs route renders or how it is checked: test files other than the + * route harness, tooling, docs, CI and repo config. Everything else must map to a route (below) or it + * counts as UNKNOWN and selects every route: a missed route is a silent hole, a full sweep is only time. + */ +const NO_ROUTE_IMPACT = [ + /\.md$/, + /^(\.github|\.changeset|\.agents|\.vscode|docs)\//, + /^(llms\.txt|lefthook\.yml|biome\.json|bunfig\.toml|tsconfig\.json|\.editorconfig|\.gitignore|LICENSE|NOTICE|package\.json)$/, + /^scripts\//, + /^packages\/ui\/tests\//, + /^packages\/ui\/(vite\.config\.ts|svelte\.config\.js|tsconfig\.json|size-limit\.json|CHANGELOG\.md)$/, + /\.(svelte\.test|vrt\.test|ssr\.test)\.ts$|\.test\.svelte$|__screenshots__\//, + /^site\/functions\//, + /^site\/tests\//, +]; + +/** A test or harness: it runs in a test, it is never rendered by a page or shipped (also under shared dirs). */ +export const isTestFile = (f: string) => + /\.(svelte\.test|vrt\.test|ssr\.test)\.ts$|\.test\.svelte$|__screenshots__\//.test(f); + +const walk = (dir: string, ext: RegExp, out: string[] = []): string[] => { + if (!existsSync(dir)) return out; + for (const name of readdirSync(dir)) { + const full = join(dir, name); + if (statSync(full).isDirectory()) walk(full, ext, out); + else if (ext.test(name)) out.push(full); + } + return out; +}; +const rel = (p: string) => relative(ROOT, p); +const read = (p: string) => readFileSync(p, "utf8"); + +/** + * `import { A, B as C } from "@codeatlasdev/ui/charts"` -> `A`, `B` plus `@charts` (the subpath, so a + * change under `src/lib/charts/` finds its routes without a symbol table for every barrel). + */ +function uiSymbols(src: string): string[] { + const out: string[] = []; + for (const m of src.matchAll( + /import\s+(?:type\s+)?\{([^}]*)\}\s+from\s+["']@codeatlasdev\/ui(?:\/([a-z-]+))?[^"']*["']/g + )) { + if (m[2]) out.push(`@${m[2]}`); + for (const part of m[1].split(",")) { + const name = part + .trim() + .replace(/^type\s+/, "") + .split(/\s+as\s+/)[0] + .trim(); + if (name) out.push(name); + } + } + return out; +} + +/** exported name -> component folder (`components//`), from the package barrels. */ +function symbolFolders(): Map { + const map = new Map(); + const barrels = [join(LIB, "index.ts"), ...walk(LIB, /^index\.ts$/)]; + for (const file of new Set(barrels)) { + const src = read(file); + for (const m of src.matchAll(/export\s+(?:type\s+)?\{([^}]*)\}\s+from\s+["']([^"']+)["']/g)) { + const target = join(dirname(file), m[2]); + const folder = rel(target).match(/components\/([^/]+)\//)?.[1]; + if (!folder) continue; + for (const part of m[1].split(",")) { + const name = part + .trim() + .replace(/^type\s+/, "") + .split(/\s+as\s+/) + .pop() + ?.trim(); + if (name) map.set(name, folder); + } + } + } + return map; +} + +export interface RouteImpact { + /** route -> package symbols its demos/page use */ + symbols: Map>; + /** route -> repo-relative source files it is built from (MDX, demos, page, templates) */ + files: Map>; + /** symbol -> component folder */ + folders: Map; + /** component folders the site's own shell code (layouts, docs shell, site chrome) renders on every page */ + globalFolders: Set; +} + +const docRoute = (mdx: string) => + `/docs/${relative(join(SITE, "content/docs"), mdx) + .replace(/\.mdx$/, "") + .replace(/(^|\/)index$/, "")}/`.replace("//", "/"); + +/** Astro page -> route (`pages/templates/app/users/index.astro` -> `/templates/app/users/`). */ +const pageRoute = (page: string) => + `/${relative(join(SITE, "pages"), page) + .replace(/\.astro$/, "") + .replace(/(^|\/)index$/, "")}/`.replace("//", "/"); + +/** The numbered demos (`src/demos/catalog.ts`), the single source of the `#NNN` ids. */ +function demoCatalog(): { id: string; path: string }[] { + return [ + ...read(join(SITE, "demos/catalog.ts")).matchAll(/\{\s*id:\s*"(\d+)",\s*path:\s*"([^"]+)"/g), + ].map((m) => ({ id: m[1], path: m[2] })); +} + +/** Gallery page items (`page-*`): the slugs of `PAGE_SPECS` in `site/src/lib/gallery-items.ts`. */ +const GALLERY_PAGE_ITEMS: string[] = [ + ...read(join(SITE, "lib/gallery-items.ts")).matchAll(/^\t\{\s*\n\t\tslug: "([^"]+)"/gm), +].map((m) => `page-${m[1]}`); + +/** + * The item pages that stand for all of them when the shared viewer chrome (or a component the viewer renders) + * changes: every `/gallery//` is the same header, width bar, stage frame and pager around a demo, and a + * demo's own content is audited by its docs route and by its own item when it changes. One static demo, one + * motion demo, one demo taller than the stage (a scrolling frame), one narrow control row and two whole pages + * (an app page in an iframe, a plain template). + */ +export const GALLERY_REPRESENTATIVES = [ + "001", + "172", + "325", + "962", + "page-app-dashboard", + "page-auth-sign-in", +]; + +export function buildImpact(): RouteImpact { + const symbols = new Map>(); + const files = new Map>(); + const bucket = (map: Map>, route: string) => { + const set = map.get(route) ?? new Set(); + map.set(route, set); + return set; + }; + const add = (route: string, file: string, src: string) => { + bucket(files, route).add(rel(file)); + for (const s of uiSymbols(src)) bucket(symbols, route).add(s); + }; + const demoFile = (spec: string) => join(SITE, spec.replace(/^~\//, "")); + + for (const mdx of walk(join(SITE, "content/docs"), /\.mdx$/)) { + const route = docRoute(mdx); + const src = read(mdx); + add(route, mdx, src); + for (const m of src.matchAll(/from\s+["'](~\/demos\/[^"']+\.svelte)["']/g)) { + const file = demoFile(m[1]); + if (existsSync(file)) add(route, file, read(file)); + } + } + // Astro pages pull their islands (landing, theme, gallery, templates) in through relative `~/` imports. + const seen = new Set(); + const followImports = (route: string, file: string) => { + if (seen.has(`${route}|${file}`) || !existsSync(file)) return; + seen.add(`${route}|${file}`); + const src = read(file); + add(route, file, src); + for (const m of src.matchAll(/(?:from|import)\s+["'](~\/[^"']+\.(?:svelte|astro|ts))["']/g)) + if (!m[1].includes("/pages/")) followImports(route, demoFile(m[1])); + }; + for (const page of walk(join(SITE, "pages"), /\.astro$/)) { + if (page.includes("[...slug]") || page.includes("/llms/") || page.includes("/gallery/[id]")) + continue; + followImports(pageRoute(page), page); + } + // `/gallery//` is one real route per item (the template route is not one). An item is its demo plus + // the viewer chrome every item shares, so a demo selects exactly its item and the chrome selects the + // representatives (GALLERY_REPRESENTATIVES), never all ~400. + const viewer = join(SITE, "pages/gallery/[id].astro"); + for (const { id, path } of demoCatalog()) { + const route = `/gallery/${id}/`; + followImports(route, viewer); + const file = join(SITE, "demos", `${path}.svelte`); + if (existsSync(file)) add(route, file, read(file)); + } + for (const id of GALLERY_PAGE_ITEMS) followImports(`/gallery/${id}/`, viewer); + // A package component rendered by the site's own shell (not by a demo) is on every page. + const folders = symbolFolders(); + const globalFolders = new Set(); + const shell = [ + ...walk(join(SITE, "layouts"), /\.(astro|svelte|ts)$/), + ...walk(join(SITE, "lib"), /\.(astro|svelte|ts)$/), + ...walk(join(SITE, "components/docs"), /\.(astro|svelte|ts)$/), + ...walk(join(SITE, "components/site"), /\.(astro|svelte|ts)$/), + ]; + for (const file of shell) + for (const sym of uiSymbols(read(file))) { + const folder = folders.get(sym); + if (folder) globalFolders.add(folder); + } + return { symbols, files, folders, globalFolders }; +} + +const isItem = (route: string) => /^\/gallery\/[^/]+\/$/.test(route); + +/** + * A change that reaches the gallery through shared code (a component, the viewer chrome) selects the + * representative items, not every item: they are the same chrome around different demos. + */ +function representatives(routes: string[]): string[] { + const items = routes.filter(isItem); + const rest = routes.filter((r) => !isItem(r)); + return items.length ? [...rest, ...GALLERY_REPRESENTATIVES.map((id) => `/gallery/${id}/`)] : rest; +} + +/** Routes whose source is `file` (a docs MDX, a demo, a page, a template). A demo keeps its exact item. */ +function routesOfSite(impact: RouteImpact, file: string): string[] { + const routes = [...impact.files].filter(([, fs]) => fs.has(file)).map(([r]) => r); + return file.startsWith("site/src/demos/") ? routes : representatives(routes); +} + +/** Routes that use a symbol exported from `components//`. */ +function routesOfFolder(impact: RouteImpact, folder: string): string[] { + const names = new Set([...impact.folders].filter(([, f]) => f === folder).map(([n]) => n)); + return representatives( + [...impact.symbols].filter(([, s]) => [...s].some((n) => names.has(n))).map(([r]) => r) + ); +} + +export interface Selection { + /** every route: a changed file can alter every page, or the map does not know the file */ + all: boolean; + /** why `all` (empty otherwise): the file and the rule */ + reason: string; + routes: string[]; + /** why each route was chosen: route -> changed file */ + why: Map; + /** changed component folders (for the VRT selection) */ + folders: Set; +} + +/** + * Routes affected by `changed` (repo-relative paths). Fails safe: a file under a shared path, a + * component the site shell renders on every page, or a file the map does not recognise selects ALL + * routes, and says which file and why. + */ +export function select(changed: string[], impact = buildImpact()): Selection { + const why = new Map(); + const folders = new Set(); + const pick = (routes: string[], file: string) => { + for (const r of routes) if (!why.has(r)) why.set(r, file); + }; + const everything = (file: string, reason: string): Selection => ({ + all: true, + reason: `${file}: ${reason}`, + routes: [], + why: new Map(), + folders, + }); + for (const f of changed) { + if (isTestFile(f)) { + const own = f.match(/^packages\/ui\/src\/lib\/components\/([^/]+)\//); + if (own) folders.add(own[1]); + continue; + } + if (GLOBAL.some((re) => re.test(f))) return everything(f, "shared by every page"); + const comp = f.match(/^packages\/ui\/src\/lib\/components\/([^/]+)\//); + if (comp) { + folders.add(comp[1]); + if (NO_ROUTE_IMPACT.some((re) => re.test(f))) continue; + if (impact.globalFolders.has(comp[1])) + return everything(f, `${comp[1]} is rendered by the site shell on every page`); + pick(routesOfFolder(impact, comp[1]), f); + continue; + } + const sub = f.match(/^packages\/ui\/src\/lib\/(charts|map|forms|seo|kit|i18n|testing)\//); + if (sub) { + pick( + [...impact.symbols].filter(([, s]) => s.has(`@${sub[1]}`)).map(([r]) => r), + f + ); + continue; + } + if (NO_ROUTE_IMPACT.some((re) => re.test(f))) continue; + if (/^site\/src\//.test(f)) { + const routes = routesOfSite(impact, f); + if (!routes.length) return everything(f, "no route is built from this site file"); + pick(routes, f); + continue; + } + return everything(f, "not a file the route map knows"); + } + return { all: false, reason: "", routes: [...why.keys()].sort(), why, folders }; +} + +/** Routes the viewport sweep covers: layout primitives, templates/landing/theme (composed pages), changed. */ +export function sweepRoutes(all: string[], changed: string[] = []): Set { + const impact = buildImpact(); + const out = new Set(); + for (const r of all) { + const used = impact.symbols.get(r); + if (!r.startsWith("/docs/") || (used && [...used].some((s) => LAYOUT_PRIMITIVES.has(s)))) + out.add(r); + } + const sel = select(changed, impact); + if (sel.all) for (const r of all) out.add(r); + else for (const r of sel.routes) out.add(r); + return out; +} diff --git a/scripts/test-changed.ts b/scripts/test-changed.ts new file mode 100755 index 00000000..d36a4d82 --- /dev/null +++ b/scripts/test-changed.ts @@ -0,0 +1,254 @@ +#!/usr/bin/env bun +/** + * `bun run test` / `bun run test:changed`: run what the diff against origin/main affects, say what was + * selected and why, and stay inside the `changed` time budget (scripts/test-run.ts). `--land` is the + * same selection plus the route canaries, the gate `issue-land.sh` runs. `bun run test:full` runs the + * whole suite and `bun run test:vrt` every screenshot; both run at release. + * + * A SHARED change (a shared component file, or a route-wide one: styles, tokens, theme, primitives, the + * site layout, a test setup file, or a file the route map cannot place) runs every component test and, when + * it reaches the site, every route, and NO screenshots: those run at release (`test:vrt`) and on demand. + * `--print-budget` prints the budget this selection runs under (`land`, 180 s, or `landShared`, 360 s). + * + * unit `bun test --changed ` (module graph); + * components the component tests whose imports reach a changed file (scripts/test-graph.ts; Vitest's + * own `--changed` selects every file in browser mode), or EVERY component test when a + * shared file changed (styles, tokens, theme, stores, primitives, a test setup file: all + * of them render through it); + * visual component screenshots (`*.vrt.test.ts`) through the same module graph, and the site's visual + * project when a changed route is one of its five pages (landing, templates, theme); + * site the docs routes whose source changed (scripts/route-impact.ts), or EVERY route when a + * changed file is shared by all pages or unknown to the map; changed non-route Playwright + * specs run themselves. A dev run needs `site/dist` to be fresh (a build is ~75 s, over + * the 60 s budget) and says so when it is not; `--land` builds and adds the canaries below. + * + * `CA_TEST_BASE=` compares against another ref (`CA_TEST_BASE=HEAD`: only what is not committed yet). + * + * Steps run one after the other under scripts/test-run.ts (machine-wide lock, 25% browser cap). + * Extra args go to Vitest: `bun run test:changed -- --reporter=minimal`. + */ +import { spawnSync } from "node:child_process"; +import { existsSync, readFileSync, statSync } from "node:fs"; +import { join } from "node:path"; +import { lockReachesRuntime, ROOT, select } from "./route-impact.ts"; +import { affected, buildGraph, isSharedComponent, landBudget } from "./test-graph.ts"; + +if (!import.meta.main) throw new Error("scripts/test-changed.ts is a command, not a module"); + +/** One page per layout family, always retested at land: home, the biggest docs page, the app shell, an app template, the theme editor. */ +const CANARIES = [ + "/", + "/docs/getting-started/recipes/", + "/docs/layout/app-shell/", + "/templates/app/dashboard/", + "/theme/", +]; + +/** The routes `pages.visual.spec.ts` captures (keep in step with its PAGES). */ +const VISUAL_ROUTES = new Set([ + "/", + "/templates/", + "/templates/app/dashboard/", + "/templates/auth/sign-in/", + "/theme/", +]); + +/** A site spec that is not the route harness: it runs itself when it changed. */ +const ownSpec = (f: string) => + /^site\/tests\/[^/]+\.spec\.ts$/.test(f) && + !/\/(routes|routes\.axe|dist-html|header)\.spec\.ts$/.test(f); + +const land = process.argv.includes("--land"); +const git = (...args: string[]) => + spawnSync("git", args, { cwd: ROOT, encoding: "utf8" }).stdout.trim().split("\n").filter(Boolean); + +const base = process.env.CA_TEST_BASE + ? git("rev-parse", process.env.CA_TEST_BASE)[0] + : git("merge-base", "HEAD", "origin/main")[0]; +if (!base) { + console.error("test:changed: no base commit (run `git fetch origin main`, or set CA_TEST_BASE)"); + process.exit(2); +} +// A lockfile change that only touches a workspace's devDependencies cannot change a page or a component: +// it does not widen the run to every route and every component test (route-impact `lockReachesRuntime`). +const lockIsDevOnly = () => { + const before = spawnSync("git", ["show", `${base}:bun.lock`], { cwd: ROOT, encoding: "utf8" }); + return ( + before.status === 0 && + !lockReachesRuntime(before.stdout, readFileSync(join(ROOT, "bun.lock"), "utf8")) + ); +}; +const changed = [ + ...new Set([ + ...git("diff", "--name-only", base), + ...git("ls-files", "--others", "--exclude-standard"), + ]), +].filter((f) => f !== "bun.lock" || !lockIsDevOnly()); +const short = base.slice(0, 8); +if (!changed.length && !land) { + console.log(`test:changed: no changes against origin/main (${short}), nothing to run`); + process.exit(0); +} + +const sel = select(changed); +const sharedFile = changed.find(isSharedComponent); +const specs = changed.filter(ownSpec); +const routes = sel.all ? [] : [...new Set([...sel.routes, ...(land ? CANARIES : [])])].sort(); +const sweepAll = sel.all || (land && sharedFile !== undefined); +const siteRun = sweepAll || routes.length > 0 || specs.length > 0; +const visualRun = sel.all || sel.routes.some((r) => VISUAL_ROUTES.has(r)); +const routeRun = sweepAll || routes.length > 0; +// Shared: everything that can reach every test runs, and screenshots are left to `test:vrt`. +const shared = sel.all || sharedFile !== undefined; +if (process.argv.includes("--print-budget")) { + console.log(land ? landBudget(changed) : "changed"); + process.exit(0); +} + +console.log( + `test:changed${land ? " --land" : ""}: ${changed.length} changed file(s) against ${short}` +); +console.log(` unit bun test --changed ${short}`); +const graph = buildGraph(); +const pickTests = (candidates: string[]) => [...affected(graph, candidates, changed).keys()]; +const compTests = sharedFile ? graph.tests.browser : pickTests(graph.tests.browser); +const ssrTests = sharedFile ? graph.tests.ssr : pickTests(graph.tests.ssr); +const vrtTests = sharedFile ? graph.tests.vrt : pickTests(graph.tests.vrt); +console.log( + sharedFile + ? ` components ALL ${compTests.length}: ${sharedFile} is shared by every component test` + : ` components ${compTests.length} of ${graph.tests.browser.length} test files reach a changed file` +); +console.log( + shared + ? " visual none: a shared change runs every screenshot at release (bun run test:vrt)" + : ` visual ${vrtTests.length} of ${graph.tests.vrt.length} component screenshot files${visualRun ? "; site: landing, templates, theme" : ""}` +); +if (sel.all) console.log(` routes ALL: ${sel.reason}`); +else if (sweepAll) + console.log(` routes ALL: ${sharedFile} is shared by every component test`); +else if (routes.length) { + console.log( + ` routes ${routes.length}${land ? ` (incl. ${CANARIES.length} canaries)` : ""}:` + ); + for (const r of routes.slice(0, 10)) + console.log(` ${r} <- ${sel.why.get(r) ?? "canary"}`); + if (routes.length > 10) console.log(` … ${routes.length - 10} more`); +} else console.log(" routes none: no changed file reaches a docs route"); +if (specs.length) + console.log(` specs ${specs.map((f) => f.replace("site/tests/", "")).join(" ")}`); + +const run = (cmd: string[], env: Record = {}, cwd = ROOT) => + spawnSync(process.execPath, [join(ROOT, "scripts/test-run.ts"), ...cmd], { + cwd, + stdio: "inherit", + env: { ...process.env, ...env }, + }).status ?? 1; + +const failed: string[] = []; +let lap = performance.now(); +const step = (name: string, code: number) => { + const seconds = ((performance.now() - lap) / 1000).toFixed(1); + console.log(`test:changed: ${name} ${code === 0 ? "ok" : "FAILED"} (${seconds}s)`); + lap = performance.now(); + if (code !== 0) failed.push(name); +}; + +// Unit tests: only the files the diff reaches. `bun test` exits 1 when nothing matched, which is not a failure here. +const unit = run(["bun", "test", `--changed=${base}`]); +step("unit", unit === 1 && !changed.some((f) => /\.(ts|js|svelte)$/.test(f)) ? 0 : unit); + +const vitestExtra = process.argv.slice(2).filter((a) => a !== "--" && a !== "--land"); +const inPkg = (files: string[]) => files.map((f) => f.replace("packages/ui/", "")); +const pkgDir = join(ROOT, "packages/ui"); +if (compTests.length || ssrTests.length) + step( + "components", + run( + [ + "vitest", + "run", + ...(compTests.length ? ["--project", "browser"] : []), + ...(ssrTests.length ? ["--project", "ssr"] : []), + ...(sharedFile ? [] : inPkg([...compTests, ...ssrTests])), + ...vitestExtra, + ], + {}, + pkgDir + ) + ); + +// Screenshots of the changed components only (the platform's own baselines, rendered natively). +if (!shared && vrtTests.length) + step( + "visual (components)", + run( + [ + "vitest", + "run", + "--project", + "vrt", + "--maxWorkers=2", + ...(sharedFile ? [] : inPkg(vrtTests)), + ], + {}, + pkgDir + ) + ); + +if (siteRun) { + // The route specs read `site/dist`: rebuild only what is older than the diff. + const mtime = (p: string) => (existsSync(p) ? statSync(p).mtimeMs : 0); + const pkgDist = mtime(join(ROOT, "packages/ui/dist/index.js")); + const siteDist = mtime(join(ROOT, "site/dist/index.html")); + const newer = (re: RegExp, than: number) => + changed.some((f) => re.test(f) && mtime(join(ROOT, f)) > than); + const pkgStale = !pkgDist || newer(/^packages\/ui\/(src|scripts|package\.json)/, pkgDist); + const siteStale = + !siteDist || + pkgStale || + newer(/^site\/(src|public|astro\.config\.mjs|package\.json)/, siteDist); + // A build is ~75 s (package 17 s + site 58 s), more than the whole 60 s dev budget: only the land gate + // builds. A dev run uses `site/dist` when it is fresh, and says so when it cannot. + if ((pkgStale || siteStale) && !land) { + console.log( + "test:changed: site SKIPPED: site/dist is older than the diff. Run `bun run build` once and repeat, or let the land gate (`issue-land.sh`) run it." + ); + process.exit(failed.length ? 1 : 0); + } + if (pkgStale) step("build package", run(["bun", "run", "--cwd", "packages/ui", "build"])); + if (siteStale) step("build site", run(["bun", "run", "--cwd", "site", "build"])); + + const env = { + E2E_PREBUILT: "1", + ...(routeRun && !sweepAll ? { E2E_ROUTES: JSON.stringify(routes) } : {}), + // A full sweep at land audits the gallery through its representatives; `test:full` audits every item. + ...(land && sweepAll ? { E2E_GALLERY: "representatives" } : {}), + }; + const args = [ + "playwright", + "test", + ...(routeRun + ? [ + "--project=desktop", + "--project=mobile", + "--project=axe", + "routes.spec", + "routes.axe", + "dist-html", + ] + : ["--project=desktop", "--project=mobile"]), + ...specs.map((f) => f.replace("site/tests/", "")), + ]; + step("site", run(args, env, join(ROOT, "site"))); + if (!shared && visualRun) + step( + "visual (site)", + run( + ["playwright", "test", "--project=visual-desktop", "--project=visual-mobile"], + { E2E_PREBUILT: "1" }, + join(ROOT, "site") + ) + ); +} +process.exit(failed.length ? 1 : 0); diff --git a/scripts/test-graph.ts b/scripts/test-graph.ts new file mode 100644 index 00000000..ad95bc0a --- /dev/null +++ b/scripts/test-graph.ts @@ -0,0 +1,215 @@ +/** + * Which component tests does a changed file reach? A static import graph of `packages/ui/src` (and + * the test helpers), read from the sources: Vitest's own `--changed` cannot do this in browser mode + * (the graph of the test files is not populated before they run, so it selects all of them: measured, + * 198 of 198 files for a one-line edit to Avatar). + * + * test file -imports-> component / harness / helper -imports-> ... (relative imports, `@codeatlasdev/ui`) + * + * A package barrel (`src/lib/index.ts`, a folder `index.ts` that only re-exports) is flattened: + * `import { Avatar } from "../../index"` is an edge to `components/Avatar/Avatar.svelte`, not to the + * whole library, otherwise every test would depend on every component. Whatever the graph cannot + * resolve (`import * as`, a namespace barrel import, a bare `export *`) keeps the barrel edge, so the + * selection can only grow, never shrink. + */ +import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; +import { dirname, join, relative, resolve } from "node:path"; +import { isTestFile, ROOT, select } from "./route-impact.ts"; + +const PKG = join(ROOT, "packages/ui"); +const SRC = join(PKG, "src"); +const LIB_INDEX = join(SRC, "lib/index.ts"); +const EXT = ["", ".ts", ".js", ".svelte", ".svelte.ts", ".svelte.js", ".css", ".json"]; + +const walk = (dir: string, out: string[] = []): string[] => { + if (!existsSync(dir)) return out; + for (const name of readdirSync(dir)) { + if (name === "node_modules" || name === "__screenshots__" || name === "__traces__") continue; + const full = join(dir, name); + if (statSync(full).isDirectory()) walk(full, out); + else if (/\.(ts|js|svelte|css)$/.test(name)) out.push(full); + } + return out; +}; + +function resolveFile(from: string, spec: string): string | undefined { + if (/^@codeatlasdev\/ui(\/|$)/.test(spec)) return LIB_INDEX; + if (!spec.startsWith(".")) return undefined; // a package: not ours to track + const base = resolve(dirname(from), spec); + for (const candidate of [ + ...EXT.map((e) => base + e), + base.replace(/\.js$/, ".ts"), + base.replace(/\.js$/, ".svelte.ts"), + ...EXT.map((e) => join(base, `index${e || ".ts"}`)), + ]) + if (existsSync(candidate) && statSync(candidate).isFile()) return candidate; + return undefined; +} + +interface Edge { + spec: string; + names: string[] | "all"; +} + +/** `import {A, B as C} from "x"` → names A, B; `import X from`, `import * as`, `export *` → "all". */ +function edgesOf(src: string): Edge[] { + const out: Edge[] = []; + const stripped = src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1"); + for (const m of stripped.matchAll( + /(?:import|export)\s+(type\s+)?([^"'`;]*?)\s*from\s*["']([^"']+)["']/g + )) { + const clause = m[2].trim(); + const named = clause.match(/^\{([\s\S]*)\}$/); + out.push({ + spec: m[3], + names: named + ? named[1] + .split(",") + .map((p) => + p + .trim() + .replace(/^type\s+/, "") + .split(/\s+as\s+/)[0] + .trim() + ) + .filter(Boolean) + : "all", + }); + } + for (const m of stripped.matchAll(/(?:^|[^\w.])import\s*["']([^"']+)["']/g)) + out.push({ spec: m[1], names: "all" }); + for (const m of stripped.matchAll(/import\(\s*["']([^"']+)["']\s*\)/g)) + out.push({ spec: m[1], names: "all" }); + for (const m of stripped.matchAll(/@import\s+(?:url\()?["']([^"']+)["']/g)) + out.push({ spec: m[1], names: "all" }); + return out; +} + +export interface TestGraph { + /** file -> files it depends on */ + deps: Map>; + /** every `*.svelte.test.ts` / `*.vrt.test.ts` / `*.ssr.test.ts`, repo-relative */ + tests: { browser: string[]; vrt: string[]; ssr: string[] }; +} + +export function buildGraph(): TestGraph { + const files = [...walk(SRC), ...walk(join(PKG, "tests"))]; + const text = new Map(files.map((f) => [f, readFileSync(f, "utf8")])); + const edges = new Map(files.map((f) => [f, edgesOf(text.get(f) ?? "")])); + + // A barrel is a file made only of re-exports: name -> the file that defines it. + const barrel = new Map>(); + const isBarrel = (f: string) => { + if (!/(^|\/)index\.ts$/.test(f)) return false; + const body = (text.get(f) ?? "") + .replace(/\/\*[\s\S]*?\*\//g, "") + .replace(/\/\/.*$/gm, "") + .replace(/(?:export)\s+(?:type\s+)?\{[^}]*\}\s+from\s*["'][^"']+["'];?/g, "") + .replace(/export\s+\*\s+from\s*["'][^"']+["'];?/g, "") + .trim(); + return body === ""; + }; + const members = (f: string): Map => { + const hit = barrel.get(f); + if (hit) return hit; + const map = new Map(); + barrel.set(f, map); + for (const m of (text.get(f) ?? "").matchAll( + /export\s+(?:type\s+)?\{([^}]*)\}\s+from\s*["']([^"']+)["']/g + )) { + const target = resolveFile(f, m[2]); + if (!target) continue; + for (const part of m[1].split(",")) { + const exported = part + .trim() + .replace(/^type\s+/, "") + .split(/\s+as\s+/) + .pop() + ?.trim(); + if (exported) map.set(exported, isBarrel(target) ? "" : target); + } + } + return map; + }; + + const deps = new Map>(); + for (const f of files) { + const set = new Set(); + for (const e of edges.get(f) ?? []) { + const target = resolveFile(f, e.spec); + if (!target) continue; + if (isBarrel(target) && e.names !== "all") { + const m = members(target); + for (const name of e.names) { + const direct = m.get(name); + // a name the barrel table lacks (or defined through another barrel): keep the barrel edge + set.add(direct ? direct : target); + } + } else set.add(target); + } + deps.set(f, set); + } + + const rel = (f: string) => relative(ROOT, f); + const only = (re: RegExp) => + files + .filter((f) => re.test(f) && f.startsWith(SRC)) + .map(rel) + .sort(); + return { + deps: new Map([...deps].map(([f, d]) => [rel(f), new Set([...d].map(rel))])), + tests: { + browser: only(/\.svelte\.test\.ts$/), + vrt: only(/\.vrt\.test\.ts$/), + ssr: only(/\.ssr\.test\.ts$/), + }, + }; +} + +/** + * The tests among `candidates` (repo-relative) whose transitive imports include a changed file, each + * with the changed file that reached it. A reverse breadth-first search from the changed files, so an + * import cycle cannot hide a dependent. + */ +export function affected( + graph: TestGraph, + candidates: string[], + changed: string[] +): Map { + const dependents = new Map(); + for (const [file, deps] of graph.deps) + for (const d of deps) dependents.set(d, [...(dependents.get(d) ?? []), file]); + + // a screenshot belongs to the test file that took it: `/__screenshots__//.png` + const start = changed.map((f) => [f.replace(/\/__screenshots__\/([^/]+)\/[^/]+$/, "/$1"), f]); + const reached = new Map(start.map(([file, origin]) => [file, origin])); + const queue = start.map(([file]) => file); + for (let file = queue.shift(); file !== undefined; file = queue.shift()) + for (const dependent of dependents.get(file) ?? []) + if (!reached.has(dependent)) { + reached.set(dependent, reached.get(file) ?? file); + queue.push(dependent); + } + return new Map(candidates.filter((t) => reached.has(t)).map((t) => [t, reached.get(t) ?? t])); +} + +/** A change here is rendered through by every component test: run them all, not the import-graph subset. */ +const SHARED_COMPONENT = [ + /^packages\/ui\/src\/lib\/(styles|tokens|theme|stores|utils|actions|primitives|i18n|testing)\//, + /^packages\/ui\/src\/lib\/(index|theme-script|types)\.ts$/, + /^packages\/ui\/tests\/(setup\.[a-z]+|a11y|dom|viewports|settled|vrt|timing-reporter)\.ts$/, + /^packages\/ui\/(vite\.config\.ts|package\.json)$/, + /^bun\.lock$/, +]; + +export const isSharedComponent = (file: string) => + !isTestFile(file) && SHARED_COMPONENT.some((re) => re.test(file)); + +/** + * The budget `issue-land.sh` runs the land gate under: a SHARED change (a shared component file, or one the + * route map says reaches every page or does not know) runs every component test and every route and gets + * `landShared`; any other change runs what it affects plus the canaries under `land`. + */ +export function landBudget(changed: string[]): "land" | "landShared" { + return changed.some(isSharedComponent) || select(changed).all ? "landShared" : "land"; +} diff --git a/scripts/test-run.ts b/scripts/test-run.ts index ac05b4fc..1b9e2a74 100755 --- a/scripts/test-run.ts +++ b/scripts/test-run.ts @@ -3,10 +3,22 @@ * Runs a browser test command so it can never leave browsers behind, and cleans up after runs * that were killed hard (SIGKILL, closed terminal, killed agent job). * - * bun scripts/test-run.ts [args...] run a test command under a tracked run + * bun scripts/test-run.ts [--budget=] [args...] run a test command under a tracked run * bun scripts/test-run.ts --reap only clean up dead runs of THIS repo * bun scripts/test-run.ts --status list live/dead runs and their browsers * + * One suite at a time per machine: a run takes the lock in `$TMPDIR/ca-test-lock` (`CA_TEST_LOCK_DIR` overrides; shared by every + * worktree and checkout) and a second run waits, printing who holds it, instead of starting more + * browsers on top of the first. A lock whose holder died is taken over. Set CA_TEST_LOCK=0 to skip + * (CI runners are single-tenant). + * + * Time budgets: `--budget=` gives the command a wall-time budget (BUDGETS below, the one place + * they live). When it ends, the 10 slowest tests are printed (runners append `{name, ms}` lines to + * `CA_TEST_TIMINGS`: tests/timing-reporter.ts for Playwright, packages/ui/tests/timing-reporter.ts for + * Vitest), and a run over its budget FAILS, so slowness is caught like any other regression. The + * clock starts after the lock is taken: time spent waiting for another suite is not this suite's. + * A budget is raised only in its own commit, with the measured reason. + * * Ownership is explicit, never guessed: every run gets an id, written to * `.cache/test-runs/.json` with the runner's pid, and exported as CA_TEST_RUN. The Vitest and * Playwright configs pass it to every browser as `--ca-test-run=`. The reaper only kills @@ -15,6 +27,7 @@ */ import { spawn, spawnSync } from "node:child_process"; import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { availableParallelism, loadavg, tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -109,6 +122,8 @@ export function reap(): number { } function status() { + const h = holder(); + console.log(h && alive(h.pid) ? `test lock: ${describeHolder(h)}` : "test lock: free"); const rs = records(); if (!rs.length) return console.log("no recorded test runs"); for (const r of rs) { @@ -119,11 +134,161 @@ function status() { } } -async function run(cmd: string[]): Promise { +const lockDir = process.env.CA_TEST_LOCK_DIR ?? join(tmpdir(), "ca-test-lock"); +const holderFile = join(lockDir, "holder.json"); + +interface Holder { + pid: number; + started: string; + cwd: string; + cmd: string[]; +} + +const holder = (): Holder | undefined => { + try { + return JSON.parse(readFileSync(holderFile, "utf8")) as Holder; + } catch { + return undefined; + } +}; + +const describeHolder = (h: Holder) => + `pid ${h.pid}, since ${h.started.slice(11, 19)}Z, ${h.cmd.join(" ")} (${h.cwd})`; + +/** + * Takes the machine-wide lock, waiting while a live run holds it. `mkdir` is the atomic step: it + * fails when the directory exists. A directory without a live holder (crash between `mkdir` and the + * holder file, or a dead pid) is taken over. + */ +export async function acquireLock(me: Holder, poll = 2_000): Promise { + let announced = 0; + for (;;) { + try { + mkdirSync(lockDir); + writeFileSync(holderFile, JSON.stringify(me)); + return; + } catch (e) { + if ((e as NodeJS.ErrnoException).code !== "EEXIST") throw e; + } + const h = holder(); + if (h && alive(h.pid)) { + if (announced !== h.pid) { + console.error(`test-run: waiting for the test lock held by ${describeHolder(h)}`); + announced = h.pid; + } + } else { + // Holder gone (or never written): a run that died takes the lock down with it only if it + // got to clean up, so reclaim it. Re-check right before removing so two waiters do not + // both delete a lock the first one has just retaken. + const again = holder(); + if (!again || !alive(again.pid)) rmSync(lockDir, { recursive: true, force: true }); + continue; + } + await new Promise((r) => setTimeout(r, poll)); + } +} + +export function releaseLock(pid: number) { + if (holder()?.pid === pid) rmSync(lockDir, { recursive: true, force: true }); +} + +/** Wall-time budget in seconds of each command. Raise one only in its own commit, with the measured reason. */ +export const BUDGETS = { + /** `bun run test` / `test:changed`: what the diff against origin/main affects */ + changed: 60, + /** `bun run validate`: build, types, lint, unit tests, size */ + validate: 90, + /** `issue-land.sh`: `test:changed --land` (affected tests, changed screenshots, the route canaries) */ + land: 180, + /** + * `issue-land.sh` when a SHARED file changed: every component test and every route (the gallery through its + * representatives), no screenshots. Measured at load 10: unit 5 s + components 109 s + builds 99 s + routes 132 s = 346 s; + * the 300 s it started with was never measured (#546). + */ + landShared: 360, + /** `bun run test:full`: unit, every component test, every route, fixtures (release gate, part 1) */ + full: 300, + /** `bun run test:vrt`: every component and site screenshot (release gate, part 2) */ + vrt: 180, +} as const; +type BudgetName = keyof typeof BUDGETS; + +/** The slowest `n` tests recorded in `file` (JSON lines `{name, ms}` appended by the runners). */ +export function slowest(file: string, n = 10): { name: string; ms: number }[] { + if (!existsSync(file)) return []; + return readFileSync(file, "utf8") + .split("\n") + .flatMap((l) => { + try { + return l ? [JSON.parse(l) as { name: string; ms: number }] : []; + } catch { + return []; + } + }) + .sort((a, b) => b.ms - a.ms) + .slice(0, n); +} + +/** + * Prints the slowest tests and the budget verdict; returns the exit code the budget adds. + * A budget is wall time, so on a machine that is oversubscribed (load average above its cores: other + * suites, builds, other projects) a correct run takes several times longer for no reason in the code, and + * failing it would make the gate a lottery. The verdict is then printed, with the slowest tests, but does + * not fail the run: rerun on a quiet machine. `load` is injectable for the unit test. + */ +export function report( + budget: BudgetName | undefined, + seconds: number, + timings: string | undefined, + load = loadavg()[0], + cores = availableParallelism() +): number { + const top = timings ? slowest(timings) : []; + const over = budget !== undefined && seconds > BUDGETS[budget]; + if (top.length && (over || process.env.CA_TEST_SLOWEST === "1")) { + console.error("test-run: slowest tests"); + for (const t of top) console.error(` ${(t.ms / 1000).toFixed(1).padStart(6)}s ${t.name}`); + } + if (budget === undefined) return 0; + const line = `${budget} ${seconds.toFixed(1)}s of ${BUDGETS[budget]}s (load average ${load.toFixed(1)} on ${cores} cores)`; + if (!over) { + console.error(`test-run: within budget: ${line}`); + return 0; + } + if (load > cores) { + console.error( + `test-run: OVER BUDGET, NOT ENFORCED: ${line}. The machine is oversubscribed, so this time says nothing about the code: rerun when load is below ${cores}.` + ); + return 0; + } + console.error( + `test-run: OVER BUDGET: ${line}. A budget is a regression gate: make the run faster, or raise it in its own commit with the measured reason.` + ); + return 1; +} + +async function run(cmd: string[], budget?: BudgetName): Promise { const reaped = reap(); if (reaped) console.error(`test-run: cleaned ${reaped} browser process(es) left by an interrupted run`); + // A nested run (`test:changed` runs vitest and playwright through test-run too) reuses the lock its + // parent holds instead of waiting for it forever. + const parent = Number(process.env.CA_TEST_LOCK_HELD); + const locked = process.env.CA_TEST_LOCK !== "0" && !(parent && alive(parent)); + if (locked) + await acquireLock({ + pid: process.pid, + started: new Date().toISOString(), + cwd: process.cwd(), + cmd, + }); + + const t0 = performance.now(); + // One timings file per top-level run; a nested run appends to its parent's. + const timings = process.env.CA_TEST_TIMINGS ?? join(tmpdir(), `ca-timings-${process.pid}.jsonl`); + rmSync(process.env.CA_TEST_TIMINGS ? "/nonexistent" : timings, { force: true }); + mkdirSync(runsDir, { recursive: true }); const id = `${Date.now().toString(36)}-${process.pid}`; const record: RunRecord = { @@ -148,7 +313,13 @@ async function run(cmd: string[]): Promise { const child = spawn(cmd[0], cmd.slice(1), { stdio: "inherit", detached: true, - env: { ...process.env, PATH, CA_TEST_RUN: id }, + env: { + ...process.env, + PATH, + CA_TEST_RUN: id, + CA_TEST_TIMINGS: timings, + CA_TEST_LOCK_HELD: String(locked ? process.pid : parent || ""), + }, }); let finished = false; @@ -160,6 +331,7 @@ async function run(cmd: string[]): Promise { } catch {} killAll(taggedTree(id)); rmSync(file, { force: true }); + if (locked) releaseLock(process.pid); }; for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"] as const) process.on(sig, () => { @@ -176,11 +348,16 @@ async function run(cmd: string[]): Promise { }); }); cleanup(); - return code; + const verdict = report(budget, (performance.now() - t0) / 1000, timings); + if (!process.env.CA_TEST_TIMINGS_KEEP && timings.includes(`ca-timings-${process.pid}`)) + rmSync(timings, { force: true }); + return code || verdict; } const args = process.argv.slice(2); -if (args[0] === "--reap") { +if (!import.meta.main) { + // imported by tests: no CLI +} else if (args[0] === "--reap") { const n = reap(); console.log( n ? `test-run: cleaned ${n} browser process(es) from dead runs` : "test-run: nothing to clean" @@ -188,8 +365,16 @@ if (args[0] === "--reap") { } else if (args[0] === "--status") { status(); } else if (args.length) { - process.exit(await run(args)); + const flag = args[0].startsWith("--budget=") ? args.shift() : undefined; + const name = flag?.slice("--budget=".length) as BudgetName | undefined; + if (name !== undefined && !(name in BUDGETS)) { + console.error(`test-run: unknown budget "${name}" (${Object.keys(BUDGETS).join(", ")})`); + process.exit(2); + } + process.exit(await run(args, name)); } else { - console.error("usage: bun scripts/test-run.ts | --reap | --status"); + console.error( + "usage: bun scripts/test-run.ts [--budget=] | --reap | --status" + ); process.exit(2); } diff --git a/scripts/validate.ts b/scripts/validate.ts index 9ed962cd..a104233f 100644 --- a/scripts/validate.ts +++ b/scripts/validate.ts @@ -2,9 +2,11 @@ // on each other. Output of each step is buffered and printed only if it fails. // // phase 0: package build (svelte-check self-imports resolve to dist/) -// phase 1 (parallel): generated themes, token lint, Biome, unit tests, test/config type check, package type check -// phase 2 (parallel, needs dist/): site type check, size budget -// Locally at most 4 steps run at once; CI runs each phase fully in parallel. +// phase 1 (parallel, needs dist/ for the last two): generated themes, token lint, Biome, unit tests, +// test/config type check, package type check, site type check, size budget +// The slowest step (unit tests, ~30 s) is queued first so it starts at once; nothing else waits for it. +// Locally at most 4 steps run at once; CI runs the whole phase in parallel. `bun run validate` has a 90 s +// budget (scripts/test-run.ts). type Step = { name: string; cmd: string[]; cwd?: string }; @@ -13,15 +15,13 @@ const phase0: Step[] = [ ]; const phase1: Step[] = [ + { name: "unit tests", cmd: ["bun", "run", "test:unit"] }, + { name: "types (package)", cmd: ["bun", "run", "check"], cwd: "packages/ui" }, + { name: "types (site)", cmd: ["bun", "run", "check"], cwd: "site" }, { name: "themes up to date", cmd: ["bun", "run", "themes:check"], cwd: "packages/ui" }, { name: "token lint", cmd: ["bun", "run", "lint:tokens"], cwd: "packages/ui" }, { name: "biome", cmd: ["bun", "run", "lint"] }, - { name: "unit tests", cmd: ["bun", "run", "test:unit"] }, { name: "types (tests, configs)", cmd: ["bun", "run", "check:native"] }, - { name: "types (package)", cmd: ["bun", "run", "check"], cwd: "packages/ui" }, -]; -const phase2: Step[] = [ - { name: "types (site)", cmd: ["bun", "run", "check"], cwd: "site" }, { name: "size budget", cmd: ["bun", "run", "size"], cwd: "packages/ui" }, ]; @@ -59,7 +59,7 @@ async function runPhase(phase: Step[]): Promise { return ok; } -for (const phase of [phase0, phase1, phase2]) { +for (const phase of [phase0, phase1]) { if (!(await runPhase(phase))) process.exit(1); } console.log(`validate ok in ${((performance.now() - start) / 1000).toFixed(1)}s`); diff --git a/scripts/vrt-docker.sh b/scripts/vrt-docker.sh deleted file mode 100755 index 9e37b56c..00000000 --- a/scripts/vrt-docker.sh +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env sh -# Render or check the visual baselines locally, in the same environment CI used (linux/amd64, -# Playwright image `noble`), so the pixels match. CI is paused: this is the only way to produce them. -# sh scripts/vrt-docker.sh --update regenerate every baseline (package + site) -# sh scripts/vrt-docker.sh --check compare against the committed baselines (default) -# Runs the steps of the `visual` job in .github/workflows/ci.yml: build the package and the site, then -# `test:vrt` (Vitest, packages/ui) and the site's visual-desktop/visual-mobile projects (Playwright). -# The worktree is bind-mounted; node_modules, build output and caches live in container-local volumes -# so the host's macOS node_modules is never seen (nor written). Needs Docker. Baselines land in the -# worktree under `__screenshots__/`; failure diffs under site/test-results and packages/ui/.vitest. -set -e -MODE=check -case "${1:-}" in - --update) MODE=update ;; - --check | "") MODE=check ;; - *) echo "usage: vrt-docker.sh [--update|--check]" >&2; exit 1 ;; -esac -command -v docker >/dev/null 2>&1 || { echo "Docker is required (https://docs.docker.com/get-docker/)." >&2; exit 1; } -docker info >/dev/null 2>&1 || { echo "The Docker daemon is not running." >&2; exit 1; } - -ROOT=$(cd "$(dirname "$0")/.." && pwd) -# The image must match the installed browser build: site/package.json pins @playwright/test (the -# only place the version lives, same as .github/actions/setup). -PW=$(sed -n 's/.*"@playwright\/test": *"\([0-9][0-9.]*\)".*/\1/p' "$ROOT/site/package.json") -[ -n "$PW" ] || { echo "Cannot read the @playwright/test version from site/package.json." >&2; exit 1; } -IMAGE="mcr.microsoft.com/playwright:v$PW-noble" -BUN=$(sed -n 's/.*"packageManager": *"bun@\([0-9][0-9.]*\)".*/\1/p' "$ROOT/package.json") -[ -n "$BUN" ] || { echo "Cannot read the bun version from package.json." >&2; exit 1; } - -# One set of volumes per worktree path, so parallel worktrees never share an install. -KEY=$(printf %s "$ROOT" | cksum | cut -d' ' -f1) -VOL="ca-vrt-$KEY" - -if [ "$MODE" = update ]; then - PKG_ARGS="--update" - SITE_ARGS="--update-snapshots" -else - PKG_ARGS="" - SITE_ARGS="" -fi -# CI runs on 4 native vCPUs; under amd64 emulation (Apple Silicon) more workers only starve each other -# into the 5s screenshot timeout. Override with VRT_WORKERS. -WORKERS="${VRT_WORKERS:-4}" - -# Everything below runs inside the container (single quotes: expanded there, not here). -INNER=' -set -e -export CI=1 E2E_PREBUILT=1 HOME=/root -# Build output is written into the worktree (gitignored, same paths as a host build) and removed on exit, -# so a Linux build never lingers next to a macOS one. -OUT="/work/packages/ui/dist /work/site/dist /work/site/.astro /work/packages/ui/.svelte-kit" -trap "rm -rf $OUT" EXIT -rm -rf $OUT -# Match the fonts of the ubuntu-24.04 runner that rendered the committed baselines. The Playwright image -# differs in two ways: it has no DejaVu (so `system-ui`, `sans-serif`, `serif` and `monospace` fall back to -# WenQuanYi/Liberation, while the runner resolves them to DejaVu), and it ships FreeFont, whose FreeMono -# wins a language-less `monospace` lookup while a language-tagged one gives DejaVu Sans Mono. Chromium uses -# both lookups, so code blocks changed height by ~27px after the first full-page capture and no screenshot -# was ever stable. DejaVu in, FreeFont out gives one answer for every lookup. -apt-get update -qq >/dev/null -apt-get install -y -qq fonts-dejavu-core >/dev/null -apt-get remove -y -qq fonts-freefont-ttf >/dev/null -fc-cache -f -# node_modules are volumes: copy nothing from the host, install from the lockfile like CI does. -npm install -g "bun@$BUN" >/dev/null -bun install --frozen-lockfile --ignore-scripts -node scripts/fix-ts-symlink.js -# `validate` builds the package first (phase 0); the visual job downloads the `build` artifact instead. -bun run --cwd packages/ui build -bun run --cwd site build -# The two suites are independent jobs in CI (matrix): run both, so one failing never hides the other. -rc=0 -bun run --cwd packages/ui test:vrt $PKG_ARGS || rc=1 -(cd site && bunx playwright test --project=visual-desktop --project=visual-mobile --workers="$WORKERS" $SITE_ARGS) || rc=1 -exit $rc -' - -echo "vrt-docker: $MODE with $IMAGE (linux/amd64, volumes $VOL-*)" -# The bind mount's node_modules directories (root, package, site) are shadowed by volumes. -exec docker run --rm --platform linux/amd64 --ipc=host \ - -e BUN="$BUN" -e PKG_ARGS="$PKG_ARGS" -e SITE_ARGS="$SITE_ARGS" -e WORKERS="$WORKERS" \ - -v "$ROOT:/work" -w /work \ - -v "$VOL-nm:/work/node_modules" \ - -v "$VOL-nm-ui:/work/packages/ui/node_modules" \ - -v "$VOL-nm-site:/work/site/node_modules" \ - -v "$VOL-bun:/root/.bun" \ - "$IMAGE" sh -c "$INNER" diff --git a/site/package.json b/site/package.json index ce1bec37..5d0249c3 100644 --- a/site/package.json +++ b/site/package.json @@ -13,7 +13,9 @@ "check": "astro check --minimumFailingSeverity warning && tsc -p functions/tsconfig.json && tsc -p functions/tsconfig.test.json", "test:unit": "bun test ./functions", "test:e2e": "bun ../scripts/test-run.ts playwright test", - "test:a11y": "bun ../scripts/test-run.ts playwright test --project=a11y" + "test:a11y": "bun ../scripts/test-run.ts playwright test --project=a11y", + "test:vrt": "bun ../scripts/test-run.ts playwright test --project=visual-desktop --project=visual-mobile", + "test:vrt:update": "bun ../scripts/test-run.ts playwright test --project=visual-desktop --project=visual-mobile --update-snapshots" }, "dependencies": { "@astrojs/mdx": "8.0.3", @@ -45,6 +47,7 @@ "@astrojs/check": "0.9.10", "@axe-core/playwright": "4.13.0", "@playwright/test": "1.64.0", + "happy-dom": "21.0.1", "typescript": "6.0.3", "wrangler": "4.149.0" } diff --git a/site/playwright.config.ts b/site/playwright.config.ts index db1a65a4..cd52cb11 100644 --- a/site/playwright.config.ts +++ b/site/playwright.config.ts @@ -1,11 +1,20 @@ import { defineConfig, devices } from "@playwright/test"; -const port = process.env.E2E_PORT ?? "4321"; +// One port per checkout, derived from its path: 4321 is what every Astro project on a developer machine +// defaults to, and a run that silently reused ANOTHER project's server compared that site against this one's +// baselines (and committed its screenshots as baselines, #546). `E2E_PORT` overrides it. +const checkoutPort = + 43000 + ([...process.cwd()].reduce((h, c) => (h * 31 + c.charCodeAt(0)) >>> 0, 7) % 1000); +const port = process.env.E2E_PORT ?? String(checkoutPort); const baseURL = `http://localhost:${port}`; +// Platforms with committed baselines; elsewhere the visual projects run but compare nothing. +const NATIVE_BASELINES: string[] = ["darwin", "linux"]; const desktop = { ...devices["Desktop Chrome"], viewport: { width: 1280, height: 800 } }; export default defineConfig({ testDir: "./tests", + // Fails the run at once when the server on the port is not this site (see global-setup.ts). + globalSetup: "./tests/global-setup.ts", fullyParallel: true, forbidOnly: !!process.env.CI, failOnFlakyTests: !!process.env.CI, @@ -15,7 +24,10 @@ export default defineConfig({ // machine that other projects share, and 12 Chromium workers running axe there starved each other // into random 30s timeouts on a different route every run, 3 workers passed 3 of 3 (#539). workers: process.env.GITHUB_ACTIONS ? "100%" : "25%", - snapshotPathTemplate: "{testDir}/__screenshots__/{projectName}/{testFilePath}/{arg}{ext}", + // The platform is part of the file name (`home-dark-darwin.webp`), like Vitest's `-chromium-darwin.png`: a baseline + // is only ever compared on the platform that rendered it, so each platform keeps its own set side by side. + snapshotPathTemplate: + "{testDir}/__screenshots__/{projectName}/{testFilePath}/{arg}-{platform}{ext}", expect: { timeout: 5_000, toHaveScreenshot: { @@ -30,12 +42,14 @@ export default defineConfig({ }, reporter: process.env.CI ? [ + ["./tests/timing-reporter.ts"], ["dot"], ["github"], ["html", { open: "never" }], ["json", { outputFile: "test-results/results.json" }], ] : [ + ["./tests/timing-reporter.ts"], ["list", { printFailuresInline: true }], ["html", { open: "never" }], ], @@ -56,32 +70,34 @@ export default defineConfig({ projects: [ { name: "desktop", - testIgnore: /\.(a11y|visual|webkit)\.spec\.ts/, + testIgnore: /\.(a11y|visual|webkit|axe)\.spec\.ts/, use: { ...desktop, colorScheme: "dark" }, }, { name: "mobile", - testIgnore: /\.(a11y|visual|webkit)\.spec\.ts/, + testIgnore: /\.(a11y|visual|webkit|axe)\.spec\.ts/, use: { ...devices["Pixel 7"], colorScheme: "dark" }, }, { name: "a11y", testMatch: /\.a11y\.spec\.ts/, use: { ...desktop } }, + // axe on every route: its own project so it shards and runs beside the layout project. + { name: "axe", testMatch: /\.axe\.spec\.ts/, use: { ...desktop } }, { name: "visual-desktop", testMatch: /\.visual\.spec\.ts/, - ignoreSnapshots: process.platform !== "linux", + ignoreSnapshots: !NATIVE_BASELINES.includes(process.platform), use: { ...desktop }, }, { name: "visual-mobile", testMatch: /\.visual\.spec\.ts/, - ignoreSnapshots: process.platform !== "linux", + ignoreSnapshots: !NATIVE_BASELINES.includes(process.platform), use: { ...devices["Pixel 7"] }, }, { name: "webkit", default: false, ignoreSnapshots: true, - testIgnore: /\.(a11y|visual|webkit)\.spec\.ts/, + testIgnore: /\.(a11y|visual|webkit|axe)\.spec\.ts/, use: { ...devices["Desktop Safari"] }, }, { @@ -97,7 +113,9 @@ export default defineConfig({ // command exit and a stale lock blocks the next run. command: `${process.env.E2E_PREBUILT ? "" : "bun run build && "}bunx astro preview --port ${port} --ignore-lock`, url: baseURL, - reuseExistingServer: !process.env.CI, + // Never reuse a server found on the port: it may not be this site, or this build. Runs are serialised by + // scripts/test-run.ts, so a busy port is another project's, and the run should say so, not use it. + reuseExistingServer: false, timeout: 300_000, }, }); diff --git a/site/public/api/v1/catalog.json b/site/public/api/v1/catalog.json index 63586e33..d8720dd2 100644 --- a/site/public/api/v1/catalog.json +++ b/site/public/api/v1/catalog.json @@ -11764,14 +11764,14 @@ "slug": "pinned-step-story", "title": "Pinned step story", "description": "", - "summary": "A pinned media panel on the left changes its figure and caption as each step on the right scrolls through the middle of the frame; inactive steps dim to 40%.", + "summary": "A pinned media panel on the left changes its figure and caption as each step on the right scrolls through the middle of the frame; the step in view has the primary title colour, the others the secondary one.", "use": "Explaining a product flow: connect, sort, review. One visual stays on screen while the copy advances.", "spec": [ "`MotionStage` from `@codeatlasdev/ui` with `loop={5600}`: autoplay scrolls the container, a wheel or touch scroll takes over", "`.story__media` is `position: sticky;` with `inset-block-start: 0;` and pins the media while the steps list scrolls past", "`IntersectionObserver` with `rootMargin: '-45% 0px -45% 0px'` makes the step crossing the middle 10% band the active one", "Figure transition: `opacity 320ms cubic-bezier(0.2, 0, 0, 1)` and a `translateY(12px) scale(0.96)` start state", - "Inactive steps use `opacity: 0.4`, the active step `opacity: 1`", + "The active step title is `var(--ca-text-primary)`, the others `var(--ca-text-secondary)` (a colour change, never a lower opacity, so every state stays AA)", "Under reduced motion the last step is active and nothing scrolls by itself" ], "tags": [ @@ -11795,7 +11795,7 @@ "easing": "cubic-bezier(0.2, 0, 0, 1)" }, { - "property": "opacity (step copy 0.4 to 1)", + "property": "color (step title, secondary to primary)", "duration": "320ms", "easing": "cubic-bezier(0.2, 0, 0, 1)" } @@ -12605,7 +12605,7 @@ "spec": [ "`MotionStage` with `label=\"Tab indicator: a pill slides under the active tab\"` and `loop={1800}`", "`Tabs`, `TabsList`, `TabsTrigger` and `TabsContent` come from `@codeatlasdev/ui`, untouched", - "The active trigger's `offsetLeft` and `offsetWidth` are written to `--x` and `--w`; the pill reads `transform: translateX(var(--x))` and `inline-size: var(--w)`", + "The active trigger's `offsetLeft` and `offsetWidth` are written to `--ti-x` and `--ti-w`; the pill reads `transform: translateX(var(--ti-x))` and `inline-size: var(--ti-w)`", "`transition: transform 360ms cubic-bezier(0.2, 0, 0, 1), inline-size 360ms cubic-bezier(0.2, 0, 0, 1)`; no transition before the first measure and under reduced motion", "A `ResizeObserver` re-measures when the list resizes" ], @@ -12863,9 +12863,9 @@ "use": "Hero or feature grids where hovering should reveal structure: pricing tables, bento grids, docs landing pages.", "spec": [ "`MotionStage` with `loop={1600}`; each cycle moves the light to the next entry of `waypoints`", - "`@property --x` and `@property --y` are registered percentages, so `transition: --x 520ms cubic-bezier(0.2, 0, 0, 1)` eases them", - "The pointer sets `--x` and `--y` from `getBoundingClientRect()`; `onpointerleave` hands control back to the autoplay waypoint", - "One mask: `radial-gradient(circle 8rem at var(--x) var(--y), #000, transparent 72%)` reveals the line grid, `circle 10rem` paints the brand glow", + "`@property --spot-x` and `@property --spot-y` are registered percentages, so `transition: --spot-x 520ms cubic-bezier(0.2, 0, 0, 1)` eases them", + "The pointer sets `--spot-x` and `--spot-y` from `getBoundingClientRect()`; `onpointerleave` hands control back to the autoplay waypoint", + "One mask: `radial-gradient(circle 8rem at var(--spot-x) var(--spot-y), #000, transparent 72%)` reveals the line grid, `circle 10rem` paints the brand glow", "`@media (pointer: coarse)` sets `touch-action: none` so a finger drags the light instead of scrolling the demo", "Under reduced motion `class:spot--run={!m.reduced}` removes the transition and the light rests at `50%` / `40%`" ], @@ -12887,7 +12887,7 @@ ], "motion": [ { - "property": "--x, --y (radial mask center)", + "property": "--spot-x, --spot-y (radial mask center)", "duration": "520ms", "easing": "cubic-bezier(0.2, 0, 0, 1)" } diff --git a/site/public/api/v1/en/catalog.tsv b/site/public/api/v1/en/catalog.tsv index 86538af3..627051af 100644 --- a/site/public/api/v1/en/catalog.tsv +++ b/site/public/api/v1/en/catalog.tsv @@ -268,7 +268,7 @@ id group page title names tags summary 322 motion scroll Parallax Parallax | Parallax layers | Paralaxe | Parallaxe motion,scroll,parallax,layers,depth,hero Three layers (sky, ridge, foreground) plus a caption move at different speeds while the container scrolls, so the scene gains depth. Native scroll timeline, with a scroll listener fallback. 323 motion scroll Reading progress bar Reading progress bar | Scroll progress | Barra de progresso de leitura | Barre de progression de lecture motion,scroll,progress,reading,article,scroll-driven A 4px bar stuck to the top of an article fills from left to right as it is read, with a percentage readout, tied directly to scroll position. 324 motion scroll Horizontal scroll section Horizontal scroll section | Scroll-jacked horizontal | Rolagem horizontal por scroll vertical | Défilement horizontal motion,scroll,horizontal,pinned,sticky,steps Vertical scrolling inside a pinned frame slides a row of four step cards sideways, with a progress bar underneath. The page scrolls down; the content moves across. -325 motion scroll Pinned step story Pinned step story | Sticky scrollytelling | Scrollytelling fixo por etapas | Récit épinglé par étapes motion,scroll,pinned,sticky,steps,storytelling A pinned media panel on the left changes its figure and caption as each step on the right scrolls through the middle of the frame; inactive steps dim to 40%. +325 motion scroll Pinned step story Pinned step story | Sticky scrollytelling | Scrollytelling fixo por etapas | Récit épinglé par étapes motion,scroll,pinned,sticky,steps,storytelling A pinned media panel on the left changes its figure and caption as each step on the right scrolls through the middle of the frame; the step in view has the primary title colour, the others the secondary one. 326 motion scroll Scroll-linked counter and chart draw Scroll-linked counter and chart draw | Count-up on scroll | Contador e gráfico ligados ao scroll | Compteur lié au défilement motion,scroll,counter,chart,draw,scroll-driven A revenue figure counts up from $0 to $48,210 while a line chart draws itself, both tied to the container's scroll position; scrolling back reverses them. 340 motion micro Magnetic button Magnetic button | Botão magnético | Bouton magnétique motion,magnetic,button,hover,spring,pointer A pill button that leans toward the pointer inside a 72px range, pulled by a Spring and released on leave. 341 motion micro Press spring Press spring | Press scale | Efeito de pressionar | Pression élastique motion,press,button,spring,tap,feedback A button that scales to 0.94 on press and overshoots past 1 on release before settling, driven by a Spring. diff --git a/site/public/api/v1/en/e/325.txt b/site/public/api/v1/en/e/325.txt index 21c95b7c..9b4a3594 100644 --- a/site/public/api/v1/en/e/325.txt +++ b/site/public/api/v1/en/e/325.txt @@ -1,17 +1,17 @@ Element: #325 Pinned step story (motion/scroll) -What it shows: A pinned media panel on the left changes its figure and caption as each step on the right scrolls through the middle of the frame; inactive steps dim to 40%. +What it shows: A pinned media panel on the left changes its figure and caption as each step on the right scrolls through the middle of the frame; the step in view has the primary title colour, the others the secondary one. When to use: Explaining a product flow: connect, sort, review. One visual stays on screen while the copy advances. Spec: - `MotionStage` from `@codeatlasdev/ui` with `loop={5600}`: autoplay scrolls the container, a wheel or touch scroll takes over - `.story__media` is `position: sticky;` with `inset-block-start: 0;` and pins the media while the steps list scrolls past - `IntersectionObserver` with `rootMargin: '-45% 0px -45% 0px'` makes the step crossing the middle 10% band the active one - Figure transition: `opacity 320ms cubic-bezier(0.2, 0, 0, 1)` and a `translateY(12px) scale(0.96)` start state -- Inactive steps use `opacity: 0.4`, the active step `opacity: 1` +- The active step title is `var(--ca-text-primary)`, the others `var(--ca-text-secondary)` (a colour change, never a lower opacity, so every state stays AA) - Under reduced motion the last step is active and nothing scrolls by itself Motion: - opacity, transform: translateY, scale (media figure): 320ms cubic-bezier(0.2, 0, 0, 1) -- opacity (step copy 0.4 to 1): 320ms cubic-bezier(0.2, 0, 0, 1) +- color (step title, secondary to primary): 320ms cubic-bezier(0.2, 0, 0, 1) Names: Pinned step story | Sticky scrollytelling | Scrollytelling fixo por etapas | Récit épinglé par étapes Tags: motion, scroll, pinned, sticky, steps, storytelling Needs: @codeatlasdev/ui@4.5.0 @@ -142,10 +142,10 @@ Live demo: https://ui.codeatlas.com.br/docs/motion/scroll#pinned-step-story gap: var(--ca-space-1); block-size: 13rem; padding: 0 var(--ca-space-4); - opacity: 0.4; - transition: opacity 320ms cubic-bezier(0.2, 0, 0, 1); } - .story__step--on { opacity: 1; } - .story__title { margin: 0; font-weight: var(--ca-font-weight-semibold); color: var(--ca-text-primary); } + /* The step in view is the loud one. Passed and upcoming steps drop to the secondary text colour + (AA on the stage), never to a lower opacity: opacity dims text below 4.5:1. */ + .story__title { margin: 0; font-weight: var(--ca-font-weight-semibold); color: var(--ca-text-secondary); transition: color 320ms; } .story__body { margin: 0; font-size: var(--ca-font-size-body2); color: var(--ca-text-secondary); } + .story__step--on .story__title { color: var(--ca-text-primary); } diff --git a/site/public/api/v1/en/e/363.txt b/site/public/api/v1/en/e/363.txt index e1612b39..25ca3468 100644 --- a/site/public/api/v1/en/e/363.txt +++ b/site/public/api/v1/en/e/363.txt @@ -5,7 +5,7 @@ When to use: Segmented controls, tab bars and filters where the eye should follo Spec: - `MotionStage` with `label="Tab indicator: a pill slides under the active tab"` and `loop={1800}` - `Tabs`, `TabsList`, `TabsTrigger` and `TabsContent` come from `@codeatlasdev/ui`, untouched -- The active trigger's `offsetLeft` and `offsetWidth` are written to `--x` and `--w`; the pill reads `transform: translateX(var(--x))` and `inline-size: var(--w)` +- The active trigger's `offsetLeft` and `offsetWidth` are written to `--ti-x` and `--ti-w`; the pill reads `transform: translateX(var(--ti-x))` and `inline-size: var(--ti-w)` - `transition: transform 360ms cubic-bezier(0.2, 0, 0, 1), inline-size 360ms cubic-bezier(0.2, 0, 0, 1)`; no transition before the first measure and under reduced motion - A `ResizeObserver` re-measures when the list resizes Motion: @@ -68,7 +68,7 @@ Live demo: https://ui.codeatlas.com.br/docs/motion/transitions#tab-indicator {#snippet children(m)}
{ stage = m; }}> -
+
{#each tabs as t (t.value)} @@ -88,12 +88,13 @@ Live demo: https://ui.codeatlas.com.br/docs/motion/transitions#tab-indicator .ti { width: 100%; max-width: 380px; } .ti__rail { display: inline-flex; max-inline-size: 100%; } /* The package trigger paints its own active pill; hand that job to the sliding one. */ - .ti :global(.ca-tabs-trigger[data-state='active']) { background: transparent; position: relative; z-index: 1; } + .ti--ready :global(.ca-tabs-trigger[data-state='active']) { background: transparent; } + .ti :global(.ca-tabs-trigger[data-state='active']) { position: relative; z-index: 1; } .ti :global(.ca-tabs-list) { position: relative; } .ti__pill { position: absolute; inset-block: var(--ca-space-1); inset-inline-start: 0; z-index: 0; - inline-size: var(--w); border-radius: var(--ca-radius-pill); background: var(--ca-brand-fill); - transform: translateX(var(--x)); opacity: 0; + inline-size: var(--ti-w); border-radius: var(--ca-radius-pill); background: var(--ca-brand-fill); + transform: translateX(var(--ti-x)); opacity: 0; } .ti--ready .ti__pill { opacity: 1; transition: transform 360ms cubic-bezier(0.2, 0, 0, 1), inline-size 360ms cubic-bezier(0.2, 0, 0, 1); } .ti--instant .ti__pill { transition: none; } diff --git a/site/public/api/v1/en/e/380.txt b/site/public/api/v1/en/e/380.txt index aafd6fe3..197ab74f 100644 --- a/site/public/api/v1/en/e/380.txt +++ b/site/public/api/v1/en/e/380.txt @@ -4,13 +4,13 @@ What it shows: A pricing grid lit by a soft brand spotlight and a grid of fine l When to use: Hero or feature grids where hovering should reveal structure: pricing tables, bento grids, docs landing pages. Spec: - `MotionStage` with `loop={1600}`; each cycle moves the light to the next entry of `waypoints` -- `@property --x` and `@property --y` are registered percentages, so `transition: --x 520ms cubic-bezier(0.2, 0, 0, 1)` eases them -- The pointer sets `--x` and `--y` from `getBoundingClientRect()`; `onpointerleave` hands control back to the autoplay waypoint -- One mask: `radial-gradient(circle 8rem at var(--x) var(--y), #000, transparent 72%)` reveals the line grid, `circle 10rem` paints the brand glow +- `@property --spot-x` and `@property --spot-y` are registered percentages, so `transition: --spot-x 520ms cubic-bezier(0.2, 0, 0, 1)` eases them +- The pointer sets `--spot-x` and `--spot-y` from `getBoundingClientRect()`; `onpointerleave` hands control back to the autoplay waypoint +- One mask: `radial-gradient(circle 8rem at var(--spot-x) var(--spot-y), #000, transparent 72%)` reveals the line grid, `circle 10rem` paints the brand glow - `@media (pointer: coarse)` sets `touch-action: none` so a finger drags the light instead of scrolling the demo - Under reduced motion `class:spot--run={!m.reduced}` removes the transition and the light rests at `50%` / `40%` Motion: -- --x, --y (radial mask center): 520ms cubic-bezier(0.2, 0, 0, 1) +- --spot-x, --spot-y (radial mask center): 520ms cubic-bezier(0.2, 0, 0, 1) Names: Cursor spotlight | Mouse spotlight | Spotlight hover | Holofote do cursor | Foco de luz no cursor | Luz que segue o mouse Tags: motion, spotlight, cursor, hero, mask, grid Needs: @codeatlasdev/ui@4.5.0 @@ -54,7 +54,7 @@ Live demo: https://ui.codeatlas.com.br/docs/motion/hero-ambient#cursor-spotlight class="spot" class:spot--run={!m.reduced} role="presentation" - style="--x:{m.reduced ? 50 : at.x}%; --y:{m.reduced ? 40 : at.y}%" + style="--spot-x:{m.reduced ? 50 : at.x}%; --spot-y:{m.reduced ? 40 : at.y}%" onpointermove={follow} onpointerleave={() => (pointer = null)} > @@ -75,8 +75,8 @@ Live demo: https://ui.codeatlas.com.br/docs/motion/hero-ambient#cursor-spotlight diff --git a/site/src/demos/motion/transitions/TabIndicator.meta.json b/site/src/demos/motion/transitions/TabIndicator.meta.json index a1f29827..ba013b13 100644 --- a/site/src/demos/motion/transitions/TabIndicator.meta.json +++ b/site/src/demos/motion/transitions/TabIndicator.meta.json @@ -4,7 +4,7 @@ "spec": [ "`MotionStage` with `label=\"Tab indicator: a pill slides under the active tab\"` and `loop={1800}`", "`Tabs`, `TabsList`, `TabsTrigger` and `TabsContent` come from `@codeatlasdev/ui`, untouched", - "The active trigger's `offsetLeft` and `offsetWidth` are written to `--x` and `--w`; the pill reads `transform: translateX(var(--x))` and `inline-size: var(--w)`", + "The active trigger's `offsetLeft` and `offsetWidth` are written to `--ti-x` and `--ti-w`; the pill reads `transform: translateX(var(--ti-x))` and `inline-size: var(--ti-w)`", "`transition: transform 360ms cubic-bezier(0.2, 0, 0, 1), inline-size 360ms cubic-bezier(0.2, 0, 0, 1)`; no transition before the first measure and under reduced motion", "A `ResizeObserver` re-measures when the list resizes" ], diff --git a/site/src/demos/motion/transitions/TabIndicator.svelte b/site/src/demos/motion/transitions/TabIndicator.svelte index cae567a2..117504df 100644 --- a/site/src/demos/motion/transitions/TabIndicator.svelte +++ b/site/src/demos/motion/transitions/TabIndicator.svelte @@ -50,7 +50,7 @@ {#snippet children(m)}
{ stage = m; }}> -
+
{#each tabs as t (t.value)} @@ -70,12 +70,13 @@ .ti { width: 100%; max-width: 380px; } .ti__rail { display: inline-flex; max-inline-size: 100%; } /* The package trigger paints its own active pill; hand that job to the sliding one. */ - .ti :global(.ca-tabs-trigger[data-state='active']) { background: transparent; position: relative; z-index: 1; } + .ti--ready :global(.ca-tabs-trigger[data-state='active']) { background: transparent; } + .ti :global(.ca-tabs-trigger[data-state='active']) { position: relative; z-index: 1; } .ti :global(.ca-tabs-list) { position: relative; } .ti__pill { position: absolute; inset-block: var(--ca-space-1); inset-inline-start: 0; z-index: 0; - inline-size: var(--w); border-radius: var(--ca-radius-pill); background: var(--ca-brand-fill); - transform: translateX(var(--x)); opacity: 0; + inline-size: var(--ti-w); border-radius: var(--ca-radius-pill); background: var(--ca-brand-fill); + transform: translateX(var(--ti-x)); opacity: 0; } .ti--ready .ti__pill { opacity: 1; transition: transform 360ms cubic-bezier(0.2, 0, 0, 1), inline-size 360ms cubic-bezier(0.2, 0, 0, 1); } .ti--instant .ti__pill { transition: none; } diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/command-palette-dark.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/command-palette-dark.webp deleted file mode 100644 index 06bfda27..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/command-palette-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/command-palette-light.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/command-palette-light.webp deleted file mode 100644 index 88ef9df6..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/command-palette-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/dialog-dark.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/dialog-dark.webp deleted file mode 100644 index 1f1ff98a..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/dialog-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/dialog-light.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/dialog-light.webp deleted file mode 100644 index 4424decf..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/dialog-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-dark.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-dark.webp deleted file mode 100644 index 551db76b..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-display-data-table-dark.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-display-data-table-dark.webp deleted file mode 100644 index 71468c6a..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-display-data-table-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-display-data-table-light.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-display-data-table-light.webp deleted file mode 100644 index 76246c66..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-display-data-table-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-display-table-dark.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-display-table-dark.webp deleted file mode 100644 index eef11984..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-display-table-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-display-table-light.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-display-table-light.webp deleted file mode 100644 index 4cd1b059..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-display-table-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-feedback-dialog-dark.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-feedback-dialog-dark.webp deleted file mode 100644 index 56bfbc0c..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-feedback-dialog-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-feedback-dialog-light.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-feedback-dialog-light.webp deleted file mode 100644 index c97ae601..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-feedback-dialog-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-foundation-colors-dark.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-foundation-colors-dark.webp deleted file mode 100644 index 894ab3d5..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-foundation-colors-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-foundation-colors-light.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-foundation-colors-light.webp deleted file mode 100644 index 5ee712e3..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-foundation-colors-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-inputs-button-dark.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-inputs-button-dark.webp deleted file mode 100644 index 32c578a2..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-inputs-button-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-inputs-button-light.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-inputs-button-light.webp deleted file mode 100644 index 7388a7c5..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-inputs-button-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-inputs-date-picker-dark.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-inputs-date-picker-dark.webp deleted file mode 100644 index 0f86d7a1..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-inputs-date-picker-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-inputs-date-picker-light.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-inputs-date-picker-light.webp deleted file mode 100644 index 080dd72f..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-inputs-date-picker-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-inputs-text-field-dark.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-inputs-text-field-dark.webp deleted file mode 100644 index 54f490b8..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-inputs-text-field-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-inputs-text-field-light.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-inputs-text-field-light.webp deleted file mode 100644 index 9ce021c6..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-inputs-text-field-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-layout-app-shell-dark.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-layout-app-shell-dark.webp deleted file mode 100644 index 90a76ebb..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-layout-app-shell-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-layout-app-shell-light.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-layout-app-shell-light.webp deleted file mode 100644 index 2d240f0b..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-layout-app-shell-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-light.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-light.webp deleted file mode 100644 index f5377386..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-navigation-app-bar-dark.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-navigation-app-bar-dark.webp deleted file mode 100644 index defd6a01..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-navigation-app-bar-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-navigation-app-bar-light.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-navigation-app-bar-light.webp deleted file mode 100644 index 513b644d..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/docs-navigation-app-bar-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/home-dark-darwin.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/home-dark-darwin.webp new file mode 100644 index 00000000..618d4904 Binary files /dev/null and b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/home-dark-darwin.webp differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/home-dark.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/home-dark.webp deleted file mode 100644 index ddc372d5..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/home-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/home-light-darwin.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/home-light-darwin.webp new file mode 100644 index 00000000..d50caf2b Binary files /dev/null and b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/home-light-darwin.webp differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/home-light.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/home-light.webp deleted file mode 100644 index b06d0047..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/home-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/sheet-dark.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/sheet-dark.webp deleted file mode 100644 index 2fd147a0..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/sheet-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/sheet-light.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/sheet-light.webp deleted file mode 100644 index e64e1517..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/sheet-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-app-dashboard-dark-darwin.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-app-dashboard-dark-darwin.webp new file mode 100644 index 00000000..3262808f Binary files /dev/null and b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-app-dashboard-dark-darwin.webp differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-app-dashboard-dark.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-app-dashboard-dark.webp deleted file mode 100644 index 4c271126..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-app-dashboard-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-app-dashboard-light.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-app-dashboard-light-darwin.webp similarity index 60% rename from site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-app-dashboard-light.webp rename to site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-app-dashboard-light-darwin.webp index 63eeaf0d..9032a845 100644 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-app-dashboard-light.webp and b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-app-dashboard-light-darwin.webp differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-display-data-table-dark-2.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-auth-sign-in-dark-darwin.webp similarity index 57% rename from site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-display-data-table-dark-2.webp rename to site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-auth-sign-in-dark-darwin.webp index cfc42af8..3b467575 100644 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-display-data-table-dark-2.webp and b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-auth-sign-in-dark-darwin.webp differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-auth-sign-in-dark.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-auth-sign-in-dark.webp deleted file mode 100644 index 78949dfb..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-auth-sign-in-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-display-data-table-light-2.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-auth-sign-in-light-darwin.webp similarity index 61% rename from site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-display-data-table-light-2.webp rename to site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-auth-sign-in-light-darwin.webp index af522b34..8b2a43c3 100644 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-display-data-table-light-2.webp and b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-auth-sign-in-light-darwin.webp differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-auth-sign-in-light.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-auth-sign-in-light.webp deleted file mode 100644 index ad33bdbe..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-auth-sign-in-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-dark-darwin.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-dark-darwin.webp new file mode 100644 index 00000000..34c35242 Binary files /dev/null and b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-dark-darwin.webp differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-dark.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-dark.webp deleted file mode 100644 index b3d94b9d..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/theme-light.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-light-darwin.webp similarity index 51% rename from site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/theme-light.webp rename to site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-light-darwin.webp index 0737d0dc..9ed42bc6 100644 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/theme-light.webp and b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-light-darwin.webp differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-light.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-light.webp deleted file mode 100644 index efa7d34a..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/templates-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/theme-dark-darwin.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/theme-dark-darwin.webp new file mode 100644 index 00000000..46a872c3 Binary files /dev/null and b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/theme-dark-darwin.webp differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/theme-dark.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/theme-dark.webp deleted file mode 100644 index 385e8f61..00000000 Binary files a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/theme-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/theme-light-darwin.webp b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/theme-light-darwin.webp new file mode 100644 index 00000000..09ab6ccd Binary files /dev/null and b/site/tests/__screenshots__/visual-desktop/pages.visual.spec.ts/theme-light-darwin.webp differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/command-palette-dark.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/command-palette-dark.webp deleted file mode 100644 index 61812a02..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/command-palette-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/command-palette-light.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/command-palette-light.webp deleted file mode 100644 index c03471f3..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/command-palette-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/dialog-dark.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/dialog-dark.webp deleted file mode 100644 index bdb01da1..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/dialog-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/dialog-light.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/dialog-light.webp deleted file mode 100644 index 5e39fccb..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/dialog-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-dark.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-dark.webp deleted file mode 100644 index 7f886299..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-display-data-table-dark-1.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-display-data-table-dark-1.webp deleted file mode 100644 index ea1afcc9..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-display-data-table-dark-1.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-display-data-table-dark.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-display-data-table-dark.webp deleted file mode 100644 index 3633bdb1..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-display-data-table-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-display-data-table-light-1.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-display-data-table-light-1.webp deleted file mode 100644 index 06556a78..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-display-data-table-light-1.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-display-data-table-light.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-display-data-table-light.webp deleted file mode 100644 index b1539b93..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-display-data-table-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-display-table-dark.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-display-table-dark.webp deleted file mode 100644 index 0d1d5a68..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-display-table-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-display-table-light.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-display-table-light.webp deleted file mode 100644 index eabc9f3a..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-display-table-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-feedback-dialog-dark.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-feedback-dialog-dark.webp deleted file mode 100644 index 90f35dd1..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-feedback-dialog-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-feedback-dialog-light.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-feedback-dialog-light.webp deleted file mode 100644 index 4e811df1..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-feedback-dialog-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-foundation-colors-dark.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-foundation-colors-dark.webp deleted file mode 100644 index 71eeb1c1..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-foundation-colors-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-foundation-colors-light.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-foundation-colors-light.webp deleted file mode 100644 index dea74e91..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-foundation-colors-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-inputs-button-dark.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-inputs-button-dark.webp deleted file mode 100644 index 07a6e238..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-inputs-button-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-inputs-button-light.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-inputs-button-light.webp deleted file mode 100644 index d1f7e607..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-inputs-button-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-inputs-date-picker-dark.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-inputs-date-picker-dark.webp deleted file mode 100644 index fb2ec566..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-inputs-date-picker-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-inputs-date-picker-light.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-inputs-date-picker-light.webp deleted file mode 100644 index 5cb68e04..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-inputs-date-picker-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-inputs-text-field-dark.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-inputs-text-field-dark.webp deleted file mode 100644 index 24799fe2..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-inputs-text-field-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-inputs-text-field-light.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-inputs-text-field-light.webp deleted file mode 100644 index bbc7adb0..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-inputs-text-field-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-layout-app-shell-dark-1.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-layout-app-shell-dark-1.webp deleted file mode 100644 index f91e6d63..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-layout-app-shell-dark-1.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-layout-app-shell-dark-2.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-layout-app-shell-dark-2.webp deleted file mode 100644 index 24f28936..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-layout-app-shell-dark-2.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-layout-app-shell-dark.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-layout-app-shell-dark.webp deleted file mode 100644 index 33040dda..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-layout-app-shell-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-layout-app-shell-light-1.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-layout-app-shell-light-1.webp deleted file mode 100644 index afcafac0..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-layout-app-shell-light-1.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-layout-app-shell-light-2.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-layout-app-shell-light-2.webp deleted file mode 100644 index fd531e42..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-layout-app-shell-light-2.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-layout-app-shell-light.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-layout-app-shell-light.webp deleted file mode 100644 index c33532ff..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-layout-app-shell-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-light.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-light.webp deleted file mode 100644 index 15bf2c56..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-navigation-app-bar-dark.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-navigation-app-bar-dark.webp deleted file mode 100644 index 1cae07ae..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-navigation-app-bar-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-navigation-app-bar-light.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-navigation-app-bar-light.webp deleted file mode 100644 index 8fc736d7..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/docs-navigation-app-bar-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/home-dark-darwin.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/home-dark-darwin.webp new file mode 100644 index 00000000..715acd95 Binary files /dev/null and b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/home-dark-darwin.webp differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/home-dark.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/home-dark.webp deleted file mode 100644 index 64e6841d..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/home-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/home-light-darwin.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/home-light-darwin.webp new file mode 100644 index 00000000..41560f2c Binary files /dev/null and b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/home-light-darwin.webp differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/home-light.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/home-light.webp deleted file mode 100644 index 359efdb3..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/home-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/sheet-dark.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/sheet-dark.webp deleted file mode 100644 index ccd73ada..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/sheet-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/sheet-light.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/sheet-light.webp deleted file mode 100644 index b05137be..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/sheet-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-app-dashboard-dark-darwin.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-app-dashboard-dark-darwin.webp new file mode 100644 index 00000000..ac29a511 Binary files /dev/null and b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-app-dashboard-dark-darwin.webp differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-app-dashboard-dark.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-app-dashboard-dark.webp deleted file mode 100644 index a3d53676..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-app-dashboard-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-app-dashboard-light-darwin.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-app-dashboard-light-darwin.webp new file mode 100644 index 00000000..ad35731a Binary files /dev/null and b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-app-dashboard-light-darwin.webp differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-app-dashboard-light.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-app-dashboard-light.webp deleted file mode 100644 index b68c66e0..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-app-dashboard-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-auth-sign-in-dark-darwin.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-auth-sign-in-dark-darwin.webp new file mode 100644 index 00000000..d96a19f8 Binary files /dev/null and b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-auth-sign-in-dark-darwin.webp differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-auth-sign-in-dark.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-auth-sign-in-dark.webp deleted file mode 100644 index 38edf8b7..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-auth-sign-in-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-auth-sign-in-light-darwin.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-auth-sign-in-light-darwin.webp new file mode 100644 index 00000000..92fb29ff Binary files /dev/null and b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-auth-sign-in-light-darwin.webp differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-auth-sign-in-light.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-auth-sign-in-light.webp deleted file mode 100644 index d04fd64d..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-auth-sign-in-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-dark-darwin.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-dark-darwin.webp new file mode 100644 index 00000000..04f02f6f Binary files /dev/null and b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-dark-darwin.webp differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-dark.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-dark.webp deleted file mode 100644 index 130d5669..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-light-darwin.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-light-darwin.webp new file mode 100644 index 00000000..b630fc18 Binary files /dev/null and b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-light-darwin.webp differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-light.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-light.webp deleted file mode 100644 index 8f041add..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/templates-light.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/theme-dark-darwin.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/theme-dark-darwin.webp new file mode 100644 index 00000000..a97a441c Binary files /dev/null and b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/theme-dark-darwin.webp differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/theme-dark.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/theme-dark.webp deleted file mode 100644 index a889334a..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/theme-dark.webp and /dev/null differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/theme-light-darwin.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/theme-light-darwin.webp new file mode 100644 index 00000000..9243a21c Binary files /dev/null and b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/theme-light-darwin.webp differ diff --git a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/theme-light.webp b/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/theme-light.webp deleted file mode 100644 index 73aba7a0..00000000 Binary files a/site/tests/__screenshots__/visual-mobile/pages.visual.spec.ts/theme-light.webp and /dev/null differ diff --git a/site/tests/dist-html.spec.ts b/site/tests/dist-html.spec.ts new file mode 100644 index 00000000..98ad5a98 --- /dev/null +++ b/site/tests/dist-html.spec.ts @@ -0,0 +1,101 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { expect, test } from "@playwright/test"; +import { Window } from "happy-dom"; +import { DIST, ROUTES } from "./routes"; + +/** + * Page rules that are pure functions of the prerendered HTML, checked on `site/dist` with a DOM + * parser and no browser (32 ms a page, the whole site in about a second). It adds to the axe project + * and replaces none of it: axe checks what the browser renders; this checks what the server sent. + * Rules that depend on client rendering are NOT here (`page-has-heading-one` is false on the app + * templates, whose `

` renders in the island). + */ +const PAGES = ROUTES.filter( + (r) => !readFileSync(join(DIST, r, "index.html"), "utf8").includes('http-equiv="refresh"') +); + +/** + * Duplicate ids that exist today: a docs heading and a demo section slug to the same anchor, so the + * in-page link and the table of contents point at the first of the two. axe never reported them (its + * `duplicate-id` rules are outside the WCAG tag set the axe project runs). Listed so a NEW duplicate + * fails; remove an entry when its page is fixed. A fixed page still listed also fails. Fixing all of + * them and emptying this list is tracked in #548. + */ +const KNOWN_DUPLICATE_IDS: Record = { + "/docs/display/carousel/": ["autoplay"], + "/docs/display/table/": ["props"], + "/docs/docs-kit/docs/": ["docssearch"], + "/docs/feedback/skeleton/": ["the-skeleton-prop"], + "/docs/foundation/drag-and-drop/": ["drop-zones"], + "/docs/foundation/typography/": ["type-roles"], + "/docs/layout/app-shell/": ["panel-width"], + "/docs/layout/resizable-panels/": ["props"], + "/docs/map/": ["props"], + "/docs/navigation/command-palette/": ["async-results"], + "/docs/navigation/loading-boundary/": ["props"], + "/docs/navigation/menu/": [ + "checkbox-and-radio-items", + "submenus", + "leading-icons-and-brand-logos", + ], + "/docs/navigation/menubar/": ["props"], + "/docs/navigation/navigation-menu/": ["props"], +}; + +// happy-dom's nodes are not the DOM lib's `Element`: type only what is read. +type Named = { + textContent: string | null; + getAttribute(name: string): string | null; + querySelector(selector: string): unknown; +}; +const named = (e: Named) => + (e.textContent ?? "").trim() || + e.getAttribute("aria-label") || + e.getAttribute("aria-labelledby") || + e.getAttribute("title") || + e.querySelector("img[alt]:not([alt=''])") || + e.querySelector("svg title"); + +function problems(html: string): { problems: string[]; duplicateIds: string[] } { + const window = new Window({ + settings: { + disableJavaScriptEvaluation: true, + disableJavaScriptFileLoading: true, + disableCSSFileLoading: true, + }, + }); + window.document.write(html); + const d = window.document; + const out: string[] = []; + if (!d.title.trim()) out.push("document-title: empty "); + if (!d.documentElement.getAttribute("lang")) out.push("html-has-lang: <html> has no lang"); + if (!d.querySelector('meta[name="viewport"]')) out.push("meta-viewport: no viewport meta"); + const main = d.querySelectorAll("main").length; + if (main !== 1) out.push(`landmark-one-main: ${main} <main> elements`); + for (const img of d.querySelectorAll("img")) + if (!img.hasAttribute("alt") && img.getAttribute("role") !== "presentation") + out.push(`image-alt: <img src="${img.getAttribute("src")}"> has no alt`); + for (const a of d.querySelectorAll("a[href]")) + if (!named(a)) out.push(`link-name: <a href="${a.getAttribute("href")}"> has no name`); + + const seen = new Map<string, number>(); + for (const e of d.querySelectorAll("[id]")) seen.set(e.id, (seen.get(e.id) ?? 0) + 1); + const duplicateIds = [...seen].filter(([, n]) => n > 1).map(([id]) => id); + window.close(); + return { problems: out.slice(0, 10), duplicateIds }; +} + +test.describe("prerendered HTML", () => { + for (const route of PAGES) { + test(route, () => { + const { problems: found, duplicateIds } = problems( + readFileSync(join(DIST, route, "index.html"), "utf8") + ); + expect(found).toEqual([]); + expect(duplicateIds, "duplicate ids (known ones are listed in this file)").toEqual( + KNOWN_DUPLICATE_IDS[route] ?? [] + ); + }); + } +}); diff --git a/site/tests/fixtures.ts b/site/tests/fixtures.ts index 8cfe5e0e..4206e9ac 100644 --- a/site/tests/fixtures.ts +++ b/site/tests/fixtures.ts @@ -1,4 +1,7 @@ -import AxeBuilder from "@axe-core/playwright"; +import { readFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { dirname, join } from "node:path"; +import type AxeBuilder from "@axe-core/playwright"; import { test as base, expect, type Page, type TestInfo } from "@playwright/test"; export interface ThemeOptions { @@ -39,12 +42,74 @@ export const test = base.extend<{ theme: ThemeOptions; consoleErrors: string[] } export { expect }; -export function makeAxe(page: Page) { - return new AxeBuilder({ page }) - .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"]) - .exclude("canvas"); +const AXE_TAGS = ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"]; + +// The axe-core build @axe-core/playwright pins, read once: injected into the page like the builder +// does, but only once per page and frame instead of once per run. +const AXE_SOURCE = readFileSync( + join( + dirname( + createRequire(createRequire(import.meta.url).resolve("@axe-core/playwright")).resolve( + "axe-core" + ) + ), + "axe.min.js" + ), + "utf8" +); + +/** + * axe over the page with the WCAG 2.x A/AA tag set, `<canvas>` excluded, run by calling + * `axe.run` in the page. `@axe-core/playwright`'s `AxeBuilder` does the same run but re-injects axe + * and wires up every frame on each `analyze()`: measured on a quiet machine that is 568 ms against + * 155 ms on a 1,027-node docs page and 3,661 ms against 514 ms on /gallery/ (6.2k nodes), so a page + * checked three times (dark, light, forced colors) spent most of its time outside axe. Same + * builder-style API, so a caller reads the same: `makeAxe(page).exclude("pre").analyze()`. + */ +class Axe { + #exclude = ["canvas"]; + #off: string[] = []; + constructor(private page: Page) {} + + exclude(selector: string) { + this.#exclude.push(selector); + return this; + } + + disableRules(ids: string[]) { + this.#off.push(...ids); + return this; + } + + async analyze(): Promise<AxeResults> { + // A frame is audited too: axe in the top frame asks the axe in each frame over postMessage, and + // trusts only the origins it is configured with (a `srcdoc` frame's origin is `null`). + // A frame that cannot be scripted (cross-origin) is skipped. + for (const frame of this.page.frames()) { + const injected = await frame.evaluate(() => "axe" in window).catch(() => true); + if (injected) continue; + await frame + .evaluate(`${AXE_SOURCE};axe.configure({ allowedOrigins: ["<unsafe_all_origins>"] })`) + .catch(() => {}); + } + return this.page.evaluate( + ({ tags, off, exclude }) => + // biome-ignore lint/suspicious/noExplicitAny: `axe` is the global the injected source defines + (window as any).axe.run( + { exclude: exclude.map((selector) => [selector]) }, + { + runOnly: { type: "tag", values: tags }, + rules: Object.fromEntries(off.map((id) => [id, { enabled: false }])), + resultTypes: ["violations"], + } + ), + { tags: AXE_TAGS, off: this.#off, exclude: this.#exclude } + ) as Promise<AxeResults>; + } } +export const makeAxe = (page: Page) => new Axe(page); + type AxeResults = Awaited<ReturnType<AxeBuilder["analyze"]>>; /** Attaches the raw axe JSON and fails with a compact `{id, impact, helpUrl, targets}` list. */ @@ -67,10 +132,14 @@ export async function expectNoViolations(results: AxeResults, testInfo: TestInfo /** * Waits until the islands matching `selector` (default: all) have hydrated. Astro removes the - * `ssr` attribute from an `<astro-island>` once its component is mounted. `client:visible` - * islands only hydrate after entering the viewport, so pending ones are scrolled to first; the - * scroll position is restored afterwards. Islands inside a closed `<details>` (the demo "Code" - * panels) are never rendered, so they never intersect and are ignored. + * `ssr` attribute from an `<astro-island>` once its component is mounted and then fires + * `astro:hydrate` on it, so the wait wakes on that event instead of polling a timer. The event does + * not bubble and a nested island is re-created by its parent's hydration, so it is listened for in + * the capture phase on `document`. `client:visible` islands only hydrate after entering the + * viewport: the next pending one is scrolled to (nested ones become pending once their parent + * hydrated), and the scroll position is restored afterwards. Islands inside a closed `<details>` + * (the demo "Code" panels) are never rendered, so they never intersect and are ignored. The 50ms + * timeout is only a safety net for a hydration that finished between two checks. */ export async function waitIslands(page: Page, selector = "astro-island") { await page.evaluate(async (sel) => { @@ -78,19 +147,19 @@ export async function waitIslands(page: Page, selector = "astro-island") { const rendered = (el: HTMLElement) => !el.closest("details:not([open])"); const pending = () => [...document.querySelectorAll<HTMLElement>(`${sel}[ssr]`)].filter(rendered); - for (const el of pending()) { - if (el.getAttribute("client") !== "visible") continue; + let wake = () => {}; + const onHydrate = () => wake(); + document.addEventListener("astro:hydrate", onHydrate, true); + for (let p = pending(); p.length; p = pending()) { + const next = p.find((el) => el.getAttribute("client") === "visible"); // `astro-island` is `display: contents` (no box), so scroll its first rendered child. - (el.firstElementChild ?? el).scrollIntoView({ block: "center" }); + if (next) (next.firstElementChild ?? next).scrollIntoView({ block: "center" }); await new Promise<void>((resolve) => { - const t = setInterval(() => { - if (el.hasAttribute("ssr")) return; - clearInterval(t); - resolve(); - }, 25); + wake = resolve; + setTimeout(resolve, 50); }); } - while (pending().length) await new Promise((r) => setTimeout(r, 25)); + document.removeEventListener("astro:hydrate", onHydrate, true); scrollTo(0, y); }, selector); } @@ -102,11 +171,11 @@ export async function gotoReady(page: Page, path: string) { } /** - * Waits until the page is visually final: fonts loaded, finite animations done, async code - * highlighting swapped in, and document height unchanged for 500ms (late reflows would otherwise - * make full-page screenshots differ by a pixel row between captures). + * Waits until computed styles are final: fonts loaded, finite animations done, async code + * highlighting swapped in. Enough for axe (`color-contrast` reads computed colors, so a color + * transition in flight would be measured mid-way) and for DOM geometry audits. No time-based wait. */ -export async function settle(page: Page) { +export async function settleDom(page: Page) { await page.evaluate(async () => { await document.fonts.ready; // Infinite animations (spinners, blobs) never finish; only wait for finite ones. Poll `playState` @@ -132,8 +201,16 @@ export async function settle(page: Page) { (el) => !el.closest("details:not([open])") ) ); - // Page height AND chart canvas boxes must hold for 500ms: canvases are masked in visual - // snapshots, and a chart that mounts late moves its canvas between the two compared shots. +} + +/** + * `settleDom` plus: page height AND chart canvas boxes unchanged for 500ms. Canvases are hidden in + * visual snapshots, and a chart that mounts late moves its canvas between the two compared shots; + * late reflows would otherwise make screenshots differ by a pixel row between captures. The 500ms is + * a real stability window, so only screenshots pay for it. + */ +export async function settle(page: Page) { + await settleDom(page); await page.waitForFunction( () => new Promise<boolean>((resolve) => { diff --git a/site/tests/global-setup.ts b/site/tests/global-setup.ts new file mode 100644 index 00000000..05cd0ba0 --- /dev/null +++ b/site/tests/global-setup.ts @@ -0,0 +1,16 @@ +import type { FullConfig } from "@playwright/test"; + +/** + * The server answering on the base URL must be THIS site. A different project's dev server on the same + * port once served its own pages to this suite, whose screenshots were then committed as the baselines of + * this one. The home page names the product in its `<title>`; anything else stops the run before a test. + */ +export default async function globalSetup(config: FullConfig) { + const baseURL = config.projects[0]?.use.baseURL ?? ""; + const res = await fetch(`${baseURL}/`); + const title = (await res.text()).match(/<title>([^<]*)<\/title>/)?.[1] ?? ""; + if (!/CodeAtlas/i.test(title)) + throw new Error( + `${baseURL} is not the CodeAtlas UI site (its <title> is "${title}"): another project is using this port. Set E2E_PORT to a free one.` + ); +} diff --git a/site/tests/layout.spec.ts b/site/tests/layout-audit.ts similarity index 71% rename from site/tests/layout.spec.ts rename to site/tests/layout-audit.ts index d268a3e0..9b49f56b 100644 --- a/site/tests/layout.spec.ts +++ b/site/tests/layout-audit.ts @@ -1,8 +1,4 @@ -import { readFileSync } from "node:fs"; -import { join } from "node:path"; import type { Page } from "@playwright/test"; -import { expect, gotoReady, test } from "./fixtures"; -import { DIST, ROUTES } from "./routes"; /** * Layout integrity on every prerendered page (desktop + mobile projects): @@ -13,12 +9,7 @@ import { DIST, ROUTES } from "./routes"; */ const INTENTIONAL = ".ca-avatar-group, .ca-avatar, .ca-badge, .ca-pin, [class*='badge']"; -// Meta-refresh redirect stubs render nothing; their targets are routes of their own. -const PAGES = ROUTES.filter( - (r) => !readFileSync(join(DIST, r, "index.html"), "utf8").includes('http-equiv="refresh"') -); - -async function audit(page: Page, isMobile: boolean): Promise<string[]> { +export async function audit(page: Page, isMobile: boolean): Promise<string[]> { const issues = await page.evaluate((intentional) => { const out: string[] = []; const de = document.documentElement; @@ -154,49 +145,3 @@ async function audit(page: Page, isMobile: boolean): Promise<string[]> { } return issues; } - -/** Sign-in pages bounce a signed-in user to the dashboard: measure them signed out. */ -async function open(page: Page, route: string) { - if (route.endsWith("/sign-in/")) - await page.addInitScript(() => localStorage.removeItem("ca_auth")); - await gotoReady(page, route); -} - -for (const route of PAGES) { - test(`layout ${route}`, async ({ page, isMobile }) => { - // Narrowest common phone width: anything that fits at 360px fits on larger phones. - if (isMobile) await page.setViewportSize({ width: 360, height: 780 }); - await open(page, route); - expect(await audit(page, isMobile)).toEqual([]); - }); -} - -// The theme editor shows one panel at a time below 900px: each must hold the same guarantees. -for (const panel of ["Controls", "Code"]) { - test(`layout /theme/ (${panel} panel)`, async ({ page, isMobile }) => { - test.skip(!isMobile, "panels are tabs only on narrow screens"); - await page.setViewportSize({ width: 360, height: 780 }); - await gotoReady(page, "/theme/"); - await page.getByRole("tab", { name: panel }).click(); - expect(await audit(page, isMobile)).toEqual([]); - }); -} - -// Viewport sweep (viewport_complete §15.2): the in-between widths and a short landscape -// phone, where fixed heights and two-column breakpoints break first. Desktop project only: -// the device emulation stays fine-pointer, so this checks geometry, not touch targets. -const SWEEP = [ - { name: "tablet 768", width: 768, height: 1024 }, - { name: "laptop 1024", width: 1024, height: 768 }, - { name: "landscape 844x390", width: 844, height: 390 }, -] as const; -for (const vp of SWEEP) { - for (const route of PAGES) { - test(`layout ${route} @ ${vp.name}`, async ({ page, isMobile }) => { - test.skip(isMobile, "sweep runs once, on the desktop project"); - await page.setViewportSize({ width: vp.width, height: vp.height }); - await open(page, route); - expect(await audit(page, false)).toEqual([]); - }); - } -} diff --git a/site/tests/pages.visual.spec.ts b/site/tests/pages.visual.spec.ts index fa3da028..ab426fdc 100644 --- a/site/tests/pages.visual.spec.ts +++ b/site/tests/pages.visual.spec.ts @@ -1,29 +1,21 @@ import { expect, settle, test, waitIslands } from "./fixtures"; +// The pages whose own composition is what a screenshot can show: the landing, the templates and the theme +// editor, at the visible viewport (the first screen a visitor sees). Every docs page is a stack of component +// demos that the component baselines (packages/ui `*.vrt.test.ts`) already compare one by one, and the layout +// audit (routes.spec.ts) checks their geometry on every route, so a tall full-page docs shot added only the +// slowest and flakiest copy of both. const PAGES = [ "/", - "/docs/", - "/docs/foundation/colors/", - "/docs/inputs/button/", - "/docs/inputs/text-field/", - "/docs/inputs/date-picker/", - "/docs/feedback/dialog/", - "/docs/navigation/app-bar/", - "/docs/display/data-table/", - "/docs/display/table/", - "/docs/layout/app-shell/", + "/templates/", "/templates/app/dashboard/", "/templates/auth/sign-in/", - "/templates/", "/theme/", ]; -// A full-page shot is taken twice and compared (and written when updating). On a native 4-vCPU runner the -// 5s expect and 30s test defaults are enough; under amd64 emulation (scripts/vrt-docker.sh on Apple Silicon) -// the tallest pages (15k px) need longer. Per-call and per-test timeouts keep the rest of -// `expect.toHaveScreenshot` (stylePath, threshold, ...) from the config. +// Per-test timeout: a viewport shot is taken twice and compared (and written when updating); the +// 5 s expect and 30 s test defaults are enough on a native machine, so only the shot itself is bounded. const SHOT_TIMEOUT = 30_000; -test.describe.configure({ timeout: 120_000 }); const slug = (p: string) => (p === "/" ? "home" : p.replace(/^\/|\/$/g, "").replaceAll("/", "-")); @@ -42,55 +34,13 @@ for (const scheme of ["dark", "light"] as const) { for (const route of PAGES) { test(route, async ({ page }) => { await prepare(page, route); - // Layout heights are fractional (e.g. 2054.86px); a bare fullPage capture rounds them - // differently between the two shots Playwright compares. Pin an integer clip once. - const { width, height } = await page.evaluate(() => { - const r = document.documentElement.getBoundingClientRect(); - return { width: Math.floor(r.width), height: Math.floor(r.height) }; + // Visible viewport only: no `fullPage`, so no layout-height rounding between the two compared + // shots. No `mask`: Playwright places mask boxes from a layout read during the capture, and they + // drift between the shots; canvases are hidden in-layout by tests/screenshot.css (stylePath). + await expect(page).toHaveScreenshot(`${slug(route)}-${scheme}.webp`, { + timeout: SHOT_TIMEOUT, }); - // WebP stops at 16383 px per side (#336: /docs/display/data-table/ is 17488 px on - // mobile). Taller pages are captured in segments; a page that fits keeps one file. - const SEGMENT = 16000; - const parts = Math.ceil(height / SEGMENT); - for (let i = 0; i < parts; i++) { - const y = i * SEGMENT; - const name = - parts === 1 - ? `${slug(route)}-${scheme}.webp` - : `${slug(route)}-${scheme}-${i + 1}.webp`; - await expect(page).toHaveScreenshot(name, { - timeout: SHOT_TIMEOUT, - fullPage: true, - clip: { x: 0, y, width, height: Math.min(SEGMENT, height - y) }, - // No `mask`: Playwright places mask boxes from a layout read during the full-page - // capture, and they drift between the two compared shots (seen as a 23px shift on - // `/`). Canvases are hidden in-layout by tests/screenshot.css (stylePath) instead. - }); - } }); } - - test("overlays open", async ({ page }) => { - await prepare(page, "/docs/feedback/dialog/"); - await page.getByRole("button", { name: "View details" }).first().click(); - await settle(page); - await expect(page.getByRole("dialog")).toHaveScreenshot(`dialog-${scheme}.webp`, { - timeout: SHOT_TIMEOUT, - }); - - await prepare(page, "/docs/feedback/sheet/"); - await page.getByRole("button", { name: "Open sheet" }).first().click(); - await settle(page); - await expect(page.getByRole("dialog")).toHaveScreenshot(`sheet-${scheme}.webp`, { - timeout: SHOT_TIMEOUT, - }); - - await prepare(page, "/docs/navigation/command-palette/"); - await page.getByRole("button", { name: "Open command palette" }).first().click(); - await settle(page); - await expect(page.getByRole("dialog")).toHaveScreenshot(`command-palette-${scheme}.webp`, { - timeout: SHOT_TIMEOUT, - }); - }); }); } diff --git a/site/tests/routes.a11y.spec.ts b/site/tests/routes.a11y.spec.ts deleted file mode 100644 index fe93e9a6..00000000 --- a/site/tests/routes.a11y.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { expect, expectNoViolations, makeAxe, settle, test, waitIslands } from "./fixtures"; -import { ROUTES } from "./routes"; - -// One navigation per route, every accessibility check on it. The page follows the OS scheme -// (`system`), so `emulateMedia` switches dark → light → forced colors live, exactly like a user -// changing their OS setting, without reloading. -test.use({ theme: { scheme: "system" } }); - -for (const route of ROUTES) { - test(route, async ({ page }, testInfo) => { - // Redirect stubs (<meta http-equiv=refresh>) navigate away mid-test: check the raw HTML first. - const html = await (await page.request.get(route)).text(); - test.skip(/http-equiv="?refresh/i.test(html), "redirect page"); - // A signed-in user is bounced off sign-in pages (mid-test navigation): check them signed out. - if (route.endsWith("/sign-in/")) - await page.addInitScript(() => localStorage.removeItem("ca_auth")); - if (process.env.A11Y_CPU_THROTTLE) - await (await page.context().newCDPSession(page)).send("Emulation.setCPUThrottlingRate", { - rate: Number(process.env.A11Y_CPU_THROTTLE), - }); - await page.emulateMedia({ colorScheme: "dark" }); - await page.goto(route); - await waitIslands(page); - - // axe walks every node and this test runs it three times (dark, light, forced colors), so the - // work grows with the page: /docs/getting-started/recipes/ (10k nodes, mostly shiki token - // spans) needs 5.5 s per pass alone, /gallery/ (4.3k nodes) 5.7 s, a 1k-node docs page 0.6 s. - // A fixed 30 s fits the small pages and loses to load on the big ones (#533), and a hand-kept - // list of slow routes (#250, #316) misses the next big page. Budget each route from its own - // node count instead: a 15 s floor plus 3 passes at 4x the measured cost per node, 3x load. - const nodes = await page.evaluate(() => document.getElementsByTagName("*").length); - test.setTimeout(Math.max(30_000, 15_000 + nodes * 3 * 1.2 * 4)); - - // Pages that put a package shell in an island must still expose exactly one skip target. - expect(await page.locator("#main").count(), "elements with id=main").toBeLessThanOrEqual(1); - expect(await page.locator("main").count(), "<main> landmarks").toBeLessThanOrEqual(1); - expect(await page.locator('a[href="#main"]').count(), "skip links").toBeLessThanOrEqual(1); - - for (const scheme of ["dark", "light"] as const) { - await page.emulateMedia({ colorScheme: scheme }); - await expect(page.locator("html")).toHaveClass( - scheme === "dark" ? /\bdark\b/ : /^(?!.*\bdark\b)/ - ); - await settle(page); - await expectNoViolations(await makeAxe(page).analyze(), testInfo, scheme); - } - - // Forced colors (Windows High Contrast): the UA swaps every author color for a system color. - // axe-core's `color-contrast` reads the author colors and ignores that override, so the rule - // is off here (contrast is covered above and by packages/ui/tests/contrast.test.ts). Every - // other rule stays on: this pass catches names, roles and controls that stop being perceivable. - await page.emulateMedia({ colorScheme: "dark", forcedColors: "active" }); - await settle(page); - await expectNoViolations( - await makeAxe(page).disableRules(["color-contrast"]).analyze(), - testInfo, - "forced-colors" - ); - }); -} diff --git a/site/tests/routes.axe.spec.ts b/site/tests/routes.axe.spec.ts new file mode 100644 index 00000000..5007856f --- /dev/null +++ b/site/tests/routes.axe.spec.ts @@ -0,0 +1,67 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { expect, expectNoViolations, makeAxe, settleDom, test, waitIslands } from "./fixtures"; +import { DIST, ROUTES } from "./routes"; + +/** + * axe on every route, in its own Playwright project (`axe`) so it shards and runs beside the layout + * project. One load per route, three runs on it, every rule on in each (contrast included): + * + * - dark and light: `emulateMedia` switches the scheme live, like a user changing the OS setting; + * - forced colors (Windows High Contrast): the UA swaps every author color for a system color. + * axe's `color-contrast` reads author colors and ignores that override, so it is off here and + * measured in the two runs above; every other rule runs, which catches names, roles and controls + * that stop being perceivable. (Measured: with contrast on, a forced-colors run costs as much as + * a normal one and checks nothing new.) + * + * `<pre>` is excluded from every run: a docs page holds thousands of Shiki token spans (8.4k of the + * 10.1k nodes of /docs/getting-started/recipes/, 14.4 s per run, 1.9 s without them) and the + * highlighted palette is proven for both schemes by CodeSnippet.svelte.test.ts. A code listing is + * checked for structure by tests/dist-html.spec.ts. + * + * `A11Y_CPU_THROTTLE=<n>` (CDP CPU throttling) reproduces a loaded machine deterministically. + */ +const PAGES = ROUTES.filter( + (r) => !readFileSync(join(DIST, r, "index.html"), "utf8").includes('http-equiv="refresh"') +); + +// The page follows the OS scheme (`system`). +test.use({ theme: { scheme: "system" } }); + +for (const route of PAGES) { + test(route, async ({ page }, testInfo) => { + // A signed-in user is bounced off sign-in pages (mid-test navigation): check them signed out. + if (route.endsWith("/sign-in/")) + await page.addInitScript(() => localStorage.removeItem("ca_auth")); + if (process.env.A11Y_CPU_THROTTLE) + await (await page.context().newCDPSession(page)).send("Emulation.setCPUThrottlingRate", { + rate: Number(process.env.A11Y_CPU_THROTTLE), + }); + await page.emulateMedia({ colorScheme: "dark" }); + await page.goto(route, { waitUntil: "domcontentloaded" }); + await waitIslands(page); + + // axe walks every node (0.69 ms per node per run): the budget grows with the page. A fixed 30 s + // fits small pages and loses to load on the big ones (#533); a hand-kept list of slow routes + // misses the next big page (#250, #316). + const nodes = await page.evaluate(() => document.getElementsByTagName("*").length); + test.setTimeout(Math.max(30_000, 15_000 + nodes * 3 * 1.2 * 4)); + + for (const scheme of ["dark", "light"] as const) { + await page.emulateMedia({ colorScheme: scheme }); + await expect(page.locator("html")).toHaveClass( + scheme === "dark" ? /\bdark\b/ : /^(?!.*\bdark\b)/ + ); + await settleDom(page); + await expectNoViolations(await makeAxe(page).exclude("pre").analyze(), testInfo, scheme); + } + + await page.emulateMedia({ colorScheme: "dark", forcedColors: "active" }); + await settleDom(page); + await expectNoViolations( + await makeAxe(page).exclude("pre").disableRules(["color-contrast"]).analyze(), + testInfo, + "forced-colors" + ); + }); +} diff --git a/site/tests/routes.smoke.spec.ts b/site/tests/routes.smoke.spec.ts deleted file mode 100644 index 598ec0f3..00000000 --- a/site/tests/routes.smoke.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { expect, test, waitIslands } from "./fixtures"; -import { ROUTES } from "./routes"; - -test.describe("routes smoke", () => { - for (const route of ROUTES) { - test(route, async ({ page, consoleErrors }) => { - // A signed-in user is bounced off sign-in pages (mid-test navigation): check them signed out. - if (route.endsWith("/sign-in/")) - await page.addInitScript(() => localStorage.removeItem("ca_auth")); - const res = await page.goto(route); - expect(res?.status()).toBe(200); - await expect(page).toHaveTitle(/\S/); - await expect(page.getByRole("main")).toBeVisible(); - await expect(page.getByRole("main")).toHaveCount(1); - await waitIslands(page); - expect(consoleErrors, `console/page errors on ${route}`).toEqual([]); - }); - } -}); diff --git a/site/tests/routes.spec.ts b/site/tests/routes.spec.ts new file mode 100644 index 00000000..db845c6d --- /dev/null +++ b/site/tests/routes.spec.ts @@ -0,0 +1,98 @@ +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import type { Page } from "@playwright/test"; +import { sweepRoutes } from "../../scripts/route-impact"; +import { expect, gotoReady, settleDom, test, waitIslands } from "./fixtures"; +import { audit } from "./layout-audit"; +import { DIST, ROUTES } from "./routes"; + +/** + * One pass per route for everything except axe (routes.axe.spec.ts, its own project so it shards + * and runs beside this one). Each route is loaded once per project and the same load runs: + * + * - smoke: 200, a title, exactly one <main>, no console/page errors, one skip target; + * - layout audit (no sideways scroll, no overlap, no clipped text; mobile: 44px targets) at the + * project's own viewport: 1280x800 on desktop, 360x780 on mobile (the narrowest common phone); + * - desktop only, same load: the viewport sweep (in-between widths and a short landscape phone, + * resized inside the page) on routes that render layout primitives or changed. + * + * It replaced routes.smoke, layout and routes.a11y, which loaded every page 8 times between them. + */ +const REDIRECT_STUB = 'http-equiv="refresh"'; + +// Meta-refresh redirect stubs render nothing; their targets are routes of their own. +const PAGES = ROUTES.filter( + (r) => !readFileSync(join(DIST, r, "index.html"), "utf8").includes(REDIRECT_STUB) +); + +// Routes touched by the diff against origin/main (empty on a clean tree or without git/origin). +function changedFiles(): string[] { + try { + const run = (...args: string[]) => + execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }); + const base = run("merge-base", "HEAD", "origin/main").trim(); + return [ + ...run("diff", "--name-only", base).split("\n"), + ...run("ls-files", "-o", "--exclude-standard").split("\n"), + ].filter(Boolean); + } catch { + return []; + } +} +const SWEEP = sweepRoutes(PAGES, changedFiles()); + +// Viewport sweep (viewport_complete §15.2): the in-between widths and a short landscape phone, +// where fixed heights and two-column breakpoints break first. Desktop project only: the device +// emulation stays fine-pointer, so this checks geometry, not touch targets. +const VIEWPORTS = [ + { name: "tablet 768", width: 768, height: 1024 }, + { name: "laptop 1024", width: 1024, height: 768 }, + { name: "landscape 844x390", width: 844, height: 390 }, +] as const; + +for (const route of PAGES) { + test(route, async ({ page, isMobile, consoleErrors }) => { + // A signed-in user is bounced off sign-in pages (mid-test navigation): check them signed out. + if (route.endsWith("/sign-in/")) + await page.addInitScript(() => localStorage.removeItem("ca_auth")); + // Narrowest common phone width: anything that fits at 360px fits on larger phones. + if (isMobile) await page.setViewportSize({ width: 360, height: 780 }); + + const res = await page.goto(route, { waitUntil: "domcontentloaded" }); + expect(res?.status()).toBe(200); + await expect(page).toHaveTitle(/\S/); + await waitIslands(page); + await settleDom(page); + + // Smoke: exactly one landmark and one skip target (a package shell in an island must not add one). + expect(await page.locator("main").count(), "<main> landmarks").toBe(1); + expect(await page.locator("#main").count(), "elements with id=main").toBeLessThanOrEqual(1); + expect(await page.locator('a[href="#main"]').count(), "skip links").toBeLessThanOrEqual(1); + expect(consoleErrors, `console/page errors on ${route}`).toEqual([]); + + expect(await audit(page, isMobile), "layout at the project viewport").toEqual([]); + if (isMobile) return; + + if (SWEEP.has(route)) + for (const vp of VIEWPORTS) { + await page.setViewportSize({ width: vp.width, height: vp.height }); + // Breakpoints can mount or hide islands: settle again at the new width. + await waitIslands(page); + await settleDom(page); + expect(await audit(page, false), `layout @ ${vp.name}`).toEqual([]); + } + await page.setViewportSize({ width: 1280, height: 800 }); + }); +} + +// The theme editor shows one panel at a time below 900px: each must hold the same guarantees. +for (const panel of ["Controls", "Code"]) { + test(`/theme/ (${panel} panel, mobile)`, async ({ page, isMobile }) => { + test.skip(!isMobile, "panels are tabs only on narrow screens"); + await page.setViewportSize({ width: 360, height: 780 }); + await gotoReady(page, "/theme/"); + await page.getByRole("tab", { name: panel }).click(); + expect(await audit(page, isMobile)).toEqual([]); + }); +} diff --git a/site/tests/routes.ts b/site/tests/routes.ts index b6ad61cd..4aa4d184 100644 --- a/site/tests/routes.ts +++ b/site/tests/routes.ts @@ -1,6 +1,7 @@ import { existsSync, readdirSync, statSync } from "node:fs"; import { dirname, join, relative } from "node:path"; import { fileURLToPath } from "node:url"; +import { GALLERY_REPRESENTATIVES } from "../../scripts/route-impact"; export const DIST = process.env.E2E_DIST ?? join(dirname(fileURLToPath(import.meta.url)), "../dist"); @@ -18,9 +19,23 @@ function walk(dir: string): string[] { return out; } -/** Every prerendered page, derived from `dist/**\/index.html`. Embeds (`/landing-embed/`) are excluded. */ +/** + * Every prerendered page, derived from `dist/**\/index.html`. Embeds (`/landing-embed/`) are excluded. + * `E2E_ROUTES` (a JSON array of routes, set by `bun run test:changed`) narrows it to the routes a diff + * affects; every route spec follows it. + */ +const only: string[] | undefined = process.env.E2E_ROUTES + ? JSON.parse(process.env.E2E_ROUTES) + : undefined; +// `E2E_GALLERY=representatives` (the land gate, on a full sweep) keeps the gallery's item pages to the +// representatives: they are one viewer chrome around ~400 demos, each audited on its docs route and on its +// own item when it changes. `bun run test:full` (release) leaves it unset and audits every item. +const items = + process.env.E2E_GALLERY === "representatives" ? new Set(GALLERY_REPRESENTATIVES) : undefined; +const isItem = (p: string) => /^\/gallery\/[^/]+\/$/.test(p); export const ROUTES: string[] = walk(DIST) .map((file) => relative(DIST, dirname(file))) .map((p) => (p === "" ? "/" : `/${p}/`)) - .filter((p) => !p.startsWith("/landing-embed/")) + .filter((p) => !p.startsWith("/landing-embed/") && (!only || only.includes(p))) + .filter((p) => !items || !isItem(p) || items.has(p.split("/")[2])) .sort(); diff --git a/site/tests/timing-reporter.ts b/site/tests/timing-reporter.ts new file mode 100644 index 00000000..8df301d1 --- /dev/null +++ b/site/tests/timing-reporter.ts @@ -0,0 +1,15 @@ +import { appendFileSync } from "node:fs"; +import type { Reporter, TestCase, TestResult } from "@playwright/test/reporter"; + +/** Appends `{name, ms}` per test to `CA_TEST_TIMINGS` so scripts/test-run.ts can print the slowest ones. */ +export default class TimingReporter implements Reporter { + onTestEnd(test: TestCase, result: TestResult) { + const file = process.env.CA_TEST_TIMINGS; + if (!file) return; + const project = test.parent.project()?.name ?? ""; + appendFileSync( + file, + `${JSON.stringify({ name: `${project} ${test.title}`, ms: result.duration })}\n` + ); + } +} diff --git a/tsconfig.json b/tsconfig.json index 292d8972..3eeb9000 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,9 @@ }, "include": [ "scripts/**/*.js", + "scripts/route-impact.ts", + "scripts/test-run.ts", + "scripts/test-changed.ts", "packages/ui/scripts/**/*.ts", "packages/ui/tests/setup.unit.ts", "packages/ui/tests/stub-global.ts",