fix(studio): don't crash resizing an element whose keyframes were removed

commitWholePropertyOffset reduced the tween's keyframe list to find the
"nearest" stop without an initial value. When a to()/from() tween had been
collapsed to a zero-duration immediateRender hold (what removeAllKeyframes
leaves behind), synthesizeFlatTweenKeyframes correctly treats it as a
static hold and returns null, leaving an empty keyframe list — so the
reduce threw "Reduce of empty array with no initial value". Reachable by
resizing such an element with auto-keyframe recording off.

With no keyframe shape to preserve, persist the flat value directly via an
update-properties mutation instead.
This commit is contained in:
Miguel Angel Simon Sierra
2026-07-01 21:55:31 -07:00
parent 70606f840d
commit dff3634ea8
2 changed files with 38 additions and 3 deletions
@@ -133,4 +133,30 @@ describe("commitWholePropertyOffset", () => {
});
expect(keyframes[1]).toEqual({ percentage: 100, properties: { x: 120, opacity: 0.5 } });
});
it("persists a flat update instead of crashing when the tween has no synthesizable shape", async () => {
// Regression: removeAllKeyframesFromScript collapses a keyframed tween into a
// zero-duration immediateRender hold — synthesizeFlatTweenKeyframes treats that
// as a static hold (returns null), so `kfs` is empty. Resizing this element with
// auto-keyframe off used to call `kfs.reduce(...)` with no initial value, which
// throws "Reduce of empty array with no initial value".
const anim = {
id: "#box-hold",
targetSelector: "#box",
method: "to",
resolvedStart: 0,
duration: 0,
properties: { width: 200 },
extras: { immediateRender: "__raw:true" },
} as unknown as GsapAnimation;
const { mutations, callbacks } = recordingCallbacks();
await expect(
commitWholePropertyOffset(selection(), anim, { width: 300 }, 0, null, callbacks, "Resize"),
).resolves.toBeUndefined();
expect(mutations).toEqual([
{ type: "update-properties", animationId: "#box-hold", properties: { width: 300 } },
]);
});
});