// tests/detect-captcha.test.js // 测试 content.js 中的 detectCaptcha:各类验证码场景识别 // 通过 vm 在沙箱中加载 content.js,注入 mock document // 运行:node --test tests/detect-captcha.test.js const { describe, it, beforeEach } = require("node:test"); const assert = require("node:assert/strict"); const fs = require("node:fs"); const path = require("node:path"); const vm = require("node:vm"); // 创建一个最小化的 mock document,支持 querySelector / querySelectorAll // elements 是一个选择器 -> 元素对象的映射;querySelector 返回第一个匹配 function createMockDoc(elements) { return { querySelector(selector) { // 简化:把逗号分隔的选择器拆开,返回第一个命中的 const parts = selector.split(",").map((s) => s.trim()); for (const sel of parts) { if (elements[sel]) return elements[sel]; } return null; }, querySelectorAll(selector) { const parts = selector.split(",").map((s) => s.trim()); const result = []; for (const sel of parts) { if (elements[sel]) result.push(elements[sel]); } return result; }, body: {}, }; } // 加载 content.js 到 vm 沙箱,返回沙箱上下文 function loadContentJs(mockDoc) { const code = fs.readFileSync(path.join(__dirname, "..", "content.js"), "utf8"); const sandbox = { URL: globalThis.URL, window: { location: { href: "https://example.com/login" } }, document: mockDoc, chrome: { runtime: { onMessage: { addListener() {} }, }, // i18n mock:getMessage 返回 key 自身作为文案(fallback) i18n: { getMessage: (key) => key || "" }, storage: { local: { get: async () => ({}), set: async () => {}, remove: async () => {} }, session: { get: async () => ({}), set: async () => {}, remove: async () => {} }, }, }, MutationObserver: class { observe() {} }, setTimeout, clearTimeout, console, }; // 让 utils.js 提供的 isDomainMatch / escapeHtml 在沙箱中可用 sandbox.globalThis = sandbox; vm.createContext(sandbox); // 先加载 utils.js const utilsCode = fs.readFileSync(path.join(__dirname, "..", "utils.js"), "utf8"); vm.runInContext(utilsCode, sandbox); vm.runInContext(code, sandbox); return sandbox; } describe("detectCaptcha", () => { let sandbox; beforeEach(() => { // 默认 mock:无任何验证码元素 const doc = createMockDoc({}); sandbox = loadContentJs(doc); }); it("无验证码元素时返回 null", () => { assert.equal(sandbox.detectCaptcha(), null); }); it("检测到 reCAPTCHA(.g-recaptcha)", () => { const doc = createMockDoc({ ".g-recaptcha": { offsetParent: {} }, }); const sb = loadContentJs(doc); const r = sb.detectCaptcha(); assert.ok(r); assert.equal(r.type, "reCAPTCHA"); // reason 由 i18n 提供,测试环境 fallback 到 key,断言非空字符串 assert.ok(typeof r.reason === "string" && r.reason.length > 0); }); it("检测到 reCAPTCHA iframe", () => { const doc = createMockDoc({ "iframe[src*='recaptcha']": { offsetParent: {} }, }); const sb = loadContentJs(doc); const r = sb.detectCaptcha(); assert.equal(r.type, "reCAPTCHA"); }); it("检测到 hCaptcha", () => { const doc = createMockDoc({ ".h-captcha": { offsetParent: {} }, }); const sb = loadContentJs(doc); const r = sb.detectCaptcha(); assert.equal(r.type, "hCaptcha"); }); it("检测到滑块验证码(#slider)", () => { const doc = createMockDoc({ "#slider": { offsetParent: {} }, }); const sb = loadContentJs(doc); const r = sb.detectCaptcha(); assert.equal(r.type, "slider/captcha"); }); it("检测到图形验证码容器(.captcha)", () => { const doc = createMockDoc({ ".captcha": { offsetParent: {} }, }); const sb = loadContentJs(doc); const r = sb.detectCaptcha(); assert.equal(r.type, "slider/captcha"); }); it("检测到 geetest 极验验证码", () => { const doc = createMockDoc({ ".geetest": { offsetParent: {} }, }); const sb = loadContentJs(doc); const r = sb.detectCaptcha(); assert.equal(r.type, "slider/captcha"); }); it("检测到验证码输入框(name=captcha)", () => { const doc = createMockDoc({ "input[name*='captcha' i]": { offsetParent: {}, type: "text" }, }); const sb = loadContentJs(doc); const r = sb.detectCaptcha(); assert.equal(r.type, "captcha-input"); }); it("检测到 placeholder 含「验证码」的输入框", () => { const doc = createMockDoc({ "input[placeholder*='验证码' i]": { offsetParent: {}, type: "text" }, }); const sb = loadContentJs(doc); const r = sb.detectCaptcha(); assert.equal(r.type, "captcha-input"); }); it("hidden 类型的验证码输入框不被识别(忽略隐藏域)", () => { const doc = createMockDoc({ "input[name*='captcha' i]": { offsetParent: {}, type: "hidden" }, }); const sb = loadContentJs(doc); const r = sb.detectCaptcha(); assert.equal(r, null); }); it("offsetParent 为 null 的滑块元素不可见,不被识别", () => { // 滑块/输入框分支会检查 offsetParent !== null,不可见元素不触发 const doc = createMockDoc({ "#slider": { offsetParent: null }, }); const sb = loadContentJs(doc); const r = sb.detectCaptcha(); assert.equal(r, null); }); it("reCAPTCHA 优先级高于滑块/输入框", () => { const doc = createMockDoc({ ".g-recaptcha": { offsetParent: {} }, "#slider": { offsetParent: {} }, "input[name*='captcha' i]": { offsetParent: {}, type: "text" }, }); const sb = loadContentJs(doc); const r = sb.detectCaptcha(); assert.equal(r.type, "reCAPTCHA"); }); it("返回对象包含 reason 字段说明原因", () => { const doc = createMockDoc({ "#captcha": { offsetParent: {} } }); const sb = loadContentJs(doc); const r = sb.detectCaptcha(); assert.ok(r && typeof r.reason === "string" && r.reason.length > 0); }); });