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: '
', 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"] ); }); });