@szc-ft/mcp-szcd-client 0.38.0 → 0.39.1

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.
Files changed (58) hide show
  1. package/mcp-proxy.js +30 -16
  2. package/opencode-extension/skills/local-browser-test/ANALYSIS-PLAYWRIGHT-GAP.md +453 -0
  3. package/opencode-extension/skills/local-browser-test/IMPLEMENTATION-TRACE-VIEWER.md +467 -0
  4. package/opencode-extension/skills/local-browser-test/SKILL.md +147 -18
  5. package/opencode-extension/skills/local-browser-test/lib/actionability-probe.js +96 -0
  6. package/opencode-extension/skills/local-browser-test/lib/diagnosis.js +69 -0
  7. package/opencode-extension/skills/local-browser-test/lib/filmstrip-recorder.js +67 -0
  8. package/opencode-extension/skills/local-browser-test/lib/global-setup.js +2 -1
  9. package/opencode-extension/skills/local-browser-test/lib/html-reporter.js +377 -166
  10. package/opencode-extension/skills/local-browser-test/lib/network-waterfall.js +61 -0
  11. package/opencode-extension/skills/local-browser-test/lib/redact.js +12 -2
  12. package/opencode-extension/skills/local-browser-test/lib/step-recorder.js +171 -0
  13. package/opencode-extension/skills/local-browser-test/lib/trace-packager.js +20 -0
  14. package/opencode-extension/skills/local-browser-test/lib/trace-renderer.js +76 -0
  15. package/opencode-extension/skills/local-browser-test/lib/trace-viewer/index.html +42 -0
  16. package/opencode-extension/skills/local-browser-test/lib/trace-viewer/viewer.css +29 -0
  17. package/opencode-extension/skills/local-browser-test/lib/trace-viewer/viewer.js +201 -0
  18. package/opencode-extension/skills/local-browser-test/local-browser-executor.js +310 -80
  19. package/opencode-extension/skills/local-browser-test/tests/integration/pr-trace-v2.test.js +70 -0
  20. package/package.json +1 -1
  21. package/qwen-extension/qwen-extension.json +1 -1
  22. package/qwen-extension/skills/local-browser-test/ANALYSIS-PLAYWRIGHT-GAP.md +453 -0
  23. package/qwen-extension/skills/local-browser-test/IMPLEMENTATION-TRACE-VIEWER.md +467 -0
  24. package/qwen-extension/skills/local-browser-test/SKILL.md +147 -18
  25. package/qwen-extension/skills/local-browser-test/lib/actionability-probe.js +96 -0
  26. package/qwen-extension/skills/local-browser-test/lib/diagnosis.js +69 -0
  27. package/qwen-extension/skills/local-browser-test/lib/filmstrip-recorder.js +67 -0
  28. package/qwen-extension/skills/local-browser-test/lib/global-setup.js +2 -1
  29. package/qwen-extension/skills/local-browser-test/lib/html-reporter.js +377 -166
  30. package/qwen-extension/skills/local-browser-test/lib/network-waterfall.js +61 -0
  31. package/qwen-extension/skills/local-browser-test/lib/redact.js +12 -2
  32. package/qwen-extension/skills/local-browser-test/lib/step-recorder.js +171 -0
  33. package/qwen-extension/skills/local-browser-test/lib/trace-packager.js +20 -0
  34. package/qwen-extension/skills/local-browser-test/lib/trace-renderer.js +76 -0
  35. package/qwen-extension/skills/local-browser-test/lib/trace-viewer/index.html +42 -0
  36. package/qwen-extension/skills/local-browser-test/lib/trace-viewer/viewer.css +29 -0
  37. package/qwen-extension/skills/local-browser-test/lib/trace-viewer/viewer.js +201 -0
  38. package/qwen-extension/skills/local-browser-test/local-browser-executor.js +310 -80
  39. package/qwen-extension/skills/local-browser-test/tests/integration/pr-trace-v2.test.js +70 -0
  40. package/scripts/lib/opencode.js +23 -73
  41. package/standard-skill/local-browser-test/ANALYSIS-PLAYWRIGHT-GAP.md +453 -0
  42. package/standard-skill/local-browser-test/IMPLEMENTATION-TRACE-VIEWER.md +467 -0
  43. package/standard-skill/local-browser-test/SKILL.md +147 -18
  44. package/standard-skill/local-browser-test/lib/actionability-probe.js +96 -0
  45. package/standard-skill/local-browser-test/lib/diagnosis.js +69 -0
  46. package/standard-skill/local-browser-test/lib/filmstrip-recorder.js +67 -0
  47. package/standard-skill/local-browser-test/lib/global-setup.js +2 -1
  48. package/standard-skill/local-browser-test/lib/html-reporter.js +377 -166
  49. package/standard-skill/local-browser-test/lib/network-waterfall.js +61 -0
  50. package/standard-skill/local-browser-test/lib/redact.js +12 -2
  51. package/standard-skill/local-browser-test/lib/step-recorder.js +171 -0
  52. package/standard-skill/local-browser-test/lib/trace-packager.js +20 -0
  53. package/standard-skill/local-browser-test/lib/trace-renderer.js +76 -0
  54. package/standard-skill/local-browser-test/lib/trace-viewer/index.html +42 -0
  55. package/standard-skill/local-browser-test/lib/trace-viewer/viewer.css +29 -0
  56. package/standard-skill/local-browser-test/lib/trace-viewer/viewer.js +201 -0
  57. package/standard-skill/local-browser-test/local-browser-executor.js +310 -80
  58. package/standard-skill/local-browser-test/tests/integration/pr-trace-v2.test.js +70 -0
package/mcp-proxy.js CHANGED
@@ -54,13 +54,22 @@ const SERVER_URL = process.env.MCP_SERVER_URL || fileConfig.MCP_SERVER_URL || "h
54
54
  const SERVER_TIMEOUT = parseInt(process.env.MCP_TIMEOUT || fileConfig.MCP_TIMEOUT || "120000", 10);
55
55
  const SERVER_API_KEY = process.env.MCP_API_KEY || fileConfig.MCP_API_KEY || "";
56
56
 
