用途
flutter-mcp-e2e-harness 用于为 Flutter 应用编写和运行可重复的端到端(E2E)场景,以 checked-in Dart 代码形式提交入库,适合在 CI 中执行并以退出码反馈结果。与之相对,一次性的交互式调试应使用 flutter-mcp 提供的 MCP fmt_* 工具。
主要能力
- 完整生命周期管理:packages/harness(flutter_mcp_harness)负责构建应用、启动进程、连接其 Dart VM service,然后再进行驱动与断言。
- 同一套扩展:以纯 Dart 方式调用与 MCP 服务器相同的 ext.mcp.toolkit.* 服务扩展。
- 通用驾驶契约:ToolkitDriver 暴露 observe/act/verify 循环:snapshot 返回带引用与边界的语义 AxNode 树;perform 支持 click、type、key、命名路由跳转与 evaluate;screenshot 输出 PNG。
- 意图路由:IntentDriverRouter 可将意图的自动化提示(transport + verb + locator)路由到绑定的驱动。
- 应用专属动作:通过 invoke 层(ADR-0017),应用自身的 intent 注册表即为动作目录;driver.actions() 列出动作,InvokeAction(name, args) 以 schema 校验参数派发到注册条目。
- Web 支持:无 VM service 的 Web 表面(Jaspr、纯 JS)通过 CdpDriver 上的 window.__mcpActions 页面注册表以相同方式组合。
使用方式
场景由 Scenario 对象定义,步骤按顺序执行,首个失败(抛出或 report.fail)中止后续步骤,清理仍在调用方 finally 中执行。
final scenario = Scenario('checkout E2E', steps: [
('launch app', (context) async {
final app = await MacosAppTarget(projectDir: appDir, binaryPath: binary)
.launch(build: build);
launched = app;
await app.stdout.waitFor('app ready');
context.bag['app'] = app;
}),
('drive + assert', (context) async {
final driver = await attachDriver(context.take<LaunchedApp>('app'));
await driver.expectLabel('checkout');
await driver.perform(const ClickAction(name: 'checkout'));
}),
]);
var passed = false;
try {
passed = await scenario.run();
scenario.report.printSummary();
} finally {
await launched?.stop();
}
exit(passed ? 0 : 1);
HarnessContext.bag 在步骤间传递值,take<T>(key) 读取;retry(action) 会轮询直到非空,用于应用侧就绪竞态。完整双实例组合根示例见 packages/harness/example/desktop_pair.dart。
Web/AOT 层示例:
final app = await WebBuildTarget(projectDir: '.', build: !skipBuild).launch();
final flutter = app.flutter;
await flutter.waitText('Settings');
await flutter.clickText('Sync between devices', expect: 'I have a pairing code');
await flutter.typeFocused('mesh-pair/v1 ...');
await app.stop();
FlutterWebCdp 中已固化的实测规则包括:语义点击需逐次用 expect 验证效果后再重试(切换控件重复点击会关掉自身);加载后首次点击会被吞掉,用 warmUpClick() 消耗;输入需要 Page.bringToFront + Input.insertText;仅做应用内跳转(popRoute = history.back()),重载会丢失内存状态;Headless Chrome 不执行 rAF 会使 Flutter 冻结,因此 WebBuildTarget 默认使用有界面 Chrome。
启动路径(按所有权选择,每个应用至多一个拥有会话):
- FlutterRunTarget:由 flutter 工具拥有构建 + 热重载(经 stdin 的 r/R/q)。
- BinaryAppTarget / MacosAppTarget / WindowsAppTarget:执行全新完整 flutter build <os> --debug 后直接启动二进制;launch(build: false) 可复用已有二进制;不会打开拥有会话。
- AppOwnership.attachOnly:应用已在运行且不应由 harness 停止时使用;按二进制名发现 pid,校验运行中的可执行文件与 binaryPath、磁盘上的构建与进程启动时间,通过显式命名的 vmServiceUri 连接,stop() 不向进程发送信号。
- 外部运行器拥有应用时:围绕拥有进程直接构造 LaunchedApp,从运行器会话契约而非 stdout 读取 VM URI。
证据收集:EvidenceCollector 可在场景运行时附加证据,首个失败步骤将证据转储到 $TMPDIR/<scenario>/;attachCdp 缓冲页面控制台与未捕获异常,attachLines 跟踪应用 stdout,attachFile 延迟输出 AX/状态转储。证据生成按契约是尽力而为,不应掩盖失败。对启动中的应用调用 VM 扩展可用 vm.callExtensionUntil(...),避免手写重试循环。
适用场景
- 以代码形式编写、可重复执行、CI 友好的 E2E 场景。
- 通过语义树与统一动作词汇驱动 Flutter 应用、浏览器(CdpDriver)和 OS 原生目标。
- 在 AOT Web 构建(无 VM service)上通过 CDP 层做端到端验证。
限制
- 不新增 toolkit 动作;ToolkitExtensions 须与 mcp_toolkit 的 interaction_toolkit.dart 保持同步。
- BinaryAppTarget 不提供拥有的编译会话,只做全新完整构建。
- 不支持声明式场景文档(YAML 运行器)。
- Android/iOS 设备启动交由拥有的开发会话(oka_harness,ADR-0014),不在本包范围。
- 证据边界:MCP/驱动 UI 场景只能证明控件存在、可达且有响应,无法证明输入不经过 UI 的路径(如摄像头手势);各层级结论不可相互替代。
验证
cd packages/harness && dart test
dart run example/desktop_pair.dart --skip-build