@szc-ft/mcp-szcd-client 0.28.2 → 0.30.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.
@@ -215,13 +215,18 @@ AI 直接构造测试计划 JSON:
215
215
  | click | 点击元素(兼容) | `selector`, `waitForNavigation` | `{ clicked, tagName }` |
216
216
  | type | 输入文本(兼容) | `selector`, `text`, `clear` | `{ typed, value }` |
217
217
  | waitFor | 等待条件 | `selector` 或 `url`, `timeout` | `{ matched, waited }` |
218
- | evaluate | 执行 JS | `expression` | `{ result }` |
218
+ | evaluate | 执行 JS(浏览器端) | `expression`, `expectResult?`, `expectResultContains?`, `failOnFalsy?` | `{ result, passed, reason }` |
219
219
  | checkElement | 元素断言(兼容) | `selector`, `expect: {visible, minCount, maxCount, hasText}` | `{ passed, checks, elementCount }` |
220
220
  | findPage | 查找已打开的标签页 | `urlIncludes` 或 `urlRegex` 或 `titleIncludes` | `{ found, url, title, totalPages }` |
221
- | findFrame | 查找微前端 iframe | `urlIncludes` 或 `urlRegex` 或 `titleIncludes` 或 `contentIncludes` 或 `frameIndex` | `{ found, frameUrl, matchedBy, totalFrames }` |
221
+ | findFrame | 查找微前端 iframe | `frameSelector`(CSS选择器) 或 `frameUrlContains` 或 `frameContentContains` 或 `frameIndex` | `{ found, frameUrl, matchedBy, totalFrames }` |
222
222
  | loginWait | 等待 SSO 登录完成 | `loginSelector`, `timeout`, `interval` | `{ loggedIn, elapsed, currentUrl }` |
223
223
  | aiAssert | 语义断言(截图+审查) | `assertion`, `filename` | `{ screenshotPath, assertion, needsVisualReview }` |
224
224
  | runTask | 高层任务 runner(批量动态菜单扫描) | `task: "menu-api-scan"`, `menuApiPattern`, `menuLimit`, `menuPathPrefix[]`, `menuFilter[]`, `drawerTriggerSelector`, `useNativeHover`, `apiCaptureOptions`, `continueOnFail` | `{ task, discovered, attempted, byOutcome, results[] }` |
225
+ | **apiCall** | **API 直调(绕过 UI)** | `method`, `url`, `body?`, `headers?` | `{ status, data, passed }` |
226
+ | **switchPage** | **多标签页切换** | `urlContains?`/`titleIncludes?`/`pageIndex?` | `{ passed, url, title }` |
227
+ | **cdpScript** | **调用内置/已注册脚本** | `script`, `params?` | `{ acted, passed, ...result }` |
228
+ | **rawScript** | **Node.js 端执行任意 async** | `code`, `params?` | `{ acted, passed, ...result }` |
229
+ | **registerCdpScript** | **注册可复用脚本** | `name`, `code` | `{ registered, passed }` |
225
230
 
226
231
  ### 场景选择策略
227
232
 
@@ -246,14 +251,33 @@ AI 直接构造测试计划 JSON:
246
251
 
247
252
  ### 构造步骤的指导原则
248
253
 
249
- 1. **selector 必须从用户代码中读取,不要猜测**
250
- - 读代码中的 `id`、`className`、`data-testid`
251
- - szcd 组件的 class 可通过 `get_component_full_profile` 获取
252
- 2. **API 路径从代码中的接口调用读取**
253
- 3. **checkElement 的 expect 从代码逻辑推断**
254
- - columns 定义了 N 列 → `expect: { minCount: N }`(检查 th 数量)
255
- - dataSource 有数据 → `expect: { minCount: 1 }`(检查行数)
256
- 4. **compare 的 regions 从设计稿分析结果或组件布局推断**
254
+ **⚠️ 强制流程:构造 test-case JSON 前必须先执行 DOM 预检查**
255
+
256
+ 1. **observe 检查目标页面 DOM 结构**
257
+ - 确认 frame 结构(主页面 + 子应用 iframe)
258
+ - 确认表格/表单/按钮的 selector
259
+ - 确认 setup.findFrame 的匹配方式(frameSelector/frameUrlContains)
260
+
261
+ 2. **evaluate 检查关键元素文字**
262
+ - 按钮文字(如“提 交”中间有空格,不能用“提交”匹配)
263
+ - Select 选项文字(如“AI知识主题库”可能与 UI 显示不一致)
264
+ - 表单字段 label(如“数据资源名称”vs“数据集名称”)
265
+ - 表格列头(确认列数和列名)
266
+
267
+ 3. **根据 DOM 检查结果构造 test-case JSON**
268
+ - selector 从 observe 获取(不猜测)
269
+ - 选项文字从 evaluate 获取(不猜测)
270
+ - 按钮文字从 evaluate 获取(注意空格)
271
+ - 无项目代码时,DOM 检查替代代码读取
272
+
273
+ 4. **act 文字匹配**
274
+ - `text=新建` → 模糊匹配(includes),可能误匹配“新建目录”
275
+ - `text==新建` → 精确匹配(===),只匹配文字恰好为“新建”的元素
276
+ - 按钮文字可能有空格(如“提 交”),evaluate 检查后再选择匹配方式
277
+
278
+ 5. **API 路径从代码中的接口调用读取,或从 API 文档获取**
279
+ 6. **checkElement 的 expect 从代码逻辑推断**
280
+ 7. **compare 的 regions 从设计稿分析结果或组件布局推断**
257
281
 
