Midscene.js框架(5):测试报告系统与调试技巧

📅 2026/7/15 5:33:56 👁️ 阅读次数 📝 编程学习
Midscene.js框架(5):测试报告系统与调试技巧

本篇将深入讲解 Midscene.js 的可视化报告系统调试技巧,重点探索如何通过"双报告协同"快速定位 AI 自动化测试的失败根因。

前言

一、Midscene 可视化报告体系

1.1 报告存储结构

1.2 报告配置选项

1.3 报告内部结构

1.4 手动记录截图到报告

二、双报告协同机制

2.1 测试框架报告

2.2 Midscene 可视化报告

2.3 协同定位流程

2.4 自动关联两份报告

2.5 报告合并工具

三、通过报告定位失败根因

3.1 场景一:AI 规划错误

3.2 场景二:元素定位偏移

3.3 场景三:动作执行时序问题

3.4 场景四:AI 断言误判

3.5 场景五:缓存导致的幽灵失败

四、高级调试技巧

4.1 LLM 使用指标监控

4.2 实时 LLM 调用回调

4.3 页面上下文冻结

4.4 日志内容提取

4.5 截图缩放优化

4.6 Deep Locate 精确定位

4.7 任务中止与超时控制

4.8 Chrome 扩展 Playground 调试

总结


前言

AI 驱动的 UI 自动化带来一个全新的调试挑战:当测试失败时,到底哪一步出了问题?是 AI 规划错了?元素定位偏了?还是页面压根没加载出来?

传统 UI 自动化(Selenium / Playwright)的调试手段主要依赖日志和截图,但在 AI 自动化场景中,每一步操作都涉及模型推理,失败的原因可能隐藏在:

  • AI 规划阶段:模型误解了自然语言指令,生成了错误的操作计划
  • 元素定位阶段:模型在截图中找到了错误的元素,或定位坐标偏移
  • 动作执行阶段:操作正确但页面响应异常,如弹窗遮挡、网络延迟
  • 断言判断阶段:AI 对页面状态的理解与预期不一致

Midscene.js 针对这些痛点设计了可视化报告系统——每一轮 AI 推理、每一次元素定位、每一个动作执行,都会被完整记录下来,附带截图、耗时、模型输出等上下文信息。配合测试框架自身的报告,构成了强大的双报告协同调试体系

在 Midscene.js + 测试框架(如 Pytest / Playwright Test)的架构中,测试执行后会同时产生两份报告:

两份报告互为补充:测试框架报告告诉你"哪条用例挂了",Midscene 报告告诉你"这条用例的哪一步、哪个 AI 决策出了问题"。这种协同关系,正是高效调试的核心。

一、Midscene 可视化报告体系

1.1 报告存储结构

每次执行后,Midscene 会在当前工作目录下生成midscene_run目录:

1.2 报告配置选项

Midscene Agent 提供了完整的报告控制选项,适用于代码模式和 YAML 模式:

输出格式对比

重要提示html-and-external-assets格式不能通过file://协议直接打开(浏览器 CORS 限制),需启动本地服务器:

# Node.js npx serve # Python python -m http.server

然后通过http://localhost:3000访问。

代码中配置报告

// Playwright Agent 中配置 const agent = new PuppeteerAgent(page, { generateReport: true, reportFileName: "checkout-flow-test", outputFormat: "single-html", groupName: "E2E Checkout Suite", groupDescription: "Complete checkout flow with payment validation", autoPrintReportMsg: true, persistExecutionDump: true, // 额外保存 JSON dump 用于深度分析 });
# YAML 脚本中配置 agent: generateReport: true reportFileName: "smoke-test-report" groupName: "Smoke Test" groupDescription: "Daily smoke test for core user paths"

1.3 报告内部结构

打开 Midscene 生成的 HTML 报告,你会看到完整的执行时间线。以 GitHub 注册表单填写任务为例,报告结构如下:

