import type { Page } from 'playwright'; import { type PageRegion, type RawCapture, type Recipe, type Viewport } from '@variance-authority/core/format'; import { type NetworkObservation, type NetworkOptions } from './network.js'; /** * A persistent Chromium harness: one browser, one page, one navigation, one * bundle injection, N captures. * * The economic claim of this project is that a semantic capture costs less than a * screenshot. A harness that spawns a browser per subject spends that saving * before it collects anything — process launch and first navigation dominate the * capture itself by roughly two orders of magnitude (see * `journal/0007-persistent-harness-and-p4.md` for the measurement). So the * expensive steps happen once and subjects are switched by calling into the page, * which is the difference between "cheaper than a screenshot" being an argument * and being a number. * * The cost of that decision is stated rather than hidden: subjects share a * document. Sharing one is only safe because the page agent is required to tear * down the previous subject completely — see {@link HarnessOptions.bundle}. A * bundle that leaks a stylesheet between subjects makes the *next* subject's * `unchanged` verdict vacuous, which is exactly the failure ADR-0003 is about. */ export interface HarnessOptions extends PageRegion { /** Page to navigate to, once. `file://` is fine and needs no server. */ readonly url: string; /** * JavaScript source installing a {@link PageAgent} at {@link AGENT_GLOBAL}. * * Injected as a classic script, so it must be an IIFE bundle rather than ESM — * a module would be evaluated asynchronously and the harness would have to poll * for the global instead of failing immediately when the bundle is broken. * * The agent owns subject teardown. The harness cannot do it: it does not know * what the previous subject installed. */ readonly bundle: string; readonly viewport: Viewport; /** See {@link CaptureRequest.fonts}. Omitted means the capture says so. */ readonly fonts?: readonly string[]; /** * Media features the project declares, such as `prefers-reduced-motion`. * A declared motion preference is the one {@link HarnessOptions.stabilize}'s * `reduced-motion` emulates. */ readonly features?: Readonly>; readonly assets?: Readonly>; /** * The stabilization recipe whose driver tricks — reduced motion, a fixed * clock, the pointer off the page — are applied before the first navigation. * Absent drives nothing. The ids applied are {@link Harness.driven}. */ readonly stabilize?: Recipe; /** Defaults to `fixture:`, matching the JSDOM measurement. */ readonly subjectId?: (subject: string) => string; /** Defaults to `true`. Set false to watch a case that is behaving oddly. */ readonly headless?: boolean; /** * Open the page in a context shared with every other harness of the same * viewport and region on the same browser. Defaults to `false`. * * The context is the browser's HTTP cache, so a page booted in one that * another page already filled is answered from memory: the second worker of a * run does not fetch the preview again. The pages also share cookies and * storage, which is why it is asked for rather than given. */ readonly warm?: boolean; /** * Launch a browser of this harness's own instead of leasing the process's * shared one. Defaults to `false`. * * For a reading whose point is that nothing else has touched its world. Every * other harness shares one browser per launch recipe, which is what keeps a * run with workers at one launch. */ readonly isolated?: boolean; /** * Run against the page *before* it is navigated for the first time. * * The hook exists because of a class of thing that has to be installed before * a load rather than after one: `observeNetwork` watches requests, and a * watcher attached after `goto` has already missed every asset the document * pulled in. The alternative — attach afterwards and reload — pays a second * page load per run to observe the first one, on every subject of every suite. * * Deliberately a hook and not a `network` option. The harness owns a browser * and nothing else; teaching it what an asset hash is would put a second * decision about the environment key in a file whose job is a page. */ readonly prepare?: (page: Page) => Promise; } export interface Harness { /** `chromium@`, as it lands in the environment key. */ readonly engine: string; /** * The timezone and locale the page was opened in, as they land in the * environment key: the declared pair, or `UTC` and `en-US`. */ readonly region: Required; /** * Render `(subject, variant)` in the shared page and return its capture. * * Sequential by contract. Two concurrent calls would render two subjects into * one document and let one decide the other's verdict. */ capture(subject: string, variant: string): Promise; /** * The driver tricks applied before the first navigation, for the page agent * to keep in the recipe it records. Empty when nothing was driven. */ readonly driven: readonly string[]; /** Console errors the page reported, in order. Empty is the expected case. */ pageErrors(): readonly string[]; /** Escape hatch for debugging a case by hand. Not part of the capture path. */ readonly page: Page; close(): Promise; } export declare function createHarness(options: HarnessOptions): Promise; /** * A harness whose page is watched from before its first navigation: by * {@link observeNetwork} when `network` asks for it, and always by * {@link watchArrivals}, which a page agent asks what is still arriving. * * The observation is the caller's to close, before the harness: it outlives the * harness's `prepare`, which is where it has to start, because a watcher * installed after `goto` has missed every asset the document pulled in. */ export declare function createObservedHarness(options: Omit, network: NetworkOptions | undefined): Promise<{ readonly harness: Harness; readonly network?: NetworkObservation; }>; /** * One capture with nothing reused: launch, navigate, inject, collect, close. * * This is the cost the persistent harness exists to avoid, kept in the shipped * code rather than only in the benchmark so that the comparison measures the same * capture path on both sides. A benchmark whose slow arm is a bespoke script * measures the script. */ export declare function captureOnce(options: HarnessOptions, subject: string, variant: string): Promise; //# sourceMappingURL=harness.d.ts.map