// tests/utils.test.js // 测试 utils.js 中的纯函数:isDomainMatch(URL 路径匹配)和 escapeHtml(HTML 转义) // 运行:node --test tests/utils.test.js const { describe, it } = require("node:test"); const assert = require("node:assert/strict"); const { isDomainMatch, escapeHtml, toMatchPatterns, pickFrameWithForm } = require("../dist/utils.js"); describe("isDomainMatch - 纯 hostname 匹配", () => { it("精确域名匹配", () => { assert.equal(isDomainMatch("https://example.com/login", "example.com"), true); assert.equal(isDomainMatch("https://example.com/", "example.com"), true); assert.equal(isDomainMatch("https://other.com/", "example.com"), false); }); it("子域名通配符 *.example.com", () => { assert.equal(isDomainMatch("https://oa.example.com/", "*.example.com"), true); assert.equal(isDomainMatch("https://a.b.example.com/", "*.example.com"), true); assert.equal(isDomainMatch("https://example.com/", "*.example.com"), true); // 根域也匹配 assert.equal(isDomainMatch("https://notexample.com/", "*.example.com"), false); }); it("任意位置通配符", () => { assert.equal(isDomainMatch("https://api.dev.example.com/", "api.*.example.com"), true); assert.equal(isDomainMatch("https://api.prod.example.com/", "api.*.example.com"), true); assert.equal(isDomainMatch("https://web.dev.example.com/", "api.*.example.com"), false); }); it("端口和协议不影响匹配", () => { assert.equal(isDomainMatch("http://example.com:8080/path", "example.com"), true); assert.equal(isDomainMatch("https://example.com:443/", "example.com"), true); }); it("非法 URL 返回 false", () => { assert.equal(isDomainMatch("not-a-url", "example.com"), false); assert.equal(isDomainMatch("", "example.com"), false); assert.equal(isDomainMatch("https://example.com/", ""), false); }); }); describe("isDomainMatch - hostname + path 匹配", () => { it("精确路径匹配", () => { assert.equal(isDomainMatch("https://oa.com/login", "oa.com/login"), true); assert.equal(isDomainMatch("https://oa.com/admin/login", "oa.com/login"), false); assert.equal(isDomainMatch("https://oa.com/", "oa.com/login"), false); }); it("路径前缀通配 login/*", () => { assert.equal(isDomainMatch("https://oa.com/login", "oa.com/login/*"), true); assert.equal(isDomainMatch("https://oa.com/login/step2", "oa.com/login/*"), true); assert.equal(isDomainMatch("https://oa.com/login/sub/deep", "oa.com/login/*"), true); assert.equal(isDomainMatch("https://oa.com/logins", "oa.com/login/*"), false); assert.equal(isDomainMatch("https://oa.com/admin", "oa.com/login/*"), false); }); it("路径全通配 *", () => { assert.equal(isDomainMatch("https://oa.com/anything", "oa.com/*"), true); assert.equal(isDomainMatch("https://oa.com/", "oa.com/*"), true); }); it("仅 hostname + 空路径匹配该域名所有页面", () => { assert.equal(isDomainMatch("https://oa.com/any/path", "oa.com/"), true); assert.equal(isDomainMatch("https://oa.com/", "oa.com/"), true); }); it("路径内任意位置通配", () => { assert.equal(isDomainMatch("https://site.com/a/123/b", "site.com/a/*/b"), true); assert.equal(isDomainMatch("https://site.com/a/xyz/b", "site.com/a/*/b"), true); assert.equal(isDomainMatch("https://site.com/a/123/c", "site.com/a/*/b"), false); }); it("path 匹配与子域名通配组合", () => { assert.equal(isDomainMatch("https://api.dev.com/login", "*.dev.com/login"), true); assert.equal(isDomainMatch("https://web.dev.com/admin", "*.dev.com/login"), false); assert.equal(isDomainMatch("https://api.dev.com/login/step2", "*.dev.com/login/*"), true); }); it("带 query 和 hash 的 URL 不影响 path 匹配", () => { assert.equal(isDomainMatch("https://oa.com/login?next=/home", "oa.com/login"), true); assert.equal(isDomainMatch("https://oa.com/login#section", "oa.com/login"), true); }); }); describe("toMatchPatterns - 配置域名转 match pattern", () => { it("纯域名同时生成 http / https 两条", () => { assert.deepEqual(toMatchPatterns("example.com"), ["http://example.com/*", "https://example.com/*"]); }); it("带协议前缀的配置会被剥离", () => { assert.deepEqual(toMatchPatterns("https://example.com"), [ "http://example.com/*", "https://example.com/*", ]); }); it("子域名通配符 *.example.com 保持原样", () => { assert.deepEqual(toMatchPatterns("*.example.com"), ["http://*.example.com/*", "https://*.example.com/*"]); }); it("路径规则按前缀截断到第一个 *", () => { assert.deepEqual(toMatchPatterns("oa.com/login/*"), ["http://oa.com/login/*", "https://oa.com/login/*"]); // 中间通配无法表达,截断成前缀(放宽注入范围,精度仍由 isDomainMatch 把关) assert.deepEqual(toMatchPatterns("site.com/a/*/b"), ["http://site.com/a/*", "https://site.com/a/*"]); }); it("无通配的路径补上结尾 *", () => { assert.deepEqual(toMatchPatterns("oa.com/admin"), ["http://oa.com/admin*", "https://oa.com/admin*"]); }); it("去掉端口(match pattern 不支持端口)", () => { assert.deepEqual(toMatchPatterns("example.com:8080/app"), [ "http://example.com/app*", "https://example.com/app*", ]); }); it("中文域名转成 punycode", () => { assert.deepEqual(toMatchPatterns("中文域名.中国"), [ "http://xn--fiq06l2rdsvs.xn--fiqs8s/*", "https://xn--fiq06l2rdsvs.xn--fiqs8s/*", ]); }); it("query / hash 会被忽略", () => { assert.deepEqual(toMatchPatterns("oa.com/login?next=/home#x"), [ "http://oa.com/login*", "https://oa.com/login*", ]); }); it("无法表达的规则返回空数组", () => { assert.deepEqual(toMatchPatterns(""), []); assert.deepEqual(toMatchPatterns(null), []); assert.deepEqual(toMatchPatterns("*"), []); // 裸通配 assert.deepEqual(toMatchPatterns("a.*.b.com"), []); // 中间通配的 host }); }); describe("pickFrameWithForm - 从多帧结果里挑出有表单的帧", () => { it("只有一个帧有表单时返回该帧", () => { assert.equal( pickFrameWithForm([ { frameId: 0, result: 0 }, { frameId: 5, result: 1 }, ]), 5, ); }); it("顶层帧有表单时优先顶层帧", () => { assert.equal( pickFrameWithForm([ { frameId: 0, result: 1 }, { frameId: 3, result: 2 }, ]), 0, ); }); it("多个子帧都有表单时取 frameId 最小的", () => { assert.equal( pickFrameWithForm([ { frameId: 7, result: 1 }, { frameId: 3, result: 1 }, ]), 3, ); }); it("都没有表单时返回 null", () => { assert.equal( pickFrameWithForm([ { frameId: 0, result: 0 }, { frameId: 2, result: 0 }, ]), null, ); assert.equal(pickFrameWithForm([]), null); assert.equal(pickFrameWithForm(undefined), null); }); it("缺少 frameId 时按顶层帧处理", () => { assert.equal(pickFrameWithForm([{ result: 1 }]), 0); }); }); describe("debugLog - 调试日志开关", () => { it("默认静默,打开开关后才输出", async () => { const logs = []; const originalLog = console.log; console.log = (...args) => logs.push(args.join(" ")); const loadUtils = () => { delete require.cache[require.resolve("../dist/utils.js")]; return require("../dist/utils.js"); }; try { delete global.chrome; let U = loadUtils(); assert.equal(await U.initDebugLog(), false); U.debugLog("默认不该出现"); assert.equal(logs.filter((l) => l.includes("默认不该出现")).length, 0); global.chrome = { storage: { local: { get: async () => ({ debugLog: true }) } } }; U = loadUtils(); assert.equal(await U.initDebugLog(), true); U.debugLog("开启后应出现"); assert.equal(logs.filter((l) => l.includes("开启后应出现")).length, 1); } finally { console.log = originalLog; delete global.chrome; loadUtils(); } }); }); describe("escapeHtml", () => { it("转义 5 个特殊字符", () => { assert.equal(escapeHtml("<"), "<"); assert.equal(escapeHtml(">"), ">"); assert.equal(escapeHtml('"'), """); assert.equal(escapeHtml("'"), "'"); assert.equal(escapeHtml("&"), "&"); }); it("组合转义", () => { assert.equal( escapeHtml('text & more'), "<a href="x" onclick="alert('hi')">text & more</a>", ); }); it("null 和 undefined 返回空串", () => { assert.equal(escapeHtml(null), ""); assert.equal(escapeHtml(undefined), ""); }); it("数字会被转成字符串", () => { assert.equal(escapeHtml(123), "123"); }); it("普通文本不变", () => { assert.equal(escapeHtml("hello world"), "hello world"); assert.equal(escapeHtml("中文测试123"), "中文测试123"); }); });