mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
190 lines
5.6 KiB
TypeScript
190 lines
5.6 KiB
TypeScript
// @vitest-environment happy-dom
|
|
|
|
import { act, createElement } from "react";
|
|
import { createRoot, type Root } from "react-dom/client";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
hasUnloadedAssets,
|
|
Player,
|
|
readPreviewErrorMessage,
|
|
shouldShowCompositionLoadingOverlay,
|
|
} from "./Player";
|
|
|
|
vi.mock("@hyperframes/player", () => ({}));
|
|
|
|
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
|
|
|
|
let root: Root | null = null;
|
|
let lifecycleLog: string[] = [];
|
|
|
|
class TestHyperframesPlayer extends HTMLElement {
|
|
readonly iframeElement = document.createElement("iframe");
|
|
|
|
constructor() {
|
|
super();
|
|
|
|
const addIframeListener = this.iframeElement.addEventListener.bind(this.iframeElement);
|
|
this.iframeElement.addEventListener = ((type, listener, options) => {
|
|
lifecycleLog.push(`iframe:${type}`);
|
|
addIframeListener(type, listener, options);
|
|
}) as typeof this.iframeElement.addEventListener;
|
|
|
|
const addPlayerListener = this.addEventListener.bind(this);
|
|
this.addEventListener = ((type, listener, options) => {
|
|
lifecycleLog.push(`player:${type}`);
|
|
addPlayerListener(type, listener, options);
|
|
}) as typeof this.addEventListener;
|
|
|
|
const setPlayerAttribute = this.setAttribute.bind(this);
|
|
this.setAttribute = (name, value) => {
|
|
if (name === "src") lifecycleLog.push("src");
|
|
setPlayerAttribute(name, value);
|
|
};
|
|
}
|
|
}
|
|
|
|
if (!customElements.get("hyperframes-player")) {
|
|
customElements.define("hyperframes-player", TestHyperframesPlayer);
|
|
}
|
|
|
|
afterEach(() => {
|
|
if (root) act(() => root?.unmount());
|
|
root = null;
|
|
lifecycleLog = [];
|
|
document.body.innerHTML = "";
|
|
});
|
|
|
|
async function mountPlayer() {
|
|
const host = document.createElement("div");
|
|
document.body.append(host);
|
|
root = createRoot(host);
|
|
await act(async () => {
|
|
root?.render(
|
|
createElement(Player, {
|
|
directUrl: "/api/projects/demo/preview",
|
|
onLoad: vi.fn(),
|
|
suppressLoadingOverlay: true,
|
|
}),
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
const player = host.querySelector<TestHyperframesPlayer>("hyperframes-player");
|
|
if (!player) throw new Error("player did not mount");
|
|
return { host, player };
|
|
}
|
|
|
|
function createAudioIframe() {
|
|
const iframe = document.createElement("iframe");
|
|
document.body.appendChild(iframe);
|
|
const audio = iframe.contentDocument?.createElement("audio");
|
|
expect(audio).toBeDefined();
|
|
iframe.contentDocument?.body.appendChild(audio!);
|
|
return { audio: audio!, iframe };
|
|
}
|
|
|
|
describe("preview errors", () => {
|
|
it("reads the player probe error for the visible retry state", () => {
|
|
expect(
|
|
readPreviewErrorMessage(
|
|
new CustomEvent("error", {
|
|
detail: { message: "Composition timeline not found after 8s" },
|
|
}),
|
|
),
|
|
).toBe("Composition timeline not found after 8s");
|
|
});
|
|
|
|
it("falls back when the player emits an unstructured error", () => {
|
|
expect(readPreviewErrorMessage(new Event("error"))).toBe(
|
|
"The composition preview did not become ready.",
|
|
);
|
|
});
|
|
|
|
it("attaches lifecycle listeners before navigating the player", async () => {
|
|
await mountPlayer();
|
|
const srcIndex = lifecycleLog.indexOf("src");
|
|
|
|
expect(srcIndex).toBeGreaterThan(-1);
|
|
for (const listener of [
|
|
"iframe:load",
|
|
"player:click",
|
|
"player:shadertransitionstate",
|
|
"player:ready",
|
|
"player:error",
|
|
]) {
|
|
expect(lifecycleLog.indexOf(listener)).toBeGreaterThan(-1);
|
|
expect(lifecycleLog.indexOf(listener)).toBeLessThan(srcIndex);
|
|
}
|
|
});
|
|
|
|
it("retries a failed preview with a fresh player URL", async () => {
|
|
const { host, player } = await mountPlayer();
|
|
|
|
act(() => {
|
|
player.dispatchEvent(
|
|
new CustomEvent("error", {
|
|
detail: { message: "Composition timeline not found after 8s" },
|
|
}),
|
|
);
|
|
});
|
|
|
|
expect(host.querySelector('[data-testid="composition-preview-error"]')).not.toBeNull();
|
|
const retry = Array.from(host.querySelectorAll("button")).find(
|
|
(button) => button.textContent === "Retry preview",
|
|
);
|
|
if (!retry) throw new Error("retry action did not render");
|
|
|
|
act(() => retry.click());
|
|
|
|
const retryUrl = new URL(player.getAttribute("src") ?? "", window.location.origin);
|
|
expect(retryUrl.searchParams.get("_hfStudioRetry")).toBe("1");
|
|
expect(host.querySelector('[data-testid="composition-preview-error"]')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("composition loading overlay", () => {
|
|
it("shows while the composition is loading", () => {
|
|
expect(shouldShowCompositionLoadingOverlay(true)).toBe(true);
|
|
});
|
|
|
|
it("hides after the composition is ready", () => {
|
|
expect(shouldShowCompositionLoadingOverlay(false)).toBe(false);
|
|
});
|
|
|
|
it("keeps the asset overlay up while media is still buffering", () => {
|
|
const { audio, iframe } = createAudioIframe();
|
|
Object.defineProperty(audio, "readyState", {
|
|
value: 0,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(audio, "networkState", {
|
|
value: 2,
|
|
configurable: true,
|
|
});
|
|
|
|
expect(hasUnloadedAssets(iframe, false)).toBe(true);
|
|
|
|
iframe.remove();
|
|
});
|
|
|
|
it("does not keep the asset overlay stuck on failed media sources", () => {
|
|
const { audio, iframe } = createAudioIframe();
|
|
Object.defineProperty(audio, "error", {
|
|
value: { code: 4, message: "format error" },
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(audio, "readyState", {
|
|
value: 0,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(audio, "networkState", {
|
|
value: 3,
|
|
configurable: true,
|
|
});
|
|
|
|
expect(hasUnloadedAssets(iframe, false)).toBe(false);
|
|
|
|
iframe.remove();
|
|
});
|
|
});
|