|
| 1 | +import { describe, expect, it } from "vitest"; |
| 2 | +import type { Page } from "puppeteer-core"; |
| 3 | +import { collectMediaReadinessWarnings } from "./frameCapture.js"; |
| 4 | + |
| 5 | +function makePage(input: { |
| 6 | + images?: Array<{ src: string; complete: boolean; naturalWidth: number }>; |
| 7 | + media?: Array<{ |
| 8 | + id?: string; |
| 9 | + tagName: "VIDEO" | "AUDIO"; |
| 10 | + src: string; |
| 11 | + readyState: number; |
| 12 | + networkState?: number; |
| 13 | + error?: unknown; |
| 14 | + }>; |
| 15 | +}): Page { |
| 16 | + return { |
| 17 | + evaluate: async (fn: (skipIds: readonly string[]) => unknown, skipIds: readonly string[]) => { |
| 18 | + const previousDocument = Reflect.get(globalThis, "document"); |
| 19 | + const previousMedia = Reflect.get(globalThis, "HTMLMediaElement"); |
| 20 | + Reflect.set(globalThis, "HTMLMediaElement", { |
| 21 | + NETWORK_NO_SOURCE: 3, |
| 22 | + HAVE_CURRENT_DATA: 2, |
| 23 | + }); |
| 24 | + Reflect.set(globalThis, "document", { |
| 25 | + querySelectorAll: (selector: string) => { |
| 26 | + if (selector === "img") { |
| 27 | + return (input.images ?? []).map((image) => ({ |
| 28 | + ...image, |
| 29 | + getAttribute: (name: string) => (name === "src" ? image.src : null), |
| 30 | + })); |
| 31 | + } |
| 32 | + const media = |
| 33 | + selector === "video" |
| 34 | + ? (input.media ?? []).filter((element) => element.tagName === "VIDEO") |
| 35 | + : (input.media ?? []); |
| 36 | + return media.map((media) => ({ |
| 37 | + id: media.id ?? "", |
| 38 | + tagName: media.tagName, |
| 39 | + currentSrc: media.src, |
| 40 | + readyState: media.readyState, |
| 41 | + networkState: media.networkState ?? 1, |
| 42 | + error: media.error ?? null, |
| 43 | + getAttribute: (name: string) => (name === "src" ? media.src : null), |
| 44 | + })); |
| 45 | + }, |
| 46 | + }); |
| 47 | + try { |
| 48 | + return await fn(skipIds); |
| 49 | + } finally { |
| 50 | + Reflect.set(globalThis, "document", previousDocument); |
| 51 | + Reflect.set(globalThis, "HTMLMediaElement", previousMedia); |
| 52 | + } |
| 53 | + }, |
| 54 | + } as unknown as Page; |
| 55 | +} |
| 56 | + |
| 57 | +describe("collectMediaReadinessWarnings", () => { |
| 58 | + it("returns stable warnings for visual media and ignores out-of-band audio", async () => { |
| 59 | + const page = makePage({ |
| 60 | + images: [ |
| 61 | + { src: "/pending.png", complete: false, naturalWidth: 0 }, |
| 62 | + { src: "/broken.png", complete: true, naturalWidth: 0 }, |
| 63 | + ], |
| 64 | + media: [ |
| 65 | + { tagName: "VIDEO", src: "/broken.mp4", readyState: 0, error: new Error("decode") }, |
| 66 | + { tagName: "AUDIO", src: "/pending.mp3", readyState: 1 }, |
| 67 | + { id: "injected", tagName: "VIDEO", src: "/injected.mp4", readyState: 0 }, |
| 68 | + ], |
| 69 | + }); |
| 70 | + |
| 71 | + const warnings = await collectMediaReadinessWarnings(page, ["injected"], 45000); |
| 72 | + |
| 73 | + expect(warnings.map((warning) => [warning.code, warning.details?.mediaType])).toEqual([ |
| 74 | + ["media_readiness_timeout", "image"], |
| 75 | + ["media_load_failed", "image"], |
| 76 | + ["media_load_failed", "video"], |
| 77 | + ]); |
| 78 | + expect(warnings.every((warning) => warning.details?.timeoutMs === 45000)).toBe(true); |
| 79 | + }); |
| 80 | + |
| 81 | + it("returns no warnings when every relevant resource is ready", async () => { |
| 82 | + const page = makePage({ |
| 83 | + images: [{ src: "/ready.png", complete: true, naturalWidth: 100 }], |
| 84 | + media: [ |
| 85 | + { tagName: "VIDEO", src: "/ready.mp4", readyState: 2 }, |
| 86 | + { tagName: "AUDIO", src: "/ready.mp3", readyState: 2 }, |
| 87 | + ], |
| 88 | + }); |
| 89 | + |
| 90 | + await expect(collectMediaReadinessWarnings(page, [], 1000)).resolves.toEqual([]); |
| 91 | + }); |
| 92 | +}); |
0 commit comments