@hyperframes/engine 0.6.119 → 0.6.120

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 (73) hide show
  1. package/package.json +24 -7
  2. package/scripts/generate-lut-reference.py +0 -168
  3. package/scripts/test-fitTextFontSize-browser.ts +0 -135
  4. package/src/cdp-headless-experimental.d.ts +0 -54
  5. package/src/config.test.ts +0 -213
  6. package/src/config.ts +0 -417
  7. package/src/index.ts +0 -273
  8. package/src/services/audioMixer.test.ts +0 -326
  9. package/src/services/audioMixer.ts +0 -604
  10. package/src/services/audioMixer.types.ts +0 -35
  11. package/src/services/audioVolumeEnvelope.test.ts +0 -176
  12. package/src/services/audioVolumeEnvelope.ts +0 -138
  13. package/src/services/browserManager.test.ts +0 -330
  14. package/src/services/browserManager.ts +0 -670
  15. package/src/services/chunkEncoder.test.ts +0 -1415
  16. package/src/services/chunkEncoder.ts +0 -831
  17. package/src/services/chunkEncoder.types.ts +0 -60
  18. package/src/services/extractionCache.test.ts +0 -199
  19. package/src/services/extractionCache.ts +0 -216
  20. package/src/services/fileServer.ts +0 -110
  21. package/src/services/frameCapture-discardWarmup.test.ts +0 -183
  22. package/src/services/frameCapture-namePolyfill.test.ts +0 -78
  23. package/src/services/frameCapture-pollImagesReady.test.ts +0 -153
  24. package/src/services/frameCapture-staticDedupIndex.test.ts +0 -76
  25. package/src/services/frameCapture-warmupTicks.test.ts +0 -174
  26. package/src/services/frameCapture.test.ts +0 -192
  27. package/src/services/frameCapture.ts +0 -1934
  28. package/src/services/hdrCapture.test.ts +0 -159
  29. package/src/services/hdrCapture.ts +0 -315
  30. package/src/services/parallelCoordinator.test.ts +0 -139
  31. package/src/services/parallelCoordinator.ts +0 -437
  32. package/src/services/screenshotService.test.ts +0 -510
  33. package/src/services/screenshotService.ts +0 -615
  34. package/src/services/streamingEncoder.test.ts +0 -832
  35. package/src/services/streamingEncoder.ts +0 -594
  36. package/src/services/systemMemory.test.ts +0 -324
  37. package/src/services/systemMemory.ts +0 -180
  38. package/src/services/videoFrameExtractor.test.ts +0 -1062
  39. package/src/services/videoFrameExtractor.ts +0 -1139
  40. package/src/services/videoFrameInjector.test.ts +0 -300
  41. package/src/services/videoFrameInjector.ts +0 -687
  42. package/src/services/vp9Options.ts +0 -13
  43. package/src/types.ts +0 -191
  44. package/src/utils/alphaBlit.test.ts +0 -1349
  45. package/src/utils/alphaBlit.ts +0 -1015
  46. package/src/utils/assertSwiftShader.test.ts +0 -130
  47. package/src/utils/assertSwiftShader.ts +0 -126
  48. package/src/utils/ffmpegBinaries.test.ts +0 -43
  49. package/src/utils/ffmpegBinaries.ts +0 -63
  50. package/src/utils/ffprobe.test.ts +0 -342
  51. package/src/utils/ffprobe.ts +0 -457
  52. package/src/utils/gpuEncoder.test.ts +0 -140
  53. package/src/utils/gpuEncoder.ts +0 -268
  54. package/src/utils/hdr.test.ts +0 -191
  55. package/src/utils/hdr.ts +0 -137
  56. package/src/utils/hdrCompositing.test.ts +0 -130
  57. package/src/utils/htmlTemplate.test.ts +0 -42
  58. package/src/utils/htmlTemplate.ts +0 -42
  59. package/src/utils/layerCompositor.test.ts +0 -150
  60. package/src/utils/layerCompositor.ts +0 -58
  61. package/src/utils/parityContract.ts +0 -1
  62. package/src/utils/processTracker.test.ts +0 -74
  63. package/src/utils/processTracker.ts +0 -41
  64. package/src/utils/readWebGlVendorInfoFromCanvas.ts +0 -52
  65. package/src/utils/runFfmpeg.test.ts +0 -102
  66. package/src/utils/runFfmpeg.ts +0 -136
  67. package/src/utils/shaderTransitions.test.ts +0 -738
  68. package/src/utils/shaderTransitions.ts +0 -1130
  69. package/src/utils/uint16-alignment-audit.test.ts +0 -125
  70. package/src/utils/urlDownloader.test.ts +0 -65
  71. package/src/utils/urlDownloader.ts +0 -143
  72. package/tsconfig.json +0 -19
  73. package/vitest.config.ts +0 -7
