191 lines
5.8 KiB
JavaScript
191 lines
5.8 KiB
JavaScript
// 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() {} } },
|
||
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");
|
||
assert.match(r.reason, /reCAPTCHA/);
|
||
});
|
||
|
||
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);
|
||
});
|
||
});
|