258
282
  ## 微前端测试指南(实战经验)
259
283
 
@@ -352,7 +376,7 @@ node "$EXECUTOR" \
352
376
  --output /tmp/browser-observe.json
353
377
  ```
354
378
 
355
- `observe` 输出 `frames`、`tree`、`interactiveElements[index]`、`selectorCandidates`、`bbox`、`state`。Agent 后续应优先使用 `index` 调用 `act`,不要从 DOM 树重新猜 selector。直接 `--action observe` 默认只连接用户已启动的真实浏览器 CDP;如确需无 CDP 时启动 headless 回退,追加 `--allow-launch`。
379
+ `observe` 输出 `frames`、`tree`、`interactiveElements[index]`、`selectorCandidates`、`bbox`、`state`、`wujieContext`(含 `shadowBodyText` 全文 + `shadowBodyTextLength`)。Agent 后续应优先使用 `index` 调用 `act`,不要从 DOM 树重新猜 selector。直接 `--action observe` 默认只连接用户已启动的真实浏览器 CDP;如确需无 CDP 时启动 headless 回退,追加 `--allow-launch`。
356
380
 
357
381
  ### 观察后按 index 交互(主链路闭环)
358
382
  ```bash
@@ -416,6 +440,38 @@ node "$EXECUTOR" \
416
440
 
417
441
  计划内可直接在 `apiCapture` 后追加 `{ "type": "assertApi", "maxFailed": 0, "forbidStatusGte": 400 }`,会自动读取上一步捕获结果。
418
442
 
443
+ ### 浏览器端 JS 执行(构造 testCase 前提取表单元数据)
444
+ ```bash
445
+ node "$EXECUTOR" \
446
+ --action evaluate \
447
+ --url-contains "platform.aicityos.com" \
448
+ --frame-index 1 \
449
+ --expression '(() => Array.from(document.querySelectorAll(".ant-form-item")).map(item => ({
450
+ label: item.querySelector(".ant-form-item-label")?.textContent?.trim() || "",
451
+ required: !!item.querySelector(".ant-form-item-required"),
452
+ error: item.querySelector(".ant-form-item-explain-error")?.textContent?.trim() || ""
453
+ })).filter(x => x.label))()' \
454
+ --fail-on-falsy \
455
+ --output /tmp/form-fields.json
456
+ ```
457
+
458
+ `--action evaluate` 在目标 frame 中执行任意 JS 表达式(需用 IIFE 包裹),返回 `{ result, passed }`。这是构造 testCase JSON 前的核心步骤:**无源码依赖地提取页面表单元数据、校验错误、Select 选项列表等**。
459
+
460
+ 关键参数:
461
+ - `--expression <js>`:执行的 JS 表达式(必填)
462
+ - `--fail-on-falsy`:结果为 falsy 时标记 FAIL
463
+ - `--expect-result-contains <text>`:结果 JSON 包含指定文本
464
+
465
+ ### 执行测试计划 JSON(ES Module 版)
466
+ ```bash
467
+ node "$EXECUTOR" \
468
+ --action plan \
469
+ --plan-file /tmp/test-plan.json \
470
+ --output /tmp/browser-test-result.json
471
+ ```
472
+
473
+ `--action plan` 使用 ES Module 执行器运行测试计划 JSON,支持全部步骤类型(包括 evaluate、antSelect 等)。与 `--action test-case` 的区别:plan 不走 dataset 循环,适合单次执行。
474
+
419
475
  ### 快速诊断(过渡兼容)
420
476
  ```bash
421
477
  NODE_PATH=~/.szcd-mcp/deps/node_modules node "$SKILL_DIR/local-browser-executor.cjs" \
@@ -502,7 +558,8 @@ NODE_PATH=~/.szcd-mcp/deps/node_modules node "$SKILL_DIR/local-browser-executor.
502
558
  --scroll down --scroll-amount 500
503
559
  ```
