fix(studio): settle boundary retimes, delete every keyframed tween, tighten the test locks

Review follow-ups on the expanded keyframe lanes.

Writer:
- `onMoveKeyframe`'s flat-tween boundary branch answered `true` the moment it
  dispatched update-meta, so a rejected write left the diamond parked at its drop
  position. `observeGsapMutation` now resolves to whether the mutation landed and
  the boundary branch returns it, matching the other branches.
- "Delete All Keyframes" cleared only the first keyframed tween on the layer, so
  a layer with position AND opacity keyframes kept half of them. It now walks
  every keyframed tween, serially, through the clicked element's selection.
- The post-convert lookup in `commitFlatViaKeyframes` matched by target selector,
  which picks an arbitrary tween when a target carries several. Match by id first.

Interaction and a11y:
- A rejected retime whose commit settled after a newer drag reverted the
  selection to its own source keyframe, undoing a retime the user could see. The
  revert now only runs while it is still the lane's latest gesture.
- Diamonds key on the authored identity instead of index plus rendered clip-%, so
  a neighbour's retime no longer remounts the button mid-drag.
- The disclosure caret gets `aria-controls` on an always-mounted lanes container,
  and both it and the property-group toggle grow to the 24x24 WCAG 2.2 minimum.
- `LayerDisclosureRow` takes the same adaptive `columnWidth` as its sibling lane
  rows instead of hardcoding LABEL_COL_W over the canvas.

Test locks:
- The timeline callbacks harness resolves a DISTINCT selection per element, so
  the clicked-element writes are actually pinned; three assertions that passed
  either way now name the clicked element's selection.
- New: null-selection aborts every mutation, delete-all covers both tweens, a
  rejected boundary retime reports `false`, and a stale revert leaves selection.
- The playhead-percentage assertion checks 25, not `expect.any(Number)` (which
  also accepts NaN); ease segments assert their label ORDER, not just that the
  three curves differ; the collapsed-diamond callback asserts the whole target.
- Dropped a duplicate `selection override` describe left by a rebase.
This commit is contained in:
Miguel Angel Simon Sierra
2026-07-28 01:19:05 +02:00
parent c20c5366da
commit 8470b88aa1
10 changed files with 325 additions and 74 deletions
@@ -210,8 +210,13 @@ async function commitFlatViaKeyframes(
{ label: "Convert to keyframes for drag", skipReload: true, coalesceKey },
);
const fresh = callbacks.fetchAnimations ? await callbacks.fetchAnimations() : [];
// By id first: a target with several tweens (two `to`s on the same selector)
// matches the selector lookup on whichever one happens to be first, and the
// extend-and-add below would then rewrite a tween the drag never touched.
const converted =
fresh.find((a) => a.targetSelector === anim.targetSelector && a.keyframes) ?? anim;
fresh.find((a) => a.id === anim.id && a.keyframes) ??
fresh.find((a) => a.targetSelector === anim.targetSelector && a.keyframes) ??
anim;
const convertedStart = resolveTweenStart(converted) ?? ts;
const convertedDur = resolveTweenDuration(converted) || td;
await extendTweenAndAddKeyframe(