Skip to content

Screenshot capture infrastructure ​

This prerequisite extends the default capture contract in Docs site, item 20. It changes no product route, component, fixture data or organization behavior. Existing surfaces keep their desktop light/dark outputs. A future isolated preview registers its own server and inputs; it does not extend the legacy fixture server merely to register a capture.

Contract ​

A surface declares its local fixture server and extra configuration files. Its captures default to desktop, 1440×900 at 2×, in light and dark, with the existing <surface>-<theme>.png filenames. Explicit profiles select their own dimensions and suffixes (for example a phone viewport with -phone). A profile describes viewport capture, not mobile user-agent or touch emulation. Only supported configuration is accepted; surface names, profiles, suffixes, themes, repository input paths and local preview routes are validated before generating, deleting or overwriting outputs. Output uniqueness is checked across the entire registry.

The manifest records every resolved capture, its theme, dimensions, output filename and screenshot options, together with the fixture configuration and deterministic browser settings. Its configuration hash is canonical and surface-local. The registry is literal data: adding an independent row does not change another surface's hash. All executable capture/manifest mechanics remain shared hashed inputs. The declared fixture server and configuration files, their rendering dependencies, the page's imports, shared renderer, styles and tokens remain tracked; unrelated execution wiring such as src/execution/factory.ts does not enter the manifests. Dependency hashes select the renderer, build and capture packages and their resolved lockfile graph (including installed optional and peer dependencies), not unrelated CLI dependencies or scripts. CSS imports/plugins, bundled icon collections and the capture launcher participate too. Relevant dependency declarations and build/capture scripts remain hashed; package release labels and unrelated lockfile entries do not. Binary inputs are hashed as bytes. Missing required dependencies fail closed.

Generation and checking use the same resolver, manifest constructor and drift predicate. A source change requires generation to refresh the source receipt; it does not by itself require browser capture. After building and starting the fixture, generation compares the last capture's actual GET resources: fixture-served JavaScript, CSS, fonts and images (resolved from the current build), the HTML shell and its seed, and supplemental fixture responses. Hashed asset references use stable filenames so unrelated build hashes do not invalidate a page. Only resources observed during capture participate; changed code or data forces a new capture that discovers new conditional requests. Executable capture mechanics, Chromium package identity and capture settings remain inputs. Comments and erased TypeScript types in capture mechanics do not invalidate images. Matching rendered inputs refresh the manifest without launching Chromium. Missing cache, images or resources, failed or unbounded response reads, non-GET or remote browser requests, changed inputs, and --force require capture. Timed fixture streams provide a finite snapshot of the same script and pacing, plus the executable replay and clock mechanics; idle feeds end after their initial frames for snapshot requests only. A fixture opts into snapshots by acknowledging the x-switchboard-preview-snapshot: 1 request header on its responses. Servers without this contract always capture; unsupported or unbounded responses cannot authorize reuse. The clock, locale, timezone, theme initialization, reduced motion, font readiness and settle behavior stay deterministic. Compatible captures reuse the historical warm browser context per fixture and theme/profile so cold font loading does not race scroll-to-result rendering. Generation selects an unused loopback port so parallel captures do not attach to another preview. Each fixture server accepts PORT and SWITCHBOARD_PREVIEW_NOW; captures explicitly select full capabilities and the local web bundle. Captures whose decoded pixels equal the existing PNG retain the original PNG bytes. A changed image requires a before/after comparison and an explanation, not blind acceptance of a golden.

Screenshot and manifest operations run in a short-lived output worker, leaving the generator's process-wide working directory unchanged while other threads run. The worker walks each directory component without following symlinks, verifies the opened inode against its pinned working directory, and runs synchronously before restoring it. A parent swap therefore cannot redirect existing-image reads, publication or stray cleanup. Existing files are read only through no-follow regular-file handles; publication uses a sibling temporary file and atomic rename. Import discovery uses Vue's SFC parser to select real script blocks, including mixed-case tags, while ignoring template comments and script-prefixed custom elements; malformed input fails within the test budget.

Validation criteria ​

