import { describe, expect, it } from "vitest"; import { TOOLCRAFT_COMPONENT_CONTRACTS, getToolcraftComponentContract, } from "./component-contracts"; import { TOOLCRAFT_INFINITY_CANVAS_CANONICAL_BEHAVIOR, TOOLCRAFT_INFINITY_CANVAS_DECISION_RULE_ID, getToolcraftDecisionRule, } from "./decision-contracts"; import { TOOLCRAFT_PERFORMANCE_VERIFICATION_POLICY } from "./performance-verification-policy"; describe("Toolcraft template component contracts: runtime and composition", () => { it("owns Space-pan and scopes pinch zoom to the canvas instead of the app UI", () => { const rules = getToolcraftComponentContract("canvas").aiUsageRules; expect(rules).toContain( "CanvasShell owns Space + primary drag panning when canvas.draggable is enabled, with grab while Space is held and grabbing during the drag. Space-pan takes priority over scene handles and model orbit; plain mouse drag does not pan. Text editors and keyboard-focused controls retain Space input/activation. Pointer-focused panel controls yield Space to the hand tool when the pointer returns to the canvas.", ); expect(rules).toContain( "Trackpad pinch, Ctrl/Meta-wheel, and native gesture events zoom only the canvas under the pointer. ToolcraftRoot suppresses browser zoom across the app, including panels and portaled popups; ordinary panel scrolling remains available. Product code must not recreate these input handlers.", ); }); it("keeps toolbar and controls panels behind PanelHost with their snap defaults", () => { expect(TOOLCRAFT_COMPONENT_CONTRACTS.toolbar.requiredWrapper).toBe( "PanelHost", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.toolbar.defaultPlacement).toBe( "bottom", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.toolbar.snapEdges).toEqual([ "top", "bottom", ]); expect(TOOLCRAFT_COMPONENT_CONTRACTS.toolbar.capabilities).toContain( "keyboardShortcuts", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.toolbar.aiUsageRules).toContain( "Toolbar history owns Undo and Redo buttons plus runtime keyboard shortcuts.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.toolbar.aiUsageRules).toContain( "Do not add app-level Cmd/Ctrl+Z, Cmd/Ctrl+Shift+Z, or Ctrl+Y listeners; use toolbar history and runtime commands.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.toolbar.aiUsageRules).toContain( "Undo/redo keyboard shortcuts stay native while the user edits text-entry inputs, textareas, selects, or contentEditable value labels; focused range, checkbox, and other non-text inputs use Toolcraft history.", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.controlsPanel.requiredWrapper).toBe( "PanelHost", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.controlsPanel.defaultPlacement).toBe( "right", ); expect(TOOLCRAFT_COMPONENT_CONTRACTS.controlsPanel.snapEdges).toEqual([ "left", "right", ]); }); it("preserves canvas runtime commands and patch history", () => { const contract = getToolcraftComponentContract("canvas"); expect(contract.kind).toBe("canvas"); expect(contract.stateMode).toBe("runtime-owned"); expect(contract.historyPolicy).toBe("patch"); expect(contract.visualComponent).toBe("CanvasShell"); expect(contract.commands).toContain("canvas.setSize"); expect(contract.commands).toContain("canvas.setMode"); expect(contract.commands).toContain("canvas.panBy"); expect(contract.commands).toContain("canvas.setOffset"); expect(contract.commands).toContain("media.importBatch"); expect(contract.commands).toContain("media.transform"); expect(contract.capabilities).toContain("editable-size"); expect(contract.capabilities).toContain("infinity-canvas"); const infinityRules = contract.aiUsageRules .filter((rule) => /Infinity|infinite|finite\/infinite/u.test(rule)) .join("\n"); const canonicalInfinityRule = getToolcraftDecisionRule( TOOLCRAFT_INFINITY_CANVAS_DECISION_RULE_ID, ); expect(canonicalInfinityRule?.desiredBehavior).toBe( TOOLCRAFT_INFINITY_CANVAS_CANONICAL_BEHAVIOR, ); expect(infinityRules).toContain(TOOLCRAFT_INFINITY_CANVAS_DECISION_RULE_ID); expect(infinityRules).toMatch(/runtime Infinity canvas switch before finite sizing controls/u); expect(infinityRules).toMatch(/CanvasShell fills the complete infinite viewport/u); expect(infinityRules).toMatch(/disabling Background atomically restores finite mode/u); expect(infinityRules).toMatch(/useToolcraftProductSceneFrame/u); expect(infinityRules).toMatch(/backing and renderer instance unchanged/u); expect(contract.aiUsageRules).toContain( "Do not let canvas.upload choose intrinsic-media by default. Upload without explicit sizing resolves to editable-output so source/background images do not own product output size.", ); expect(contract.aiUsageRules).toContain( "Use intrinsic-media only for true media-viewer or source-native apps where the natural uploaded/generated media size is the product output; record the reason and prove it with intrinsic-media-size acceptance.", ); expect(contract.aiUsageRules).toContain( "Use editable-output for generated, exportable, shader, poster, badge, wall, banner, thumbnail, procedural, reference-clone, and product-output apps so users always see Aspect ratio, Canvas width, and Canvas height.", ); expect(contract.aiUsageRules).toContain( "When an uploaded image is a background/source inside the product canvas, keep the current canvas.size, keep Setup/canvas controls visible, and render the image as cover/crop inside the current canvas bounds without letterbox or aspect distortion.", ); expect(contract.aiUsageRules).toContain( "A user-provided, reference, fixed-format, or base/default size is not a reason to remove size controls; model it as canvas.size plus editable-output so the size is an initial value, not a hidden lock.", ); expect(contract.aiUsageRules).toContain( "Do not use fixed-output for generated product/output apps with export actions. Reserve fixed-output for non-product internal fixtures where width and height truly must never be user-editable, and prove that lock with canvasSizingCoverage fixed-output-size acceptance.", ); expect(contract.aiUsageRules).toContain( "A reference or previous app lacking a size editor, or defining a fixed-size baseline, is not a fixed-output reason for a generated product app; product-output clones still use editable-output.", ); expect(contract.aiUsageRules).toContain( "Resolved canvas.size exists for every canvas app, but visible Canvas width and Canvas height controls are mandatory only for editable-output sizing and live in the mandatory runtime Setup section.", ); expect(contract.aiUsageRules).toContain( "If canvas.size is provided without an explicit sizing mode, defineToolcraft treats it as editable-output and adds Canvas width and Canvas height controls.", ); expect(contract.aiUsageRules).toContain( "The runtime Canvas width and Canvas height block belongs to the mandatory Settings section with the standard header; do not add a separate Canvas section label above these fields.", ); expect(contract.aiUsageRules).toContain( "When the user commits Canvas width or Canvas height, runtime keeps the typed dimension, preserves the selected preset or custom ratio, and recalculates the opposite dimension with integer rounding as one undoable change.", ); expect(contract.aiUsageRules).toContain( "Aspect ratio presets apply canonical initial dimensions. Custom shows separate Ratio W and Ratio H fields; editing a custom ratio preserves canvas width and recalculates height. Pixel edits retain the active proportion, and repeated values do not introduce rounding drift.", ); expect(contract.aiUsageRules).toContain( "For non-vector raster, Canvas 2D, WebGL, and WebGPU previews, set canvas.renderScale: true so the runtime adds Resolution scale after canvas sizing. The scale changes backing pixels from 1 to 2 without changing visible canvas size.", ); expect(contract.aiUsageRules).toContain( TOOLCRAFT_PERFORMANCE_VERIFICATION_POLICY.renderScaleFidelity, ); expect(contract.aiUsageRules).toContain( "Do not enable canvas.renderScale for DOM/SVG/vector-native previews; preserve vector fidelity through native vector rendering instead of raster supersampling.", ); }); it("documents persistence as a runtime-owned policy instead of ad hoc localStorage", () => { const contract = getToolcraftComponentContract("persistence"); expect(contract.kind).toBe("persistence"); expect(contract.stateMode).toBe("runtime-owned"); expect(contract.historyPolicy).toBe("never"); expect(contract.aiUsageRules).toContain( "Do not write app state to localStorage directly.", ); expect(contract.aiUsageRules).toContain( "Use schema persistence policy for app state that should survive reload.", ); expect(contract.aiUsageRules).toContain( "Persistence may include values, canvas, panels, timeline, layers, and media; history is not persisted.", ); expect(contract.aiUsageRules).toContain( 'Use persistence include: ["media"] only when runtime media state must survive reload, such as predefined attached files that users can delete, reorder, or transform. Do not use ad hoc storage for media state.', ); expect(contract.aiUsageRules).toContain( 'Apps with visible runtime panels and localStorage persistence must include "panels" so dragged panel positions survive reload.', ); expect(contract.aiUsageRules).toContain( "Apps with localStorage persistence must include acceptance coverage for changing a user setting, reloading the browser page, and seeing the restored value or product output.", ); expect(contract.aiUsageRules).toContain( "Saving source defaults must not hide or replace broken workspace persistence reload behavior.", ); }); it("documents runtime defaults authoring and retained settings codec semantics", () => { const contract = getToolcraftComponentContract("settingsTransfer"); expect(contract.kind).toBe("settings"); expect(contract.stateMode).toBe("runtime-owned"); expect(contract.aiUsageRules).toContain( "Generated apps keep a controls panel so runtime Setup is visible from the first run; product controls are added after that mandatory runtime section.", ); expect(contract.aiUsageRules).toContain( "Local Save State as Default occupies a separate headerless block above Settings; the whole block is absent without host authoring capability. Settings uses standard section Reset and collapse controls; the panel-header Reset remains available in every host.", ); expect(contract.aiUsageRules).toContain( "Do not add settings file actions or source-writing controls in product routes or panelActions.", ); expect(contract.commands).toEqual(["settings.apply"]); expect(contract.aiUsageRules).toEqual(expect.arrayContaining([ expect.stringContaining("attachment paths with durable resource references"), expect.stringContaining("missing or invalid attachments are skipped independently"), ])); expect(contract.aiUsageRules).toContain( "App-authored sections must not declare runtime Setup targets such as runtime.settingsTransfer, canvas.aspectRatio, canvas.size.width, canvas.size.height, canvas.renderScale, or panels.timeline.extended; those controls never suppress the mandatory runtime Setup controls.", ); expect(contract.aiUsageRules).toContain( "Runtime Setup places the standard Background switch beside Infinity canvas inside Settings, below the separate local defaults section when available, then Background color beside the Blanc/Dots workspace selector and finite sizing; Timeline and optional Lock rotation share the final Setup row when enabled.", ); expect(contract.aiUsageRules).toContain( "Timeline and Infinity canvas are self-explanatory runtime mode switches and do not render help icons.", ); }); });