@szc-ft/mcp-szcd-client 0.27.2 → 0.27.4
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/browser-engine.js +703 -3
- package/local-browser-executor.js +231 -3
- package/opencode-extension/skills/local-browser-test/SKILL.md +112 -1
- 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 +112 -1
- package/qwen-extension/skills/local-browser-test/local-browser-executor.cjs +15 -0
- package/standard-skill/local-browser-test/SKILL.md +112 -1
- 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
- package/standard-skill/local-browser-test/local-browser-executor-old.cjs +0 -395
|
@@ -5,11 +5,14 @@
|
|
|
5
5
|
* 用法:
|
|
6
6
|
* node local-browser-executor.js --action observe --output /tmp/observe.json
|
|
7
7
|
* node local-browser-executor.js --action act --index 1 --observe-after
|
|
8
|
+
* node local-browser-executor.js --action act --menu-path "目录管理/数据集目录" --observe-after
|
|
9
|
+
* node local-browser-executor.js --action api-capture --wait 15000 --include-response-body
|
|
10
|
+
* node local-browser-executor.js --action assert-api --capture-file /tmp/browser-api-capture.json --max-failed 0
|
|
8
11
|
* node local-browser-executor.js --plan '<JSON>' --output /tmp/result.json
|
|
9
12
|
* node local-browser-executor.js --plan-file /path/to/plan.json --output /tmp/result.json
|
|
10
13
|
*
|
|
11
14
|
* 参数:
|
|
12
|
-
* --action 直接执行动作,当前支持 observe/act/assert
|
|
15
|
+
* --action 直接执行动作,当前支持 observe/act/assert/api-capture
|
|
13
16
|
* --plan 测试计划 JSON 字符串
|
|
14
17
|
* --plan-file 测试计划 JSON 文件路径(与 --plan 二选一)
|
|
15
18
|
* --output 结果输出文件路径(默认 stdout)
|
|
@@ -93,6 +96,12 @@ function parseArgs() {
|
|
|
93
96
|
case "--hover":
|
|
94
97
|
parsed.hover = args[++i];
|
|
95
98
|
break;
|
|
99
|
+
case "--menu-path":
|
|
100
|
+
parsed.menuPath = args[++i];
|
|
101
|
+
break;
|
|
102
|
+
case "--wait-after-each":
|
|
103
|
+
parsed.waitAfterEach = args[++i];
|
|
104
|
+
break;
|
|
96
105
|
case "--observe-after":
|
|
97
106
|
parsed.observeAfter = true;
|
|
98
107
|
break;
|
|
@@ -111,12 +120,102 @@ function parseArgs() {
|
|
|
111
120
|
case "--has-text":
|
|
112
121
|
parsed.hasText = args[++i];
|
|
113
122
|
break;
|
|
123
|
+
case "--wait":
|
|
124
|
+
parsed.wait = args[++i];
|
|
125
|
+
break;
|
|
126
|
+
case "--method":
|
|
127
|
+
parsed.method = args[++i];
|
|
128
|
+
break;
|
|
129
|
+
case "--url-pattern":
|
|
130
|
+
parsed.urlPattern = args[++i];
|
|
131
|
+
break;
|
|
132
|
+
case "--target-url-contains":
|
|
133
|
+
parsed.targetUrlContains = args[++i];
|
|
134
|
+
break;
|
|
135
|
+
case "--include-request-body":
|
|
136
|
+
parsed.includeRequestBody = true;
|
|
137
|
+
break;
|
|
138
|
+
case "--no-request-body":
|
|
139
|
+
parsed.includeRequestBody = false;
|
|
140
|
+
break;
|
|
141
|
+
case "--include-response-body":
|
|
142
|
+
parsed.includeResponseBody = true;
|
|
143
|
+
break;
|
|
144
|
+
case "--reload":
|
|
145
|
+
parsed.reload = true;
|
|
146
|
+
break;
|
|
147
|
+
case "--include-all-targets":
|
|
148
|
+
parsed.includeAllTargets = true;
|
|
149
|
+
break;
|
|
150
|
+
case "--capture-file":
|
|
151
|
+
parsed.captureFile = args[++i];
|
|
152
|
+
break;
|
|
153
|
+
case "--max-failed":
|
|
154
|
+
parsed.maxFailed = args[++i];
|
|
155
|
+
break;
|
|
156
|
+
case "--forbid-status-gte":
|
|
157
|
+
parsed.forbidStatusGte = args[++i];
|
|
158
|
+
break;
|
|
159
|
+
case "--required-url":
|
|
160
|
+
parsed.requiredUrls = [...(parsed.requiredUrls || []), args[++i]];
|
|
161
|
+
break;
|
|
162
|
+
case "--required-method":
|
|
163
|
+
parsed.requiredMethods = [...(parsed.requiredMethods || []), args[++i]];
|
|
164
|
+
break;
|
|
165
|
+
case "--min-total":
|
|
166
|
+
parsed.minTotal = args[++i];
|
|
167
|
+
break;
|
|
114
168
|
case "--base-url":
|
|
115
169
|
parsed.baseUrl = args[++i];
|
|
116
170
|
break;
|
|
117
171
|
case "--allow-launch":
|
|
118
172
|
parsed.allowLaunch = true;
|
|
119
173
|
break;
|
|
174
|
+
case "--task":
|
|
175
|
+
parsed.task = args[++i];
|
|
176
|
+
break;
|
|
177
|
+
case "--menu-api-pattern":
|
|
178
|
+
parsed.menuApiPattern = args[++i];
|
|
179
|
+
break;
|
|
180
|
+
case "--menu-limit":
|
|
181
|
+
parsed.menuLimit = Number.parseInt(args[++i], 10);
|
|
182
|
+
break;
|
|
183
|
+
case "--menu-path-prefix":
|
|
184
|
+
parsed.menuPathPrefix = (parsed.menuPathPrefix || []).concat(args[++i]);
|
|
185
|
+
break;
|
|
186
|
+
case "--menu-filter":
|
|
187
|
+
parsed.menuFilter = (parsed.menuFilter || []).concat(args[++i]);
|
|
188
|
+
break;
|
|
189
|
+
case "--drawer-trigger-selector":
|
|
190
|
+
parsed.drawerTriggerSelector = args[++i];
|
|
191
|
+
break;
|
|
192
|
+
case "--drawer-open-selector":
|
|
193
|
+
parsed.drawerOpenSelector = args[++i];
|
|
194
|
+
break;
|
|
195
|
+
case "--menu-scroll-container":
|
|
196
|
+
parsed.menuScrollContainer = args[++i];
|
|
197
|
+
break;
|
|
198
|
+
case "--use-native-hover":
|
|
199
|
+
parsed.useNativeHover = args[++i] !== "false";
|
|
200
|
+
break;
|
|
201
|
+
case "--max-retry-per-step":
|
|
202
|
+
parsed.maxRetryPerStep = Number.parseInt(args[++i], 10);
|
|
203
|
+
break;
|
|
204
|
+
case "--snapshot-each":
|
|
205
|
+
parsed.snapshotEach = true;
|
|
206
|
+
break;
|
|
207
|
+
case "--screenshot-each":
|
|
208
|
+
parsed.screenshotEach = true;
|
|
209
|
+
break;
|
|
210
|
+
case "--continue-on-fail":
|
|
211
|
+
parsed.continueOnFail = args[++i] !== "false";
|
|
212
|
+
break;
|
|
213
|
+
case "--no-continue-on-fail":
|
|
214
|
+
parsed.continueOnFail = false;
|
|
215
|
+
break;
|
|
216
|
+
case "--capture-input-file":
|
|
217
|
+
parsed.captureInputFile = args[++i];
|
|
218
|
+
break;
|
|
120
219
|
case "--help":
|
|
121
220
|
case "-h":
|
|
122
221
|
printHelp();
|
|
@@ -133,12 +232,16 @@ function printHelp() {
|
|
|
133
232
|
用法:
|
|
134
233
|
node local-browser-executor.js --action observe [options]
|
|
135
234
|
node local-browser-executor.js --action act --index 1 --observe-after [options]
|
|
235
|
+
node local-browser-executor.js --action act --menu-path "目录管理/数据集目录" --observe-after [options]
|
|
136
236
|
node local-browser-executor.js --action assert --selector ".ant-table" --expect-visible [options]
|
|
237
|
+
node local-browser-executor.js --action api-capture --wait 15000 --include-response-body [options]
|
|
238
|
+
node local-browser-executor.js --action assert-api --capture-file /tmp/browser-api-capture.json --max-failed 0 [options]
|
|
239
|
+
node local-browser-executor.js --action run-task --task menu-api-scan --capture-input-file /tmp/menu-tree.json [options]
|
|
137
240
|
node local-browser-executor.js --plan '<JSON>' [options]
|
|
138
241
|
node local-browser-executor.js --plan-file <path> [options]
|
|
139
242
|
|
|
140
243
|
选项:
|
|
141
|
-
--action <name> 直接执行动作,当前支持 observe/act/assert
|
|
244
|
+
--action <name> 直接执行动作,当前支持 observe/act/assert/api-capture/assert-api/run-task
|
|
142
245
|
--plan <json> 测试计划 JSON 字符串
|
|
143
246
|
--plan-file <path> 测试计划 JSON 文件路径
|
|
144
247
|
--output <path> 结果输出文件路径(默认 stdout)
|
|
@@ -156,14 +259,45 @@ function printHelp() {
|
|
|
156
259
|
--type <text> act 时输入文本
|
|
157
260
|
--clear act 输入前清空目标
|
|
158
261
|
--hover <selector> act 时悬停 selector/text=/role=/~ 目标
|
|
262
|
+
--menu-path <a/b> act 时按菜单路径展开并点击,如 "目录管理/数据集目录"
|
|
263
|
+
--wait-after-each <ms> menu-path 每步点击后的等待时间(默认 300)
|
|
159
264
|
--observe-after act 后再次 observe,返回 before/act/after 闭环结果
|
|
160
265
|
--url-contains-assert <text> assert 当前 URL 包含文本
|
|
161
266
|
--text-contains <text> assert 页面文本包含内容
|
|
162
267
|
--expect-visible assert selector 可见
|
|
163
268
|
--min-count <n> assert selector 数量下限
|
|
164
269
|
--has-text <text> assert selector 文本包含内容
|
|
270
|
+
--wait <ms> api-capture 监听时长(默认 15000)
|
|
271
|
+
--method <GET|POST> api-capture 按请求方法过滤
|
|
272
|
+
--url-pattern <text> api-capture 按 URL 片段过滤
|
|
273
|
+
--target-url-contains <text> api-capture 按 target URL 过滤 iframe/page target
|
|
274
|
+
--include-request-body api-capture 包含请求体(默认包含)
|
|
275
|
+
--no-request-body api-capture 不包含请求体
|
|
276
|
+
--include-response-body api-capture 调用 Network.getResponseBody 获取响应体
|
|
277
|
+
--reload api-capture 开始监听后刷新页面触发请求
|
|
278
|
+
--include-all-targets api-capture 不限制到匹配 frame,监听所有 page/iframe targets
|
|
279
|
+
--capture-file <path> assert-api 读取 api-capture JSON 结果
|
|
280
|
+
--max-failed <n> assert-api 允许失败请求数上限
|
|
281
|
+
--forbid-status-gte <n> assert-api 禁止出现大于等于该状态码的响应
|
|
282
|
+
--required-url <text> assert-api 要求出现的 URL 片段,可重复传入
|
|
283
|
+
--required-method <GET|POST> assert-api 要求出现的请求方法,可重复传入
|
|
284
|
+
--min-total <n> assert-api 要求捕获请求总数下限
|
|
165
285
|
--allow-launch action 模式允许无 CDP 时启动 headless Chrome
|
|
166
286
|
--base-url <url> 基础 URL,替换计划中的 {{baseUrl}} 占位符
|
|
287
|
+
--task <name> run-task 任务名(当前仅支持 menu-api-scan)
|
|
288
|
+
--capture-input-file <path> run-task 读取已抓到的菜单接口 JSON 作为 lastApiCapture
|
|
289
|
+
--menu-api-pattern <regex> run-task 在 lastApiCapture 中筛选菜单接口的正则(默认 listUserViewMenus)
|
|
290
|
+
--menu-limit <n> run-task 仅探索前 N 条菜单
|
|
291
|
+
--menu-path-prefix <text> run-task 仅探索 path 以此开头的菜单(可重复)
|
|
292
|
+
--menu-filter <text> run-task 仅探索 name/path 包含此关键字的菜单(可重复)
|
|
293
|
+
--drawer-trigger-selector <css> run-task / act 抽屉式菜单的触发器选择器
|
|
294
|
+
--drawer-open-selector <css> run-task / act 抽屉打开态选择器(默认 .ant-drawer-open)
|
|
295
|
+
--menu-scroll-container <css> run-task / act 菜单容器需要滚动时的容器选择器
|
|
296
|
+
--use-native-hover <true|false> run-task / act 使用 puppeteer 原生 hover 触发子菜单(默认 true)
|
|
297
|
+
--max-retry-per-step <n> run-task / act 每级菜单查找重试次数(默认 2)
|
|
298
|
+
--snapshot-each run-task 每条菜单成功后做一次轻量 observe
|
|
299
|
+
--screenshot-each run-task 失败时截图(依赖 outputDir,默认关闭)
|
|
300
|
+
--no-continue-on-fail run-task 第一条失败即终止(默认遇错继续)
|
|
167
301
|
-h, --help 显示帮助
|
|
168
302
|
`);
|
|
169
303
|
}
|
|
@@ -269,6 +403,25 @@ async function executePlan(plan, options) {
|
|
|
269
403
|
|
|
270
404
|
// ==================== 主流程 ====================
|
|
271
405
|
|
|
406
|
+
function executeAssertApiAction(args) {
|
|
407
|
+
if (!args.captureFile) {
|
|
408
|
+
return {
|
|
409
|
+
error: "必须提供 --capture-file 参数",
|
|
410
|
+
errorCode: "CAPTURE_FILE_REQUIRED",
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const captureResult = JSON.parse(fs.readFileSync(args.captureFile, "utf8"));
|
|
415
|
+
const engine = new BrowserEngine();
|
|
416
|
+
return engine.assertApi(captureResult, {
|
|
417
|
+
maxFailed: args.maxFailed,
|
|
418
|
+
forbidStatusGte: args.forbidStatusGte,
|
|
419
|
+
requiredUrls: args.requiredUrls,
|
|
420
|
+
requiredMethods: args.requiredMethods,
|
|
421
|
+
minTotal: args.minTotal,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
272
425
|
async function executeAction(args) {
|
|
273
426
|
const engine = new BrowserEngine();
|
|
274
427
|
const startedAt = new Date().toISOString();
|
|
@@ -308,6 +461,8 @@ async function executeAction(args) {
|
|
|
308
461
|
selector: args.selector,
|
|
309
462
|
click: args.click,
|
|
310
463
|
hover: args.hover,
|
|
464
|
+
menuPath: args.menuPath,
|
|
465
|
+
waitAfterEach: args.waitAfterEach,
|
|
311
466
|
typeText: args.typeText,
|
|
312
467
|
clear: args.clear,
|
|
313
468
|
frameUrlContains: args.frameUrlContains,
|
|
@@ -342,11 +497,69 @@ async function executeAction(args) {
|
|
|
342
497
|
});
|
|
343
498
|
return { type: "assert", ...result, browserInfo: initResult, startTime: startedAt, endTime: new Date().toISOString() };
|
|
344
499
|
}
|
|
500
|
+
case "api-capture":
|
|
501
|
+
case "apiCapture": {
|
|
502
|
+
const result = await engine.captureApi({
|
|
503
|
+
wait: args.wait,
|
|
504
|
+
method: args.method,
|
|
505
|
+
urlPattern: args.urlPattern,
|
|
506
|
+
targetUrlContains: args.targetUrlContains,
|
|
507
|
+
includeRequestBody: args.includeRequestBody,
|
|
508
|
+
includeResponseBody: args.includeResponseBody,
|
|
509
|
+
reload: args.reload,
|
|
510
|
+
includeAllTargets: args.includeAllTargets,
|
|
511
|
+
frameUrlContains: args.frameUrlContains,
|
|
512
|
+
frameContentContains: args.frameContentContains,
|
|
513
|
+
frameIndex: args.frameIndex,
|
|
514
|
+
});
|
|
515
|
+
return { ...result, browserInfo: initResult, startTime: startedAt, endTime: new Date().toISOString() };
|
|
516
|
+
}
|
|
517
|
+
case "run-task":
|
|
518
|
+
case "runTask": {
|
|
519
|
+
// 把外部 captureFile 当作 lastApiCapture 注入 context
|
|
520
|
+
const context = {};
|
|
521
|
+
if (args.captureInputFile) {
|
|
522
|
+
const fs = await import("node:fs");
|
|
523
|
+
try {
|
|
524
|
+
context.lastApiCapture = JSON.parse(fs.readFileSync(args.captureInputFile, "utf8"));
|
|
525
|
+
} catch (err) {
|
|
526
|
+
return {
|
|
527
|
+
error: `读取 capture 输入文件失败: ${err.message}`,
|
|
528
|
+
errorCode: "CAPTURE_INPUT_INVALID",
|
|
529
|
+
startTime: startedAt,
|
|
530
|
+
endTime: new Date().toISOString(),
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
const result = await engine.runTask({
|
|
535
|
+
task: args.task || "menu-api-scan",
|
|
536
|
+
menuApiPattern: args.menuApiPattern,
|
|
537
|
+
menuLimit: args.menuLimit,
|
|
538
|
+
menuPathPrefix: args.menuPathPrefix,
|
|
539
|
+
menuFilter: args.menuFilter,
|
|
540
|
+
drawerTriggerSelector: args.drawerTriggerSelector,
|
|
541
|
+
drawerOpenSelector: args.drawerOpenSelector,
|
|
542
|
+
menuScrollContainer: args.menuScrollContainer,
|
|
543
|
+
useNativeHover: args.useNativeHover,
|
|
544
|
+
maxRetryPerStep: args.maxRetryPerStep,
|
|
545
|
+
snapshotEach: args.snapshotEach,
|
|
546
|
+
screenshotEach: args.screenshotEach,
|
|
547
|
+
continueOnFail: args.continueOnFail,
|
|
548
|
+
apiCaptureOptions: {
|
|
549
|
+
wait: args.wait,
|
|
550
|
+
targetUrlContains: args.targetUrlContains,
|
|
551
|
+
includeAllTargets: args.includeAllTargets,
|
|
552
|
+
includeResponseBody: args.includeResponseBody !== false,
|
|
553
|
+
urlPattern: args.urlPattern,
|
|
554
|
+
},
|
|
555
|
+
}, context);
|
|
556
|
+
return { type: "runTask", ...result, browserInfo: initResult, startTime: startedAt, endTime: new Date().toISOString() };
|
|
557
|
+
}
|
|
345
558
|
default:
|
|
346
559
|
return {
|
|
347
560
|
error: `未知 action: ${args.action}`,
|
|
348
561
|
errorCode: "UNKNOWN_ACTION",
|
|
349
|
-
supportedActions: ["observe", "act", "assert"],
|
|
562
|
+
supportedActions: ["observe", "act", "assert", "api-capture", "assert-api", "run-task"],
|
|
350
563
|
startTime: startedAt,
|
|
351
564
|
endTime: new Date().toISOString(),
|
|
352
565
|
};
|
|
@@ -367,6 +580,21 @@ async function executeAction(args) {
|
|
|
367
580
|
async function main() {
|
|
368
581
|
const args = parseArgs();
|
|
369
582
|
|
|
583
|
+
if (args.action === "assert-api" || args.action === "assertApi") {
|
|
584
|
+
try {
|
|
585
|
+
const results = executeAssertApiAction(args);
|
|
586
|
+
outputResult(results, args.output);
|
|
587
|
+
process.exit(results.error ? 2 : results.passed === false ? 1 : 0);
|
|
588
|
+
} catch (err) {
|
|
589
|
+
const errorResult = {
|
|
590
|
+
error: err.message,
|
|
591
|
+
errorCode: "ASSERT_API_FAILED",
|
|
592
|
+
};
|
|
593
|
+
outputResult(errorResult, args.output);
|
|
594
|
+
process.exit(2);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
370
598
|
if (args.action) {
|
|
371
599
|
const results = await executeAction(args);
|
|
372
600
|
outputResult(results, args.output);
|
|
@@ -79,8 +79,10 @@ AI 直接构造测试计划 JSON:
|
|
|
79
79
|
| type | 功能 | 参数 | 返回 |
|
|
80
80
|
|------|------|------|------|
|
|
81
81
|
| observe | 结构化观察 | `selector`, `depth`, `frameContentContains`, `maxInteractiveElements` | `{ frames, tree, interactiveElements[] }` |
|
|
82
|
-
| act | 按 index/selector 交互 | `index` 或 `selector/click/hover`, `typeText`, `clear`, `frameContentContains` | `{ acted, action, element }` |
|
|
82
|
+
| act | 按 index/selector/menuPath 交互 | `index` 或 `selector/click/hover` 或 `menuPath`, `typeText`, `clear`, `frameContentContains` | `{ acted, action, element/steps }` |
|
|
83
83
|
| assert | 轻量断言 | `selector`, `expect`, `urlContains`, `textContains` | `{ passed, checks }` |
|
|
84
|
+
| apiCapture | API 请求/响应捕获 | `wait`, `urlPattern`, `method`, `includeResponseBody`, `frameContentContains` | `{ targets, requests, summary }` |
|
|
85
|
+
| assertApi | API 捕获结果断言 | `maxFailed`, `requiredUrls`, `forbidStatusGte`, `minTotal` | `{ passed, checks, summary }` |
|
|
84
86
|
| navigate | 导航到 URL | `url`, `waitUntil` | `{ url, title, loadTime }` |
|
|
85
87
|
| screenshot | 页面截图 | `fullPage`, `clip`, `filename` | `{ filepath, width, height }` |
|
|
86
88
|
| compare | 设计稿对比 | `designImagePath`, `threshold`, `regions[]` | `{ fidelity, diffPixels, diffImagePath, regions[] }` |
|
|
@@ -93,6 +95,7 @@ AI 直接构造测试计划 JSON:
|
|
|
93
95
|
| findFrame | 查找微前端 iframe | `urlIncludes` 或 `urlRegex` 或 `titleIncludes` 或 `contentIncludes` 或 `frameIndex` | `{ found, frameUrl, matchedBy, totalFrames }` |
|
|
94
96
|
| loginWait | 等待 SSO 登录完成 | `loginSelector`, `timeout`, `interval` | `{ loggedIn, elapsed, currentUrl }` |
|
|
95
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[] }` |
|
|
96
99
|
|
|
97
100
|
### 场景选择策略
|
|
98
101
|
|
|
@@ -236,6 +239,18 @@ node {client_path}/local-browser-executor.js \
|
|
|
236
239
|
--output /tmp/browser-act-observe.json
|
|
237
240
|
```
|
|
238
241
|
|
|
242
|
+
### 左侧菜单路径稳定点击
|
|
243
|
+
```bash
|
|
244
|
+
node {client_path}/local-browser-executor.js \
|
|
245
|
+
--action act \
|
|
246
|
+
--url-contains "platform.aicityos.com" \
|
|
247
|
+
--menu-path "目录管理/数据集目录" \
|
|
248
|
+
--observe-after \
|
|
249
|
+
--output /tmp/browser-menu-act.json
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
`--menu-path` 会按路径逐级匹配菜单项,非末级优先执行展开,末级执行点击;匹配优先考虑 `role=menuitem`、`.ant-menu-item`、`.ant-menu-submenu-title` 和文本精确/包含匹配。
|
|
253
|
+
|
|
239
254
|
### 轻量 DOM 断言
|
|
240
255
|
```bash
|
|
241
256
|
node {client_path}/local-browser-executor.js \
|
|
@@ -247,6 +262,34 @@ node {client_path}/local-browser-executor.js \
|
|
|
247
262
|
--output /tmp/browser-assert.json
|
|
248
263
|
```
|
|
249
264
|
|
|
265
|
+
### API 请求/响应捕获(真实浏览器 + 微前端)
|
|
266
|
+
```bash
|
|
267
|
+
node {client_path}/local-browser-executor.js \
|
|
268
|
+
--action api-capture \
|
|
269
|
+
--url-contains "platform.aicityos.com" \
|
|
270
|
+
--frame-content-contains "数据集目录" \
|
|
271
|
+
--url-pattern "/api/" \
|
|
272
|
+
--wait 15000 \
|
|
273
|
+
--include-response-body \
|
|
274
|
+
--reload \
|
|
275
|
+
--output /tmp/browser-api-capture.json
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
`api-capture` 使用 CDP `Network.*` 事件监听主页面和可 attach 的 page/iframe target,不使用 `setRequestInterception`,避免阻断真实请求。wujie/qiankun 场景优先传 `--frame-content-contains` 锁定业务 iframe;如需监听全部 target,追加 `--include-all-targets`。
|
|
279
|
+
|
|
280
|
+
### API 捕获结果断言
|
|
281
|
+
```bash
|
|
282
|
+
node {client_path}/local-browser-executor.js \
|
|
283
|
+
--action assert-api \
|
|
284
|
+
--capture-file /tmp/browser-api-capture.json \
|
|
285
|
+
--max-failed 0 \
|
|
286
|
+
--forbid-status-gte 400 \
|
|
287
|
+
--required-url "/list" \
|
|
288
|
+
--output /tmp/browser-api-assert.json
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
计划内可直接在 `apiCapture` 后追加 `{ "type": "assertApi", "maxFailed": 0, "forbidStatusGte": 400 }`,会自动读取上一步捕获结果。
|
|
292
|
+
|
|
250
293
|
### 快速诊断(过渡兼容)
|
|
251
294
|
```bash
|
|
252
295
|
NODE_PATH=~/.szcd-mcp/deps/node_modules node {skill_path}/local-browser-executor.cjs \
|
|
@@ -446,3 +489,71 @@ cd ~/.szcd-mcp/deps && npm install --registry=https://registry.npmmirror.com --@
|
|
|
446
489
|
"outputDir": "/tmp/browser-test-xxx"
|
|
447
490
|
}
|
|
448
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
|
+
|
|
551
|
+
## 非目标声明
|
|
552
|
+
|
|
553
|
+
`local-browser-test` **不**承担以下职责,遇到时显式拒绝并指给正确工具:
|
|
554
|
+
|
|
555
|
+
- ❌ 接口压力测试 / 并发性能验证 → 用 k6、wrk、jmeter
|
|
556
|
+
- ❌ 跨浏览器兼容性矩阵(Safari/Firefox 多版本)→ BrowserStack / SauceLabs
|
|
557
|
+
- ❌ 单元测试 / 组件级测试 → Vitest / Jest / RTL
|
|
558
|
+
- ❌ 视觉回归全量对比(无 baseline 设计稿)→ Percy / Chromatic
|
|
559
|
+
- ✅ 真实浏览器内的功能验证、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
|
@@ -79,8 +79,10 @@ AI 直接构造测试计划 JSON:
|
|
|
79
79
|
| type | 功能 | 参数 | 返回 |
|
|
80
80
|
|------|------|------|------|
|
|
81
81
|
| observe | 结构化观察 | `selector`, `depth`, `frameContentContains`, `maxInteractiveElements` | `{ frames, tree, interactiveElements[] }` |
|
|
82
|
-
| act | 按 index/selector 交互 | `index` 或 `selector/click/hover`, `typeText`, `clear`, `frameContentContains` | `{ acted, action, element }` |
|
|
82
|
+
| act | 按 index/selector/menuPath 交互 | `index` 或 `selector/click/hover` 或 `menuPath`, `typeText`, `clear`, `frameContentContains` | `{ acted, action, element/steps }` |
|
|
83
83
|
| assert | 轻量断言 | `selector`, `expect`, `urlContains`, `textContains` | `{ passed, checks }` |
|
|
84
|
+
| apiCapture | API 请求/响应捕获 | `wait`, `urlPattern`, `method`, `includeResponseBody`, `frameContentContains` | `{ targets, requests, summary }` |
|
|
85
|
+
| assertApi | API 捕获结果断言 | `maxFailed`, `requiredUrls`, `forbidStatusGte`, `minTotal` | `{ passed, checks, summary }` |
|
|
84
86
|
| navigate | 导航到 URL | `url`, `waitUntil` | `{ url, title, loadTime }` |
|
|
85
87
|
| screenshot | 页面截图 | `fullPage`, `clip`, `filename` | `{ filepath, width, height }` |
|
|
86
88
|
| compare | 设计稿对比 | `designImagePath`, `threshold`, `regions[]` | `{ fidelity, diffPixels, diffImagePath, regions[] }` |
|
|
@@ -93,6 +95,7 @@ AI 直接构造测试计划 JSON:
|
|
|
93
95
|
| findFrame | 查找微前端 iframe | `urlIncludes` 或 `urlRegex` 或 `titleIncludes` 或 `contentIncludes` 或 `frameIndex` | `{ found, frameUrl, matchedBy, totalFrames }` |
|
|
94
96
|
| loginWait | 等待 SSO 登录完成 | `loginSelector`, `timeout`, `interval` | `{ loggedIn, elapsed, currentUrl }` |
|
|
95
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[] }` |
|
|
96
99
|
|
|
97
100
|
### 场景选择策略
|
|
98
101
|
|
|
@@ -236,6 +239,18 @@ node {client_path}/local-browser-executor.js \
|
|
|
236
239
|
--output /tmp/browser-act-observe.json
|
|
237
240
|
```
|
|
238
241
|
|
|
242
|
+
### 左侧菜单路径稳定点击
|
|
243
|
+
```bash
|
|
244
|
+
node {client_path}/local-browser-executor.js \
|
|
245
|
+
--action act \
|
|
246
|
+
--url-contains "platform.aicityos.com" \
|
|
247
|
+
--menu-path "目录管理/数据集目录" \
|
|
248
|
+
--observe-after \
|
|
249
|
+
--output /tmp/browser-menu-act.json
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
`--menu-path` 会按路径逐级匹配菜单项,非末级优先执行展开,末级执行点击;匹配优先考虑 `role=menuitem`、`.ant-menu-item`、`.ant-menu-submenu-title` 和文本精确/包含匹配。
|
|
253
|
+
|
|
239
254
|
### 轻量 DOM 断言
|
|
240
255
|
```bash
|
|
241
256
|
node {client_path}/local-browser-executor.js \
|
|
@@ -247,6 +262,34 @@ node {client_path}/local-browser-executor.js \
|
|
|
247
262
|
--output /tmp/browser-assert.json
|
|
248
263
|
```
|
|
249
264
|
|
|
265
|
+
### API 请求/响应捕获(真实浏览器 + 微前端)
|
|
266
|
+
```bash
|
|
267
|
+
node {client_path}/local-browser-executor.js \
|
|
268
|
+
--action api-capture \
|
|
269
|
+
--url-contains "platform.aicityos.com" \
|
|
270
|
+
--frame-content-contains "数据集目录" \
|
|
271
|
+
--url-pattern "/api/" \
|
|
272
|
+
--wait 15000 \
|
|
273
|
+
--include-response-body \
|
|
274
|
+
--reload \
|
|
275
|
+
--output /tmp/browser-api-capture.json
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
`api-capture` 使用 CDP `Network.*` 事件监听主页面和可 attach 的 page/iframe target,不使用 `setRequestInterception`,避免阻断真实请求。wujie/qiankun 场景优先传 `--frame-content-contains` 锁定业务 iframe;如需监听全部 target,追加 `--include-all-targets`。
|
|
279
|
+
|
|
280
|
+
### API 捕获结果断言
|
|
281
|
+
```bash
|
|
282
|
+
node {client_path}/local-browser-executor.js \
|
|
283
|
+
--action assert-api \
|
|
284
|
+
--capture-file /tmp/browser-api-capture.json \
|
|
285
|
+
--max-failed 0 \
|
|
286
|
+
--forbid-status-gte 400 \
|
|
287
|
+
--required-url "/list" \
|
|
288
|
+
--output /tmp/browser-api-assert.json
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
计划内可直接在 `apiCapture` 后追加 `{ "type": "assertApi", "maxFailed": 0, "forbidStatusGte": 400 }`,会自动读取上一步捕获结果。
|
|
292
|
+
|
|
250
293
|
### 快速诊断(过渡兼容)
|
|
251
294
|
```bash
|
|
252
295
|
NODE_PATH=~/.szcd-mcp/deps/node_modules node {skill_path}/local-browser-executor.cjs \
|
|
@@ -446,3 +489,71 @@ cd ~/.szcd-mcp/deps && npm install --registry=https://registry.npmmirror.com --@
|
|
|
446
489
|
"outputDir": "/tmp/browser-test-xxx"
|
|
447
490
|
}
|
|
448
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
|
+
|
|
551
|
+
## 非目标声明
|
|
552
|
+
|
|
553
|
+
`local-browser-test` **不**承担以下职责,遇到时显式拒绝并指给正确工具:
|
|
554
|
+
|
|
555
|
+
- ❌ 接口压力测试 / 并发性能验证 → 用 k6、wrk、jmeter
|
|
556
|
+
- ❌ 跨浏览器兼容性矩阵(Safari/Firefox 多版本)→ BrowserStack / SauceLabs
|
|
557
|
+
- ❌ 单元测试 / 组件级测试 → Vitest / Jest / RTL
|
|
558
|
+
- ❌ 视觉回归全量对比(无 baseline 设计稿)→ Percy / Chromatic
|
|
559
|
+
- ✅ 真实浏览器内的功能验证、API 时序捕获、设计稿单图对比、动态菜单批量扫描
|