mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
feat(sdk,studio): populate animationIds; shadow GSAP update/delete + value fidelity (#1474)
* feat(sdk,studio): populate animationIds; shadow GSAP update/delete Closes the GSAP shadow gaps. The server's animationId was assumed to live in a separate id-space — it does not: the studio-api read path (T6e) and the SDK both derive tween ids as targetSelector-method-position from the same acorn parser, so server ids are dispatchable in the SDK as-is. SDK: populate ElementSnapshot.animationIds (was a hardcoded stub) from parseGsapScriptAcornForWrite().located, resolving each tween's targetSelector to element hf-ids. Makes the snapshot truthful and enables real GSAP parity. Studio: shadow deleteGsapAnimation (removeGsapTween) and updateGsapMeta (setGsapTween) using the server animationId directly. GSAP add/remove parity now verifies via animationIds (present after add, gone after remove). set is existence-only — the SDK still has no per-tween property reader (value fidelity would need serialize()-script round-trip diffing). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(studio): GSAP value fidelity via serialize round-trip diff Closes the last shadow gap: GSAP value fidelity. Existence parity confirmed a tween was created/removed but not that its values (duration/ease/position/ properties) matched the server, since the SDK has no per-tween property reader. runShadowGsapFidelity opens a fresh SDK doc from the server's pre-op file (result.before), applies the same typed op, serializes, and structurally diffs the SDK's GSAP script against the server's resulting script (result.scriptText). Both are re-parsed via parseGsapScriptAcorn, so formatting/whitespace never produces false positives — only real value drift does. gsapFidelityMismatches reports per-field drift and tween presence/absence. Wired at the commitMutation chokepoint (the only place with the server's before+after scripts); handlers pass the typed ShadowGsapOp via CommitMutationOptions.shadowGsapOp. Emits sdk_shadow_dispatch op:gsap_fidelity. Complements the existing live existence shadow (op:gsap). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(studio,sdk): address shadow code-review findings - gsapFidelityMismatches: canonical comparison (sort property keys, numeric- coerce position/duration/values). Server (addAnimationToScript) and SDK (gsapWriterAcorn) are different writers; non-canonical compare flagged key-order / number-vs-string differences as false value drift. - document.ts buildAnimationIdMap: memoize the acorn parse by script text (single-entry). getElements() invalidates on every dispatch, so shadow's frequent dispatches were re-parsing the full GSAP AST each rebuild. Selector resolution still runs per-call (depends on live DOM). - runShadowGsapFidelity: early-bail when serverScript/beforeHtml is empty — skip the costly openComposition. - useSafeGsapCommitMutation: import the shared CommitMutationOptions/ CommitMutation instead of a stale local duplicate (was missing shadowGsapOp). - align extractGsapScript marker set across sdkShadow.ts and document.ts (gsap || __timelines || ScrollTrigger) so both pick the same script. Tests: +2 canonical-compare cases (key-order, number-vs-string → no drift). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(studio,sdk): fallow gate for #1474 (fidelity diff + test clones) - suppress moderate CRAP on gsapFidelityMismatches and the runShadowGsapTween parity arrow (comparison/parity functions are inherently branchy) - suppress two pre-existing test clones in session.test.ts surfaced by the added animationIds tests (TestPreviewAdapter stub, selectionchange setup) Rebased onto the updated #1473 (no-persist shadow session); inherits the persist-race fix and prior fallow suppressions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(studio): extract GSAP fidelity to its own module (file-size gate) sdkShadow.ts hit 602 lines (CI File size check: max 600). Move the GSAP value-fidelity diff (gsapFidelityMismatches, runShadowGsapFidelity, and their private helpers) into sdkShadowGsapFidelity.ts; re-export from sdkShadow.ts so the import surface is unchanged. sdkShadow.ts now 430 lines. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(studio,sdk): address #1474 review feedback - CodeQL js/bad-tag-filter: the GSAP <script> extraction + test regexes now match </script\s*> (whitespace-before-close variant). 3 alerts resolved. - Wiring (Miguel): extract resolveGsapFidelityArgs — a pure, narrowing gate for the commitMutation chokepoint (no non-null assertions) — and unit-test the fire/skip conditions (session, op, before, scriptText). Replaces the inline guard so the wiring decision is covered without rendering the hook. - Property-handler scope (Rames): comment at the chokepoint documenting that only meta-level ops (add/update-meta/delete) carry shadowGsapOp today; per-property and keyframe handlers are a deliberate follow-up. Also why scriptText can be null. - Test coverage (Rames): multi-tween-per-element and shared-selector cross-element animationIds cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(studio): CodeQL js/bad-tag-filter — match </script[^>]*> close tags `</script\s*>` still tripped CodeQL on attribute-junk closes like `</script foo>` (HTML5 ignores junk before `>`). Widen the close-tag match to `</script[^>]*>` in the GSAP-script extraction and the test regexes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
5f6ced116d
commit
7593aac5ef
@@ -17,6 +17,7 @@ const BASE_HTML = `
|
||||
class TestPreviewAdapter implements PreviewAdapter {
|
||||
private selectionHandlers: Array<(ids: string[]) => void> = [];
|
||||
|
||||
// fallow-ignore-next-line code-duplication
|
||||
elementAtPoint(_x: number, _y: number, _opts?: { atTime?: number }): ElementAtPointResult | null {
|
||||
return null;
|
||||
}
|
||||
@@ -386,6 +387,7 @@ describe("setSelection", () => {
|
||||
expect(patches).toHaveLength(0);
|
||||
});
|
||||
|
||||
// fallow-ignore-next-line code-duplication
|
||||
it("setSelection with same ids does not fire selectionchange again", async () => {
|
||||
const comp = await openComposition(BASE_HTML);
|
||||
const calls: string[][] = [];
|
||||
@@ -419,3 +421,65 @@ describe("setSelection", () => {
|
||||
expect(calls).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("animationIds population", () => {
|
||||
const GSAP_HTML = `
|
||||
<div data-hf-id="hf-stage" data-hf-root style="width: 1280px; height: 720px">
|
||||
<div data-hf-id="hf-box" style="opacity: 0">box</div>
|
||||
<div data-hf-id="hf-plain">plain</div>
|
||||
<script>var tl = gsap.timeline({ paused: true });
|
||||
tl.to("[data-hf-id=\\"hf-box\\"]", { opacity: 1, duration: 0.5 }, 0.2);
|
||||
window.__timelines["t"] = tl;</script>
|
||||
</div>`.trim();
|
||||
|
||||
it("attaches the parser's stable tween id to the targeted element", async () => {
|
||||
const comp = await openComposition(GSAP_HTML);
|
||||
const box = comp.getElement("hf-box");
|
||||
expect(box?.animationIds.length).toBe(1);
|
||||
// Stable id-space shared with studio-api / GSAP ops: targetSelector-method-position.
|
||||
expect(box?.animationIds[0]).toContain("hf-box");
|
||||
expect(box?.animationIds[0]).toContain("-to-");
|
||||
});
|
||||
|
||||
it("leaves untargeted elements with an empty animationIds", async () => {
|
||||
const comp = await openComposition(GSAP_HTML);
|
||||
expect(comp.getElement("hf-plain")?.animationIds).toEqual([]);
|
||||
});
|
||||
|
||||
it("the populated id is dispatchable as a removeGsapTween target", async () => {
|
||||
const comp = await openComposition(GSAP_HTML);
|
||||
const id = comp.getElement("hf-box")?.animationIds[0];
|
||||
expect(id).toBeDefined();
|
||||
if (id) expect(comp.can({ type: "removeGsapTween", animationId: id }).ok).toBe(true);
|
||||
});
|
||||
|
||||
it("attaches multiple distinct tween ids when one element has several tweens", async () => {
|
||||
const html = `
|
||||
<div data-hf-id="hf-stage" data-hf-root style="width: 1280px; height: 720px">
|
||||
<div data-hf-id="hf-box" style="opacity: 0">box</div>
|
||||
<script>var tl = gsap.timeline({ paused: true });
|
||||
tl.to("[data-hf-id=\\"hf-box\\"]", { opacity: 1, duration: 0.5 }, 0);
|
||||
tl.from("[data-hf-id=\\"hf-box\\"]", { x: -100, duration: 0.5 }, 1);
|
||||
window.__timelines["t"] = tl;</script>
|
||||
</div>`.trim();
|
||||
const ids = (await openComposition(html)).getElement("hf-box")?.animationIds ?? [];
|
||||
expect(ids.length).toBe(2);
|
||||
expect(new Set(ids).size).toBe(2); // distinct
|
||||
});
|
||||
|
||||
it("fans a shared-selector tween out to every matched element", async () => {
|
||||
const html = `
|
||||
<div data-hf-id="hf-stage" data-hf-root style="width: 1280px; height: 720px">
|
||||
<div data-hf-id="hf-a" class="fade">a</div>
|
||||
<div data-hf-id="hf-b" class="fade">b</div>
|
||||
<script>var tl = gsap.timeline({ paused: true });
|
||||
tl.to(".fade", { opacity: 1, duration: 0.5 }, 0);
|
||||
window.__timelines["t"] = tl;</script>
|
||||
</div>`.trim();
|
||||
const comp = await openComposition(html);
|
||||
const a = comp.getElement("hf-a")?.animationIds ?? [];
|
||||
const b = comp.getElement("hf-b")?.animationIds ?? [];
|
||||
expect(a.length).toBe(1);
|
||||
expect(b).toEqual(a); // same tween id on both matched elements
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user