import type { Page } from 'playwright'; import type { ScreenshotOptions, Viewport } from '@variance-authority/core/format'; /** * The longest side, in device pixels, of a subject this package photographs. * * Chromium's largest texture side, and past any image a person reviews: a * subject that long is a document, and its change is read as text. Not a * setting. A subject past it is refused with its size, never clipped, * because a clipped image claims a review of pixels nobody saw. */ export declare const MAX_CAPTURE_PIXELS = 16384; /** * The subject's pixels and its box, by the cheapest route that produces the same * bytes the element path produces. * * `locator.screenshot()` does not just capture. It first runs Playwright's * actionability wait, which requires the subject's box to hold still across two * consecutive animation frames — about 30 ms at 60 Hz, charged once per subject * whether or not anything on the page is moving. Measured by this package's own * benchmark (`yarn workspace @variance-authority/playwright host`), that wait is * most of a light subject's cost. Median ms per paint on macOS, one page reused: * * engine element clip * chromium 59.2 26.2 * firefox 33.3 8.5 * webkit 33.3 3.7 * * We have already settled the page ourselves — the recipe's waits have run, fonts * have loaded, and every network channel is blocked for the whole lease — so the * wait re-establishes something we have just established, at the price of the * paint. It is also why the engines looked closer together than they are: a * constant every engine pays compressed a 7x spread into a 1.8x one. The * benchmark's heavy-subject arm shows what that concealed — WebKit is first on * the subject above and last on one that rasterizes — so the saving here is not * uniform across engines and is largest exactly where the paint is cheapest. * * The two paths are byte-identical only if the clip is rounded the way the * element path rounds it: see {@link outward}. Verified across 240 randomly * generated subjects — fractional sizes, fractional offsets, borders, shadows, * axis-aligned transforms — in all three engines at both device scale factors. * * Three subjects keep the slow path, which is why this is a fallback and not a * replacement. Each was measured, not assumed: * * - **No reported box.** Detached, or `display: none`. There is no rectangle to * clip to, and the element path is left to raise its own error for it. * - **Not wholly inside the viewport.** A clip rect is viewport-relative and * Playwright refuses one that leaves it — a subject below the fold throws * `Clipped area is either empty or outside the resulting image` in all three * engines — while an oversized subject would come back silently truncated. The * element path scrolls and captures beyond the viewport, so it takes these. * - **Rotated or skewed.** The reported box is then not the rectangle the element * path captures, and the two disagree by a pixel or two on every edge. * * The transform test reads the composed matrix rather than the declared string, * so a rotation reached through any spelling — `rotate`, `skew`, `matrix`, a * shorthand — is caught by one check. It is deliberately conservative: * `rotate(90deg)` is axis-aligned in its result but still takes the slow path, * because being wrong here rewrites every baseline a user owns. * * Animation handling is unchanged and still the browser's: the recipe's * `animations: 'disabled'` is passed to whichever path runs. It fast-forwards a * finite animation to completion — the state a user comes to rest on — and * cancels an infinite one to its initial frame. Injected CSS can only pin frame * zero, which captures a fade-in at the moment it is invisible. * * The box is returned rather than left to the caller because the fast path needs * it first, and reading it twice would be two protocol round trips for one fact. * * A subject that occupies no pixels comes back with no bytes and no box, and * neither path is asked for a screenshot of it. Both refuse, in their own words * and about their own internals: the clip path with `Expected options.clip.height * to be greater than 0` and the element path with a visibility complaint. Those * sentences describe a rectangle argument, and the reader has a component that * rendered nothing — a wrapper whose only child went to a portal, a fixture * captured before its content mounted. Naming that is the caller's job, and the * caller is the one holding the subject id. */ export declare function captureSubject(page: Page, viewport: Viewport, options: ScreenshotOptions & { readonly type: 'png'; }): Promise; /** * A subject's pixels, or the fact that it has none. * * One nullable field would let a caller read the bytes of a subject that was * never photographed. Two, moving together, do not. */ export type SubjectCapture = { readonly bytes: Buffer; readonly box: Box; } | { readonly bytes: null; readonly box: null; }; /** A rectangle as Playwright reports it, in CSS pixels relative to the viewport. */ export interface Box { readonly x: number; readonly y: number; readonly width: number; readonly height: number; } /** * A fractional box snapped to the pixel grid the way the element path snaps it: * floor the near edge, ceil the far one, and take everything between. * * This is the union of the pixels the subject touches — what a compositor's * damage rect is — and it is the only rounding of the five tried that reproduces * the element path's bytes. Rounding the *extent* instead of the far *edge* is * the tempting mistake: the two differ whenever a box starts at a fraction, which * includes a box of integer size sitting at a fractional offset. */ export declare function outward(box: Box): Box; //# sourceMappingURL=capture.d.ts.map