57
- // 提取用户本地 CODING 配置,通过 Header 透传给服务器(服务器优先使用客户端配置)
58
- const CLIENT_CODING_CONFIG = {};
59
- if (fileConfig.CODING_BASE_URL) CLIENT_CODING_CONFIG.CODING_BASE_URL = fileConfig.CODING_BASE_URL;
60
- if (fileConfig.CODING_DEFAULT_PROJECT_ID) CLIENT_CODING_CONFIG.CODING_DEFAULT_PROJECT_ID = fileConfig.CODING_DEFAULT_PROJECT_ID;
61
- if (fileConfig.CODING_ACCOUNT) CLIENT_CODING_CONFIG.CODING_ACCOUNT = fileConfig.CODING_ACCOUNT;
62
- if (fileConfig.CODING_COOKIES) CLIENT_CODING_CONFIG.CODING_COOKIES = fileConfig.CODING_COOKIES;
63
- if (fileConfig.CODING_PASSWORD) CLIENT_CODING_CONFIG.CODING_PASSWORD = fileConfig.CODING_PASSWORD;
57
+ // 提取用户本地配置,通过 Header 透传给服务器(服务器优先使用客户端配置)
58
+ const CLIENT_CONFIG = {};
59
+ // CODING 配置
60
+ if (fileConfig.CODING_BASE_URL) CLIENT_CONFIG.CODING_BASE_URL = fileConfig.CODING_BASE_URL;
61
+ if (fileConfig.CODING_DEFAULT_PROJECT_ID) CLIENT_CONFIG.CODING_DEFAULT_PROJECT_ID = fileConfig.CODING_DEFAULT_PROJECT_ID;
62
+ if (fileConfig.CODING_ACCOUNT) CLIENT_CONFIG.CODING_ACCOUNT = fileConfig.CODING_ACCOUNT;
63
+ if (fileConfig.CODING_COOKIES) CLIENT_CONFIG.CODING_COOKIES = fileConfig.CODING_COOKIES;
64
+ if (fileConfig.CODING_PASSWORD) CLIENT_CONFIG.CODING_PASSWORD = fileConfig.CODING_PASSWORD;
65
+ // YAPI 配置
66
+ if (fileConfig.YAPI_BASE_URL) CLIENT_CONFIG.YAPI_BASE_URL = fileConfig.YAPI_BASE_URL;
67
+ if (fileConfig.YAPI_ACCOUNT) CLIENT_CONFIG.YAPI_ACCOUNT = fileConfig.YAPI_ACCOUNT;
68
+ if (fileConfig.YAPI_PASSWORD) CLIENT_CONFIG.YAPI_PASSWORD = fileConfig.YAPI_PASSWORD;
69
+ if (fileConfig.YAPI_COOKIES) CLIENT_CONFIG.YAPI_COOKIES = fileConfig.YAPI_COOKIES;
70
+ // Swagger 配置
71
+ if (fileConfig.SWAGGER_DEFAULT_USERNAME) CLIENT_CONFIG.SWAGGER_DEFAULT_USERNAME = fileConfig.SWAGGER_DEFAULT_USERNAME;
72
+ if (fileConfig.SWAGGER_DEFAULT_PASSWORD) CLIENT_CONFIG.SWAGGER_DEFAULT_PASSWORD = fileConfig.SWAGGER_DEFAULT_PASSWORD;
64
73
 
65
74
  // ==================== 日志 ====================
66
75
 
