fix(sdk): code-review follow-ups (WS-B/C/3.C, #1569/#1570/#1572) (#1588)

Addresses the still-outstanding review concerns from merged PRs #1569 / #1570 /
#1572 not already hoisted into #1573.

WS-B (#1569):
- validateVariables requires discriminant fields for object-valued font/image
  ({name,source} / {url}); a {name:42} font or {foo:42} image previously passed
  runtime validation and surfaced as a bogus font-family / missing image.
- Dropped ImageValue's [key:string]:unknown index signature (let any {url}-shaped
  object through, swallowed typos); explicit alt?/fit? instead.
- Documented the OverrideSet widening for SDK consumers.

WS-C (#1570):
- getElementTimings caches parsed GSAP labels by exact script text (avoids a full
  acorn re-parse per read; content-key invalidates on edit).
- Documented end-inclusive label window + best-effort extractGsapLabels catch.

WS-3.C (#1572):
- Added typed Composition.addWithKeyframes / replaceWithKeyframes (was asymmetric
  with addGsapTween; Studio had to use raw dispatch).
- Extracted shared KeyframeSpec type; documented position as seconds/number-only.

Gates: build + core 18/18 + sdk 19/19 + oxlint + oxfmt + fallow + typecheck all green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-06-19 00:07:53 -07:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 418f198b33
commit dd6fad6bb2
6 changed files with 227 additions and 24 deletions
+42 -4
View File
@@ -34,6 +34,13 @@ async function withPatch(html: string): Promise<{ comp: Composition; events: Pat
return { comp, events };
}
function expectGsapScriptPatch(id: string, events: PatchEvent[]): void {
expect(typeof id).toBe("string");
expect(id.length).toBeGreaterThan(0);
expect(events).toHaveLength(1);
expect(events[0]!.patches.find((p) => p.path.includes("/script/gsap"))).toBeDefined();
}
// ─── patch event emission ─────────────────────────────────────────────────────
describe("dispatch emits patch event", () => {
@@ -182,10 +189,7 @@ describe("addGsapTween via session", () => {
const { comp, events } = await withPatch(GSAP_HTML);
const id = comp.addGsapTween("hf-box", { method: "to", duration: 0.3, properties: { x: 200 } });
expect(typeof id).toBe("string");
expect(id.length).toBeGreaterThan(0);
expect(events).toHaveLength(1);
expect(events[0]!.patches.find((p) => p.path.includes("/script/gsap"))).toBeDefined();
expectGsapScriptPatch(id, events);
});
it("undo removes the added tween", async () => {
@@ -197,6 +201,40 @@ describe("addGsapTween via session", () => {
});
});
// ─── addWithKeyframes / replaceWithKeyframes via session API ──────────────────
describe("keyframe ops via session", () => {
it("addWithKeyframes returns an animationId and emits a GSAP script patch", async () => {
const { comp, events } = await withPatch(GSAP_HTML);
const id = comp.addWithKeyframes('[data-hf-id="hf-box"]', 0, 0.5, [
{ percentage: 0, properties: { opacity: 0 } },
{ percentage: 100, properties: { opacity: 1 } },
]);
expectGsapScriptPatch(id, events);
});
it("replaceWithKeyframes returns the replacement id; undo restores the prior script", async () => {
const comp = await openComposition(GSAP_HTML);
const before = comp.serialize();
const addId = comp.addWithKeyframes('[data-hf-id="hf-box"]', 0, 0.5, [
{ percentage: 0, properties: { opacity: 0 } },
{ percentage: 100, properties: { opacity: 1 } },
]);
const newId = comp.replaceWithKeyframes(addId, '[data-hf-id="hf-box"]', 0, 0.8, [
{ percentage: 0, properties: { x: 0 } },
{ percentage: 100, properties: { x: 100 } },
]);
expect(typeof newId).toBe("string");
expect(newId.length).toBeGreaterThan(0);
comp.undo(); // undo replace
comp.undo(); // undo add
expect(comp.serialize()).toBe(before);
});
});
// ─── dispatch with explicit origin ───────────────────────────────────────────
describe("dispatch origin", () => {