mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-07 15:27:14 +00:00
* refactor: extract @hyperframes/studio-server package from core Moves all studio-api routes, helpers, and Hono server wiring from packages/core/src/studio-api/ into a new standalone packages/studio-server package (@hyperframes/studio-server). Core keeps thin re-export stubs at @hyperframes/core/studio-api and the subpath helpers (screenshot-clip, draft-markers, etc.) for backward compatibility. Consumer imports (cli studioServer, vite adapter/config, producer htmlCompiler, studio manualEditsTypes) are updated to import from @hyperframes/studio-server directly. Also exports rewriteInlineStyleAssetUrls from @hyperframes/core root (was in compiler/rewriteSubCompPaths.ts but not re-exported), required by @hyperframes/studio-server/helpers/subComposition. Removes postcss-selector-parser from @hyperframes/core dependencies (moved to @hyperframes/studio-server which owns the routes that used it). Depends on @hyperframes/parsers (PR #1755). * fix(ci): add parsers+studio-server to Dockerfile and build before preview tests * fix(ci): build @hyperframes/studio-server before Test and studio load smoke Studio's vite.config.ts imports @hyperframes/studio-server, which resolves via its "node" export condition to built dist. The Test and studio-load-smoke jobs only built parsers + core, so esbuild's config load failed to resolve the package entry. Build studio-server too. * fix(studio): repoint sdkCutoverParity test import to studio-server sourceMutation moved from core's studio-api to @hyperframes/studio-server; the test still imported the deleted core path. This was masked while studio's vite.config failed to load (couldn't resolve studio-server); now that the config loads, the test runs and the stale import surfaced.
206 lines
5.7 KiB
TypeScript
206 lines
5.7 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { Window } from "happy-dom";
|
|
import { createStudioMotionRenderBodyScript } from "./studioMotionRenderScript";
|
|
|
|
function runScript(window: Window, script: string): void {
|
|
const execute = new Function("window", "document", "HTMLElement", script);
|
|
execute(window, window.document, window.HTMLElement);
|
|
}
|
|
|
|
function installFakeGsap(window: Window): {
|
|
calls: Array<{
|
|
target: HTMLElement;
|
|
from: Record<string, unknown>;
|
|
to: Record<string, unknown>;
|
|
at: number;
|
|
}>;
|
|
timeCalls: number[];
|
|
customEaseCalls: Array<{ id: string; data: string }>;
|
|
killCalls: number;
|
|
} {
|
|
const state = {
|
|
calls: [] as Array<{
|
|
target: HTMLElement;
|
|
from: Record<string, unknown>;
|
|
to: Record<string, unknown>;
|
|
at: number;
|
|
}>,
|
|
timeCalls: [] as number[],
|
|
customEaseCalls: [] as Array<{ id: string; data: string }>,
|
|
killCalls: 0,
|
|
};
|
|
const timeline = {
|
|
fromTo(
|
|
target: HTMLElement,
|
|
from: Record<string, unknown>,
|
|
to: Record<string, unknown>,
|
|
at: number,
|
|
) {
|
|
state.calls.push({ target, from, to, at });
|
|
return timeline;
|
|
},
|
|
time(value: number) {
|
|
state.timeCalls.push(value);
|
|
return timeline;
|
|
},
|
|
pause() {
|
|
return timeline;
|
|
},
|
|
kill() {
|
|
state.killCalls += 1;
|
|
},
|
|
duration() {
|
|
return 2;
|
|
},
|
|
};
|
|
(
|
|
window as unknown as {
|
|
gsap: {
|
|
timeline: () => typeof timeline;
|
|
set: (target: HTMLElement, vars: Record<string, unknown>) => void;
|
|
};
|
|
CustomEase: { create: (id: string, data: string) => void };
|
|
__player?: { getTime: () => number };
|
|
}
|
|
).gsap = {
|
|
timeline: () => timeline,
|
|
set(target, vars) {
|
|
if (vars.clearProps === "transform,opacity,visibility") {
|
|
target.style.removeProperty("transform");
|
|
target.style.removeProperty("opacity");
|
|
target.style.removeProperty("visibility");
|
|
}
|
|
},
|
|
};
|
|
(
|
|
window as unknown as {
|
|
CustomEase: { create: (id: string, data: string) => void };
|
|
}
|
|
).CustomEase = {
|
|
create(id, data) {
|
|
state.customEaseCalls.push({ id, data });
|
|
},
|
|
};
|
|
return state;
|
|
}
|
|
|
|
describe("createStudioMotionRenderBodyScript", () => {
|
|
it("returns null for an empty manifest", () => {
|
|
expect(createStudioMotionRenderBodyScript("")).toBeNull();
|
|
});
|
|
|
|
it("returns null for a valid manifest without motions", () => {
|
|
expect(createStudioMotionRenderBodyScript(`{"version":1,"motions":[]}`)).toBeNull();
|
|
});
|
|
|
|
it("registers Studio-authored GSAP motion into window.__timelines", () => {
|
|
const window = new Window();
|
|
window.document.body.innerHTML = '<div id="card" style="opacity: 0.6"></div>';
|
|
const card = window.document.getElementById("card");
|
|
if (!(card instanceof window.HTMLElement)) throw new Error("card fixture missing");
|
|
const gsapState = installFakeGsap(window);
|
|
(
|
|
window as unknown as {
|
|
__player: { getTime: () => number };
|
|
}
|
|
).__player = { getTime: () => 0.5 };
|
|
|
|
const script = createStudioMotionRenderBodyScript(
|
|
JSON.stringify({
|
|
version: 1,
|
|
motions: [
|
|
{
|
|
kind: "gsap-motion",
|
|
target: { sourceFile: "index.html", id: "card" },
|
|
start: 0.2,
|
|
duration: 0.7,
|
|
ease: "power2.out",
|
|
from: { y: 32, autoAlpha: 0 },
|
|
to: { y: 0, autoAlpha: 1 },
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
if (!script) throw new Error("script fixture missing");
|
|
|
|
runScript(window, script);
|
|
|
|
expect(gsapState.calls[0]).toMatchObject({
|
|
target: card,
|
|
from: { y: 32, autoAlpha: 0 },
|
|
to: { y: 0, autoAlpha: 1, duration: 0.7, ease: "power2.out" },
|
|
at: 0.2,
|
|
});
|
|
expect(gsapState.timeCalls).toEqual([0.5]);
|
|
expect(
|
|
(window as unknown as { __timelines?: Record<string, unknown> }).__timelines?.[
|
|
"studio-motion"
|
|
],
|
|
).toBeTruthy();
|
|
});
|
|
|
|
it("does not mutate when GSAP is unavailable", () => {
|
|
const window = new Window();
|
|
window.document.body.innerHTML = '<div id="card" style="opacity: 0.6"></div>';
|
|
const script = createStudioMotionRenderBodyScript(
|
|
JSON.stringify({
|
|
version: 1,
|
|
motions: [
|
|
{
|
|
kind: "gsap-motion",
|
|
target: { sourceFile: "index.html", id: "card" },
|
|
start: 0,
|
|
duration: 1,
|
|
ease: "none",
|
|
from: { x: 0 },
|
|
to: { x: 10 },
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
if (!script) throw new Error("script fixture missing");
|
|
|
|
runScript(window, script);
|
|
|
|
expect(
|
|
(window as unknown as { __timelines?: Record<string, unknown> }).__timelines?.[
|
|
"studio-motion"
|
|
],
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("registers CustomEase data before adding Studio motion tweens", () => {
|
|
const window = new Window();
|
|
window.document.body.innerHTML = '<div id="card"></div>';
|
|
const gsapState = installFakeGsap(window);
|
|
const script = createStudioMotionRenderBodyScript(
|
|
JSON.stringify({
|
|
version: 1,
|
|
motions: [
|
|
{
|
|
kind: "gsap-motion",
|
|
target: { sourceFile: "index.html", id: "card" },
|
|
start: 0,
|
|
duration: 1,
|
|
ease: "studio-card-bounce",
|
|
customEase: {
|
|
id: "studio-card-bounce",
|
|
data: "M0,0 C0.18,0.9 0.32,1 1,1",
|
|
},
|
|
from: { y: 32 },
|
|
to: { y: 0 },
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
if (!script) throw new Error("script fixture missing");
|
|
|
|
runScript(window, script);
|
|
|
|
expect(gsapState.customEaseCalls).toEqual([
|
|
{ id: "studio-card-bounce", data: "M0,0 C0.18,0.9 0.32,1 1,1" },
|
|
]);
|
|
expect(gsapState.calls[0]?.to.ease).toBe("studio-card-bounce");
|
|
});
|
|
});
|