我做了个让 AI 看屏幕跑测试的工具,因为 Playwright 测不了我的 Flutter Web

我是 jeff,这两个月在做 RecompDaily(一个 Flutter应用 + Go 的健身记录 app)。

这篇讲一个我自己挠头三个月、最后写代码砸出来解决的事:Flutter Web 应用怎么做 E2E 测试

如果你也在用 Flutter Web / Figma 风格的 canvas-based 应用,被 Playwright 折磨过,这文章可能对你有用。最后我把工具开源了:github.com/jackwangfeng/qdesk(Apache 2.0)。


痛点:Flutter Web 是 canvas,Playwright 是瞎子

如果你做过 Flutter Web 你应该知道,Flutter 不像 React/Vue 把每个组件渲染成 DOM,它把整个 UI 画到一块 canvas 上(用 Skia 或 Impeller)。

意思是:

html 复制代码
<flt-glass-pane>
  <canvas><!-- 整个 app 的所有内容都在这里 --></canvas>
</flt-glass-pane>

DOM 里就一个 canvas 元素。

这对 Playwright 是灾难:

// 想点"保存"按钮?祝你好运
await page.locator('button:has-text("Save")').click();
// → Error: locator resolved to 0 elements

// 想等待页面加载完?
await page.waitForSelector('[data-testid="dashboard"]');
// → Timeout: there are no DOM nodes inside the canvas

// Playwright 唯一能做的就是按坐标点
await page.mouse.click(100, 200);
// 但坐标硬编码,UI 一改就废

我们的 RecompDaily 上,Playwright 测试基本只能覆盖:登录页那几个 native HTML 表单 + URL 路由变化。Flutter app 主体 90% 的功能完全测不了。

QA 同事每次发版前手工点半天。改一次 UI,他从 demo 第一遍开始重测。

---
三个月里我试过的烂方案

第一阶段:给 Flutter widget 都加 testid。 改了几百个 widget,最后发现 Flutter Web 里 testid 根本不会变成 DOM 属性 --- 它只是个内部 key。卒。

第二阶段:像素 diff(Percy/Applitools)。 跑起来了,但只能验证"长得一样",不能验证"功能对"。而且字体抗锯齿差几个像素就报告失败,假阳性满天飞。

第三阶段:让 QA 录视频对比。 真这么干了一个月。一次发版录 30 个流程,人均 2 小时。

第四阶段:开始研究Computer Use 这类 vision-LLM...

---
转念:这不就是 vision model 该干的事吗

2024 年底 Anthropic 发了 Computer Use,Google Gemini 2.5 也有视觉能力。它们能直接看截屏判断 UI 状态。

那为啥不让它看 Flutter Web 的截屏,直接点呢?

我画了个最简单的设想:

1. 跑个 Linux 容器,里面装 Chrome,打开你的 app
2. agent 截屏
3. 把截屏 + 自然语言任务("点击保存按钮") 发给 Gemini
4. Gemini 返回 JSON: {type:"click", x:300, y:450}
5. 容器里 xdotool 模拟点击
6. 再截屏,继续循环,直到任务完成
7. 用 Gemini 验证"成功的样子"

关键:测试不是脚本,是描述 + 期望。 UI 改了,描述不动,AI 自动适配。

我用周末搭了个 demo 跑通了。觉得有意思,接着花了几天做了个完整版。

---
几天搭出来的样子

整个东西叫 qdesk,3000 行 Go,4 个 binary,Apache 2.0:

┌─────────────────────────────────────────────┐
│  qdesk run  tests/login.qdesk.yaml           │
│        │                                     │
│        ▼                                     │
│  control plane (HTTP API,SQLite)            │
│        │                                     │
│        ├─→ Docker:跑 Linux + Xvfb + Chrome   │
│        │   └─ qdesk-agentd: /screenshot      │
│        │      /actions {click/type/...}      │
│        │      via scrot + xdotool            │
│        │                                     │
│        └─→ runner: 循环                       │
│            screenshot → Gemini → action      │
│            → 直到 done → 验证 expect          │
│            → 出 HTML 报告 + AI 诊断          │
└─────────────────────────────────────────────┘

写一个测试就这五行 YAML:

name: 登录跳转
url: http://host.docker.internal:8888
goal: 在欢迎页点 "Get started" 按钮
expect:
  - 页面跳到 Sign in 屏,左上角显示 "Sign in"

跑:

$ qdesk run tests/login.qdesk.yaml
🤖 看到欢迎页有红色 "Get started" 按钮(bbox 中心 x=960, y=720)
👆 click(960, 720)
🤖 看到 Sign in 屏出现,左上角有返回箭头 + "Sign in" 文字
✅ DONE
✅ PASS  (1 step, 41 秒, ~$0.005)
📄 报告: file:///qdesk-runs/login-2026-05-04/report.html

41 秒,5 毛钱。

---
真实跑 RecompDaily 时踩的几个坑

测我自己 app 时几个真实经验,留这里给后来人:

1. Gemini 2.5 Flash 在 canvas 上 Y 坐标系统性偏小

实测一个按钮真实 y=740,Gemini Flash 估的是 y=592(偏小 148 像素)。
Pro 好一些 y=685,但还是上偏。

我猜原因是训练数据里"canvas 像素 → 语义"对齐不够。

Mitigation: 在 prompt 里强制 LLM 先输出 bounding box,再算几何中心:

GROUNDING DISCIPLINE:
1. First identify target bounding box (x_min, y_min, x_max, y_max)
2. Compute center: x = (x_min+x_max)/2, y = (y_min+y_max)/2
3. If previous click didn't change page, retry 30-60px LOWER