CriterionProof
Green preview target reads return the exact allowed local transcript; absent target reads remain broad and unknown or empty targets refuse without broad fallback. Thread and Unit observers retain their current reply proof.[unit] web/green/previewSession.test.ts::green preview target session::returns the requested allowed local transcript without changing the broad destination, ::refuses unknown or empty target…, ::keeps the allowed…
Existing profiles, viewport and all existing output names are unchanged[unit] src/docs/screenshotManifest.test.ts::capture profiles::preserves all existing desktop outputs and themes
Existing PNG reads reject symlinks and non-files; a swapped output symlink cannot redirect publication[unit] src/docs/screenshotOutput.test.ts::screenshot output::rejects a symlinked prior PNG instead of reading its target, src/docs/screenshotOutput.test.ts::screenshot output::replaces a swapped output symlink without writing through it, src/docs/screenshotOutput.test.ts::screenshot output::rejects a directory in place of a prior PNG
Parent symlinks and swaps cannot redirect PNG reads, publication or manifest stray cleanup; callbacks are synchronous and errors restore cwd or fail closed[unit] src/docs/screenshotOutput.test.ts::screenshot output::rejects a symlinked screenshot directory before reading outside bytes, ::rejects a symlinked screenshot directory before publishing outside bytes, ::rejects a symlinked manifest directory before removing an outside stray, ::keeps reads, publication, and stray cleanup in the pinned directory after a parent swap, ::restores cwd after an operation throws, ::rejects an asynchronous callback and restores cwd, ::fails closed when the saved cwd path is replaced before restoration
The generator's cwd stays unchanged while a concurrent Worker uses relative paths, and capture operations use the isolated worker[unit] src/docs/screenshotOutput.test.ts::screenshot output::does not redirect a concurrent worker's relative write, ::routes capture reads, publication, and stray cleanup through the isolated worker, ::isolated worker rejects a symlinked output parent for every operation
Script imports are found in either tag case, template comments and script-prefixed custom elements are ignored, closing whitespace is accepted, and malformed tags take bounded time[unit] src/docs/screenshotManifest.test.ts::importSpecifiers::finds imports in upper and lower case script tags, ::ignores a script-prefixed custom element before a real script tag, ::ignores script-looking text inside a template comment, ::accepts whitespace before a closing script bracket, ::scans many unterminated script tags within the test budget
Desktop and phone select declared viewports and deterministic suffixes[unit] src/docs/screenshotManifest.test.ts::capture profiles::resolves desktop and phone into the browser and output options used by generation
Duplicate profiles, suffix collisions and unsafe or unsupported configuration fail before writes[unit] src/docs/screenshotManifest.test.ts::capture profiles::rejects ambiguous profiles and outputs, src/docs/screenshotManifest.test.ts::capture profiles::rejects unsafe paths and unsupported configuration
Resolved dimensions, profile, theme, route and output behavior are deterministically hashed[unit] src/docs/screenshotManifest.test.ts::capture profiles::hashes every resolved capture setting deterministically
Registering an isolated fixture/profile leaves existing hashes unchanged; executable mechanics remain tracked[unit] src/docs/screenshotManifest.test.ts::surface isolation::adding an isolated fixture and profile registration leaves existing hashes unchanged, src/docs/screenshotManifest.test.ts::surface isolation::does not exclude executable registration code or capture mechanics
Local inputs invalidate only their consumers; shared renderer and tokens invalidate every affected surface[unit] src/docs/screenshotManifest.test.ts::surface isolation::invalidates only consumers of a surface-local dependency, src/docs/screenshotManifest.test.ts::surface isolation::invalidates all consumers of shared rendering inputs
Actual imported dependencies and missing declared inputs cannot silently disappear[unit] src/docs/screenshotManifest.test.ts::surface isolation::tracks fixture and shared runtime import closures and fails on missing inputs
Execution wiring that cannot affect fixture pixels leaves every screenshot manifest unchanged[unit] src/docs/screenshotManifest.test.ts::listInputs::keeps execution wiring out of every screenshot manifest
Warm-context grouping preserves the historical theme ordering without mixing fixtures or viewports[unit] src/docs/screenshotManifest.test.ts::capture profiles::reuses one context per fixture and resolved browser profile in historical theme order
Dependency versions and binary inputs stay tracked without invalidating on package release labels[unit] src/docs/screenshotManifest.test.ts::surface isolation::tracks dependency versions without making a package release invalidate pictures, src/docs/screenshotManifest.test.ts::surface isolation::hashes binary local inputs by bytes rather than lossy text decoding
Dependency drift follows only the rendering/build/capture graph, resolves nested packages and optional/peer edges, and ignores unrelated dependencies/scripts and JSON key order[unit] src/docs/screenshotManifest.test.ts::capture dependency graph::ignores unrelated dependencies, scripts and lockfile entries, src/docs/screenshotManifest.test.ts::capture dependency graph::tracks renderer, build, capture, styles, icons and resolved transitive dependencies, src/docs/screenshotManifest.test.ts::capture dependency graph::tracks only the surfaces importing a local package, src/docs/screenshotManifest.test.ts::capture dependency graph::fails closed on missing required packages but allows absent optional edges, src/docs/screenshotManifest.test.ts::capture dependency graph::canonicalizes dependency metadata instead of hashing JSON key order
Bundled tarballs and linked workspace dependencies remain tracked without following unrelated hoisted packages or workspace release labels[unit] src/docs/screenshotManifest.test.ts::capture dependency graph::tracks bundled packages through the owning tarball instead of a hoisted namesake, src/docs/screenshotManifest.test.ts::capture dependency graph::follows workspace links without hashing their release labels
Unrelated CLI dependency and script edits leave every registered surface's hashes unchanged[unit] src/docs/screenshotManifest.test.ts::capture dependency graph::leaves every registered surface current after an unrelated CLI dependency or script changes
Every generated output is recorded, missing or extra output detected, and generation/check agree[unit] src/docs/screenshotManifest.test.ts::capture profiles::records every output and uses the same drift predicate for generation and check; [unit] src/docs/screenshotManifest.test.ts::the repository's screenshots::each surface's manifest inputs equal the tree's, and every picture is present
Changes to the mounted run route wrapper invalidate both run and pull-request captures[unit] src/docs/screenshotManifest.test.ts::listInputs::detects run and pull-request capture drift when their active route wrapper changes
Pixel-identical recaptures keep original PNG bytes[unit] src/docs/screenshotManifest.test.ts::PNG preservation::retains original bytes only when decoded dimensions and pixels match; [agent] the generation receipt must report byte retention for identical pixels, and a forced repeat must leave PNG bytes unchanged.
Existing images have been compared, and repeated captures remain deterministic[agent] Run npm run screenshots:gen, compare all existing PNGs against the base in decoded RGBA pixels, explain any changed pixels, then force a second generation and compare again. Run npm run screenshots:check; all declared outputs must be current.
Rendered inputs are retained with the manifest[unit] src/docs/screenshotManifest.test.ts::renderManifest::retains the rendered inputs and fixture requests used to decide whether capture is needed
Source-only changes and unrelated hashed asset names skip capture[unit] src/docs/screenshotRenderedInputs.test.ts::rendered screenshot inputs::skips capture after source-only changes and unrelated asset filename changes
Loaded component code, data, streams, fonts, response behavior and capture mechanics invalidate capture[unit] src/docs/screenshotRenderedInputs.test.ts::rendered screenshot inputs::captures changes to loaded component code, styles, fixture data, streams, fonts and response behavior
Missing receipts, images or resources and changed settings cannot reuse capture[unit] src/docs/screenshotRenderedInputs.test.ts::rendered screenshot inputs::requires capture for missing cache, unavailable inputs, missing images and changed settings, ::fails closed on absent assets, ambiguous names, remote requests and failed fixture reads
Comments and erased types do not invalidate capture; executable mechanics and Chromium do[unit] src/docs/screenshotRenderedInputs.test.ts::rendered screenshot inputs::ignores comments and erased types while keeping executable capture code and Chromium pinned
A backend comment edit refreshes receipts without changing PNGs or launching Chromium[agent] After one generation has recorded rendered inputs, append a comment to a fixture dependency and run npm run screenshots:gen; every affected surface reports rendered inputs unchanged — skipped browser capture. Restore the comment, regenerate and run npm run screenshots:check. Compare PNG hashes with the original set.
Actual asset response bodies, statuses, headers and snapshot support remain rendering inputs even with unchanged emitted files[unit] src/docs/screenshotRenderedInputs.test.ts::rendered screenshot inputs::compares served asset bodies, status and headers instead of trusting emitted bytes
Live stream replay selection and clock code invalidate capture while comments do not[unit] src/docs/screenshotRenderedInputs.test.ts::rendered screenshot inputs::invalidates live snapshots when replay selection or clock code changes, but ignores comments
Same-name chunks from different frontend entries retain separate loaded assets and response fingerprints[unit] src/docs/screenshotRenderedInputs.test.ts::rendered screenshot inputs::keeps same-name chunks from different frontend entries distinct