/** * 本地图标目录/分片和共享 ResourceLoader 测试。直接读仓库生成资产,校验本地哈希和引用,不访问图标 CDN。 * loader 的活跃并发与缓存容量受限,不能据此推断待处理队列本身有长度上限。 */ import { test } from "node:test"; import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import { createHash } from "node:crypto"; import catalog from "../src/components/canvas/icon-catalog-data.json" with { type: "json" }; import manifest from "../src/lib/canvas-icon-assets.json" with { type: "json" }; import { iconAsset, validateIconShard } from "../src/lib/canvas-icon-loader.ts"; import { decodeCanvas, emptyCanvas, makeCanvasNode } from "../src/lib/canvas.ts"; import { createResourceLoader } from "../src/lib/resource-loader.ts"; // 核对目录数量、ID 唯一性、分片文件哈希/体积/合法 SVG 数据、每个图标和分类存在;选取三个 ID 再验证旧快照往返。 test("all Lucide and experimental Tabler IDs map to valid local shards and survive snapshots", async () => { assert.equal(catalog.filter((icon) => icon.source === "lucide").length, 2313); assert.equal(catalog.filter((icon) => icon.source === "tabler").length, 24); assert.equal(new Set(catalog.map((icon) => icon.name)).size, catalog.length); // 按 manifest 逐分片读取本地 JSON;文件名哈希取 trimEnd 后文本的 SHA-256 前 12 位,单片小于 20000 字节。 const loaded = new Map(); for (const asset of Object.values(manifest.assets)) { const content = await readFile(`public${manifest.root}/${asset}`, "utf8"); assert.ok(asset.includes(createHash("sha256").update(content.trimEnd()).digest("hex").slice(0, 12))); loaded.set(`${manifest.root}/${asset}`, validateIconShard(JSON.parse(content))); assert.ok(Buffer.byteLength(content) < 20_000, asset); } for (const icon of catalog) { assert.ok(loaded.get(iconAsset(icon.name))[icon.name]); assert.ok(icon.categories.every((category) => ["arrows", "people", "office", "media", "tech", "business", "nature", "travel", "shapes"].includes(category))); } for (const name of ["Star", "TablerAlien", "TablerCube3dSphere"]) { assert.ok(catalog.some((icon) => icon.name === name)); const node = makeCanvasNode("icon", { x: 0, y: 0 }); node.data.icon = name; assert.equal(decodeCanvas(JSON.stringify({ ...emptyCanvas(), nodes: [node] })).nodes[0].data.icon, name); } }); // 对 script、onload、外部 paint URL 和 use/href 的样例逐个拒绝;这是数据校验例,不是完整浏览器 SVG 安全审计。 test("icon data rejects executable shapes, events and external paint references", () => { for (const node of [["script", {}], ["path", { onload: "alert(1)" }], ["path", { fill: "url(https://example.com)" }], ["use", { href: "bad" }]]) { assert.throws(() => validateIconShard({ Star: [node] })); } }); // 让 setImmediate 推进到调度任务阶段;用于观察尚未开始、排队及已释放容量的加载。 const tick = () => new Promise((resolve) => setImmediate(resolve)); // request 后立即 abort,尚未进入调度时不能启动网络加载,也不能把未完成值留入缓存。 test("an immediately unmounted icon does not start a network request", async () => { let calls = 0; const loader = createResourceLoader(async () => { calls++; return "unused"; }); const controller = new AbortController(); const pending = loader.request("unused", controller.signal); const rejected = assert.rejects(pending, { name: "AbortError" }); controller.abort(); await rejected; await tick(); assert.equal(calls, 0); assert.equal(loader.peek("unused"), undefined); }); // 同 key a 两个订阅只加载一次,容量二时 c 等待;完成后缓存命中不新增调用,峰值为二。 test("loader bounds concurrency, deduplicates shared shards and reuses cache", async () => { let active = 0, peak = 0, calls = 0; // 手动完成 active 加载,用于在容量为二时观察第三项等待及共享 key 去重。 const finish = []; const loader = createResourceLoader(async (key) => { active++; peak = Math.max(peak, active); calls++; await new Promise((resolve) => finish.push(resolve)); active--; return key; }, 2, 3); const a = loader.request("a"), duplicate = loader.request("a"), b = loader.request("b"), c = loader.request("c"); await tick(); assert.equal(calls, 2); finish.splice(0).forEach((resolve) => resolve()); await tick(); assert.equal(calls, 3); finish.splice(0).forEach((resolve) => resolve()); assert.deepEqual(await Promise.all([a, duplicate, b, c]), ["a", "a", "b", "c"]); assert.equal(await loader.request("a"), "a"); assert.equal(calls, 3); assert.equal(peak, 2); }); // 取消排队 b 后它永不启动;取消 a 的一个订阅不能中止另一个订阅共享的 active a。 test("unused queued loads never start; a remaining subscriber keeps a shared load alive", async () => { const seen = []; let release; const loader = createResourceLoader(async (key) => { seen.push(key); if (key === "a") await new Promise((resolve) => release = resolve); return key; }, 1); const first = new AbortController(), queued = new AbortController(); const a = loader.request("a", first.signal), shared = loader.request("a"); const b = loader.request("b", queued.signal); const rejected = Promise.all([assert.rejects(a, { name: "AbortError" }), assert.rejects(b, { name: "AbortError" })]); await tick(); first.abort(); queued.abort(); release(); await rejected; assert.equal(await shared, "a"); await tick(); assert.deepEqual(seen, ["a"]); }); // active abort 后释放容量,失败 key 可重新加载;容量二的 LRU 在加入 a/b 后淘汰更早 fail,保留仍在缓存的 a。 test("active aborts and failed requests can retry; least recently used cache is bounded", async () => { let fail = true; const loader = createResourceLoader(async (key, signal) => { if (key === "abort") return new Promise((_, reject) => signal.addEventListener("abort", () => reject(new DOMException("Aborted", "AbortError")), { once: true })); if (key === "fail" && fail) { fail = false; throw new Error("offline"); } return key; }, 1, 2); const controller = new AbortController(); const pending = loader.request("abort", controller.signal); const rejected = assert.rejects(pending, { name: "AbortError" }); await tick(); controller.abort(); await rejected; await tick(); await assert.rejects(loader.request("fail"), /offline/); await tick(); assert.equal(await loader.request("fail"), "fail"); await loader.request("a"); await loader.request("b"); assert.equal(loader.peek("fail"), undefined); assert.equal(loader.peek("a"), "a"); });