import { describe, expect, it } from "vitest"; import { getToolcraftComponentContract } from "./component-contracts"; describe("Toolcraft template component contracts: media and custom controls", () => { it("documents minimal UI rules for custom controls", () => { const contract = getToolcraftComponentContract("customControl"); expect(contract.visualComponent).toBe("CustomControlRenderer"); expect(contract.aiUsageRules).toContain( "Custom controls must render the minimum UI needed to understand the value, context, and available actions; avoid decorative metadata and text that repeats what the section, label, or visible item already explains.", ); expect(contract.aiUsageRules).toContain( "Every visible custom-control element must justify its space by enabling selection, ordering, preview, removal, upload, editing, or status that affects the product.", ); expect(contract.aiUsageRules).toContain( "Do not use a custom control to recreate a built-in Slider, RangeSlider, Select, Segmented, Switch, Checkbox, Color, ColorOpacity, Gradient, FontPicker, ImagePicker, FileDrop, TextInput, CodeTextarea, RangeInput, Palette, Actions, CollectionActions, SourceCollection, Curves, AnchorGrid, ChannelMixer, Vector, or PanelActions control.", ); expect(contract.aiUsageRules).toContain( "Every custom-control builtInFitCheck declares typed capabilities. Use collection, reorder, selection, commands, custom-interaction, custom-value-model, and custom-visualization to describe behavior without relying on product nouns.", ); expect(contract.aiUsageRules).toContain( "When a custom control owns a repeated runtime item set, its builtInFitCheck must explicitly check sourceCollection, collectionActions, and actions before choosing custom; this is based on source versus user cardinality and the value workflow, not on entity names such as masks or glyphs.", ); expect(contract.aiUsageRules).toContain( "Do not justify custom controls with icons, layout, styling, compactness, or custom buttons alone. The fit check must name the product interaction or value model that built-ins cannot express.", ); expect(contract.aiUsageRules).toContain( "Custom controls may use Toolcraft primitives for small app-specific chrome, but must not import or render low-level runtime surfaces or duplicate toolbar, timeline, layers, canvas, panel, or built-in control mechanics.", ); expect(contract.aiUsageRules).toContain( "Custom-control action buttons must be sized for the interaction. Do not shrink destructive, reorder, upload, or primary actions below comfortable kit button/icon-button sizes just to fit more text.", ); }); it("documents image picker option acceptance as product behavior", () => { const contract = getToolcraftComponentContract("imagePicker"); expect(contract.kind).toBe("control"); expect(contract.defaultSectionLayout).toBe("standalone"); expect(contract.aiUsageRules).toContain( "Every visible ImagePicker item must be actionable in the current product context.", ); expect(contract.aiUsageRules).toContain( "Do not show selectable image choices that the renderer later sanitizes to a fallback or no-op.", ); expect(contract.aiUsageRules).toContain( "Tests must choose each visible ImagePicker item and assert the selected image, texture, gradient, or exported pixels change in the product output.", ); expect(contract.aiUsageRules).toContain( "Do not accept renderer data attributes, runtime target changes, or option existence as final proof that an image choice works.", ); }); it("documents file upload ownership across single-layer and multi-layer apps", () => { const contract = getToolcraftComponentContract("fileDrop"); expect(contract.commands).toEqual([ "media.delete", "media.import", "media.importBatch", "media.reorder", "media.transform", ]); expect(contract.aiUsageRules).toContain( 'Use fileDrop with assetKind: "image" for image-only source media and assetKind: "file" for arbitrary uploaded files.', ); expect(contract.aiUsageRules).toContain( 'Use fileDrop with assetKind: "model" for one GLB, glTF, FBX, OBJ, STL, or PLY model package. The selected source may be a standalone root, a folder batch, or one bounded ZIP archive.', ); expect(contract.aiUsageRules).toContain( "Model import preserves the supported authored material, texture, vertex-color, and static appearance subset. It derives a canonical document without rewriting the durable original source package.", ); expect(contract.aiUsageRules).toContain( "renderDefaultCanvasMedia controls generic image and file preview only. It does not hide standard runtime model layers; modelPresentation custom mode suppresses only the declared model targets.", ); expect(contract.aiUsageRules).toContain( "Rotatable models pair their source target with orientationGizmo. Gizmo drag, axis snap, direct model drag, preview, history/reset, and export consume the same pose and presentation lease; a geometry miss remains canvas pan.", ); expect(contract.aiUsageRules).toContain( "If an app ships with predefined source files or background images, declare them as schema media.defaultAssets with sourceTarget matching the fileDrop control. They must render as ordinary attached files in fileDrop, not as hidden renderer constants or canvas placeholder artwork.", ); expect(contract.aiUsageRules).toContain( "Predefined media files are default runtime state: users can remove them to get an empty source/canvas state, persistence may keep that removal with include: [\"media\"], and global or section Reset restores the default attached files.", ); expect(contract.aiUsageRules).toContain( "When uploaded/imported content is part of the source-material flow, the canvas must not show agent-invented artwork, CTA text, fake sample output, decorative placeholders, or preset source designs before real content exists; keep the canvas neutral/runtime-backed and put upload affordance in fileDrop.", ); expect(contract.aiUsageRules).toContain( "Do not add procedural Source Preset modes only to avoid an empty canvas. A default procedural or reference source is allowed only when the prompt/reference explicitly defines it and the worklog records that evidence.", ); expect(contract.aiUsageRules).toContain( "In single-layer apps, the runtime shows the uploaded image as the fileDrop preview and provides the clear action.", ); expect(contract.aiUsageRules).toContain( "In image mode, the runtime owns image transform actions: 90° Right, Flip horizontal, and Flip vertical. These actions render through the built-in actions-control in one three-column row with compact visible labels: 90°, Flip H, Flip V; keep a 6px vertical gap between the uploader and action row. Do not create a custom image action button grid. They update runtime mediaAssets transform metadata, and product preview/export must consume that metadata instead of keeping a separate transform state.", ); expect(contract.aiUsageRules).toContain( "When exactly one uploaded image is present, image transform actions are visible immediately. When multiple images are present, users select a thumbnail first; no transform actions render until a thumbnail is selected, and the action applies only to that selected image.", ); expect(contract.aiUsageRules).toContain( "In file mode, the runtime shows uploaded files as a sortable list with paperclip icons, file names, remove buttons, and --border/5 separators.", ); expect(contract.aiUsageRules).toContain( "In single-layer apps, global Reset controls and section reset must restore fileDrop source media to schema media.defaultAssets for that target; when no default asset exists, Reset removes uploaded media and returns the fileDrop target to defaultValue.", ); expect(contract.aiUsageRules).toContain( "Use fileDrop with multiple: true when the app needs several uploaded images as one source set; do not build a custom thumbnail uploader for this.", ); expect(contract.aiUsageRules).toContain( 'Use fileDrop variant: "collection-actions" only with assetKind: "file" and multiple: true when the user explicitly wants collectionActions-style compact − / + cardinality. The variant keeps runtime media order, uses + to add an empty upload slot, and uses − to remove the final slot or attached file.', ); expect(contract.aiUsageRules).toContain( "Each collection-actions FileDrop slot and its itemControls settings form one logical item group; runtime renders one line only between adjacent groups.", ); expect(contract.aiUsageRules).toContain( "fileDrop recommendedMaxItems is advisory and never rejects an import. A finite nonnegative safe-integer hardMaxItems is an actual admission limit: additive imports count existing assets for the same source target plus the incoming logical batch, replacement imports count only the replacement batch, and overflow is rejected before decoding or repository allocation.", ); expect(contract.aiUsageRules).toContain( "When multiple uploaded images are present, the runtime appends media, shows a sortable four-column preview grid, puts the add-more tile last, and exposes per-image removal.", ); expect(contract.aiUsageRules).toContain( "Canvas drops route to the first visible matching fileDrop target by asset kind: image files prefer image uploaders, non-image files prefer file uploaders, and file uploaders accept images only when no image uploader matches.", ); expect(contract.aiUsageRules).toContain( "Dragging thumbnails reorders runtime mediaAssets; preview, export, and renderer mapping must consume that media order instead of maintaining a separate product-only order.", ); expect(contract.aiUsageRules).toContain( "When uploaded images are used as canvas/background source material, draw them with cover/crop behavior: scale proportionally until the current canvas bounds are fully covered, keep the canvas size/settings unchanged, and crop overflow at the canvas bounds.", ); expect(contract.aiUsageRules).toContain( "Do not create custom upload buttons, file lists, or file sorting for generic source uploads when fileDrop can represent the source set.", ); expect(contract.aiUsageRules).toContain( "In multi-layer apps, deletion and visibility belong to the Layers panel; fileDrop remains an upload target.", ); expect(contract.aiUsageRules).toContain( 'When panels.layers is enabled and typed interactionOwnership assigns media management to Layers, fileDrop acceptance proves upload/admission, remove, reset, defaults, and model/binary lifecycle only; fixed layerCoverage "reorder" and "selected-layer-controls" recipes prove order output and selected image rotate/flip transform output without duplicating those claims on the fileDrop row.', ); }); });