/** * 旧画布 CanvasElement 类体系的纯图片能力及图标搜索测试,与 headless component registry 的模型定义分开。 * 只操作节点数据、几何和序列化,不挂载 renderer 或读取图片字节。 */ import { test } from "node:test"; import assert from "node:assert/strict"; import { canvasReducer, decodeCanvas, emptyCanvas, makeCanvasNode } from "../src/lib/canvas.ts"; import { CanvasElement, CardElement, PlainImageElement } from "../src/lib/canvas-elements.ts"; import { resizeFromPointer } from "../src/lib/canvas-geometry.ts"; import { iconCatalog, iconsById, searchIcons } from "../src/components/canvas/icon-catalog.ts"; // PlainImageElement 继承通用变换但不继承 CardElement,比例缩放、名称和 src 可恢复,重命名可撤销。 // YQ== 只是单字节 a 的 data URL:用于来源字段往返,不是可解码图片;尺寸断言来自模型默认值。 test("plain images inherit transforms without card connections and preserve source/name on restore", () => { const image = new PlainImageElement({ x: 0, y: 0 }, "", "data:image/png;base64,YQ=="); assert(image instanceof CanvasElement); assert.equal(image instanceof CardElement, false); const node = image.toNode(); assert.equal(node.type, "plainImage"); assert.equal(PlainImageElement.capabilities.connectable, false); const resized = resizeFromPointer(node, "bottom-right", { x: 320, y: 240 }); assert.equal(resized.width, 640); assert.equal(resized.height, 480); const renamed = canvasReducer({ past: [], present: { ...emptyCanvas(), nodes: [node] }, future: [] }, { type: "rename-node", id: node.id, title: "参考照片" }); const restored = decodeCanvas(JSON.stringify(renamed.present)); assert.equal(restored.nodes[0].data.title, "参考照片"); assert.equal(restored.nodes[0].data.src, node.data.src); assert.equal(canvasReducer(renamed, { type: "undo" }).present.nodes[0].data.title, "图片"); }); // emptyCanvas 的 schema 1 快照拒绝连向 plainImage 的旧卡片边;这不是 schema 2 的通用连线规则。 // 两次断言分别隔离非法连线和非法来源:先保持有效 src 测边,再清空 edges 测 javascript: src。 test("plain image sources are required and pure images cannot acquire card connections", () => { const image = makeCanvasNode("plain-image", { x: 0, y: 0 }, "data:image/png;base64,YQ=="); const card = makeCanvasNode("text", { x: 300, y: 0 }); const document = { ...emptyCanvas(), nodes: [image, card], edges: [{ id: "edge", source: card.id, target: image.id }] }; assert.throws(() => decodeCanvas(JSON.stringify(document))); image.data.src = "javascript:bad"; document.edges = []; assert.throws(() => decodeCanvas(JSON.stringify(document))); }); // 图标 ID 唯一并符合后端字符形状,中文类别与英文词可搜索;当前本地目录同时含 Lucide 和实验性 Tabler。 // 数量下限与两个固定 slug 是回归样本,不证明上游图标库的每个版本都已完整收录,也不请求后端搜索。 test("the complete Lucide catalog has unique backend-compatible identifiers and searchable names", () => { assert(iconCatalog.length > 2000); assert.equal(iconsById.size, iconCatalog.length); for (const icon of iconCatalog) assert.match(icon.name, /^[A-Z][A-Za-z0-9]{0,63}$/); assert(searchIcons("箭头").length > 45); assert(searchIcons("arrow left").some((icon) => icon.name === "ArrowLeft")); assert.equal(iconsById.get("Star").slug, "star"); assert.equal(iconsById.get("WandSparkles").slug, "wand-sparkles"); });