200 lines
6.4 KiB
JavaScript
200 lines
6.4 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, "..", "dist", "content.js"), "utf8");
|
||
const sandbox = {
|
||
URL: globalThis.URL,
|
||
window: { location: { href: "https://example.com/login" } },
|
||
document: mockDoc,
|
||
chrome: {
|
||
runtime: {
|
||
id: "mock-extension-id",
|
||
onMessage: { addListener() {} },
|
||
// content.js 加载时会自动调用 loadAllConfigs → sendMessage,
|
||
// 缺少该 mock 会抛 "_sendMessage is not a function"(未处理拒绝,污染测试结果)
|
||
sendMessage: () => Promise.resolve({ configs: [] }),
|
||
},
|
||
// 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, "..", "dist", "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);
|
||
});
|
||
});
|