报告头部 ├── 任务名称:Sign up for Github, pass form validation ├── 总耗时、模型信息、缓存命中情况 │ 执行步骤时间线 ├── Action: Sign up for Github... ← 用户指令 │ ├── Plan: Open the GitHub signup page... ← AI 规划(6.95s) │ ├── Locate: [截图 + 元素标注] ← 元素定位(6.18s) │ ├── Tap: [点击坐标记录] ← 动作执行(2.08s) │ │ │ ├── Plan: Enter a valid email... ← AI 规划(5.03s) │ ├── Locate: [截图 + 元素标注] ← 元素定位(6.60s) │ ├── Input: "user@example.com" ← 动作执行(1.45s) │ │ │ ├── Plan: Enter a strong password... ← AI 规划(5.33s) │ ├── Locate: [截图 + 元素标注] ← 元素定位(5.18s) │ ├── Input: "********" ← 动作执行(1.35s) │ │ │ ├── ... │ │ │ └── Print_Assert_Result: ✅ All fields valid ← 断言结果(0.99s) │ 报告尾部 ├── Token 使用统计(按意图、按模型分类) ├── 缓存命中统计 └── 每步截图缩略图导航

每个步骤的关键信息

1.4 手动记录截图到报告

除了自动记录每步操作,你还可以通过recordToReport()手动插入截图标记点:

// 简单记录:自动截图并添加标题和描述 await agent.recordToReport("Login page loaded", { content: "User A is about to log in" }); // 多截图对比:手动提供截图(不自动截图) const beforeSubmit = await page.screenshot({ encoding: "base64" }); await agent.aiTap("submit button"); const afterSubmit = await page.screenshot({ encoding: "base64" }); await agent.recordToReport("Submit comparison", { content: "Compare state before and after submit", screenshots: [ { base64: `data:image/png;base64,${beforeSubmit}`, description: "Before submit" }, { base64: `data:image/png;base64,${afterSubmit}`, description: "After submit" }, ], });

这在以下场景特别有用:

  • 关键节点标记:在复杂流程中标记重要状态变化
  • 前后对比:记录操作前后的页面差异
  • 外部截图整合:将非 Midscene 操作的截图纳入统一报告

二、双报告协同机制

2.1 测试框架报告

以 Pytest 为例,测试框架报告关注的是用例级别的执行结果

pytest tests/ --html=reports/report.html --self-contained-html -v

生成的 Pytest HTML 报告包含:

2.2 Midscene 可视化报告

Midscene 报告关注的是AI 操作级别的执行细节

  • 每一步 AI 推理的输入和输出
  • 每一次元素定位的截图和坐标
  • 每一个动作执行的耗时和结果
  • 模型 token 消耗统计
  • 缓存命中情况

2.3 协同定位流程

当一条用例失败时,标准的调试流程如下:

┌──────────────────────────────────────────────────────────────┐ │ Step 1: 查看测试框架报告 │ │ │ │ 确认哪条用例失败、失败在哪一行、assert 断言信息是什么 │ │ → 得到:用例名称、失败行号、失败原因概要 │ │ │ ├──────────────────────────────────────────────────────────────┤ │ Step 2: 打开 Midscene 报告 │ │ │ │ 在终端输出中找到 Midscene 报告路径: │ │ "Midscene - report file updated: ./midscene_run/report/xxx.html"│ │ 用浏览器打开该 HTML 文件 │ │ → 得到:完整的 AI 操作时间线 │ │ │ ├──────────────────────────────────────────────────────────────┤ │ Step 3: 定位失败步骤 │ │ │ │ 在 Midscene 报告中,按时间线找到与失败用例对应的操作段 │ │ 重点关注: │ │ ├── Plan 步骤:AI 规划是否正确理解了指令? │ │ ├── Locate 步骤:截图标注的元素是否是正确的目标? │ │ ├── Action 步骤:动作执行是否成功?有无异常? │ │ └── Assert 步骤:AI 断言结果与预期是否一致? │ │ → 得到:具体失败的步骤和原因 │ │ │ ├──────────────────────────────────────────────────────────────┤ │ Step 4: 分析根因并修复 │ │ │ │ 根据失败步骤的类型采取不同策略: │ │ ├── AI 规划错误 → 优化自然语言指令,或改用结构化 API │ │ ├── 元素定位错误 → 启用 deepLocate,或调整元素描述 │ │ ├── 动作执行失败 → 增加 waitAfterAction,或处理弹窗 │ │ └── 断言判断错误 → 调整断言描述,或增加 aiWaitFor 等待 │ │ │ └──────────────────────────────────────────────────────────────┘

2.4 自动关联两份报告

为了让调试流程更顺畅,可以在测试框架的失败处理钩子中自动打印 Midscene 报告路径

