@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.
- package/package.json +24 -7
- package/scripts/generate-lut-reference.py +0 -168
- package/scripts/test-fitTextFontSize-browser.ts +0 -135
- package/src/cdp-headless-experimental.d.ts +0 -54
- package/src/config.test.ts +0 -213
- package/src/config.ts +0 -417
- package/src/index.ts +0 -273
- package/src/services/audioMixer.test.ts +0 -326
- package/src/services/audioMixer.ts +0 -604
- package/src/services/audioMixer.types.ts +0 -35
- package/src/services/audioVolumeEnvelope.test.ts +0 -176
- package/src/services/audioVolumeEnvelope.ts +0 -138
- package/src/services/browserManager.test.ts +0 -330
- package/src/services/browserManager.ts +0 -670
- package/src/services/chunkEncoder.test.ts +0 -1415
- package/src/services/chunkEncoder.ts +0 -831
- package/src/services/chunkEncoder.types.ts +0 -60
- package/src/services/extractionCache.test.ts +0 -199
- package/src/services/extractionCache.ts +0 -216
- package/src/services/fileServer.ts +0 -110
- package/src/services/frameCapture-discardWarmup.test.ts +0 -183
- package/src/services/frameCapture-namePolyfill.test.ts +0 -78
- package/src/services/frameCapture-pollImagesReady.test.ts +0 -153
- package/src/services/frameCapture-staticDedupIndex.test.ts +0 -76
- package/src/services/frameCapture-warmupTicks.test.ts +0 -174
- package/src/services/frameCapture.test.ts +0 -192
- package/src/services/frameCapture.ts +0 -1934
- package/src/services/hdrCapture.test.ts +0 -159
- package/src/services/hdrCapture.ts +0 -315
- package/src/services/parallelCoordinator.test.ts +0 -139
- package/src/services/parallelCoordinator.ts +0 -437
- package/src/services/screenshotService.test.ts +0 -510
- package/src/services/screenshotService.ts +0 -615
- package/src/services/streamingEncoder.test.ts +0 -832
- package/src/services/streamingEncoder.ts +0 -594
- package/src/services/systemMemory.test.ts +0 -324
- package/src/services/systemMemory.ts +0 -180
- package/src/services/videoFrameExtractor.test.ts +0 -1062
- package/src/services/videoFrameExtractor.ts +0 -1139
- package/src/services/videoFrameInjector.test.ts +0 -300
- package/src/services/videoFrameInjector.ts +0 -687
- package/src/services/vp9Options.ts +0 -13
- package/src/types.ts +0 -191
- package/src/utils/alphaBlit.test.ts +0 -1349
- package/src/utils/alphaBlit.ts +0 -1015
- package/src/utils/assertSwiftShader.test.ts +0 -130
- package/src/utils/assertSwiftShader.ts +0 -126
- package/src/utils/ffmpegBinaries.test.ts +0 -43
- package/src/utils/ffmpegBinaries.ts +0 -63
- package/src/utils/ffprobe.test.ts +0 -342
- package/src/utils/ffprobe.ts +0 -457
- package/src/utils/gpuEncoder.test.ts +0 -140
- package/src/utils/gpuEncoder.ts +0 -268
- package/src/utils/hdr.test.ts +0 -191
- package/src/utils/hdr.ts +0 -137
- package/src/utils/hdrCompositing.test.ts +0 -130
- package/src/utils/htmlTemplate.test.ts +0 -42
- package/src/utils/htmlTemplate.ts +0 -42
- package/src/utils/layerCompositor.test.ts +0 -150
- package/src/utils/layerCompositor.ts +0 -58
- package/src/utils/parityContract.ts +0 -1
- package/src/utils/processTracker.test.ts +0 -74
- package/src/utils/processTracker.ts +0 -41
- package/src/utils/readWebGlVendorInfoFromCanvas.ts +0 -52
- package/src/utils/runFfmpeg.test.ts +0 -102
- package/src/utils/runFfmpeg.ts +0 -136
- package/src/utils/shaderTransitions.test.ts +0 -738
- package/src/utils/shaderTransitions.ts +0 -1130
- package/src/utils/uint16-alignment-audit.test.ts +0 -125
- package/src/utils/urlDownloader.test.ts +0 -65
- package/src/utils/urlDownloader.ts +0 -143
- package/tsconfig.json +0 -19
- 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
|
-
});
|