Files
auto-login/tests/detect-captcha.test.js
T
2026-09-14 15:38:07 +08:00

196 lines
6.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);
});
});