# conftest.py (Pytest) import pytest @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield report = outcome.get_result() if report.when == "call" and report.failed: agent = item.funcargs.get("agent") page = item.funcargs.get("page") print(f"\n{'='*60}") print(f"❌ 测试失败: {item.name}") print(f"{'='*60}") if page: screenshot_path = f"reports/failure_{item.name}.png" page.screenshot(path=screenshot_path) print(f"📸 失败截图: {screenshot_path}") if agent: report_path = agent.finish() print(f"📊 Midscene 可视化报告: {report_path}") print(f" 用浏览器打开上述路径查看 AI 操作详情") print(f"{'='*60}\n")

这样每次测试失败时,终端会直接输出两份报告的路径,无需手动查找。

2.5 报告合并工具

当测试套件中运行了多条用例,每条用例都会生成独立的 Midscene 报告。使用ReportMergingTool可以将多个报告合并为一个统一报告:

import { ReportMergingTool } from "@midscene/core/report"; const mergingTool = new ReportMergingTool(); // 在每条用例结束后追加报告 mergingTool.append({ reportFilePath: agent.reportFile as string, reportAttributes: { testId: "checkout-test-001", testTitle: "Complete checkout with credit card", testDescription: "End-to-end checkout flow validation", testDuration: 45000, testStatus: "passed", // passed | failed | timedOut | skipped | interrupted }, }); mergingTool.append({ reportFilePath: agent2.reportFile as string, reportAttributes: { testId: "checkout-test-002", testTitle: "Checkout with PayPal", testDuration: 32000, testStatus: "failed", }, }); // 合并为单个 HTML 报告 const mergedPath = mergingTool.mergeReports("checkout-suite-summary", { rmOriginalReports: false, // 保留原始报告 overwrite: true, // 覆盖已存在的合并报告 }); console.log(`合并报告: ${mergedPath}`); // → midscene_run/report/checkout-suite-summary.html

合并后的报告包含所有用例的执行详情和状态概览,非常适合 CI/CD 场景下的统一查看。

三、通过报告定位失败根因

3.1 场景一:AI 规划错误

现象aiAct("点击购物车图标并进入结算页面")执行后,页面跳转到了错误的地方。

报告分析

打开 Midscene 报告,找到对应的 Action 步骤,查看 Plan 子步骤:

Action: Input "iPhone" and search ├── Plan: (0.5s) ├── Locate: search input box (5.2s) ├── Input: "iPhone" (1.3s) │ └── 截图显示:输入框内容为 "iPhone",但搜索按钮仍处于禁用状态 ← ⚠️ ├── Plan: (0.4s) ├── Locate: search button (4.1s) ├── Tap: search button (1.0s) │ └── 截图显示:按钮被点击但搜索未触发,因为输入未完成验证 ← ❌ └── ...

根因:AI 将"购物车"误解为"心愿单"(Wishlist),导致规划了错误的操作。

修复方案

  • 优化指令描述:aiAct("Click the 'Cart' icon (with a shopping bag symbol) in the top right navigation")
  • 或改用即时动作:await agent.aiTap("the Cart icon in the top navigation bar")

3.2 场景二:元素定位偏移

现象aiTap("the first product in the list")点击了错误的商品。

报告分析

查看报告中的 Locate 步骤,截图上会标注 AI 识别到的元素位置:

Locate: the first product in the list ├── 截图显示:AI 标注了列表中第二个商品的区域 ← ❌ 定位偏移 ├── 坐标: (450, 320) └── 耗时: 6.18s

根因:列表顶部有一个广告 banner,AI 将其视为"第一个商品"。

修复方案

  • 启用deepLocate进行精确定位:
await agent.aiTap("the first product in the list", { deepLocate: true });
  • 或调整描述排除广告区域:
await agent.aiTap("the first product card below the banner");

3.3 场景三:动作执行时序问题

现象aiInput输入文本后立即aiTap("search button"),但搜索结果为空。

报告分析

查看报告时间线,关注每步耗时和截图:

Action: Input "iPhone" and search ├── Plan: (0.5s) ├── Locate: search input box (5.2s) ├── Input: "iPhone" (1.3s) │ └── 截图显示:输入框内容为 "iPhone",但搜索按钮仍处于禁用状态 ← ⚠️ ├── Plan: (0.4s) ├── Locate: search button (4.1s) ├── Tap: search button (1.0s) │ └── 截图显示:按钮被点击但搜索未触发,因为输入未完成验证 ← ❌ └── ...

