import { type Diagnostic, type Digest, type RenderResource } from '@variance-authority/core/format'; import type { Page } from 'playwright'; import { type BlankRule } from './blank.js'; /** * What a collector decides about the wire, and every default it can move. * * A value rather than a set of arguments because a collector that can open a * second world has to open both from the same one. `collectAlone` asks whether a * subject differs because another subject ran first, and the only reading that * answers it is one where isolation is the *only* difference — so the two worlds * share this object, and a hash setting or a blanking rule that drifted between * them would come back looking exactly like the pollution being hunted. * * Every field is a position rather than a preference: each one is either in the * environment key or in what the page was served, so changing it makes a * baseline read against a different recipe. */ export interface NetworkOptions { /** * Serve animated GIFs as their first frame. Defaults to `true`. * * See [`gif.ts`](gif.ts) for why doing this on the wire beats doing it in the * page, and `docs/stabilization.md` for what it is one of. */ readonly freezeAnimatedImages?: boolean; /** * Hash asset bodies into {@link NetworkObservation.assets}. Defaults to `true`. * * Off is a real position for a page whose assets are content-addressed already * — a URL that contains its own hash is an identity, and hashing the bytes * again buys nothing but the read. */ readonly hashAssets?: boolean; /** * Bytes above which an asset is recorded by size rather than by content. * Defaults to 8 MiB. * * A ceiling rather than a cliff: a video that would cost more to hash than to * download is recorded as `size:`, which still moves the key when the file * moves and says in the value itself that it is the weaker claim. Silently * skipping it would leave a hole in the key that nothing announces, and a hole * in this key is a false `unchanged`. */ readonly hashCeilingBytes?: number; /** * Images to serve as nothing, at their own size. Empty by default. * * The stronger relative of an ignore mask, and the reason it is stronger is * that it happens *first*: a mask hides pixels after a page has fetched an * image, laid out around it, and moved the environment key with its bytes. See * [`blank.ts`](blank.ts) for what it can and cannot decide — in one line, it * knows the URL and the intrinsic size, and it does not know the DOM. */ readonly blank?: readonly BlankRule[]; /** * Keep the bytes, not just their digest, so the document can be painted * somewhere with no route to this origin. Off by default. * * The read has already happened — every hashed type is fetched and its body * held long enough to digest it. This is retention, not acquisition, which is * why the portable document costs a map rather than a second crawl. * * What is kept is what was *served*: the blank an image became, the single * frame a GIF was truncated to, the bytes that actually reached the page. The * alternative — keeping what arrived — would paint a different picture later * than the run that observed it, which is the one thing an archive exists to * prevent. */ readonly retainResources?: boolean; /** * Leave the browser's HTTP cache on. Off by default. * * On, requests are held through the DevTools protocol instead of Playwright's * routing, which turns the cache off: a page whose context another page * already filled is answered from it, and every answer still passes here. An * image the same page already shows does not — it is drawn again without a * request — so only a caller that never calls `reset` may turn this on. * Chromium only; see `pause.ts`. */ readonly cache?: boolean; } export interface NetworkObservation { /** `EnvironmentInputs.assets`: request URL to content digest. */ readonly assets: Readonly>; /** URLs served as a single frame. Empty is the expected case. */ readonly frozen: readonly string[]; /** * Images served as nothing, and which rule did it. * * A ledger rather than a count, because blanking is the one intervention here * that can hide a real regression: an operator who blanked more than they meant * to needs to be able to read back exactly what disappeared, per subject, * without re-running with the feature off. */ readonly blanked: readonly BlankedAsset[]; /** Anything it could not do. Never a thrown error — see {@link observeNetwork}. */ readonly diagnostics: readonly Diagnostic[]; /** * Whether what was served can be handed to a renderer with no route to this * origin, and the bytes if it can. * * A result rather than a map, because "portable" is a claim that can fail and * the failure has to arrive at capture, holding the URLs, while the operator * is still standing next to the page that produced them. Discovering it at * render time means discovering it on a different machine, in a different * process, from a digest mismatch — the same fact, arriving where nobody can * act on it. * * Always `ok: false` when {@link NetworkOptions.retainResources} is off: no * bytes were kept, so nothing can be claimed. */ closure(): ResourceClosure; /** * Resolve when no image, font or media request is in flight, or say which are. * * Resolves `undefined` once the last one is answered. At `timeoutMs` with any * still open it resolves the refusal instead, naming them: a subject read * without its image records bytes nobody hashed, and the caller refuses it * rather than read it. Only asset requests count, so a long-poll never holds * it open, and only those started since the last settle or reset, so an * earlier subject's stall — named in that subject's refusal — never does. */ settle(timeoutMs?: number): Promise; /** * Forget what has been observed, for the page that is about to be loaded. * * The observation is per *page*, as a request carries no idea which subject * will use it. A caller navigating per subject resets per navigation and gets * exactly its subject's assets. One reusing a page across subjects — a * Storybook run — does not, so each subject carries the page's whole set: * over-invalidating, the direction to be wrong in. */ reset(): void; /** Stop routing. The recorded assets stay readable. */ close(): Promise; } /** One image that was replaced, with enough to find it again. */ /** * Either every served resource, or the names of the ones that stop the claim. * * `unresolved` carries a reason per URL rather than a bare list. An operator * reading "could not close 3 resources" has to go and find out why; the same * operator reading that a video is past the hash ceiling and two fonts were * never observed already knows which lever each one needs. */ export type ResourceClosure = { readonly ok: true; readonly resources: Readonly>; } | { readonly ok: false; readonly unresolved: readonly string[]; }; export interface BlankedAsset { readonly url: string; /** {@link BlankRule.id} of the rule that matched. */ readonly rule: string; readonly width: number; readonly height: number; } /** * Watch and, where it helps, rewrite what a page is served. * * **Nothing here may fail a page load.** Every branch that could throw ends in * the request continuing unmodified with a diagnostic recorded, because a * stabilizer that can take down the page it was stabilizing is worse than the * flake it prevents. The diagnostics are the difference between that policy and * silence. */ export declare function observeNetwork(page: Page, options?: NetworkOptions): Promise; //# sourceMappingURL=network.d.ts.map