@flighthq/tool-capture 0.2.0 → 0.2.1-next.423.3350e53

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"functionalVerify.d.ts","sourceRoot":"","sources":["../src/functionalVerify.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACV,iBAAiB,EACjB,aAAa,EACb,cAAc,EACd,aAAa,EACb,OAAO,EACP,eAAe,EAChB,MAAM,iBAAiB,CAAC;AAEzB,eAAO,MAAM,iCAAiC,oBAAoB,CAAC;AAMnE,MAAM,MAAM,sBAAsB,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1F,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,iBAAiB,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,KAAK,CAAC;IACZ,KAAK,EAAE,cAAc,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,aAAa,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,oBAAoB;IACnC,YAAY,CAAC,EAAE,sBAAsB,CAAC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,eAAe,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,MAAM,gBAAgB,GAAG,sBAAsB,GAAG,mBAAmB,GAAG,kBAAkB,GAAG,oBAAoB,CAAC;AAgBxH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAenE;AAED,wBAAgB,wBAAwB,CAAC,CAAC,SAAS,gBAAgB,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAGjF;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,eAAe,EAAE,KAAK,SAAI,GAAG,IAAI,CAUpF;AAED,wBAAsB,qBAAqB,CAAC,UAAU,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA+B3G;AAED,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAQxE"}
@@ -0,0 +1,140 @@
1
+ import { createSurfaceFromWgpuRenderState, enableWgpuFrameCapture } from '@flighthq/render-wgpu';
2
+ import { createSurface, createSurfaceFingerprint, createSurfaceFromImageSource, formatSurfaceFingerprint, getSurfaceCoverage, getSurfacePixel, } from '@flighthq/surface';
3
+ export const FUNCTIONAL_VERIFICATION_IMAGE_KEY = '__ftRenderImage';
4
+ const DEFAULT_MIN_COVERAGE = 0.0008;
5
+ const BACKGROUND_CHANNEL_TOLERANCE = 6;
6
+ const FINGERPRINT_GRID = 16;
7
+ // Synchronously reads the registered webgl target's default framebuffer and publishes it for the capture
8
+ // harness (sets __ftVerification + __ftRenderImage). Unlike runRenderVerification it does NOT wait for a
9
+ // presented frame — so it MUST be called in the same task as the draw (right after presentGlScene in an
10
+ // animation frame), while the drawing buffer is still valid. This is what lets an animated app read back
11
+ // without preserveDrawingBuffer:true (which a wait-then-read would need, and which breaks on-screen
12
+ // animation on some drivers). Returns true once a non-blank frame was published; the caller stops calling
13
+ // it then, and retries on the next frame while it returns false.
14
+ export function publishFunctionalRenderSync(render) {
15
+ const target = window.__ftTarget;
16
+ if (target?.kind !== 'webgl')
17
+ return false;
18
+ const surface = createSurfaceFromGlRenderState(target.state);
19
+ if (surface === null)
20
+ return false;
21
+ const background = getSurfacePixel(surface, 0, 0);
22
+ const coverage = getSurfaceCoverage(surface, background, BACKGROUND_CHANNEL_TOLERANCE);
23
+ if (coverage < DEFAULT_MIN_COVERAGE)
24
+ return false;
25
+ window.__ftVerification = {
26
+ render,
27
+ coverage,
28
+ fingerprint: formatSurfaceFingerprint(createSurfaceFingerprint(surface, FINGERPRINT_GRID)),
29
+ };
30
+ window.__ftRenderImage = encodeSurfaceToDataUrl(surface);
31
+ return true;
32
+ }
33
+ export function registerFunctionalTarget(target) {
34
+ window.__ftTarget = target;
35
+ return target;
36
+ }
37
+ export function registerWgpuFunctionalTarget(state, scale = 1) {
38
+ enableWgpuFrameCapture(state);
39
+ registerFunctionalTarget({
40
+ kind: 'webgpu',
41
+ state,
42
+ width: state.canvas.width,
43
+ height: state.canvas.height,
44
+ scale,
45
+ render: () => { },
46
+ });
47
+ }
48
+ export async function runRenderVerification(testModule, render) {
49
+ const result = { render, coverage: null, fingerprint: null };
50
+ window.__ftVerification = result;
51
+ if (render === 'dom') {
52
+ const target = window.__ftTarget;
53
+ if (target?.kind === 'dom') {
54
+ const element = target.state.element;
55
+ const hasContent = element.childElementCount > 0 || (element.textContent ?? '').trim() !== '';
56
+ if (!hasContent)
57
+ throw new Error(`[verify:${render}] blank render: no DOM output produced`);
58
+ }
59
+ return;
60
+ }
61
+ await waitForPresentedFrame();
62
+ const surface = await snapshotFunctionalRender();
63
+ if (surface === null)
64
+ return;
65
+ const background = getSurfacePixel(surface, 0, 0);
66
+ const coverage = getSurfaceCoverage(surface, background, BACKGROUND_CHANNEL_TOLERANCE);
67
+ result.coverage = coverage;
68
+ result.fingerprint = formatSurfaceFingerprint(createSurfaceFingerprint(surface, FINGERPRINT_GRID));
69
+ const minCoverage = testModule.minCoverage ?? DEFAULT_MIN_COVERAGE;
70
+ if (coverage < minCoverage) {
71
+ throw new Error(`[verify:${render}] blank render: coverage ${coverage.toFixed(5)} below ${minCoverage}`);
72
+ }
73
+ await testModule.assertRender?.(surface);
74
+ window.__ftRenderImage = encodeSurfaceToDataUrl(getFunctionalRenderImageSurface() ?? surface);
75
+ }
76
+ export async function snapshotFunctionalRender() {
77
+ const target = window.__ftTarget;
78
+ if (target?.kind === 'dom')
79
+ return null;
80
+ if (target?.kind === 'webgpu')
81
+ return createSurfaceFromWgpuRenderState(target.state);
82
+ const canvas = target ? target.state.canvas : findRenderCanvas();
83
+ if (canvas === null || canvas.width === 0 || canvas.height === 0)
84
+ return null;
85
+ if (target?.kind === 'webgl')
86
+ target.state.gl.finish();
87
+ return createSurfaceFromImageSource(canvas, canvas.width, canvas.height);
88
+ }
89
+ function getFunctionalRenderImageSurface() {
90
+ const target = window.__ftTarget;
91
+ if (target?.kind !== 'webgl')
92
+ return null;
93
+ return createSurfaceFromGlRenderState(target.state);
94
+ }
95
+ function createSurfaceFromGlRenderState(state) {
96
+ const canvas = state.canvas;
97
+ const width = canvas.width;
98
+ const height = canvas.height;
99
+ if (width === 0 || height === 0)
100
+ return null;
101
+ const gl = state.gl;
102
+ gl.finish();
103
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
104
+ const bottomUp = new Uint8Array(width * height * 4);
105
+ gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, bottomUp);
106
+ const surface = createSurface(width, height);
107
+ const out = surface.data;
108
+ const rowBytes = width * 4;
109
+ for (let y = 0; y < height; y++) {
110
+ const srcRow = (height - 1 - y) * rowBytes;
111
+ const dstRow = y * rowBytes;
112
+ out.set(bottomUp.subarray(srcRow, srcRow + rowBytes), dstRow);
113
+ }
114
+ return surface;
115
+ }
116
+ function encodeSurfaceToDataUrl(surface) {
117
+ const canvas = document.createElement('canvas');
118
+ canvas.width = surface.width;
119
+ canvas.height = surface.height;
120
+ const ctx = canvas.getContext('2d');
121
+ if (ctx === null)
122
+ return '';
123
+ ctx.putImageData(new ImageData(new Uint8ClampedArray(surface.data), surface.width, surface.height), 0, 0);
124
+ return canvas.toDataURL('image/png');
125
+ }
126
+ function findRenderCanvas() {
127
+ let best = null;
128
+ for (const canvas of document.querySelectorAll('canvas')) {
129
+ if (best === null || canvas.width * canvas.height > best.width * best.height)
130
+ best = canvas;
131
+ }
132
+ return best;
133
+ }
134
+ function waitForPresentedFrame() {
135
+ const raf = window.__ftRealRequestAnimationFrame ?? window.requestAnimationFrame.bind(window);
136
+ return new Promise((resolve) => {
137
+ raf(() => raf(() => resolve()));
138
+ });
139
+ }
140
+ //# sourceMappingURL=functionalVerify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"functionalVerify.js","sourceRoot":"","sources":["../src/functionalVerify.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -7,4 +7,6 @@ export * from './captureInterrupt';
7
7
  export * from './captureRenderTarget';
