@szc-ft/mcp-szcd-client 0.27.1 → 0.27.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/szcd-mcp-browser-test.md +1 -1
- package/lib/browser-engine.js +831 -5
- package/lib/shared-deps.js +80 -40
- package/lib/visual-compare.js +3 -3
- package/local-browser-executor.js +321 -7
- package/opencode-extension/commands/szcd-mcp-browser-test.md +1 -1
- package/opencode-extension/skills/local-browser-test/SKILL.md +199 -15
- package/opencode-extension/skills/local-browser-test/local-browser-executor-old.cjs +395 -0
- package/opencode-extension/skills/local-browser-test/local-browser-executor.cjs +348 -16
- package/package.json +1 -1
- package/qwen-extension/commands/szcd-mcp-browser-test.md +1 -1
- package/qwen-extension/qwen-extension.json +1 -1
- package/qwen-extension/skills/local-browser-test/SKILL.md +199 -15
- package/qwen-extension/skills/local-browser-test/local-browser-executor-old.cjs +395 -0
- package/qwen-extension/skills/local-browser-test/local-browser-executor.cjs +348 -16
- package/standard-skill/local-browser-test/SKILL.md +199 -15
- package/standard-skill/local-browser-test/local-browser-executor.cjs +348 -16
package/lib/shared-deps.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 共享依赖加载器
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* 首次使用时自动安装,后续直接加载。
|
|
4
|
+
* 浏览器自动化依赖安装到 ~/.szcd-mcp/deps/node_modules/,所有项目共享复用。
|
|
5
|
+
* 主链路 observe/act/assert 只安装 puppeteer-core;视觉对比按需安装 pixelmatch/pngjs/sharp。
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
import { createRequire } from "node:module";
|
|
@@ -14,11 +13,14 @@ import fs from "node:fs";
|
|
|
14
13
|
/** 共享缓存目录 */
|
|
15
14
|
const SHARED_DEPS_DIR = path.join(homedir(), ".szcd-mcp", "deps");
|
|
16
15
|
|
|
17
|
-
/**
|
|
18
|
-
const
|
|
16
|
+
/** 核心浏览器自动化依赖 */
|
|
17
|
+
const CORE_PACKAGES = ["puppeteer-core"];
|
|
19
18
|
|
|
20
|
-
/**
|
|
21
|
-
const
|
|
19
|
+
/** 视觉对比依赖 */
|
|
20
|
+
const VISUAL_COMPARE_PACKAGES = ["pixelmatch", "pngjs", "sharp"];
|
|
21
|
+
|
|
22
|
+
/** 兼容旧导出 */
|
|
23
|
+
const PACKAGES = [...CORE_PACKAGES, ...VISUAL_COMPARE_PACKAGES];
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* 从共享缓存目录加载模块
|
|
@@ -30,27 +32,9 @@ export function loadFromShared(moduleName) {
|
|
|
30
32
|
return require_(moduleName);
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
* 确保共享依赖已安装,未安装则自动安装
|
|
35
|
-
* @returns {Promise<void>}
|
|
36
|
-
*/
|
|
37
|
-
export async function ensureSharedDeps() {
|
|
38
|
-
// 检查标记文件
|
|
39
|
-
if (fs.existsSync(INSTALLED_MARKER)) {
|
|
40
|
-
// 快速验证:puppeteer-core 目录是否存在
|
|
41
|
-
const pcoreDir = path.join(SHARED_DEPS_DIR, "node_modules", "puppeteer-core");
|
|
42
|
-
if (fs.existsSync(pcoreDir)) {
|
|
43
|
-
return; // 已安装
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
process.stderr.write("[shared-deps] 首次使用浏览器测试功能,正在安装共享依赖...\n");
|
|
48
|
-
process.stderr.write(`[shared-deps] 安装目录: ${SHARED_DEPS_DIR}\n`);
|
|
49
|
-
|
|
50
|
-
// 确保目录存在
|
|
35
|
+
function ensurePackageJson() {
|
|
51
36
|
fs.mkdirSync(SHARED_DEPS_DIR, { recursive: true });
|
|
52
37
|
|
|
53
|
-
// 初始化 package.json(如果不存在)
|
|
54
38
|
const pkgPath = path.join(SHARED_DEPS_DIR, "package.json");
|
|
55
39
|
if (!fs.existsSync(pkgPath)) {
|
|
56
40
|
fs.writeFileSync(pkgPath, JSON.stringify({
|
|
@@ -59,39 +43,95 @@ export async function ensureSharedDeps() {
|
|
|
59
43
|
private: true,
|
|
60
44
|
}, null, 2));
|
|
61
45
|
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isPackageInstalled(packageName) {
|
|
49
|
+
try {
|
|
50
|
+
const require_ = createRequire(path.join(SHARED_DEPS_DIR, "node_modules", "_"));
|
|
51
|
+
require_.resolve(packageName);
|
|
52
|
+
return true;
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function installPackages(packages, label) {
|
|
59
|
+
const missing = packages.filter((packageName) => !isPackageInstalled(packageName));
|
|
60
|
+
if (missing.length === 0) return;
|
|
61
|
+
|
|
62
|
+
process.stderr.write(`[shared-deps] 正在安装${label}: ${missing.join(" ")}\n`);
|
|
63
|
+
process.stderr.write(`[shared-deps] 安装目录: ${SHARED_DEPS_DIR}\n`);
|
|
64
|
+
|
|
65
|
+
ensurePackageJson();
|
|
62
66
|
|
|
63
|
-
// 安装依赖
|
|
64
67
|
const { execSync } = await import("node:child_process");
|
|
65
|
-
const pkgList =
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
+
const pkgList = missing.join(" ");
|
|
69
|
+
const registryFlags = [
|
|
70
|
+
"--registry=https://registry.npmmirror.com",
|
|
71
|
+
"--@img:registry=https://registry.npmmirror.com",
|
|
72
|
+
];
|
|
73
|
+
const speedFlags = [
|
|
74
|
+
"--prefer-offline",
|
|
75
|
+
"--no-audit",
|
|
76
|
+
"--no-fund",
|
|
77
|
+
"--omit=dev",
|
|
78
|
+
"--package-lock=false",
|
|
79
|
+
"--loglevel=warn",
|
|
80
|
+
];
|
|
81
|
+
const sharpFlags = missing.includes("sharp") ? [
|
|
82
|
+
"--sharp_binary_host=https://npmmirror.com/mirrors/sharp",
|
|
83
|
+
"--sharp_libvips_binary_host=https://npmmirror.com/mirrors/sharp-libvips",
|
|
84
|
+
] : [];
|
|
85
|
+
const installFlags = [...registryFlags, ...speedFlags, ...sharpFlags].join(" ");
|
|
68
86
|
|
|
69
87
|
try {
|
|
70
|
-
execSync(`npm install ${
|
|
88
|
+
execSync(`npm install ${installFlags} ${pkgList}`.trim(), {
|
|
71
89
|
cwd: SHARED_DEPS_DIR,
|
|
72
90
|
stdio: "inherit",
|
|
73
91
|
timeout: 180000,
|
|
74
92
|
});
|
|
75
93
|
} catch (err) {
|
|
76
94
|
throw new Error(
|
|
77
|
-
|
|
78
|
-
` cd ${SHARED_DEPS_DIR} && npm install ${pkgList}\n` +
|
|
95
|
+
`${label}安装失败。请手动执行:\n` +
|
|
96
|
+
` cd ${SHARED_DEPS_DIR} && npm install ${installFlags} ${pkgList}\n` +
|
|
79
97
|
`原始错误:${err.message}`
|
|
80
98
|
);
|
|
81
99
|
}
|
|
82
100
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
101
|
+
process.stderr.write(`[shared-deps] ${label}安装完成。\n`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function ensureCoreDeps() {
|
|
105
|
+
await installPackages(CORE_PACKAGES, "浏览器核心依赖");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function ensureVisualCompareDeps() {
|
|
109
|
+
await ensureCoreDeps();
|
|
110
|
+
await installPackages(VISUAL_COMPARE_PACKAGES, "视觉对比依赖");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 确保全部共享依赖已安装,保留给旧调用兼容。
|
|
115
|
+
* @returns {Promise<void>}
|
|
116
|
+
*/
|
|
117
|
+
export async function ensureSharedDeps() {
|
|
118
|
+
await ensureVisualCompareDeps();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 检查核心依赖是否已安装
|
|
123
|
+
* @returns {boolean}
|
|
124
|
+
*/
|
|
125
|
+
export function isCoreDepsInstalled() {
|
|
126
|
+
return CORE_PACKAGES.every(isPackageInstalled);
|
|
86
127
|
}
|
|
87
128
|
|
|
88
129
|
/**
|
|
89
|
-
*
|
|
130
|
+
* 检查全部共享依赖是否已安装
|
|
90
131
|
* @returns {boolean}
|
|
91
132
|
*/
|
|
92
133
|
export function isSharedDepsInstalled() {
|
|
93
|
-
return
|
|
94
|
-
fs.existsSync(path.join(SHARED_DEPS_DIR, "node_modules", "puppeteer-core"));
|
|
134
|
+
return PACKAGES.every(isPackageInstalled);
|
|
95
135
|
}
|
|
96
136
|
|
|
97
|
-
export { SHARED_DEPS_DIR, PACKAGES };
|
|
137
|
+
export { SHARED_DEPS_DIR, PACKAGES, CORE_PACKAGES, VISUAL_COMPARE_PACKAGES };
|
package/lib/visual-compare.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import fs from "node:fs";
|
|
17
17
|
import path from "node:path";
|
|
18
|
-
import {
|
|
18
|
+
import { ensureVisualCompareDeps, loadFromShared } from "./shared-deps.js";
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* 全局视觉对比
|
|
@@ -27,8 +27,8 @@ import { ensureSharedDeps, loadFromShared } from "./shared-deps.js";
|
|
|
27
27
|
* @returns {Promise<object>} 对比结果
|
|
28
28
|
*/
|
|
29
29
|
export async function compareDesign(screenshotPath, designImagePath, options = {}) {
|
|
30
|
-
//
|
|
31
|
-
await
|
|
30
|
+
// 视觉对比才安装 pixelmatch/pngjs/sharp,避免 observe 主链路被重依赖阻塞
|
|
31
|
+
await ensureVisualCompareDeps();
|
|
32
32
|
const sharp = loadFromShared("sharp");
|
|
33
33
|
const pixelmatch = loadFromShared("pixelmatch");
|
|
34
34
|
const { PNG } = loadFromShared("pngjs");
|
|
@@ -3,10 +3,16 @@
|
|
|
3
3
|
* 本地浏览器测试执行器 - CLI 入口
|
|
4
4
|
*
|
|
5
5
|
* 用法:
|
|
6
|
+
* node local-browser-executor.js --action observe --output /tmp/observe.json
|
|
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
|
|
6
11
|
* node local-browser-executor.js --plan '<JSON>' --output /tmp/result.json
|
|
7
12
|
* node local-browser-executor.js --plan-file /path/to/plan.json --output /tmp/result.json
|
|
8
13
|
*
|
|
9
14
|
* 参数:
|
|
15
|
+
* --action 直接执行动作,当前支持 observe/act/assert/api-capture
|
|
10
16
|
* --plan 测试计划 JSON 字符串
|
|
11
17
|
* --plan-file 测试计划 JSON 文件路径(与 --plan 二选一)
|
|
12
18
|
* --output 结果输出文件路径(默认 stdout)
|
|
@@ -36,6 +42,9 @@ function parseArgs() {
|
|
|
36
42
|
|
|
37
43
|
for (let i = 0; i < args.length; i++) {
|
|
38
44
|
switch (args[i]) {
|
|
45
|
+
case "--action":
|
|
46
|
+
parsed.action = args[++i];
|
|
47
|
+
break;
|
|
39
48
|
case "--plan":
|
|
40
49
|
parsed.planJson = args[++i];
|
|
41
50
|
break;
|
|
@@ -51,9 +60,117 @@ function parseArgs() {
|
|
|
51
60
|
case "--page-url":
|
|
52
61
|
parsed.pageUrl = args[++i];
|
|
53
62
|
break;
|
|
63
|
+
case "--url-contains":
|
|
64
|
+
parsed.urlContains = args[++i];
|
|
65
|
+
break;
|
|
66
|
+
case "--frame-url-contains":
|
|
67
|
+
parsed.frameUrlContains = args[++i];
|
|
68
|
+
break;
|
|
69
|
+
case "--frame-content-contains":
|
|
70
|
+
parsed.frameContentContains = args[++i];
|
|
71
|
+
break;
|
|
72
|
+
case "--frame-index":
|
|
73
|
+
parsed.frameIndex = args[++i];
|
|
74
|
+
break;
|
|
75
|
+
case "--selector":
|
|
76
|
+
parsed.selector = args[++i];
|
|
77
|
+
break;
|
|
78
|
+
case "--depth":
|
|
79
|
+
parsed.depth = args[++i];
|
|
80
|
+
break;
|
|
81
|
+
case "--max-interactive-elements":
|
|
82
|
+
parsed.maxInteractiveElements = args[++i];
|
|
83
|
+
break;
|
|
84
|
+
case "--index":
|
|
85
|
+
parsed.index = args[++i];
|
|
86
|
+
break;
|
|
87
|
+
case "--click":
|
|
88
|
+
parsed.click = args[++i];
|
|
89
|
+
break;
|
|
90
|
+
case "--type":
|
|
91
|
+
parsed.typeText = args[++i];
|
|
92
|
+
break;
|
|
93
|
+
case "--clear":
|
|
94
|
+
parsed.clear = true;
|
|
95
|
+
break;
|
|
96
|
+
case "--hover":
|
|
97
|
+
parsed.hover = args[++i];
|
|
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;
|
|
105
|
+
case "--observe-after":
|
|
106
|
+
parsed.observeAfter = true;
|
|
107
|
+
break;
|
|
108
|
+
case "--url-contains-assert":
|
|
109
|
+
parsed.urlContainsAssert = args[++i];
|
|
110
|
+
break;
|
|
111
|
+
case "--text-contains":
|
|
112
|
+
parsed.textContains = args[++i];
|
|
113
|
+
break;
|
|
114
|
+
case "--expect-visible":
|
|
115
|
+
parsed.expectVisible = true;
|
|
116
|
+
break;
|
|
117
|
+
case "--min-count":
|
|
118
|
+
parsed.minCount = args[++i];
|
|
119
|
+
break;
|
|
120
|
+
case "--has-text":
|
|
121
|
+
parsed.hasText = args[++i];
|
|
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;
|
|
54
168
|
case "--base-url":
|
|
55
169
|
parsed.baseUrl = args[++i];
|
|
56
170
|
break;
|
|
171
|
+
case "--allow-launch":
|
|
172
|
+
parsed.allowLaunch = true;
|
|
173
|
+
break;
|
|
57
174
|
case "--help":
|
|
58
175
|
case "-h":
|
|
59
176
|
printHelp();
|
|
@@ -68,17 +185,60 @@ function printHelp() {
|
|
|
68
185
|
本地浏览器测试执行器
|
|
69
186
|
|
|
70
187
|
用法:
|
|
188
|
+
node local-browser-executor.js --action observe [options]
|
|
189
|
+
node local-browser-executor.js --action act --index 1 --observe-after [options]
|
|
190
|
+
node local-browser-executor.js --action act --menu-path "目录管理/数据集目录" --observe-after [options]
|
|
191
|
+
node local-browser-executor.js --action assert --selector ".ant-table" --expect-visible [options]
|
|
192
|
+
node local-browser-executor.js --action api-capture --wait 15000 --include-response-body [options]
|
|
193
|
+
node local-browser-executor.js --action assert-api --capture-file /tmp/browser-api-capture.json --max-failed 0 [options]
|
|
71
194
|
node local-browser-executor.js --plan '<JSON>' [options]
|
|
72
195
|
node local-browser-executor.js --plan-file <path> [options]
|
|
73
196
|
|
|
74
197
|
选项:
|
|
75
|
-
--
|
|
76
|
-
--plan
|
|
77
|
-
--
|
|
78
|
-
--
|
|
79
|
-
--
|
|
80
|
-
--
|
|
81
|
-
-
|
|
198
|
+
--action <name> 直接执行动作,当前支持 observe/act/assert/api-capture
|
|
199
|
+
--plan <json> 测试计划 JSON 字符串
|
|
200
|
+
--plan-file <path> 测试计划 JSON 文件路径
|
|
201
|
+
--output <path> 结果输出文件路径(默认 stdout)
|
|
202
|
+
--cdp-url <url> Chrome DevTools Protocol URL(默认 http://localhost:9222)
|
|
203
|
+
--page-url <url> 目标页面 URL(connect 模式时匹配标签页)
|
|
204
|
+
--url-contains <text> 按 URL 片段匹配已打开标签页
|
|
205
|
+
--frame-url-contains <text> 按 URL 片段匹配 iframe
|
|
206
|
+
--frame-content-contains <text> 按文本内容匹配 iframe
|
|
207
|
+
--frame-index <n> 按 frame 下标选择 iframe
|
|
208
|
+
--selector <css> observe/assert 根节点或目标元素(observe 默认 body)
|
|
209
|
+
--depth <n> observe 的 DOM 树深度(默认 5)
|
|
210
|
+
--max-interactive-elements <n> observe 的交互元素上限(默认 200)
|
|
211
|
+
--index <n> act 时按 observe 的 interactiveElements[index] 操作
|
|
212
|
+
--click <selector> act 时点击 selector/text=/role=/~ 目标
|
|
213
|
+
--type <text> act 时输入文本
|
|
214
|
+
--clear act 输入前清空目标
|
|
215
|
+
--hover <selector> act 时悬停 selector/text=/role=/~ 目标
|
|
216
|
+
--menu-path <a/b> act 时按菜单路径展开并点击,如 "目录管理/数据集目录"
|
|
217
|
+
--wait-after-each <ms> menu-path 每步点击后的等待时间(默认 300)
|
|
218
|
+
--observe-after act 后再次 observe,返回 before/act/after 闭环结果
|
|
219
|
+
--url-contains-assert <text> assert 当前 URL 包含文本
|
|
220
|
+
--text-contains <text> assert 页面文本包含内容
|
|
221
|
+
--expect-visible assert selector 可见
|
|
222
|
+
--min-count <n> assert selector 数量下限
|
|
223
|
+
--has-text <text> assert selector 文本包含内容
|
|
224
|
+
--wait <ms> api-capture 监听时长(默认 15000)
|
|
225
|
+
--method <GET|POST> api-capture 按请求方法过滤
|
|
226
|
+
--url-pattern <text> api-capture 按 URL 片段过滤
|
|
227
|
+
--target-url-contains <text> api-capture 按 target URL 过滤 iframe/page target
|
|
228
|
+
--include-request-body api-capture 包含请求体(默认包含)
|
|
229
|
+
--no-request-body api-capture 不包含请求体
|
|
230
|
+
--include-response-body api-capture 调用 Network.getResponseBody 获取响应体
|
|
231
|
+
--reload api-capture 开始监听后刷新页面触发请求
|
|
232
|
+
--include-all-targets api-capture 不限制到匹配 frame,监听所有 page/iframe targets
|
|
233
|
+
--capture-file <path> assert-api 读取 api-capture JSON 结果
|
|
234
|
+
--max-failed <n> assert-api 允许失败请求数上限
|
|
235
|
+
--forbid-status-gte <n> assert-api 禁止出现大于等于该状态码的响应
|
|
236
|
+
--required-url <text> assert-api 要求出现的 URL 片段,可重复传入
|
|
237
|
+
--required-method <GET|POST> assert-api 要求出现的请求方法,可重复传入
|
|
238
|
+
--min-total <n> assert-api 要求捕获请求总数下限
|
|
239
|
+
--allow-launch action 模式允许无 CDP 时启动 headless Chrome
|
|
240
|
+
--base-url <url> 基础 URL,替换计划中的 {{baseUrl}} 占位符
|
|
241
|
+
-h, --help 显示帮助
|
|
82
242
|
`);
|
|
83
243
|
}
|
|
84
244
|
|
|
@@ -183,9 +343,163 @@ async function executePlan(plan, options) {
|
|
|
183
343
|
|
|
184
344
|
// ==================== 主流程 ====================
|
|
185
345
|
|
|
346
|
+
function executeAssertApiAction(args) {
|
|
347
|
+
if (!args.captureFile) {
|
|
348
|
+
return {
|
|
349
|
+
error: "必须提供 --capture-file 参数",
|
|
350
|
+
errorCode: "CAPTURE_FILE_REQUIRED",
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const captureResult = JSON.parse(fs.readFileSync(args.captureFile, "utf8"));
|
|
355
|
+
const engine = new BrowserEngine();
|
|
356
|
+
return engine.assertApi(captureResult, {
|
|
357
|
+
maxFailed: args.maxFailed,
|
|
358
|
+
forbidStatusGte: args.forbidStatusGte,
|
|
359
|
+
requiredUrls: args.requiredUrls,
|
|
360
|
+
requiredMethods: args.requiredMethods,
|
|
361
|
+
minTotal: args.minTotal,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function executeAction(args) {
|
|
366
|
+
const engine = new BrowserEngine();
|
|
367
|
+
const startedAt = new Date().toISOString();
|
|
368
|
+
try {
|
|
369
|
+
const cdpUrl = args.cdpUrl || "http://localhost:9222";
|
|
370
|
+
const debuggerAvailable = await engine.isDebuggerAvailable(cdpUrl);
|
|
371
|
+
if (!debuggerAvailable && !args.allowLaunch) {
|
|
372
|
+
return {
|
|
373
|
+
error: `无法连接到 Chrome 调试端口: ${cdpUrl}`,
|
|
374
|
+
errorCode: "CDP_UNAVAILABLE",
|
|
375
|
+
guide: "请先以 --remote-debugging-port=9222 启动用户真实浏览器;如确需启动 headless 回退,追加 --allow-launch。",
|
|
376
|
+
startTime: startedAt,
|
|
377
|
+
endTime: new Date().toISOString(),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const initResult = await engine.init({
|
|
382
|
+
cdpUrl,
|
|
383
|
+
pageUrl: args.pageUrl || args.urlContains,
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
switch (args.action) {
|
|
387
|
+
case "observe": {
|
|
388
|
+
const result = await engine.observe({
|
|
389
|
+
selector: args.selector,
|
|
390
|
+
depth: args.depth,
|
|
391
|
+
frameUrlContains: args.frameUrlContains,
|
|
392
|
+
frameContentContains: args.frameContentContains,
|
|
393
|
+
frameIndex: args.frameIndex,
|
|
394
|
+
maxInteractiveElements: args.maxInteractiveElements,
|
|
395
|
+
});
|
|
396
|
+
return { ...result, browserInfo: initResult, startTime: startedAt, endTime: new Date().toISOString() };
|
|
397
|
+
}
|
|
398
|
+
case "act": {
|
|
399
|
+
const actResult = await engine.act({
|
|
400
|
+
index: args.index,
|
|
401
|
+
selector: args.selector,
|
|
402
|
+
click: args.click,
|
|
403
|
+
hover: args.hover,
|
|
404
|
+
menuPath: args.menuPath,
|
|
405
|
+
waitAfterEach: args.waitAfterEach,
|
|
406
|
+
typeText: args.typeText,
|
|
407
|
+
clear: args.clear,
|
|
408
|
+
frameUrlContains: args.frameUrlContains,
|
|
409
|
+
frameContentContains: args.frameContentContains,
|
|
410
|
+
frameIndex: args.frameIndex,
|
|
411
|
+
});
|
|
412
|
+
if (!args.observeAfter) {
|
|
413
|
+
return { type: "act", ...actResult, browserInfo: initResult, startTime: startedAt, endTime: new Date().toISOString() };
|
|
414
|
+
}
|
|
415
|
+
const after = await engine.observe({
|
|
416
|
+
depth: args.depth,
|
|
417
|
+
frameUrlContains: args.frameUrlContains,
|
|
418
|
+
frameContentContains: args.frameContentContains,
|
|
419
|
+
frameIndex: args.frameIndex,
|
|
420
|
+
maxInteractiveElements: args.maxInteractiveElements,
|
|
421
|
+
});
|
|
422
|
+
return { type: "actObserve", act: actResult, after, browserInfo: initResult, startTime: startedAt, endTime: new Date().toISOString() };
|
|
423
|
+
}
|
|
424
|
+
case "assert": {
|
|
425
|
+
const expect = {};
|
|
426
|
+
if (args.expectVisible) expect.visible = true;
|
|
427
|
+
if (args.minCount !== undefined) expect.minCount = Number.parseInt(args.minCount, 10);
|
|
428
|
+
if (args.hasText !== undefined) expect.hasText = args.hasText;
|
|
429
|
+
const result = await engine.assert({
|
|
430
|
+
selector: args.selector,
|
|
431
|
+
expect,
|
|
432
|
+
urlContains: args.urlContainsAssert,
|
|
433
|
+
textContains: args.textContains,
|
|
434
|
+
frameUrlContains: args.frameUrlContains,
|
|
435
|
+
frameContentContains: args.frameContentContains,
|
|
436
|
+
frameIndex: args.frameIndex,
|
|
437
|
+
});
|
|
438
|
+
return { type: "assert", ...result, browserInfo: initResult, startTime: startedAt, endTime: new Date().toISOString() };
|
|
439
|
+
}
|
|
440
|
+
case "api-capture":
|
|
441
|
+
case "apiCapture": {
|
|
442
|
+
const result = await engine.captureApi({
|
|
443
|
+
wait: args.wait,
|
|
444
|
+
method: args.method,
|
|
445
|
+
urlPattern: args.urlPattern,
|
|
446
|
+
targetUrlContains: args.targetUrlContains,
|
|
447
|
+
includeRequestBody: args.includeRequestBody,
|
|
448
|
+
includeResponseBody: args.includeResponseBody,
|
|
449
|
+
reload: args.reload,
|
|
450
|
+
includeAllTargets: args.includeAllTargets,
|
|
451
|
+
frameUrlContains: args.frameUrlContains,
|
|
452
|
+
frameContentContains: args.frameContentContains,
|
|
453
|
+
frameIndex: args.frameIndex,
|
|
454
|
+
});
|
|
455
|
+
return { ...result, browserInfo: initResult, startTime: startedAt, endTime: new Date().toISOString() };
|
|
456
|
+
}
|
|
457
|
+
default:
|
|
458
|
+
return {
|
|
459
|
+
error: `未知 action: ${args.action}`,
|
|
460
|
+
errorCode: "UNKNOWN_ACTION",
|
|
461
|
+
supportedActions: ["observe", "act", "assert", "api-capture"],
|
|
462
|
+
startTime: startedAt,
|
|
463
|
+
endTime: new Date().toISOString(),
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
} catch (err) {
|
|
467
|
+
return {
|
|
468
|
+
error: err.message,
|
|
469
|
+
errorCode: err.code || "ACTION_FAILED",
|
|
470
|
+
installGuide: err.code === "CHROME_NOT_FOUND" || err.code === "DEPENDENCY_MISSING" ? err.message : undefined,
|
|
471
|
+
startTime: startedAt,
|
|
472
|
+
endTime: new Date().toISOString(),
|
|
473
|
+
};
|
|
474
|
+
} finally {
|
|
475
|
+
await engine.close();
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
186
479
|
async function main() {
|
|
187
480
|
const args = parseArgs();
|
|
188
481
|
|
|
482
|
+
if (args.action === "assert-api" || args.action === "assertApi") {
|
|
483
|
+
try {
|
|
484
|
+
const results = executeAssertApiAction(args);
|
|
485
|
+
outputResult(results, args.output);
|
|
486
|
+
process.exit(results.error ? 2 : results.passed === false ? 1 : 0);
|
|
487
|
+
} catch (err) {
|
|
488
|
+
const errorResult = {
|
|
489
|
+
error: err.message,
|
|
490
|
+
errorCode: "ASSERT_API_FAILED",
|
|
491
|
+
};
|
|
492
|
+
outputResult(errorResult, args.output);
|
|
493
|
+
process.exit(2);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (args.action) {
|
|
498
|
+
const results = await executeAction(args);
|
|
499
|
+
outputResult(results, args.output);
|
|
500
|
+
process.exit(results.error ? 2 : 0);
|
|
501
|
+
}
|
|
502
|
+
|
|
189
503
|
let plan;
|
|
190
504
|
try {
|
|
191
505
|
plan = loadPlan(args);
|
|
@@ -53,5 +53,5 @@ argument-hint: <目标页面URL> [可选:设计稿图片路径]
|
|
|
53
53
|
|
|
54
54
|
**注意事项**:
|
|
55
55
|
- 首次使用需确保 Chrome 以调试模式启动:`google-chrome --remote-debugging-port=9222`
|
|
56
|
-
-
|
|
56
|
+
- 如执行器报错依赖缺失,优先按提示安装核心依赖;仅设计稿像素对比需要额外安装 `pixelmatch pngjs sharp`
|
|
57
57
|
- connect 模式下天然继承用户浏览器的登录态和微前端环境
|