import test from "node:test"; import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; import path from "node:path"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const staticDir = path.join(root, "app", "static"); async function source(relativePath) { return readFile(path.join(staticDir, relativePath), "utf8"); } test("unavailable controls never masquerade as indefinite loading", async () => { const [style, index, interaction, app] = await Promise.all([ source("style.css"), source("index.html"), source("interaction-feedback.js"), source("app.js"), ]); assert.doesNotMatch(style, /cursor\s*:\s*wait/); assert.doesNotMatch(index, /]*\sdisabled(?:\s|>|=)/i); assert.doesNotMatch(index, /]*type="checkbox"[^>]*\sdisabled(?:\s|>|=)/i); assert.match(index, /id="control-unavailable-tooltip"[^>]*role="tooltip"/); assert.match(index, /id="control-feedback-toast"[^>]*role="status"[^>]*aria-live="polite"/); assert.match(index, /请先新建或打开一个动图,再把当前画板添加为帧。/); assert.match(interaction, /setAttribute\("aria-disabled", "true"\)/); assert.match(interaction, /setAttribute\("aria-busy", "true"\)/); assert.match(interaction, /data-unavailable-reason/); assert.match(app, /import "\.\/interaction-feedback\.js"/); }); test("action controls do not bypass the shared state module", async () => { const files = [ "app.js", "color-picker.js", "scene-board.js", "views/animations.js", "views/device.js", "views/settings.js", "views/templates.js", "views/text.js", ]; const directAssignments = []; for (const relativePath of files) { const text = await source(relativePath); text.split(/\r?\n/).forEach((line, index) => { if (/\.disabled\s*=/.test(line) && !/fields\.disabled\s*=/.test(line) && !/calibrationReference\.disabled\s*=/.test(line)) { directAssignments.push(`${relativePath}:${index + 1}:${line.trim()}`); } }); } assert.deepEqual(directAssignments, []); }); test("HTTP requests time out with a stable user-facing error", async () => { const originalWindow = globalThis.window; const originalFetch = globalThis.fetch; globalThis.window = { setTimeout, clearTimeout }; globalThis.fetch = (_path, options) => new Promise((_resolve, reject) => { options.signal.addEventListener("abort", () => reject(new DOMException("aborted", "AbortError")), { once: true }); }); try { const { apiJson, DEFAULT_API_TIMEOUT_MS } = await import("../app/static/core.js"); assert.equal(DEFAULT_API_TIMEOUT_MS, 15000); await assert.rejects( () => apiJson("/never-returns", { timeoutMs: 5 }), (error) => error.code === "REQUEST_TIMEOUT" && error.message === "请求超时,请检查核桃派网络或服务状态后重试", ); } finally { globalThis.window = originalWindow; globalThis.fetch = originalFetch; } }); test("demo library actions stay black, explain restrictions, and preserve copy", async () => { const [templates, style] = await Promise.all([ source("views/templates.js"), source("style.css"), ]); assert.match(templates, /演示案例不得编辑,请复制/); assert.match(templates, /include_demo=true/); assert.match(templates, /demo_order/); assert.match(templates, /api\/animations\/\$\{animation\.id\}\/copy/); assert.match(templates, /restrictDemoAction\(edit\)/); assert.match(templates, /restrictDemoAction\(rename\)/); assert.match(templates, /restrictDemoAction\(remove\)/); assert.doesNotMatch(templates, /restrictDemoAction\(copy\)/); assert.match(style, /\.demo-restricted-action\[aria-disabled="true"\][\s\S]*background:\s*#050505/); }); test("sortable cards protect controls and keep demo cards fixed", async () => { const [sortable, animations, templates, style] = await Promise.all([ source("sortable-card.js"), source("views/animations.js"), source("views/templates.js"), source("style.css"), ]); assert.match(sortable, /DRAG_START_GUARD_PX = 8/); assert.match(sortable, /button, input, select, textarea/); assert.match(sortable, /pointerdown/); assert.doesNotMatch(sortable, /pointerdown[\s\S]{0,500}preventDefault\(\)/); assert.match(animations, /protectCardDragStart\(card\)/); assert.match(templates, /protectCardDragStart\(card, \{ locked: true \}\)/); assert.match(templates, /template-card-order-actions/); assert.match(templates, /libraryDragType/); assert.match(style, /\.library-demo-fixed/); assert.match(style, /\.library-sortable-card\.library-drop-target/); });