8
8
  export * from './captureServer';
9
9
  export * from './functionalScenes';
10
+ export * from './functionalVerify';
11
+ export * from './referenceCapture';
10
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC"}
package/dist/index.js CHANGED
@@ -7,4 +7,6 @@ export * from './captureInterrupt';
7
7
  export * from './captureRenderTarget';
8
8
  export * from './captureServer';
9
9
  export * from './functionalScenes';
10
+ export * from './functionalVerify';
11
+ export * from './referenceCapture';
10
12
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC"}
@@ -0,0 +1,31 @@
1
+ import type { Entry } from './captureEntries.js';
2
+ export interface ReferenceCaptureOptions {
3
+ /** The flight-reference checkout root (contains content/frameworks/…). */
4
+ checkoutDir: string;
5
+ /** The Flight monorepo root, passed to the dev server as FLIGHT_REPO so @flighthq/* resolve to source. */
6
+ repoRoot: string;
7
+ /** Only capture cases whose `framework/corpus/case` id contains this substring. */
8
+ filter?: string;
9
+ captureFrames?: number;
10
+ extraWait?: number;
11
+ updateBaseline?: boolean;
12
+ failOnError?: boolean;
13
+ /** Eyes mode: never fail closed on a blank render — emit a screenshot + diagnostics per case. */
14
+ observe?: boolean;
15
+ /** Output base; artifacts land at {outBase}/reference/{framework/corpus/case}/{renderer}/. */
16
+ outBase: string;
17
+ workerCount?: number;
18
+ }
19
+ export interface ReferenceCaptureResult {
20
+ captured: number;
21
+ changed: number;
22
+ failed: number;
23
+ aborted: boolean;
24
+ }
25
+ export declare function discoverReferenceEntries(checkoutDir: string): Entry[];
26
+ export declare function runReferenceCapture(options: Readonly<ReferenceCaptureOptions>): Promise<ReferenceCaptureResult>;
27
+ export declare function startReferenceDevServer(checkoutDir: string, repoRoot: string): Promise<{
28
+ url: string;
29
+ kill: () => void;
30
+ }>;
31
+ //# sourceMappingURL=referenceCapture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"referenceCapture.d.ts","sourceRoot":"","sources":["../src/referenceCapture.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAMjD,MAAM,WAAW,uBAAuB;IACtC,0EAA0E;IAC1E,WAAW,EAAE,MAAM,CAAC;IACpB,0GAA0G;IAC1G,QAAQ,EAAE,MAAM,CAAC;IACjB,mFAAmF;IACnF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,iGAAiG;IACjG,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,8FAA8F;IAC9F,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CAClB;AAKD,wBAAgB,wBAAwB,CAAC,WAAW,EAAE,MAAM,GAAG,KAAK,EAAE,CAuBrE;AAID,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,QAAQ,CAAC,uBAAuB,CAAC,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAuDrH;AASD,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,IAAI,CAAA;CAAE,CAAC,CAiC5C"}
@@ -0,0 +1,142 @@
1
+ // The reference-subject capture driver: start flight-reference's Vite dev server (pointed at a Flight
2
+ // checkout via FLIGHT_REPO), enumerate its framework/corpus/case routes, and drive
3
+ // the shared captureEntry path with verify:true. This lives in tool-capture — beside the in-page
4
+ // verifier (functionalVerify) and the capture core (captureEntry) — so BOTH ends of the reference
5
+ // capture are contained in one package; scripts/reference-capture.ts is a thin CLI over runReferenceCapture.
6
+ import { spawn } from 'node:child_process';
7
+ import { existsSync, readdirSync } from 'node:fs';
8
+ import { join, resolve } from 'node:path';
9
+ import { launchBrowser } from './captureBrowser.js';
10
+ import { routeSegment } from './captureEntries.js';
11
+ import { captureParallel } from './captureEntry.js';
12
+ import { formatSummaryCount, formatSummaryLine } from './captureFormat.js';
13
+ import { installAbortHandler } from './captureInterrupt.js';
14
+ // One capture Entry per flight-reference case: name = framework/corpus/case, and a `route` producing the
15
+ // harness's `${framework}-tests/${corpus}/${case}/flight/${renderer}/` URL. Only cases with a Flight
16
+ // implementation (flight/src/app.ts) are captured; every framework (openfl/starling/awayjs) is included.
17
+ export function discoverReferenceEntries(checkoutDir) {
18
+ const frameworksDir = join(checkoutDir, 'content', 'frameworks');
19
+ const entries = [];
20
+ if (!existsSync(frameworksDir))
21
+ return entries;
22
+ for (const framework of readdirSync(frameworksDir, { withFileTypes: true })) {
23
+ if (!framework.isDirectory())
24
+ continue;
25
+ const frameworkDir = join(frameworksDir, framework.name);
26
+ for (const corpus of readdirSync(frameworkDir, { withFileTypes: true })) {
27
+ if (!corpus.isDirectory())
28
+ continue;
29
+ const corpusDir = join(frameworkDir, corpus.name);
30
+ for (const c of readdirSync(corpusDir, { withFileTypes: true })) {
31
+ if (!c.isDirectory() || c.name === '_shared')
32
+ continue;
33
+ if (!existsSync(join(corpusDir, c.name, 'flight', 'src', 'app.ts')))
34
+ continue;
35
+ const routePrefix = `${framework.name}-tests`;
36
+ entries.push({
37
+ name: `${framework.name}/${corpus.name}/${c.name}`,
38
+ renderers: ['webgl'],
39
+ route: (renderer) => `${routePrefix}/${corpus.name}/${c.name}/flight/${routeSegment(renderer)}/`,
40
+ });
41
+ }
42
+ }
43
+ }
44
+ return entries.sort((a, b) => a.name.localeCompare(b.name));
45
+ }
46
+ // The full reference-capture session: enumerate, start the dev server, drive captureEntry (verify:true) in
47
+ // parallel, then tear the server + browser down. Prints the same summary line as the monorepo capture.
48
+ export async function runReferenceCapture(options) {
49
+ let entries = discoverReferenceEntries(options.checkoutDir);
50
+ if (options.filter)
51
+ entries = entries.filter((e) => e.name.includes(options.filter));
52
+ if (entries.length === 0)
53
+ throw new Error(`No reference cases found filter="${options.filter ?? ''}"`);
54
+ console.log('Starting flight-reference Vite dev server…');
55
+ const server = await startReferenceDevServer(options.checkoutDir, options.repoRoot);
56
+ console.log(`Ready at ${server.url}\n`);
57
+ const { browser, context } = await launchBrowser({
58
+ captureFrames: options.captureFrames ?? 1,
59
+ verify: true,
60
+ observe: options.observe,
61
+ });
62
+ const isAborted = installAbortHandler();
63
+ let captured = 0;
64
+ let changed = 0;
65
+ let failed = 0;
66
+ try {
67
+ const result = await captureParallel({
68
+ context,
69
+ entries,
70
+ rendererFilter: [],
71
+ baseUrl: server.url,
72
+ tool: 'reference',
73
+ outBase: resolve(options.outBase),
74
+ root: options.checkoutDir,
75
+ updateBaseline: options.updateBaseline,
76
+ extraWait: options.extraWait,
77
+ captureFrames: options.captureFrames,
78
+ failOnError: options.failOnError,
79
+ observe: options.observe,
80
+ verify: true,
81
+ isAborted,
82
+ workerCount: options.workerCount ?? 4,
83
+ });
84
+ captured = result.captured;
85
+ changed = result.changed;
86
+ failed = result.failed;
87
+ }
88
+ finally {
89
+ await browser.close().catch(() => { });
90
+ server.kill();
91
+ }
92
+ console.log('\n' +
93
+ formatSummaryLine(failed > 0, [
94
+ formatSummaryCount(captured, 'captured', 'pass'),
95
+ formatSummaryCount(changed, 'changed', 'warn'),
96
+ formatSummaryCount(failed, 'failed', 'fail'),
97
+ ]));
98
+ console.log(`Output: ${resolve(options.outBase)}/reference/`);
99
+ return { captured, changed, failed, aborted: isAborted() };
100
+ }
101
+ // Spawns flight-reference's Vite dev server in the checkout, resolving once it logs its Local URL.
102
+ // FLIGHT_REPO points its @flighthq/* aliases at the Flight source. We deliberately do NOT set
103
+ // FLIGHT_SDK_SOURCE: that flag excludes @flighthq/sdk from Vite's dep pre-bundling to serve it as raw
104
+ // source over HMR (for actively editing SDK internals) — which makes a cold page transform the entire SDK
105
+ // module graph (~1700 modules, tens of seconds) and blow past the capture's page timeout. Capture renders
106
+ // once and only needs the built result, so the pre-bundled path (esbuild-bundling the same local source
107
+ // once) is both correct and far faster here.
108
+ export function startReferenceDevServer(checkoutDir, repoRoot) {
109
+ return new Promise((resolveUrl, reject) => {
110
+ const child = spawn('npx', ['vite', '--host', '0.0.0.0'], {
111
+ cwd: checkoutDir,
112
+ stdio: ['ignore', 'pipe', 'pipe'],
113
+ env: { ...process.env, FLIGHT_REPO: repoRoot },
114
+ });
115
+ let resolved = false;
116
+ const timeout = setTimeout(() => {
117
+ if (!resolved) {
118
+ resolved = true;
119
+ reject(new Error('flight-reference Vite dev server did not start within 60s'));
120
+ }
121
+ }, 60_000);
122
+ const ansi = new RegExp(String.fromCharCode(27) + '\\[[0-9;]*m', 'g');
123
+ const onData = (data) => {
124
+ const match = /Local:\s+(https?:\/\/\S+)/.exec(data.toString().replace(ansi, ''));
125
+ if (match && !resolved) {
126
+ resolved = true;
127
+ clearTimeout(timeout);
128
+ resolveUrl({ url: match[1].replace(/\/$/, ''), kill: () => child.kill('SIGTERM') });
129
+ }
130
+ };
131
+ child.stdout?.on('data', onData);
132
+ child.stderr?.on('data', onData);
133
+ child.on('error', (err) => {
134
+ if (!resolved) {
135
+ resolved = true;
136
+ clearTimeout(timeout);
137
+ reject(err);
138
+ }
139
+ });
140
+ });
141
+ }
142
+ //# sourceMappingURL=referenceCapture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"referenceCapture.js","sourceRoot":"","sources":["../src/referenceCapture.ts"],"names":[],"mappings":"AAAA,sGAAsG;AACtG,mFAAmF;AACnF,iGAAiG;AACjG,kGAAkG;AAClG,6GAA6G;AAE7G,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAE1C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC3E,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AA2B5D,yGAAyG;AACzG,qGAAqG;AACrG,yGAAyG;AACzG,MAAM,UAAU,wBAAwB,CAAC,WAAmB;IAC1D,MAAM,aAAa,GAAG,IAAI,CAAC,WAAW,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC;IACjE,MAAM,OAAO,GAAY,EAAE,CAAC;IAC5B,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC;QAAE,OAAO,OAAO,CAAC;IAC/C,KAAK,MAAM,SAAS,IAAI,WAAW,CAAC,aAAa,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QAC5E,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE;YAAE,SAAS;QACvC,MAAM,YAAY,GAAG,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;QACzD,KAAK,MAAM,MAAM,IAAI,WAAW,CAAC,YAAY,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;YACxE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE;gBAAE,SAAS;YACpC,MAAM,SAAS,GAAG,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;YAClD,KAAK,MAAM,CAAC,IAAI,WAAW,CAAC,SAAS,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;gBAChE,IAAI,CAAC,CAAC,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS;oBAAE,SAAS;gBACvD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;oBAAE,SAAS;gBAC9E,MAAM,WAAW,GAAG,GAAG,SAAS,CAAC,IAAI,QAAQ,CAAC;gBAC9C,OAAO,CAAC,IAAI,CAAC;oBACX,IAAI,EAAE,GAAG,SAAS,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,EAAE;oBAClD,SAAS,EAAE,CAAC,OAAO,CAAC;oBACpB,KAAK,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,GAAG,WAAW,IAAI,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,WAAW,YAAY,CAAC,QAAQ,CAAC,GAAG;iBACjG,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AAC9D,CAAC;AAED,2GAA2G;AAC3G,uGAAuG;AACvG,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,OAA0C;IAClF,IAAI,OAAO,GAAG,wBAAwB,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IAC5D,IAAI,OAAO,CAAC,MAAM;QAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAO,CAAC,CAAC,CAAC;IACtF,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,qCAAqC,OAAO,CAAC,MAAM,IAAI,EAAE,GAAG,CAAC,CAAC;IAExG,OAAO,CAAC,GAAG,CAAC,4CAA4C,CAAC,CAAC;IAC1D,MAAM,MAAM,GAAG,MAAM,uBAAuB,CAAC,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IACpF,OAAO,CAAC,GAAG,CAAC,YAAY,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC;IAExC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,MAAM,aAAa,CAAC;QAC/C,aAAa,EAAE,OAAO,CAAC,aAAa,IAAI,CAAC;QACzC,MAAM,EAAE,IAAI;QACZ,OAAO,EAAE,OAAO,CAAC,OAAO;KACzB,CAAC,CAAC;IACH,MAAM,SAAS,GAAG,mBAAmB,EAAE,CAAC;IAExC,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC;YACnC,OAAO;YACP,OAAO;YACP,cAAc,EAAE,EAAE;YAClB,OAAO,EAAE,MAAM,CAAC,GAAG;YACnB,IAAI,EAAE,WAAW;YACjB,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC;YACjC,IAAI,EAAE,OAAO,CAAC,WAAW;YACzB,cAAc,EAAE,OAAO,CAAC,cAAc;YACtC,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,aAAa,EAAE,OAAO,CAAC,aAAa;YACpC,WAAW,EAAE,OAAO,CAAC,WAAW;YAChC,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,MAAM,EAAE,IAAI;YACZ,SAAS;YACT,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,CAAC;SACtC,CAAC,CAAC;QACH,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;QAC3B,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;QACzB,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IACzB,CAAC;YAAS,CAAC;QACT,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACtC,MAAM,CAAC,IAAI,EAAE,CAAC;IAChB,CAAC;IAED,OAAO,CAAC,GAAG,CACT,IAAI;QACF,iBAAiB,CAAC,MAAM,GAAG,CAAC,EAAE;YAC5B,kBAAkB,CAAC,QAAQ,EAAE,UAAU,EAAE,MAAM,CAAC;YAChD,kBAAkB,CAAC,OAAO,EAAE,SAAS,EAAE,MAAM,CAAC;YAC9C,kBAAkB,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC;SAC7C,CAAC,CACL,CAAC;IACF,OAAO,CAAC,GAAG,CAAC,aAAa,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;IAChE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,CAAC;AAC7D,CAAC;AAED,mGAAmG;AACnG,8FAA8F;AAC9F,sGAAsG;AACtG,0GAA0G;AAC1G,0GAA0G;AAC1G,wGAAwG;AACxG,6CAA6C;AAC7C,MAAM,UAAU,uBAAuB,CACrC,WAAmB,EACnB,QAAgB;IAEhB,OAAO,IAAI,OAAO,CAAC,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE;QACxC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,SAAS,CAAC,EAAE;YACxD,GAAG,EAAE,WAAW;YAChB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC;YACjC,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,WAAW,EAAE,QAAQ,EAAE;SAC/C,CAAC,CAAC;QACH,IAAI,QAAQ,GAAG,KAAK,CAAC;QACrB,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE;YAC9B,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,QAAQ,GAAG,IAAI,CAAC;gBAChB,MAAM,CAAC,IAAI,KAAK,CAAC,2DAA2D,CAAC,CAAC,CAAC;YACjF,CAAC;QACH,CAAC,EAAE,MAAM,CAAC,CAAC;QACX,MAAM,IAAI,GAAG,IAAI,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC,GAAG,aAAa,EAAE,GAAG,CAAC,CAAC;QACtE,MAAM,MAAM,GAAG,CAAC,IAAY,EAAE,EAAE;YAC9B,MAAM,KAAK,GAAG,2BAA2B,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;YAClF,IAAI,KAAK,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACvB,QAAQ,GAAG,IAAI,CAAC;gBAChB,YAAY,CAAC,OAAO,CAAC,CAAC;gBACtB,UAAU,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC,CAAE,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;YACvF,CAAC;QACH,CAAC,CAAC;QACF,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACjC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACjC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YACxB,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,QAAQ,GAAG,IAAI,CAAC;gBAChB,YAAY,CAAC,OAAO,CAAC,CAAC;gBACtB,MAAM,CAAC,GAAG,CAAC,CAAC;YACd,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flighthq/tool-capture",
3
- "version": "0.2.0",
3
+ "version": "0.2.1-next.423.3350e53",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/flighthq/flight.git",
@@ -15,6 +15,9 @@
15
15
  "default": "./dist/index.js"
