test(studio): close shadow reload regression gaps (#4223)

* test(studio): close shadow reload regression gaps

* chore: refresh follow-up checks

* test: keep follow-up free of unused exports

* test(studio): distinguish root timeline selection
This commit is contained in:
Miguel Ángel
2026-09-20 17:15:29 -04:00
committed by GitHub
parent f09ea7af3b
commit 2fb6f6987e
6 changed files with 96 additions and 2 deletions
@@ -2796,6 +2796,7 @@ describe("HyperframesPlayer asset-ready gate", () => {
pause(): void;
seek(timeInSeconds: number): void;
shaderLoader: { showAssetsLoading(): void };
_settleAssetsReady(generation: number): void;
};
beforeEach(async () => {
@@ -2974,6 +2975,25 @@ describe("HyperframesPlayer asset-ready gate", () => {
player.remove();
});
it("dispatches painted once for the current generation when settles share a loader fade", async () => {
vi.useFakeTimers();
try {
const player = await createConnectedPlayer();
const painted = vi.fn();
player.addEventListener("painted", painted);
player.shaderLoader.showAssetsLoading();
player._waitForAssetsReady(null);
player._waitForAssetsReady(null);
await vi.runOnlyPendingTimersAsync();
expect(painted).toHaveBeenCalledTimes(1);
player.remove();
} finally {
vi.useRealTimers();
}
});
it("defers play() until a pending video settles, then plays and clears the overlay attribute", async () => {
const player = await createConnectedPlayer();
@@ -52,7 +52,6 @@ it("re-evaluates set-destination availability when a reload swaps the live ifram
expect(usePlayerStore.getState().motionPathCreateAvailable).toBe(false);
act(() => {
ref.current = after;
usePreviewIframeStore.getState().setIframe(after);
});
expect(usePlayerStore.getState().motionPathCreateAvailable).toBe(true);
@@ -261,6 +261,7 @@ describe("NLEPreview", () => {
expect(players[0].style.clipPath).toBe("");
expect(players[1].style.clipPath).toBe("inset(100%)");
expect(players[1].style.visibility).toBe("hidden");
expect(players[1].style.pointerEvents).toBe("none");
view.cleanup();
});
@@ -71,7 +71,6 @@ it("re-reads the size from the promoted iframe's document", () => {
act(() => root.render(<Harness />));
expect(host.textContent).toBe('{"width":1920,"height":1080}');
act(() => {
ref.current = b;
usePreviewIframeStore.getState().setIframe(b);
});
expect(host.textContent).toBe('{"width":1080,"height":1920}');
@@ -85,6 +85,8 @@ async function mountPlayer(props: Partial<PlayerProps> = {}) {
const twoFrames = () => act(async () => void (await new Promise((r) => setTimeout(r, 80))));
const flushEffects = () => act(async () => await Promise.resolve());
function createAudioIframe() {
const iframe = document.createElement("iframe");
document.body.appendChild(iframe);
@@ -216,6 +218,38 @@ describe("ready to show", () => {
const painted = (player: TestHyperframesPlayer) =>
act(() => void player.dispatchEvent(new Event("painted")));
it("waits two animation frames before notifying that the preview can show", async () => {
const callbacks: FrameRequestCallback[] = [];
const requestAnimationFrame = vi
.spyOn(window, "requestAnimationFrame")
.mockImplementation((callback) => {
callbacks.push(callback);
return callbacks.length;
});
try {
const onReadyToShowChange = vi.fn();
const { player } = await mountPlayer({ onReadyToShowChange });
const el = player as TestHyperframesPlayer;
loadAndReady(el);
painted(el);
await flushEffects();
expect(onReadyToShowChange).not.toHaveBeenCalledWith(true);
expect(callbacks).toHaveLength(1);
act(() => callbacks.shift()?.(0));
await flushEffects();
expect(onReadyToShowChange).not.toHaveBeenCalledWith(true);
expect(callbacks).toHaveLength(1);
act(() => callbacks.shift()?.(16));
await flushEffects();
expect(onReadyToShowChange).toHaveBeenLastCalledWith(true);
} finally {
requestAnimationFrame.mockRestore();
}
});
it("promotes only once the player reports painted, not at ready or assetsready", async () => {
const onReadyToShowChange = vi.fn();
const { player } = await mountPlayer({ onReadyToShowChange });
@@ -0,0 +1,41 @@
// @vitest-environment happy-dom
import { describe, expect, it, vi } from "vitest";
import { resolvePlaybackAdapter } from "./playbackAdapterResolution";
function timeline(duration: number) {
return {
duration: vi.fn(() => duration),
time: vi.fn(() => 0),
seek: vi.fn(),
play: vi.fn(),
pause: vi.fn(),
isActive: vi.fn(() => false),
};
}
describe("resolvePlaybackAdapter", () => {
it("uses the __timelines key matching the document root, not the first key", () => {
const first = timeline(12);
const second = timeline(12);
const third = timeline(12);
const iframe = document.createElement("iframe");
const doc = document.implementation.createHTMLDocument("preview");
doc.body.innerHTML = '<div data-composition-id="second" data-duration="10"></div>';
Object.defineProperty(iframe, "contentDocument", { configurable: true, value: doc });
const win = {
__timelines: { first, second, third },
} as never;
const adapter = resolvePlaybackAdapter(iframe, win, {
cache: { current: null },
warned: { current: false },
});
expect(adapter).not.toBeNull();
adapter?.seek(7);
expect(second.seek).toHaveBeenCalledWith(7);
expect(first.seek).not.toHaveBeenCalled();
expect(third.seek).not.toHaveBeenCalled();
});
});