Files
hyperframes/packages/player/src/parent-media.test.ts
T
Vance IngallsandClaude Fable 5 992a9b6607 feat(lint,player): fast-capture lint rule + player media sync (#1921)
* feat(engine): drawElementImage capture service

* feat(engine): 3D projection + compositor-effect risk gate

* fix(engine): gate filter drop-shadow wherever blur gates (review)

detectCssEffectRisk documented drop-shadow as a ~29dB damage case but only
detected blur( in its three scan paths — a drop-shadow comp stayed on the
fast path despite the gate's own correctness contract. Detect drop-shadow(
in computed styles, stylesheet rules, and GSAP tween vars, pinned by a
focused test that runs the real page-side closure against a DOM shim
(computed / stylesheet / tween coverage + blur regression + effect-free
null).

Addresses miguel-heygen's blocker on #1918.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(engine): frame-capture core — fast-capture routing, worker-encode, dedup extension

# Conflicts:
#	packages/engine/src/services/screenshotService.ts

* fix(engine): document HF_FORCE_DRAWELEMENT as diagnostic-only; make armStaticDedup idempotent (review)

Addresses miguel-heygen's blockers on #1919:

- HF_FORCE_DRAWELEMENT promoted from a stale "SCRATCH/Uncommitted" comment to
  a documented diagnostic flag: it exists for upstream-Chromium repro work
  (gate-vs-API isolation, crbug 521861819 149-vs-151) and R&D on gated effect
  classes; renders under it may be damaged BY DESIGN since it bypasses gates
  whose thresholds encode measured damage. Never production; the safety-net
  blank guard also stands down under it so diagnostic frames arrive unmodified.
- armStaticDedup is now idempotent: the drawElement init path arms dedup
  before canvas injection, then initializeSession called it again — the
  second run overwrote the armed state with skipReason="capture_mode"
  (captureMode is "drawelement" by then), producing contradictory telemetry
  (armed frames + a skip reason), and re-ran the verification seeks on the
  fallback path. It now no-ops once staticFrames or a skip decision exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(producer): fast-capture render stages + remote bg-image localizer

* feat(lint,player): fast-capture lint rule + player media sync

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 16:22:35 -07:00

165 lines
5.8 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { ParentMediaManager, type ProxyEntry } from "./parent-media";
// A fake media element whose paused state is driven by play()/pause() stubs.
function makeFakeAudio(initiallyPaused: boolean): HTMLMediaElement {
const el = new Audio();
let paused = initiallyPaused;
Object.defineProperty(el, "paused", { get: () => paused });
el.pause = () => {
paused = true;
};
el.play = () => {
paused = false;
return Promise.resolve();
};
el.src = "https://example.test/music.mp3";
return el;
}
function makeManager(overrides: Partial<{ isPaused: boolean; owner: "runtime" | "parent" }> = {}) {
const mgr = new ParentMediaManager({
dispatchEvent: () => {},
getMuted: () => false,
getVolume: () => 1,
getPlaybackRate: () => 1,
getCurrentTime: () => 0,
isPaused: () => overrides.isPaused ?? true,
});
return mgr;
}
describe("ParentMediaManager audio-src proxy lifecycle", () => {
it("replaces the audio-src proxy instead of stacking a second one", () => {
const mgr = makeManager();
mgr.setupFromUrl("https://example.test/a.mp3");
expect(mgr.entries).toHaveLength(1);
mgr.setupFromUrl("https://example.test/b.mp3");
// The old proxy must be gone, not accumulated alongside the new one.
expect(mgr.entries).toHaveLength(1);
expect(mgr.entries[0].el.src).toBe("https://example.test/b.mp3");
});
it("is a no-op when the same audio-src URL is set again", () => {
const mgr = makeManager();
mgr.setupFromUrl("https://example.test/a.mp3");
const first = mgr.entries[0];
mgr.setupFromUrl("https://example.test/a.mp3");
expect(mgr.entries).toHaveLength(1);
// Same element reference — not torn down and rebuilt.
expect(mgr.entries[0]).toBe(first);
});
it("clears the audio-src proxy on teardownUrlAudio", () => {
const mgr = makeManager();
mgr.setupFromUrl("https://example.test/a.mp3");
const el = mgr.entries[0].el;
mgr.teardownUrlAudio();
expect(mgr.entries).toHaveLength(0);
// The proxy's source is reset so it stops preloading.
expect(el.src).not.toBe("https://example.test/a.mp3");
});
it("teardownUrlAudio removes only the url proxy, leaving other entries", () => {
const mgr = makeManager();
// Simulate an iframe-adopted entry already in the pool.
const adopted: ProxyEntry = {
el: new Audio(),
start: 0,
duration: Infinity,
driftSamples: 0,
};
adopted.el.src = "https://example.test/iframe-clip.mp4";
mgr.entries.push(adopted);
mgr.setupFromUrl("https://example.test/a.mp3");
expect(mgr.entries).toHaveLength(2);
mgr.teardownUrlAudio();
expect(mgr.entries).toHaveLength(1);
expect(mgr.entries[0]).toBe(adopted);
});
it("teardownUrlAudio is safe to call with no audio-src set", () => {
const mgr = makeManager();
expect(() => mgr.teardownUrlAudio()).not.toThrow();
expect(mgr.entries).toHaveLength(0);
});
it("pauses a proxy once the playhead passes the clip end (trimmed clip)", () => {
const mgr = makeManager({ owner: "parent", isPaused: false });
const el = makeFakeAudio(false); // already playing within the clip
mgr.entries.push({ el, start: 0, duration: 5, driftSamples: 0 });
mgr.mirrorTime(3); // inside [0, 5) — stays playing
expect(el.paused).toBe(false);
mgr.mirrorTime(6); // past the trimmed end — must pause
expect(el.paused).toBe(true);
});
it("re-reads the source element's live data-duration so trims bound the proxy", () => {
const mgr = makeManager({ owner: "parent", isPaused: false });
const source = new Audio();
source.setAttribute("data-start", "0");
source.setAttribute("data-duration", "30");
// jsdom reports isConnected=false unless attached; attach it.
document.body.appendChild(source);
const el = makeFakeAudio(false);
mgr.entries.push({ el, start: 0, duration: 30, driftSamples: 0, source });
mgr.mirrorTime(20); // within 30 → playing
expect(el.paused).toBe(false);
// User trims the clip to 10s; the proxy must pick it up and pause at 20s.
source.setAttribute("data-duration", "10");
mgr.mirrorTime(20);
expect(el.paused).toBe(true);
source.remove();
});
it("scrubAll plays in-window proxies at the playhead and pauses out-of-window ones", () => {
const mgr = makeManager({ owner: "parent" });
const inWin = makeFakeAudio(true); // currently paused — scrub should start it
const outWin = makeFakeAudio(false); // currently playing, but outside its window
mgr.entries.push({ el: inWin, start: 0, duration: 5, driftSamples: 0 });
mgr.entries.push({ el: outWin, start: 10, duration: 5, driftSamples: 0 });
mgr.scrubAll(2); // playhead at 2s
// in-window proxy: positioned at rel time and AUDIBLE (the point of scrub-audio)
expect(inWin.currentTime).toBe(2);
expect(inWin.paused).toBe(false);
// out-of-window proxy: paused, not blipped
expect(outWin.paused).toBe(true);
});
it("does not duplicate or hijack a clip the composition already owns", () => {
const mgr = makeManager();
// The composition already adopted a clip with this URL.
const adopted: ProxyEntry = {
el: new Audio(),
start: 0,
duration: Infinity,
driftSamples: 0,
};
adopted.el.src = "https://example.test/shared.mp3";
mgr.entries.push(adopted);
// Pointing audio-src at the same URL must not create a second proxy...
mgr.setupFromUrl("https://example.test/shared.mp3");
expect(mgr.entries).toHaveLength(1);
expect(mgr.entries[0]).toBe(adopted);
// ...and removing audio-src must not tear down the composition's own clip
// (teardown targets the tracked proxy by reference, not by URL match).
mgr.teardownUrlAudio();
expect(mgr.entries).toHaveLength(1);
expect(mgr.entries[0]).toBe(adopted);
});
});