tests: fill macro-clips coverage gaps (+35 tests)

Refactor + new tests triggered by an explicit coverage audit of the
macro-clips feature set.

- extracted blendButtonStates into src/lib/buttonBlend.ts so it can be
  tested without mounting CanvasStage; the component now delegates to
  it. Behaviour unchanged.
- store coverage: removeMarker / setMarkerTime / setMarkerColor on
  unknown ids are no-ops, addMarker accepts an initial color,
  removeScriptFrame / setScriptFrameActions / setScriptFrameTime on
  unknown ids are no-ops, removeButtonInstance keeps the clip,
  addButtonInstance defaults x/y to stage center,
  setButtonInstancePosition writes through, setButtonInstanceLabel('')
  clears, setButtonClipTransitions stores + deletes.
- button runtime: applyButtonStateChange is a no-op when the target
  equals the current state, honors the persisted disabled flag as the
  from-state, reset() clears transitions alongside state/disabled.
  store.actionRuntime warns on unknown button refs.
- blendButtonStates: matched layer lerps props (incl. fill color),
  unmatched from-layer fades out, unmatched to-layer fades in,
  u=0/u=1 boundary conditions, missing asset id resolves are skipped.
- DSL edge cases: empty source, comments-only, trailing blank lines,
  wrong state ref prefix, disable without arg, jump @ without name.
- tick firing robustness: multi-track frames fire in time order,
  actions within a frame fire in list order, scriptFramesInRange
  open-closed boundary is exact.

Macro-clips suites now total 76 tests across 11 groups; grand total
277 tests in the runner.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.7 committed 2026-04-20 22:00:58 +02:00
1 parent 6471c446df
commit 22b553e931
3 files changed
+394 -40

No files matched your search

+55
View File
@@ -0,0 +1,55 @@
import type { AnimatableProps, Asset, ButtonClip, ButtonState, Layer } from '@/types/scene'
import { lerpProps, sampleLayer } from '@/lib/interpolate'
export interface BlendedLayerRender {
layer: Layer
asset: Asset
props: AnimatableProps
}
/**
* Blend the "from" state's layers into the "to" state's layers by layer
* name. Matched pairs lerp their sampled (t=0) props; unmatched
* from-layers fade out (opacity × (1 − u)), unmatched to-layers fade in
* (opacity × u). Used by the canvas renderer and the export runtime
* while a button state transition is in progress.
*
* `u` is expected to be post-easing progress ∈ [0, 1].
*/
export function blendButtonStates(
clip: ButtonClip,
from: ButtonState,
to: ButtonState,
u: number,
assetBy: (id: string) => Asset | undefined,
): BlendedLayerRender[] {
const fromLayers = clip.states[from] ?? []
const toLayers = clip.states[to] ?? []
const fromByName = new Map<string, Layer>()
const toByName = new Map<string, Layer>()
for (const l of fromLayers) fromByName.set(l.name, l)
for (const l of toLayers) toByName.set(l.name, l)
const out: BlendedLayerRender[] = []
// Unmatched from-layers render first (behind to-layers) and fade out.
for (const l of fromLayers) {
if (toByName.has(l.name)) continue
const asset = assetBy(l.assetId)
if (!asset) continue
const base = sampleLayer(l, 0)
out.push({ layer: l, asset, props: { ...base, opacity: base.opacity * (1 - u) } })
}
for (const l of toLayers) {
const asset = assetBy(l.assetId)
if (!asset) continue
const fromLayer = fromByName.get(l.name)
if (fromLayer) {
const a = sampleLayer(fromLayer, 0)
const b = sampleLayer(l, 0)
out.push({ layer: l, asset, props: lerpProps(a, b, u) })
} else {
const base = sampleLayer(l, 0)
out.push({ layer: l, asset, props: { ...base, opacity: base.opacity * u } })
}
}
return out
}