根因:输入完成后搜索按钮需要时间激活,点击过早导致搜索未触发。

修复方案

  • 增加aiWaitFor等待按钮激活:
await agent.aiInput("search box", "iPhone"); await agent.aiWaitFor("the search button is enabled and clickable", { timeoutMs: 5000 }); await agent.aiTap("search button");
  • 或增大waitAfterAction
const agent = new PuppeteerAgent(page, { waitAfterAction: 1000, // 默认 300ms,增大到 1s });

3.4 场景四:AI 断言误判

现象aiAssert("the cart shows 3 items")通过了,但实际购物车有 5 件商品。

报告分析

查看报告中的 Assert 步骤:

Print_Assert_Result: ✅ the cart shows 3 items ├── 截图显示:购物车图标上的数字是 "5" ← ❌ AI 看到了 5 但断言通过 ├── AI 输出: "The cart badge shows number 3" ← ❌ 模型视觉理解错误 └── 耗时: 0.99s

根因:视觉模型对小数字的识别存在偏差,将 "5" 误读为 "3"。

修复方案

  • 改用aiQuery提取具体数值并用 Python/JS 断言:
const count = await agent.aiNumber("the number on the cart badge"); assert(count === 3, `Expected 3 items, but got ${count}`);
  • 或使用aiString提取文本后验证:
const cartText = await agent.aiString("the text on the cart badge"); assert(cartText.includes("3"));

3.5 场景五:缓存导致的幽灵失败

现象:用例在本地一直通过,但在 CI 上偶发失败。

报告分析

对比本地和 CI 的 Midscene 报告:

本地报告: ├── Locate: the login button │ └── 🧊 cache hit — XPath: /html/body/div[2]/button[1] ← 缓存命中 │ └── 耗时: 0.3s(极快) CI 报告: ├── Locate: the login button │ └── cache miss — falling back to AI model ← 缓存未命中 │ └── AI 定位到 /html/body/div[3]/button[1] ← ❌ DOM 结构变化 │ └── 耗时: 6.2s

根因:CI 环境的页面 DOM 结构与本地不同(如 A/B 测试、不同分支代码),导致缓存 XPath 失效,AI 重新定位时找到了错误元素。

修复方案

  • 将缓存文件提交到代码仓库,确保 CI 和本地使用相同的缓存
  • 或在 CI 中禁用缓存,始终使用 AI 实时定位:
const agent = new PuppeteerAgent(page, { cache: false, // CI 环境禁用缓存 });
  • 设置缓存调试日志:
export DEBUG=midscene:cache:*

四、高级调试技巧

4.1 LLM 使用指标监控

Midscene Agent 提供metrics属性,可以实时获取 LLM 调用的 token 使用情况:

await agent.aiAct("search for headphones and add the first one to cart"); const usage = agent.metrics; console.log("=== LLM 使用统计 ==="); console.log(`总 Token: ${usage.totalTokens}`); console.log(`Prompt Token: ${usage.totalPromptTokens}`); console.log(`Completion Token: ${usage.totalCompletionTokens}`); console.log(`缓存命中 Token: ${usage.totalCachedInput}`); console.log(`总耗时: ${usage.totalTimeCostMs}ms`); console.log(`调用次数: ${usage.calls}`); console.log("\n=== 按意图分类 ==="); for (const [intent, bucket] of Object.entries(usage.byIntent)) { console.log(`${intent}: ${bucket.totalTokens} tokens, ${bucket.calls} calls`); } // planning: 1200 tokens, 3 calls // insight: 800 tokens, 2 calls console.log("\n=== 按模型分类 ==="); for (const [model, bucket] of Object.entries(usage.byModel)) { console.log(`${model}: ${bucket.totalTokens} tokens`); }

调试价值

  • 成本追踪:计算每条用例的模型调用成本
  • 性能分析:识别 token 消耗异常的步骤
  • 模型对比:比较多模型策略下不同模型的贡献

4.2 实时 LLM 调用回调

通过onLLMUsage回调实现实时监控,可对接 Langfuse 等可观测性平台:

const agent = new PuppeteerAgent(page, { onLLMUsage: (usage) => { console.log(`[${usage.intent}] ${usage.model} → ${usage.total_tokens} tokens`); // 对接 Langfuse // langfuse.event({ name: usage.intent, value: usage.total_tokens }); }, });

4.3 页面上下文冻结

当需要对同一页面状态执行多次aiQuery时,使用freezePageContext避免重复截图:

// 冻结页面上下文(复用同一截图) await agent.freezePageContext(); // 并发执行多个查询(共享同一页面快照) const [username, password, loginBtn] = await Promise.all([ agent.aiQuery("the value in the username input"), agent.aiQuery("the value in the password input"), agent.aiLocate("the login button"), ]); // 解冻,恢复实时页面状态 await agent.unfreezePageContext();

注意:冻结期间不要执行交互操作(如aiTap),否则 AI 会基于过期截图做决策。报告中冻结操作会显示 🧊 标记。

4.4 日志内容提取

通过_unstableLogContent()获取完整的执行日志(不稳定 API,结构可能变化):

await agent.aiAct("fill the registration form"); const logContent = agent._unstableLogContent(); console.log(logContent); // 输出包含:每步的 AI 输入、模型原始响应、解析结果、执行状态等

4.5 截图缩放优化

通过screenshotShrinkFactor减少截图尺寸,降低 token 消耗:

const agent = new PuppeteerAgent(page, { screenshotShrinkFactor: 2, // 宽高减半,面积减少到 1/4 });

缩放系数

面积变化

Token 节省

适用场景

1(默认)

不变

日常使用

2

1/4

~40%

移动设备、简单页面

3

1/9

~60%

极端节省场景(可能影响 AI 理解)

建议:移动端设为 2,Web 端保持 1。不建议超过 3,否则截图过于模糊影响 AI 元素识别。

4.6 Deep Locate 精确定位

当目标元素较小或与周围元素难以区分时,启用deepLocate进行两阶段精确定位:

// 普通定位(单次 AI 调用) await agent.aiTap("the small close button on the popup"); // 深度定位(两次 AI 调用,更精确) await agent.aiTap("the small close button on the popup", { deepLocate: true });

模式

AI 调用次数

耗时

准确率

适用场景

普通定位

1

标准

大多数场景

Deep Locate

2

约 2 倍

更高

小元素、密集列表、相似元素

4.7 任务中止与超时控制

通过abortSignal实现超时控制和用户取消:

const controller = new AbortController(); // 30 秒超时 setTimeout(() => controller.abort("timeout"), 30000); try { await agent.aiAct("complete the complex checkout process", { abortSignal: controller.signal, }); } catch (e) { if (e.message.includes("abort")) { console.log("任务超时中止,查看报告了解执行到哪一步"); // 打开 Midscene 报告查看已执行的步骤 } }

4.8 Chrome 扩展 Playground 调试

Midscene 提供 Chrome 扩展,可以在任何网页上即时测试 AI 指令,无需编写代码:

  1. 从 Chrome Web Store 安装 Midscene 扩展
  2. 配置模型 API Key(环境变量)
  3. 打开目标网页,在右侧侧边栏使用以下功能:
  • Act:测试aiAct指令,查看 AI 规划和执行过程
  • Query:测试aiQuery数据提取,验证返回格式
  • Assert:测试aiAssert断言,查看判断结果
  • Tap:测试aiTap即时点击,验证元素定位

调试技巧:在编写正式测试脚本前,先用 Chrome 扩展在目标页面上验证 AI 指令的有效性,确认 AI 能正确定位元素和执行操作后,再将指令写入脚本。这能大幅减少后期调试成本。

总结

调试速查表

症状

首先检查

修复方向

AI 执行了错误操作

报告中 Plan 步骤的输出

优化自然语言指令,或改用结构化 API

点击了错误元素

报告中 Locate 步骤的截图标注

启用deepLocate,调整元素描述

操作成功但结果不对

报告中 Action 步骤的时序和截图

增加aiWaitForwaitAfterAction

断言结果不可靠

报告中 Assert 步骤的 AI 输出

改用aiQuery+ 编程断言

本地通过 CI 失败

报告中缓存命中状态对比

提交缓存文件或 CI 禁用缓存

执行太慢

agent.metrics的 token 和耗时

启用缓存、增大screenshotShrinkFactor

成本过高

metrics.byIntentbyModel

多模型策略、缓存、截图缩放