Files
auto-login/tests/content-fill.test.js
T

725 lines
26 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/content-fill.test.js
// 用 jsdom(真实 DOM)测试 content.js 的登录表单探测与填充引擎:
// 字段猜测、隐藏元素跳过、Shadow DOM 穿透、HTML 片段定位、
// 自动提交/验证码拦截、失败次数限制、多环境浮窗、SPA 重试、手动填充消息。
// 运行:node --test tests/content-fill.test.js
//
// jsdom 不实现布局(offsetWidth/offsetParent 恒为 0/null),因此这里统一把元素视为"可见",
// 需要模拟隐藏时用 hide() 单独覆盖 —— 这样能真实覆盖 content.js 里的可见性判断分支。
const { describe, it } = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const { JSDOM } = require("jsdom");
const UTILS_CODE = fs.readFileSync(path.join(__dirname, "..", "dist", "utils.js"), "utf8");
const CONTENT_CODE = fs.readFileSync(path.join(__dirname, "..", "dist", "content.js"), "utf8");
const tick = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// autoSubmit 的最小延迟是 300ms(content.js 里 Math.max(300, delayMs))
const AFTER_SUBMIT = 400;
function createChromeMock({ configs = [], attempts = {}, activeEnvId = null } = {}) {
const state = {
configs,
activeEnvId,
attempts: new Map(Object.entries(attempts)),
calls: [],
messageListener: null,
// 每个帧注入 content.js 都会注册一个监听器;allFrames 场景下用最后一个(最内层帧)
messageListeners: [],
};
const api = {
runtime: {
id: "mock-extension-id",
onMessage: {
addListener: (fn) => {
state.messageListener = fn;
state.messageListeners.push(fn);
},
},
sendMessage: (message) => {
state.calls.push(message.action);
switch (message.action) {
case "loadAllConfigs":
return Promise.resolve({ configs: state.configs, activeEnvId: state.activeEnvId });
case "getVaultStatus":
return Promise.resolve({ hasVault: false, unlocked: true, pendingImport: false });
case "getDomainAttempts": {
const key = `${message.envId}::${message.domain}`;
return Promise.resolve({ attempts: state.attempts.get(key) || { count: 0, lastAttempt: 0 } });
}
case "incrementDomainAttempts": {
const key = `${message.envId}::${message.domain}`;
const cur = state.attempts.get(key) || { count: 0, lastAttempt: 0 };
cur.count += 1;
cur.lastAttempt = Date.now();
state.attempts.set(key, cur);
return Promise.resolve({ attempts: cur });
}
case "resetDomainAttempts":
return Promise.resolve({ success: true });
default:
return Promise.resolve({});
}
},
},
i18n: { getMessage: (key) => key },
storage: {
local: { get: async () => ({}), set: async () => {}, remove: async () => {} },
session: { get: async () => ({}), set: async () => {}, remove: async () => {} },
},
};
return { api, state };
}
/**
* jsdom 没有布局引擎(offsetParent 恒为 null、offsetWidth 恒为 0),
* 而 content.js 会用它们判断元素是否可见。这里按 realm 逐个打补丁:
* 主窗口与每个 iframe 都各有自己的 HTMLElement.prototype,必须分别处理。
*/
function patchLayout(win) {
Object.defineProperty(win.HTMLElement.prototype, "offsetWidth", {
get() {
return 100;
},
configurable: true,
});
Object.defineProperty(win.HTMLElement.prototype, "offsetHeight", {
get() {
return 20;
},
configurable: true,
});
Object.defineProperty(win.HTMLElement.prototype, "offsetParent", {
get() {
return this.parentElement || this.ownerDocument.body;
},
configurable: true,
});
// jsdom 未实现 CSS.escape(真实浏览器有)
if (!win.CSS) win.CSS = {};
if (!win.CSS.escape)
win.CSS.escape = (s) => String(s).replace(/[^a-zA-Z0-9_\u00a0-\uffff-]/g, (c) => "\\" + c);
}
/**
* 在 jsdom 页面里执行 utils.js + content.js(与浏览器一致:按普通脚本顺序执行)。
* setup 回调在注入脚本之前执行,用于准备"脚本加载前就必须存在"的结构(例如 Shadow Root)。
*/
function loadContentPage({
html = "",
url = "https://example.com/login",
configs = [],
attempts = {},
activeEnvId = null,
timing,
setup,
} = {}) {
// pretendToBeVisual: 让 jsdom 提供 requestAnimationFrame(content.js 的填充完成 toast 依赖它),
// 否则 showFillToast() 会抛 ReferenceError。
const dom = new JSDOM(`<!DOCTYPE html><html><body>${html}</body></html>`, {
url,
runScripts: "dangerously",
pretendToBeVisual: true,
});
const win = dom.window;
patchLayout(win);
// 缩短时间常量(30 秒超时 / 4 秒结果观察),否则这些用例要真等半分钟
if (timing) win.__autoLoginTiming = timing;
const chrome = createChromeMock({ configs, attempts, activeEnvId });
win.chrome = chrome.api;
if (setup) setup(win, win.document);
const inject = (code) => {
const el = win.document.createElement("script");
el.textContent = code;
win.document.head.appendChild(el);
};
inject(UTILS_CODE);
inject(CONTENT_CODE);
return { dom, win, doc: win.document, chrome };
}
/** 模拟元素不可见(隐藏域 / display:none) */
function hide(el) {
Object.defineProperty(el, "offsetWidth", { get: () => 0, configurable: true });
Object.defineProperty(el, "offsetParent", { get: () => null, configurable: true });
return el;
}
function makeConfig(overrides = {}) {
return {
id: "cfg1",
envId: "env1",
envName: "测试",
domain: "example.com",
alias: null,
username: "auto-user",
password: "auto-pass",
autoSubmit: false,
usernameSelector: null,
passwordSelector: null,
submitSelector: null,
enabled: true,
delayMs: null,
...overrides,
};
}
describe("content - 字段探测", () => {
it("标准登录表单:用户名 / 密码 / 提交按钮都能找到", () => {
const ctx = loadContentPage({
html: `<form>
<input type="text" name="username">
<input type="password" name="pass">
<button type="submit">登录</button>
</form>`,
});
assert.equal(ctx.win.findPasswordField({}).getAttribute("name"), "pass");
assert.equal(ctx.win.findUsernameField({}).getAttribute("name"), "username");
assert.equal(
ctx.win.findSubmitButton({}, ctx.doc.querySelector("input[type=password]")).textContent,
"登录",
);
});
it("显式选择器优先于自动探测", () => {
const ctx = loadContentPage({
html: `<form>
<input type="password" id="wrong">
<input type="password" id="right">
<input type="text" id="u1">
<input type="text" id="u2" name="account">
</form>`,
});
const config = { passwordSelector: "#right", usernameSelector: "#u2" };
assert.equal(ctx.win.findPasswordField(config).id, "right");
assert.equal(ctx.win.findUsernameField(config).id, "u2");
});
it("用户名按 name / placeholder 关键词猜测", () => {
const ctx = loadContentPage({
html: `<form>
<input type="text" placeholder="请输入手机号" id="phone">
<input type="text" placeholder="登录账号" id="account">
<input type="password">
</form>`,
});
assert.equal(ctx.win.findUsernameField({}).id, "account");
});
it("隐藏的密码框被跳过,选可见的那个", () => {
const ctx = loadContentPage({
html: `<form>
<input type="password" id="ghost">
<input type="password" id="real">
</form>`,
});
hide(ctx.doc.getElementById("ghost"));
assert.equal(ctx.win.findPasswordField({}).id, "real");
});
it("页面上没有密码框时返回 null,fillForm 返回 false", () => {
const ctx = loadContentPage({ html: `<form><input type="text" name="q"></form>` });
assert.equal(ctx.win.findPasswordField({}), null);
assert.equal(ctx.win.fillForm({ username: "u", password: "p", autoSubmit: false }, false), false);
});
it("提交按钮按文案识别(非 type=submit)", () => {
const ctx = loadContentPage({
html: `<form><input type="password"><button id="btn">Sign in</button></form>`,
});
assert.equal(ctx.win.findSubmitButton({}, ctx.doc.querySelector("input[type=password]")).id, "btn");
});
it("能穿透 Shadow DOM 找到登录框", () => {
const ctx = loadContentPage({ html: `<div id="host"></div>` });
const shadow = ctx.doc.getElementById("host").attachShadow({ mode: "open" });
shadow.innerHTML = `<form>
<input type="text" name="user">
<input type="password" name="pass">
<button type="submit">登录</button>
</form>`;
assert.equal(ctx.win.findPasswordField({}).getAttribute("name"), "pass");
assert.equal(ctx.win.findUsernameField({}).getAttribute("name"), "user");
});
it("resolveElement 支持选择器与整段 HTML 片段两种写法", () => {
const ctx = loadContentPage({
html: `<form><input type="password" id="pwd" name="pass"></form>`,
});
assert.equal(ctx.win.resolveElement("input[name=pass]").id, "pwd");
// 从 HTML 片段反推选择器(用户直接粘贴元素代码的场景)
assert.equal(ctx.win.resolveElement('<input type="password" id="pwd">').id, "pwd");
});
});
describe("content - 填充与提交", () => {
it("填充用户名和密码,并对两个字段派发 input/change 事件", () => {
const ctx = loadContentPage({
html: `<form>
<input type="text" name="username" id="u">
<input type="password" id="p">
<button type="submit" id="btn">登录</button>
</form>`,
});
const events = [];
for (const id of ["u", "p"]) {
const el = ctx.doc.getElementById(id);
el.addEventListener("input", () => events.push(`${id}:input`));
el.addEventListener("change", () => events.push(`${id}:change`));
}
assert.equal(ctx.win.fillForm({ username: "alice", password: "secret", autoSubmit: false }, false), true);
assert.equal(ctx.doc.getElementById("u").value, "alice");
assert.equal(ctx.doc.getElementById("p").value, "secret");
assert.deepEqual(events, ["u:input", "u:change", "p:input", "p:change"]);
});
it("autoSubmit 为真时延迟点击提交按钮", async () => {
const ctx = loadContentPage({
html: `<form><input type="text" name="username"><input type="password"><button type="submit" id="btn">登录</button></form>`,
});
const btn = ctx.doc.getElementById("btn");
let clicks = 0;
btn.addEventListener("click", () => clicks++);
ctx.win.fillForm({ username: "u", password: "p", autoSubmit: true }, true);
assert.equal(clicks, 0, "不应立即点击");
await tick(AFTER_SUBMIT);
assert.equal(clicks, 1);
});
it("检测到验证码时不自动提交,并弹出提示", async () => {
const ctx = loadContentPage({
html: `<form>
<input type="text" name="username">
<input type="password">
<div class="g-recaptcha"></div>
<button type="submit" id="btn">登录</button>
</form>`,
});
const btn = ctx.doc.getElementById("btn");
let clicks = 0;
btn.addEventListener("click", () => clicks++);
ctx.win.fillForm({ username: "u", password: "p", autoSubmit: true }, true);
await tick(AFTER_SUBMIT);
assert.equal(clicks, 0, "有验证码时不能自动提交");
assert.ok(ctx.doc.body.innerHTML.includes("captchaToastTitle"), "应显示验证码提示浮窗");
});
it("没有提交按钮但在 form 内时回退到 requestSubmit", async () => {
const ctx = loadContentPage({
html: `<form><input type="text" name="username"><input type="password"></form>`,
});
let submitted = 0;
const form = ctx.doc.querySelector("form");
Object.defineProperty(form, "requestSubmit", {
value: () => {
submitted++;
},
configurable: true,
});
ctx.win.fillForm({ username: "u", password: "p", autoSubmit: true }, true);
await tick(AFTER_SUBMIT);
assert.equal(submitted, 1);
});
});
describe("content - 自动填充主流程", () => {
const LOGIN_HTML = `<form>
<input type="text" name="username" id="u">
<input type="password" id="p">
<button type="submit" id="btn">登录</button>
</form>`;
it("域名命中配置时自动填充", async () => {
const ctx = loadContentPage({
html: LOGIN_HTML,
url: "https://example.com/login",
configs: [makeConfig()],
});
await tick(60);
assert.equal(ctx.doc.getElementById("u").value, "auto-user");
assert.equal(ctx.doc.getElementById("p").value, "auto-pass");
});
it("域名不匹配时什么都不做", async () => {
const ctx = loadContentPage({
html: LOGIN_HTML,
url: "https://other.com/login",
configs: [makeConfig()],
});
await tick(60);
assert.equal(ctx.doc.getElementById("u").value, "");
assert.equal(ctx.doc.getElementById("p").value, "");
});
it("配置被禁用时不填充", async () => {
const ctx = loadContentPage({ html: LOGIN_HTML, configs: [makeConfig({ enabled: false })] });
await tick(60);
assert.equal(ctx.doc.getElementById("u").value, "");
});
it("未达失败上限时自动提交并累加计数", async () => {
const ctx = loadContentPage({ html: LOGIN_HTML, configs: [makeConfig({ autoSubmit: true })] });
let clicks = 0;
ctx.doc.getElementById("btn").addEventListener("click", () => clicks++);
await tick(AFTER_SUBMIT);
assert.equal(ctx.doc.getElementById("u").value, "auto-user");
assert.equal(clicks, 1);
assert.ok(ctx.chrome.state.calls.includes("incrementDomainAttempts"));
});
it("达到上限后只填充、不自动提交,并给出可见提示", async () => {
const ctx = loadContentPage({
html: LOGIN_HTML,
configs: [makeConfig({ autoSubmit: true })],
attempts: { "env1::example.com": { count: 3, lastAttempt: Date.now() } },
});
let clicks = 0;
ctx.doc.getElementById("btn").addEventListener("click", () => clicks++);
await tick(AFTER_SUBMIT);
assert.equal(ctx.doc.getElementById("u").value, "auto-user", "仍然填充");
assert.equal(clicks, 0, "不应自动提交");
assert.ok(!ctx.chrome.state.calls.includes("incrementDomainAttempts"));
// 用户必须能看懂"为什么只填不提交",否则会以为扩展坏了
assert.ok(ctx.doc.body.innerHTML.includes("attemptToastTitle"), "应显示暂停提示浮窗");
});
it("命中配置但页面上没有登录框时不消耗计数(浏览站内页面不会被误扣)", async () => {
const ctx = loadContentPage({
html: `<div>登录成功后的站内页面</div>`,
configs: [makeConfig({ autoSubmit: true })],
timing: { fillTimeoutMs: 120 },
});
await tick(500);
assert.ok(!ctx.chrome.state.calls.includes("incrementDomainAttempts"), "没有提交就不应计数");
assert.ok(ctx.chrome.state.calls.includes("resetDomainAttempts"), "始终没有登录框 → 清掉旧计数");
});
it("只有真正触发提交之后才累加计数", async () => {
const ctx = loadContentPage({ html: LOGIN_HTML, configs: [makeConfig({ autoSubmit: true })] });
ctx.doc.getElementById("btn").addEventListener("click", () => {});
await tick(150);
assert.ok(!ctx.chrome.state.calls.includes("incrementDomainAttempts"), "提交前不应计数");
await tick(300); // 提交延迟 300ms
assert.ok(ctx.chrome.state.calls.includes("incrementDomainAttempts"), "提交后才计数");
});
it("提交后登录框消失视为登录成功并重置计数(SPA 场景)", async () => {
const ctx = loadContentPage({
html: LOGIN_HTML,
configs: [makeConfig({ autoSubmit: true })],
timing: { submitResultCheckMs: 120 },
});
ctx.doc.getElementById("btn").addEventListener("click", () => {});
await tick(330); // 300ms 时已提交
assert.ok(ctx.chrome.state.calls.includes("incrementDomainAttempts"), "提交后先计数");
// 模拟 SPA 登录成功:登录表单从页面上消失
ctx.doc.querySelector("form").remove();
await tick(160);
assert.ok(ctx.chrome.state.calls.includes("resetDomainAttempts"), "登录成功应重置计数");
});
it("提交后登录框仍在(登录失败)不重置计数", async () => {
const ctx = loadContentPage({
html: LOGIN_HTML,
configs: [makeConfig({ autoSubmit: true })],
timing: { submitResultCheckMs: 120 },
});
ctx.doc.getElementById("btn").addEventListener("click", () => {});
await tick(560); // 覆盖 300ms 提交 + 120ms 结果观察
assert.ok(ctx.chrome.state.calls.includes("incrementDomainAttempts"));
assert.ok(!ctx.chrome.state.calls.includes("resetDomainAttempts"), "登录框仍在,视为失败,不应重置");
});
it("被禁用的配置不再阻止计数重置", async () => {
const ctx = loadContentPage({
url: "https://example.com/login/page",
html: `<div id="app"></div>`,
configs: [
makeConfig({ id: "a", domain: "example.com/login/*" }),
makeConfig({ id: "b", domain: "example.com", enabled: false }),
],
});
// 从 /login/page 跳到 /dashboard:只有被禁用的那条还匹配旧地址
ctx.dom.reconfigure({ url: "https://example.com/dashboard" });
ctx.doc.getElementById("app").textContent = "route changed"; // 触发 MutationObserver
await tick(80);
assert.ok(ctx.chrome.state.calls.includes("resetDomainAttempts"), "启用中的配置离开匹配范围应重置");
});
it("失败计数超过冷却时间后重新计数并允许提交", async () => {
const ctx = loadContentPage({
html: LOGIN_HTML,
configs: [makeConfig({ autoSubmit: true })],
attempts: { "env1::example.com": { count: 3, lastAttempt: Date.now() - 10 * 60 * 1000 } },
});
let clicks = 0;
ctx.doc.getElementById("btn").addEventListener("click", () => clicks++);
await tick(AFTER_SUBMIT);
assert.equal(clicks, 1, "冷却后应重新允许提交");
});
});
describe("content - 多环境命中", () => {
const LOGIN_HTML = `<form>
<input type="text" name="username" id="u">
<input type="password" id="p">
</form>`;
it("多个环境命中时弹出选择浮窗,点击后填充对应账号", async () => {
const ctx = loadContentPage({
html: LOGIN_HTML,
configs: [
makeConfig({ id: "c1", envId: "e1", envName: "个人", username: "personal@x.com" }),
makeConfig({ id: "c2", envId: "e2", envName: "生产", username: "prod@x.com" }),
],
});
await tick(60);
const picker = ctx.doc.getElementById("auto-login-env-picker");
assert.ok(picker, "应出现多环境选择浮窗");
assert.equal(ctx.doc.getElementById("u").value, "", "选择前不填充");
const buttons = [...picker.querySelectorAll("button")];
const prodBtn = buttons.find((b) => b.textContent.includes("生产"));
prodBtn.dispatchEvent(new ctx.win.MouseEvent("click", { bubbles: true }));
await tick(60);
assert.equal(ctx.doc.getElementById("u").value, "prod@x.com");
assert.equal(ctx.doc.getElementById("auto-login-env-picker"), null, "选择后浮窗关闭");
});
it("点击取消关闭浮窗且不填充", async () => {
const ctx = loadContentPage({
html: LOGIN_HTML,
configs: [makeConfig({ id: "c1" }), makeConfig({ id: "c2", envId: "e2", envName: "生产" })],
});
await tick(60);
const picker = ctx.doc.getElementById("auto-login-env-picker");
const cancelBtn = [...picker.querySelectorAll("button")].pop();
cancelBtn.dispatchEvent(new ctx.win.MouseEvent("click", { bubbles: true }));
assert.equal(ctx.doc.getElementById("auto-login-env-picker"), null);
assert.equal(ctx.doc.getElementById("u").value, "");
});
});
describe("content - SPA 延迟渲染", () => {
it("表单晚于脚本出现时,重试后仍能填充", async () => {
const ctx = loadContentPage({
html: `<div id="app"></div>`,
configs: [makeConfig()],
});
await tick(80);
assert.equal(ctx.doc.querySelectorAll("input").length, 0, "此时还没有表单");
// 模拟 SPA 前端稍后渲染出登录表单
ctx.doc.getElementById("app").innerHTML = `<form>
<input type="text" name="username" id="u">
<input type="password" id="p">
</form>`;
await tick(700);
assert.equal(ctx.doc.getElementById("u").value, "auto-user");
});
it("Shadow DOM 内部后渲染的登录框也能填充(light DOM 完全没变)", async () => {
let shadow;
loadContentPage({
html: `<div id="host"></div>`,
configs: [makeConfig()],
setup: (win, doc) => {
shadow = doc.getElementById("host").attachShadow({ mode: "open" });
},
});
// 首轮扫描:发现空的 shadow root 并开始监听它
await tick(80);
assert.equal(shadow.querySelectorAll("input").length, 0);
// 只改 shadow 内部,body 上的 MutationObserver 感知不到这种变化
shadow.innerHTML = `<form>
<input type="text" name="username" id="u">
<input type="password" id="p">
</form>`;
await tick(700);
assert.equal(shadow.getElementById("u").value, "auto-user");
});
});
describe("content - iframe 内的登录表单(allFrames)", () => {
const FORM_HTML = `<form>
<input type="text" name="username" id="u">
<input type="password" id="p">
</form>`;
/** 在页面里创建同源 iframe,并按 allFrames 的方式往帧内注入脚本 */
function injectIntoFrame(ctx, html) {
const iframe = ctx.doc.createElement("iframe");
ctx.doc.body.appendChild(iframe);
const iwin = iframe.contentWindow;
const idoc = iframe.contentDocument;
idoc.body.innerHTML = html;
patchLayout(iwin); // iframe 是独立的 realm,需要单独打布局补丁
iwin.chrome = ctx.chrome.api;
for (const code of [UTILS_CODE, CONTENT_CODE]) {
const el = idoc.createElement("script");
el.textContent = code;
idoc.head.appendChild(el);
}
return { iwin, idoc };
}
it("iframe 内的表单能被填充(按 iframe 自己的地址匹配)", async () => {
// iframe 在 jsdom 里是 about:blank,用通配规则让它命中;顶层帧没有表单所以不会误填
const ctx = loadContentPage({ html: `<div id="top"></div>`, configs: [makeConfig({ domain: "*" })] });
const { idoc } = injectIntoFrame(ctx, FORM_HTML);
await tick(150);
assert.equal(idoc.getElementById("u").value, "auto-user");
assert.equal(idoc.getElementById("p").value, "auto-pass");
});
it("子帧命中多个环境时不弹浮窗,按当前激活环境填充(顶层帧照旧弹窗)", async () => {
const ctx = loadContentPage({
html: `<div id="top"></div>`,
configs: [
makeConfig({ id: "c1", envId: "e1", envName: "个人", domain: "*", username: "personal@x.com" }),
makeConfig({ id: "c2", envId: "e2", envName: "生产", domain: "*", username: "prod@x.com" }),
],
activeEnvId: "e2",
});
const { idoc } = injectIntoFrame(ctx, FORM_HTML);
await tick(150);
assert.equal(idoc.getElementById("u").value, "prod@x.com", "子帧应按激活环境选用凭据");
assert.equal(idoc.getElementById("auto-login-env-picker"), null, "子帧不弹浮窗");
assert.ok(ctx.doc.getElementById("auto-login-env-picker"), "顶层帧仍然弹浮窗");
});
it("子帧收到 manualFillMulti 时同样按激活环境填充", async () => {
const ctx = loadContentPage({
html: `<div id="top"></div>`,
configs: [
makeConfig({ id: "c1", envId: "e1", domain: "*", username: "personal@x.com" }),
makeConfig({ id: "c2", envId: "e2", domain: "*", username: "prod@x.com" }),
],
activeEnvId: "e2",
});
const { idoc } = injectIntoFrame(ctx, FORM_HTML);
// 最后注册的监听器来自最内层帧
const listeners = ctx.chrome.state.messageListeners;
const frameListener = listeners[listeners.length - 1];
frameListener({ action: "manualFillMulti", configs: [...ctx.chrome.state.configs] }, {}, () => {});
await tick(150);
assert.equal(idoc.getElementById("u").value, "prod@x.com");
assert.equal(idoc.getElementById("auto-login-env-picker"), null);
});
it("pickConfigForFrame:优先当前激活环境,其次第一条", () => {
const ctx = loadContentPage({ html: "" });
const a = makeConfig({ id: "a", envId: "e1" });
const b = makeConfig({ id: "b", envId: "e2" });
assert.equal(ctx.win.pickConfigForFrame([a, b], "e2").id, "b");
assert.equal(ctx.win.pickConfigForFrame([a, b], "e9").id, "a");
assert.equal(ctx.win.pickConfigForFrame([a, b], null).id, "a");
});
});
describe("content - 手动填充消息", () => {
const LOGIN_HTML = `<form>
<input type="text" name="username" id="u">
<input type="password" id="p">
</form>`;
it("manualFill 填充指定配置并回包 success", async () => {
const ctx = loadContentPage({ html: LOGIN_HTML });
let response = null;
ctx.chrome.state.messageListener(
{ action: "manualFill", config: makeConfig({ username: "manual@x.com" }) },
{},
(r) => {
response = r;
},
);
await tick(30);
// 注意:response 由 jsdom realm 创建,跨 realm 不能用 deepEqual(原型不同)
assert.equal(response.success, true);
assert.equal(ctx.doc.getElementById("u").value, "manual@x.com");
});
it("manualFill 找不到表单时回包诊断信息", async () => {
const ctx = loadContentPage({ html: `<div>没有表单</div>` });
let response = null;
ctx.chrome.state.messageListener({ action: "manualFill", config: makeConfig() }, {}, (r) => {
response = r;
});
await tick(30);
assert.equal(response.success, false);
assert.equal(response.diag.total, 0);
assert.equal(response.diag.forms, 0);
});
it("manualFillMulti 弹出多环境浮窗", async () => {
const ctx = loadContentPage({ html: LOGIN_HTML });
let response = null;
ctx.chrome.state.messageListener(
{
action: "manualFillMulti",
configs: [makeConfig({ id: "c1" }), makeConfig({ id: "c2", envId: "e2", envName: "生产" })],
},
{},
(r) => {
response = r;
},
);
await tick(30);
assert.equal(response.success, true);
assert.ok(ctx.doc.getElementById("auto-login-env-picker"));
});
});