16
16
  }
17
17
  },
18
+ "bin": {
19
+ "tool-capture": "dist/bin.js"
20
+ },
18
21
  "files": [
19
22
  "dist",
20
23
  "src/**/*.test.ts",
@@ -32,6 +35,9 @@
32
35
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
33
36
  },
34
37
  "dependencies": {
38
+ "@flighthq/render-wgpu": "0.2.1-next.423.3350e53",
39
+ "@flighthq/surface": "0.2.1-next.423.3350e53",
40
+ "@flighthq/types": "0.2.1-next.423.3350e53",
35
41
  "@playwright/test": "^1.50.0",
36
42
  "picocolors": "^1.1.1"
37
43
  },
@@ -2,7 +2,112 @@ import { isAbsolute, join } from 'node:path';
2
2
 
3
3
  import { describe, expect, it } from 'vitest';
4
4
 
5
- import { captureEntry, captureParallel, getCaptureOutputPaths } from './captureEntry';
5
+ import {
6
+ buildCaptureObserveDiagnostics,
7
+ captureEntry,
8
+ captureParallel,
9
+ captureUrl,
10
+ getCaptureOutputPaths,
11
+ } from './captureEntry';
12
+
13
+ describe('buildCaptureObserveDiagnostics', () => {
14
+ it('passes through the render facts and separates page exceptions from console/network errors', () => {
15
+ const d = buildCaptureObserveDiagnostics({
16
+ backend: 'webgl',
17
+ blank: true,
18
+ coverage: 0,
19
+ verifyPublished: false,
20
+ verifyTargetKind: 'webgl',
21
+ warmupFrames: 3,
22
+ logs: [
23
+ { level: 'pageerror', data: { msg: 'boom' } },
24
+ { level: 'error', channel: 'network', data: { msg: 'request failed' } },
25
+ { level: 'error', channel: 'console', data: { msg: 'console.error' } },
26
+ { level: 'info', data: { msg: 'ignored' } },
27
+ ],
28
+ });
29
+ expect(d).toEqual({
30
+ backend: 'webgl',
31
+ blank: true,
32
+ coverage: 0,
33
+ errorCount: 2,
34
+ pageErrorCount: 1,
35
+ verifyPublished: false,
36
+ verifyTargetKind: 'webgl',
37
+ warmupFrames: 3,
38
+ });
39
+ });
40
+
41
+ it('reports a clean non-blank observation with no errors', () => {
42
+ const d = buildCaptureObserveDiagnostics({
43
+ backend: 'webgl',
44
+ blank: false,
45
+ coverage: 0.42,
46
+ verifyPublished: true,
47
+ verifyTargetKind: 'webgl',
48
+ warmupFrames: 0,
49
+ logs: [],
50
+ });
51
+ expect(d.blank).toBe(false);
52
+ expect(d.coverage).toBe(0.42);
53
+ expect(d.pageErrorCount).toBe(0);
54
+ expect(d.errorCount).toBe(0);
55
+ expect(d.warmupFrames).toBe(0);
56
+ });
57
+
58
+ it('folds measured coverage into blank: an empty frame is blank even with no verify target', () => {
59
+ // A scene that registered no verify target (blank=false from the screenshot path) but drew nothing
60
+ // to the canvas (coverage 0) must still read as blank — the 2D-assets-never-loaded case.
61
+ const empty = buildCaptureObserveDiagnostics({
62
+ backend: 'webgl',
63
+ blank: false,
64
+ coverage: 0,
65
+ verifyPublished: false,
66
+ verifyTargetKind: null,
67
+ warmupFrames: 0,
68
+ logs: [],
69
+ });
70
+ expect(empty.blank).toBe(true);
71
+
72
+ // A frame with real content stays non-blank; a null coverage (unmeasured) does not force blank.
73
+ expect(
74
+ buildCaptureObserveDiagnostics({
75
+ backend: 'webgl',
76
+ blank: false,
77
+ coverage: 0.1,
78
+ verifyPublished: false,
79
+ verifyTargetKind: null,
80
+ warmupFrames: 0,
81
+ logs: [],
82
+ }).blank,
83
+ ).toBe(false);
84
+ expect(
85
+ buildCaptureObserveDiagnostics({
86
+ backend: 'webgl',
87
+ blank: false,
88
+ coverage: null,
89
+ verifyPublished: false,
90
+ verifyTargetKind: null,
91
+ warmupFrames: 0,
92
+ logs: [],
93
+ }).blank,
94
+ ).toBe(false);
95
+
96
+ // Measured pixels win over the verifier: a scene the verifier never published (blank=true) but
97
+ // whose canvas is clearly full (coverage 0.98) is NOT blank — the verify-publish false-positive.
98
+ expect(
99
+ buildCaptureObserveDiagnostics({
100
+ backend: 'webgl',
101
+ blank: true,
102
+ coverage: 0.98,
103
+ verifyPublished: false,
104
+ verifyTargetKind: 'webgl',
105
+ warmupFrames: 600,
106
+ logs: [],
107
+ }).blank,
108
+ ).toBe(false);
109
+ });
110
+ });
6
111
 
