import { describe, expect, it } from "vitest"; import { TOOLCRAFT_COMPONENT_CONTRACTS, getToolcraftComponentContract, } from "./component-contracts"; import { TOOLCRAFT_PERFORMANCE_VERIFICATION_POLICY } from "./performance-verification-policy"; describe("Toolcraft template component contracts: runtime and composition", () => { 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.import"); expect(contract.commands).toContain("media.importBatch"); expect(contract.commands).toContain("media.transform"); expect(contract.capabilities).toContain("editable-size"); expect(contract.capabilities).toContain("infinity-canvas"); expect(contract.aiUsageRules).toContain( "Editable-output apps expose the runtime Infinity canvas switch before finite sizing controls; generated apps do not recreate or shadow that mode in product state.", ); expect(contract.aiUsageRules).toContain( "Infinity canvas removes finite artboard bounds and clipping, hides Aspect ratio, Canvas width, and Canvas height, and preserves the dormant finite canvas size for exact restoration when disabled.", ); expect(contract.aiUsageRules).toContain( "Infinity canvas suppresses the bounded product-rendered preview background so the dormant finite output does not appear as a second canvas; when the standard Background pair is enabled, CanvasShell fills the complete infinite viewport with the selected Background color.", ); expect(contract.aiUsageRules).toContain( "When the standard Background pair exists, disabling Background atomically restores finite mode and disables Infinity canvas; re-enabling Background restores switch availability without enabling Infinity automatically.", ); expect(contract.aiUsageRules).toContain( "Infinite exports use canonical world-space scene frames: ToolcraftAppComposition.sceneBoundsProvider contributes exact-state product bounds, runtime image and model bounds are unioned, hidden or suppressed content is excluded, PNG crops tightly, and runtime video unions every scheduled frame state into one envelope.", ); expect(contract.aiUsageRules).toContain( "Infinite live preview uses that same exact-state product frame: runtime positions one product scene surface from sceneBoundsProvider, and custom raster/WebGL output consumes useToolcraftProductSceneFrame for backing size and world-to-local translation instead of dormant canvas.size.", ); 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 uses the mandatory headerless Setup controls block; do not add a separate Canvas section label above these fields.", ); expect(contract.aiUsageRules).toContain( "When the user manually edits Canvas width or Canvas height, the runtime keeps the typed dimension, keeps the other dimension unchanged, switches Aspect ratio to Custom, stores the reduced current ratio in state, and keeps Canvas width and Canvas height as the sole custom numeric editors.", ); expect(contract.aiUsageRules).toContain( "Aspect ratio presets are the only interaction that may resize both canvas dimensions from a preset; manual size inputs are exact output dimensions.", ); 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( "Settings import/export is a mandatory runtime preset transfer feature; it must not be used to hide or replace broken persistence reload behavior.", ); }); it("documents settings transfer as a mandatory runtime-owned feature", () => { 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( "Do not gate Export Settings / Import Settings behind complexity thresholds, app size, or prompt wording.", ); expect(contract.aiUsageRules).toContain( "Do not hand-roll settings import/export through app routes, hidden file inputs, or panelActions.", ); expect(contract.aiUsageRules).toContain( "Settings transfer appears in the first visible headerless Setup controls-panel block; it imports and exports control values, canvas size, and timeline state.", ); 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 immediately after settings transfer, then Background color and finite sizing; Timeline is the final Setup control when enabled.", ); expect(contract.aiUsageRules).toContain( "Timeline and Infinity canvas are self-explanatory runtime mode switches and do not render help icons.", ); }); });