@szc-ft/mcp-szcd-client 0.27.3 → 0.28.0
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/lib/ant-adapter.js +716 -0
- package/lib/browser-engine.js +898 -123
- package/lib/expect.js +392 -0
- package/lib/test-plan.js +48 -0
- package/local-browser-executor.js +172 -2
- package/opencode-extension/skills/local-browser-test/SKILL.md +182 -0
- package/opencode-extension/skills/local-browser-test/local-browser-executor.cjs +15 -0
- package/package.json +1 -1
- package/qwen-extension/qwen-extension.json +1 -1
- package/qwen-extension/skills/local-browser-test/SKILL.md +182 -0
- package/qwen-extension/skills/local-browser-test/local-browser-executor.cjs +15 -0
- package/standard-skill/local-browser-test/SKILL.md +182 -0
- package/standard-skill/local-browser-test/local-browser-executor.cjs +15 -0
- package/opencode-extension/skills/local-browser-test/local-browser-executor-old.cjs +0 -395
- package/qwen-extension/skills/local-browser-test/local-browser-executor-old.cjs +0 -395
|
@@ -95,6 +95,7 @@ AI 直接构造测试计划 JSON:
|
|
|
95
95
|
| findFrame | 查找微前端 iframe | `urlIncludes` 或 `urlRegex` 或 `titleIncludes` 或 `contentIncludes` 或 `frameIndex` | `{ found, frameUrl, matchedBy, totalFrames }` |
|
|
96
96
|
| loginWait | 等待 SSO 登录完成 | `loginSelector`, `timeout`, `interval` | `{ loggedIn, elapsed, currentUrl }` |
|
|
97
97
|
| aiAssert | 语义断言(截图+审查) | `assertion`, `filename` | `{ screenshotPath, assertion, needsVisualReview }` |
|
|
98
|
+
| runTask | 高层任务 runner(批量动态菜单扫描) | `task: "menu-api-scan"`, `menuApiPattern`, `menuLimit`, `menuPathPrefix[]`, `menuFilter[]`, `drawerTriggerSelector`, `useNativeHover`, `apiCaptureOptions`, `continueOnFail` | `{ task, discovered, attempted, byOutcome, results[] }` |
|
|
98
99
|
|
|
99
100
|
### 场景选择策略
|
|
100
101
|
|
|
@@ -488,3 +489,184 @@ cd ~/.szcd-mcp/deps && npm install --registry=https://registry.npmmirror.com --@
|
|
|
488
489
|
"outputDir": "/tmp/browser-test-xxx"
|
|
489
490
|
}
|
|
490
491
|
```
|
|
492
|
+
|
|
493
|
+
## 批量动态菜单扫描(runTask)
|
|
494
|
+
|
|
495
|
+
适用场景:测试系统有 N 条动态路由(来自后端菜单接口),要逐条点击触发并验证接口是否返回 200。6/18 反馈中,AI 最终用 130 余条 `act --menu-path` + `api-capture` 手工组合走完 129 条菜单——`runTask` 把这套流水固化为单步骤。
|
|
496
|
+
|
|
497
|
+
工作流:
|
|
498
|
+
|
|
499
|
+
1. **第一步:先抓菜单接口**(一次即可,作为整张菜单树的来源)
|
|
500
|
+
```bash
|
|
501
|
+
node local-browser-executor.js --action api-capture \
|
|
502
|
+
--url-pattern "listUserViewMenus" \
|
|
503
|
+
--include-response-body \
|
|
504
|
+
--include-all-targets \
|
|
505
|
+
--reload --wait 8000 \
|
|
506
|
+
--output /tmp/menu-tree.json
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
2. **第二步:把菜单树喂给 runTask**
|
|
510
|
+
```bash
|
|
511
|
+
node local-browser-executor.js --action run-task \
|
|
512
|
+
--task menu-api-scan \
|
|
513
|
+
--capture-input-file /tmp/menu-tree.json \
|
|
514
|
+
--menu-path-prefix /catalog \
|
|
515
|
+
--menu-limit 20 \
|
|
516
|
+
--drawer-trigger-selector "[data-test=menu-toggle]" \
|
|
517
|
+
--use-native-hover true \
|
|
518
|
+
--max-retry-per-step 2 \
|
|
519
|
+
--wait 4000 \
|
|
520
|
+
--output /tmp/menu-scan-report.json
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
3. **结果解读**:`byOutcome` 给出四档统计:
|
|
524
|
+
- `apiOk`:菜单点开 + 接口 200 ✅
|
|
525
|
+
- `apiFailed`:菜单点开 + 接口 ≥400(看 `results[i].api.requests` 找具体 URL)
|
|
526
|
+
- `noApi`:菜单点开但没产生任何 XHR(可能是纯静态页或路由没触发请求)
|
|
527
|
+
- `clickFailed`:菜单点不开(看 `results[i].menuResult.failedAt` + `candidates`)
|
|
528
|
+
|
|
529
|
+
4. **plan JSON 形式**(也可作为 `--plan-file` 一步:先 apiCapture 再 runTask):
|
|
530
|
+
```json
|
|
531
|
+
{
|
|
532
|
+
"steps": [
|
|
533
|
+
{ "type": "apiCapture", "urlPattern": "listUserViewMenus", "includeResponseBody": true, "reload": true, "wait": 8000, "includeAllTargets": true },
|
|
534
|
+
{ "type": "runTask", "task": "menu-api-scan", "menuLimit": 30, "drawerTriggerSelector": "[data-test=menu-toggle]" }
|
|
535
|
+
]
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
`runTask` 会自动从 `context.lastApiCapture` 拿菜单树,不需要再传文件。
|
|
539
|
+
|
|
540
|
+
## 反模式备忘(来自 6/17 + 6/18 实战反馈)
|
|
541
|
+
|
|
542
|
+
| 错误做法 | 正确做法 | 原因 |
|
|
543
|
+
|---------|---------|------|
|
|
544
|
+
| `setRequestInterception(true)` 抓接口 | 仅 CDP `Network.responseReceived` + `getResponseBody` | request interception 会拦下 wujie iframe 的 RPC 帧造成挂起 |
|
|
545
|
+
| `page.click("text=保存")` 不加可见性过滤 | 默认走 `act` 的 `text=`,已自动只匹配 `visible+enabled` 的元素 | 隐藏在收起菜单/未渲染抽屉里的同名文本会被错误命中 |
|
|
546
|
+
| 多级菜单只用一次 `dispatchEvent("mouseover")` | `useNativeHover: true`(默认) + `drawerTriggerSelector` 每级前重开 | Vue 的 v-on:mouseenter 对 dispatchEvent 不触发;抽屉式菜单点完一次会自动收起 |
|
|
547
|
+
| `setTimeout(awaitPage.bringToFront(), 30000)` 之外不做兜底 | 调用前自查"目标页是否还活着"(`findPage` / `_findFrame`),失败立刻退而求其次操作主帧 | bringToFront 在 OS 抢焦点失败时会无限挂起,没超时 |
|
|
548
|
+
| iframe attach 失败就报错退出 | `attachRetries` 默认 3 次、间隔 1.5s 重抓 `browser.targets()` | wujie 的 blob iframe target 在主页面 ready 后才注册,时序敏感 |
|
|
549
|
+
| 大批量探索每条都 throw | `_actMenuPath` 失败时返回 `acted:false + candidates`,配合 `runTask --continue-on-fail` | 探索性扫描遇到一条点不开就全部中断,没法发现整体面 |
|
|
550
|
+
| 手写 puppeteer 脚本绕过 `act` 操作 Ant Select | 用 `antSelect` 步骤(内置 mousedown + 轮询 + closeAllDropdowns + VERIFY) | Ant Design Select 必须 mousedown 触发,浮层在 body 末尾,选项异步渲染,单步 `act.evaluate` 无法覆盖四步组合 |
|
|
551
|
+
| 每个 Select 后忘记 `closeAllDropdowns` | `act` 自动检测上一次 antSelect/antTreeSelect 并预清理 | 下拉浮层残留会污染下一次 Select 操作,选项串选 |
|
|
552
|
+
| 硬编码 `sleep(15*200)` 等选项渲染 | `antSelect` 内置 `pollEval` 15×200ms 轮询,`expect(poll)` 可自定义条件 | 选项渲染时间受网络/数据量影响,固定 sleep 时机不可靠 |
|
|
553
|
+
| 表单校验用 ad-hoc evaluate 检查 | 用 `expect validation.toPass` 或 `formFill` 的 `validateAfter` | 语义断言自带 poll + 结构化错误信息(field + message),可直接进报告 |
|
|
554
|
+
|
|
555
|
+
## Ant Design 适配层(6/22 反馈落地)
|
|
556
|
+
|
|
557
|
+
把"触发器定位 → 浮层等待 → 选项交互 → VERIFY → 全局清理"固化为单步骤 JSON。
|
|
558
|
+
|
|
559
|
+
| type | 功能 | 关键参数 |
|
|
560
|
+
|------|------|---------|
|
|
561
|
+
| `antSelect` | Ant Select 选项选择 | `field`, `option`, `mode` |
|
|
562
|
+
| `antTreeSelect` | 树形选择 | `field`, `path` |
|
|
563
|
+
| `antInput` | 输入框(支持 `placeholder` 定位) | `field`/`placeholder`, `value` |
|
|
564
|
+
| `antRadio` / `antCheckbox` / `antSwitch` | 选择类 | `field`, `option`/`on` |
|
|
565
|
+
| `antDatePicker` / `antCascader` / `antUpload` | 其他控件 | 见各方法文档 |
|
|
566
|
+
| `formFill` | 批量填充 + 可选 `validateAfter` | `fields[]` |
|
|
567
|
+
| `closeAllDropdowns` | 关闭所有下拉浮层 | - |
|
|
568
|
+
|
|
569
|
+
**关键约束**:
|
|
570
|
+
- Ant Select 必须 `mousedown` 触发(不是 click)
|
|
571
|
+
- 选项异步渲染,内置 15×200ms 轮询
|
|
572
|
+
- 每次操作前后自动 `closeAllDropdowns`,`act` 入口检测残留并预清理
|
|
573
|
+
- 操作后 VERIFY `.ant-select-selection-item[title]` 回写
|
|
574
|
+
|
|
575
|
+
```json
|
|
576
|
+
{ "type": "antSelect", "field": "数据集名称", "option": "测试集A" }
|
|
577
|
+
{ "type": "antInput", "placeholder": "挂接资源", "value": "abc" }
|
|
578
|
+
{ "type": "formFill", "fields": [
|
|
579
|
+
{ "label": "名称", "type": "input", "value": "{{name}}" },
|
|
580
|
+
{ "label": "类型", "type": "select", "value": "{{type}}" }
|
|
581
|
+
], "validateAfter": true }
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
## 用例 DSL(Test Case)
|
|
585
|
+
|
|
586
|
+
把多个用例统一为参数化 testCase JSON:
|
|
587
|
+
|
|
588
|
+
```bash
|
|
589
|
+
node local-browser-executor.js --action test-case --test-case-file /tmp/case.json [--dataset-index 0] [--test-report /tmp/report.json]
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
```json
|
|
593
|
+
{
|
|
594
|
+
"title": "创建数据集 - P0",
|
|
595
|
+
"dataset": [{ "name": "A", "type": "图像" }, { "name": "B", "type": "文本" }],
|
|
596
|
+
"steps": [
|
|
597
|
+
{ "type": "formFill", "fields": [
|
|
598
|
+
{ "label": "名称", "type": "input", "value": "{{name}}" },
|
|
599
|
+
{ "label": "类型", "type": "select", "value": "{{type}}" }
|
|
600
|
+
], "validateAfter": true },
|
|
601
|
+
{ "type": "expect", "assertion": "validation.toPass" },
|
|
602
|
+
{ "type": "act", "click": "text=提交" },
|
|
603
|
+
{ "type": "expect", "assertion": "url.toContain", "value": "/list" }
|
|
604
|
+
],
|
|
605
|
+
"recovery": { "maxRetriesPerStep": 2 }
|
|
606
|
+
}
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
## 语义断言(expect)
|
|
610
|
+
|
|
611
|
+
| assertion | 功能 |
|
|
612
|
+
|-----------|------|
|
|
613
|
+
| `validation.toPass` | 表单校验无错误 |
|
|
614
|
+
| `validation.toHaveError` | 某字段有错误 |
|
|
615
|
+
| `locator.toBeVisible` / `toHaveText` / `toHaveValue` / `toHaveCount` | 元素状态 |
|
|
616
|
+
| `url.toContain` / `url.toMatch` | URL 断言 |
|
|
617
|
+
| `api.toAllPass` / `api.toInclude` | API 断言 |
|
|
618
|
+
|
|
619
|
+
所有断言内置 `poll` 轮询(默认 timeout 5s, interval 200ms)。
|
|
620
|
+
|
|
621
|
+
## 微前端(wujie / qiankun)场景适配
|
|
622
|
+
|
|
623
|
+
**核心问题与解决:**
|
|
624
|
+
|
|
625
|
+
| 微前端坑 | 自动修复 |
|
|
626
|
+
|---------|---------|
|
|
627
|
+
| 子应用业务在 frame[1],每步都要重传 frameContentContains | testCase `setup.findFrame` 设一次,所有步骤默认走 `_activeFrame` |
|
|
628
|
+
| 子应用路由切换销毁 iframe,导致后续 step 都 evaluate 失败 | `_getTargetFrame` 每次执行前探活,失活自动召回(标记 `frameRecovered: true`) |
|
|
629
|
+
| Ant Select 的 `getPopupContainer` 把浮层渲染到父文档 | `antSelect` 触发器/浮层双 frame 协作:trigger 在子 frame mousedown,浮层在父 frame 找选项 |
|
|
630
|
+
| 残留浮层污染下一次操作 | `closeAllDropdowns` 跨 frame 清理(主 + 所有可访问子 frame) |
|
|
631
|
+
| `api.toAllPass` 因主应用埋点 404 误判子应用 P0 测试失败 | `step.scope: "current-frame"` 仅断言 `_activeFrame` 同源的请求 |
|
|
632
|
+
|
|
633
|
+
**testCase 微前端模板:**
|
|
634
|
+
|
|
635
|
+
```json
|
|
636
|
+
{
|
|
637
|
+
"title": "数据集创建 P0",
|
|
638
|
+
"setup": {
|
|
639
|
+
"findFrame": { "contentContains": "数据集目录" }
|
|
640
|
+
},
|
|
641
|
+
"dataset": [{ "name": "A" }, { "name": "B" }],
|
|
642
|
+
"steps": [
|
|
643
|
+
{ "type": "apiCapture", "urlPattern": "/api/dataset", "wait": 5000, "includeAllTargets": true },
|
|
644
|
+
{ "type": "antInput", "field": "名称", "value": "{{name}}" },
|
|
645
|
+
{ "type": "antSelect", "field": "类型", "option": "图像" },
|
|
646
|
+
{ "type": "act", "click": "text=提交" },
|
|
647
|
+
{ "type": "expect", "assertion": "validation.toPass" },
|
|
648
|
+
{ "type": "expect", "assertion": "api.toAllPass", "scope": "current-frame" }
|
|
649
|
+
]
|
|
650
|
+
}
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
**关键参数:**
|
|
654
|
+
|
|
655
|
+
- `setup.findFrame.contentContains` — 一次定位子应用 frame
|
|
656
|
+
- `step.frameContentContains` — 单步覆盖(少用,违反 DRY)
|
|
657
|
+
- `step.scope: "current-frame"` — API 断言仅看 `_activeFrame` 同源请求
|
|
658
|
+
- `apiCapture.includeAllTargets: true` — 跨 page/iframe target 监听网络
|
|
659
|
+
|
|
660
|
+
**调试技巧:**
|
|
661
|
+
|
|
662
|
+
每个 ant 步骤结果含 `triggerFrameUrl` / `popupFrameUrl`,可看到触发器和浮层实际在哪个 frame。若 `_dropdownFrame: "main"` 出现,说明该控件用了 `getPopupContainer` 跨 frame 渲染浮层。
|
|
663
|
+
|
|
664
|
+
## 非目标声明
|
|
665
|
+
|
|
666
|
+
`local-browser-test` **不**承担以下职责,遇到时显式拒绝并指给正确工具:
|
|
667
|
+
|
|
668
|
+
- ❌ 接口压力测试 / 并发性能验证 → 用 k6、wrk、jmeter
|
|
669
|
+
- ❌ 跨浏览器兼容性矩阵(Safari/Firefox 多版本)→ BrowserStack / SauceLabs
|
|
670
|
+
- ❌ 单元测试 / 组件级测试 → Vitest / Jest / RTL
|
|
671
|
+
- ❌ 视觉回归全量对比(无 baseline 设计稿)→ Percy / Chromatic
|
|
672
|
+
- ✅ 真实浏览器内的功能验证、API 时序捕获、设计稿单图对比、动态菜单批量扫描
|
|
@@ -38,6 +38,21 @@
|
|
|
38
38
|
* --max-rounds <n> 最大轮数(默认 60)
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
|
+
// 共享缓存自愈:6/17 反馈中 NODE_PATH 未注入会导致 puppeteer-core MODULE_NOT_FOUND。
|
|
42
|
+
// 把 ~/.szcd-mcp/deps/node_modules 注入 module 解析路径,再 require puppeteer-core。
|
|
43
|
+
(function ensureSharedDeps() {
|
|
44
|
+
try {
|
|
45
|
+
const os = require('os');
|
|
46
|
+
const Module = require('module');
|
|
47
|
+
const sharedDeps = require('path').join(os.homedir(), '.szcd-mcp', 'deps', 'node_modules');
|
|
48
|
+
if (require('fs').existsSync(sharedDeps) && !Module.globalPaths.includes(sharedDeps)) {
|
|
49
|
+
Module.globalPaths.push(sharedDeps);
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// ignore — 没有共享缓存时退回项目本地 node_modules
|
|
53
|
+
}
|
|
54
|
+
})();
|
|
55
|
+
|
|
41
56
|
const puppeteer = require('puppeteer-core');
|
|
42
57
|
const fs = require('fs');
|
|
43
58
|
const path = require('path');
|
package/package.json
CHANGED
|
@@ -95,6 +95,7 @@ AI 直接构造测试计划 JSON:
|
|
|
95
95
|
| findFrame | 查找微前端 iframe | `urlIncludes` 或 `urlRegex` 或 `titleIncludes` 或 `contentIncludes` 或 `frameIndex` | `{ found, frameUrl, matchedBy, totalFrames }` |
|
|
96
96
|
| loginWait | 等待 SSO 登录完成 | `loginSelector`, `timeout`, `interval` | `{ loggedIn, elapsed, currentUrl }` |
|
|
97
97
|
| aiAssert | 语义断言(截图+审查) | `assertion`, `filename` | `{ screenshotPath, assertion, needsVisualReview }` |
|
|
98
|
+
| runTask | 高层任务 runner(批量动态菜单扫描) | `task: "menu-api-scan"`, `menuApiPattern`, `menuLimit`, `menuPathPrefix[]`, `menuFilter[]`, `drawerTriggerSelector`, `useNativeHover`, `apiCaptureOptions`, `continueOnFail` | `{ task, discovered, attempted, byOutcome, results[] }` |
|
|
98
99
|
|
|
99
100
|
### 场景选择策略
|
|
100
101
|
|
|
@@ -488,3 +489,184 @@ cd ~/.szcd-mcp/deps && npm install --registry=https://registry.npmmirror.com --@
|
|
|
488
489
|
"outputDir": "/tmp/browser-test-xxx"
|
|
489
490
|
}
|
|
490
491
|
```
|
|
492
|
+
|
|
493
|
+
## 批量动态菜单扫描(runTask)
|
|
494
|
+
|
|
495
|
+
适用场景:测试系统有 N 条动态路由(来自后端菜单接口),要逐条点击触发并验证接口是否返回 200。6/18 反馈中,AI 最终用 130 余条 `act --menu-path` + `api-capture` 手工组合走完 129 条菜单——`runTask` 把这套流水固化为单步骤。
|
|
496
|
+
|
|
497
|
+
工作流:
|
|
498
|
+
|
|
499
|
+
1. **第一步:先抓菜单接口**(一次即可,作为整张菜单树的来源)
|
|
500
|
+
```bash
|
|
501
|
+
node local-browser-executor.js --action api-capture \
|
|
502
|
+
--url-pattern "listUserViewMenus" \
|
|
503
|
+
--include-response-body \
|
|
504
|
+
--include-all-targets \
|
|
505
|
+
--reload --wait 8000 \
|
|
506
|
+
--output /tmp/menu-tree.json
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
2. **第二步:把菜单树喂给 runTask**
|
|
510
|
+
```bash
|
|
511
|
+
node local-browser-executor.js --action run-task \
|
|
512
|
+
--task menu-api-scan \
|
|
513
|
+
--capture-input-file /tmp/menu-tree.json \
|
|
514
|
+
--menu-path-prefix /catalog \
|
|
515
|
+
--menu-limit 20 \
|
|
516
|
+
--drawer-trigger-selector "[data-test=menu-toggle]" \
|
|
517
|
+
--use-native-hover true \
|
|
518
|
+
--max-retry-per-step 2 \
|
|
519
|
+
--wait 4000 \
|
|
520
|
+
--output /tmp/menu-scan-report.json
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
3. **结果解读**:`byOutcome` 给出四档统计:
|
|
524
|
+
- `apiOk`:菜单点开 + 接口 200 ✅
|
|
525
|
+
- `apiFailed`:菜单点开 + 接口 ≥400(看 `results[i].api.requests` 找具体 URL)
|
|
526
|
+
- `noApi`:菜单点开但没产生任何 XHR(可能是纯静态页或路由没触发请求)
|
|
527
|
+
- `clickFailed`:菜单点不开(看 `results[i].menuResult.failedAt` + `candidates`)
|
|
528
|
+
|
|
529
|
+
4. **plan JSON 形式**(也可作为 `--plan-file` 一步:先 apiCapture 再 runTask):
|
|
530
|
+
```json
|
|
531
|
+
{
|
|
532
|
+
"steps": [
|
|
533
|
+
{ "type": "apiCapture", "urlPattern": "listUserViewMenus", "includeResponseBody": true, "reload": true, "wait": 8000, "includeAllTargets": true },
|
|
534
|
+
{ "type": "runTask", "task": "menu-api-scan", "menuLimit": 30, "drawerTriggerSelector": "[data-test=menu-toggle]" }
|
|
535
|
+
]
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
`runTask` 会自动从 `context.lastApiCapture` 拿菜单树,不需要再传文件。
|
|
539
|
+
|
|
540
|
+
## 反模式备忘(来自 6/17 + 6/18 实战反馈)
|
|
541
|
+
|
|
542
|
+
| 错误做法 | 正确做法 | 原因 |
|
|
543
|
+
|---------|---------|------|
|
|
544
|
+
| `setRequestInterception(true)` 抓接口 | 仅 CDP `Network.responseReceived` + `getResponseBody` | request interception 会拦下 wujie iframe 的 RPC 帧造成挂起 |
|
|
545
|
+
| `page.click("text=保存")` 不加可见性过滤 | 默认走 `act` 的 `text=`,已自动只匹配 `visible+enabled` 的元素 | 隐藏在收起菜单/未渲染抽屉里的同名文本会被错误命中 |
|
|
546
|
+
| 多级菜单只用一次 `dispatchEvent("mouseover")` | `useNativeHover: true`(默认) + `drawerTriggerSelector` 每级前重开 | Vue 的 v-on:mouseenter 对 dispatchEvent 不触发;抽屉式菜单点完一次会自动收起 |
|
|
547
|
+
| `setTimeout(awaitPage.bringToFront(), 30000)` 之外不做兜底 | 调用前自查"目标页是否还活着"(`findPage` / `_findFrame`),失败立刻退而求其次操作主帧 | bringToFront 在 OS 抢焦点失败时会无限挂起,没超时 |
|
|
548
|
+
| iframe attach 失败就报错退出 | `attachRetries` 默认 3 次、间隔 1.5s 重抓 `browser.targets()` | wujie 的 blob iframe target 在主页面 ready 后才注册,时序敏感 |
|
|
549
|
+
| 大批量探索每条都 throw | `_actMenuPath` 失败时返回 `acted:false + candidates`,配合 `runTask --continue-on-fail` | 探索性扫描遇到一条点不开就全部中断,没法发现整体面 |
|
|
550
|
+
| 手写 puppeteer 脚本绕过 `act` 操作 Ant Select | 用 `antSelect` 步骤(内置 mousedown + 轮询 + closeAllDropdowns + VERIFY) | Ant Design Select 必须 mousedown 触发,浮层在 body 末尾,选项异步渲染,单步 `act.evaluate` 无法覆盖四步组合 |
|
|
551
|
+
| 每个 Select 后忘记 `closeAllDropdowns` | `act` 自动检测上一次 antSelect/antTreeSelect 并预清理 | 下拉浮层残留会污染下一次 Select 操作,选项串选 |
|
|
552
|
+
| 硬编码 `sleep(15*200)` 等选项渲染 | `antSelect` 内置 `pollEval` 15×200ms 轮询,`expect(poll)` 可自定义条件 | 选项渲染时间受网络/数据量影响,固定 sleep 时机不可靠 |
|
|
553
|
+
| 表单校验用 ad-hoc evaluate 检查 | 用 `expect validation.toPass` 或 `formFill` 的 `validateAfter` | 语义断言自带 poll + 结构化错误信息(field + message),可直接进报告 |
|
|
554
|
+
|
|
555
|
+
## Ant Design 适配层(6/22 反馈落地)
|
|
556
|
+
|
|
557
|
+
把"触发器定位 → 浮层等待 → 选项交互 → VERIFY → 全局清理"固化为单步骤 JSON。
|
|
558
|
+
|
|
559
|
+
| type | 功能 | 关键参数 |
|
|
560
|
+
|------|------|---------|
|
|
561
|
+
| `antSelect` | Ant Select 选项选择 | `field`, `option`, `mode` |
|
|
562
|
+
| `antTreeSelect` | 树形选择 | `field`, `path` |
|
|
563
|
+
| `antInput` | 输入框(支持 `placeholder` 定位) | `field`/`placeholder`, `value` |
|
|
564
|
+
| `antRadio` / `antCheckbox` / `antSwitch` | 选择类 | `field`, `option`/`on` |
|
|
565
|
+
| `antDatePicker` / `antCascader` / `antUpload` | 其他控件 | 见各方法文档 |
|
|
566
|
+
| `formFill` | 批量填充 + 可选 `validateAfter` | `fields[]` |
|
|
567
|
+
| `closeAllDropdowns` | 关闭所有下拉浮层 | - |
|
|
568
|
+
|
|
569
|
+
**关键约束**:
|
|
570
|
+
- Ant Select 必须 `mousedown` 触发(不是 click)
|
|
571
|
+
- 选项异步渲染,内置 15×200ms 轮询
|
|
572
|
+
- 每次操作前后自动 `closeAllDropdowns`,`act` 入口检测残留并预清理
|
|
573
|
+
- 操作后 VERIFY `.ant-select-selection-item[title]` 回写
|
|
574
|
+
|
|
575
|
+
```json
|
|
576
|
+
{ "type": "antSelect", "field": "数据集名称", "option": "测试集A" }
|
|
577
|
+
{ "type": "antInput", "placeholder": "挂接资源", "value": "abc" }
|
|
578
|
+
{ "type": "formFill", "fields": [
|
|
579
|
+
{ "label": "名称", "type": "input", "value": "{{name}}" },
|
|
580
|
+
{ "label": "类型", "type": "select", "value": "{{type}}" }
|
|
581
|
+
], "validateAfter": true }
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
## 用例 DSL(Test Case)
|
|
585
|
+
|
|
586
|
+
把多个用例统一为参数化 testCase JSON:
|
|
587
|
+
|
|
588
|
+
```bash
|
|
589
|
+
node local-browser-executor.js --action test-case --test-case-file /tmp/case.json [--dataset-index 0] [--test-report /tmp/report.json]
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
```json
|
|
593
|
+
{
|
|
594
|
+
"title": "创建数据集 - P0",
|
|
595
|
+
"dataset": [{ "name": "A", "type": "图像" }, { "name": "B", "type": "文本" }],
|
|
596
|
+
"steps": [
|
|
597
|
+
{ "type": "formFill", "fields": [
|
|
598
|
+
{ "label": "名称", "type": "input", "value": "{{name}}" },
|
|
599
|
+
{ "label": "类型", "type": "select", "value": "{{type}}" }
|
|
600
|
+
], "validateAfter": true },
|
|
601
|
+
{ "type": "expect", "assertion": "validation.toPass" },
|
|
602
|
+
{ "type": "act", "click": "text=提交" },
|
|
603
|
+
{ "type": "expect", "assertion": "url.toContain", "value": "/list" }
|
|
604
|
+
],
|
|
605
|
+
"recovery": { "maxRetriesPerStep": 2 }
|
|
606
|
+
}
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
## 语义断言(expect)
|
|
610
|
+
|
|
611
|
+
| assertion | 功能 |
|
|
612
|
+
|-----------|------|
|
|
613
|
+
| `validation.toPass` | 表单校验无错误 |
|
|
614
|
+
| `validation.toHaveError` | 某字段有错误 |
|
|
615
|
+
| `locator.toBeVisible` / `toHaveText` / `toHaveValue` / `toHaveCount` | 元素状态 |
|
|
616
|
+
| `url.toContain` / `url.toMatch` | URL 断言 |
|
|
617
|
+
| `api.toAllPass` / `api.toInclude` | API 断言 |
|
|
618
|
+
|
|
619
|
+
所有断言内置 `poll` 轮询(默认 timeout 5s, interval 200ms)。
|
|
620
|
+
|
|
621
|
+
## 微前端(wujie / qiankun)场景适配
|
|
622
|
+
|
|
623
|
+
**核心问题与解决:**
|
|
624
|
+
|
|
625
|
+
| 微前端坑 | 自动修复 |
|
|
626
|
+
|---------|---------|
|
|
627
|
+
| 子应用业务在 frame[1],每步都要重传 frameContentContains | testCase `setup.findFrame` 设一次,所有步骤默认走 `_activeFrame` |
|
|
628
|
+
| 子应用路由切换销毁 iframe,导致后续 step 都 evaluate 失败 | `_getTargetFrame` 每次执行前探活,失活自动召回(标记 `frameRecovered: true`) |
|
|
629
|
+
| Ant Select 的 `getPopupContainer` 把浮层渲染到父文档 | `antSelect` 触发器/浮层双 frame 协作:trigger 在子 frame mousedown,浮层在父 frame 找选项 |
|
|
630
|
+
| 残留浮层污染下一次操作 | `closeAllDropdowns` 跨 frame 清理(主 + 所有可访问子 frame) |
|
|
631
|
+
| `api.toAllPass` 因主应用埋点 404 误判子应用 P0 测试失败 | `step.scope: "current-frame"` 仅断言 `_activeFrame` 同源的请求 |
|
|
632
|
+
|
|
633
|
+
**testCase 微前端模板:**
|
|
634
|
+
|
|
635
|
+
```json
|
|
636
|
+
{
|
|
637
|
+
"title": "数据集创建 P0",
|
|
638
|
+
"setup": {
|
|
639
|
+
"findFrame": { "contentContains": "数据集目录" }
|
|
640
|
+
},
|
|
641
|
+
"dataset": [{ "name": "A" }, { "name": "B" }],
|
|
642
|
+
"steps": [
|
|
643
|
+
{ "type": "apiCapture", "urlPattern": "/api/dataset", "wait": 5000, "includeAllTargets": true },
|
|
644
|
+
{ "type": "antInput", "field": "名称", "value": "{{name}}" },
|
|
645
|
+
{ "type": "antSelect", "field": "类型", "option": "图像" },
|
|
646
|
+
{ "type": "act", "click": "text=提交" },
|
|
647
|
+
{ "type": "expect", "assertion": "validation.toPass" },
|
|
648
|
+
{ "type": "expect", "assertion": "api.toAllPass", "scope": "current-frame" }
|
|
649
|
+
]
|
|
650
|
+
}
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
**关键参数:**
|
|
654
|
+
|
|
655
|
+
- `setup.findFrame.contentContains` — 一次定位子应用 frame
|
|
656
|
+
- `step.frameContentContains` — 单步覆盖(少用,违反 DRY)
|
|
657
|
+
- `step.scope: "current-frame"` — API 断言仅看 `_activeFrame` 同源请求
|
|
658
|
+
- `apiCapture.includeAllTargets: true` — 跨 page/iframe target 监听网络
|
|
659
|
+
|
|
660
|
+
**调试技巧:**
|
|
661
|
+
|
|
662
|
+
每个 ant 步骤结果含 `triggerFrameUrl` / `popupFrameUrl`,可看到触发器和浮层实际在哪个 frame。若 `_dropdownFrame: "main"` 出现,说明该控件用了 `getPopupContainer` 跨 frame 渲染浮层。
|
|
663
|
+
|
|
664
|
+
## 非目标声明
|
|
665
|
+
|
|
666
|
+
`local-browser-test` **不**承担以下职责,遇到时显式拒绝并指给正确工具:
|
|
667
|
+
|
|
668
|
+
- ❌ 接口压力测试 / 并发性能验证 → 用 k6、wrk、jmeter
|
|
669
|
+
- ❌ 跨浏览器兼容性矩阵(Safari/Firefox 多版本)→ BrowserStack / SauceLabs
|
|
670
|
+
- ❌ 单元测试 / 组件级测试 → Vitest / Jest / RTL
|
|
671
|
+
- ❌ 视觉回归全量对比(无 baseline 设计稿)→ Percy / Chromatic
|
|
672
|
+
- ✅ 真实浏览器内的功能验证、API 时序捕获、设计稿单图对比、动态菜单批量扫描
|
|
@@ -38,6 +38,21 @@
|
|
|
38
38
|
* --max-rounds <n> 最大轮数(默认 60)
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
|
+
// 共享缓存自愈:6/17 反馈中 NODE_PATH 未注入会导致 puppeteer-core MODULE_NOT_FOUND。
|
|
42
|
+
// 把 ~/.szcd-mcp/deps/node_modules 注入 module 解析路径,再 require puppeteer-core。
|
|
43
|
+
(function ensureSharedDeps() {
|
|
44
|
+
try {
|
|
45
|
+
const os = require('os');
|
|
46
|
+
const Module = require('module');
|
|
47
|
+
const sharedDeps = require('path').join(os.homedir(), '.szcd-mcp', 'deps', 'node_modules');
|
|
48
|
+
if (require('fs').existsSync(sharedDeps) && !Module.globalPaths.includes(sharedDeps)) {
|
|
49
|
+
Module.globalPaths.push(sharedDeps);
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// ignore — 没有共享缓存时退回项目本地 node_modules
|
|
53
|
+
}
|
|
54
|
+
})();
|
|
55
|
+
|
|
41
56
|
const puppeteer = require('puppeteer-core');
|
|
42
57
|
const fs = require('fs');
|
|
43
58
|
const path = require('path');
|
|
@@ -95,6 +95,7 @@ AI 直接构造测试计划 JSON:
|
|
|
95
95
|
| findFrame | 查找微前端 iframe | `urlIncludes` 或 `urlRegex` 或 `titleIncludes` 或 `contentIncludes` 或 `frameIndex` | `{ found, frameUrl, matchedBy, totalFrames }` |
|
|
96
96
|
| loginWait | 等待 SSO 登录完成 | `loginSelector`, `timeout`, `interval` | `{ loggedIn, elapsed, currentUrl }` |
|
|
97
97
|
| aiAssert | 语义断言(截图+审查) | `assertion`, `filename` | `{ screenshotPath, assertion, needsVisualReview }` |
|
|
98
|
+
| runTask | 高层任务 runner(批量动态菜单扫描) | `task: "menu-api-scan"`, `menuApiPattern`, `menuLimit`, `menuPathPrefix[]`, `menuFilter[]`, `drawerTriggerSelector`, `useNativeHover`, `apiCaptureOptions`, `continueOnFail` | `{ task, discovered, attempted, byOutcome, results[] }` |
|
|
98
99
|
|
|
99
100
|
### 场景选择策略
|
|
100
101
|
|
|
@@ -488,3 +489,184 @@ cd ~/.szcd-mcp/deps && npm install --registry=https://registry.npmmirror.com --@
|
|
|
488
489
|
"outputDir": "/tmp/browser-test-xxx"
|
|
489
490
|
}
|
|
490
491
|
```
|
|
492
|
+
|
|
493
|
+
## 批量动态菜单扫描(runTask)
|
|
494
|
+
|
|
495
|
+
适用场景:测试系统有 N 条动态路由(来自后端菜单接口),要逐条点击触发并验证接口是否返回 200。6/18 反馈中,AI 最终用 130 余条 `act --menu-path` + `api-capture` 手工组合走完 129 条菜单——`runTask` 把这套流水固化为单步骤。
|
|
496
|
+
|
|
497
|
+
工作流:
|
|
498
|
+
|
|
499
|
+
1. **第一步:先抓菜单接口**(一次即可,作为整张菜单树的来源)
|
|
500
|
+
```bash
|
|
501
|
+
node local-browser-executor.js --action api-capture \
|
|
502
|
+
--url-pattern "listUserViewMenus" \
|
|
503
|
+
--include-response-body \
|
|
504
|
+
--include-all-targets \
|
|
505
|
+
--reload --wait 8000 \
|
|
506
|
+
--output /tmp/menu-tree.json
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
2. **第二步:把菜单树喂给 runTask**
|
|
510
|
+
```bash
|
|
511
|
+
node local-browser-executor.js --action run-task \
|
|
512
|
+
--task menu-api-scan \
|
|
513
|
+
--capture-input-file /tmp/menu-tree.json \
|
|
514
|
+
--menu-path-prefix /catalog \
|
|
515
|
+
--menu-limit 20 \
|
|
516
|
+
--drawer-trigger-selector "[data-test=menu-toggle]" \
|
|
517
|
+
--use-native-hover true \
|
|
518
|
+
--max-retry-per-step 2 \
|
|
519
|
+
--wait 4000 \
|
|
520
|
+
--output /tmp/menu-scan-report.json
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
3. **结果解读**:`byOutcome` 给出四档统计:
|
|
524
|
+
- `apiOk`:菜单点开 + 接口 200 ✅
|
|
525
|
+
- `apiFailed`:菜单点开 + 接口 ≥400(看 `results[i].api.requests` 找具体 URL)
|
|
526
|
+
- `noApi`:菜单点开但没产生任何 XHR(可能是纯静态页或路由没触发请求)
|
|
527
|
+
- `clickFailed`:菜单点不开(看 `results[i].menuResult.failedAt` + `candidates`)
|
|
528
|
+
|
|
529
|
+
4. **plan JSON 形式**(也可作为 `--plan-file` 一步:先 apiCapture 再 runTask):
|
|
530
|
+
```json
|
|
531
|
+
{
|
|
532
|
+
"steps": [
|
|
533
|
+
{ "type": "apiCapture", "urlPattern": "listUserViewMenus", "includeResponseBody": true, "reload": true, "wait": 8000, "includeAllTargets": true },
|
|
534
|
+
{ "type": "runTask", "task": "menu-api-scan", "menuLimit": 30, "drawerTriggerSelector": "[data-test=menu-toggle]" }
|
|
535
|
+
]
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
`runTask` 会自动从 `context.lastApiCapture` 拿菜单树,不需要再传文件。
|
|
539
|
+
|
|
540
|
+
## 反模式备忘(来自 6/17 + 6/18 实战反馈)
|
|
541
|
+
|
|
542
|
+
| 错误做法 | 正确做法 | 原因 |
|
|
543
|
+
|---------|---------|------|
|
|
544
|
+
| `setRequestInterception(true)` 抓接口 | 仅 CDP `Network.responseReceived` + `getResponseBody` | request interception 会拦下 wujie iframe 的 RPC 帧造成挂起 |
|
|
545
|
+
| `page.click("text=保存")` 不加可见性过滤 | 默认走 `act` 的 `text=`,已自动只匹配 `visible+enabled` 的元素 | 隐藏在收起菜单/未渲染抽屉里的同名文本会被错误命中 |
|
|
546
|
+
| 多级菜单只用一次 `dispatchEvent("mouseover")` | `useNativeHover: true`(默认) + `drawerTriggerSelector` 每级前重开 | Vue 的 v-on:mouseenter 对 dispatchEvent 不触发;抽屉式菜单点完一次会自动收起 |
|
|
547
|
+
| `setTimeout(awaitPage.bringToFront(), 30000)` 之外不做兜底 | 调用前自查"目标页是否还活着"(`findPage` / `_findFrame`),失败立刻退而求其次操作主帧 | bringToFront 在 OS 抢焦点失败时会无限挂起,没超时 |
|
|
548
|
+
| iframe attach 失败就报错退出 | `attachRetries` 默认 3 次、间隔 1.5s 重抓 `browser.targets()` | wujie 的 blob iframe target 在主页面 ready 后才注册,时序敏感 |
|
|
549
|
+
| 大批量探索每条都 throw | `_actMenuPath` 失败时返回 `acted:false + candidates`,配合 `runTask --continue-on-fail` | 探索性扫描遇到一条点不开就全部中断,没法发现整体面 |
|
|
550
|
+
| 手写 puppeteer 脚本绕过 `act` 操作 Ant Select | 用 `antSelect` 步骤(内置 mousedown + 轮询 + closeAllDropdowns + VERIFY) | Ant Design Select 必须 mousedown 触发,浮层在 body 末尾,选项异步渲染,单步 `act.evaluate` 无法覆盖四步组合 |
|
|
551
|
+
| 每个 Select 后忘记 `closeAllDropdowns` | `act` 自动检测上一次 antSelect/antTreeSelect 并预清理 | 下拉浮层残留会污染下一次 Select 操作,选项串选 |
|
|
552
|
+
| 硬编码 `sleep(15*200)` 等选项渲染 | `antSelect` 内置 `pollEval` 15×200ms 轮询,`expect(poll)` 可自定义条件 | 选项渲染时间受网络/数据量影响,固定 sleep 时机不可靠 |
|
|
553
|
+
| 表单校验用 ad-hoc evaluate 检查 | 用 `expect validation.toPass` 或 `formFill` 的 `validateAfter` | 语义断言自带 poll + 结构化错误信息(field + message),可直接进报告 |
|
|
554
|
+
|
|
555
|
+
## Ant Design 适配层(6/22 反馈落地)
|
|
556
|
+
|
|
557
|
+
把"触发器定位 → 浮层等待 → 选项交互 → VERIFY → 全局清理"固化为单步骤 JSON。
|
|
558
|
+
|
|
559
|
+
| type | 功能 | 关键参数 |
|
|
560
|
+
|------|------|---------|
|
|
561
|
+
| `antSelect` | Ant Select 选项选择 | `field`, `option`, `mode` |
|
|
562
|
+
| `antTreeSelect` | 树形选择 | `field`, `path` |
|
|
563
|
+
| `antInput` | 输入框(支持 `placeholder` 定位) | `field`/`placeholder`, `value` |
|
|
564
|
+
| `antRadio` / `antCheckbox` / `antSwitch` | 选择类 | `field`, `option`/`on` |
|
|
565
|
+
| `antDatePicker` / `antCascader` / `antUpload` | 其他控件 | 见各方法文档 |
|
|
566
|
+
| `formFill` | 批量填充 + 可选 `validateAfter` | `fields[]` |
|
|
567
|
+
| `closeAllDropdowns` | 关闭所有下拉浮层 | - |
|
|
568
|
+
|
|
569
|
+
**关键约束**:
|
|
570
|
+
- Ant Select 必须 `mousedown` 触发(不是 click)
|
|
571
|
+
- 选项异步渲染,内置 15×200ms 轮询
|
|
572
|
+
- 每次操作前后自动 `closeAllDropdowns`,`act` 入口检测残留并预清理
|
|
573
|
+
- 操作后 VERIFY `.ant-select-selection-item[title]` 回写
|
|
574
|
+
|
|
575
|
+
```json
|
|
576
|
+
{ "type": "antSelect", "field": "数据集名称", "option": "测试集A" }
|
|
577
|
+
{ "type": "antInput", "placeholder": "挂接资源", "value": "abc" }
|
|
578
|
+
{ "type": "formFill", "fields": [
|
|
579
|
+
{ "label": "名称", "type": "input", "value": "{{name}}" },
|
|
580
|
+
{ "label": "类型", "type": "select", "value": "{{type}}" }
|
|
581
|
+
], "validateAfter": true }
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
## 用例 DSL(Test Case)
|
|
585
|
+
|
|
586
|
+
把多个用例统一为参数化 testCase JSON:
|
|
587
|
+
|
|
588
|
+
```bash
|
|
589
|
+
node local-browser-executor.js --action test-case --test-case-file /tmp/case.json [--dataset-index 0] [--test-report /tmp/report.json]
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
```json
|
|
593
|
+
{
|
|
594
|
+
"title": "创建数据集 - P0",
|
|
595
|
+
"dataset": [{ "name": "A", "type": "图像" }, { "name": "B", "type": "文本" }],
|
|
596
|
+
"steps": [
|
|
597
|
+
{ "type": "formFill", "fields": [
|
|
598
|
+
{ "label": "名称", "type": "input", "value": "{{name}}" },
|
|
599
|
+
{ "label": "类型", "type": "select", "value": "{{type}}" }
|
|
600
|
+
], "validateAfter": true },
|
|
601
|
+
{ "type": "expect", "assertion": "validation.toPass" },
|
|
602
|
+
{ "type": "act", "click": "text=提交" },
|
|
603
|
+
{ "type": "expect", "assertion": "url.toContain", "value": "/list" }
|
|
604
|
+
],
|
|
605
|
+
"recovery": { "maxRetriesPerStep": 2 }
|
|
606
|
+
}
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
## 语义断言(expect)
|
|
610
|
+
|
|
611
|
+
| assertion | 功能 |
|
|
612
|
+
|-----------|------|
|
|
613
|
+
| `validation.toPass` | 表单校验无错误 |
|
|
614
|
+
| `validation.toHaveError` | 某字段有错误 |
|
|
615
|
+
| `locator.toBeVisible` / `toHaveText` / `toHaveValue` / `toHaveCount` | 元素状态 |
|
|
616
|
+
| `url.toContain` / `url.toMatch` | URL 断言 |
|
|
617
|
+
| `api.toAllPass` / `api.toInclude` | API 断言 |
|
|
618
|
+
|
|
619
|
+
所有断言内置 `poll` 轮询(默认 timeout 5s, interval 200ms)。
|
|
620
|
+
|
|
621
|
+
## 微前端(wujie / qiankun)场景适配
|
|
622
|
+
|
|
623
|
+
**核心问题与解决:**
|
|
624
|
+
|
|
625
|
+
| 微前端坑 | 自动修复 |
|
|
626
|
+
|---------|---------|
|
|
627
|
+
| 子应用业务在 frame[1],每步都要重传 frameContentContains | testCase `setup.findFrame` 设一次,所有步骤默认走 `_activeFrame` |
|
|
628
|
+
| 子应用路由切换销毁 iframe,导致后续 step 都 evaluate 失败 | `_getTargetFrame` 每次执行前探活,失活自动召回(标记 `frameRecovered: true`) |
|
|
629
|
+
| Ant Select 的 `getPopupContainer` 把浮层渲染到父文档 | `antSelect` 触发器/浮层双 frame 协作:trigger 在子 frame mousedown,浮层在父 frame 找选项 |
|
|
630
|
+
| 残留浮层污染下一次操作 | `closeAllDropdowns` 跨 frame 清理(主 + 所有可访问子 frame) |
|
|
631
|
+
| `api.toAllPass` 因主应用埋点 404 误判子应用 P0 测试失败 | `step.scope: "current-frame"` 仅断言 `_activeFrame` 同源的请求 |
|
|
632
|
+
|
|
633
|
+
**testCase 微前端模板:**
|
|
634
|
+
|
|
635
|
+
```json
|
|
636
|
+
{
|
|
637
|
+
"title": "数据集创建 P0",
|
|
638
|
+
"setup": {
|
|
639
|
+
"findFrame": { "contentContains": "数据集目录" }
|
|
640
|
+
},
|
|
641
|
+
"dataset": [{ "name": "A" }, { "name": "B" }],
|
|
642
|
+
"steps": [
|
|
643
|
+
{ "type": "apiCapture", "urlPattern": "/api/dataset", "wait": 5000, "includeAllTargets": true },
|
|
644
|
+
{ "type": "antInput", "field": "名称", "value": "{{name}}" },
|
|
645
|
+
{ "type": "antSelect", "field": "类型", "option": "图像" },
|
|
646
|
+
{ "type": "act", "click": "text=提交" },
|
|
647
|
+
{ "type": "expect", "assertion": "validation.toPass" },
|
|
648
|
+
{ "type": "expect", "assertion": "api.toAllPass", "scope": "current-frame" }
|
|
649
|
+
]
|
|
650
|
+
}
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
**关键参数:**
|
|
654
|
+
|
|
655
|
+
- `setup.findFrame.contentContains` — 一次定位子应用 frame
|
|
656
|
+
- `step.frameContentContains` — 单步覆盖(少用,违反 DRY)
|
|
657
|
+
- `step.scope: "current-frame"` — API 断言仅看 `_activeFrame` 同源请求
|
|
658
|
+
- `apiCapture.includeAllTargets: true` — 跨 page/iframe target 监听网络
|
|
659
|
+
|
|
660
|
+
**调试技巧:**
|
|
661
|
+
|
|
662
|
+
每个 ant 步骤结果含 `triggerFrameUrl` / `popupFrameUrl`,可看到触发器和浮层实际在哪个 frame。若 `_dropdownFrame: "main"` 出现,说明该控件用了 `getPopupContainer` 跨 frame 渲染浮层。
|
|
663
|
+
|
|
664
|
+
## 非目标声明
|
|
665
|
+
|
|
666
|
+
`local-browser-test` **不**承担以下职责,遇到时显式拒绝并指给正确工具:
|
|
667
|
+
|
|
668
|
+
- ❌ 接口压力测试 / 并发性能验证 → 用 k6、wrk、jmeter
|
|
669
|
+
- ❌ 跨浏览器兼容性矩阵(Safari/Firefox 多版本)→ BrowserStack / SauceLabs
|
|
670
|
+
- ❌ 单元测试 / 组件级测试 → Vitest / Jest / RTL
|
|
671
|
+
- ❌ 视觉回归全量对比(无 baseline 设计稿)→ Percy / Chromatic
|
|
672
|
+
- ✅ 真实浏览器内的功能验证、API 时序捕获、设计稿单图对比、动态菜单批量扫描
|