import { describe, expect, it } from "vitest"; import { TOOLCRAFT_COMPONENT_CONTRACTS } from "./component-contracts"; describe("Toolcraft template component contracts: runtime and composition", () => { it("defines the runtime-owned controls-panel section navigation", () => { expect( TOOLCRAFT_COMPONENT_CONTRACTS.controlsPanel.capabilities, ).toContain("section-navigation"); expect( TOOLCRAFT_COMPONENT_CONTRACTS.controlsPanel.aiUsageRules, ).toContain( "An overflowing controls panel reveals runtime-owned section navigation only after a 300ms pointer dwell within its 12px inner-left padding strip, ending before control content begins. Fast crossings cancel the reveal; while visible, the popup and direct 8px corridor retain it by pointer geometry, remaining outside for 50ms starts a fast 120ms reduced-motion-aware exit before unmount, and reopening requires another dwell. It reuses PanelSurface and the shared ScrollFade, keeps its scrollbar at the right surface edge, shows position-aware top and bottom fades, uses a 12px list inset with zero horizontal item padding, sits 8px to the left centered on the content viewport, enters and exits with opacity and a 2px horizontal offset, renders 13px labels with 8px item gaps at foreground 50%, uses foreground 80% on hover and full foreground for the current item, renders no vertical item markers, jumps immediately to clicked sections, lets focused Arrow Up and Arrow Down wrap, focus, and immediately activate visible sections, includes Setup, and excludes sticky action sections.", ); }); it("preserves runtime panel commands for optional layers and timeline panels", () => { expect(TOOLCRAFT_COMPONENT_CONTRACTS.layersPanel.commands).toContain("layers.reorder"); expect(TOOLCRAFT_COMPONENT_CONTRACTS.layersPanel.capabilities).toContain("selection"); expect(TOOLCRAFT_COMPONENT_CONTRACTS.layersPanel.aiUsageRules).toContain( "Do not enable the layers panel for single-layer apps.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.layersPanel.aiUsageRules).toContain( "When layers are enabled, layer-specific controls should target selectedLayer.* and apply to the currently selected runtime layer.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.layersPanel.aiUsageRules).toContain( "Do not use selectedLayer.* targets when panels.layers is disabled; single-layer apps use app-specific targets.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.layersPanel.aiUsageRules).toContain( "Layer-enabled apps need layerCoverage acceptance for selection, visibility, reorder, and grouping.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.layersPanel.aiUsageRules).toContain( "Every selectedLayer.* control needs selected-layer-controls acceptance proving it edits the currently selected layer output.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.layersPanel.aiUsageRules).toContain( "Layer browser coverage must use real LayersPanel rows and buttons, not direct layers.* command dispatch.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.layersPanel.aiUsageRules).toContain( "Layer-enabled custom renderers need layers.interactions coverage around real selection, visibility, reorder or grouping, and selected-layer output checks.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.commands).toContain( "timeline.setCurrentTime", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.commands).toContain( "timeline.togglePlayback", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.commands).toContain( "panels.setHidden", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.capabilities).toContain("keyframes"); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( "Any product output animation must enable the top Toolcraft timeline; do not ship animated product output with only local requestAnimationFrame playback.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( "Before choosing no timeline for any animated product, write an Animation Intent Inventory: product transport, editable keyframes, or autonomous decorative output, plus the user-facing time behaviors present or intentionally absent.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( 'User-requested product animation defaults to panels.timeline mode "playback" unless the spec explicitly declares autonomous decorative/self-running output with no play, pause, scrub, duration, loop, export-at-time behavior, or video export.', ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( 'Any product app with Export Video must enable the top Toolcraft timeline: use panels.timeline mode "playback" for product animation transport, or mode "keyframes" when exported animation is driven by keyframes.', ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( 'Export Video is valid only when productReadiness.exportIntent.video is "user-requested" with non-empty explicit user-request evidence; animation, playback, keyframes, and timeline presence never authorize video export.', ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( "Video export frames are rendered deterministically from the runtime-owned 30 FPS timeline schedule; product code must not use autonomous wall-clock time, captureStream, or MediaRecorder as the source or fallback for product duration.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( "Playback renderers must consume runtime timeline state; pause freezes output, scrubbing renders a deterministic frame, and the full animation cycle maps to state.timeline.durationSeconds instead of a local fixed duration.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( 'When panels.timeline is enabled for a new Toolcraft app, appTransferMode.animationIntent must match it: mode "timeline-playback" for playback, or mode "timeline-keyframes" for keyframes.', ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( "When the product has a known loop duration, declare it as panels.timeline.defaultDurationSeconds; the runtime timeline duration starts from that loop duration instead of an unrelated 8s default.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( 'Timeline animation intent must declare loopDuration with source "reference", "user-request", or "product-derived", plus seconds and evidence; runtime/template fallback 8s is not a valid source.', ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( "panels.timeline.defaultDurationSeconds must match animationIntent.loopDuration.seconds for playback/keyframe animation, so the initial timeline UI shows the declared product loop instead of a generic default.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( "Playback renderers may compute an initial loop duration default during app initialization or reset, but must not watch state.timeline.durationSeconds and dispatch timeline.setDuration back to a computed local duration. User-edited timeline duration becomes the loop duration source of truth after initialization or reset.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( "Playback renderers should use getToolcraftTimelineLoopTime or getToolcraftTimelineLoopProgress to derive product loop phase from state.timeline.currentTimeSeconds and state.timeline.durationSeconds; do not hand-roll wall-clock, fixed-duration, mirror, yoyo, ping-pong, or reverse phase math.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( "Product animation loop means a seamless forward-only cycle by default: motion advances in one direction, first and last frames stitch without a visible jump, and mirror/yoyo/ping-pong/reverse loops require explicit user request.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( "Changing timeline duration must preserve seamless forward-loop semantics: one full product animation cycle maps from 0 to state.timeline.durationSeconds, first and last frames still stitch, direction does not reverse, and changing duration must not switch the renderer to wall-clock or fixed local speed.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( "When non-looping playback reaches the end, pressing Play again restarts playback from time 0.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( "Intrinsic-media upload timelines must stay paused at time 0 until source media exists; clearing the last media asset must pause and reset playback.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( "Do not put Pause or Resume in panelActions; playback belongs to TimelinePanel transport controls.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( "In keyframes mode, Toolcraft infers keyframe diamonds from control type; AI must not manually pick a smaller subset of slider/vector/color-style controls.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( "Keyframe state stores typed control values; valueLabel is display-only and must never be parsed by renderers or tests as the source of truth.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( "Custom renderers with keyframes must consume evaluateToolcraftTimelineValues or useToolcraftEvaluatedValues for keyframed settings instead of raw state.values for those targets.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( "Every inferred keyframe-capable control must be evaluated from runtime timeline keyframes and needs acceptance proving diamond creation, row creation, keyframe updates, scrub/playback evaluation, and product output change.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( "Keyframe custom renderers must prove zoom, radar, and canvas viewport stability while expanding the timeline, creating keyframes, and scrubbing or playing the timeline.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( "Keyframe renderers must not re-decode media or re-upload source textures on timeline ticks, scrubs, playback, or evaluated setting changes.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( "Timeline-driven preview renderers must suspend or coalesce non-essential animation work during canvas drag, pan, pinch, zoom, and radar/center interactions without mutating the user's timeline play/pause state.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( "Use keyframeable: false only on controls that are structurally unsupported by the shared keyframe capability helper; capable controls cannot opt out to hide broken animation wiring.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( "Right-panel animation controls may tune renderer parameters such as mode, intensity, speed, or stagger only after animation intent is declared; they must not replace top timeline transport.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( "Do not replace TimelinePanel with an app-level playback, transport, or timeline panel to avoid runtime performance issues; fix the Toolcraft runtime clock/state path instead.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( 'Custom timeline UI is allowed only for explicit referenceTimeline.mode "custom-reference-timeline" transfers with browser-backed referenceTimelineCoverage.', ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( "Runtime Setup places the standard Background switch beside Infinity canvas immediately after settings transfer, then Background color and finite sizing; Timeline is the final Setup control when enabled.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( "Timeline and Infinity canvas are self-explanatory runtime mode switches and do not render help icons.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( "The Timeline Setup switch controls only compact versus extended runtime presentation: off shows the compact Play-only transport, on shows the extended timeline with scrubber, duration, loop, and keyframe UI. It must not stop playback, change keyframes, affect export, enter product values, or be reset by Reset controls/settings transfer.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain( "If timeline verification fails, wire the renderer to runtime timeline state or remove panels.timeline.", ); }); });