7
112
  describe('captureEntry', () => {
8
113
  // Driving a page needs a live Playwright BrowserContext and server; that path is exercised end to end
@@ -18,6 +123,13 @@ describe('captureParallel', () => {
18
123
  });
19
124
  });
20
125
 
126
+ describe('captureUrl', () => {
127
+ // Drives its own headless browser against a live URL; exercised end to end by the `observe` bin.
128
+ it('is a callable single-url observe pass', () => {
129
+ expect(typeof captureUrl).toBe('function');
130
+ });
131
+ });
132
+
21
133
  describe('getCaptureOutputPaths', () => {
22
134
  it('derives the {outBase}/{tool}/{name}/{routeSegment}/… artifact layout', () => {
23
135
  const paths = getCaptureOutputPaths('out', 'functional', 'foo', 'flight:webgl');
@@ -0,0 +1,94 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import type { DomRenderState } from '@flighthq/types';
4
+ import { afterEach, describe, expect, it } from 'vitest';
5
+
6
+ import type { FunctionalTarget } from './functionalVerify';
7
+ import {
8
+ publishFunctionalRenderSync,
9
+ registerFunctionalTarget,
10
+ registerWgpuFunctionalTarget,
11
+ runRenderVerification,
12
+ snapshotFunctionalRender,
13
+ } from './functionalVerify';
14
+
15
+ // The verifier communicates through window globals; clear them between cases so state doesn't leak.
16
+ interface VerificationWindowLike {
17
+ __ftTarget?: unknown;
18
+ __ftVerification?: unknown;
19
+ __ftRenderImage?: unknown;
20
+ }
21
+
22
+ afterEach(() => {
23
+ const w = window as unknown as VerificationWindowLike;
24
+ w.__ftTarget = undefined;
25
+ w.__ftVerification = undefined;
26
+ w.__ftRenderImage = undefined;
27
+ });
28
+
29
+ // A minimal DOM target — the only backend snapshot/verification handles without a real GPU context.
30
+ function domTarget(element: HTMLElement): FunctionalTarget {
31
+ return {
32
+ kind: 'dom',
33
+ state: { element } as unknown as DomRenderState,
34
+ width: 10,
35
+ height: 10,
36
+ scale: 1,
37
+ render: () => {},
38
+ };
39
+ }
40
+
41
+ describe('publishFunctionalRenderSync', () => {
42
+ it('returns false when no target is registered', () => {
43
+ expect(publishFunctionalRenderSync('webgl')).toBe(false);
44
+ });
45
+
46
+ it('returns false for a non-webgl target and does not publish', () => {
47
+ registerFunctionalTarget(domTarget(document.createElement('div')));
48
+ expect(publishFunctionalRenderSync('webgl')).toBe(false);
49
+ expect((window as unknown as VerificationWindowLike).__ftRenderImage).toBeUndefined();
50
+ });
51
+ });
52
+
53
+ describe('registerFunctionalTarget', () => {
54
+ it('records the target on window and returns it', () => {
55
+ const target = domTarget(document.createElement('div'));
56
+ const returned = registerFunctionalTarget(target);
57
+ expect(returned).toBe(target);
58
+ expect((window as unknown as VerificationWindowLike).__ftTarget).toBe(target);
59
+ });
60
+ });
61
+
62
+ describe('registerWgpuFunctionalTarget', () => {
63
+ // Enabling wgpu frame capture needs a real WgpuRenderState; end-to-end coverage is the functional
64
+ // suite's job. Here we only assert the entry point is wired.
65
+ it('is a callable registrar', () => {
66
+ expect(typeof registerWgpuFunctionalTarget).toBe('function');
67
+ });
68
+ });
69
+
70
+ describe('runRenderVerification', () => {
71
+ it('throws on a blank DOM render (no elements or text)', async () => {
72
+ registerFunctionalTarget(domTarget(document.createElement('div')));
73
+ await expect(runRenderVerification({}, 'dom')).rejects.toThrow(/blank render/);
74
+ });
75
+
76
+ it('passes a DOM render that emitted content and records the verification', async () => {
77
+ const host = document.createElement('div');
78
+ host.appendChild(document.createElement('span'));
79
+ registerFunctionalTarget(domTarget(host));
80
+ await runRenderVerification({}, 'dom');
81
+ expect((window as unknown as VerificationWindowLike).__ftVerification).toMatchObject({ render: 'dom' });
82
+ });
83
+ });
84
+
85
+ describe('snapshotFunctionalRender', () => {
86
+ it('returns null for a DOM target', async () => {
87
+ registerFunctionalTarget(domTarget(document.createElement('div')));
88
+ expect(await snapshotFunctionalRender()).toBeNull();
89
+ });
90
+
91
+ it('returns null when no target and no canvas is present', async () => {
92
+ expect(await snapshotFunctionalRender()).toBeNull();
93
+ });
94
+ });