@szc-ft/mcp-szcd-client 0.28.2 → 0.29.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.
@@ -17,6 +17,7 @@ skills:
17
17
  - local-browser-test
18
18
  - szcd-component-helper
19
19
  - szcd-design-to-code
20
+ - szcd-upload-zip
20
21
  mcpServers:
21
22
  - szcd-component-helper
22
23
  - sketch-mcp-server
@@ -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
 
@@ -692,11 +716,15 @@ cd ~/.szcd-mcp/deps && npm install --registry=https://registry.npmmirror.com --@
692
716
  | `formFill` | 批量填充 + 可选 `validateAfter` | `fields[]` |
693
717
  | `closeAllDropdowns` | 关闭所有下拉浮层 | - |
694
718
 
695
- **关键约束**:
696
- - Ant Select 必须 `mousedown` 触发(不是 click)
697
- - 选项异步渲染,内置 15×200ms 轮询
698
- - 每次操作前后自动 `closeAllDropdowns`,`act` 入口检测残留并预清理
699
- - 操作后 VERIFY `.ant-select-selection-item[title]` 回写
719
+ **关键约束(6/22 反馈后更新)**:
720
+ - Ant Select 必须 `mousedown` + `click` 触发(click 触发 React onChange
721
+ - 选项异步渲染,内置 poll 轮询(timeout 默认 3s,不限制 1.5s)
722
+ - `closeAllDropdowns` 去掉 `body >` 限制(适配微前端浮层)+ 增加 300ms sleep
723
+ - 选项点击后 mousedown + mouseup + **click**(三步组合)
724
+ - VERIFY 全局兜底:marker 丢失时全局搜索 `.ant-select-selection-item`
725
+ - VERIFY 检查 `title` + `innerText` + `textContent`(三重兜底)
726
+ - `popupResolved` 变量提升到 for 循环外(避免作用域 bug)
727
+ - 浮层选择器去掉 `body >` 限制(`.ant-select-dropdown` 而非 `body > .ant-select-dropdown`)
700
728
 
701
729
  ```json
702
730
  { "type": "antSelect", "field": "数据集名称", "option": "测试集A" }
@@ -732,6 +760,75 @@ node local-browser-executor.js --action test-case --test-case-file /tmp/case.jso
732
760
  }
733
761
  ```
734
762
 
763
+ ## 脚本执行能力分层
764
+
765
+ | 层级 | 步骤类型 | 执行环境 | 能力 | 适用场景 |
766
+ |------|---------|---------|------|---------|
767
+ | 1 | evaluate | 浏览器端 | DOM 查询/操作 | 简单检查、点击元素 |
768
+ | 2 | rawScript | Node.js 端 | 完整 puppeteer API | 跨 frame、等待、多标签页 |
769
+ | 3 | cdpScript | Node.js 端 | 调用已注册脚本 | 复用通用交互模式 |
770
+ | 4 | 内置 cdpScript | Node.js 端 | 预定义通用 UI 交互 | Drawer/Modal/表格等 |
771
+
772
+ **evaluate**:浏览器端执行 JS,只能操作 DOM,不能等待/跨 frame/调用 puppeteer
773
+ **rawScript**:Node.js 端执行 async 函数,接收 `(page, frame, params, browser)`,可调用所有 puppeteer API
774
+ **registerCdpScript**:注册可复用脚本,后续通过 cdpScript 步骤按 name 调用
775
+ **cdpScript**:调用 registerCdpScript 注册的脚本或内置脚本
776
+
777
+ ### 内置 cdpScript(通用 UI 交互模式)
778
+
779
+ | script | 功能 | params |
780
+ |--------|------|--------|
781
+ | `drawer-action` | Drawer 操作 | `action: 'search'\|'select'\|'confirm'\|'cancel'`, `radioIndex?`, `searchText?`, `searchPlaceholder?` |
782
+ | `modal-action` | Modal/Popconfirm 操作 | `action: 'confirm'\|'cancel'`, `buttonText?` |
783
+ | `switch-tab` | 多标签页切换 | `urlContains?`, `titleIncludes?`, `pageIndex?` |
784
+ | `wait-api` | 等待 API 响应 | `urlPattern`, `timeout?`, `method?` |
785
+ | `get-table-data` | 提取表格数据 | `selector?`, `maxRows?` |
786
+
787
+ **设计原则**:cdpScript 内置通用 UI 交互模式(跨项目复用),业务流程由 LLM 用 rawScript 动态生成
788
+
789
+ ```json
790
+ // 内置 cdpScript 示例
791
+ { "type": "cdpScript", "script": "get-table-data", "params": { "maxRows": 5 } }
792
+ { "type": "cdpScript", "script": "drawer-action", "params": { "action": "select", "radioIndex": 0 } }
793
+ { "type": "cdpScript", "script": "drawer-action", "params": { "action": "confirm" } }
794
+ { "type": "cdpScript", "script": "modal-action", "params": { "action": "confirm" } }
795
+ { "type": "cdpScript", "script": "switch-tab", "params": { "urlContains": "localhost:8088" } }
796
+
797
+ // rawScript 示例(LLM 动态生成业务逻辑)
798
+ { "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' } }" }
799
+
800
+ // registerCdpScript + cdpScript(先注册再调用)
801
+ { "type": "registerCdpScript", "name": "my-check", "code": "async (page, frame, params) => { ... }" }
802
+ { "type": "cdpScript", "script": "my-check", "params": { "id": "123" } }
803
+ ```
804
+
805
+ ### evaluate 增强
806
+
807
+ ```json
808
+ // expectResult:精确匹配返回值
809
+ { "type": "evaluate", "expression": "...", "expectResult": "found" }
810
+
811
+ // expectResultContains:包含匹配返回值
812
+ { "type": "evaluate", "expression": "...", "expectResultContains": "数据资源" }
813
+
814
+ // failOnFalsy:返回 falsy 时标记 FAIL
815
+ { "type": "evaluate", "expression": "...", "failOnFalsy": true }
816
+ ```
817
+
818
+ ### apiCall(API 直调)
819
+
820
+ ```json
821
+ { "type": "apiCall", "method": "POST", "url": "http://localhost:8088/api/submit", "body": { "id": "123" } }
822
+ // → { status: 200, data: { code: "200" }, passed: true }
823
+ ```
824
+
825
+ ### switchPage(多标签页切换)
826
+
827
+ ```json
828
+ { "type": "switchPage", "urlContains": "localhost:8088" }
829
+ // → { passed: true, url: "http://localhost:8088/...", title: "..." }
830
+ ```
831
+
735
832
  ## 语义断言(expect)
736
833
 
737
834
  | assertion | 功能 |
@@ -762,20 +859,26 @@ node local-browser-executor.js --action test-case --test-case-file /tmp/case.jso
762
859
  {
763
860
  "title": "数据集创建 P0",
764
861
  "setup": {
765
- "findFrame": { "contentContains": "数据集目录" }
862
+ "findFrame": { "frameUrlContains": "#/catalog/" }
766
863
  },
767
864
  "dataset": [{ "name": "A" }, { "name": "B" }],
768
865
  "steps": [
769
- { "type": "apiCapture", "urlPattern": "/api/dataset", "wait": 5000, "includeAllTargets": true },
866
+ { "type": "navigate", "url": "http://localhost:8088/#/szc-ai-data/catalog/catalog-manage", "waitUntil": "domcontentloaded" },
867
+ { "type": "expect", "assertion": "locator.toBeVisible", "selector": ".ant-table", "timeout": 20000 },
770
868
  { "type": "antInput", "field": "名称", "value": "{{name}}" },
771
869
  { "type": "antSelect", "field": "类型", "option": "图像" },
772
- { "type": "act", "click": "text=提交" },
870
+ { "type": "act", "click": "text==提 交" },
773
871
  { "type": "expect", "assertion": "validation.toPass" },
774
- { "type": "expect", "assertion": "api.toAllPass", "scope": "current-frame" }
872
+ { "type": "expect", "assertion": "locator.toBeVisible", "selector": ".ant-table", "timeout": 15000 }
775
873
  ]
776
874
  }
777
875
  ```
778
876
 
877
+ **findFrame 三种方案(按稳定性排序)**:
878
+ - `frameSelector: "iframe"` — 基于 DOM 选择器(最稳定,推荐)
879
+ - `frameUrlContains: "#/catalog/"` — 基于 URL 子串(navigate 后可能失效)
880
+ - `frameContentContains: "数据集目录"` — 基于内容文字(页面未加载时不可用)
881
+
779
882
  **关键参数:**
780
883
 
781
884
  - `setup.findFrame.contentContains` — 一次定位子应用 frame
@@ -49,6 +49,28 @@ export class AntAdapter {
49
49
  this.componentMeta = options.componentMeta || {};
50
50
  }
51
51
 
52
+ /**
53
+ * 统一的表单项查找 helper(浏览器端执行)
54
+ * 弹窗/抽屉打开时优先在其中搜索,避免页面搜索栏的同名 label 被错误命中
55
+ * 用法:在 frame.evaluate 内调用 _findFormItem(label)
56
+ */
57
+ static get FORM_ITEM_HELPER() {
58
+ return `
59
+ function _findFormItem(label) {
60
+ var modalOrDrawer = document.querySelector('.ant-modal:not(.ant-modal-hidden), .ant-drawer:not(.ant-drawer-hidden), .ant-drawer-open .ant-drawer-content-wrapper');
61
+ var scope = modalOrDrawer || document;
62
+ var items = Array.from(scope.querySelectorAll('.ant-form-item'));
63
+ if (items.length === 0 && scope !== document) {
64
+ items = items.concat(Array.from(document.querySelectorAll('.ant-form-item')));
65
+ }
66
+ return items.find(function(el) {
67
+ var lbl = el.querySelector('.ant-form-item-label label, .ant-form-item-label');
68
+ return lbl && (lbl.innerText || lbl.textContent || '').trim().includes(label);
69
+ }) || null;
70
+ }
71
+ `;
72
+ }
73
+
52
74
  /**
53
75
  * 关闭所有打开的下拉浮层 / 抽屉 / 弹窗
54
76
  * 反馈第 2 条:每次 Select 操作后必须调用,否则下次操作会命中残留选项
@@ -61,9 +83,9 @@ export class AntAdapter {
61
83
  });
62
84
  // 2) 隐藏所有未隐藏的下拉浮层
63
85
  const dropdowns = document.querySelectorAll(
64
- "body > .ant-select-dropdown:not(.ant-select-dropdown-hidden), " +
65
- "body > .ant-cascader-dropdown:not(.ant-cascader-dropdown-hidden), " +
66
- "body > .ant-picker-dropdown:not(.ant-picker-dropdown-hidden)"
86
+ ".ant-select-dropdown:not(.ant-select-dropdown-hidden), " +
87
+ ".ant-cascader-dropdown:not(.ant-cascader-dropdown-hidden), " +
88
+ ".ant-picker-dropdown:not(.ant-picker-dropdown-hidden)"
67
89
  );
68
90
  const count = dropdowns.length;
69
91
  dropdowns.forEach((el) => {
@@ -79,39 +101,45 @@ export class AntAdapter {
79
101
  }).catch(() => ({ closed: 0 }));
80
102
  }
81
103
 
104
+ /**
105
+ * 在 frame 上执行 evaluate,自动注入 _findFormItem helper
106
+ * 后续所有新方法只需调用本方法,无需手动处理弹窗/抽屉优先逻辑
107
+ */
108
+ async _evalWithFormHelper(frame, fnBody) {
109
+ const wrapped = "(function() {" + AntAdapter.FORM_ITEM_HELPER + "return (" + fnBody + ")();\n})()";
110
+ return frame.evaluate(wrapped);
111
+ }
112
+
82
113
  /**
83
114
  * 定位包含指定 label 的 .ant-form-item,返回其内 control 元素的 selector 信息
84
115
  * 不直接返回 ElementHandle(跨 evaluate 边界丢失),而是返回一个临时标记 id
85
116
  */
86
117
  async _markFormItemControl(frame, labelText, controlHint) {
87
- return frame.evaluate(({ label, hint }) => {
88
- const items = Array.from(document.querySelectorAll(".ant-form-item"));
89
- const item = items.find((el) => {
90
- const lbl = el.querySelector(".ant-form-item-label label, .ant-form-item-label");
91
- if (!lbl) return false;
92
- return (lbl.innerText || lbl.textContent || "").trim().includes(label);
93
- });
118
+ const label = labelText.replace(/'/g, "\\'");
119
+ const hint = controlHint;
120
+ return this._evalWithFormHelper(frame, `() => {
121
+ const item = _findFormItem('${label}');
94
122
  if (!item) return { found: false };
95
123
 
96
124
  // 找控件:根据 hint 决定优先匹配何种节点
97
125
  let control;
98
- if (hint === "select") {
126
+ if (${JSON.stringify(hint)} === "select") {
99
127
  control = item.querySelector(".ant-select:not(.ant-select-disabled)");
100
- } else if (hint === "treeSelect") {
128
+ } else if (${JSON.stringify(hint)} === "treeSelect") {
101
129
  control = item.querySelector(".ant-select.ant-tree-select, .ant-tree-select");
102
- } else if (hint === "cascader") {
130
+ } else if (${JSON.stringify(hint)} === "cascader") {
103
131
  control = item.querySelector(".ant-cascader, .ant-cascader-picker");
104
- } else if (hint === "datePicker") {
132
+ } else if (${JSON.stringify(hint)} === "datePicker") {
105
133
  control = item.querySelector(".ant-picker");
106
- } else if (hint === "input") {
134
+ } else if (${JSON.stringify(hint)} === "input") {
107
135
  control = item.querySelector("input.ant-input, textarea.ant-input, .ant-input-affix-wrapper input");
108
- } else if (hint === "radio") {
136
+ } else if (${JSON.stringify(hint)} === "radio") {
109
137
  control = item.querySelector(".ant-radio-group");
110
- } else if (hint === "checkbox") {
138
+ } else if (${JSON.stringify(hint)} === "checkbox") {
111
139
  control = item.querySelector(".ant-checkbox-group, .ant-checkbox-wrapper");
112
- } else if (hint === "switch") {
140
+ } else if (${JSON.stringify(hint)} === "switch") {
113
141
  control = item.querySelector(".ant-switch");
114
- } else if (hint === "upload") {
142
+ } else if (${JSON.stringify(hint)} === "upload") {
115
143
  control = item.querySelector(".ant-upload input[type='file'], input[type='file']");
116
144
  } else {
117
145
  control = item.querySelector("input, .ant-select, .ant-picker, .ant-cascader");
@@ -120,7 +148,7 @@ export class AntAdapter {
120
148
  if (!control) return { found: true, hasControl: false };
121
149
 
122
150
  // 用临时 data 属性标记,避免重复 traversal
123
- const marker = `__sz_marker_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
151
+ const marker = '__sz_marker_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6);
124
152
  control.setAttribute("data-sz-marker", marker);
125
153
  item.setAttribute("data-sz-form-item-marker", marker);
126
154
  const rect = control.getBoundingClientRect();
@@ -131,7 +159,7 @@ export class AntAdapter {
131
159
  tagName: control.tagName,
132
160
  visible: rect.width > 0 && rect.height > 0,
133
161
  };
134
- }, { label: labelText, hint: controlHint });
162
+ }`);
135
163
  }
136
164
 
137
165
  async _clearMarkers(frame) {
@@ -189,12 +217,12 @@ export class AntAdapter {
189
217
  }
190
218
 
191
219
  const selectedSummary = [];
220
+ let popupResolved = popupFrame;
192
221
  for (const opt of options) {
193
222
  // 3) 浮层探测:先在触发器 frame 找,没找到再在 popupFrame 找(兜底)
194
- let popupResolved = popupFrame;
195
223
  let optionFound = await pollEval(popupResolved, (text) => {
196
224
  const items = Array.from(document.querySelectorAll(
197
- "body > .ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option"
225
+ ".ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option"
198
226
  ));
199
227
  const visibleItems = items.filter((el) => {
200
228
  const rect = el.getBoundingClientRect();
@@ -220,7 +248,7 @@ export class AntAdapter {
220
248
  if (parentFrame && parentFrame !== frame) {
221
249
  const retryFound = await pollEval(parentFrame, (text) => {
222
250
  const items = Array.from(document.querySelectorAll(
223
- "body > .ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option"
251
+ ".ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option"
224
252
  ));
225
253
  const visibleItems = items.filter((el) => {
226
254
  const rect = el.getBoundingClientRect();
@@ -257,12 +285,13 @@ export class AntAdapter {
257
285
  };
258
286
  }
259
287
 
260
- // 4) mousedown 选项(在 popupResolved frame 内)
288
+ // 4) 点击选项(在 popupResolved frame 内),需同时触发 mousedown+click 才可驱动 React onChange
261
289
  await popupResolved.evaluate((tag) => {
262
290
  const el = document.querySelector(`[data-sz-option-marker="${tag}"]`);
263
291
  if (!el) return false;
264
292
  el.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true, button: 0 }));
265
293
  el.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true, button: 0 }));
294
+ el.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, button: 0 }));
266
295
  el.removeAttribute("data-sz-option-marker");
267
296
  return true;
268
297
  }, optionFound.value.tag);
@@ -338,8 +367,8 @@ export class AntAdapter {
338
367
 
339
368
  const found = await pollEval(frame, ({ text, isLeaf }) => {
340
369
  const titles = Array.from(document.querySelectorAll(
341
- "body > .ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-tree-title, " +
342
- "body > .ant-tree-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-tree-title"
370
+ ".ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-tree-title, " +
371
+ ".ant-tree-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-tree-title"
343
372
  ));
344
373
  const visible = titles.filter((el) => {
345
374
  const rect = el.getBoundingClientRect();
@@ -408,29 +437,33 @@ export class AntAdapter {
408
437
  */
409
438
  async input(frame, target, value, opts = {}) {
410
439
  const clear = opts.clear !== false;
411
- const result = await frame.evaluate(({ target, value, clear }) => {
412
- let el = null;
413
-
414
- if (target.label) {
415
- const items = Array.from(document.querySelectorAll(".ant-form-item"));
416
- const item = items.find((x) => {
417
- const lbl = x.querySelector(".ant-form-item-label");
418
- return lbl && (lbl.innerText || lbl.textContent || "").trim().includes(target.label);
419
- });
420
- el = item?.querySelector("input.ant-input, textarea.ant-input, .ant-input-affix-wrapper input, input[type='text'], input[type='number'], input[type='password'], input:not([type])");
421
- } else if (target.placeholder) {
422
- el = document.querySelector(`input[placeholder*="${target.placeholder}"], textarea[placeholder*="${target.placeholder}"]`);
423
- } else if (target.selector) {
424
- try { el = document.querySelector(target.selector); } catch { el = null; }
440
+ const label = (target.label || '').replace(/'/g, "\\'");
441
+ const placeholder = (target.placeholder || '').replace(/'/g, "\\'");
442
+ const selector = (target.selector || '').replace(/'/g, "\\'");
443
+
444
+ const result = await this._evalWithFormHelper(frame, `() => {
445
+ var el = null;
446
+ var val = ${JSON.stringify(value)};
447
+ var shouldClear = ${clear};
448
+
449
+ var item = _findFormItem('${label}');
450
+ if (item) {
451
+ el = item.querySelector("input.ant-input, textarea.ant-input, .ant-input-affix-wrapper input, input[type='text'], input[type='number'], input[type='password'], input:not([type])");
452
+ }
453
+ if (!el && '${placeholder}') {
454
+ el = document.querySelector('input[placeholder*="${placeholder}"], textarea[placeholder*="${placeholder}"]');
455
+ }
456
+ if (!el && '${selector}') {
457
+ try { el = document.querySelector('${selector}'); } catch(e) { el = null; }
425
458
  }
426
459
 
427
460
  if (!el) return { found: false };
428
461
  el.scrollIntoView({ block: "center" });
429
- const proto = Object.getPrototypeOf(el);
430
- const valueSetter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
462
+ var proto = Object.getPrototypeOf(el);
463
+ var valueSetter = Object.getOwnPropertyDescriptor(proto, "value") && Object.getOwnPropertyDescriptor(proto, "value").set;
431
464
 
432
465
  el.focus();
433
- const newValue = clear ? value : (el.value || "") + value;
466
+ var newValue = shouldClear ? val : (el.value || "") + val;
434
467
  if (valueSetter) {
435
468
  valueSetter.call(el, newValue);
436
469
  } else {
@@ -444,7 +477,7 @@ export class AntAdapter {
444
477
  placeholder: el.getAttribute("placeholder"),
445
478
  tagName: el.tagName,
446
479
  };
447
- }, { target, value, clear });
480
+ }`);
448
481
 
449
482
  return {
450
483
  acted: result.found,
@@ -460,27 +493,26 @@ export class AntAdapter {
460
493
  * Ant Radio 选择
461
494
  */
462
495
  async radio(frame, fieldLabel, optionLabel) {
463
- const result = await frame.evaluate(({ field, opt }) => {
464
- const items = Array.from(document.querySelectorAll(".ant-form-item"));
465
- const item = items.find((x) => {
466
- const lbl = x.querySelector(".ant-form-item-label");
467
- return lbl && (lbl.innerText || lbl.textContent || "").trim().includes(field);
468
- });
496
+ const field = fieldLabel.replace(/'/g, "\\'");
497
+ const opt = optionLabel.replace(/'/g, "\\'");
498
+
499
+ const result = await this._evalWithFormHelper(frame, `() => {
500
+ var item = _findFormItem('${field}');
469
501
  if (!item) return { found: false, reason: "form item not found" };
470
502
 
471
- const wrappers = Array.from(item.querySelectorAll(".ant-radio-wrapper"));
472
- const target = wrappers.find((w) => (w.innerText || "").trim().includes(opt));
473
- if (!target) return { found: false, reason: `radio option "${opt}" not found`, available: wrappers.map((w) => (w.innerText || "").trim()) };
503
+ var wrappers = Array.from(item.querySelectorAll(".ant-radio-wrapper"));
504
+ var target = wrappers.find(function(w) { return (w.innerText || "").trim().includes('${opt}'); });
505
+ if (!target) return { found: false, reason: 'radio option not found', available: wrappers.map(function(w) { return (w.innerText || "").trim(); }) };
474
506
 
475
507
  target.scrollIntoView({ block: "center" });
476
508
  target.click();
477
- const input = target.querySelector("input[type='radio']");
509
+ var input = target.querySelector("input[type='radio']");
478
510
  return {
479
511
  found: true,
480
- checked: input?.checked || false,
481
- value: input?.value || "",
512
+ checked: input && input.checked || false,
513
+ value: input && input.value || "",
482
514
  };
483
- }, { field: fieldLabel, opt: optionLabel });
515
+ }`);
484
516
  return {
485
517
  acted: result.found,
486
518
  field: fieldLabel,
@@ -495,24 +527,20 @@ export class AntAdapter {
495
527
  * Ant Checkbox 切换(按 label 文本)
496
528
  */
497
529
  async checkbox(frame, fieldLabel, optionLabel, checked = true) {
498
- const result = await frame.evaluate(({ field, opt, target }) => {
499
- const items = Array.from(document.querySelectorAll(".ant-form-item"));
500
- const item = items.find((x) => {
501
- const lbl = x.querySelector(".ant-form-item-label");
502
- return lbl && (lbl.innerText || lbl.textContent || "").trim().includes(field);
503
- });
530
+ const field = fieldLabel.replace(/'/g, "\\'");
531
+ const opt = (optionLabel || '').replace(/'/g, "\\'");
532
+
533
+ const result = await this._evalWithFormHelper(frame, `() => {
534
+ var item = _findFormItem('${field}');
504
535
  if (!item) return { found: false };
505
- // 支持 group 和单独 checkbox
506
- const wrappers = Array.from(item.querySelectorAll(".ant-checkbox-wrapper"));
507
- const wrapper = opt
508
- ? wrappers.find((w) => (w.innerText || "").includes(opt))
509
- : wrappers[0];
536
+ var wrappers = Array.from(item.querySelectorAll(".ant-checkbox-wrapper"));
537
+ var wrapper = '${opt}' ? wrappers.find(function(w) { return (w.innerText || "").includes('${opt}'); }) : wrappers[0];
510
538
  if (!wrapper) return { found: false, reason: "checkbox not found" };
511
- const input = wrapper.querySelector("input[type='checkbox']");
539
+ var input = wrapper.querySelector("input[type='checkbox']");
512
540
  if (!input) return { found: false };
513
- if (input.checked !== target) wrapper.click();
541
+ if (input.checked !== ${checked}) wrapper.click();
514
542
  return { found: true, checked: input.checked };
515
- }, { field: fieldLabel, opt: optionLabel, target: checked });
543
+ }`);
516
544
  return { acted: result.found, field: fieldLabel, option: optionLabel, verified: result.checked === checked };
517
545
  }
518
546
 
@@ -520,19 +548,17 @@ export class AntAdapter {
520
548
  * Ant Switch 切换
521
549
  */
522
550
  async switch(frame, fieldLabel, on = true) {
523
- const result = await frame.evaluate(({ field, target }) => {
524
- const items = Array.from(document.querySelectorAll(".ant-form-item"));
525
- const item = items.find((x) => {
526
- const lbl = x.querySelector(".ant-form-item-label");
527
- return lbl && (lbl.innerText || lbl.textContent || "").trim().includes(field);
528
- });
551
+ const field = fieldLabel.replace(/'/g, "\\'");
552
+
553
+ const result = await this._evalWithFormHelper(frame, `() => {
554
+ var item = _findFormItem('${field}');
529
555
  if (!item) return { found: false };
530
- const sw = item.querySelector(".ant-switch");
556
+ var sw = item.querySelector(".ant-switch");
531
557
  if (!sw) return { found: false };
532
- const isOn = sw.getAttribute("aria-checked") === "true" || sw.classList.contains("ant-switch-checked");
533
- if (isOn !== target) sw.click();
534
- return { found: true, state: target };
535
- }, { field: fieldLabel, target: on });
558
+ var isOn = sw.getAttribute("aria-checked") === "true" || sw.classList.contains("ant-switch-checked");
559
+ if (isOn !== ${on}) sw.click();
560
+ return { found: true, state: ${on} };
561
+ }`);
536
562
  return { acted: result.found, field: fieldLabel, state: result.state, verified: result.state === on };
537
563
  }
538
564
 
@@ -541,29 +567,27 @@ export class AntAdapter {
541
567
  */
542
568
  async datePicker(frame, fieldLabel, dateString) {
543
569
  await this.closeAllDropdowns(frame);
544
- const result = await frame.evaluate(({ field, value }) => {
545
- const items = Array.from(document.querySelectorAll(".ant-form-item"));
546
- const item = items.find((x) => {
547
- const lbl = x.querySelector(".ant-form-item-label");
548
- return lbl && (lbl.innerText || lbl.textContent || "").trim().includes(field);
549
- });
570
+ const field = fieldLabel.replace(/'/g, "\\'");
571
+ const val = (dateString || '').replace(/'/g, "\\'");
572
+
573
+ const result = await this._evalWithFormHelper(frame, `() => {
574
+ var item = _findFormItem('${field}');
550
575
  if (!item) return { found: false };
551
- const picker = item.querySelector(".ant-picker");
552
- const input = picker?.querySelector("input");
576
+ var picker = item.querySelector(".ant-picker");
577
+ var input = picker && picker.querySelector("input");
553
578
  if (!input) return { found: false };
554
579
 
555
580
  input.scrollIntoView({ block: "center" });
556
581
  input.focus();
557
- const setter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(input), "value")?.set;
558
- if (setter) setter.call(input, value);
559
- else input.value = value;
582
+ var setter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(input), "value") && Object.getOwnPropertyDescriptor(Object.getPrototypeOf(input), "value").set;
583
+ if (setter) setter.call(input, '${val}');
584
+ else input.value = '${val}';
560
585
  input.dispatchEvent(new Event("input", { bubbles: true }));
561
586
  input.dispatchEvent(new Event("change", { bubbles: true }));
562
- // 回车确认
563
587
  input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
564
588
  input.blur();
565
589
  return { found: true, value: input.value };
566
- }, { field: fieldLabel, value: dateString });
590
+ }`);
567
591
  await this.closeAllDropdowns(frame);
568
592
  return { acted: result.found, field: fieldLabel, value: result.value, verified: result.value === dateString };
569
593
  }
@@ -596,7 +620,7 @@ export class AntAdapter {
596
620
  const seg = segments[i];
597
621
  const result = await pollEval(frame, (text) => {
598
622
  const all = Array.from(document.querySelectorAll(
599
- "body > .ant-cascader-dropdown:not(.ant-cascader-dropdown-hidden) .ant-cascader-menu-item"
623
+ ".ant-cascader-dropdown:not(.ant-cascader-dropdown-hidden) .ant-cascader-menu-item"
600
624
  ));
601
625
  const visible = all.filter((el) => {
602
626
  const r = el.getBoundingClientRect();
@@ -625,21 +649,16 @@ export class AntAdapter {
625
649
  * Upload 文件上传
626
650
  */
627
651
  async upload(frame, fieldLabel, filePath) {
628
- // 注意:puppeteer 上传文件需要通过 ElementHandle.uploadFile,evaluate 内做不到
629
- // 这里只能返回需要上层处理的标志
630
- const result = await frame.evaluate((field) => {
631
- const items = Array.from(document.querySelectorAll(".ant-form-item"));
632
- const item = items.find((x) => {
633
- const lbl = x.querySelector(".ant-form-item-label");
634
- return lbl && (lbl.innerText || lbl.textContent || "").trim().includes(field);
635
- });
652
+ const field = fieldLabel.replace(/'/g, "\\'");
653
+ const result = await this._evalWithFormHelper(frame, `() => {
654
+ var item = _findFormItem('${field}');
636
655
  if (!item) return { found: false };
637
- const input = item.querySelector("input[type='file']");
656
+ var input = item.querySelector("input[type='file']");
638
657
  if (!input) return { found: false };
639
- const marker = `__sz_upload_${Date.now()}`;
658
+ var marker = '__sz_upload_' + Date.now();
640
659
  input.setAttribute("data-sz-marker", marker);
641
- return { found: true, marker };
642
- }, fieldLabel);
660
+ return { found: true, marker: marker };
661
+ }`);
643
662
 
644
663
  if (!result.found) return { acted: false, reason: "upload input not found" };
645
664