feat: 书库导入导出/图片变体/书籍搜索/小组件运行时等
新增: - 书库导入导出(library_import/library_export)及测试 - 图片变体生成(image_variants)与响应式图片(responsiveImage) - 书籍搜索(bookSearch)+ BookSearch/LibrarySearchGrid 组件 - 小组件运行时(widgetRuntime)与静态检查(widgetLint) - 上传缓存中间件(upload_cache)与字体 CSS 提取脚本 其它: - 后端 handlers/services 全量调整 - 前端页面、组件、库函数与配置更新
This commit is contained in:
218
frontend/lib/widgetLint.test.ts
Normal file
218
frontend/lib/widgetLint.test.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import { lintWidgetCode, type WidgetLintIssue } from "./widgetLint.ts";
|
||||
|
||||
const codes = (issues: WidgetLintIssue[]) => issues.map((i) => i.code);
|
||||
|
||||
describe("lintWidgetCode - JS 全局查找", () => {
|
||||
it("document 上的六种查找 API 全部报 error", () => {
|
||||
const js = [
|
||||
'document.getElementById("a");',
|
||||
'document.querySelector(".a");',
|
||||
'document.querySelectorAll(".a");',
|
||||
'document.getElementsByClassName("a");',
|
||||
'document.getElementsByTagName("div");',
|
||||
'document.getElementsByName("a");',
|
||||
].join("\n");
|
||||
const issues = lintWidgetCode({ js });
|
||||
assert.equal(issues.length, 6);
|
||||
assert.ok(issues.every((i) => i.level === "error" && i.code === "no-global-dom-lookup"));
|
||||
assert.deepEqual(
|
||||
issues.map((i) => i.line),
|
||||
[1, 2, 3, 4, 5, 6]
|
||||
);
|
||||
});
|
||||
|
||||
it("window.document 与 ownerDocument 上的同款查找同样命中", () => {
|
||||
const issues = lintWidgetCode({
|
||||
js: 'window.document.querySelector(".x");\nroot.ownerDocument.getElementById("y");',
|
||||
});
|
||||
assert.equal(issues.length, 2);
|
||||
assert.ok(issues.every((i) => i.code === "no-global-dom-lookup"));
|
||||
});
|
||||
|
||||
it("从 root 出发的查找不报错", () => {
|
||||
const issues = lintWidgetCode({
|
||||
js: [
|
||||
'root.querySelector(".a");',
|
||||
'root.querySelectorAll(".a");',
|
||||
'root.getElementById("a");',
|
||||
'el.getElementsByClassName("a");',
|
||||
].join("\n"),
|
||||
});
|
||||
assert.equal(issues.length, 0);
|
||||
});
|
||||
|
||||
it("注释、字符串、正则字面量中的关键词不误报", () => {
|
||||
const js = `
|
||||
// document.getElementById("x")
|
||||
/* document.querySelector(".y") */
|
||||
const s1 = "document.getElementById('z')";
|
||||
const s2 = 'document.querySelector(".z")';
|
||||
const re = /document\\.getElementById/;
|
||||
const tpl = \`document.getElementsByName("t")\`;
|
||||
root.querySelector(".real");
|
||||
`;
|
||||
const issues = lintWidgetCode({ js });
|
||||
assert.deepEqual(codes(issues), []);
|
||||
});
|
||||
|
||||
it("行列号按原文定位", () => {
|
||||
const js = "\n\n document.getElementById('x');";
|
||||
const [issue] = lintWidgetCode({ js });
|
||||
assert.equal(issue.line, 3);
|
||||
assert.equal(issue.column, 3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("lintWidgetCode - setInterval", () => {
|
||||
it("只有 setInterval、全文无 clearInterval 时报 warn", () => {
|
||||
const issues = lintWidgetCode({
|
||||
js: "const t = setInterval(fn, 100);",
|
||||
});
|
||||
assert.equal(issues.length, 1);
|
||||
assert.equal(issues[0].level, "warn");
|
||||
assert.equal(issues[0].code, "warn-set-interval");
|
||||
assert.equal(issues[0].line, 1);
|
||||
});
|
||||
|
||||
it("setInterval 与 clearInterval 配对出现时不报", () => {
|
||||
assert.deepEqual(
|
||||
lintWidgetCode({ js: "const t = setInterval(fn, 100);\nclearInterval(t);" }),
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
it("safeInterval 封装(cleanup 内清理)不报", () => {
|
||||
const js = [
|
||||
"function safeInterval(fn, ms) {",
|
||||
" var id = setInterval(fn, ms);",
|
||||
" registerCleanup(function () { clearInterval(id); });",
|
||||
" return id;",
|
||||
"}",
|
||||
"safeInterval(render, 1000);",
|
||||
].join("\n");
|
||||
assert.deepEqual(lintWidgetCode({ js }), []);
|
||||
});
|
||||
|
||||
it("clearInterval 只写在注释或字符串里不算配对,仍报 warn", () => {
|
||||
const issues = lintWidgetCode({
|
||||
js: 'const t = setInterval(f, 100);\n// clearInterval(t);\nconst s = "clearInterval(t)";',
|
||||
});
|
||||
assert.equal(issues.length, 1);
|
||||
assert.equal(issues[0].code, "warn-set-interval");
|
||||
assert.equal(issues[0].line, 1);
|
||||
});
|
||||
|
||||
it("window.setInterval 同样命中;注释里的 setInterval 不报", () => {
|
||||
const issues = lintWidgetCode({
|
||||
js: "window.setInterval(f, 50);\n// setInterval(f, 50);",
|
||||
});
|
||||
assert.equal(issues.length, 1);
|
||||
assert.equal(issues[0].code, "warn-set-interval");
|
||||
});
|
||||
|
||||
it("多个无清理的 setInterval 逐处提醒", () => {
|
||||
const issues = lintWidgetCode({
|
||||
js: "setInterval(a, 10);\nsetInterval(b, 20);",
|
||||
});
|
||||
assert.deepEqual(
|
||||
issues.map((i) => i.line),
|
||||
[1, 2]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("lintWidgetCode - CSS position:fixed", () => {
|
||||
it("position: fixed 报 error,兼容空格与 !important", () => {
|
||||
const css = ".a { position: fixed; }\n.b { position:fixed !important; }\n.c { position: absolute; }";
|
||||
const issues = lintWidgetCode({ css });
|
||||
assert.equal(issues.length, 2);
|
||||
assert.ok(issues.every((i) => i.code === "no-fixed-position" && i.level === "error"));
|
||||
assert.deepEqual(
|
||||
issues.map((i) => i.line),
|
||||
[1, 2]
|
||||
);
|
||||
});
|
||||
|
||||
it("@media 内层规则同样检查", () => {
|
||||
const issues = lintWidgetCode({ css: "@media (min-width: 100px) { .m { position: fixed; } }" });
|
||||
assert.equal(issues.length, 1);
|
||||
assert.equal(issues[0].code, "no-fixed-position");
|
||||
});
|
||||
|
||||
it("注释与 content 字符串不造成误报", () => {
|
||||
const css = `/* position: fixed */\n.a::after { content: "position: fixed"; color: red; }`;
|
||||
assert.deepEqual(codes(lintWidgetCode({ css })), []);
|
||||
});
|
||||
});
|
||||
|
||||
describe("lintWidgetCode - CSS 全局选择器", () => {
|
||||
it("body / html / * 开头的规则报 error", () => {
|
||||
const issues = lintWidgetCode({
|
||||
css: "body { margin: 0; }\nhtml.dark .a { color: #000; }\n* { box-sizing: border-box; }",
|
||||
});
|
||||
assert.equal(issues.length, 3);
|
||||
assert.ok(issues.every((i) => i.code === "no-global-selector" && i.level === "error"));
|
||||
});
|
||||
|
||||
it("选择器列表逐个判定,.body 类名与后代通配不误伤", () => {
|
||||
const issues = lintWidgetCode({
|
||||
css: ".body, body .card, .a *, html, *.x {}",
|
||||
});
|
||||
// 命中:body .card、html、*.x;.body 与 .a * 不命中
|
||||
assert.equal(issues.length, 3);
|
||||
});
|
||||
|
||||
it("@media 内层选择器同样检查", () => {
|
||||
const issues = lintWidgetCode({ css: "@media (min-width: 1px) { body { color: red; } }" });
|
||||
assert.equal(issues.length, 1);
|
||||
assert.equal(issues[0].code, "no-global-selector");
|
||||
});
|
||||
});
|
||||
|
||||
describe("lintWidgetCode - transform: scale", () => {
|
||||
it("transform 含 scale() 报 warn,支持厂商前缀与嵌套", () => {
|
||||
const issues = lintWidgetCode({
|
||||
css: ".a { transform: scale(1.2); }\n@media (min-width: 1px) { .b { -webkit-transform: scale(0.8); } }\n.c { transform: translateX(2px); }",
|
||||
});
|
||||
assert.equal(issues.length, 2);
|
||||
assert.ok(issues.every((i) => i.code === "warn-scale-transform" && i.level === "warn"));
|
||||
});
|
||||
|
||||
it("@keyframes 里的 scale 不报", () => {
|
||||
const css = "@keyframes spin { from { transform: scale(0.9); } to { transform: scale(1); } }";
|
||||
assert.deepEqual(codes(lintWidgetCode({ css })), []);
|
||||
});
|
||||
});
|
||||
|
||||
describe("lintWidgetCode - 综合", () => {
|
||||
it("空输入与干净代码返回空列表", () => {
|
||||
assert.deepEqual(lintWidgetCode({}), []);
|
||||
assert.deepEqual(
|
||||
lintWidgetCode({
|
||||
html: '<div class="box"></div>',
|
||||
css: ".box { color: red; transform: rotate(45deg); }",
|
||||
js: 'root.querySelector(".box").textContent = "hi";',
|
||||
}),
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
it("css 问题排在 js 问题之前,error 与 warn 各自分级", () => {
|
||||
const issues = lintWidgetCode({
|
||||
css: "body { position: fixed; }\n.a { transform: scale(2); }",
|
||||
js: "setInterval(f, 10);",
|
||||
});
|
||||
assert.deepEqual(codes(issues), [
|
||||
"no-global-selector",
|
||||
"no-fixed-position",
|
||||
"warn-scale-transform",
|
||||
"warn-set-interval",
|
||||
]);
|
||||
assert.deepEqual(
|
||||
issues.map((i) => i.level),
|
||||
["error", "error", "warn", "warn"]
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user