/** * Tests for `discardWarmupCapture` — the helper distributed chunk workers * run before their first real capture to prime `lastFrameCache`. * * The helper is a thin wrapper around the inner `captureFrameCore` * machinery, so its testable contract is post-conditional rather than * pixel-level: * * 1. The wrapper invokes the inner capture exactly once with the supplied * `(frameIndex, time)`. * 2. After the wrapper returns, the session's perf and BeginFrame damage * counters look exactly as they did before — even though the inner * capture mutated them. * 3. The wrapper writes no file to disk (no path is plumbed through; * asserted indirectly by observing that `outputDir` is never read). * 4. State is restored even if the inner capture throws. * * The inner capture is stubbed (the helper accepts an injectable * `innerCapture` for exactly this reason). We don't need a real Chrome. */ import { describe, expect, it } from "vitest"; import { existsSync, readdirSync, mkdtempSync, rmSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { discardWarmupCapture, type CaptureSession } from "./frameCapture.js"; function makeFakeSession(): CaptureSession { // The discardWarmupCapture wrapper only reads `capturePerf`, // `beginFrameHasDamageCount`, `beginFrameNoDamageCount`. Everything else // is unused — leave it as bare-minimum stubs cast through `unknown`. return { browser: {} as unknown, page: {} as unknown, options: { width: 1, height: 1, fps: { num: 30, den: 1 } }, serverUrl: "", outputDir: mkdtempSync(join(tmpdir(), "discard-warmup-")), onBeforeCapture: null, isInitialized: true, browserConsoleBuffer: [], capturePerf: { frames: 7, seekMs: 100, beforeCaptureMs: 50, screenshotMs: 200, totalMs: 350, }, captureMode: "beginframe", beginFrameTimeTicks: 0, beginFrameIntervalMs: 33, beginFrameHasDamageCount: 4, beginFrameNoDamageCount: 3, } as unknown as CaptureSession; } describe("discardWarmupCapture", () => { it("calls the inner capture exactly once with (frameIndex=0, time=0) by default", async () => { const session = makeFakeSession(); try { let calls = 0; let receivedFrameIndex = -1; let receivedTime = -1; await discardWarmupCapture(session, undefined, undefined, async (_s, fi, t) => { calls++; receivedFrameIndex = fi; receivedTime = t; return { buffer: Buffer.alloc(0), quantizedTime: t, captureTimeMs: 0 }; }); expect(calls).toBe(1); expect(receivedFrameIndex).toBe(0); expect(receivedTime).toBe(0); } finally { rmSync(session.outputDir, { recursive: true, force: true }); } }); it("passes through caller-supplied (frameIndex, time)", async () => { const session = makeFakeSession(); try { let received: { fi: number; t: number } | null = null; await discardWarmupCapture(session, 240, 8, async (_s, fi, t) => { received = { fi, t }; return { buffer: Buffer.alloc(0), quantizedTime: t, captureTimeMs: 0 }; }); expect(received).toEqual({ fi: 240, t: 8 }); } finally { rmSync(session.outputDir, { recursive: true, force: true }); } }); it("restores perf counters after the inner capture mutates them", async () => { const session = makeFakeSession(); const before = { ...session.capturePerf }; try { await discardWarmupCapture(session, 0, 0, async (s) => { s.capturePerf.frames += 1; s.capturePerf.seekMs += 12; s.capturePerf.beforeCaptureMs += 5; s.capturePerf.screenshotMs += 33; s.capturePerf.totalMs += 50; return { buffer: Buffer.alloc(0), quantizedTime: 0, captureTimeMs: 50 }; }); expect(session.capturePerf).toEqual(before); } finally { rmSync(session.outputDir, { recursive: true, force: true }); } }); it("restores BeginFrame damage counters after the inner capture mutates them", async () => { const session = makeFakeSession(); const hasBefore = session.beginFrameHasDamageCount; const noBefore = session.beginFrameNoDamageCount; try { await discardWarmupCapture(session, 0, 0, async (s) => { s.beginFrameHasDamageCount += 10; s.beginFrameNoDamageCount += 1; return { buffer: Buffer.alloc(0), quantizedTime: 0, captureTimeMs: 0 }; }); expect(session.beginFrameHasDamageCount).toBe(hasBefore); expect(session.beginFrameNoDamageCount).toBe(noBefore); } finally { rmSync(session.outputDir, { recursive: true, force: true }); } }); it("restores state even when the inner capture throws", async () => { const session = makeFakeSession(); const perfBefore = { ...session.capturePerf }; const hasBefore = session.beginFrameHasDamageCount; const noBefore = session.beginFrameNoDamageCount; try { let thrown: unknown; try { await discardWarmupCapture(session, 0, 0, async (s) => { s.capturePerf.frames += 5; s.beginFrameNoDamageCount += 2; throw new Error("simulated capture failure"); }); } catch (err) { thrown = err; } expect((thrown as Error).message).toBe("simulated capture failure"); // The whole point of `finally { restore }`: failure must not leak // inflated counters into the real capture summary. expect(session.capturePerf).toEqual(perfBefore); expect(session.beginFrameHasDamageCount).toBe(hasBefore); expect(session.beginFrameNoDamageCount).toBe(noBefore); } finally { rmSync(session.outputDir, { recursive: true, force: true }); } }); it("writes no output file to the session's outputDir", async () => { const session = makeFakeSession(); try { expect(existsSync(session.outputDir)).toBe(true); const before = readdirSync(session.outputDir); await discardWarmupCapture(session, 0, 0, async () => ({ buffer: Buffer.from([0xff, 0xff, 0xff]), quantizedTime: 0, captureTimeMs: 0, })); const after = readdirSync(session.outputDir); expect(after).toEqual(before); } finally { rmSync(session.outputDir, { recursive: true, force: true }); } }); it("returns undefined (no result type, so the buffer can't escape)", async () => { const session = makeFakeSession(); try { const result = await discardWarmupCapture(session, 0, 0, async () => ({ buffer: Buffer.from([0x01]), quantizedTime: 0, captureTimeMs: 1, })); expect(result).toBeUndefined(); } finally { rmSync(session.outputDir, { recursive: true, force: true }); } }); });