@flighthq/tool-capture 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/baselineStore.d.ts +5 -0
  2. package/dist/baselineStore.d.ts.map +1 -0
  3. package/dist/baselineStore.js +54 -0
  4. package/dist/baselineStore.js.map +1 -0
  5. package/dist/captureBrowser.d.ts +9 -0
  6. package/dist/captureBrowser.d.ts.map +1 -0
  7. package/dist/captureBrowser.js +83 -0
  8. package/dist/captureBrowser.js.map +1 -0
  9. package/dist/captureEntries.d.ts +11 -0
  10. package/dist/captureEntries.d.ts.map +1 -0
  11. package/dist/captureEntries.js +44 -0
  12. package/dist/captureEntries.js.map +1 -0
  13. package/dist/captureEntry.d.ts +83 -0
  14. package/dist/captureEntry.d.ts.map +1 -0
  15. package/dist/captureEntry.js +334 -0
  16. package/dist/captureEntry.js.map +1 -0
  17. package/dist/captureFormat.d.ts +6 -0
  18. package/dist/captureFormat.d.ts.map +1 -0
  19. package/dist/captureFormat.js +48 -0
  20. package/dist/captureFormat.js.map +1 -0
  21. package/dist/captureInterrupt.d.ts +3 -0
  22. package/dist/captureInterrupt.d.ts.map +1 -0
  23. package/dist/captureInterrupt.js +29 -0
  24. package/dist/captureInterrupt.js.map +1 -0
  25. package/dist/captureRenderTarget.d.ts +29 -0
  26. package/dist/captureRenderTarget.d.ts.map +1 -0
  27. package/dist/captureRenderTarget.js +27 -0
  28. package/dist/captureRenderTarget.js.map +1 -0
  29. package/dist/captureServer.d.ts +16 -0
  30. package/dist/captureServer.d.ts.map +1 -0
  31. package/dist/captureServer.js +142 -0
  32. package/dist/captureServer.js.map +1 -0
  33. package/dist/functionalScenes.d.ts +9 -0
  34. package/dist/functionalScenes.d.ts.map +1 -0
  35. package/dist/functionalScenes.js +64 -0
  36. package/dist/functionalScenes.js.map +1 -0
  37. package/dist/index.d.ts +10 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +10 -0
  40. package/dist/index.js.map +1 -0
  41. package/package.json +38 -0
  42. package/src/baselineStore.test.ts +59 -0
  43. package/src/captureBrowser.test.ts +12 -0
  44. package/src/captureEntries.test.ts +55 -0
  45. package/src/captureEntry.test.ts +32 -0
  46. package/src/captureFormat.test.ts +45 -0
  47. package/src/captureInterrupt.test.ts +29 -0
  48. package/src/captureRenderTarget.test.ts +11 -0
  49. package/src/captureServer.test.ts +19 -0
  50. package/src/functionalScenes.test.ts +55 -0
