@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,142 @@
1
+ // Server lifecycle for a capture run: either a Vite dev server (on-demand transform), a lightweight
2
+ // Node.js static server over a pre-built dist (the default, faster), or an already-running external
3
+ // URL. Each resolves to a { url, kill } handle the capture loop drives pages against.
4
+ import { spawn, spawnSync } from 'node:child_process';
5
+ import { existsSync, readFileSync } from 'node:fs';
6
+ import { createServer } from 'node:http';
7
+ import { extname, join, relative } from 'node:path';
8
+ export function resolveServer(opts) {
9
+ const { tool, root, externalUrl } = opts;
10
+ if (externalUrl) {
11
+ const url = externalUrl.replace(/\/$/, '');
12
+ return Promise.resolve({ url, kill: () => { } });
13
+ }
14
+ const toolDir = tool === 'examples' ? join(root, 'examples', 'runners', 'web') : join(root, 'tools', tool);
15
+ const viteJs = join(root, 'node_modules', 'vite', 'bin', 'vite.js');
16
+ const configPath = join(toolDir, 'vite.config.ts');
17
+ // Run predev (asset download) before starting the server, mirroring what
18
+ // npm run dev would do. npm_execpath is set by npm and points to the npm
19
+ // CLI script; fall back to shell npm for direct tsx invocations.
20
+ const toolPkg = JSON.parse(readFileSync(join(toolDir, 'package.json'), 'utf-8'));
21
+ if (toolPkg.scripts?.predev) {
22
+ const npmExecPath = process.env['npm_execpath'];
23
+ const result = npmExecPath
24
+ ? spawnSync(process.execPath, [npmExecPath, 'run', 'predev'], { cwd: toolDir, stdio: 'inherit' })
25
+ : spawnSync('npm', ['run', 'predev'], { cwd: toolDir, stdio: 'inherit', shell: true });
26
+ if (result.status !== 0)
27
+ throw new Error(`predev failed for ${tool}`);
28
+ }
29
+ return new Promise((resolve, reject) => {
30
+ const proc = spawn(process.execPath, [viteJs, '--config', configPath], {
31
+ cwd: toolDir,
32
+ stdio: ['ignore', 'pipe', 'pipe'],
33
+ env: { ...process.env },
34
+ });
35
+ let done = false;
36
+ let output = '';
37
+ const timeout = setTimeout(() => {
38
+ if (!done) {
39
+ proc.kill();
40
+ reject(new Error(`Server did not start within 60s.\nCaptured output:\n${output}\n\n` +
41
+ `Tip: start the server manually with "npm run ${DEV_SCRIPT[tool]}" ` +
42
+ `and pass --url=http://localhost:5173`));
43
+ }
44
+ }, 60_000);
45
+ const scan = (chunk) => {
46
+ output += chunk.toString();
47
+ // eslint-disable-next-line no-control-regex -- ESC (0x1b) is required to strip ANSI color codes
48
+ const clean = output.replace(/\x1b\[[0-9;]*m/g, '');
49
+ const match = clean.match(/localhost:(\d+)/);
50
+ if (match && !done) {
51
+ done = true;
52
+ clearTimeout(timeout);
53
+ resolve({ url: `http://localhost:${match[1]}`, kill: () => proc.kill('SIGTERM') });
54
+ }
55
+ };
56
+ proc.stdout?.on('data', scan);
57
+ proc.stderr?.on('data', scan);
58
+ proc.on('error', reject);
59
+ });
60
+ }
61
+ // Serve a pre-built tool dist from a lightweight Node.js HTTP server, bypassing the Vite dev
62
+ // server and its on-demand transform overhead. Auto-builds when dist is absent; pass forceBuild to
63
+ // always rebuild (e.g. for baseline captures that must be authoritative).
64
+ export function resolveStaticServer(opts) {
65
+ const { tool, root, forceBuild = false } = opts;
66
+ const toolDir = tool === 'examples' ? join(root, 'examples', 'runners', 'web') : join(root, 'tools', tool);
67
+ const distDir = join(toolDir, 'dist');
68
+ if (!existsSync(distDir) || forceBuild) {
69
+ console.log(`Building tools/${tool}…`);
70
+ const npmExecPath = process.env['npm_execpath'];
71
+ const workspace = tool === 'examples' ? '@flighthq/examples' : `tools/${tool}`;
72
+ const args = ['run', 'build', `--workspace=${workspace}`];
73
+ const result = npmExecPath
74
+ ? spawnSync(process.execPath, [npmExecPath, ...args], {
75
+ cwd: root,
76
+ stdio: 'inherit',
77
+ })
78
+ : spawnSync('npm', args, {
79
+ cwd: root,
80
+ stdio: 'inherit',
81
+ shell: true,
82
+ });
83
+ if (result.status !== 0) {
84
+ return Promise.reject(new Error(`Build failed. Run "npm run build:${tool}" to debug.`));
85
+ }
86
+ }
87
+ if (!existsSync(distDir)) {
88
+ return Promise.reject(new Error(`No build found at ${distDir} after build. Run "npm run build:${tool}" to debug.`));
89
+ }
90
+ const MIME = {
91
+ '.css': 'text/css',
92
+ '.gif': 'image/gif',
93
+ '.html': 'text/html; charset=utf-8',
94
+ '.jpeg': 'image/jpeg',
95
+ '.jpg': 'image/jpeg',
96
+ '.js': 'application/javascript',
97
+ '.json': 'application/json',
98
+ '.jsonl': 'text/plain; charset=utf-8',
99
+ '.mp3': 'audio/mpeg',
100
+ '.ogg': 'audio/ogg',
101
+ '.png': 'image/png',
102
+ '.svg': 'image/svg+xml',
103
+ '.ttf': 'font/ttf',
104
+ '.utf8': 'text/plain; charset=utf-8',
105
+ '.wav': 'audio/wav',
106
+ '.wasm': 'application/wasm',
107
+ '.webp': 'image/webp',
108
+ '.woff': 'font/woff',
109
+ '.woff2': 'font/woff2',
110
+ };
111
+ return new Promise((resolve, reject) => {
112
+ const server = createServer((req, res) => {
113
+ let urlPath = (req.url ?? '/').split('?')[0];
114
+ if (urlPath.endsWith('/'))
115
+ urlPath += 'index.html';
116
+ const fsPath = join(distDir, urlPath);
117
+ if (relative(distDir, fsPath).startsWith('..')) {
118
+ res.writeHead(403);
119
+ res.end();
120
+ return;
121
+ }
122
+ if (!existsSync(fsPath)) {
123
+ res.writeHead(404);
124
+ res.end();
125
+ return;
126
+ }
127
+ res.setHeader('Content-Type', MIME[extname(fsPath)] ?? 'application/octet-stream');
128
+ res.end(readFileSync(fsPath));
129
+ });
130
+ server.on('error', reject);
131
+ server.listen(0, '127.0.0.1', () => {
132
+ const { port } = server.address();
133
+ resolve({ url: `http://localhost:${port}`, kill: () => server.close() });
134
+ });
135
+ });
136
+ }
137
+ // The root npm script that starts each tool's dev server, used in the manual-start tip.
138
+ const DEV_SCRIPT = {
139
+ examples: 'dev:examples',
140
+ functional: 'dev:functional',
141
+ };
142
+ //# sourceMappingURL=captureServer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"captureServer.js","sourceRoot":"","sources":["../src/captureServer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ export declare const FUNCTIONAL_BACKENDS: readonly ["canvas", "dom", "webgl", "webgpu"];
2
+ export type FunctionalBackend = (typeof FUNCTIONAL_BACKENDS)[number];
3
+ export interface FunctionalScene {
4
+ name: string;
5
+ renderers: string[];
6
+ }
7
+ export declare function discoverFunctionalScenes(scenesDir: string): FunctionalScene[];
8
+ export declare function functionalSceneFile(scenesDir: string, name: string, backend: string): string;
9
+ //# sourceMappingURL=functionalScenes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"functionalScenes.d.ts","sourceRoot":"","sources":["../src/functionalScenes.ts"],"names":[],"mappings":"AAYA,eAAO,MAAM,mBAAmB,+CAAgD,CAAC;AACjF,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AAErE,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAKD,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,GAAG,eAAe,EAAE,CAuB7E;AAID,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAG5F"}
@@ -0,0 +1,64 @@
1
+ // Functional scene discovery — the single source of truth shared by the functional Vite harness
2
+ // (tools/functional/vite.config.ts) and the capture pipeline (discoverEntries).
3
+ //
4
+ // A scene is a file under functional/scenes/. Its filename encodes its backend set:
5
+ // <name>.ts backend-agnostic — one file that runs on every default backend.
6
+ // <name>.<backend>.ts backend-specific — a self-contained target for that one backend.
7
+ // The backend a comparison groups by is the `<name>`; the set of backends a name runs on is simply
8
+ // which files exist. There is no package.json and no renderers[] field: existence is the manifest.
9
+ import { existsSync, readdirSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ export const FUNCTIONAL_BACKENDS = ['canvas', 'dom', 'webgl', 'webgpu'];
12
+ // Every scene grouped by name, each with the sorted backend set it runs on. A name is either
13
+ // backend-agnostic (a single no-suffix file → all default backends) or backend-specific (one entry
14
+ // per <name>.<backend>.ts file); the two forms do not mix for one name.
15
+ export function discoverFunctionalScenes(scenesDir) {
16
+ if (!existsSync(scenesDir))
17
+ return [];
18
+ const agnostic = new Set();
19
+ const specific = new Map();
20
+ for (const file of readdirSync(scenesDir)) {
21
+ const parsed = parseSceneFile(file);
22
+ if (parsed === null)
23
+ continue;
24
+ if (parsed.backend === null) {
25
+ agnostic.add(parsed.name);
26
+ }
27
+ else {
28
+ if (!specific.has(parsed.name))
29
+ specific.set(parsed.name, new Set());
30
+ specific.get(parsed.name).add(parsed.backend);
31
+ }
32
+ }
33
+ const scenes = [];
34
+ for (const name of agnostic)
35
+ scenes.push({ name, renderers: [...DEFAULT_BACKENDS] });
36
+ for (const [name, backends] of specific) {
37
+ if (agnostic.has(name))
38
+ continue; // a no-suffix file wins if both somehow exist
39
+ scenes.push({ name, renderers: DEFAULT_BACKENDS.filter((b) => backends.has(b)) });
40
+ }
41
+ return scenes.sort((a, b) => a.name.localeCompare(b.name));
42
+ }
43
+ // The scene file backing one capture target: the backend-specific file when it exists, else the
44
+ // backend-agnostic file.
45
+ export function functionalSceneFile(scenesDir, name, backend) {
46
+ const specific = join(scenesDir, `${name}.${backend}.ts`);
47
+ return existsSync(specific) ? specific : join(scenesDir, `${name}.ts`);
48
+ }
49
+ function parseSceneFile(file) {
50
+ if (!file.endsWith('.ts'))
51
+ return null;
52
+ const stem = file.slice(0, -'.ts'.length);
53
+ const dot = stem.lastIndexOf('.');
54
+ if (dot !== -1) {
55
+ const suffix = stem.slice(dot + 1);
56
+ if (FUNCTIONAL_BACKENDS.includes(suffix)) {
57
+ return { name: stem.slice(0, dot), backend: suffix };
58
+ }
59
+ }
60
+ return { name: stem, backend: null };
61
+ }
62
+ // The backends a backend-agnostic (no-suffix) scene runs on, in capture order.
63
+ const DEFAULT_BACKENDS = ['dom', 'canvas', 'webgl', 'webgpu'];
64
+ //# sourceMappingURL=functionalScenes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"functionalScenes.js","sourceRoot":"","sources":["../src/functionalScenes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ export * from './baselineStore';
2
+ export * from './captureBrowser';
3
+ export * from './captureEntries';
4
+ export * from './captureEntry';
5
+ export * from './captureFormat';
6
+ export * from './captureInterrupt';
7
+ export * from './captureRenderTarget';
8
+ export * from './captureServer';
9
+ export * from './functionalScenes';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export * from './baselineStore';
2
+ export * from './captureBrowser';
3
+ export * from './captureEntries';
4
+ export * from './captureEntry';
5
+ export * from './captureFormat';
6
+ export * from './captureInterrupt';
7
+ export * from './captureRenderTarget';
8
+ export * from './captureServer';
9
+ export * from './functionalScenes';
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@flighthq/tool-capture",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src/**/*.test.ts",
16
+ "!dist/**/*.test.js",
17
+ "!dist/**/*.test.d.ts",
18
+ "!dist/**/*.test.js.map",
19
+ "!dist/**/*.test.d.ts.map"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -b",
23
+ "clean": "tsc -b --clean",
24
+ "test": "vitest run --config vitest.config.ts",
25
+ "test:watch": "vitest --watch --config vitest.config.ts",
26
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
27
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
28
+ },
29
+ "dependencies": {
30
+ "@playwright/test": "^1.50.0",
31
+ "picocolors": "^1.1.1"
32
+ },
33
+ "devDependencies": {
34
+ "typescript": "^5.3.0"
35
+ },
36
+ "description": "Capture-execution harness (tool-*): drives a Flight example/functional/site page in headless Chromium, synchronizes to one presented frame, and extracts screenshot.png + logs.jsonl + status.json with baseline hash comparison. Playwright imported lazily; not shipped to a browser bundle",
37
+ "sideEffects": false
38
+ }
@@ -0,0 +1,59 @@
1
+ import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+
5
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6
+
7
+ import { baselinePath, getBaselineField, setBaselineField } from './baselineStore';
8
+
9
+ let root: string;
10
+
11
+ beforeEach(() => {
12
+ root = mkdtempSync(join(tmpdir(), 'tc-baseline-'));
13
+ });
14
+
15
+ afterEach(() => {
16
+ rmSync(root, { recursive: true, force: true });
17
+ });
18
+
19
+ describe('baselinePath', () => {
20
+ it('maps known subjects to their suite root', () => {
21
+ expect(baselinePath(root, 'functional', 'foo')).toBe(join(root, 'functional', 'baselines', 'foo.json'));
22
+ expect(baselinePath(root, 'examples', 'bar')).toBe(join(root, 'examples', 'baselines', 'bar.json'));
23
+ });
24
+
25
+ it('falls back to the subject name for an unknown subject', () => {
26
+ expect(baselinePath(root, 'other', 'foo')).toBe(join(root, 'other', 'baselines', 'foo.json'));
27
+ });
28
+ });
29
+
30
+ describe('getBaselineField', () => {
31
+ it('returns null when no baseline file exists', () => {
32
+ expect(getBaselineField(root, 'functional', 'foo', 'canvas', 'sha256')).toBeNull();
33
+ });
34
+
35
+ it('reads back a written field', () => {
36
+ setBaselineField(root, 'functional', 'foo', 'canvas', 'sha256', 'abc123');
37
+ expect(getBaselineField(root, 'functional', 'foo', 'canvas', 'sha256')).toBe('abc123');
38
+ });
39
+ });
40
+
41
+ describe('setBaselineField', () => {
42
+ it('preserves other fields and columns on a read-merge-write', () => {
43
+ setBaselineField(root, 'functional', 'foo', 'canvas', 'fingerprint', 'fp');
44
+ setBaselineField(root, 'functional', 'foo', 'canvas', 'sha256', 'hash');
45
+ setBaselineField(root, 'functional', 'foo', 'webgl', 'sha256', 'hash2');
46
+
47
+ expect(getBaselineField(root, 'functional', 'foo', 'canvas', 'fingerprint')).toBe('fp');
48
+ expect(getBaselineField(root, 'functional', 'foo', 'canvas', 'sha256')).toBe('hash');
49
+ expect(getBaselineField(root, 'functional', 'foo', 'webgl', 'sha256')).toBe('hash2');
50
+ });
51
+
52
+ it('writes sorted, prettier-compatible JSON with a trailing newline', () => {
53
+ setBaselineField(root, 'functional', 'foo', 'webgl', 'sha256', 'h2');
54
+ setBaselineField(root, 'functional', 'foo', 'canvas', 'sha256', 'h1');
55
+ const text = readFileSync(baselinePath(root, 'functional', 'foo'), 'utf8');
56
+ expect(text.endsWith('\n')).toBe(true);
57
+ expect(text.indexOf('canvas')).toBeLessThan(text.indexOf('webgl'));
58
+ });
59
+ });
@@ -0,0 +1,12 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { launchBrowser } from './captureBrowser';
4
+
5
+ describe('launchBrowser', () => {
6
+ // Launching headless Chromium requires the Playwright browser binaries and is exercised end to end by
7
+ // the capture:* scripts; a headless unit run cannot drive a real browser. Assert the lazy-Playwright
8
+ // entry point is wired without launching.
9
+ it('is a callable browser launcher', () => {
10
+ expect(typeof launchBrowser).toBe('function');
11
+ });
12
+ });
@@ -0,0 +1,55 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+
5
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6
+
7
+ import { discoverEntries, rendererMatchesFilter, routeSegment } from './captureEntries';
8
+
9
+ let root: string;
10
+
11
+ beforeEach(() => {
12
+ root = mkdtempSync(join(tmpdir(), 'tc-entries-'));
13
+ });
14
+
15
+ afterEach(() => {
16
+ rmSync(root, { recursive: true, force: true });
17
+ });
18
+
19
+ describe('discoverEntries', () => {
20
+ it('discovers functional scenes under functional/scenes', () => {
21
+ mkdirSync(join(root, 'functional', 'scenes'), { recursive: true });
22
+ writeFileSync(join(root, 'functional', 'scenes', 'foo.ts'), '');
23
+ expect(discoverEntries('functional', root)).toEqual([
24
+ { name: 'foo', renderers: ['dom', 'canvas', 'webgl', 'webgpu'] },
25
+ ]);
26
+ });
27
+
28
+ it('discovers example packages by their render.<backend>.ts files', () => {
29
+ const exDir = join(root, 'examples', 'packages', 'demo', 'src');
30
+ mkdirSync(exDir, { recursive: true });
31
+ writeFileSync(join(root, 'examples', 'packages', 'demo', 'package.json'), '{}');
32
+ writeFileSync(join(exDir, 'render.canvas.ts'), '');
33
+ writeFileSync(join(exDir, 'render.webgl.ts'), '');
34
+ expect(discoverEntries('examples', root)).toEqual([{ name: 'demo', renderers: ['canvas', 'webgl'] }]);
35
+ });
36
+ });
37
+
38
+ describe('rendererMatchesFilter', () => {
39
+ it('matches everything when the filter is empty', () => {
40
+ expect(rendererMatchesFilter('webgl', [])).toBe(true);
41
+ });
42
+
43
+ it('matches on the full id or the backend after a library colon', () => {
44
+ expect(rendererMatchesFilter('flight:webgl', ['webgl'])).toBe(true);
45
+ expect(rendererMatchesFilter('webgl', ['webgl'])).toBe(true);
46
+ expect(rendererMatchesFilter('webgl', ['canvas'])).toBe(false);
47
+ });
48
+ });
49
+
50
+ describe('routeSegment', () => {
51
+ it('replaces a library colon with a dash and passes plain ids through', () => {
52
+ expect(routeSegment('flight:webgl')).toBe('flight-webgl');
53
+ expect(routeSegment('canvas')).toBe('canvas');
54
+ });
55
+ });
@@ -0,0 +1,32 @@
1
+ import { isAbsolute, join } from 'node:path';
2
+
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ import { captureEntry, captureParallel, getCaptureOutputPaths } from './captureEntry';
6
+
7
+ describe('captureEntry', () => {
8
+ // Driving a page needs a live Playwright BrowserContext and server; that path is exercised end to end
9
+ // by the capture:* scripts. Assert the entry point is wired.
10
+ it('is a callable capture pass', () => {
11
+ expect(typeof captureEntry).toBe('function');
12
+ });
13
+ });
14
+
15
+ describe('captureParallel', () => {
16
+ it('is a callable parallel capture pass', () => {
17
+ expect(typeof captureParallel).toBe('function');
18
+ });
19
+ });
20
+
21
+ describe('getCaptureOutputPaths', () => {
22
+ it('derives the {outBase}/{tool}/{name}/{routeSegment}/… artifact layout', () => {
23
+ const paths = getCaptureOutputPaths('out', 'functional', 'foo', 'flight:webgl');
24
+ expect(isAbsolute(paths.outDir)).toBe(true);
25
+ expect(paths.outDir.endsWith(join('functional', 'foo', 'flight-webgl'))).toBe(true);
26
+ expect(paths.finalScreenshot).toBe(join(paths.outDir, 'screenshot.png'));
27
+ expect(paths.tmpScreenshot).toBe(join(paths.outDir, 'screenshot.tmp.png'));
28
+ expect(paths.finalLogs).toBe(join(paths.outDir, 'logs.jsonl'));
29
+ expect(paths.tmpLogs).toBe(join(paths.outDir, 'logs.tmp.jsonl'));
30
+ expect(paths.statusPath).toBe(join(paths.outDir, 'status.json'));
31
+ });
32
+ });
@@ -0,0 +1,45 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { formatDetailLine, formatStatusLine, formatSummaryCount, formatSummaryLine } from './captureFormat';
4
+
5
+ // picocolors emits ANSI codes only on a color-capable TTY; strip them so assertions hold regardless of
6
+ // the environment's color support.
7
+ // eslint-disable-next-line no-control-regex -- ESC (0x1b) is required to strip ANSI color codes
8
+ const strip = (s: string): string => s.replace(/\x1b\[[0-9;]*m/g, '');
9
+
10
+ describe('formatDetailLine', () => {
11
+ it('omits label padding when there is no message', () => {
12
+ expect(strip(formatDetailLine('✓', 'canvas', 10, ''))).toBe(' ✓ canvas');
13
+ });
14
+
15
+ it('pads the label to the column width when a message follows', () => {
16
+ expect(strip(formatDetailLine('✓', 'canvas', 10, 'ok'))).toBe(' ✓ canvas ok');
17
+ });
18
+ });
19
+
20
+ describe('formatStatusLine', () => {
21
+ it('uses the tone glyph for the verdict', () => {
22
+ expect(strip(formatStatusLine('pass', 'webgl', 6, ''))).toBe(' ✓ webgl');
23
+ expect(strip(formatStatusLine('fail', 'webgl', 6, 'boom'))).toContain('✗ webgl');
24
+ expect(strip(formatStatusLine('skip', 'webgl', 6, 'nope'))).toContain('⊘ webgl');
25
+ expect(strip(formatStatusLine('muted', 'webgl', 6, ''))).toContain('· webgl');
26
+ });
27
+
28
+ it('keeps the message alongside the label', () => {
29
+ expect(strip(formatStatusLine('fail', 'webgl', 6, 'boom'))).toContain('boom');
30
+ });
31
+ });
32
+
33
+ describe('formatSummaryCount', () => {
34
+ it('formats a value/label pair', () => {
35
+ expect(strip(formatSummaryCount(3, 'captured', 'pass'))).toBe('3 captured');
36
+ expect(strip(formatSummaryCount(0, 'failed', 'fail'))).toBe('0 failed');
37
+ });
38
+ });
39
+
40
+ describe('formatSummaryLine', () => {
41
+ it('leads with the verdict then joins the counts', () => {
42
+ expect(strip(formatSummaryLine(false, ['3 captured', '0 failed']))).toBe('✓ ok 3 captured 0 failed');
43
+ expect(strip(formatSummaryLine(true, ['1 failed']))).toBe('✗ FAILED 1 failed');
44
+ });
45
+ });
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { installAbortHandler, isBrowserClosedError } from './captureInterrupt';
4
+
5
+ describe('installAbortHandler', () => {
6
+ it('returns a getter that is false before any interrupt', () => {
7
+ const isAborted = installAbortHandler();
8
+ expect(typeof isAborted).toBe('function');
9
+ expect(isAborted()).toBe(false);
10
+ });
11
+
12
+ it('is idempotent — repeated calls return a working getter', () => {
13
+ expect(installAbortHandler()()).toBe(false);
14
+ expect(installAbortHandler()()).toBe(false);
15
+ });
16
+ });
17
+
18
+ describe('isBrowserClosedError', () => {
19
+ it('recognizes Playwright teardown rejections', () => {
20
+ expect(isBrowserClosedError(new Error('Target page, context or browser has been closed'))).toBe(true);
21
+ expect(isBrowserClosedError(new Error('Browser has been closed'))).toBe(true);
22
+ expect(isBrowserClosedError(new Error('Target crashed'))).toBe(true);
23
+ });
24
+
25
+ it('does not flag a genuine render error or a non-error value', () => {
26
+ expect(isBrowserClosedError(new Error('blank render — coverage below threshold'))).toBe(false);
27
+ expect(isBrowserClosedError('some string')).toBe(false);
28
+ });
29
+ });
@@ -0,0 +1,11 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { captureRenderTarget } from './captureRenderTarget';
4
+
5
+ describe('captureRenderTarget', () => {
6
+ // The programmatic capture entry drives a real page through a Playwright BrowserContext, so it is
7
+ // exercised end to end by the capture:* scripts rather than headlessly here. Assert it is wired.
8
+ it('is a callable single-target capture entry', () => {
9
+ expect(typeof captureRenderTarget).toBe('function');
10
+ });
11
+ });
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { resolveServer, resolveStaticServer } from './captureServer';
4
+
5
+ describe('resolveServer', () => {
6
+ it('resolves immediately to an external URL, stripping a trailing slash', async () => {
7
+ const server = await resolveServer({ tool: 'examples', root: '/repo', externalUrl: 'http://localhost:5173/' });
8
+ expect(server.url).toBe('http://localhost:5173');
9
+ expect(() => server.kill()).not.toThrow();
10
+ });
11
+ });
12
+
13
+ describe('resolveStaticServer', () => {
14
+ // Building and serving a real dist needs a full toolchain and Vite build, so this is exercised end to
15
+ // end by the capture:* scripts. Here we only assert the entry point is wired.
16
+ it('is a callable server resolver', () => {
17
+ expect(typeof resolveStaticServer).toBe('function');
18
+ });
19
+ });
@@ -0,0 +1,55 @@
1
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+
5
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6
+
7
+ import { discoverFunctionalScenes, functionalSceneFile } from './functionalScenes';
8
+
9
+ let dir: string;
10
+
11
+ beforeEach(() => {
12
+ dir = mkdtempSync(join(tmpdir(), 'tc-scenes-'));
13
+ });
14
+
15
+ afterEach(() => {
16
+ rmSync(dir, { recursive: true, force: true });
17
+ });
18
+
19
+ const touch = (name: string): void => writeFileSync(join(dir, name), '');
20
+
21
+ describe('discoverFunctionalScenes', () => {
22
+ it('returns an empty list for a missing directory', () => {
23
+ expect(discoverFunctionalScenes(join(dir, 'nope'))).toEqual([]);
24
+ });
25
+
26
+ it('runs a backend-agnostic scene on every default backend', () => {
27
+ touch('foo.ts');
28
+ expect(discoverFunctionalScenes(dir)).toEqual([{ name: 'foo', renderers: ['dom', 'canvas', 'webgl', 'webgpu'] }]);
29
+ });
30
+
31
+ it('collects backend-specific files into one entry in default-backend order', () => {
32
+ touch('bar.webgl.ts');
33
+ touch('bar.canvas.ts');
34
+ expect(discoverFunctionalScenes(dir)).toEqual([{ name: 'bar', renderers: ['canvas', 'webgl'] }]);
35
+ });
36
+
37
+ it('ignores non-ts files and sorts by name', () => {
38
+ touch('zed.ts');
39
+ touch('alpha.ts');
40
+ touch('README.md');
41
+ expect(discoverFunctionalScenes(dir).map((s) => s.name)).toEqual(['alpha', 'zed']);
42
+ });
43
+ });
44
+
45
+ describe('functionalSceneFile', () => {
46
+ it('prefers the backend-specific file when it exists', () => {
47
+ touch('bar.webgl.ts');
48
+ expect(functionalSceneFile(dir, 'bar', 'webgl')).toBe(join(dir, 'bar.webgl.ts'));
49
+ });
50
+
51
+ it('falls back to the backend-agnostic file', () => {
52
+ touch('bar.ts');
53
+ expect(functionalSceneFile(dir, 'bar', 'webgl')).toBe(join(dir, 'bar.ts'));
54
+ });
55
+ });