/** * Tests for `pollImagesReady` — the image-side analog of `pollVideosReady`. * * Critical contract: * - Successfully loaded image (complete=true, naturalWidth>0) → settled. * - Broken / 404 image (complete=true, naturalWidth=0) → settled. * This mirrors `pollVideosReady`'s `ve.error` early-exit. Without it, * the htmlCompiler 404-fallback path (where a remote URL failed * to download and the original URL is preserved) would silently spin * the full `pageReadyTimeout` budget waiting for an image that will * never load — a 45 s regression vs the pre-PR behavior. * - In-flight image (complete=false) → still waiting. * - data: URI src → settled (no network fetch). * - Empty src → settled (nothing to load). */ import { describe, expect, it } from "vitest"; import type { Page } from "puppeteer-core"; import { pollImagesReady } from "./frameCapture.js"; interface ImageSpec { src: string; complete: boolean; naturalWidth: number; } // Mock `page` whose `evaluate(fn)` invokes `fn` with a Node-side `document` // mock that returns synthetic image objects matching the spec. Snapshots the // image state at evaluate-time, so callers can mutate `imgs` between polls // to simulate progressive load completion. function makeMockPage(imgs: () => ImageSpec[]): Page { return { evaluate: async (fn: () => unknown) => { const snapshot = imgs(); // eslint-disable-next-line @typescript-eslint/no-explicit-any const prevDoc = (globalThis as any).document; // eslint-disable-next-line @typescript-eslint/no-explicit-any (globalThis as any).document = { querySelectorAll: () => snapshot.map((spec) => ({ getAttribute: (attr: string) => (attr === "src" ? spec.src : null), complete: spec.complete, naturalWidth: spec.naturalWidth, })), }; try { return await fn(); } finally { // eslint-disable-next-line @typescript-eslint/no-explicit-any (globalThis as any).document = prevDoc; } }, // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any as Page; } describe("pollImagesReady", () => { it("resolves immediately when there are no elements", async () => { const page = makeMockPage(() => []); const result = await pollImagesReady(page, 1000, 10); expect(result).toBe(true); }); it("resolves immediately when every image has loaded successfully", async () => { const page = makeMockPage(() => [ { src: "/a.png", complete: true, naturalWidth: 100 }, { src: "/b.png", complete: true, naturalWidth: 200 }, ]); const result = await pollImagesReady(page, 1000, 10); expect(result).toBe(true); }); it("treats a broken image (complete=true, naturalWidth=0) as settled — does NOT wait for timeout", async () => { // This is the bug Magi flagged. Without the broken-image escape, this // test would block the full 1000ms timeout and return false. const page = makeMockPage(() => [ { src: "/a.png", complete: true, naturalWidth: 100 }, { src: "https://broken.example.com/404.png", complete: true, naturalWidth: 0 }, ]); const t0 = Date.now(); const result = await pollImagesReady(page, 1000, 10); const elapsed = Date.now() - t0; expect(result).toBe(true); // Must resolve fast — well under the 1000ms timeout. expect(elapsed).toBeLessThan(500); }); it("treats a data: URI src as settled regardless of complete/naturalWidth", async () => { const page = makeMockPage(() => [ { src: "data:image/svg+xml,%3Csvg/%3E", complete: false, naturalWidth: 0 }, ]); const result = await pollImagesReady(page, 1000, 10); expect(result).toBe(true); }); it("treats an empty src as settled (nothing to load)", async () => { const page = makeMockPage(() => [{ src: "", complete: false, naturalWidth: 0 }]); const result = await pollImagesReady(page, 1000, 10); expect(result).toBe(true); }); it("waits for an in-flight image and resolves once it completes", async () => { // Image starts in-flight, then completes after ~50ms. let started = false; const startTime = { value: 0 }; const page = makeMockPage(() => { if (!started) { started = true; startTime.value = Date.now(); } const elapsed = Date.now() - startTime.value; const loaded = elapsed >= 50; return [{ src: "/slow.png", complete: loaded, naturalWidth: loaded ? 100 : 0 }]; }); const result = await pollImagesReady(page, 1000, 10); expect(result).toBe(true); }); it("times out and returns false when an in-flight image never resolves", async () => { // Image stays in-flight (complete=false) for the full timeout. const page = makeMockPage(() => [ { src: "/never-loads.png", complete: false, naturalWidth: 0 }, ]); const result = await pollImagesReady(page, 100, 10); expect(result).toBe(false); }); it("mixed batch: loaded + broken + data: + in-flight → waits only on the in-flight image", async () => { let resolved = false; const start = Date.now(); const page = makeMockPage(() => { const elapsed = Date.now() - start; if (elapsed >= 30) resolved = true; return [ { src: "/loaded.png", complete: true, naturalWidth: 800 }, { src: "https://broken.example.com/404.jpg", complete: true, naturalWidth: 0 }, { src: "data:image/svg+xml,abc", complete: false, naturalWidth: 0 }, { src: "/in-flight.png", complete: resolved, naturalWidth: resolved ? 200 : 0, }, ]; }); const t0 = Date.now(); const result = await pollImagesReady(page, 1000, 10); const elapsed = Date.now() - t0; expect(result).toBe(true); // Should wait roughly for the in-flight image to settle (~30ms) — not the // full timeout. Allow generous slack for CI scheduler jitter. expect(elapsed).toBeLessThan(500); }); });