@@ -0,0 +1,5 @@
1
+ export type BaselineField = 'fingerprint' | 'sha256';
2
+ export declare function baselinePath(root: string, subject: string, name: string): string;
3
+ export declare function getBaselineField(root: string, subject: string, name: string, column: string, field: BaselineField): string | null;
4
+ export declare function setBaselineField(root: string, subject: string, name: string, column: string, field: BaselineField, value: string): void;
5
+ //# sourceMappingURL=baselineStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baselineStore.d.ts","sourceRoot":"","sources":["../src/baselineStore.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,aAAa,GAAG,aAAa,GAAG,QAAQ,CAAC;AAWrD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAGhF;AAED,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,aAAa,GACnB,MAAM,GAAG,IAAI,CAEf;AAED,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,aAAa,EACpB,KAAK,EAAE,MAAM,GACZ,IAAI,CAKN"}
@@ -0,0 +1,54 @@
1
+ // Per-test committed baseline store: one JSON file per test at <subject-root>/baselines/<name>.json,
2
+ // holding every column's values, e.g. { "canvas": { "fingerprint": "…", "sha256": "…" }, "flight:webgl": {…} }.
3
+ // captureEntry writes each column's `sha256` (screenshot hash); compare-render writes its `fingerprint`
4
+ // (coarse render fingerprint). Both read-merge-write so they preserve each other's fields and the other
5
+ // columns. Output is prettier-compatible (sorted keys, 2-space, trailing newline) so it never churns the
6
+ // format gate. Replaces the old tools/baselines/<subject>/<name>/<renderer>/{fingerprint.txt,baseline.sha256}.
7
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
8
+ import { dirname, join } from 'node:path';
9
+ // Per-subject baseline root: baselines colocate with their suite (functional/examples are
10
+ // top-level). One JSON file per test under the root's baselines/ dir.
11
+ const BASELINE_ROOTS = {
12
+ functional: 'functional',
13
+ examples: 'examples',
14
+ };
15
+ export function baselinePath(root, subject, name) {
16
+ const base = BASELINE_ROOTS[subject] ?? subject;
17
+ return join(root, base, 'baselines', `${name}.json`);
18
+ }
19
+ export function getBaselineField(root, subject, name, column, field) {
20
+ return readBaseline(baselinePath(root, subject, name))[column]?.[field] ?? null;
21
+ }
22
+ export function setBaselineField(root, subject, name, column, field, value) {
23
+ const path = baselinePath(root, subject, name);
24
+ const data = readBaseline(path);
25
+ (data[column] ??= {})[field] = value;
26
+ writeBaseline(path, data);
27
+ }
28
+ function readBaseline(path) {
29
+ if (!existsSync(path))
30
+ return {};
31
+ try {
32
+ return JSON.parse(readFileSync(path, 'utf8'));
33
+ }
34
+ catch {
35
+ return {};
36
+ }
37
+ }
38
+ // Stable, prettier-compatible serialisation: columns and fields in sorted order so a re-baseline of one
39
+ // column produces a minimal diff and the format gate stays green.
40
+ function writeBaseline(path, data) {
41
+ mkdirSync(dirname(path), { recursive: true });
42
+ const sorted = {};
43
+ for (const column of Object.keys(data).sort()) {
44
+ const entry = data[column];
45
+ const out = {};
46
+ if (entry.fingerprint !== undefined)
47
+ out.fingerprint = entry.fingerprint;
48
+ if (entry.sha256 !== undefined)
49
+ out.sha256 = entry.sha256;
50
+ sorted[column] = out;
51
+ }
52
+ writeFileSync(path, JSON.stringify(sorted, null, 2) + '\n');
53
+ }
54
+ //# sourceMappingURL=baselineStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baselineStore.js","sourceRoot":"","sources":["../src/baselineStore.ts"],"names":[],"mappings":"AAAA,qGAAqG;AACrG,gHAAgH;AAChH,wGAAwG;AACxG,wGAAwG;AACxG,yGAAyG;AACzG,+GAA+G;AAC/G,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAM1C,0FAA0F;AAC1F,sEAAsE;AACtE,MAAM,cAAc,GAA2B;IAC7C,UAAU,EAAE,YAAY;IACxB,QAAQ,EAAE,UAAU;CACrB,CAAC;AAEF,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,OAAe,EAAE,IAAY;IACtE,MAAM,IAAI,GAAG,cAAc,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC;IAChD,OAAO,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,IAAI,OAAO,CAAC,CAAC;AACvD,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,IAAY,EACZ,OAAe,EACf,IAAY,EACZ,MAAc,EACd,KAAoB;IAEpB,OAAO,YAAY,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC;AAClF,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,IAAY,EACZ,OAAe,EACf,IAAY,EACZ,MAAc,EACd,KAAoB,EACpB,KAAa;IAEb,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IAC/C,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAChC,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC;IACrC,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AAC5B,CAAC;AAED,SAAS,YAAY,CAAC,IAAY;IAChC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACjC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAiB,CAAC;IAChE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,wGAAwG;AACxG,kEAAkE;AAClE,SAAS,aAAa,CAAC,IAAY,EAAE,IAAkB;IACrD,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,MAAM,MAAM,GAAiB,EAAE,CAAC;IAChC,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;QAC9C,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;QAC3B,MAAM,GAAG,GAAmB,EAAE,CAAC;QAC/B,IAAI,KAAK,CAAC,WAAW,KAAK,SAAS;YAAE,GAAG,CAAC,WAAW,GAAG,KAAK,CAAC,WAAW,CAAC;QACzE,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS;YAAE,GAAG,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;QAC1D,MAAM,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC;IACvB,CAAC;IACD,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;AAC9D,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { Browser, BrowserContext } from '@playwright/test';
2
+ export declare function launchBrowser(options?: {
3
+ captureFrames?: number;
4
+ verify?: boolean;
5
+ }): Promise<{
6
+ browser: Browser;
7
+ context: BrowserContext;
8
+ }>;
9
+ //# sourceMappingURL=captureBrowser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"captureBrowser.d.ts","sourceRoot":"","sources":["../src/captureBrowser.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEhE,wBAAsB,aAAa,CACjC,OAAO,GAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACzD,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,cAAc,CAAA;CAAE,CAAC,CA+FxD"}
@@ -0,0 +1,83 @@
1
+ // Headless Chromium launch for a capture run, plus the pre-script init that makes every capture
2
+ // deterministic: a fixed viewport, capture-mode flag, seeded Math.random, and the optional frame-halt.
3
+ //
4
+ // Playwright is imported lazily (a dynamic import inside launchBrowser) so that importing this package
5
+ // has no top-level Playwright cost and pulls no browser driver into a consumer's module graph until a
6
+ // capture is actually run. Only the `chromium` value is loaded this way; the Browser/BrowserContext
7
+ // types are erased at compile time.
8
+ export async function launchBrowser(options = {}) {
9
+ const { chromium } = await import('@playwright/test');
10
+ // Headless Chromium exposes navigator.gpu but withholds a Wgpu adapter on a software-only,
11
+ // "untrusted" config unless --enable-unsafe-webgpu is set; with it, Dawn falls back to the
12
+ // SwiftShader Vulkan ICD bundled inside Playwright's Chromium (no host GPU / driver needed).
13
+ // --use-webgpu-adapter=swiftshader pins that software adapter so output is identical on a dev
14
+ // machine with a real GPU and in CI — required for stable cross-backend baselines.
15
+ const browser = await chromium.launch({
16
+ args: ['--enable-unsafe-webgpu', '--use-webgpu-adapter=swiftshader'],
17
+ });
18
+ const context = await browser.newContext({ viewport: { width: 800, height: 600 } });
19
+ // Always signal capture mode before any page script runs. A page whose render advances over time
20
+ // holds a fixed, self-seeded frame in this mode (the landing backgrounds do), so its screenshot —
21
+ // and thus its baseline hash — is byte-identical run to run. Pages that ignore the flag animate.
22
+ //
23
+ // Also seed Math.random with a fixed value so a scene built with random positions/colours renders
24
+ // the same on every load and backend — what makes the render fingerprint stable run-to-run (Tier 5)
25
+ // and lets the cross-backend differential (Tier 3) compare like with like. It does not pin the clock,
26
+ // so time-based animation is not frozen here — the --frames halt handles that. (Pages with their own
27
+ // seeded RNG, like the landing, are unaffected.) The generator is mulberry32 — the same algorithm as
28
+ // the SDK's createRandomSource (@flighthq/math), inlined below; see the note there for why it cannot
29
+ // be imported into this pre-module, bare-page init script.
30
+ //
31
+ // Optionally also install a frame-halt (the --frames=N mode): count the page's animation frames,
32
+ // and on the Nth one stop invoking its callback so the scene halts on a fixed frame, then flag it
33
+ // for the screenshot. N=1 is the robust common case (frame 1 always completes before the screenshot,
34
+ // with no settle-timing race).
35
+ const captureFrames = options.captureFrames ?? 0;
36
+ const verify = options.verify ?? true;
37
+ await context.addInitScript((args) => {
38
+ const flags = window;
39
+ const { frames, verify } = args;
40
+ flags.__flightCapture = true;
41
+ flags.__flightCaptureVerify = verify;
42
+ // Inline mulberry32 — the exact algorithm of the SDK's createRandomSource (@flighthq/math). It is
43
+ // copied (not imported) on purpose: addInitScript is serialized and injected before any module
44
+ // loads, and the SDK function's *built* source references bundler helpers (e.g. __name) that do
45
+ // not exist in this bare page context, so injecting its source breaks. Keep this in sync with that
46
+ // one function — it is the only duplicate, and it is trivial and frozen.
47
+ let seed = 0x9e3779b9 >>> 0;
48
+ Math.random = () => {
49
+ seed = (seed + 0x6d2b79f5) | 0;
50
+ let r = Math.imul(seed ^ (seed >>> 15), 1 | seed);
51
+ r = (r + Math.imul(r ^ (r >>> 7), 61 | r)) ^ r;
52
+ return ((r ^ (r >>> 14)) >>> 0) / 4294967296;
53
+ };
54
+ if (frames < 1)
55
+ return;
56
+ flags.__captureFramesReached = false;
57
+ // Force preserveDrawingBuffer so the halted Gl frame survives in the buffer for the screenshot
58
+ // (the default clears it once composited, which would shoot blank once the loop stops).
59
+ const realGetContext = HTMLCanvasElement.prototype.getContext;
60
+ HTMLCanvasElement.prototype.getContext = function (type, attrs) {
61
+ if (type === 'webgl' || type === 'webgl2' || type === 'experimental-webgl') {
62
+ return realGetContext.call(this, type, { ...attrs, preserveDrawingBuffer: true });
63
+ }
64
+ return realGetContext.call(this, type, attrs);
65
+ };
66
+ let count = 0;
67
+ const realRequestAnimationFrame = window.requestAnimationFrame.bind(window);
68
+ // Expose the un-hijacked rAF so the render verifier can await a genuine presented frame before it
69
+ // reads the canvas back. The override below stops invoking callbacks past the halt frame, so a
70
+ // verifier awaiting window.requestAnimationFrame could hang; it uses this stashed one instead.
71
+ flags.__ftRealRequestAnimationFrame = realRequestAnimationFrame;
72
+ window.requestAnimationFrame = (callback) => realRequestAnimationFrame((time) => {
73
+ if (count >= frames)
74
+ return; // halt: scene stops advancing on frame N
75
+ count++;
76
+ if (count >= frames)
77
+ flags.__captureFramesReached = true;
78
+ callback(time);
79
+ });
80
+ }, { frames: captureFrames, verify });
81
+ return { browser, context };
82
+ }
83
+ //# sourceMappingURL=captureBrowser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"captureBrowser.js","sourceRoot":"","sources":["../src/captureBrowser.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ export declare const RENDERERS: readonly ["dom", "canvas", "webgl", "webgpu"];
2
+ export type Tool = 'examples' | 'functional';
3
+ export interface Entry {
4
+ name: string;
5
+ renderers: string[];
6
+ }
7
+ export declare const BACKEND_UNAVAILABLE: RegExp;
8
+ export declare function discoverEntries(tool: Tool, root: string): Entry[];
9
+ export declare function rendererMatchesFilter(renderer: string, filter: readonly string[]): boolean;
10
+ export declare function routeSegment(renderer: string): string;
11
+ //# sourceMappingURL=captureEntries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"captureEntries.d.ts","sourceRoot":"","sources":["../src/captureEntries.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,SAAS,+CAAgD,CAAC;AACvE,MAAM,MAAM,IAAI,GAAG,UAAU,GAAG,YAAY,CAAC;AAE7C,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AASD,eAAO,MAAM,mBAAmB,QACgJ,CAAC;AAEjL,wBAAgB,eAAe,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,KAAK,EAAE,CAkBjE;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAI1F;AAID,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAErD"}
@@ -0,0 +1,44 @@
1
+ // Capture-subject vocabulary and discovery: the tool set, the renderer id helpers, and the per-tool
2
+ // enumeration of (entry, renderers) pairs the capture pipeline drives.
3
+ import { existsSync, readdirSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { discoverFunctionalScenes } from './functionalScenes.js';
6
+ export const RENDERERS = ['dom', 'canvas', 'webgl', 'webgpu'];
7
+ // A backend the environment cannot provide or sustain: WebGPU with no adapter/device, or a software
8
+ // adapter that loses its device mid-run under sustained per-frame GPU load. These are the only
9
+ // null-fingerprint / page-error outcomes that may be skipped rather than failed — the gate verifies
10
+ // backends where they exist and stays green on machines without them. A real render bug still fails:
11
+ // a validation error (bad writeBuffer/copy) is logged before the device dies, and is matched first.
12
+ // Shared with compare-render.ts so the smoke gate and the parity/regression gate agree on what counts
13
+ // as "unavailable".
14
+ export const BACKEND_UNAVAILABLE = /WebGPU adapter|WebGPU device|requestAdapter|requestDevice|GPUAdapter|WebGPU is not supported|external Instance reference no longer exists|device (was )?lost|device is lost/i;
15
+ export function discoverEntries(tool, root) {
16
+ // Functional scenes are flat files under functional/scenes/; the shared discovery is the single
17
+ // source of truth (also used by tools/functional/vite.config.ts).
18
+ if (tool === 'functional')
19
+ return discoverFunctionalScenes(join(root, 'functional', 'scenes'));
20
+ const dir = join(root, 'examples', 'packages');
21
+ if (!existsSync(dir))
22
+ return [];
23
+ return readdirSync(dir, { withFileTypes: true })
24
+ .filter((d) => d.isDirectory() && existsSync(join(dir, d.name, 'package.json')))
25
+ .sort((a, b) => a.name.localeCompare(b.name))
26
+ .map(({ name }) => {
27
+ const testDir = join(dir, name);
28
+ const customRenderers = RENDERERS.filter((r) => existsSync(join(testDir, `src/render.${r}.ts`)));
29
+ return { name, renderers: customRenderers };
30
+ })
31
+ .filter((e) => e.renderers.length > 0);
32
+ }
33
+ export function rendererMatchesFilter(renderer, filter) {
34
+ if (filter.length === 0)
35
+ return true;
36
+ const backend = renderer.includes(':') ? renderer.slice(renderer.indexOf(':') + 1) : renderer;
37
+ return filter.includes(renderer) || filter.includes(backend);
38
+ }
39
+ // A column id may carry a `<library>:<renderer>` colon; map it to a URL/dir-safe
40
+ // segment. Colon-free ids (canvas, webgl, …) pass through unchanged.
41
+ export function routeSegment(renderer) {
42
+ return renderer.replace(':', '-');
43
+ }
44
+ //# sourceMappingURL=captureEntries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"captureEntries.js","sourceRoot":"","sources":["../src/captureEntries.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,uEAAuE;AAEvE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AAEjE,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,CAAU,CAAC;AAQvE,oGAAoG;AACpG,+FAA+F;AAC/F,oGAAoG;AACpG,qGAAqG;AACrG,oGAAoG;AACpG,sGAAsG;AACtG,oBAAoB;AACpB,MAAM,CAAC,MAAM,mBAAmB,GAC9B,8KAA8K,CAAC;AAEjL,MAAM,UAAU,eAAe,CAAC,IAAU,EAAE,IAAY;IACtD,gGAAgG;IAChG,kEAAkE;IAClE,IAAI,IAAI,KAAK,YAAY;QAAE,OAAO,wBAAwB,CAAC,IAAI,CAAC,IAAI,EAAE,YAAY,EAAE,QAAQ,CAAC,CAAC,CAAC;IAE/F,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC;IAC/C,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,CAAC;IAChC,OAAO,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;SAC7C,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC;SAC/E,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;SAC5C,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE;QAChB,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAChC,MAAM,eAAe,GAAI,SAA+B,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CACpE,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,cAAc,CAAC,KAAK,CAAC,CAAC,CAChD,CAAC;QACF,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,eAAe,EAAE,CAAC;IAC9C,CAAC,CAAC;SACD,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,QAAgB,EAAE,MAAyB;IAC/E,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACrC,MAAM,OAAO,GAAG,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;IAC9F,OAAO,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;AAC/D,CAAC;AAED,iFAAiF;AACjF,qEAAqE;AACrE,MAAM,UAAU,YAAY,CAAC,QAAgB;IAC3C,OAAO,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;AACpC,CAAC"}
@@ -0,0 +1,83 @@
1
+ import type { BrowserContext } from '@playwright/test';
2
+ import type { Entry, Tool } from './captureEntries.js';
3
+ export interface CaptureStatus {
4
+ state: 'ready' | 'error';
5
+ capturedAt: number;
6
+ error: string | null;
7
+ hash: string | null;
8
+ /** The committed baseline sha256, or null when no baseline exists yet. */
9
+ baselineHash: string | null;
10
+ /** null = no baseline exists yet; false = hash matches baseline; true = hash differs from baseline */
11
+ changed: boolean | null;
12
+ }
13
+ export interface CaptureEntryOptions {
14
+ context: BrowserContext;
15
+ entry: Entry;
16
+ renderers: string[];
17
+ baseUrl: string;
18
+ tool: Tool;
19
+ outBase: string;
20
+ /** Repo root — committed baselines live at tests/<tool>/baselines/<name>.json. */
21
+ root: string;
22
+ /** When true, writes the current screenshot hash as the new baseline instead of comparing. */
23
+ updateBaseline?: boolean;
24
+ extraWait?: number;
25
+ /**
26
+ * When set (≥1), run the page's animation loop exactly this many frames, halt it on that frame,
27
+ * and screenshot it — a deterministic "capture the Nth frame" mode. Omit for the default behavior
28
+ * (render two frames and shoot, letting self-freezing pages like the landing hold their frame).
29
+ * Must match the value passed to launchBrowser, which installs the frame-halt in the page.
30
+ */
31
+ captureFrames?: number;
32
+ /**
33
+ * When true, an entry whose page logged an error or page error (a thrown exception, a failed
34
+ * request, or a render-verification failure from the functional harness) counts as a failure. This
35
+ * is the render smoke/not-blank gate: "CI green" then means every entry loaded and drew without
36
+ * error on every backend.
37
+ */
38
+ failOnError?: boolean;
39
+ /**
40
+ * Polled before each renderer: when it returns true the run is being interrupted (Ctrl+C), so the
41
+ * renderer loop stops and a torn-down page is not reported as a failure. See installAbortHandler.
42
+ */
43
+ isAborted?: () => boolean;
44
+ /**
45
+ * When set, replaces the renderer name in status lines. Used by captureParallel so each line
46
+ * identifies its entry: `entry.name/renderer` rather than just `renderer`.
47
+ */
48
+ displayLabel?: string;
49
+ }
50
+ export interface ParallelCaptureOptions {
51
+ context: BrowserContext;
52
+ entries: Entry[];
53
+ rendererFilter: string[];
54
+ baseUrl: string;
55
+ tool: Tool;
56
+ outBase: string;
57
+ root: string;
58
+ updateBaseline?: boolean;
59
+ extraWait?: number;
60
+ captureFrames?: number;
61
+ failOnError?: boolean;
62
+ isAborted?: () => boolean;
63
+ /** Number of Playwright pages to run concurrently. Default: 6. */
64
+ workerCount?: number;
65
+ }
66
+ export interface ParallelCaptureResult {
67
+ captured: number;
68
+ changed: number;
69
+ failed: number;
70
+ }
71
+ /** The artifact file paths for one (tool, entry, renderer) capture, under the output base directory. */
72
+ export interface CaptureOutputPaths {
73
+ outDir: string;
74
+ tmpScreenshot: string;
75
+ finalScreenshot: string;
76
+ tmpLogs: string;
77
+ finalLogs: string;
78
+ statusPath: string;
79
+ }
80
+ export declare function captureEntry(opts: CaptureEntryOptions): Promise<'ok' | 'changed' | 'error'>;
81
+ export declare function captureParallel(opts: ParallelCaptureOptions): Promise<ParallelCaptureResult>;
82
+ export declare function getCaptureOutputPaths(outBase: string, tool: Tool, name: string, renderer: string): CaptureOutputPaths;
83
+ //# sourceMappingURL=captureEntry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"captureEntry.d.ts","sourceRoot":"","sources":["../src/captureEntry.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,cAAc,EAAQ,MAAM,kBAAkB,CAAC;AAI7D,OAAO,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAMvD,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,0EAA0E;IAC1E,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,sGAAsG;IACtG,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,cAAc,CAAC;IACxB,KAAK,EAAE,KAAK,CAAC;IACb,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,IAAI,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb,8FAA8F;IAC9F,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC;IAC1B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,cAAc,CAAC;IACxB,OAAO,EAAE,KAAK,EAAE,CAAC;IACjB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,IAAI,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC;IAC1B,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wGAAwG;AACxG,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAQD,wBAAsB,YAAY,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,CA+PjG;AA2CD,wBAAsB,eAAe,CAAC,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CA8ClG;AAKD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,kBAAkB,CAUrH"}