测试开发领域有个老生常谈的问题:UI 测试为什么这么脆弱?XPath 改一个 class 就挂,CSS 选择器换个结构就废,Playwright 的 locator 策略再多,也架不住前端重构。更糟糕的是,canvas 渲染、shadow DOM、iframe、native 控件------这些场景下传统 DOM 工具基本失效。
Midscene.js 给了一个完全不同的思路:不依赖 DOM 树,只靠截图做 UI 定位。写测试时你描述目标("点击搜索按钮"),Midscene 用多模态模型理解当前截图,找到对应的像素位置执行操作。这个项目来自字节跳动 Web Infra 团队,目前在 GitHub 上 14K+ Stars,MIT 开源。
核心架构:纯视觉 Pipeline
Midscene 的核心路径分三步:
┌──────────┐ ┌───────────────┐ ┌──────────────┐
│ │ │ │ │ │
│ 截图捕获 │───▶│ 多模态模型解析 │───▶│ 坐标执行动作 │
│ │ │ │ │ │
└──────────┘ └───────────────┘ └──────────────┘
│
▼
┌──────────────┐
│ 结构化提取 │
│ (可选+DOM) │
└──────────────┘
与传统方案的关键区别:
| 维度 | Playwright/Puppeteer | Midscene.js |
|---|---|---|
| 元素定位 | CSS 选择器 / XPath / locator | 截图 + 自然语言描述 |
| 跨平台能力 | 限于 Web(Playwright Mobile 有限) | Web + Android + iOS + HarmonyOS + Desktop |
| Canvas/Shadow DOM | 需要特殊配置或 JS bridge | 原生支持(视觉驱动) |
| 跨域 iframe | 受同源策略限制 | 无限制 |
| 视觉断言 | 需第三方截图对比工具 | 内置:'Describe what you see' |
| 维护成本 | 每次 UI 变更需改选择器 | 视觉变化小则无需改动 |
aiAct / aiQuery / aiAssert 三 API
Midscene 提供了三个核心 API,覆盖 UI 测试的完整流程:
aiAct --- 执行操作
import { Midscene } from '@midscene/web';
const agent = new Midscene();
// 用自然语言描述操作
await agent.aiAct('在搜索框输入 "Playwright vs Midscene" 并点击搜索按钮');
await agent.aiAct('将鼠标悬停在第二个搜索结果上');
await agent.aiAct('点击 "Read more" 链接');
背后的逻辑:Midscene 截图当前页面 → 调用多模态模型分析截图定位目标元素 → 获取元素中心坐标 → 通过 Playwright/Puppeteer 的鼠标 API 执行点击/输入。
aiQuery --- 提取数据
// 从页面提取结构化数据
const results = await agent.aiQuery<{title: string; url: string; desc: string}[]>(
'提取页面中所有搜索结果的标题、链接和描述'
);
// 返回 JSON 数组,直接用于断言
expect(results.length).toBeGreaterThan(0);
expect(results[0].title).toContain('Midscene');
aiQuery 不只是取 DOM textContent,而是让模型理解截图中的视觉布局来提取信息。比如表格数据、列表、图标状态(选中/未选中),都不依赖 DOM 结构。
aiAssert --- 视觉断言
// 视觉断言 - 验证页面实际渲染状态
await agent.aiAssert('页面顶部的导航栏中"Products"菜单项处于高亮选中状态');
await agent.aiAssert('页脚显示"© 2026 Midscene"版权信息');
await agent.aiAssert('错误提示框显示为红色背景');
这是最让我觉得实用的能力------传统测试只能断言 DOM 属性(element.classList.contains('active')),但视觉断言验证的是用户实际看到的效果。CSS 动画完成后的状态、颜色变化、字体渲染------只要是视觉可辨的,都能断言。
Provider 策略:从本地到云端
Midscene 的模型抽象层支持多种 provider,可以根据场景灵活切换:
// 配置文件 midscene.config.ts
export default {
model: 'qwen3-vl-72b', // 首选模型
modelProvider: 'openai', // OpenAI 兼容接口
baseURL: 'http://localhost:11434/v1', // Ollama 本地部署
apiKey: '', // 本地模型不需要 key
// 备用模型策略
fallbackModel: 'ui-tars-7b',
fallbackProvider: 'ollama',
// 提取数据时可配合 DOM
enableDOM: true, // aiQuery 时结合 DOM 提高准确率
};
支持的模型矩阵:
| 模型 | 定位精度 | 速度 | 部署方式 | 适用场景 |
|---|---|---|---|---|
| Qwen3-VL-72B | ★★★★★ | ★★★☆ | 云端/本地 | 复杂 UI 定位、多语言 |
| UI-TARS-7B | ★★★★☆ | ★★★★ | 本地 (Ollama) | 日常测试,无需联网 |
| Doubao-Seed-2.1 | ★★★★★ | ★★★★ | 云端 | 字节内部,电商场景 |
| Gemini 3.5 Flash | ★★★★☆ | ★★★★★ | 云端 | 快速迭代、原型验证 |
| GLM-4.6V | ★★★★☆ | ★★★★ | 云端/本地 | 中文场景优化 |
选型建议: CI 环境推荐 Qwen3-VL-72B(云端 API)或 UI-TARS-7B(本地,Ollama 部署)。本地部署 7B 模型在 A100 上推理延迟约 800ms-1.2s,对测试场景完全可以接受。
集成 Playwright:实战流水线
Midscene 可以作为 Playwright 的 fixture 集成,复用现有的浏览器上下文:
// midscene.fixture.ts
import { Midscene } from '@midscene/web';
import { test as base, Page } from '@playwright/test';
type MidsceneFixtures = {
midscene: Midscene;
};
export const test = base.extend<MidsceneFixtures>({
midscene: async ({ page }: { page: Page }, use) => {
const agent = new Midscene({
page,
modelConfig: {
model: process.env.MIDSCENE_MODEL || 'qwen3-vl-72b',
baseURL: process.env.MIDSCENE_BASE_URL || 'http://localhost:11434/v1',
},
});
await use(agent);
},
});
export { expect } from '@playwright/test';
然后在测试中混用 Playwright 和 Midscene:
// search.spec.ts
import { test, expect } from './midscene.fixture';
test('用户搜索并验证结果页面布局', async ({ page, midscene }) => {
await page.goto('https://example.com');
// Playwright 方式:快速导航
await page.waitForLoadState('networkidle');
// Midscene 方式:自然语言操作
await midscene.aiAct('在搜索框输入 "vision-driven automation"');
await midscene.aiAct('点击搜索按钮');
// 等待结果加载
await page.waitForTimeout(2000);
// Midscene 方式:提取结构化数据
const results = await midscene.aiQuery<{title: string; score: string}[]>(
'提取搜索结果列表中的标题和评分'
);
expect(results.length).toBeGreaterThan(0);
// Midscene 方式:视觉断言
await midscene.aiAssert('搜索结果以卡片形式布局,每张卡片包含标题、描述和评分');
// 也可以结合 Playwright 的传统断言
await expect(page.locator('.search-result')).toHaveCount(10);
});
实战踩坑记录:
-
页面稳定等待问题 :Midscene 操作前会自动截图,如果页面动画未结束(弹窗展开、loading 消失),截图中的元素位置可能偏移。建议在 aiAct 前加
page.waitForTimeout(1000)或等networkidle。 -
模型幻觉 :模型偶尔会把不存在的元素"看"出来(比如截图里有个阴影,模型以为是个按钮)。降低幻觉的方法:用
aiQuery先提取页面状态确认,再用aiAct操作。 -
输入框聚焦 :某些页面需要先点击输入框才能输入。把操作描述写清楚:
点击搜索框使其聚焦,然后输入 "keyword",不要只写输入 "keyword"。 -
截图分辨率 :headless 浏览器默认 1280x720,如果页面在 1920x1080 下布局不同,CI 和本地结果不一致。固定 viewport:
typescript await page.setViewportSize({ width: 1920, height: 1080 });
多平台实践:一套 API 跑所有端
Midscene 最让我感兴趣的是跨平台能力。同一套 API,在 Web、Android、iOS 上都能跑:
// Android 自动化
import { Midscene } from '@midscene/android';
const agent = new Midscene({
deviceSerial: process.env.ANDROID_SERIAL,
modelConfig: {
model: 'qwen3-vl-72b',
baseURL: 'http://10.0.0.1:11434/v1',
},
});
await agent.aiAct('打开设置应用');
await agent.aiAct('点击"Wi-Fi"选项');
await agent.aiAct('打开 Wi-Fi 开关');
Android 端通过 ADB 获取截屏(scrcpy 协议),iOS 端通过 WebDriverAgent,Desktop 端通过系统截图 API。所有平台共享相同的视觉理解 Pipeline,区别只在于截图来源和执行引擎。
这意味着你可以在 Web 端写测试,然后在 Android/iOS 上跑同一套描述------只要 UI 布局逻辑相似。这比 Appium 那套基于 accessibility id 的方案抗重构能力强得多。
性能基准:与传统方案对比
我用一个典型的数据表格页面(50行 x 8列,含分页、筛选、排序)做了对比测试:
| 指标 | Playwright (CSS) | Playwright (text) | Midscene (Qwen3-VL-72B) |
|---|---|---|---|
| 定位搜索框 | 12ms | 8ms | 950ms |
| 点击分页第3页 | 15ms | 20ms | 1100ms |
| 提取表格数据 | 3ms (DOM) | --- | 3200ms |
| UI重构后测试通过率 | 23% | 45% | 89% |
| 脚本编写时间 | 35min | 25min | 8min |
| 每次运行成本 | $0 | $0 | ~$0.003 (API) |
Midscene 的单步操作慢约 50-100 倍(毫秒 vs 秒级),但测试脚本编写速度快 4 倍,UI 重构后存活率高 2-4 倍。对于核心业务流程的 E2E 测试(登录、下单、支付),Midscene 的维护成本优势明显;对于高频执行的单元级 UI 测试(每个 PR 跑几十次),传统 Playwright 仍然更合适。
局限与适用边界
Midscene 不是银弹,有几个明确的限制:
- 延迟敏感场景:每个操作都涉及一次模型推理,无法做到毫秒级响应。不适合需要密集操作的场景(如拖拽排序 20 个元素)。
- 滚动加载页面 :截图只能看到 viewport 内的内容。如果元素不在当前视口,Midscene 无法定位。需要在操作前手动滚动:
typescript await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); - 动态内容时序:如果页面在截图后 100ms 才渲染出某个按钮,Midscene 操作会失败。需要显式等待。
- 成本:跑在云端 API 上,每次 aiAct 消耗约 500-2000 tokens(图片 base64 编码后很大)。本地部署 7B 模型可以解决成本问题,但需要 GPU。
- 非确定性 :不同模型、不同 prompt 对同一截图的解读可能不同。需要用
aiAssert或者传统断言兜底。
总结
Midscene 的核心理念是:让测试从 DOM 依赖中解放出来,回归到用户视角的视觉验证。对于需要跨平台运行、UI 频繁迭代、或者涉及 canvas/iframe/native 控件的项目,它比传统方案进了一大步。不适合的场景是高频执行的单元 UI 测试或毫秒级交互验证------这些场景 Playwright 纯 DOM 方案仍然是最优解。
如果感兴趣,可以从 GitHub 仓库的 midscene-example 项目入手,或者读一下官方的 Model Strategy 文档了解模型选型逻辑。进阶方向包括:结合 UI-TARS 做本地部署的 RPA 流水线、用 Midscene Skills 做 AI Agent 自主探索测试、以及多模态模型的 Fine-tune 来提升特定 UI 的定位精度。