@mrrisega/dsh-remote 0.7.10-beta.3 → 0.7.11-beta.3
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 +214 -1
- 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/wecom-bot.test.mjs +1438 -0
- package/clients/dsh-remote/wecom-bot.mjs +2444 -0
- package/dsh-setup.mjs +18 -3
- package/package.json +2 -2
- package/packages/dsh-remote-web/lib/client.js +1048 -223
- package/packages/dsh-remote-web/lib/index.js +299 -2
- 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 +1 -1
- 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 +359 -0
- package/packages/dsh-remote-web/test/lib-panel-ia.mjs +98 -0
- package/packages/dsh-remote-web/test/login-selfheal-ui.test.mjs +13 -5
- package/packages/dsh-remote-web/test/panel-ia.test.mjs +798 -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 +10 -1
- package/packages/dsh-remote-web/test/remote-access-ui.test.mjs +20 -8
- package/packages/dsh-remote-web/test/settings-entry.test.mjs +30 -21
- package/packages/dsh-remote-web/test/sms-captcha-ui.test.mjs +15 -2
- package/packages/dsh-remote-web/test/telemetry.test.mjs +5 -3
- package/packages/dsh-remote-web/test/wechat-bot-ui.test.mjs +57 -130
|
@@ -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 播报位");
|