504
560
  选择器语法:
505
- - `text=Build` → 按文本内容查找(类似 Playwright text selector)
561
+ - `text=Build` → 按文本内容模糊查找,多个匹配时选 innerText 最短的元素(叶子节点优先,避免命中父容器)
562
+ - `text==Build` → 精确文本匹配(Playwright 风格),只匹配 innerText 完全相等的元素
506
563
  - `role=button` → 按 ARIA role 查找
507
564
  - `~ant-menu-item` → CSS class 模糊匹配(`[class*="ant-menu-item"]`)
508
565
  - `.my-class` → 精确 CSS 选择器
@@ -692,11 +749,15 @@ cd ~/.szcd-mcp/deps && npm install --registry=https://registry.npmmirror.com --@
692
749
  | `formFill` | 批量填充 + 可选 `validateAfter` | `fields[]` |
693
750
  | `closeAllDropdowns` | 关闭所有下拉浮层 | - |
694
751
 
695
- **关键约束**:
696
- - Ant Select 必须 `mousedown` 触发(不是 click)
697
- - 选项异步渲染,内置 15×200ms 轮询
698
- - 每次操作前后自动 `closeAllDropdowns`,`act` 入口检测残留并预清理
699
- - 操作后 VERIFY `.ant-select-selection-item[title]` 回写
752
+ **关键约束(6/22 反馈后更新)**:
753
+ - Ant Select 必须 `mousedown` + `click` 触发(click 触发 React onChange
754
+ - 选项异步渲染,内置 poll 轮询(timeout 默认 3s,不限制 1.5s)
755
+ - `closeAllDropdowns` 去掉 `body >` 限制(适配微前端浮层)+ 增加 300ms sleep
756
+ - 选项点击后 mousedown + mouseup + **click**(三步组合)
757
+ - VERIFY 全局兜底:marker 丢失时全局搜索 `.ant-select-selection-item`
758
+ - VERIFY 检查 `title` + `innerText` + `textContent`(三重兜底)
759
+ - `popupResolved` 变量提升到 for 循环外(避免作用域 bug)
760
+ - 浮层选择器去掉 `body >` 限制(`.ant-select-dropdown` 而非 `body > .ant-select-dropdown`)
700
761
 
701
762
  ```json
702
763
  { "type": "antSelect", "field": "数据集名称", "option": "测试集A" }
@@ -732,6 +793,75 @@ node local-browser-executor.js --action test-case --test-case-file /tmp/case.jso
732
793
  }
733
794
  ```
734
795
 
796
+ ## 脚本执行能力分层
797
+
798
+ | 层级 | 步骤类型 | 执行环境 | 能力 | 适用场景 |
799
+ |------|---------|---------|------|---------|
800
+ | 1 | evaluate | 浏览器端 | DOM 查询/操作 | 简单检查、点击元素 |
801
+ | 2 | rawScript | Node.js 端 | 完整 puppeteer API | 跨 frame、等待、多标签页 |
802
+ | 3 | cdpScript | Node.js 端 | 调用已注册脚本 | 复用通用交互模式 |
803
+ | 4 | 内置 cdpScript | Node.js 端 | 预定义通用 UI 交互 | Drawer/Modal/表格等 |
804
+
805
+ **evaluate**:浏览器端执行 JS,只能操作 DOM,不能等待/跨 frame/调用 puppeteer
806
+ **rawScript**:Node.js 端执行 async 函数,接收 `(page, frame, params, browser)`,可调用所有 puppeteer API
807
+ **registerCdpScript**:注册可复用脚本,后续通过 cdpScript 步骤按 name 调用
808
+ **cdpScript**:调用 registerCdpScript 注册的脚本或内置脚本
809
+
810
+ ### 内置 cdpScript(通用 UI 交互模式)
811
+
812
+ | script | 功能 | params |
813
+ |--------|------|--------|
814
+ | `drawer-action` | Drawer 操作 | `action: 'search'\|'select'\|'confirm'\|'cancel'`, `radioIndex?`, `searchText?`, `searchPlaceholder?` |
815
+ | `modal-action` | Modal/Popconfirm 操作 | `action: 'confirm'\|'cancel'`, `buttonText?` |
816
+ | `switch-tab` | 多标签页切换 | `urlContains?`, `titleIncludes?`, `pageIndex?` |
817
+ | `wait-api` | 等待 API 响应 | `urlPattern`, `timeout?`, `method?` |
818
+ | `get-table-data` | 提取表格数据 | `selector?`, `maxRows?` |
819
+
820
+ **设计原则**:cdpScript 内置通用 UI 交互模式(跨项目复用),业务流程由 LLM 用 rawScript 动态生成
821
+
822
+ ```json
823
+ // 内置 cdpScript 示例
824
+ { "type": "cdpScript", "script": "get-table-data", "params": { "maxRows": 5 } }
825
+ { "type": "cdpScript", "script": "drawer-action", "params": { "action": "select", "radioIndex": 0 } }
826
+ { "type": "cdpScript", "script": "drawer-action", "params": { "action": "confirm" } }
827
+ { "type": "cdpScript", "script": "modal-action", "params": { "action": "confirm" } }
828
+ { "type": "cdpScript", "script": "switch-tab", "params": { "urlContains": "localhost:8088" } }
829
+
830
+ // rawScript 示例(LLM 动态生成业务逻辑)
831
+ { "type": "rawScript", "code": "async (page, frame, params, browser) => { var count = await frame.evaluate(() => document.querySelectorAll('tr').length); return { acted: true, passed: count > 0, rowCount: count, status: 'PASS' } }" }
832
+
833
+ // registerCdpScript + cdpScript(先注册再调用)
834
+ { "type": "registerCdpScript", "name": "my-check", "code": "async (page, frame, params) => { ... }" }
835
+ { "type": "cdpScript", "script": "my-check", "params": { "id": "123" } }
836
+ ```
837
+
838
+ ### evaluate 增强
839
+
840
+ ```json
841
+ // expectResult:精确匹配返回值
842
+ { "type": "evaluate", "expression": "...", "expectResult": "found" }
843
+
844
+ // expectResultContains:包含匹配返回值
845
+ { "type": "evaluate", "expression": "...", "expectResultContains": "数据资源" }
846
+
847
+ // failOnFalsy:返回 falsy 时标记 FAIL
848
+ { "type": "evaluate", "expression": "...", "failOnFalsy": true }
849
+ ```
850
+
851
+ ### apiCall(API 直调)
852
+
853
+ ```json
854
+ { "type": "apiCall", "method": "POST", "url": "http://localhost:8088/api/submit", "body": { "id": "123" } }
855
+ // → { status: 200, data: { code: "200" }, passed: true }
856
+ ```
857
+
858
+ ### switchPage(多标签页切换)
859
+
860
+ ```json
861
+ { "type": "switchPage", "urlContains": "localhost:8088" }
862
+ // → { passed: true, url: "http://localhost:8088/...", title: "..." }
863
+ ```
864
+
735
865
  ## 语义断言(expect)
736
866
 
737
867
  | assertion | 功能 |
@@ -762,20 +892,26 @@ node local-browser-executor.js --action test-case --test-case-file /tmp/case.jso
762
892
  {
763
893
  "title": "数据集创建 P0",
764
894
  "setup": {
765
- "findFrame": { "contentContains": "数据集目录" }
895
+ "findFrame": { "frameUrlContains": "#/catalog/" }
766
896
  },
767
897
  "dataset": [{ "name": "A" }, { "name": "B" }],
768
898
  "steps": [
769
- { "type": "apiCapture", "urlPattern": "/api/dataset", "wait": 5000, "includeAllTargets": true },
899
+ { "type": "navigate", "url": "http://localhost:8088/#/szc-ai-data/catalog/catalog-manage", "waitUntil": "domcontentloaded" },
900
+ { "type": "expect", "assertion": "locator.toBeVisible", "selector": ".ant-table", "timeout": 20000 },
770
901
  { "type": "antInput", "field": "名称", "value": "{{name}}" },
771
902
  { "type": "antSelect", "field": "类型", "option": "图像" },
772
- { "type": "act", "click": "text=提交" },
903
+ { "type": "act", "click": "text==提 交" },
773
904
  { "type": "expect", "assertion": "validation.toPass" },
774
- { "type": "expect", "assertion": "api.toAllPass", "scope": "current-frame" }
905
+ { "type": "expect", "assertion": "locator.toBeVisible", "selector": ".ant-table", "timeout": 15000 }
775
906
  ]
776
907
  }
777
908
  ```
778
909
 
910
+ **findFrame 三种方案(按稳定性排序)**:
911
+ - `frameSelector: "iframe"` — 基于 DOM 选择器(最稳定,推荐)
912
+ - `frameUrlContains: "#/catalog/"` — 基于 URL 子串(navigate 后可能失效)
913
+ - `frameContentContains: "数据集目录"` — 基于内容文字(页面未加载时不可用)
914
+
779
915
  **关键参数:**
780
916
 
781
917
  - `setup.findFrame.contentContains` — 一次定位子应用 frame