mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 00:56:23 +00:00
* feat: initial code port from hyperframes-internal Port all OSS-ready packages from the internal monorepo: - @hyperframes/core — shared types, HTML generation, GSAP utilities, runtime - @hyperframes/cli — CLI for creating, previewing, and rendering compositions - @hyperframes/engine — framework-agnostic rendering engine (BeginFrame + FFmpeg) - @hyperframes/producer — video rendering pipeline (Puppeteer + FFmpeg) - @hyperframes/ui-player — browser-based video player component - @hyperframes/studio — composition editor (React frontend + Hono backend) Includes regression test suite with Docker-based test harness. All HeyGen-internal references, deployment infrastructure, and proprietary assets have been removed. Package names migrated from @app/* to @hyperframes/*. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: scrub internal codenames and stale references from OSS port - Replace static.heygen.ai runtime URLs in test fixtures - Remove internal CDN publish script (publish-hyperframe-runtime.ts) - Replace sandbox-studio, sandbox-interceptor, __magicEditRuntime with neutral names (studio, hyperframe-runtime, __hyperframeRuntime) - Fix stale Vault API / localhost references in docs - Remove broken deprecated_studio link Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove remaining internal codenames and stale references - Delete stale producer README.md and PIPELINE.md (referenced nonexistent files) - Replace "Cerberus" codename with "HyperFrames" in test design reviews - Replace magic-edit postMessage identifiers with hf-preview/hf-parent - Rename debug-magic-edit-timeline.ts to debug-timeline.ts - Replace "Motion Cut" with "HyperFrames" in Timeline comments - Fix studio/CLI references to nonexistent archive package (use local data/projects/ dir, stub render proxy) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
137 lines
4.4 KiB
TypeScript
137 lines
4.4 KiB
TypeScript
import { describe, it, expect, vi } from "vitest";
|
|
import { createGSAPFrameAdapter } from "./gsap.js";
|
|
import type { FrameAdapter, FrameAdapterContext } from "./types.js";
|
|
|
|
function makeMockTimeline(duration = 10, totalDuration?: number) {
|
|
return {
|
|
duration: vi.fn().mockReturnValue(duration),
|
|
totalDuration: totalDuration !== undefined ? vi.fn().mockReturnValue(totalDuration) : undefined,
|
|
seek: vi.fn(),
|
|
pause: vi.fn(),
|
|
};
|
|
}
|
|
|
|
describe("createGSAPFrameAdapter", () => {
|
|
it("returns a FrameAdapter with required properties", () => {
|
|
const timeline = makeMockTimeline(10);
|
|
const adapter = createGSAPFrameAdapter({ fps: 30, timeline });
|
|
|
|
expect(adapter.id).toBe("gsap");
|
|
expect(typeof adapter.init).toBe("function");
|
|
expect(typeof adapter.getDurationFrames).toBe("function");
|
|
expect(typeof adapter.seekFrame).toBe("function");
|
|
});
|
|
|
|
it("uses custom id when provided", () => {
|
|
const timeline = makeMockTimeline(10);
|
|
const adapter = createGSAPFrameAdapter({ id: "custom-gsap", fps: 30, timeline });
|
|
|
|
expect(adapter.id).toBe("custom-gsap");
|
|
});
|
|
|
|
it("getDurationFrames returns correct frame count", () => {
|
|
const timeline = makeMockTimeline(10); // 10 seconds
|
|
const adapter = createGSAPFrameAdapter({ fps: 30, timeline });
|
|
|
|
// 10 seconds * 30 fps = 300 frames
|
|
expect(adapter.getDurationFrames()).toBe(300);
|
|
});
|
|
|
|
it("getDurationFrames uses totalDuration when available", () => {
|
|
const timeline = makeMockTimeline(5, 15); // duration=5, totalDuration=15
|
|
const adapter = createGSAPFrameAdapter({ fps: 30, timeline });
|
|
|
|
// Should use totalDuration (15) not duration (5)
|
|
// 15 seconds * 30 fps = 450 frames
|
|
expect(adapter.getDurationFrames()).toBe(450);
|
|
});
|
|
|
|
it("getDurationFrames returns 0 for zero-duration timeline", () => {
|
|
const timeline = makeMockTimeline(0);
|
|
const adapter = createGSAPFrameAdapter({ fps: 30, timeline });
|
|
|
|
expect(adapter.getDurationFrames()).toBe(0);
|
|
});
|
|
|
|
it("init pauses the timeline", () => {
|
|
const timeline = makeMockTimeline(10);
|
|
const adapter = createGSAPFrameAdapter({ fps: 30, timeline });
|
|
|
|
adapter.init!({} as FrameAdapterContext);
|
|
|
|
expect(timeline.pause).toHaveBeenCalled();
|
|
});
|
|
|
|
it("seekFrame seeks to the correct time in seconds", () => {
|
|
const timeline = makeMockTimeline(10);
|
|
const adapter = createGSAPFrameAdapter({ fps: 30, timeline });
|
|
|
|
adapter.seekFrame(90); // Frame 90 at 30fps = 3 seconds
|
|
|
|
expect(timeline.seek).toHaveBeenCalledWith(3, false);
|
|
expect(timeline.pause).toHaveBeenCalled();
|
|
});
|
|
|
|
it("seekFrame clamps negative frames to 0", () => {
|
|
const timeline = makeMockTimeline(10);
|
|
const adapter = createGSAPFrameAdapter({ fps: 30, timeline });
|
|
|
|
adapter.seekFrame(-5);
|
|
|
|
expect(timeline.seek).toHaveBeenCalledWith(0, false);
|
|
});
|
|
|
|
it("seekFrame handles non-finite frame numbers", () => {
|
|
const timeline = makeMockTimeline(10);
|
|
const adapter = createGSAPFrameAdapter({ fps: 30, timeline });
|
|
|
|
adapter.seekFrame(NaN);
|
|
|
|
expect(timeline.seek).toHaveBeenCalledWith(0, false);
|
|
});
|
|
|
|
it("seekFrame handles Infinity", () => {
|
|
const timeline = makeMockTimeline(10);
|
|
const adapter = createGSAPFrameAdapter({ fps: 30, timeline });
|
|
|
|
adapter.seekFrame(Infinity);
|
|
|
|
// Infinity is not finite, so it gets clamped to 0
|
|
expect(timeline.seek).toHaveBeenCalledWith(0, false);
|
|
});
|
|
|
|
it("works without pause method on timeline", () => {
|
|
const timeline = {
|
|
duration: vi.fn().mockReturnValue(5),
|
|
seek: vi.fn(),
|
|
// No pause method
|
|
};
|
|
|
|
const adapter = createGSAPFrameAdapter({ fps: 30, timeline });
|
|
|
|
// Should not throw
|
|
adapter.init!({} as FrameAdapterContext);
|
|
adapter.seekFrame(0);
|
|
|
|
expect(timeline.seek).toHaveBeenCalled();
|
|
});
|
|
|
|
it("getDurationFrames ceiling the frame count", () => {
|
|
const timeline = makeMockTimeline(1.05); // 1.05 seconds * 30 fps = 31.5 -> ceil = 32
|
|
const adapter = createGSAPFrameAdapter({ fps: 30, timeline });
|
|
|
|
expect(adapter.getDurationFrames()).toBe(32);
|
|
});
|
|
});
|
|
|
|
describe("FrameAdapter type", () => {
|
|
it("adapter conforms to FrameAdapter interface", () => {
|
|
const timeline = makeMockTimeline(10);
|
|
const adapter: FrameAdapter = createGSAPFrameAdapter({ fps: 30, timeline });
|
|
|
|
expect(adapter.id).toBeDefined();
|
|
expect(adapter.getDurationFrames).toBeDefined();
|
|
expect(adapter.seekFrame).toBeDefined();
|
|
});
|
|
});
|