@@ -1,183 +0,0 @@
1
- /**
2
- * Tests for `discardWarmupCapture` — the helper distributed chunk workers
3
- * run before their first real capture to prime `lastFrameCache`.
4
- *
5
- * The helper is a thin wrapper around the inner `captureFrameCore`
6
- * machinery, so its testable contract is post-conditional rather than
7
- * pixel-level:
8
- *
9
- * 1. The wrapper invokes the inner capture exactly once with the supplied
10
- * `(frameIndex, time)`.
11
- * 2. After the wrapper returns, the session's perf and BeginFrame damage
12
- * counters look exactly as they did before — even though the inner
13
- * capture mutated them.
14
- * 3. The wrapper writes no file to disk (no path is plumbed through;
15
- * asserted indirectly by observing that `outputDir` is never read).
16
- * 4. State is restored even if the inner capture throws.
17
- *
18
- * The inner capture is stubbed (the helper accepts an injectable
19
- * `innerCapture` for exactly this reason). We don't need a real Chrome.
20
- */
21
-
22
- import { describe, expect, it } from "vitest";
23
- import { existsSync, readdirSync, mkdtempSync, rmSync } from "node:fs";
24
- import { join } from "node:path";
25
- import { tmpdir } from "node:os";
26
- import { discardWarmupCapture, type CaptureSession } from "./frameCapture.js";
27
-
28
- function makeFakeSession(): CaptureSession {
29
- // The discardWarmupCapture wrapper only reads `capturePerf`,
30
- // `beginFrameHasDamageCount`, `beginFrameNoDamageCount`. Everything else
31
- // is unused — leave it as bare-minimum stubs cast through `unknown`.
32
- return {
33
- browser: {} as unknown,
34
- page: {} as unknown,
35
- options: { width: 1, height: 1, fps: { num: 30, den: 1 } },
36
- serverUrl: "",
37
- outputDir: mkdtempSync(join(tmpdir(), "discard-warmup-")),
38
- onBeforeCapture: null,
39
- isInitialized: true,
40
- browserConsoleBuffer: [],
41
- capturePerf: {
42
- frames: 7,
43
- seekMs: 100,
44
- beforeCaptureMs: 50,
45
- screenshotMs: 200,
46
- totalMs: 350,
47
- },
48
- captureMode: "beginframe",
49
- beginFrameTimeTicks: 0,
50
- beginFrameIntervalMs: 33,
51
- beginFrameHasDamageCount: 4,
52
- beginFrameNoDamageCount: 3,
53
- } as unknown as CaptureSession;
54
- }
55
-
56
- describe("discardWarmupCapture", () => {
57
- it("calls the inner capture exactly once with (frameIndex=0, time=0) by default", async () => {
58
- const session = makeFakeSession();
59
- try {
60
- let calls = 0;
61
- let receivedFrameIndex = -1;
62
- let receivedTime = -1;
63
- await discardWarmupCapture(session, undefined, undefined, async (_s, fi, t) => {
64
- calls++;
65
- receivedFrameIndex = fi;
66
- receivedTime = t;
67
- return { buffer: Buffer.alloc(0), quantizedTime: t, captureTimeMs: 0 };
68
- });
69
- expect(calls).toBe(1);
70
- expect(receivedFrameIndex).toBe(0);
71
- expect(receivedTime).toBe(0);
72
- } finally {
73
- rmSync(session.outputDir, { recursive: true, force: true });
74
- }
75
- });
76
-
77
- it("passes through caller-supplied (frameIndex, time)", async () => {
78
- const session = makeFakeSession();
79
- try {
80
- let received: { fi: number; t: number } | null = null;
81
- await discardWarmupCapture(session, 240, 8, async (_s, fi, t) => {
82
- received = { fi, t };
83
- return { buffer: Buffer.alloc(0), quantizedTime: t, captureTimeMs: 0 };
84
- });
85
- expect(received).toEqual({ fi: 240, t: 8 });
86
- } finally {
87
- rmSync(session.outputDir, { recursive: true, force: true });
88
- }
89
- });
90
-
91
- it("restores perf counters after the inner capture mutates them", async () => {
92
- const session = makeFakeSession();
93
- const before = { ...session.capturePerf };
94
- try {
95
- await discardWarmupCapture(session, 0, 0, async (s) => {
96
- s.capturePerf.frames += 1;
97
- s.capturePerf.seekMs += 12;
98
- s.capturePerf.beforeCaptureMs += 5;
99
- s.capturePerf.screenshotMs += 33;
100
- s.capturePerf.totalMs += 50;
101
- return { buffer: Buffer.alloc(0), quantizedTime: 0, captureTimeMs: 50 };
102
- });
103
- expect(session.capturePerf).toEqual(before);
104
- } finally {
105
- rmSync(session.outputDir, { recursive: true, force: true });
106
- }
107
- });
108
-
109
- it("restores BeginFrame damage counters after the inner capture mutates them", async () => {
110
- const session = makeFakeSession();
111
- const hasBefore = session.beginFrameHasDamageCount;
112
- const noBefore = session.beginFrameNoDamageCount;
113
- try {
114
- await discardWarmupCapture(session, 0, 0, async (s) => {
115
- s.beginFrameHasDamageCount += 10;
116
- s.beginFrameNoDamageCount += 1;
117
- return { buffer: Buffer.alloc(0), quantizedTime: 0, captureTimeMs: 0 };
118
- });
119
- expect(session.beginFrameHasDamageCount).toBe(hasBefore);
120
- expect(session.beginFrameNoDamageCount).toBe(noBefore);
121
- } finally {
122
- rmSync(session.outputDir, { recursive: true, force: true });
123
- }
124
- });
125
-
126
- it("restores state even when the inner capture throws", async () => {
127
- const session = makeFakeSession();
128
- const perfBefore = { ...session.capturePerf };
129
- const hasBefore = session.beginFrameHasDamageCount;
130
- const noBefore = session.beginFrameNoDamageCount;
131
- try {
132
- let thrown: unknown;
133
- try {
134
- await discardWarmupCapture(session, 0, 0, async (s) => {
135
- s.capturePerf.frames += 5;
136
- s.beginFrameNoDamageCount += 2;
137
- throw new Error("simulated capture failure");
138
- });
139
- } catch (err) {
140
- thrown = err;
141
- }
142
- expect((thrown as Error).message).toBe("simulated capture failure");
143
- // The whole point of `finally { restore }`: failure must not leak
144
- // inflated counters into the real capture summary.
145
- expect(session.capturePerf).toEqual(perfBefore);
146
- expect(session.beginFrameHasDamageCount).toBe(hasBefore);
147
- expect(session.beginFrameNoDamageCount).toBe(noBefore);
148
- } finally {
149
- rmSync(session.outputDir, { recursive: true, force: true });
150
- }
151
- });
152
-
153
- it("writes no output file to the session's outputDir", async () => {
154
- const session = makeFakeSession();
155
- try {
156
- expect(existsSync(session.outputDir)).toBe(true);
157
- const before = readdirSync(session.outputDir);
158
- await discardWarmupCapture(session, 0, 0, async () => ({
159
- buffer: Buffer.from([0xff, 0xff, 0xff]),
160
- quantizedTime: 0,
161
- captureTimeMs: 0,
162
- }));
163
- const after = readdirSync(session.outputDir);
164
- expect(after).toEqual(before);
165
- } finally {
166
- rmSync(session.outputDir, { recursive: true, force: true });
167
- }
168
- });
169
-
170
- it("returns undefined (no result type, so the buffer can't escape)", async () => {
171
- const session = makeFakeSession();
172
- try {
173
- const result = await discardWarmupCapture(session, 0, 0, async () => ({
174
- buffer: Buffer.from([0x01]),
175
- quantizedTime: 0,
176
- captureTimeMs: 1,
177
- }));
178
- expect(result).toBeUndefined();
179
- } finally {
180
- rmSync(session.outputDir, { recursive: true, force: true });
181
- }
182
- });
183
- });
@@ -1,78 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { readFileSync } from "node:fs";
3
- import { fileURLToPath } from "node:url";
4
- import { dirname, resolve } from "node:path";
5
-
6
- // Regression coverage for the `window.__name` no-op shim that
7
- // `frameCapture.ts` registers via `page.evaluateOnNewDocument`.
8
- //
9
- // Background: `@hyperframes/engine` ships raw TypeScript (see
10
- // `packages/engine/package.json` — main and exports both point at
11
- // `./src/index.ts`). Downstream transpilers like tsx run esbuild with
12
- // keepNames=true, which wraps named functions in `__name(fn, "name")`
13
- // calls. When Puppeteer serializes a `page.evaluate(callback)` argument
14
- // via `Function.prototype.toString()`, those wrappers travel into the
15
- // browser and throw `ReferenceError: __name is not defined` unless we
16
- // install a no-op shim first.
17
- //
18
- // These tests intentionally do NOT launch a browser — the rest of this
19
- // package follows the same pure-unit-test convention. Instead they:
20
- // 1. Assert the polyfill is wired up at the source level so it cannot
21
- // be silently removed by a careless edit.
22
- // 2. Probe the current Vitest runtime so a future maintainer can see at
23
- // a glance whether nested named functions still get `__name(...)`
24
- // wrappers under the test transformer. This is advisory: both
25
- // outcomes are acceptable — the reported observation is what makes
26
- // the test useful when the upstream behavior shifts.
27
-
28
- const __dirname = dirname(fileURLToPath(import.meta.url));
29
- const FRAME_CAPTURE_PATH = resolve(__dirname, "frameCapture.ts");
30
-
31
- describe("frameCapture __name polyfill", () => {
32
- it("registers a window.__name shim via evaluateOnNewDocument", () => {
33
- const source = readFileSync(FRAME_CAPTURE_PATH, "utf-8");
34
-
35
- expect(source).toMatch(/page\.evaluateOnNewDocument\(/);
36
- expect(source).toMatch(/typeof w\.__name !== "function"/);
37
- expect(source).toMatch(/w\.__name\s*=\s*<T>/);
38
- });
39
-
40
- it("installs the shim before any awaited browser-version checks", () => {
41
- const source = readFileSync(FRAME_CAPTURE_PATH, "utf-8");
42
-
43
- const polyfillIndex = source.indexOf("page.evaluateOnNewDocument(");
44
- const versionIndex = source.indexOf("await browser.version()");
45
-
46
- expect(polyfillIndex).toBeGreaterThan(-1);
47
- expect(versionIndex).toBeGreaterThan(-1);
48
- expect(polyfillIndex).toBeLessThan(versionIndex);
49
- });
50
-
51
- it("documents the current transpiler behavior for nested named functions", () => {
52
- function outer(): { wrapsNested: boolean; wrapsArrow: boolean } {
53
- // The unused declarations are deliberate: we are inspecting whether the
54
- // active transpiler rewrites `outer.toString()` to include
55
- // `__name(nested, ...)` / `__name(arrowNested, ...)` wrappers.
56
- // eslint-disable-next-line no-unused-vars
57
- function nested() {
58
- return 1;
59
- }
60
- // eslint-disable-next-line no-unused-vars
61
- const arrowNested = () => 2;
62
- const src = outer.toString();
63
- return {
64
- wrapsNested: /__name\(\s*nested\s*,/.test(src),
65
- wrapsArrow: /__name\(\s*\(\)\s*=>\s*2\s*,/.test(src) || /__name\(\s*arrowNested/.test(src),
66
- };
67
- }
68
-
69
- const { wrapsNested, wrapsArrow } = outer();
70
-
71
- // Both outcomes are acceptable; the value of this test is in surfacing
72
- // the runtime's behavior on the next failure (or first inspection).
73
- // If both flags become false everywhere this engine is consumed, the
74
- // polyfill above can probably be dropped. Until then it stays.
75
- expect(typeof wrapsNested).toBe("boolean");
76
- expect(typeof wrapsArrow).toBe("boolean");
77
- });
78
- });
@@ -1,153 +0,0 @@
1
- /**
2
- * Tests for `pollImagesReady` — the image-side analog of `pollVideosReady`.
3
- *
4
- * Critical contract:
5
- * - Successfully loaded image (complete=true, naturalWidth>0) → settled.
6
- * - Broken / 404 image (complete=true, naturalWidth=0) → settled.
7
- * This mirrors `pollVideosReady`'s `ve.error` early-exit. Without it,
8
- * the htmlCompiler 404-fallback path (where a remote <img> URL failed
9
- * to download and the original URL is preserved) would silently spin
10
- * the full `pageReadyTimeout` budget waiting for an image that will
11
- * never load — a 45 s regression vs the pre-PR behavior.
12
- * - In-flight image (complete=false) → still waiting.
13
- * - data: URI src → settled (no network fetch).
14
- * - Empty src → settled (nothing to load).
15
- */
16
-
17
- import { describe, expect, it } from "vitest";
18
- import type { Page } from "puppeteer-core";
19
- import { pollImagesReady } from "./frameCapture.js";
20
-
21
- interface ImageSpec {
22
- src: string;
23
- complete: boolean;
24
- naturalWidth: number;
25
- }
26
-
27
- // Mock `page` whose `evaluate(fn)` invokes `fn` with a Node-side `document`
28
- // mock that returns synthetic image objects matching the spec. Snapshots the
29
- // image state at evaluate-time, so callers can mutate `imgs` between polls
30
- // to simulate progressive load completion.
31
- function makeMockPage(imgs: () => ImageSpec[]): Page {
32
- return {
33
- evaluate: async (fn: () => unknown) => {
34
- const snapshot = imgs();
35
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
- const prevDoc = (globalThis as any).document;
37
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
- (globalThis as any).document = {
39
- querySelectorAll: () =>
40
- snapshot.map((spec) => ({
41
- getAttribute: (attr: string) => (attr === "src" ? spec.src : null),
42
- complete: spec.complete,
43
- naturalWidth: spec.naturalWidth,
44
- })),
45
- };
46
- try {
47
- return await fn();
48
- } finally {
49
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
50
- (globalThis as any).document = prevDoc;
51
- }
52
- },
53
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
- } as any as Page;
55
- }
56
-
57
- describe("pollImagesReady", () => {
58
- it("resolves immediately when there are no <img> elements", async () => {
59
- const page = makeMockPage(() => []);
60
- const result = await pollImagesReady(page, 1000, 10);
61
- expect(result).toBe(true);
62
- });
63
-
64
- it("resolves immediately when every image has loaded successfully", async () => {
65
- const page = makeMockPage(() => [
66
- { src: "/a.png", complete: true, naturalWidth: 100 },
67
- { src: "/b.png", complete: true, naturalWidth: 200 },
68
- ]);
69
- const result = await pollImagesReady(page, 1000, 10);
70
- expect(result).toBe(true);
71
- });
72
-
73
- it("treats a broken image (complete=true, naturalWidth=0) as settled — does NOT wait for timeout", async () => {
74
- // This is the bug Magi flagged. Without the broken-image escape, this
75
- // test would block the full 1000ms timeout and return false.
76
- const page = makeMockPage(() => [
77
- { src: "/a.png", complete: true, naturalWidth: 100 },
78
- { src: "https://broken.example.com/404.png", complete: true, naturalWidth: 0 },
79
- ]);
80
- const t0 = Date.now();
81
- const result = await pollImagesReady(page, 1000, 10);
82
- const elapsed = Date.now() - t0;
83
- expect(result).toBe(true);
84
- // Must resolve fast — well under the 1000ms timeout.
85
- expect(elapsed).toBeLessThan(500);
86
- });
87
-
88
- it("treats a data: URI src as settled regardless of complete/naturalWidth", async () => {
89
- const page = makeMockPage(() => [
90
- { src: "data:image/svg+xml,%3Csvg/%3E", complete: false, naturalWidth: 0 },
91
- ]);
92
- const result = await pollImagesReady(page, 1000, 10);
93
- expect(result).toBe(true);
94
- });
95
-
96
- it("treats an empty src as settled (nothing to load)", async () => {
97
- const page = makeMockPage(() => [{ src: "", complete: false, naturalWidth: 0 }]);
98
- const result = await pollImagesReady(page, 1000, 10);
99
- expect(result).toBe(true);
100
- });
101
-
102
- it("waits for an in-flight image and resolves once it completes", async () => {
103
- // Image starts in-flight, then completes after ~50ms.
104
- let started = false;
105
- const startTime = { value: 0 };
106
- const page = makeMockPage(() => {
107
- if (!started) {
108
- started = true;
109
- startTime.value = Date.now();
110
- }
111
- const elapsed = Date.now() - startTime.value;
112
- const loaded = elapsed >= 50;
113
- return [{ src: "/slow.png", complete: loaded, naturalWidth: loaded ? 100 : 0 }];
114
- });
115
- const result = await pollImagesReady(page, 1000, 10);
116
- expect(result).toBe(true);
117
- });
118
-
119
- it("times out and returns false when an in-flight image never resolves", async () => {
120
- // Image stays in-flight (complete=false) for the full timeout.
121
- const page = makeMockPage(() => [
122
- { src: "/never-loads.png", complete: false, naturalWidth: 0 },
123
- ]);
124
- const result = await pollImagesReady(page, 100, 10);
125
- expect(result).toBe(false);
126
- });
127
-
128
- it("mixed batch: loaded + broken + data: + in-flight → waits only on the in-flight image", async () => {
129
- let resolved = false;
130
- const start = Date.now();
131
- const page = makeMockPage(() => {
132
- const elapsed = Date.now() - start;
133
- if (elapsed >= 30) resolved = true;
134
- return [
135
- { src: "/loaded.png", complete: true, naturalWidth: 800 },
136
- { src: "https://broken.example.com/404.jpg", complete: true, naturalWidth: 0 },
137
- { src: "data:image/svg+xml,abc", complete: false, naturalWidth: 0 },
138
- {
139
- src: "/in-flight.png",
140
- complete: resolved,
141
- naturalWidth: resolved ? 200 : 0,
142
- },
143
- ];
144
- });
145
- const t0 = Date.now();
146
- const result = await pollImagesReady(page, 1000, 10);
147
- const elapsed = Date.now() - t0;
148
- expect(result).toBe(true);
149
- // Should wait roughly for the in-flight image to settle (~30ms) — not the
150
- // full timeout. Allow generous slack for CI scheduler jitter.
151
- expect(elapsed).toBeLessThan(500);
152
- });
153
- });
@@ -1,76 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { captureFrameToBuffer, type CaptureSession } from "./frameCapture.js";
3
-
4
- /**
5
- * Regression lock for the static-dedup reuse index.
6
- *
7
- * `captureFrameCore` must key the static-frame reuse set on the ABSOLUTE
8
- * composition frame — derived from `time` (`round(time * fps)`) — NOT the
9
- * `frameIndex` argument. Distributed / per-worker-range / parallel callers pass
10
- * a chunk-RELATIVE `frameIndex` (captureStage passes the loop `i`,
11
- * parallelCoordinator passes `i - outputFrameOffset`) while `staticFrames` is
12
- * keyed in absolute frames. A prior bug used `frameIndex`, so a chunk with
13
- * `startFrame > 0` reused the wrong frames (and the right frames missed).
14
- *
15
- * The reuse branch returns BEFORE any page interaction, so we can exercise the
16
- * decision with a stub session whose `page` throws if touched: a dedup HIT
17
- * returns the cached buffer (page untouched); a MISS proceeds to the page and
18
- * rejects. Both assertions below FAIL on the pre-fix (relative-index) code.
19
- */
20
-
21
- const SENTINEL = Buffer.from("cached-anchor-frame");
22
-
23
- // ponytail: minimal stub of the 40-field CaptureSession — only the fields the
24
- // reuse decision reads are real; `page` is a trap that throws on any access so
25
- // a dedup MISS (which falls through to prepareFrameForCapture) rejects loudly.
26
- function makeSession(staticFrames: Set<number>, fps: { num: number; den: number }): CaptureSession {
27
- const pageTrap = new Proxy(
28
- {},
29
- {
30
- get() {
31
- throw new Error("PAGE_TOUCHED");
32
- },
33
- },
34
- );
35
- return {
36
- page: pageTrap,
37
- options: { fps, format: "jpg" },
38
- captureMode: "screenshot",
39
- isInitialized: false,
40
- staticFrames,
41
- lastFrameBuffer: SENTINEL,
42
- staticDedupCount: 0,
43
- } as unknown as CaptureSession;
44
- }
45
-
46
- describe("static-dedup reuse keys on absolute frame index (time), not relative frameIndex", () => {
47
- const fps30 = { num: 30, den: 1 };
48
-
49
- it("HIT: relative frameIndex=0 but absolute time=90/30 reuses the anchor", async () => {
50
- const session = makeSession(new Set([90]), fps30);
51
- // Pre-fix used frameIndex (0) ∉ {90} → would miss → page trap throws.
52
- const result = await captureFrameToBuffer(session, 0, 90 / 30);
53
- expect(result.buffer).toBe(SENTINEL);
54
- expect(session.staticDedupCount).toBe(1);
55
- });
56
-
57
- it("MISS: relative frameIndex=90 but absolute time=0 does NOT reuse", async () => {
58
- const session = makeSession(new Set([90]), fps30);
59
- // Pre-fix used frameIndex (90) ∈ {90} → would wrongly reuse the anchor.
60
- await expect(captureFrameToBuffer(session, 90, 0)).rejects.toThrow();
61
- expect(session.staticDedupCount).toBe(0);
62
- });
63
-
64
- it("non-integer fps (29.97) recovers the absolute index exactly", async () => {
65
- const fps2997 = { num: 30000, den: 1001 };
66
- const session = makeSession(new Set([100]), fps2997);
67
- const time = (100 * fps2997.den) / fps2997.num; // absolute frame 100 → time
68
- const result = await captureFrameToBuffer(session, 7, time);
69
- expect(result.buffer).toBe(SENTINEL);
70
- });
71
-
72
- it("no reuse when the absolute frame is not in the static set", async () => {
73
- const session = makeSession(new Set([10, 11, 12]), fps30);
74
- await expect(captureFrameToBuffer(session, 0, 50 / 30)).rejects.toThrow();
75
- });
76
- });
@@ -1,174 +0,0 @@
1
- /**
2
- * Tests for the `driveWarmupTicks` BeginFrame warmup driver.
3
- *
4
- * The wall-clock warmup loop in `initializeSession` accumulates a different
5
- * number of ticks per host CPU speed, which shifts `beginFrameTimeTicks`
6
- * and breaks byte-identical captures across distributed workers.
7
- *
8
- * `lockWarmupTicks=true` clamps the loop to a fixed iteration count
9
- * (`LOCKED_WARMUP_TICKS = 60`) so the baseline becomes host-independent.
10
- */
11
-
12
- import { describe, expect, it } from "vitest";
13
- import {
14
- LOCKED_WARMUP_TICKS,
15
- driveWarmupTicks,
16
- warmupFrameTimeTicks,
17
- type WarmupTickState,
18
- } from "./frameCapture.js";
19
-
20
- function makeState(): WarmupTickState {
21
- return { running: true, ticks: 0 };
22
- }
23
-
24
- // Run the warmup driver "concurrently" with a simulated page-load that
25
- // flips `state.running` to false after `pageLoadIterations` of the
26
- // `tick` callback. Returns the final state.
27
- async function runWithSimulatedPageLoad(
28
- lockWarmupTicks: boolean,
29
- pageLoadIterations: number,
30
- tickDelay: () => Promise<void> = () => Promise.resolve(),
31
- ): Promise<WarmupTickState> {
32
- const state = makeState();
33
- const intervalMs = 33;
34
-
35
- const tick = async (): Promise<void> => {
36
- if (state.ticks + 1 === pageLoadIterations) {
37
- state.running = false;
38
- }
39
- await tickDelay();
40
- };
41
-
42
- await driveWarmupTicks(
43
- {
44
- intervalMs,
45
- lockWarmupTicks,
46
- tick,
47
- sleep: () => Promise.resolve(),
48
- },
49
- state,
50
- );
51
- return state;
52
- }
53
-
54
- describe("driveWarmupTicks — unlocked", () => {
55
- it("stops when state.running flips false", async () => {
56
- const state = await runWithSimulatedPageLoad(false, 10);
57
- expect(state.ticks).toBe(10);
58
- });
59
-
60
- it("yields different tick counts for different simulated page-load lengths", async () => {
61
- const fast = await runWithSimulatedPageLoad(false, 5);
62
- const slow = await runWithSimulatedPageLoad(false, 50);
63
- expect(fast.ticks).toBe(5);
64
- expect(slow.ticks).toBe(50);
65
- expect(fast.ticks).not.toBe(slow.ticks);
66
- });
67
-
68
- it("frame time derived as ticks * intervalMs", async () => {
69
- const state = await runWithSimulatedPageLoad(false, 7);
70
- expect(warmupFrameTimeTicks(state, 33)).toBe(7 * 33);
71
- });
72
-
73
- it("does not iterate when state.running starts false", async () => {
74
- const state: WarmupTickState = { running: false, ticks: 0 };
75
- await driveWarmupTicks(
76
- {
77
- intervalMs: 33,
78
- lockWarmupTicks: false,
79
- tick: async () => {},
80
- sleep: () => Promise.resolve(),
81
- },
82
- state,
83
- );
84
- expect(state.ticks).toBe(0);
85
- });
86
-
87
- it("counts ticks even when tick callback throws (page not ready yet)", async () => {
88
- // Pre-acquisition errors are swallowed — the loop must keep spinning so
89
- // it can drive the page once CDP comes up.
90
- let stopAt = 5;
91
- const state = makeState();
92
- await driveWarmupTicks(
93
- {
94
- intervalMs: 33,
95
- lockWarmupTicks: false,
96
- tick: async () => {
97
- if (--stopAt <= 0) state.running = false;
98
- throw new Error("CDP not ready");
99
- },
100
- sleep: () => Promise.resolve(),
101
- },
102
- state,
103
- );
104
- // We don't count ticks on throw, but the loop kept running until
105
- // state.running flipped — so it exited cleanly.
106
- expect(state.running).toBe(false);
107
- expect(state.ticks).toBe(0);
108
- });
109
- });
110
-
111
- describe("driveWarmupTicks — locked", () => {
112
- it("runs exactly LOCKED_WARMUP_TICKS iterations regardless of state.running", async () => {
113
- // Simulate a fast page load: state.running flips false after just 5 ticks.
114
- // The locked loop must still run to LOCKED_WARMUP_TICKS.
115
- const state = await runWithSimulatedPageLoad(true, 5);
116
- expect(state.ticks).toBe(LOCKED_WARMUP_TICKS);
117
- });
118
-
119
- it("runs exactly LOCKED_WARMUP_TICKS iterations on slow simulated load", async () => {
120
- // state.running stays true past the locked count — loop still stops at
121
- // LOCKED_WARMUP_TICKS.
122
- const state = await runWithSimulatedPageLoad(true, 9999);
123
- expect(state.ticks).toBe(LOCKED_WARMUP_TICKS);
124
- });
125
-
126
- it("yields IDENTICAL tick counts across simulated fast and slow loads", async () => {
127
- // THE determinism property the locked mode exists for.
128
- const fast = await runWithSimulatedPageLoad(true, 5);
129
- const slow = await runWithSimulatedPageLoad(true, 200);
130
- expect(fast.ticks).toBe(slow.ticks);
131
- expect(fast.ticks).toBe(LOCKED_WARMUP_TICKS);
132
- expect(warmupFrameTimeTicks(fast, 33)).toBe(warmupFrameTimeTicks(slow, 33));
133
- });
134
-
135
- it("yields LOCKED_WARMUP_TICKS even when state.running starts false", async () => {
136
- // Guards the locked contract independently of the caller's running flag.
137
- const state: WarmupTickState = { running: false, ticks: 0 };
138
- await driveWarmupTicks(
139
- {
140
- intervalMs: 33,
141
- lockWarmupTicks: true,
142
- tick: async () => {},
143
- sleep: () => Promise.resolve(),
144
- },
145
- state,
146
- );
147
- expect(state.ticks).toBe(LOCKED_WARMUP_TICKS);
148
- });
149
-
150
- it("does not exceed LOCKED_WARMUP_TICKS even if the caller never stops it", async () => {
151
- const state = makeState();
152
- let observedMax = 0;
153
- await driveWarmupTicks(
154
- {
155
- intervalMs: 33,
156
- lockWarmupTicks: true,
157
- tick: async () => {
158
- observedMax = Math.max(observedMax, state.ticks + 1);
159
- },
160
- sleep: () => Promise.resolve(),
161
- },
162
- state,
163
- );
164
- expect(observedMax).toBe(LOCKED_WARMUP_TICKS);
165
- expect(state.ticks).toBe(LOCKED_WARMUP_TICKS);
166
- });
167
-
168
- it("baseline frame time matches (LOCKED_WARMUP_TICKS * intervalMs)", async () => {
169
- // initializeSession derives session.beginFrameTimeTicks from the final
170
- // tick count — locked mode pins this to a host-independent value.
171
- const state = await runWithSimulatedPageLoad(true, 5);
172
- expect(warmupFrameTimeTicks(state, 33)).toBe(LOCKED_WARMUP_TICKS * 33);
173
- });
174
- });