@mrrisega/dsh-remote 0.7.10-beta.3 → 0.7.12-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/clients/dsh-remote/agent/README.md +412 -0
- package/clients/dsh-remote/agent/agent-e2e-codex.mjs +185 -0
- package/clients/dsh-remote/agent/agent-e2e-real.mjs +443 -0
- package/clients/dsh-remote/agent/codex.mjs +736 -0
- package/clients/dsh-remote/agent/detect.mjs +399 -0
- package/clients/dsh-remote/agent/events.mjs +511 -0
- package/clients/dsh-remote/agent/gemini.mjs +503 -0
- package/clients/dsh-remote/agent/index.mjs +339 -0
- package/clients/dsh-remote/agent/opencode.mjs +628 -0
- package/clients/dsh-remote/agent/permission-bridge.mjs +534 -0
- package/clients/dsh-remote/agent/proc.mjs +179 -0
- package/clients/dsh-remote/agent/version.mjs +148 -0
- package/clients/dsh-remote/approval-coordinator.mjs +150 -0
- package/clients/dsh-remote/decision-poller.mjs +235 -0
- package/clients/dsh-remote/dsh-bridge.mjs +220 -1
- package/clients/dsh-remote/im-format.mjs +372 -0
- package/clients/dsh-remote/push-reporter.mjs +54 -0
- package/clients/dsh-remote/test/agent-codex.test.mjs +535 -0
- package/clients/dsh-remote/test/agent-detect.test.mjs +390 -0
- package/clients/dsh-remote/test/agent-events.test.mjs +340 -0
- package/clients/dsh-remote/test/agent-gemini.test.mjs +355 -0
- package/clients/dsh-remote/test/agent-jsonl-driver.test.mjs +165 -0
- package/clients/dsh-remote/test/agent-opencode.test.mjs +369 -0
- package/clients/dsh-remote/test/agent-permission-bridge.test.mjs +340 -0
- package/clients/dsh-remote/test/agent-proc.test.mjs +175 -0
- package/clients/dsh-remote/test/agent-runtime.test.mjs +282 -0
- package/clients/dsh-remote/test/agent-version.test.mjs +122 -0
- package/clients/dsh-remote/test/approval-coordinator.test.mjs +192 -0
- package/clients/dsh-remote/test/decision-poller.test.mjs +328 -0
- package/clients/dsh-remote/test/im-format.test.mjs +229 -0
- package/clients/dsh-remote/test/upstream-discovery.test.mjs +203 -2
- package/clients/dsh-remote/test/wecom-bot.test.mjs +1718 -0
- package/clients/dsh-remote/upstream-discovery.mjs +495 -43
- package/clients/dsh-remote/wecom-bot.mjs +2670 -0
- package/dsh-setup.mjs +95 -9
- package/package.json +2 -2
- package/packages/dsh-remote-web/lib/client.js +1377 -292
- package/packages/dsh-remote-web/lib/index.js +419 -12
- package/packages/dsh-remote-web/package.json +1 -1
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/README.md +412 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/agent-e2e-codex.mjs +185 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/agent-e2e-real.mjs +443 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/codex.mjs +736 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/detect.mjs +399 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/events.mjs +511 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/gemini.mjs +503 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/index.mjs +339 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/opencode.mjs +628 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/permission-bridge.mjs +534 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/proc.mjs +179 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/version.mjs +148 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/approval-coordinator.mjs +150 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/decision-poller.mjs +235 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-bridge.mjs +258 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/push-reporter.mjs +533 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/wecom-bot.mjs +2444 -0
- package/packages/dsh-remote-web/runtime/dsh-setup.mjs +18 -3
- package/packages/dsh-remote-web/test/activation-entry.test.mjs +11 -1
- package/packages/dsh-remote-web/test/announcements-ui.test.mjs +8 -3
- package/packages/dsh-remote-web/test/connect-ui.test.mjs +16 -4
- package/packages/dsh-remote-web/test/im-bots.test.mjs +400 -0
- package/packages/dsh-remote-web/test/lib-panel-ia.mjs +98 -0
- package/packages/dsh-remote-web/test/login-fast.test.mjs +104 -0
- package/packages/dsh-remote-web/test/login-selfheal-ui.test.mjs +17 -6
- package/packages/dsh-remote-web/test/panel-ia.test.mjs +996 -0
- package/packages/dsh-remote-web/test/password-reset.test.mjs +11 -1
- package/packages/dsh-remote-web/test/plugin-link.test.mjs +78 -0
- package/packages/dsh-remote-web/test/purchase-agreement-ui.test.mjs +14 -5
- package/packages/dsh-remote-web/test/remote-access-ui.test.mjs +69 -119
- package/packages/dsh-remote-web/test/self-manage.test.mjs +5 -1
- package/packages/dsh-remote-web/test/settings-entry.test.mjs +42 -25
- package/packages/dsh-remote-web/test/sms-captcha-ui.test.mjs +15 -2
- package/packages/dsh-remote-web/test/telemetry.test.mjs +6 -4
- package/packages/dsh-remote-web/test/wechat-bot-ui.test.mjs +57 -130
|
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import test from "node:test";
|
|
4
4
|
import vm from "node:vm";
|
|
5
|
+
import { openEntry } from "./lib-panel-ia.mjs";
|
|
5
6
|
|
|
6
7
|
function walk(node, visit) {
|
|
7
8
|
if (!node || typeof node !== "object") return;
|
|
@@ -24,7 +25,9 @@ test("账号状态未返回前只显示加载态且不暴露配置路径", () =>
|
|
|
24
25
|
assert.match(source, /正在读取远控状态/);
|
|
25
26
|
assert.match(source, /st\.config\.deviceId/);
|
|
26
27
|
assert.doesNotMatch(source, /configPath/);
|
|
27
|
-
|
|
28
|
+
// IA 重做(2026-11-07):登录不再隐藏功能 —— 未登录时主面板照常显示,入口卡写「登录后可同步」。
|
|
29
|
+
// (旧的 `st !== null && !loggedIn` 内联三元结构已随账号卡迁入弹层而消失,改锁新契约。)
|
|
30
|
+
assert.match(source, /登录后可同步/);
|
|
28
31
|
});
|
|
29
32
|
|
|
30
33
|
test("短信防刷要求图形验证码时,注册面板展示验证码并随重试提交", async () => {
|
|
@@ -97,7 +100,17 @@ test("短信防刷要求图形验证码时,注册面板展示验证码并随
|
|
|
97
100
|
// 面板入口已迁入设置页 settings.section 栏目(id=dsh-remote),不再有侧边栏入口/浮动面板
|
|
98
101
|
const section = registered.get("dsh-remote");
|
|
99
102
|
assert.ok(section, "settings.section 栏目组件应已注册");
|
|
100
|
-
|
|
103
|
+
let iaOpened = false;
|
|
104
|
+
const render = () => {
|
|
105
|
+
hook = 0;
|
|
106
|
+
let tree = section({ close() {} });
|
|
107
|
+
// IA 重做(2026-11-07):登录/注册在「账号」弹层里 —— 按真实用户路径点开入口卡。
|
|
108
|
+
if (!iaOpened) {
|
|
109
|
+
iaOpened = true;
|
|
110
|
+
if (openEntry(tree, "account")) { hook = 0; tree = section({ close() {} }); }
|
|
111
|
+
}
|
|
112
|
+
return tree;
|
|
113
|
+
};
|
|
101
114
|
|
|
102
115
|
states[0] = { config: { phone: "", deviceId: "dev-test" }, service: { running: false, plistExists: true } };
|
|
103
116
|
let tree = render();
|
|
@@ -808,13 +808,15 @@ test("面板可见性:关于卡片写明「匿名统计 + 关闭方式」,
|
|
|
808
808
|
// 0.6.18 面板重排(「加入交流群」搬进账号卡)之后更是会变成空串 → 断言全部平凡通过。
|
|
809
809
|
// 这是"静默失守":比变红更危险(它看起来一直是绿的)。
|
|
810
810
|
// 现在取片边界是「关于卡片开头 → renderHome 结尾」,并**显式断言切片非空且像样**。
|
|
811
|
-
|
|
811
|
+
// IA 重做(2026-11-07):关于卡不再用 card() 助手(标题图标改成内联 SVG,禁止 emoji)→
|
|
812
|
+
// 取片锚点改成"关于卡标题 → 弹层段开头",仍然要求切片非空且真的包含卡片本体。
|
|
813
|
+
const aboutStart = client.indexOf('h("h3", null, "关于 dsh-remote-web")');
|
|
812
814
|
assert.ok(aboutStart > 0, "必须能定位到「关于」卡片的开头");
|
|
813
|
-
const aboutEnd = client.indexOf("
|
|
814
|
-
assert.ok(aboutEnd > aboutStart, "
|
|
815
|
+
const aboutEnd = client.indexOf("// ── 弹层内容", aboutStart);
|
|
816
|
+
assert.ok(aboutEnd > aboutStart, "必须能定位到「关于」卡片的结束边界(弹层段开始之前)");
|
|
815
817
|
const about = client.slice(aboutStart, aboutEnd);
|
|
816
818
|
assert.ok(about.length > 400, "「关于」卡片切片不能是空串/碎片(空串会让下面四条断言全部平凡通过)");
|
|
817
|
-
assert.ok(about.includes("关于 dsh-remote"), "切片必须真的包含「关于」卡片本体");
|
|
819
|
+
assert.ok(about.includes("关于 dsh-remote-web"), "切片必须真的包含「关于」卡片本体");
|
|
818
820
|
// ⚠️ 断言的是**渲染出来的文案**:上面那条"不再单列隐私说明行"的解释性注释本身就写着这些词,
|
|
819
821
|
// 所以先剥掉整行注释再断言(注释不会出现在用户屏幕上,不该被当成"面板里放了链接")。
|
|
820
822
|
const aboutText = about.split("\n").filter((l) => !/^\s*\/\//.test(l)).join("\n");
|
|
@@ -29,6 +29,7 @@ import assert from "node:assert/strict";
|
|
|
29
29
|
import { readFileSync } from "node:fs";
|
|
30
30
|
import test from "node:test";
|
|
31
31
|
import vm from "node:vm";
|
|
32
|
+
import { openEntry, clickChannelRow, findDialog } from "./lib-panel-ia.mjs";
|
|
32
33
|
|
|
33
34
|
const SOURCE = readFileSync(new URL("../lib/client.js", import.meta.url), "utf8");
|
|
34
35
|
|
|
@@ -305,19 +306,17 @@ function loadPlugin(opts = {}) {
|
|
|
305
306
|
let tree = renderNode(root, "root");
|
|
306
307
|
renders += 1;
|
|
307
308
|
if (!entered) {
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
const
|
|
312
|
-
if (
|
|
313
|
-
entered = true;
|
|
314
|
-
tab.props.onClick();
|
|
309
|
+
// 真实用户路径(IA 重做 2026-11-07):主面板「通知与消息通道」入口卡 → 平台列表 →
|
|
310
|
+
// 「微信机器人(扫码绑定)」行 → 微信绑定面板。原来的顶部第三个 tab 已不存在。
|
|
311
|
+
entered = true;
|
|
312
|
+
const opened = openEntry(tree, "channel");
|
|
313
|
+
if (opened) {
|
|
315
314
|
tree = renderNode(root, "root");
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
assert.fail("
|
|
315
|
+
const went = clickChannelRow(tree, "微信机器人");
|
|
316
|
+
if (went) tree = renderNode(root, "root");
|
|
317
|
+
else assert.fail("通道弹层里必须有「微信机器人(扫码绑定)」这一行");
|
|
318
|
+
} else {
|
|
319
|
+
assert.fail("主面板必须有「通知与消息通道」入口卡(登录不再隐藏任何功能)");
|
|
321
320
|
}
|
|
322
321
|
}
|
|
323
322
|
return tree;
|
|
@@ -374,74 +373,37 @@ test("入口:settings.section 里**只有**「📱 远程访问」一个栏目
|
|
|
374
373
|
assert.equal(typeof dispose, "function", "setting.section 的 inject 回调必须返回 disposer");
|
|
375
374
|
});
|
|
376
375
|
|
|
377
|
-
test("tab 条(
|
|
376
|
+
test("通道弹层:微信机器人一行能进能出;主面板不再有顶部 tab 条(IA 重做 2026-11-07)", async () => {
|
|
378
377
|
const plugin = loadPlugin();
|
|
379
|
-
let tree = await plugin.settle(); //
|
|
380
|
-
//
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
// 绿泡泡图标:装饰性 span.dru-wx-ico + aria-hidden(可访问名字来自文字标签)
|
|
391
|
-
const ico = find(tree, (n) => n.props?.className === "dru-wx-ico");
|
|
392
|
-
assert.ok(ico, "微信 tab 上必须有 .dru-wx-ico 绿泡泡图标");
|
|
393
|
-
assert.equal(ico.props["aria-hidden"], "true", "图标是装饰性的 → aria-hidden");
|
|
394
|
-
assert.equal(ico.type, "span");
|
|
395
|
-
assert.ok((ico.children || []).some((c) => typeof c === "string" && c.includes("💬")), "图标里应有 💬 气泡字符");
|
|
396
|
-
|
|
397
|
-
// 选中态:微信 tab active / aria-selected=true,两个模式 tab 都不 active(aria 与视觉同源)
|
|
398
|
-
const wxTab = tabs[2];
|
|
399
|
-
assert.match(wxTab.props.className, /dru-tab active/, "在微信视图里微信 tab 必须是 active");
|
|
400
|
-
assert.equal(wxTab.props["aria-selected"], "true");
|
|
401
|
-
assert.equal(tabs[0].props["aria-selected"], "false", "模式 tab 在微信视图里不得选中");
|
|
402
|
-
assert.equal(tabs[1].props["aria-selected"], "false");
|
|
403
|
-
assert.equal(tabs[0].props.tabIndex, 0, "tab 必须可聚焦(键盘可操作)");
|
|
404
|
-
assert.equal(wxTab.props["aria-selected"], "true", "aria-selected 与 .active 同源");
|
|
405
|
-
|
|
406
|
-
// 微信视图里没有第二个 .dru-settings-section 套娃(内嵌态省掉外壳)
|
|
407
|
-
assert.equal(tree.props.className, "dru-settings-section", "面板根仍是唯一的 .dru-settings-section");
|
|
408
|
-
assert.equal(find(tree, (n) => n.props?.className === "dru-settings-section" && n !== tree), undefined,
|
|
409
|
-
"内嵌态不得再嵌一层 .dru-settings-section");
|
|
410
|
-
|
|
411
|
-
// 出得去:点「🖥 自建服务」→ 回到 home(模式 tab 点击必须把 view 复位成 home)
|
|
412
|
-
tabs[1].props.onClick();
|
|
413
|
-
tree = plugin.render();
|
|
414
|
-
assert.equal(find(tree, (n) => n.props?.className === "dru-wx-embed"), undefined,
|
|
415
|
-
"点模式 tab 后必须离开微信视图(微信内容不再渲染)");
|
|
416
|
-
const modeTab = find(tree, (n) => n.props?.role === "tab" && (n.children || []).includes("🖥 自建服务"));
|
|
417
|
-
assert.equal(modeTab.props["aria-selected"], "true", "点自建服务后它应成为选中态(且 view 回到 home)");
|
|
418
|
-
assert.equal(find(tree, (n) => n.props?.role === "tab" && (n.children || []).some((c) => typeof c === "string" && c.includes("微信机器人通道"))).props["aria-selected"], "false",
|
|
419
|
-
"离开微信视图后微信 tab 不得再是选中态");
|
|
420
|
-
// 进得去:再点回微信 tab
|
|
421
|
-
const back = find(tree, (n) => n.props?.role === "tab" && (n.children || []).some((c) => typeof c === "string" && c.includes("微信机器人通道")));
|
|
378
|
+
let tree = await plugin.settle(); // 首次渲染自动走「通道入口 → 微信机器人行」
|
|
379
|
+
// 主面板不再有顶部 tab 条(旧结构:"云端服务 / 自建服务 / 微信机器人通道")
|
|
380
|
+
assert.equal(find(tree, (n) => n.props?.className === "dru-tabs"), undefined,
|
|
381
|
+
"主面板不得再有顶部 tab 条(已降级为弹层内的分段控件)");
|
|
382
|
+
// 微信绑定面板在弹层里(role=dialog),且弹层标题是「微信机器人(扫码绑定)」
|
|
383
|
+
const dlg = findDialog(tree);
|
|
384
|
+
assert.ok(dlg, "微信绑定内容必须在弹层里(role=dialog)");
|
|
385
|
+
assert.ok(textHas(tree, "微信机器人(扫码绑定)"), "弹层标题应是「微信机器人(扫码绑定)」");
|
|
386
|
+
// 出得去:点「返回平台列表」→ 弹层回到平台列表(不再是"进了就出不来"的 tab 视图)
|
|
387
|
+
const back = find(tree, (n) => n.type === "button" && (n.children || []).some((c) => typeof c === "string" && c.includes("返回平台列表")));
|
|
388
|
+
assert.ok(back, "弹层里必须有「返回平台列表」出口");
|
|
422
389
|
back.props.onClick();
|
|
423
|
-
|
|
424
|
-
assert.ok(textHas(
|
|
425
|
-
assert.equal(find(again, (n) => n.props?.className === "dru-wx-ico") ? true : false, true, "tab 条(含绿泡泡)仍在");
|
|
390
|
+
tree = await plugin.settle();
|
|
391
|
+
assert.ok(textHas(tree, "企业微信智能机器人"), "返回后应是平台列表(能进也能出)");
|
|
426
392
|
});
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
assert.match(SOURCE,
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
assert.match(SOURCE, /"微信机器人通道"/, "tab 必须有真实文字标签(不靠颜色/图标单独表意)");
|
|
434
|
-
// 图标 = 一个 emoji 字符 + 一条 CSS 规则,不加载任何外部资源(字体/图片/脚本/样式)
|
|
393
|
+
test("图标:新 IA 一律内联 SVG(不用 emoji 当图标);零外部资源/零依赖不变量不变", () => {
|
|
394
|
+
// 业主 UX 规范:图标必须内联 SVG 16px currentColor,**禁止 emoji 当图标**
|
|
395
|
+
assert.match(SOURCE, /function druIcon\(name, size\)/, "必须有统一的 SVG 图标函数");
|
|
396
|
+
assert.match(SOURCE, /dangerouslySetInnerHTML: \{ __html: inner \}/, "图标以 inline SVG 渲染(不是 emoji/图片)");
|
|
397
|
+
assert.match(SOURCE, /"aria-hidden": "true", focusable: "false"/, "装饰性图标必须 aria-hidden");
|
|
398
|
+
// 零外部资源:不 @import、不引远程 url、不注入 link/script、不引 QR 库
|
|
435
399
|
assert.doesNotMatch(SOURCE, /@import/, "不得 @import 外部样式");
|
|
436
400
|
assert.doesNotMatch(SOURCE, /url\(\s*["']?https?:/, "CSS 里不得引用远程 url(...)");
|
|
437
401
|
assert.doesNotMatch(SOURCE, /createElement\("link"\)|createElement\('link'\)/, "不得注入外部 <link>");
|
|
438
|
-
assert.doesNotMatch(SOURCE, /createElement\("script"\)|createElement\('script'\)/, "不得注入外部 <script>");
|
|
439
402
|
assert.doesNotMatch(SOURCE, /qrcodejs|qrcode-generator|davidshimjs|jsqr|nayuki|qrcode\.min\.js/i, "不得引入 QR 库");
|
|
440
|
-
//
|
|
403
|
+
// 依赖侧同样零新增
|
|
441
404
|
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
442
405
|
assert.deepEqual(pkg.dependencies || {}, {}, "dsh-remote-web 不得有运行时依赖(零依赖不变量)");
|
|
443
406
|
});
|
|
444
|
-
|
|
445
407
|
test("主题不变量:根节点带 .dru-settings-section + data-dru-theme;新增样式只引用令牌、不落 :root/body", async () => {
|
|
446
408
|
const plugin = loadPlugin();
|
|
447
409
|
const tree = await plugin.settle();
|
|
@@ -533,69 +495,29 @@ test("首屏导览:未绑定时就在最上面(DOM 顺序在「连接微信
|
|
|
533
495
|
assert.ok(textHas(boundTree, "已绑定"));
|
|
534
496
|
});
|
|
535
497
|
|
|
536
|
-
test("
|
|
537
|
-
const plugin = loadPlugin(
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
//
|
|
541
|
-
assert.
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
assert.deepEqual(labelsOf(tree), ["☁️ 云端服务", "🖥 自建服务"], "未登录时主 tab 条只剩两个模式 tab");
|
|
548
|
-
assert.equal(find(tree, (n) => n.props?.className === "dru-wx-embed"), undefined, "未登录时不得渲染微信内容");
|
|
549
|
-
// 不是空面板:账号/登录区就在 home
|
|
550
|
-
assert.ok(textHas(tree, "登录"), "未登录时应看到登录/账号区(不是空白)");
|
|
551
|
-
|
|
552
|
-
// 登录(/status 带上 config.phone)→ 不刷新页面,tab 当场出现
|
|
553
|
-
plugin.setSignedIn(true);
|
|
554
|
-
tree = await plugin.settle();
|
|
555
|
-
const tab = plugin.weChatTabOf(tree);
|
|
556
|
-
assert.ok(tab, "登录成功后微信 tab 必须自己出现(不需要刷新页面)");
|
|
557
|
-
assert.deepEqual(labelsOf(tree), ["☁️ 云端服务", "🖥 自建服务", "微信机器人通道"], "登录后 tab 条恢复三个 tab");
|
|
558
|
-
assert.ok(find(tree, (n) => n.props?.className === "dru-wx-ico"), "微信绿泡泡图标照旧");
|
|
559
|
-
|
|
560
|
-
// 且点得进去、内容正常
|
|
561
|
-
tab.props.onClick();
|
|
562
|
-
tree = await plugin.settle();
|
|
563
|
-
assert.ok(find(tree, (n) => n.props?.className === "dru-wx-embed"), "登录后点 tab 应进入微信机器人内容");
|
|
564
|
-
assert.ok(textHas(tree, "未绑定") || textHas(tree, "已绑定"), "内容读到状态(不是空面板)");
|
|
498
|
+
test("登录不再隐藏功能(2026-11-07 业主批评根治):未登录也能看到通道入口,进弹层后说明登录后才能绑定", async () => {
|
|
499
|
+
const plugin = loadPlugin();
|
|
500
|
+
plugin.setSignedIn(false);
|
|
501
|
+
const tree = await plugin.settle();
|
|
502
|
+
// ① 未登录时主面板照常渲染入口网格(不再隐藏整个功能)
|
|
503
|
+
assert.ok(textHas(tree, "通知与消息通道"), "未登录也必须有「通知与消息通道」入口卡");
|
|
504
|
+
// ② 通道弹层里微信一行仍在;点进去 → 说明要登录(而不是整块消失)
|
|
505
|
+
assert.ok(textHas(tree, "微信机器人(扫码绑定)"), "未登录也看得到微信通道这一行");
|
|
506
|
+
assert.ok(textHas(tree, "登录后可绑定微信机器人"), "微信步骤页要明确说明「登录后可绑定」");
|
|
507
|
+
const goLogin = find(tree, (n) => n.type === "button" && (n.children || []).some((c) => typeof c === "string" && c.includes("去登录")));
|
|
508
|
+
assert.ok(goLogin, "给一个「去登录」的出口(不再让人自己找)");
|
|
565
509
|
});
|
|
566
|
-
|
|
567
|
-
test("登录门(防御):微信 tab 开着时会话失效 → 不渲染微信内容、回落到 home,绝不留空面板", async () => {
|
|
510
|
+
test("登录门(防御):微信步骤会话失效 → 只显示登录提示,绝不渲染绑定内容或空白面板", async () => {
|
|
568
511
|
const plugin = loadPlugin();
|
|
569
|
-
let tree = await plugin.settle();
|
|
570
|
-
assert.ok(
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
// 会话在 tab 开着的时候过期:/status 不再带 config.phone(登出 / 被踢下线 / 换账号)
|
|
512
|
+
let tree = await plugin.settle();
|
|
513
|
+
assert.ok(textHas(tree, "连接微信机器人") || textHas(tree, "已绑定") || textHas(tree, "未绑定"), "登录态下应是真绑定面板");
|
|
514
|
+
// 会话中途失效(下一帧 /status 不再带 config.phone)
|
|
574
515
|
plugin.setSignedIn(false);
|
|
575
|
-
plugin.
|
|
576
|
-
tree
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
assert.equal(find(tree, (n) => n.props?.className === "dru-wx-embed"), undefined, "登出后不得再渲染微信内容体");
|
|
580
|
-
assert.equal(find(tree, (n) => n.props?.role === "region" && n.props?.["aria-label"] === "微信机器人通道"), undefined,
|
|
581
|
-
"登出后不得再渲染「微信机器人通道」region");
|
|
582
|
-
// ② 也不是空白:已经回落到 home(账号/登录卡 + 主 tab 条都在)
|
|
583
|
-
assert.ok(find(tree, (n) => n.props?.role === "tablist"), "应回到 home 的 tab 条");
|
|
584
|
-
assert.ok(textHas(tree, "登录") && textHas(tree, "注册"), "home 的账号卡(登录/注册)应在场,不留空面板");
|
|
585
|
-
assert.ok(textHas(tree, "后台服务") || textHas(tree, "远程访问"), "home 的其它卡片也应在场");
|
|
586
|
-
// ③ 微信 tab 自身也消失(登录门一致)
|
|
587
|
-
assert.equal(plugin.weChatTabOf(tree), undefined, "登出后微信 tab 必须消失");
|
|
588
|
-
|
|
589
|
-
// ④ 重新登录:user 不用刷新页面就能回到微信 tab
|
|
590
|
-
plugin.setSignedIn(true);
|
|
591
|
-
tree = await plugin.settle();
|
|
592
|
-
const tab = plugin.weChatTabOf(tree);
|
|
593
|
-
assert.ok(tab, "重新登录后微信 tab 应再次出现");
|
|
594
|
-
tab.props.onClick();
|
|
595
|
-
tree = await plugin.settle();
|
|
596
|
-
assert.ok(find(tree, (n) => n.props?.className === "dru-wx-embed"), "重新登录后可以正常再进微信 tab");
|
|
516
|
+
tree = await plugin.settle(4);
|
|
517
|
+
assert.ok(textHas(tree, "登录后可绑定微信机器人"), "失效后必须如实说明要登录(不留空面板)");
|
|
518
|
+
assert.equal(find(tree, (n) => n.props?.className === "dru-wx-embed"), undefined, "不得继续渲染绑定内容");
|
|
519
|
+
assert.ok(findDialog(tree), "弹层仍然可用(用户可以关掉或去登录)");
|
|
597
520
|
});
|
|
598
|
-
|
|
599
521
|
test("动效引导(未绑定):连接按钮带 .dru-wx-attn、装饰点 aria-hidden;样式表有 prefers-reduced-motion 关闭规则;绑定后消失", async () => {
|
|
600
522
|
const plugin = loadPlugin();
|
|
601
523
|
let tree = await plugin.settle();
|
|
@@ -864,7 +786,12 @@ test("无障碍:真 <button>、状态区 aria-live 播报、按钮有文字标
|
|
|
864
786
|
assert.ok(btns.length >= 2, "至少要有「连接微信机器人」与「刷新状态」两个真按钮");
|
|
865
787
|
for (const b of btns) {
|
|
866
788
|
assert.equal(b.props.type, "button", "必须是真 <button type=button>(不是可点的 div)");
|
|
867
|
-
|
|
789
|
+
// 可访问名来自「可见文字(含子树里的 span)」或「aria-label」——图标按钮必须给 aria-label。
|
|
790
|
+
let deepText = "";
|
|
791
|
+
walk(b, (n) => { for (const c of n.children || []) if (typeof c === "string") deepText += c; });
|
|
792
|
+
assert.ok(
|
|
793
|
+
deepText.trim() || (b.props["aria-label"] && String(b.props["aria-label"]).trim()),
|
|
794
|
+
"按钮必须有可见文字或 aria-label(图标按钮必须给 aria-label):class=" + String(b.props?.className || ""));
|
|
868
795
|
}
|
|
869
796
|
const live = find(tree, (n) => n.props && n.props["aria-live"] === "polite" && n.props.role === "status");
|
|
870
797
|
assert.ok(live, "状态变化要有 aria-live 播报位");
|