@hyperframes/engine 0.6.119 → 0.6.121

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,510 +0,0 @@
1
- // @vitest-environment node
2
- import { describe, it, expect, vi } from "vitest";
3
- import { parseHTML } from "linkedom";
4
- import { type Page } from "puppeteer-core";
5
- import {
6
- pageScreenshotCapture,
7
- cdpSessionCache,
8
- injectVideoFramesBatch,
9
- syncVideoFrameVisibility,
10
- } from "./screenshotService.js";
11
-
12
- // Stub a Page + CDPSession just enough that pageScreenshotCapture can call
13
- // `client.send("Page.captureScreenshot", ...)` and we can inspect the args.
14
- function makeFakePageWithCdp(send: (method: string, params: object) => Promise<{ data: string }>) {
15
- const fakeSession = { send } as unknown as import("puppeteer-core").CDPSession;
16
- // Stub a Page object — the WeakMap cache is the only Page-thing used in the
17
- // path under test, so we can pre-seed it and skip page.createCDPSession().
18
- const fakePage = {} as Page;
19
- cdpSessionCache.set(fakePage, fakeSession);
20
- return fakePage;
21
- }
22
-
23
- describe("pageScreenshotCapture supersample plumbing", () => {
24
- // Minimal 1×1 transparent PNG, base64. The function returns Buffer.from(data, "base64")
25
- // and we never inspect the bytes — only the params we pass to client.send.
26
- const ONE_PIXEL_PNG_B64 =
27
- "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=";
28
-
29
- it("passes `clip` with scale 1 when deviceScaleFactor is undefined (default 1)", async () => {
30
- const send = vi.fn().mockResolvedValue({ data: ONE_PIXEL_PNG_B64 });
31
- const page = makeFakePageWithCdp(send);
32
-
33
- await pageScreenshotCapture(page, {
34
- width: 1920,
35
- height: 1080,
36
- fps: { num: 30, den: 1 },
37
- format: "jpeg",
38
- quality: 80,
39
- });
40
-
41
- expect(send).toHaveBeenCalledWith(
42
- "Page.captureScreenshot",
43
- expect.objectContaining({
44
- clip: { x: 0, y: 0, width: 1920, height: 1080, scale: 1 },
45
- }),
46
- );
47
- });
48
-
49
- it("passes `clip` with scale 1 when deviceScaleFactor is exactly 1", async () => {
50
- const send = vi.fn().mockResolvedValue({ data: ONE_PIXEL_PNG_B64 });
51
- const page = makeFakePageWithCdp(send);
52
-
53
- await pageScreenshotCapture(page, {
54
- width: 1920,
55
- height: 1080,
56
- fps: { num: 30, den: 1 },
57
- format: "jpeg",
58
- deviceScaleFactor: 1,
59
- });
60
-
61
- const params = send.mock.calls[0]?.[1] as { clip?: { scale: number } };
62
- expect(params.clip).toEqual({ x: 0, y: 0, width: 1920, height: 1080, scale: 1 });
63
- });
64
-
65
- it("passes `clip` with `scale = dpr` when deviceScaleFactor > 1 (the supersample contract)", async () => {
66
- const send = vi.fn().mockResolvedValue({ data: ONE_PIXEL_PNG_B64 });
67
- const page = makeFakePageWithCdp(send);
68
-
69
- await pageScreenshotCapture(page, {
70
- width: 1920,
71
- height: 1080,
72
- fps: { num: 30, den: 1 },
73
- format: "jpeg",
74
- deviceScaleFactor: 2,
75
- });
76
-
77
- expect(send).toHaveBeenCalledWith(
78
- "Page.captureScreenshot",
79
- expect.objectContaining({
80
- clip: { x: 0, y: 0, width: 1920, height: 1080, scale: 2 },
81
- }),
82
- );
83
- });
84
-
85
- it("propagates a non-2 supersample factor (e.g. 720p → 4K = 3×)", async () => {
86
- const send = vi.fn().mockResolvedValue({ data: ONE_PIXEL_PNG_B64 });
87
- const page = makeFakePageWithCdp(send);
88
-
89
- await pageScreenshotCapture(page, {
90
- width: 1280,
91
- height: 720,
92
- fps: { num: 30, den: 1 },
93
- format: "jpeg",
94
- deviceScaleFactor: 3,
95
- });
96
-
97
- const params = send.mock.calls[0]?.[1] as { clip?: { scale: number } };
98
- expect(params.clip?.scale).toBe(3);
99
- });
100
- });
101
-
102
- describe("injectVideoFramesBatch replacement layout", () => {
103
- it("does not copy opposing inset constraints onto the injected frame image", async () => {
104
- const { window, document } = parseHTML(
105
- '<html><body><div id="root"><video id="clip" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover"></video></div></body></html>',
106
- );
107
-
108
- Object.defineProperty(window.HTMLImageElement.prototype, "decode", {
109
- configurable: true,
110
- value: () => Promise.resolve(),
111
- });
112
-
113
- const video = document.getElementById("clip") as HTMLVideoElement;
114
- Object.defineProperties(video, {
115
- offsetLeft: { configurable: true, get: () => 0 },
116
- offsetTop: { configurable: true, get: () => 0 },
117
- offsetWidth: { configurable: true, get: () => 1920 },
118
- offsetHeight: { configurable: true, get: () => 1080 },
119
- });
120
- video.getBoundingClientRect = () =>
121
- ({
122
- x: 0,
123
- y: 0,
124
- left: 0,
125
- top: 0,
126
- right: 1920,
127
- bottom: 1080,
128
- width: 1920,
129
- height: 1080,
130
- toJSON: () => ({}),
131
- }) as DOMRect;
132
-
133
- const computedStyle = document.createElement("div").style;
134
- computedStyle.position = "absolute";
135
- computedStyle.width = "1920px";
136
- computedStyle.height = "1080px";
137
- computedStyle.top = "0px";
138
- computedStyle.left = "0px";
139
- computedStyle.right = "0px";
140
- computedStyle.bottom = "0px";
141
- computedStyle.inset = "0px";
142
- computedStyle.objectFit = "cover";
143
- computedStyle.objectPosition = "center center";
144
- computedStyle.zIndex = "3";
145
- computedStyle.opacity = "1";
146
- Object.defineProperty(window, "getComputedStyle", {
147
- configurable: true,
148
- value: () => computedStyle,
149
- });
150
-
151
- const globals = globalThis as unknown as {
152
- window?: typeof window;
153
- document?: Document;
154
- };
155
- const previousWindow = globals.window;
156
- const previousDocument = globals.document;
157
- globals.window = window;
158
- globals.document = document;
159
- try {
160
- const page = {
161
- evaluate: async (
162
- fn: (
163
- updates: Array<{ videoId: string; dataUri: string }>,
164
- visualProperties: string[],
165
- ) => Promise<void>,
166
- updates: Array<{ videoId: string; dataUri: string }>,
167
- visualProperties: string[],
168
- ) => fn(updates, visualProperties),
169
- } as unknown as Page;
170
-
171
- await injectVideoFramesBatch(page, [
172
- {
173
- videoId: "clip",
174
- dataUri:
175
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=",
176
- },
177
- ]);
178
- } finally {
179
- globals.window = previousWindow;
180
- globals.document = previousDocument;
181
- }
182
-
183
- const img = video.nextElementSibling as HTMLImageElement | null;
184
- expect(img).not.toBeNull();
185
- expect(img?.style.position).toBe("absolute");
186
- expect(img?.style.left).toBe("0px");
187
- expect(img?.style.top).toBe("0px");
188
- expect(img?.style.width).toBe("1920px");
189
- expect(img?.style.height).toBe("1080px");
190
- expect(img?.style.right).toBe("auto");
191
- expect(img?.style.bottom).toBe("auto");
192
- expect(img?.style.inset).toBe("auto");
193
- });
194
- });
195
-
196
- describe("video-frame injection respects ancestor visibility", () => {
197
- // Regression guard: the runtime's `[data-start]` lifecycle hides
198
- // out-of-window sub-composition hosts with `visibility:hidden`, but the
199
- // injector used to ignore that and paint a replacement <img> for every
200
- // active `<video data-start>` element. Inner-PIP videos inside *other*
201
- // moments still appear active in the raw time-window check (their auto-
202
- // injected `data-start="0"` + probed full-source duration cover the
203
- // whole timeline), so the bug produced one full-bleed speaker overlay
204
- // per inactive sub-comp — covering whichever moment was actually visible.
205
- //
206
- // The skip is intentionally narrow: `visibility:hidden` on a regular
207
- // `[data-start]` container must NOT skip injection, because the
208
- // replacement <img>'s explicit `visibility:visible` overrides the
209
- // ancestor (CSS spec) and consumers rely on that to hold the final
210
- // GSAP-driven frame when an authored `data-duration` outlives the
211
- // composition's GSAP timeline. We therefore only treat
212
- // `visibility:hidden` as a skip signal on sub-composition hosts
213
- // (`[data-composition-src]` / `[data-composition-file]`). `display:none`,
214
- // by contrast, takes the whole subtree out of layout regardless of any
215
- // child override, so it always triggers the skip.
216
-
217
- type StyleLike = {
218
- display?: string;
219
- visibility?: string;
220
- opacity?: string;
221
- objectFit?: string;
222
- objectPosition?: string;
223
- zIndex?: string;
224
- };
225
-
226
- type HostAttribute = "data-composition-src" | "data-composition-file" | "data-start";
227
-
228
- function setupHostHiddenScenario(
229
- hostStyle: StyleLike,
230
- options: { hostAttribute?: HostAttribute } = {},
231
- ) {
232
- const hostAttribute = options.hostAttribute ?? "data-composition-src";
233
- const hostAttrMarkup =
234
- hostAttribute === "data-start"
235
- ? 'data-start="0" data-duration="10"'
236
- : `${hostAttribute}="sub.html"`;
237
- const { window, document } = parseHTML(
238
- `<html><body><div id="host" ${hostAttrMarkup}><div id="pip-frame"><video id="pip" data-start="0" data-duration="10"></video></div></div></body></html>`,
239
- );
240
-
241
- Object.defineProperty(window.HTMLImageElement.prototype, "decode", {
242
- configurable: true,
243
- value: () => Promise.resolve(),
244
- });
245
-
246
- const host = document.getElementById("host") as HTMLElement;
247
- const pipFrame = document.getElementById("pip-frame") as HTMLElement;
248
- const video = document.getElementById("pip") as HTMLVideoElement;
249
-
250
- Object.defineProperties(video, {
251
- offsetLeft: { configurable: true, get: () => 0 },
252
- offsetTop: { configurable: true, get: () => 0 },
253
- offsetWidth: { configurable: true, get: () => 1080 },
254
- offsetHeight: { configurable: true, get: () => 1920 },
255
- });
256
- video.getBoundingClientRect = () =>
257
- ({
258
- x: 0,
259
- y: 0,
260
- left: 0,
261
- top: 0,
262
- right: 1080,
263
- bottom: 1920,
264
- width: 1080,
265
- height: 1920,
266
- toJSON: () => ({}),
267
- }) as DOMRect;
268
-
269
- const styles = new Map<Element, StyleLike>();
270
- styles.set(host, hostStyle);
271
- styles.set(pipFrame, {});
272
- styles.set(video, { opacity: "1", objectFit: "cover", objectPosition: "center", zIndex: "1" });
273
-
274
- Object.defineProperty(window, "getComputedStyle", {
275
- configurable: true,
276
- value: (el: Element) => {
277
- const declared = styles.get(el) ?? {};
278
- return {
279
- display: declared.display ?? "block",
280
- visibility: declared.visibility ?? "visible",
281
- opacity: declared.opacity ?? "1",
282
- objectFit: declared.objectFit ?? "fill",
283
- objectPosition: declared.objectPosition ?? "50% 50%",
284
- zIndex: declared.zIndex ?? "auto",
285
- getPropertyValue: (prop: string) => {
286
- const camel = prop.replace(/-([a-z])/g, (_, c: string) =>
287
- c.toUpperCase(),
288
- ) as keyof StyleLike;
289
- return declared[camel] ?? "";
290
- },
291
- };
292
- },
293
- });
294
-
295
- return { window, document, video, host, pipFrame };
296
- }
297
-
298
- function withGlobals<T extends { window: Window; document: Document; video: HTMLVideoElement }>(
299
- setup: T,
300
- ): { teardown: () => void; setup: T } {
301
- const globals = globalThis as unknown as { window?: Window; document?: Document };
302
- const previousWindow = globals.window;
303
- const previousDocument = globals.document;
304
- globals.window = setup.window;
305
- globals.document = setup.document;
306
- return {
307
- setup,
308
- teardown: () => {
309
- globals.window = previousWindow;
310
- globals.document = previousDocument;
311
- },
312
- };
313
- }
314
-
315
- function passthroughPage(): Page {
316
- return {
317
- evaluate: async (fn: (...args: unknown[]) => unknown, ...args: unknown[]) =>
318
- // The implementation is built to run inside the page sandbox via
319
- // `page.evaluate`, but linkedom gives us a DOM compatible enough to
320
- // execute the function body directly in Node.
321
- Promise.resolve((fn as (...a: unknown[]) => unknown)(...args)),
322
- } as unknown as Page;
323
- }
324
-
325
- it("skips replacement-frame creation when the video's host has visibility:hidden", async () => {
326
- const { teardown, setup } = withGlobals(setupHostHiddenScenario({ visibility: "hidden" }));
327
- try {
328
- await injectVideoFramesBatch(passthroughPage(), [
329
- {
330
- videoId: "pip",
331
- dataUri:
332
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=",
333
- },
334
- ]);
335
- } finally {
336
- teardown();
337
- }
338
-
339
- // No replacement <img> should be injected next to the video — the host is
340
- // currently hidden, so painting a frame over it would bleed onto whichever
341
- // sibling host is actually visible on this seek.
342
- const sibling = setup.video.nextElementSibling as HTMLElement | null;
343
- expect(sibling).toBeNull();
344
- });
345
-
346
- it("skips replacement-frame creation when the video's host has display:none", async () => {
347
- const { teardown, setup } = withGlobals(setupHostHiddenScenario({ display: "none" }));
348
- try {
349
- await injectVideoFramesBatch(passthroughPage(), [
350
- {
351
- videoId: "pip",
352
- dataUri:
353
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=",
354
- },
355
- ]);
356
- } finally {
357
- teardown();
358
- }
359
-
360
- const sibling = setup.video.nextElementSibling as HTMLElement | null;
361
- expect(sibling).toBeNull();
362
- });
363
-
364
- it("hides an existing replacement <img> when the host becomes visibility:hidden", async () => {
365
- // First seed an existing __render_frame__ <img> next to the video (the
366
- // state the page is in after a previous seek when the host was visible).
367
- const { teardown, setup } = withGlobals(setupHostHiddenScenario({ visibility: "hidden" }));
368
- const seededImg = setup.document.createElement("img");
369
- seededImg.classList.add("__render_frame__");
370
- seededImg.style.visibility = "visible";
371
- setup.video.parentNode?.insertBefore(seededImg, setup.video.nextSibling);
372
-
373
- try {
374
- await injectVideoFramesBatch(passthroughPage(), [
375
- {
376
- videoId: "pip",
377
- dataUri:
378
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=",
379
- },
380
- ]);
381
- } finally {
382
- teardown();
383
- }
384
-
385
- expect(seededImg.style.visibility).toBe("hidden");
386
- });
387
-
388
- it("syncVideoFrameVisibility hides the replacement <img> for ancestor-hidden actives", async () => {
389
- const { teardown, setup } = withGlobals(setupHostHiddenScenario({ visibility: "hidden" }));
390
- const seededImg = setup.document.createElement("img");
391
- seededImg.classList.add("__render_frame__");
392
- seededImg.style.visibility = "visible";
393
- setup.video.parentNode?.insertBefore(seededImg, setup.video.nextSibling);
394
-
395
- try {
396
- // "pip" IS in the active set (per the raw time-window check) but the
397
- // host is hidden. sync must keep the <img> hidden, not flip it to
398
- // `visibility: visible`.
399
- await syncVideoFrameVisibility(passthroughPage(), ["pip"]);
400
- } finally {
401
- teardown();
402
- }
403
-
404
- expect(seededImg.style.visibility).toBe("hidden");
405
- });
406
-
407
- it("still injects when a plain [data-start] host is visibility:hidden (CSS-escapable)", async () => {
408
- // Regression guard for the style-9-prod symptom: a regular
409
- // `[data-start]` container whose GSAP timeline is shorter than its
410
- // authored `data-duration` ends up `visibility: hidden` past the
411
- // timeline end. The replacement <img>'s explicit `visibility: visible`
412
- // correctly overrides that per CSS spec, so the injector must NOT
413
- // short-circuit — it would otherwise drop the final-state frame and
414
- // produce blank tail frames.
415
- const { teardown, setup } = withGlobals(
416
- setupHostHiddenScenario({ visibility: "hidden" }, { hostAttribute: "data-start" }),
417
- );
418
-
419
- try {
420
- await injectVideoFramesBatch(passthroughPage(), [
421
- {
422
- videoId: "pip",
423
- dataUri:
424
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=",
425
- },
426
- ]);
427
- } finally {
428
- teardown();
429
- }
430
-
431
- const sibling = setup.video.nextElementSibling as HTMLElement | null;
432
- expect(sibling).not.toBeNull();
433
- expect(sibling?.classList.contains("__render_frame__")).toBe(true);
434
- expect(sibling?.style.visibility).toBe("visible");
435
- });
436
-
437
- it("syncVideoFrameVisibility shows the replacement <img> when a plain [data-start] host is visibility:hidden", async () => {
438
- const { teardown, setup } = withGlobals(
439
- setupHostHiddenScenario({ visibility: "hidden" }, { hostAttribute: "data-start" }),
440
- );
441
- const seededImg = setup.document.createElement("img");
442
- seededImg.classList.add("__render_frame__");
443
- seededImg.style.visibility = "hidden";
444
- setup.video.parentNode?.insertBefore(seededImg, setup.video.nextSibling);
445
-
446
- try {
447
- await syncVideoFrameVisibility(passthroughPage(), ["pip"]);
448
- } finally {
449
- teardown();
450
- }
451
-
452
- // The host's `visibility: hidden` is escapable; sync must flip the
453
- // <img> to `visibility: visible` so it overrides the ancestor.
454
- expect(seededImg.style.visibility).toBe("visible");
455
- });
456
-
457
- // Regression for the layered/HDR mask path: `applyDomLayerMask` writes an
458
- // `!important` stylesheet rule `#${showId} *{visibility:visible !important}`
459
- // which, if a sub-comp host id appears in the show set, would revive a
460
- // plain (non-important) inline `visibility: hidden` on a descendant
461
- // `__render_frame__` — the cascade rule is "important stylesheet author
462
- // beats non-important inline author". To stay safe regardless of which
463
- // layer ends up in `show`, the ancestor-hidden hide must be written with
464
- // `!important` so inline `!important` beats stylesheet `!important`.
465
- //
466
- // linkedom strips `!important` from `cssText`/`getPropertyPriority`, so we
467
- // pin the contract on the API call site instead: a `setProperty(name,
468
- // value, "important")` invocation on the live `<img>`'s style.
469
- it("injectVideoFramesBatch hides a stale <img> with !important so the layer mask cannot revive it", async () => {
470
- const { teardown, setup } = withGlobals(setupHostHiddenScenario({ visibility: "hidden" }));
471
- const seededImg = setup.document.createElement("img");
472
- seededImg.classList.add("__render_frame__");
473
- seededImg.style.visibility = "visible";
474
- setup.video.parentNode?.insertBefore(seededImg, setup.video.nextSibling);
475
- const setPropertySpy = vi.spyOn(seededImg.style, "setProperty");
476
-
477
- try {
478
- await injectVideoFramesBatch(passthroughPage(), [
479
- {
480
- videoId: "pip",
481
- dataUri:
482
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=",
483
- },
484
- ]);
485
- } finally {
486
- teardown();
487
- }
488
-
489
- expect(seededImg.style.visibility).toBe("hidden");
490
- expect(setPropertySpy).toHaveBeenCalledWith("visibility", "hidden", "important");
491
- });
492
-
493
- it("syncVideoFrameVisibility hides an existing <img> with !important so the layer mask cannot revive it", async () => {
494
- const { teardown, setup } = withGlobals(setupHostHiddenScenario({ visibility: "hidden" }));
495
- const seededImg = setup.document.createElement("img");
496
- seededImg.classList.add("__render_frame__");
497
- seededImg.style.visibility = "visible";
498
- setup.video.parentNode?.insertBefore(seededImg, setup.video.nextSibling);
499
- const setPropertySpy = vi.spyOn(seededImg.style, "setProperty");
500
-
501
- try {
502
- await syncVideoFrameVisibility(passthroughPage(), ["pip"]);
503
- } finally {
504
- teardown();
505
- }
506
-
507
- expect(seededImg.style.visibility).toBe("hidden");
508
- expect(setPropertySpy).toHaveBeenCalledWith("visibility", "hidden", "important");
509
- });
510
- });