116 lines
4.6 KiB
JavaScript
116 lines
4.6 KiB
JavaScript
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, /<button[^>]*\sdisabled(?:\s|>|=)/i);
|
|
assert.doesNotMatch(index, /<input[^>]*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/);
|
|
});
|