Files
hyperframes/packages/studio-server/src/helpers/studioMotionRenderScript.test.ts
Miguel Ángel 7a4853dfe6 refactor: extract @hyperframes/studio-server from core (#1757)
* 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.
2026-06-27 01:24:01 -04:00

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");
});
});