@@ -146,9 +155,9 @@ function forwardToStreamableHttp(msg) {
146
155
  if (httpSessionId) {
147
156
  headers["Mcp-Session-Id"] = httpSessionId;
148
157
  }
149
- // 透传用户本地 CODING 配置给服务器
150
- if (Object.keys(CLIENT_CODING_CONFIG).length > 0) {
151
- headers["X-Client-Config"] = Buffer.from(JSON.stringify(CLIENT_CODING_CONFIG)).toString("base64");
158
+ // 透传用户本地配置给服务器
159
+ if (Object.keys(CLIENT_CONFIG).length > 0) {
160
+ headers["X-Client-Config"] = Buffer.from(JSON.stringify(CLIENT_CONFIG)).toString("base64");
152
161
  }
153
162
 
154
163
  const postData = JSON.stringify(msg);
@@ -274,9 +283,9 @@ function connectSSE() {
274
283
  headers["X-API-Key"] = SERVER_API_KEY;
275
284
  headers["Authorization"] = `Bearer ${SERVER_API_KEY}`;
276
285
  }
277
- // 透传用户本地 CODING 配置给服务器
278
- if (Object.keys(CLIENT_CODING_CONFIG).length > 0) {
279
- headers["X-Client-Config"] = Buffer.from(JSON.stringify(CLIENT_CODING_CONFIG)).toString("base64");
286
+ // 透传用户本地配置给服务器
287
+ if (Object.keys(CLIENT_CONFIG).length > 0) {
288
+ headers["X-Client-Config"] = Buffer.from(JSON.stringify(CLIENT_CONFIG)).toString("base64");
280
289
  }
281
290
 
282
291
  log(`Connecting to SSE endpoint: ${url.toString()}, headers: ${JSON.stringify(headers)}`);
@@ -350,9 +359,9 @@ function forwardToSSEMessageEndpoint(msg) {
350
359
  headers["X-API-Key"] = SERVER_API_KEY;
351
360
  headers["Authorization"] = `Bearer ${SERVER_API_KEY}`;
352
361
  }
353
- // 透传用户本地 CODING 配置给服务器
354
- if (Object.keys(CLIENT_CODING_CONFIG).length > 0) {
355
- headers["X-Client-Config"] = Buffer.from(JSON.stringify(CLIENT_CODING_CONFIG)).toString("base64");
362
+ // 透传用户本地配置给服务器
363
+ if (Object.keys(CLIENT_CONFIG).length > 0) {
364
+ headers["X-Client-Config"] = Buffer.from(JSON.stringify(CLIENT_CONFIG)).toString("base64");
356
365
  }
357
366
 
358
367
  const postData = JSON.stringify(msg);
@@ -459,6 +468,7 @@ async function detectTransportMode() {
459
468
  if (result.result || (result.id === "detect" && !result.error?.message?.includes("Not Acceptable"))) {
460
469
  currentMode = TRANSPORT_MODE.STREAMABLE_HTTP;
461
470
  log(`Transport mode: Streamable HTTP (POST /mcp)`);
471
+ // 保留 sessionId,用于后续请求
462
472
  return;
463
473
  }
464
474
  } catch (e) {
@@ -471,6 +481,7 @@ async function detectTransportMode() {
471
481
  if (sseMessageEndpoint) {
472
482
  currentMode = TRANSPORT_MODE.SSE;
473
483
  log(`Transport mode: SSE (GET /sse + POST /message)`);
484
+ // 保留 sessionId,用于后续请求
474
485
  return;
475
486
  }
476
487
  } catch (e) {
@@ -480,6 +491,9 @@ async function detectTransportMode() {
480
491
  // 3. 回退到旧版 REST API
481
492
  currentMode = TRANSPORT_MODE.LEGACY_REST;
482
493
  log(`Transport mode: Legacy REST API`);
494
+
495
+ // 重置 sessionId,避免检测请求的 sessionId 影响后续请求
496
+ httpSessionId = null;
483
497
  }
484
498
 
485
499
  // ==================== 主逻辑 ====================
@@ -0,0 +1,453 @@
1
+ # local-browser-test vs Playwright:深度分析功能差距
2
+
3
+ > 编制日期:2026-07-02
4
+ > 编制人:Qwen Code(feat/local-browser-test-vs-playwright-gap)
5
+ > 适用版本:local-browser-test v0.39.0 / Playwright ≥1.45
6
+ > 对比基线:`szcd-mcp-client/standard-skill/local-browser-test/` 全部源码 + SKILL.md
7
+
8
+ ---
9
+
10
+ ## TL;DR(一句话总结)
11
+
12
+ local-browser-test 在**业务侧深度**(Ant Design 适配 / 微前端 / 真实浏览器登录态 / 凭据解密 / AI 语义断言)有 Playwright 完全不具备的独家能力;但在**浏览器自动化平台侧深度**(Trace Viewer、ARIA、Locator 链式 + 隐式重试、Code Coverage、Route/Mock、Video、Visual Regression baseline、多浏览器)有 **5 个 P0 缺口 + 4 个 P1 缺口 + 3 个 P2 缺口**。
13
+
14
+ | 项 | local-browser-test | Playwright |
15
+ |----|---------------------|------------|
16
+ | 定位 | "真实浏览器里跑业务用例 + LLM 友好工具集" | "完整 E2E 测试自动化平台" |
17
+ | 核心优势 | 微前端、Ant Design、真实登录态、凭据解密、AI 断言、观察者友好的 `observe → index → act` 闭环 | Trace Viewer、Locator 引擎、Actionability、多浏览器、Coverage、Mock、Video、Codegen |
18
+ | 深度分析完整度(10分) | 4.5/10(业务深度满分,平台深度有 5 个 P0 缺口) | 9.5/10(业务深度为 0,平台深度接近满分) |
19
+ | 互补潜力 | **极高** —— 把 Playwright 的 Trace/ARIA/Coverage 嫁接到 local-browser-test 的业务能力上 = 新的"业务测试 + 平台深度"超级工具 |
20
+
21
+ ---
22
+
23
+ ## 1. 评测口径:「深度分析」指什么
24
+
25
+ 为避免抽象,明确 4 类深度分析能力:
26
+
27
+ | 类型 | 含义 | 例子 |
28
+ |------|------|------|
29
+ | **A. 执行回溯** | 跑完用例后能否"看到"每步发生了什么(截图、trace、网络、操作步骤) | Trace Viewer、Video、Console log、screenshots timeline |
30
+ | **B. 状态捕获** | 能否系统化获取浏览器状态(DOM、API、可访问性树、覆盖率、性能指标) | ARIA Snapshot、Code Coverage、Web Vitals、API capture |
31
+ | **C. 调试能力** | 失败时能否快速定位根因(actionability 标尺、network waterfall、stacktrace、selector 评估) | Trace 时间轴、actionability 失败原因、stack trace、retry 上下文 |
32
+ | **D. 主动控制** | 能否主动改写浏览器行为(mock、route、覆盖 baseline、interception) | `page.route()`、`toHaveScreenshot` baseline、Service Worker 注册 |
33
+
34
+ local-browser-test 强在 A 的"业务侧"(截图、API capture、console archive)和 B 的"业务侧"(observe tree + 交互元素列表),弱在 A/B/C/D 的"平台侧"。
35
+
36
+ ---
37
+
38
+ ## 2. local-browser-test 深度分析能力盘点(已实测落地)
39
+
40
+ > 数据源:`lib/*.js` 实现 + `SKILL.md` 文档 + `local-browser-executor.js`
41
+ > 标 ✅ = 已实现并稳定,标 🟡 = 文档承诺但实现不完整,标 ❌ = 完全没有
42
+
43
+ ### 2.1 执行回溯
44
+
45
+ | 能力 | 状态 | 落点 | 备注 |
46
+ |------|------|------|------|
47
+ | 单步截图 | ✅ | `local-browser-executor.js:790-820` (`screenshot` action) | `--screenshot always\|only-on-failure` |
48
+ | Console 归档 | ✅ | `lib/console-archiver.js` | 含 `pageerror`,含 redact |
49
+ | API 捕获(CDP Network) | ✅ | `lib/browser-engine.js:2854-2870` | 不使用 setRequestInterception |
50
+ | HAR 录制 | ✅ | `lib/har-collector.js` | HAR 1.2 格式,含 token/Bearer 脱敏 |
51
+ | Trace 录制(CDP Tracing) | 🟡 | `lib/trace-collector.js` | 输出 **trace.json(CDP 原始事件流)**,**不打包成 trace.zip**、**无 viewer** |
52
+ | 失败自动重试 + 上下文 | ✅ | `lib/retry-policy.js` + L3 恢复 | 含机械重试 + 登录态自动恢复 |
53
+ | HTML 报告(趋势图 + 过滤 + 详情) | ✅ | `lib/html-reporter.js`(v4 Playwright 风格) | echarts CDN,单 run 自包含可分享 |
54
+ | Video 录制 | 🟡 | `--video` 参数已解析(`local-browser-executor.js:94`) | **未发现 Page.startScreencast 实际编码或文件输出**,疑似 stub |
55
+ | 失败诊断(diagnosticHint) | ✅ | L2 智能诊断(`SKILL.md` §"L1/L2/L3 健壮性") | failedAt + suggestions + summary |
56
+ | AI 语义断言 | ✅ | `aiAssert` 步骤 + 三级降级路由 | 截图 → 上传 → `assert_page_screenshot` MCP |
57
+
58
+ ### 2.2 状态捕获
59
+
60
+ | 能力 | 状态 | 落点 | 备注 |
61
+ |------|------|------|------|
62
+ | DOM 结构化观察 | ✅ | `lib/browser-engine.js:421-460` (`observe()`) | 输出 `frames / tree / interactiveElements / selectorCandidates / bbox / state / wujieContext` |
63
+ | 微前端 shadow DOM 穿透 | ✅ | `_detectWujieMode` + `wujieContext.shadowBodyText` | 三重适配(frame 自动选择 + shadow 穿透 + 坐标点击) |
64
+ | Interactive elements 列表 | ✅ | `_collectInteractiveElements` (1371) | 启发式:cursor:pointer / onclick / 表单元素 / ant-* 组件,**不含 ARIA role** |
65
+ | Selector 候选 | ✅ | `selectorCandidates` (1404) | 含 text= / role= / ~class= 等语法,**role 仅作为启发式标签,非真实 ARIA** |
66
+ | 元素 state(visible/enabled/rect) | ✅ | `state` 字段 (1469) | 一次 evaluate 拿齐所有状态 |
67
+ | API 捕获(请求/响应/响应体) | ✅ | `api-capture` action | 含 `--include-response-body` / `--include-all-targets` / `--url-pattern` |
68
+ | API 断言 | ✅ | `assertApi` + `assert-api` action | `maxFailed / requiredUrls / requiredMethods / forbidStatusGte / minTotal` |
69
+ | 性能指标(FCP/TTFB/资源) | ✅ | `local-browser-executor.js:881-928` (`perf` action) | `performance.getEntriesByType` 拿 navigation/paint/resource |
70
+ | Chrome 内部指标 | ✅ | `Performance.getMetrics` CDP | 拿到 `LayoutCount / RecalcLayoutCount / JSHeapUsedSize / JSHeapTotalSize / TaskDuration` |
71
+ | Web Vitals(LCP/CLS/INP/FID/TTFB) | 🟡 | `perfMetrics` 中有 `ttfb/fcp` 但**缺 LCP/CLS/INP/FID** | `Performance.getMetrics` 没出 LCP/CLS/INP,需 `web-vitals` 库或 LCP observer |
72
+ | Trend 趋势聚合 | ✅ | `lib/trend-aggregator.js` | byDay + byStrategy/api/ui/newman/browser + byTag/Project/Branch |
73
+ | 多 frame / iframe 信息 | ✅ | observe 输出 `frames[]` | 含 url + isMain + isWujie + childFrames |
74
+ | 抽屉/弹窗可见性 | ✅ | `act` 的 `drawerState.ensured` | `rect 宽高 + display/visibility/opacity` 判定 |
75
+ | ARIA / Accessibility tree | ❌ | — | observe 完全没碰 `Accessibility.getFullAXTree` |
76
+ | Code Coverage(JS / CSS) | ❌ | — | 没有 `Profiler.startPreciseCoverage` |
77
+ | 截图 baseline 库 | ❌ | 只有单图 `--action compare` | 无 `toHaveScreenshot` 风格的 baseline 管理 |
78
+
79
+ ### 2.3 调试能力
80
+
81
+ | 能力 | 状态 | 落点 | 备注 |
82
+ |------|------|------|------|
83
+ | 失败时截图 | ✅ | `screenshot=only-on-failure` | 写报告时附图 |
84
+ | 失败时上下文(HTML/state) | ✅ | `L1/L2/L3` 三层监控 | consoleErrors / pageErrors / failedRequests / diagnosticHint |
85
+ | Stack trace 收集 | ✅ | `pageerror` + `stack` 字段 | 落 `console.log` 时包含 |
86
+ | 元素未找到时给出 candidates | ✅ | `_actMenuPath` 失败返回 `candidates[]` | LLM 可基于 candidates 修复 JSON |
87
+ | 步骤级重试 + 智能恢复 | ✅ | L1 机械重试 / L2 智能诊断 / L3 主动修复 | 登录态过期自动 re-login |
88
+ | 行为日志(redact 后) | ✅ | `lib/redact.js` + console-archiver/HAR | URL/Headers/Console 三处都脱敏 |
89
+ | Trace 时间轴 + 标尺 | ❌ | trace.json 没有 viewer | **无法在 UI 中按时间回看** |
90
+ | Actionability 检查可视化 | ❌ | 没有"visible/enabled/stable/receivesEvents"逐项报告 | `_actMenuPath` 失败只返回 candidates |
91
+
92
+ ### 2.4 主动控制
93
+
94
+ | 能力 | 状态 | 落点 | 备注 |
95
+ |------|------|------|------|
96
+ | Route 拦截 + Mock | ❌ | 仅 `Network.enable` 监听 | **无 `Fetch.enable` / `route.fulfill` 能力** |
97
+ | 跨 frame 操作 | ✅ | `findFrame` + `_resolveObserveFrame` | 含 attach 重试(默认 3 次 × 1.5s) |
98
+ | 多标签页切换 | ✅ | `switchPage` action + `switch-tab` cdpScript | 按 urlContains / titleIncludes / pageIndex |
99
+ | 浏览器内 JS 执行(IIFE 包装) | ✅ | `evaluate` action | 含 safeEval 防 context destroyed |
100
+ | rawScript 跨 frame + 完整 puppeteer API | ✅ | `rawScript` + `codeFile`(PR #4 v3) | 静态扫描 Node-only API 提前 fail |
101
+ | 注册可复用脚本 | ✅ | `registerCdpScript` + `cdpScript` 调用 | 业务逻辑提取为可复用单元 |
102
+ | 浏览器凭据自动解密 | ✅ | `decrypt-creds.mjs` + `lib/decrypt-passwords.js` | DPAPI + AES-GCM(**Playwright 无此能力**) |
103
+ | 浏览器启动快捷方式 | ✅ | postinstall 自动创建 DevSession 桌面图标 | Windows .lnk / macOS .app / Linux .desktop |
104
+ | 录制操作生成脚本 | ❌ | — | **无 codegen 能力** |
105
+
106
+ ---
107
+
108
+ ## 3. Playwright 深度分析能力盘点(业界共识)
109
+
110
+ > 数据源:Playwright 官方文档 + GitHub README + Trace Viewer 实现
111
+ > local-browser-test 缺什么一目了然
112
+
113
+ ### 3.1 执行回溯(Playwright 强项之一)
114
+
115
+ | 能力 | 实现 |
116
+ |------|------|
117
+ | **Trace Viewer**(trace.zip) | DOM snapshot(每步) + network waterfall + console + 操作步骤 + 时间轴 + 标尺联动 + actionability 标记;**离线 HTML viewer** |
118
+ | **Video 录制**(webm) | `--video=on` 完整实现,每用例一个 webm 文件 |
119
+ | **Screenshots timeline** | trace 中每步都有截图,按时间轴联动 |
120
+ | **HAR 录制** | `--record-har` + `page.routeFromHAR()` 重放 |
121
+ | **HTML 报告** | 官方 reporter + 第三方 allure / monocart / reportportal |
122
+ | **失败时自动 attach** | trace + video + screenshot 全部自动保留 |
123
+
124
+ ### 3.2 状态捕获
125
+
126
+ | 能力 | 实现 |
127
+ |------|------|
128
+ | **ARIA Snapshot** | `page.accessibility.snapshot()` → 完整可访问性树(role/name/value/properties) |
129
+ | **Code Coverage** | `page.coverage.startJSCoverage()` / `startCSSCoverage()`,报告每文件已用/未用字节 |
130
+ | **Web Vitals** | trace 中集成 LCP/CLS/INP/TBT/TTFB + 第三方 `web-vitals` 库 |
131
+ | **Performance API 完整封装** | `page.metrics()` + `page.evaluate(() => performance.measure(...))` |
132
+ | **Network timing** | request/response 完整 timing chain(DNS/TCP/TLS/TTFB/download) |
133
+ | **Tracing.metrics** | CDP `Tracing.start` 的 `traceEvents` 详细分类 |
134
+
135
+ ### 3.3 调试能力
136
+
137
+ | 能力 | 实现 |
138
+ |------|------|
139
+ | **Actionability 检查** | click 前自动验证 visible / enabled / stable / receives events / attached;**失败时报告哪一项不满足** |
140
+ | **Locator 隐式重试** | `expect(locator).toBeVisible()` 内置 5s poll + 报错时带实际 HTML 片段 |
141
+ | **Strict mode** | locator 命中多个元素时立即失败 + 报告所有命中 |
142
+ | **Web-First Assertions** | `expect(page).toHaveTitle()` 等自动 poll + 重试 |
143
+ | **Trace 时间轴** | 鼠标悬停步骤 → 显示该步的 DOM/网络/console 三联视图 |
144
+ | **Stack trace + source map** | 失败堆栈直接定位到原始 .ts 源码 |
145
+ | **Test.step()** | 把一组 action 包成可命名步骤,trace viewer 中可折叠展开 |
146
+
147
+ ### 3.4 主动控制
148
+
149
+ | 能力 | 实现 |
150
+ |------|------|
151
+ | **`page.route()` 拦截** | `route.fulfill({status, body, headers})` 完全替代真实请求 |
152
+ | **`page.routeFromHAR()`** | 录制 → 重放,最佳 e2e 离线模式 |
153
+ | **`browserContext.route()`** | 跨 page 共享 mock 规则 |
154
+ | **`page.addInitScript()`** | 页面加载前注入 JS(polyfill / analytics disable) |
155
+ | **Service Worker 注入** | 网络层完全自定义 |
156
+ | **多 BrowserContext 隔离** | 一个 browser 实例跑多组完全隔离的测试 |
157
+ | **多浏览器** | WebKit + Chromium + Firefox(含移动版) |
158
+
159
+ ---
160
+
161
+ ## 4. 11 维度对比表
162
+
163
+ > 差距等级:🔴 P0 必补 / 🟡 P1 强烈建议 / 🟢 P2 可选 / ✅ 持平或领先
164
+
165
+ | # | 维度 | local-browser-test | Playwright | 差距 |
166
+ |---|------|---------------------|------------|------|
167
+ | 1 | **Trace Viewer(执行回溯)** | trace.json(CDP 原始事件,**无 viewer**) | trace.zip + 离线 viewer + 时间轴联动 | 🔴 P0 |
168
+ | 2 | **ARIA / Accessibility** | ❌ 无(observe 用 DOM 启发式) | ✅ `page.accessibility.snapshot()` + role-based locators | 🔴 P0 |
169
+ | 3 | **Code Coverage(JS/CSS)** | ❌ 无 | ✅ `page.coverage.startJSCoverage()` | 🔴 P0 |
170
+ | 4 | **Route / Mock 拦截** | ❌ 无(只读 Network) | ✅ `page.route()` + `routeFromHAR()` | 🔴 P0 |
171
+ | 5 | **Video 录制** | 🟡 参数已解析(`--video`),实现空缺 | ✅ webm 全程录制 | 🔴 P0 |
172
+ | 6 | **Locator 链式 + 隐式重试** | `act` 一次 snap-to-index,无重试 | `locator(...).click({timeout})` 链式 + auto-wait | 🟡 P1 |
173
+ | 7 | **Actionability 检查可视化** | 失败只给 candidates | 报告 visible/enabled/stable/receivesEvents 逐项 | 🟡 P1 |
174
+ | 8 | **Visual Regression baseline 管理** | 单图 `--action compare` | `toHaveScreenshot()` + baseline 自动归档 + maxDiffPixels | 🟡 P1 |
175
+ | 9 | **多浏览器矩阵** | Chromium only | WebKit + Chromium + Firefox | 🟡 P1 |
176
+ | 10 | **Codegen(录制生成代码)** | ❌ 无 | ✅ `playwright codegen` | 🟡 P1 |
177
+ | 11 | **Web Vitals(LCP/CLS/INP/FID)** | 🟡 FCP/TTFB/资源有,LCP/CLS/INP/FID 缺 | ✅ trace 内置 + web-vitals 库 | 🟢 P2 |
178
+ | 12 | **HAR 录制** | ✅ HAR 1.2 + 脱敏 | ✅ 标准 + routeFromHAR 重放 | ✅ 持平 |
179
+ | 13 | **Console 归档** | ✅ + 脱敏 | ✅ 标准 | ✅ 持平 |
180
+ | 14 | **API 捕获** | ✅ 强(CDP Network,iframe target) | ✅ 强(page.on('request')) | ✅ 持平 |
181
+ | 15 | **HTML 报告** | ✅ v4 Playwright 风格(echarts 趋势图) | ✅ 官方 + allure | ✅ 持平 |
182
+ | 16 | **Trend 趋势聚合** | ✅ byDay/byStrategy/byTag/Project/Branch | ❌ 无内置 | ✅ 领先 |
183
+ | 17 | **AI 语义断言** | ✅ 三级降级(截图→MCP→DOM) | ❌ 无 | ✅ 领先 |
184
+ | 18 | **Ant Design 适配** | ✅ antSelect/Tree/Date/Radio/Upload | ❌ 无 | ✅ 领先 |
185
+ | 19 | **微前端(wujie/qiankun)** | ✅ 三重适配(frame/shadow/坐标) | ❌ 无 | ✅ 领先 |
186
+ | 20 | **凭据自动解密** | ✅ DPAPI + AES-GCM(v10) | ❌ 无 | ✅ 领先 |
187
+ | 21 | **真实浏览器 connect 模式** | ✅ 继承登录态(connectOverCDP 等价) | ✅ `chromium.connectOverCDP` | ✅ 持平 |
188
+ | 22 | **testCase 数据驱动** | ✅ `dataset` 数组(参数化) | ✅ `test.describe` + `parametrize` | ✅ 持平 |
189
+
190
+ **汇总**:5 个 P0 缺口 + 4 个 P1 缺口 + 1 个 P2 缺口(其他 12 项持平或领先)。
191
+
192
+ ---
193
+
194
+ ## 5. 关键 P0 缺口的具体表现
195
+
196
+ ### 5.1 Trace Viewer(最大短板)
197
+
198
+ **Playwright**:跑完一个 test → 打开 `trace.zip` → 时间轴上点任意步骤 → 同时看到
199
+ - 步骤描述
200
+ - 该步的 DOM 快照
201
+ - 该步的截图
202
+ - 该步触发的 network waterfall
203
+ - 该步的 console 输出
204
+ - actionability 失败原因(如有)
205
+
206
+ **local-browser-test**:跑完一个 test → 拿到 `trace.json`(CDP 原始事件流,几十 MB JSON)
207
+ - **没有任何 viewer** 直接打开 JSON 看
208
+ - 即使写一个 viewer,**也缺每步 DOM snapshot**(CDP `Tracing.start` 默认不含 `disabled-by-default-devtools.timeline.frame`)
209
+ - 即使补上 DOM snapshot,**缺每步截图**(CDP `Page.startScreencast` 没启用)
210
+ - 即使补上截图,**缺 offline viewer**(HTML + JS + trace.zip 自包含)
211
+
212
+ **复杂度评估**:
213
+ - 中:trace.zip 打包(archiver 库,~50 行)
214
+ - 中:CDP 改成含 `disabled-by-default-devtools.timeline.frame` category
215
+ - 大:每步截图(CDP screencast → 每 act/click 触发一次 frame capture)
216
+ - **大**:自建离线 viewer(~500-1000 行 HTML + JS,含时间轴 + 三联视图 + 缩放)
217
+
218
+ ### 5.2 ARIA / Accessibility
219
+
220
+ **Playwright**:`page.accessibility.snapshot()` → 完整 role/name/value/selected/checked/disabled/expanded 树
221
+ **local-browser-test**:`observe()` 用 `cursor:pointer / onclick / role=button / .ant-*` 等启发式识别交互元素,**不含 ARIA**
222
+
223
+ **业务影响**:
224
+ - 改用语义化组件库(无 `.ant-*` 类名)时,observe 找不到元素
225
+ - 国际化(按钮文字是 "Submit" 而非 "提交")时,`text=` 选择器失效,但 role=button 仍能命中
226
+ - 屏幕阅读器用户用 `aria-label` 区分的按钮,observe 识别不出
227
+
228
+ **补齐路径**:
229
+ - 短期:`observe()` 增量补 `Accessibility.getFullAXTree`(CDP 命令,~30 行)
230
+ - 中期:locator 语法支持 `role=button[name="提交"]`
231
+ - 长期:取代 `interactiveElements` 的启发式,改用 AX tree
232
+
233
+ ### 5.3 Code Coverage
234
+
235
+ **Playwright**:`page.coverage.startJSCoverage()` → 跑测试 → `stopJSCoverage()` → 每文件 `{url, source, functions[], bytesUsed, bytesTotal}` 报告
236
+
237
+ **local-browser-test**:完全无此能力
238
+
239
+ **业务价值**:
240
+ - 回归测试发现"哪些代码路径从未被测试覆盖"
241
+ - 性能优化定位"未使用代码可移除"
242
+ - 死代码检测
243
+
244
+ **补齐路径**:
245
+ - 简单:`Profiler.startPreciseCoverage` + `Profiler.takePreciseCoverage`(CDP 原始命令,~80 行 wrapper)
246
+ - 中等:把 coverage 结果整合到 HTML 报告(每用例显示覆盖的文件 %)
247
+
248
+ ### 5.4 Route / Mock 拦截
249
+
250
+ **Playwright**:`page.route('**/api/login', route => route.fulfill({status: 200, body: '{"token":"fake"}'}))`
251
+ **local-browser-test**:无此能力(只读 `Network.responseReceived`)
252
+
253
+ **业务价值**:
254
+ - **离线测试**:HAR 重放,无外网也能跑 e2e
255
+ - **故障注入**:mock 500 测前端降级
256
+ - **A/B 测试**:mock 返回不同 schema
257
+ - **解耦**:后端未就绪时前端可先测
258
+
259
+ **补齐路径**:
260
+ - 简单:`Fetch.enable` + `Fetch.requestPaused` + `Fetch.fulfillRequest`(~100 行)
261
+ - 中等:route 语法 `page.route(pattern, handler)` 兼容 Playwright API
262
+ - 大:`routeFromHAR()` 重放
263
+
264
+ ### 5.5 Video 录制
265
+
266
+ **Playwright**:`--video=on` → 每 page 一个 webm,文件名含用例名
267
+ **local-browser-test**:`--video` 参数已解析(`local-browser-executor.js:94`),**实际编码未实现**
268
+
269
+ **验证方式**:
270
+ ```bash
271
+ $ grep -r "startScreencast\|ScreencastFrame\|video.*encode\|ffmpeg" /scity/zengzhijie/mcp/szcd-mcp-client/standard-skill/local-browser-test/
272
+ # (空)
273
+ ```
274
+
275
+ **补齐路径**:
276
+ - 简单:CDP `Page.startScreencast` + `ScreencastFrame` 事件流 → 保存为 PNG 序列 → ffmpeg 合成 webm(需 ffmpeg 系统依赖)
277
+ - 简单替代:每 N 步截一张图 + trace viewer 时间轴播放
278
+
279
+ ---
280
+
281
+ ## 6. 根因分析:为什么有这个差距?
282
+
283
+ ### 6.1 定位差异
284
+
285
+ | 维度 | local-browser-test | Playwright |
286
+ |------|---------------------|------------|
287
+ | 目标用户 | LLM(一次性任务执行) | 测试工程师(持续维护套件) |
288
+ | 核心痛点 | 真实登录态下"跑通看看" | 工程化 + debug + CI 集成 |
289
+ | 商业模式 | 内嵌在 szcd MCP 中(业务专精) | 通用测试平台 |
290
+ | 浏览器抽象层 | puppeteer-core + CDP 原始事件 | 自己 fork 浏览器(深度集成) |
291
+
292
+ **结论**:local-browser-test 是"业务工具",Playwright 是"平台工具"。两者目标用户不同,能力侧重天然不同。
293
+
294
+ ### 6.2 架构差异
295
+
296
+ - **local-browser-test** 用 `puppeteer-core` + CDP 直接 → **轻量、无浏览器厂商锁定、易部署**;代价是**没有浏览器侧的 actionability 实现、没有 trace viewer 基础**
297
+ - **Playwright** 自己 fork Chromium/WebKit/Firefox → **可以改浏览器源码**,所以能实现 actionability、trace 的 DOM snapshot、in-browser 注入等
298
+
299
+ ### 6.3 资源投入
300
+
301
+ - local-browser-test 累计 ~8000 行 lib 代码,2 周冲刺(v0.27 → v0.39)
302
+ - Playwright 多年迭代,团队规模 10+,Microsoft 持续投入
303
+
304
+ **结论**:差距部分来自资源/时间,部分来自定位/架构,部分来自"业务侧 vs 平台侧"的天然优先级。
305
+
306
+ ### 6.4 不是差距的部分(领先或持平)
307
+
308
+ local-browser-test 在以下场景反而是优势:
309
+ - **真实浏览器登录态**(DevSession 快捷方式 + decrypt-creds.mjs 自动解密) → Playwright 需手工 manage 登录态
310
+ - **微前端**(wujie/qiankun 三重适配)→ Playwright 需自己写 iframe 处理
311
+ - **Ant Design**(6 个内置 antXxx 步骤)→ Playwright 需手写 mousedown + 浮层轮询
312
+ - **AI 语义断言**(三级降级)→ Playwright 完全无
313
+ - **业务趋势**(byDay/byStrategy/byTag)→ Playwright 无内置
314
+
315
+ ---
316
+
317
+ ## 7. 补齐路线图(建议)
318
+
319
+ ### P0(建议 2-3 周投入,影响最大)
320
+
321
+ | 缺口 | 投入 | 收益 | 实现概要 |
322
+ |------|------|------|----------|
323
+ | **Trace Viewer** | 大(2 周) | 高 | archiver 打包 trace.zip + 离线 HTML viewer(含时间轴 + DOM/网络/console 三联视图 + 截图) |
324
+ | **ARIA / Accessibility** | 中(3 天) | 高 | observe 增量 `Accessibility.getFullAXTree` + `role=name` locator 语法 |
325
+ | **Code Coverage** | 小(2 天) | 中 | `Profiler.startPreciseCoverage` wrapper + HTML 报告整合 |
326
+ | **Route / Mock** | 中(1 周) | 高 | `Fetch.enable` 拦截 + `route(pattern, handler)` 兼容 API + `routeFromHAR` |
327
+ | **Video 录制** | 小(3 天) | 中 | CDP `startScreencast` + ffmpeg 合成 webm + HTML 报告嵌入播放 |
328
+
329
+ ### P1(建议 4-6 周分散投入)
330
+
331
+ | 缺口 | 投入 | 收益 | 实现概要 |
332
+ |------|------|------|----------|
333
+ | **Locator 链式 + 隐式重试** | 中(1 周) | 高 | `locator(sel).click({timeout})` 风格 + auto-wait + 错误带 HTML 片段 |
334
+ | **Actionability 失败诊断** | 小(3 天) | 中 | act 失败时报告 `visible/enabled/stable/receivesEvents` 逐项 |
335
+ | **Visual Regression baseline** | 中(1 周) | 中 | `toHaveScreenshot()` 风格 + baseline 归档 + maxDiffPixels 阈值 |
336
+ | **多浏览器** | 大(3-4 周) | 低 | webkit/firefox puppeteer(需评估 puppeteer 兼容性) |
337
+ | **Codegen** | 大(2 周) | 低 | recorder 自建(playwright 的 codegen 也是大模块) |
338
+
339
+ ### P2(看 ROI 再决定)
340
+
341
+ | 缺口 | 投入 | 收益 | 实现概要 |
342
+ |------|------|------|----------|
343
+ | **Web Vitals 完整化** | 小(3 天) | 中 | 集成 `web-vitals` 库 + `PerformanceObserver` 监听 LCP/CLS/INP/FID |
344
+
345
+ ### 不建议补的
346
+
347
+ - **多浏览器矩阵**(投入大、收益对业务场景低,且 puppeteer 的 firefox 支持弱于 playwright)→ 改用 Playwright 在需要时直接跑
348
+ - **Codegen**(投入大、LLM 已经能直接生成 rawScript)→ 优先级最低
349
+
350
+ ---
351
+
352
+ ## 8. 验证方法
353
+
354
+ ### 8.1 怎么验证 P0 缺口确实存在
355
+
356
+ ```bash
357
+ # Trace Viewer 缺口:trace 有没有 viewer
358
+ ls /scity/zengzhijie/mcp/szcd-mcp-client/standard-skill/local-browser-test/lib/ | grep -i "trace-viewer\|viewer"
359
+ # → 应该有 viewer 文件,但实际只有 trace-collector.js
360
+ # → 缺 zip 打包 + viewer
361
+
362
+ # ARIA 缺口:有没有用 Accessibility.getFullAXTree
363
+ grep -r "Accessibility\." /scity/zengzhijie/mcp/szcd-mcp-client/standard-skill/local-browser-test/
364
+ # → 应该空(没有结果 = 确认缺口)
365
+
366
+ # Code Coverage 缺口
367
+ grep -r "startPreciseCoverage\|coverage" /scity/zengzhijie/mcp/szcd-mcp-client/standard-skill/local-browser-test/
368
+ # → 应该空
369
+
370
+ # Route/Mock 缺口
371
+ grep -r "Fetch\.enable\|requestPaused\|fulfillRequest" /scity/zengzhijie/mcp/szcd-mcp-client/standard-skill/local-browser-test/
372
+ # → 应该空
373
+
374
+ # Video 缺口
375
+ grep -r "startScreencast\|ScreencastFrame\|ffmpeg" /scity/zengzhijie/mcp/szcd-mcp-client/standard-skill/local-browser-test/
376
+ # → 应该空(只有 --video 参数解析,无实际实现)
377
+ ```
378
+
379
+ ### 8.2 怎么验证领先项确实领先
380
+
381
+ ```bash
382
+ # 微前端:wujie 三重适配
383
+ grep -r "wujie\|shadowBodyText" /scity/zengzhijie/mcp/szcd-mcp-client/standard-skill/local-browser-test/lib/ | head -20
384
+ # → 应该有大量命中
385
+
386
+ # Ant Design:antXxx 步骤
387
+ grep -r "antSelect\|antTreeSelect\|antInput" /scity/zengzhijie/mcp/szcd-mcp-client/standard-skill/local-browser-test/lib/ | wc -l
388
+ # → 应该 30+
389
+
390
+ # 凭据解密
391
+ ls /scity/zengzhijie/mcp/szcd-mcp-client/standard-skill/local-browser-test/lib/decrypt-passwords.js
392
+ # → 存在
393
+
394
+ # AI 语义断言三级降级
395
+ grep -r "assert_page_screenshot\|aiAssert" /scity/zengzhijie/mcp/szcd-mcp-client/standard-skill/local-browser-test/
396
+ # → 存在
397
+ ```
398
+
399
+ ### 8.3 怎么验证补齐有效
400
+
401
+ **Trace Viewer 补齐后**:
402
+ ```bash
403
+ NODE_PATH=... node "$EXECUTOR" --action test-case --test-case-file /tmp/case.json --workspace /tmp/ws
404
+ # 1. 跑完用例后检查 /tmp/ws/reports/runs/<id>/trace.zip 是否存在
405
+ # 2. unzip -l trace.zip 看是否含 DOM snapshots + screenshots
406
+ # 3. 浏览器打开 trace/index.html 看时间轴是否正常
407
+ ```
408
+
409
+ **ARIA 补齐后**:
410
+ ```bash
411
+ NODE_PATH=... node "$EXECUTOR" --action observe --url-contains "..." --include-aria
412
+ # 1. observe 输出应新增 `ariaTree` 字段
413
+ # 2. act 应支持 `role=button[name="提交"]` 语法
414
+ ```
415
+
416
+ **Route/Mock 补齐后**:
417
+ ```json
418
+ { "type": "route", "pattern": "**/api/login", "fulfill": { "status": 200, "body": "{\"token\":\"fake\"}" } }
419
+ ```
420
+
421
+ ---
422
+
423
+ ## 9. 总结
424
+
425
+ | 项 | 评估 |
426
+ |----|------|
427
+ | local-browser-test 是否"落后 Playwright"? | **部分**:在 5 个平台核心能力(Trace/ARIA/Coverage/Route/Video)落后 |
428
+ | local-browser-test 是否"领先 Playwright"? | **部分**:在 5 个业务核心能力(微前端/Ant/凭据/AI 断言/真实登录态)领先 |
429
+ | 建议策略 | **不是替换,而是补充**:把 Playwright 平台能力嫁接到 local-browser-test 业务能力上,造一个"业务侧 + 平台侧都最强"的混合工具 |
430
+ | 优先补什么 | P0:Trace Viewer(最大短板,对 LLM 调试最关键)> ARIA(语义化对 LLM 选择器最关键)> Route/Mock(解耦后端最关键)> Coverage(深度分析最关键)> Video(CI 体验最关键) |
431
+ | 不建议补什么 | 多浏览器(投入产出比低)> Codegen(LLM 已能直接生成) |
432
+
433
+ ---
434
+
435
+ ## 10. 附录:参考资源
436
+
437
+ - Playwright Trace Viewer 架构:https://playwright.dev/docs/trace-viewer
438
+ - Playwright ARIA Snapshot:https://playwright.dev/docs/accessibility-testing
439
+ - Playwright Mock/Route:https://playwright.dev/docs/mock
440
+ - Playwright Code Coverage:https://playwright.dev/docs/api/class-coverage
441
+ - CDP 文档:https://chromedevtools.github.io/devtools-protocol/
442
+ - `Accessibility.getFullAXTree`
443
+ - `Profiler.startPreciseCoverage` / `takePreciseCoverage`
444
+ - `Fetch.enable` / `Fetch.requestPaused` / `fulfillRequest`
445
+ - `Page.startScreencast` / `ScreencastFrame`
446
+ - `Tracing.start` / `Tracing.dataCollected` / `Tracing.end`
447
+
448
+ ---
449
+
450
+ > **下一步行动建议**(编制人主观判断):
451
+ > 1. 本周:把 P0 5 项作为 issue 创建到 szcd-mcp-client,按工作量分级(Trace Viewer 单独 PR 2 周;ARIA + Coverage + Route 合并 PR 2 周;Video 单 PR 3 天)
452
+ > 2. 下周:在 HTML 报告里增加"未实现能力矩阵"展示页(明确告诉用户当前版本不支持什么,避免期望偏差)
453
+ > 3. 月底:写一篇"local-browser-test 设计哲学"对外文档,明确"业务深度 + 平台深度"双轨路线