A primary CTA centered horizontally on a Flutter welcome screen is
typically located between y=700 and y=780. Do NOT default to y < 650
for buttons that visually appear in the lower half of the page.

加了这段,Gemini Flash 在 4 步内能命中。

2. Flutter Web 路由切换需要 ~1.2 秒 settle

点击后 400ms 截屏,Flutter 路由还在 transition,截到的是旧屏。
runner 默认 settle 1200ms 才下一次截屏 → 才稳。

比 React/Vue 慢一个数量级,要小心。

3. dbus 报错可以无视

Chromium 在容器里没 dbus,启动时刷一堆 ERROR,但不影响渲染。

---
一个真实 bug,顺手测出来的

写 demo 的时候随手写了个测试:

goal: 点 "Get started"
expect:
  - 跳到 Sign in 页
  - 出现 Google 登录按钮

跑出 FAIL。AI 诊断:

▎ "页面卡在 'Getting ready' 状态。Google Sign-In 初始化超时。
▎  建议:加 timeout 处理,网络问题或后端不通时给用户明确反馈。"

去看 --- 原来当后端没启动时,Google Sign-In script 会无声 hang 住,
用户体验极烂(看到 "Getting ready" 转圈永不结束)。

这个 bug 之前没被 Playwright 抓到 --- Playwright 只能等 selector 出现,
没出现就 timeout 报错,不会写"诊断"。
qdesk 的 AI 看一眼就说出根因。

后来加了 5 秒 timeout 和明确的错误提示,这事修了。

---
给团队用上之后

我把这套接到 Claude Code 里,通过 Model Context Protocol(MCP)注册成工具。

效果:我团队的工程师在 Claude Code 里写完代码,跟 Claude 说一句:

▎ "测一下我刚改的 Log 页"

Claude 自动调 qdesk_quick_test,30 秒后告诉他:
- ✅ PASS,4 个期望全过,这是报告链接
- 或 ❌ FAIL,这是失败截屏 + AI 诊断

工程师收到反馈,要么改代码要么 commit。没有人再写一行 Playwright selector。

QA 那边减负 70%。

---
谁会用这个 / 谁不会用

我不替自己说好话 --- qdesk 不是 Playwright 替代品,有明确 trade-off:

qdesk 适合:
- ✅ Flutter Web / canvas-heavy 应用(Figma、Excalidraw、Miro 类)
- ✅ AI 编程工具用户(Claude Code / Cursor 写完自验证)
- ✅ UI 经常变动的探索期项目(测试自愈)
- ✅ 非工程师(QA / PM)写测试

Playwright 适合:
- ✅ 大量 CI 测试(qdesk 慢 10x,贵)
- ✅ DOM 精确断言("class 应该是 X")
- ✅ 跨浏览器测试(qdesk 只 Chromium)
- ✅ 网络层 mocking
- ✅ 成熟稳定的产品

两者长期共存,不是你死我活。我团队现在 Playwright 测我们后端 + 登录这种 native HTML,qdesk 测整个 Flutter app 主体。

---
开源 + 开始用

仓库:github.com/jackwangfeng/qdesk

5 分钟接入 Claude Code(完整文档 docs/TEAM_QUICKSTART.md): 或者把这个链接给claude code, 它会自动安装, ai极度友好

# 1. 装
curl -fsSL https://raw.githubusercontent.com/jackwangfeng/qdesk/main/scripts/install.sh | bash

# 2. 设环境(GEMINI_API_KEY 去 https://aistudio.google.com 拿,免费)
export GEMINI_API_KEY=AIza...
export QDESK_DEV_KEY=$(openssl rand -hex 16)

# 3. 后台跑控制面
nohup qdesk-control --dev-key "$QDESK_DEV_KEY" \
    --image qdesk/ubuntu-chrome:dev > ~/.qdesk-control.log 2>&1 &

# 4. 接到 Claude Code
claude mcp add --transport stdio qdesk -- qdesk-mcp \
    --control http://127.0.0.1:8090 \
    --api-key "$QDESK_DEV_KEY" --gemini-key "$GEMINI_API_KEY"

# 5. 重启 Claude Code session,/mcp 看到 qdesk

接下来 Claude Code 写完代码自动可以测 UI。

如果你也在做 Flutter Web / canvas 应用,觉得 Playwright 又烦又测不动,
来试试。Issue / Star / PR 都欢迎。

--- jeff
相关推荐
HSunR2 小时前
dify 搭建ai作业批改流
开发语言·前端·javascript
代码不加糖2 小时前
2026 跨境电商独立站实战:从 0 到 1 搭建高转化 SaaS 商城(附源码)
开发语言·前端·javascript
亲亲小宝宝鸭2 小时前
拖一拖控件,拖出个问卷(低代码平台)
前端·低代码
江南十四行2 小时前
ReAct Agent 基本理论与项目实战(一)
前端·react.js·前端框架
We་ct3 小时前
LeetCode 72. 编辑距离:动态规划经典题解
前端·算法·leetcode·typescript·动态规划
小呆呆6663 小时前
Codex 穷鬼大救星
前端·人工智能·后端
当时只道寻常4 小时前
Vue3 + IntersectionObserver 实现高性能图片懒加载
前端
sakiko_4 小时前
UIKit学习笔记3-布局、滚动视图、隐藏或显示视图
前端·笔记·学习·objective-c·swift·uikit
有一个好名字5 小时前
Agent Loop —— 一切从那个 while 循环开始
前端·javascript·chrome