import { describe, expect, it } from "vitest"; import { model3dModule } from "../modules/built-ins/model-3d/model-3d-module"; import { mediaSourceModule } from "../modules/built-ins/media-source/media-source-module"; import { defineToolcraft } from "./define-toolcraft"; import type { ToolcraftControlSchema } from "./types"; import { createFileDropSchema, createSourceControlSchema, getSourceControl, materializeControlTestDefinition, } from "./define-toolcraft.control-sections.test-support"; describe("defineToolcraft fileDrop normalization", () => { it.each(["type", "target"] as const)( "rejects an inherited control %s", (fieldName) => { const inheritedValue = fieldName === "type" ? "fileDrop" : "source.asset"; const ownFields = fieldName === "type" ? { assetKind: "image", target: "source.asset" } : { assetKind: "image", type: "fileDrop" }; const control = Object.assign( Object.create({ [fieldName]: inheritedValue }), ownFields, ) as ToolcraftControlSchema; expect(() => defineToolcraft( createSourceControlSchema(control, [mediaSourceModule()]), ), ).toThrow(/serializable plain data.*Object is forbidden/u); }, ); it.each(["type", "target"] as const)( "rejects a non-enumerable control %s", (fieldName) => { const control = { applicability: { mode: "always" as const }, assetKind: "image", target: "source.asset", type: "fileDrop", }; Object.defineProperty(control, fieldName, { configurable: true, enumerable: false, value: control[fieldName], }); expect(() => defineToolcraft( createSourceControlSchema(control as ToolcraftControlSchema, [ mediaSourceModule(), ]), ), ).toThrow( /serializable plain data; accessors and non-enumerable fields are forbidden/u, ); }, ); it.each([ ["type", "an empty string", ""], ["type", "whitespace", " "], ["type", "a number", 1], ["type", "null", null], ["target", "an empty string", ""], ["target", "whitespace", "\t"], ["target", "a number", 1], ["target", "null", null], ])("rejects control %s set to %s", (fieldName, _label, value) => { expect(() => materializeControlTestDefinition( createFileDropSchema({ [fieldName]: value }, [mediaSourceModule()]), ), ).toThrow(new RegExp(`\\[control-${fieldName}\\]`, "u")); }); it("normalizes an omitted fileDrop assetKind to image", () => { const app = defineToolcraft( createFileDropSchema({}, [mediaSourceModule()]), ); expect(getSourceControl(app)?.assetKind).toBe("image"); }); it.each([ ["negative", -1, /\[control-hard-max-items\].*nonnegative safe integer/u], [ "fractional", 1.5, /\[control-hard-max-items\].*nonnegative safe integer/u, ], [ "infinite", Number.POSITIVE_INFINITY, /serializable plain data.*non-finite numbers/u, ], [ "unsafe", Number.MAX_SAFE_INTEGER + 1, /\[control-hard-max-items\].*nonnegative safe integer/u, ], ])("rejects a %s fileDrop hardMaxItems", (_label, hardMaxItems, error) => { expect(() => defineToolcraft( createFileDropSchema({ hardMaxItems }, [mediaSourceModule()]), ), ).toThrow(error); }); it("preserves an explicit zero fileDrop hardMaxItems", () => { const app = defineToolcraft( createFileDropSchema({ hardMaxItems: 0 }, [mediaSourceModule()]), ); expect(getSourceControl(app)?.hardMaxItems).toBe(0); }); it("accepts collection actions for a multiple file-kind fileDrop", () => { const app = defineToolcraft( createFileDropSchema( { assetKind: "file", multiple: true, variant: "collection-actions", }, [mediaSourceModule()], ), ); expect(getSourceControl(app)?.variant).toBe("collection-actions"); }); it.each([ ["a single fileDrop", { assetKind: "file" }, [mediaSourceModule()]], [ "an image fileDrop", { assetKind: "image", multiple: true }, [mediaSourceModule()], ], [ "a model fileDrop", { assetKind: "model", multiple: false }, [model3dModule()], ], ])("rejects collection actions on %s", (_label, fields, modules) => { expect(() => defineToolcraft( createFileDropSchema( { ...fields, variant: "collection-actions", }, modules, ), ), ).toThrow( /\[filedrop-collection-actions\].*assetKind: "file".*multiple: true/u, ); }); it("accepts model fileDrops", () => { const createModelApp = () => defineToolcraft( createFileDropSchema({ assetKind: "model" }, [model3dModule()]), ); expect(createModelApp).not.toThrow(); expect(getSourceControl(createModelApp())?.assetKind).toBe("model"); }); it("rejects multiple logical models from one model fileDrop", () => { expect(() => defineToolcraft( createFileDropSchema({ assetKind: "model", multiple: true }, [ model3dModule(), ]), ), ).toThrow(/model fileDrop.*multiple true/u); }); it.each([ ["a string", "false", /\[model-filedrop-multiple\]/u], ["a number", 1, /\[model-filedrop-multiple\]/u], ["a symbol", Symbol("multiple"), /serializable plain data.*symbol values/u], ["null", null, /\[model-filedrop-multiple\]/u], ])("rejects model multiple set to %s", (_label, multiple, error) => { expect(() => defineToolcraft( createFileDropSchema({ assetKind: "model", multiple }, [ model3dModule(), ]), ), ).toThrow(error); }); it("rejects model-tagged panel actions before action partitioning", () => { expect(() => materializeControlTestDefinition( createFileDropSchema( { actions: ["export.image"], assetKind: "model", type: "panelActions", }, [model3dModule()], ), ), ).toThrow(/\[model-filedrop-type\]/u); }); it.each([ ["an empty string", ""], ["an unsupported string", "video"], ["a number", 1], ["a symbol", Symbol("assetKind")], ["null", null], ])("rejects fileDrop assetKind set to %s", (_label, assetKind) => { expect(() => materializeControlTestDefinition( createFileDropSchema({ assetKind }, [mediaSourceModule()]), ), ).toThrow(/\[filedrop-asset-kind\]/u); }); it.each([ ["modelFormats", ["glb"]], ["modelLimits", { maxNodes: 500 }], ["topologyProfile", "realtime-mesh"], ])( "rejects non-model controls supplying %s", (modelField, modelFieldValue) => { expect(() => defineToolcraft( createFileDropSchema( { assetKind: "image", [modelField]: modelFieldValue, }, [mediaSourceModule()], ), ), ).toThrow(new RegExp(`\\[model-only-field\\].*${modelField}`, "u")); }, ); it("preserves arbitrary custom control type strings", () => { const app = defineToolcraft( createFileDropSchema( { type: "productMaskEditor", }, [], ), ); expect(getSourceControl(app)?.type).toBe("productMaskEditor"); }); });