AI驱动UI自动化的实践心得——基于Midscene


来源|TesterHome社区网站
作者(ID)|孙高飞
前言
年后重新开始看效能相关的任务,由于去年主要在接口自动化,所以最近开始做一些 UI 自动化的东西。 目前实践了一个基于视觉驱动的 Midscene,另一个基于单模态大模型驱动的 Stagehand。 今天这篇主要介绍一下 Midscene。
PS: 我之前有过一个使用 cursor 来生成测试用例和自动化测试代码的直播录屏,还没看的同学可以看一下:https://testerhome.com/articles/43375
(编者注:有关Midscene的最新消息,可查看:AI自动化工具Midscene重要更新:鸿蒙、PC端全覆盖,新增模型、Skills及报告解析能力)
一、Midscene 是什么?
Midscene.js 是字节跳动 Web Infra 团队开源的 AI 驱动 UI 自动化框架。它的核心理念是:用自然语言描述操作意图,由 AI 来理解页面并执行操作,而不是让你写一堆 CSS 选择器。 比如:

如上图所示,控件的定位方式从以前的 xpath 和 css selector,变成了一段自然语言。控件定位的形式发生了变化,变得更简单,更灵活。只要 UI 界面没有发生重大变化,多模态大模型都可以自动调整,找到正确的控件。因为模型会通过截图和用户提供的定位控件的自然语言的描述,找到对应控件的正确位置并进行操作。 即便前端开发修改了 DOM 树结构也没有关系。
AI 驱动的两种模式
以我的理解, AI 驱动 UI 自动化其实有两种实践方式, 一种是我们上面看到的在运行时使用模型来进行控件定位。而另一种其实是利用 AI 编程工具,辅助编写 UI 自动化测试代码。 在这个阶段, cursor/codebuddy/codex 这些 AI 辅助工具会动态启动浏览器,动态获取 DOM 树,自动识别控件的定位方式(xpath/css selector, 也可以是自然语言驱动的形式),如下图:

Midscene 核心能力
|
|
|
|---|---|
aiInput(locatePrompt, { value }) |
|
aiTap(locatePrompt) |
|
aiAssert(prompt) |
|
aiQuery(prompt) |
|
constagent=newPlaywrightAgent(page);// 不需要知道是什么 CSS,直接描述awaitagent.aiInput('找到搜索输入框',{value:'提示词模板'});awaitagent.aiTap('点击放大镜搜索按钮');awaitagent.aiAssert('搜索结果列表已显示');
运行原理
- 截图捕获
→ page.screenshot()获取当前视口图像 - DOM 提取
→ 同时抓取可访问性树,提供双重上下文 - 模型推理
→ 截图 + 操作意图发送给视觉大模型(Qwen-VL 等) - 坐标反推
→ 模型识别目标元素位置,转换为可执行 locator - 执行操作
→ Playwright 真实点击/输入,生成带截图的可视化报告
二、用CodeBuddy/cursor/codex/CC + MCP + Skills编写测试用例
传统方式:手动打开浏览器 → 看 DOM 结构 → 手写选择器 → 写用例代码,流程割裂、效率低。
新方式:AI IDE 全程辅助,从需求到代码一气呵成。
IDE辅助编写UI自动化的整体架构

再看这张图展示了完整的工具链分层架构:
|
|
|
|
|---|---|---|
| AI Agent 层 |
|
|
| MCP 协议层 |
|
|
| 浏览器层 |
|
|
| 被测平台 |
|
|
两条路径的分工:
- 探索性测试/调试
:通过 Playwright MCP Server,AI 实时操控浏览器,适合开发期探索页面结构、生成初始测试代码 - 批量测试/CI
:通过 Playwright CLI + Skills,在 CI 流水线中批量执行,Skills 保证代码始终符合项目规范
工具链架构
AI IDE ↓ 理解需求意图Skills ↓ 注入项目架构规范、定位优先级规则MCP Browser Tool ↓ 实时打开目标页面,抓取真实 DOM生成符合规范的 TypeScript 测试代码
Skills的关键作用
在项目根目录的 rules.md 中定义项目规范,AI IDE 会自动将其注入到 AI 上下文中。这意味着 AI 每次生成代码都「知道」:
-
项目是四层架构,禁止用例层 new XxxPage() -
定位优先级: id > name > type > 语义class > 文案 -
v-select 组件有双 DOM 陷阱,必须用 :not(.is-hidden)过滤 -
应用名称必须用 randomAppName()生成,末尾带_qta
效果:AI 不会再犯已知错误,生成代码直接符合团队规范,无需反复纠正。
MCP浏览器工具
配合 Playwright MCP 工具,AI IDE 可以:
-
实时导航到目标页面 -
抓取真实 DOM 结构(而非猜测) -
根据真实 DOM 选择最优定位方式
三、四层分层架构设计
整体来说 rules.md 是告诉 AI 应该如何生成代码的,本质上,我们的 UI 自动化框架,应该仍然是我之前分享过的 4 层架构:

对这个架构不了解的同学可以访问:https://testerhome.com/articles/31647
tests/├── components/base/ # 【组件层】可复用 UI 控件原子操作│ ├── select.component.ts # v-select 封装(双DOM陷阱)│ ├── search-input.component.ts # 搜索框(AI 驱动)│ └── captcha.component.ts # 验证码├── pages/ # 【页面层】单页面操作封装│ ├── login.page.ts│ ├── home.page.ts│ └── agent-editor.page.ts├── services/ # 【业务逻辑层】完整业务流程│ ├── auth.service.ts # 登录流程│ └── app.service.ts # 创建应用流程└── cases/ # 【用例层】测试用例 + 断言 ├── login.spec.ts └── optimize-prompt.spec.ts
各层职责边界
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
这里可以给出,我给大模型的提示词:
我现在要涉及一个专业的,企业级的 UI自动化测试项目。 所以我需要把代码进行分层。1. 组件层:因为前端也都是组件化开发的,所以各个组件都是复用的。 比如某个button的放在了多个界面上,所以它的定位方式是一样的。 所以我需要单独一层来进行封装这些组件的定位方式。2. Page层(页面层):封装单独页面的操作:3. Service层:在页面层之上,封装成熟的业务逻辑, 可能会调用多个Page的的逻辑。4. case层:编写测试用例的地方。
上面是我当初初始化项目的时候,用的提示词。
四、混合定位策略:稳定用传统,易变用AI
这是最重要的经验总结。 Midscene 的基于计算机视觉的控件定位方式虽然好,但它有几个缺点:
-
很烧 token,如果每个控件都走 AI 识别,那必定是一笔非常大的支出。 -
视觉方案执行很慢:我用的公司内部署的多模态大模型,识别一个控件的速度大约在 5s 左右。毕竟要把截图发送给大模型进行识别,这个识别速度很难保证。并且你也无法保证大模型的效果如何,万一大模型抽风,识别错了,就尴尬了。
所以我们注定要使用传统定位方式 +AI 驱动的方式来进行定位。 下面我介绍几个核心原则。
核心原则
不是所有定位都需要 AI,AI 定位有真实的 Token 成本。
传统定位:适用场景
当元素有稳定的 HTML 属性时,永远优先用传统定位:
// ✅ 表单控件有 name 属性 → 最优选page.locator('input[name="loginName"]')page.locator('input[name="password"]')// ✅ 业务语义 class → 稳定page.locator('button.add-agent')page.locator('button.add-plugin-btn')// ✅ v-select 双DOM陷阱专用写法page.locator('.v-popper.v-select__popper:not(.is-hidden) li:has-text("Multi-Agent模式")')
AI定位:适用场景
当元素属性不稳定、无语义属性、或是复杂控件时,降级用 AI:
// ✅ 搜索框 → 无稳定属性awaitagent.aiInput('找到搜索输入框',{value:keyword});// ✅ CodeMirror 富文本编辑器 → 无 value 属性awaitagent.aiInput('找到提示词编辑区域,在"提示词"文案下方',{value:content});// ✅ 图标型按钮 → 无文案awaitagent.aiTap('点击放大镜搜索按钮');// ✅ Tab 切换 → class 不稳定awaitagent.aiTap('点击"自定义模板" Tab');
五、使用的几个技巧
注意:经过我的计算,每次 AI 调用(aiTap/aiInput)都需要截图并发送给视觉模型,约消耗 500-1500 token。
技巧1:优先传统定位
最直接的节省方式。90% 的元素都有稳定属性,只有真正不稳定的才用 AI。
技巧2:AI 调用前等待页面稳定
asyncfillPromptContent(content:string){awaitthis.page.waitForTimeout(5_000);// 等待编辑器渲染完毕constagent=newPlaywrightAgent(this.page);awaitagent.aiInput('找到提示词编辑区',{value:content});}
页面未稳定时 AI 可能定位失败触发重试,反而消耗更多 token。
技巧3:封装复用,避免重复写AI指令,并且midscence有缓存,可能利用缓存(它使用自然语言的描述作为缓存的key)
// ✅ 封装为组件层,多处复用同一 AI 指令exportclassSearchInputComponent{asyncsearch(keyword:string){constagent=newPlaywrightAgent(this.page);awaitagent.aiInput('找到搜索输入框',{value:keyword});awaitagent.aiTap('点击放大镜搜索按钮');}}
技巧4:精确描述缩小搜索范围
// ❌ 模糊描述 → 模型需要分析整个页面awaitagent.aiTap('找到输入框');// ✅ 精确描述 → 模型快速定位awaitagent.aiInput('找到"提示词"文案下方的富文本编辑区域',{value:content});
技巧5:合并连续操作用 aiAction
// ❌ 3 次截图awaitagent.aiTap('点击新建按钮');awaitagent.aiInput('找到标题输入框',{value:title});awaitagent.aiTap('点击提交按钮');// ✅ 1 次截图(连续操作合并)awaitagent.aiAction(`点击新建按钮,填写标题"${title}",点击提交`);
六、完整实战教程:用CodeBuddy + Midscene从零搭建UI自动化测试
以一个通用 Todo 应用(增删改查)为例,完整演示:环境搭建 → 配置多模态模型 → 用 CodeBuddy 对话生成用例 → 使用 AI 视觉 API 操作页面 → 运行验证。
Step1:安装依赖
mkdir my-ui-test &&cd my-ui-testnpm init -y# Playwright 测试框架npm install-D @playwright/testnpx playwright install chromium# Midscene Playwright 集成包npm install-D @midscene/web# 加载 .env 的工具npm install-D dotenv
Step2:配置多模态视觉模型
Midscene 需要一个支持视觉的多模态大模型(VL Model)。模型通过 .env 文件配置,支持所有兼容 OpenAI 接口的视觉模型。
创建 .env 文件:
# ── 方案一:使用通义千问 Qwen-VL(推荐,官方支持)──────────────────MIDSCENE_MODEL_BASE_URL="https://dashscope.aliyuncs.com/compatible-mode/v1"MIDSCENE_MODEL_API_KEY="sk-你的阿里云DashScope密钥"MIDSCENE_MODEL_NAME="qwen-vl-max-latest"MIDSCENE_MODEL_FAMILY="qwen2.5-vl"# ── 方案二:使用 Google Gemini ─────────────────────────────────────# MIDSCENE_MODEL_BASE_URL="https://generativelanguage.googleapis.com/v1beta/openai"# MIDSCENE_MODEL_API_KEY="你的Gemini API Key"# MIDSCENE_MODEL_NAME="gemini-2.0-flash"# MIDSCENE_MODEL_FAMILY="gemini"# ── 方案三:使用智谱 GLM-4V ───────────────────────────────────────# MIDSCENE_MODEL_BASE_URL="https://open.bigmodel.cn/api/paas/v4"# MIDSCENE_MODEL_API_KEY="你的智谱API Key"# MIDSCENE_MODEL_NAME="glm-4v-plus"# MIDSCENE_MODEL_FAMILY="glm-v"# ── 方案四:使用企业自建兼容接口(如腾讯混元)──────────────────────# MIDSCENE_MODEL_BASE_URL="https://你的企业模型地址"# MIDSCENE_MODEL_API_KEY="sk-你的密钥"# MIDSCENE_MODEL_NAME="模型名称/视觉模型"# 注意:若模型不在官方支持列表,注释掉 MIDSCENE_MODEL_FAMILY 即可
MIDSCENE_MODEL_FAMILY 支持的官方值:
|
|
|
|---|---|
qwen2.5-vl |
|
gemini |
|
glm-v |
|
doubao-vision |
|
gpt-5 |
|
注意:
MIDSCENE_MODEL_FAMILY必须填官方支持的值,否则报Invalid MIDSCENE_MODEL_FAMILY value。如果你的模型不在列表中(如企业私有化部署),注释掉这行,Midscene 会跳过校验直接调用。
Step3:配置 Playwright 和环境加载
playwright.config.ts
import{defineConfig}from'@playwright/test';import*asdotenvfrom'dotenv';// 主进程加载 .env(worker 进程需要额外处理,见下方)dotenv.config();exportdefaultdefineConfig({testDir:'./tests',timeout:120_000,// AI 推理需要时间,超时设长一些use:{headless:false,// 建议非 headless,便于观察 AI 操作过程baseURL:'http://localhost:3000',},});
tests/helpers/setup.ts(解决 worker 进程读不到 .env 的问题)
Playwright 每个测试文件在独立 worker 进程中运行,主进程的
dotenv.config()不会传递给 worker。需要在每个 spec 文件中手动加载一次。
import*asdotenvfrom'dotenv';import*aspathfrom'path';dotenv.config({path:path.resolve(__dirname,'../../.env'),override:true,// 覆盖已有变量,确保 worker 中生效});
Step4:用 CodeBuddy 对话生成测试用例
这是整个体系最核心的效率来源。不需要手写所有代码——直接和 CodeBuddy 对话,描述测试场景,AI 帮你生成符合规范的代码。
典型对话流程:
你:
我要测试 Todo 应用的新建任务功能。访问 http://localhost:3000,页面上有一个输入框和"添加"按钮。请帮我写一个 Playwright + Midscene 的测试用例:1. 打开页面2. 在输入框里输入"买牛奶"3. 点击"添加"按钮4. 断言列表中出现了"买牛奶"这条记录输入框没有 id,但有 placeholder="请输入任务名称"。"添加"按钮是 <button type="submit">。
CodeBuddy:
import{test,expect}from'@playwright/test';import{PlaywrightAgent}from'@midscene/web/playwright';test('新建 Todo 任务',async({page})=>{// 打开页面awaitpage.goto('http://localhost:3000');// 输入框有 placeholder 属性,用传统定位(稳定)awaitpage.locator('input[placeholder="请输入任务名称"]').fill('买牛奶');// 提交按钮有 type 属性,用传统定位awaitpage.locator('button[type="submit"]').click();// 断言列表中出现了新任务awaitexpect(page.locator('li:has-text("买牛奶")')).toBeVisible();});
你(追问):
任务列表是动态渲染的,有时候加载慢。另外"删除"按钮是一个垃圾桶图标,没有文案也没有 id,我想补充一个删除的用例,用 AI 视觉来找到它。
CodeBuddy:
test('删除 Todo 任务',async({page})=>{awaitpage.goto('http://localhost:3000');// 先创建一条任务awaitpage.locator('input[placeholder="请输入任务名称"]').fill('买牛奶');awaitpage.locator('button[type="submit"]').click();// 等待列表渲染完成awaitpage.locator('li:has-text("买牛奶")').waitFor({state:'visible'});// 删除按钮是图标,没有文案,用 AI 视觉定位constagent=newPlaywrightAgent(page);awaitagent.aiTap('"买牛奶"这条任务右侧的删除按钮(垃圾桶图标)');// 断言任务已消失awaitexpect(page.locator('li:has-text("买牛奶")')).not.toBeVisible();});
你(继续):
帮我把这两个用例整理成规范的 Page Object 结构,把页面操作封装到 page 层,用例层只调用方法和写断言。
CodeBuddy 会按你的架构规范自动生成完整的分层代码(如果你配置了 .codebuddy/rules.md,生成的代码会直接符合团队规范)。
在 AI 驱动编程的时代,学会耐心地与大模型进行沟通,通过一次次的对话来完成代码编写工作是很必要的。
Step5:Midscene AI 视觉 API 详解
当传统定位无法胜任时,使用 PlaywrightAgent 的 AI 方法。
import{PlaywrightAgent}from'@midscene/web/playwright';constagent=newPlaywrightAgent(page);
aiTap(prompt) — 视觉点击
// 找到元素并点击,prompt 用自然语言描述"你想点什么"awaitagent.aiTap('页面右上角的用户头像');awaitagent.aiTap('"买牛奶"这条任务右侧的红色删除按钮');awaitagent.aiTap('弹窗底部的"确认"按钮');// 精确描述可以减少 AI 推理时间和 Token 消耗// ❌ 模糊:await agent.aiTap('按钮');// ✅ 精确:await agent.aiTap('新建任务表单右侧的提交按钮');
aiInput(prompt, { value }) — 视觉输入
// 找到输入框并输入内容awaitagent.aiInput('任务名称输入框',{value:'买牛奶'});awaitagent.aiInput('页面顶部的搜索框',{value:'keyword'});// 适用场景:富文本编辑器(CodeMirror/Quill)、无 name/id 的输入框awaitagent.aiInput('正文编辑区域(富文本框)',{value:'这是正文内容'});
aiAssert(prompt) — 视觉断言
// 判断页面当前状态是否符合预期awaitagent.aiAssert('任务列表中存在"买牛奶"这条记录');awaitagent.aiAssert('页面显示了成功提示 Toast');awaitagent.aiAssert('提交按钮处于禁用状态(灰色不可点击)');
aiQuery(prompt) — 从页面提取信息
// 从页面视觉提取结构化数据constresult=awaitagent.aiQuery('{ count: number }','任务列表当前显示的任务总数');console.log(result.count);// 例如:5conststatus=awaitagent.aiQuery('{ text: string }','页面顶部状态栏的文字内容');
aiAction(prompt) — 合并多步操作(省 Token)
// 将多个连续操作合并为一次 AI 调用,只截图一次// 适合填写表单等连续输入场景// ❌ 低效:3 次截图 + 3 次 AI 调用awaitagent.aiInput('标题输入框',{value:'我的任务'});awaitagent.aiInput('描述输入框',{value:'任务描述'});awaitagent.aiTap('提交按钮');// ✅ 高效:1 次截图 + 1 次 AI 调用awaitagent.aiAction('在标题输入框填入"我的任务",在描述输入框填入"任务描述",然后点击提交按钮');
Step6:一个完整的测试用例文件
把以上所有元素放在一起,看一个完整的可运行示例:
import'./helpers/setup';import{test,expect}from'@playwright/test';import{PlaywrightAgent}from'@midscene/web/playwright';constBASE_URL='http://localhost:3000';test.describe('Todo 应用',()=>{test('新建任务 - 传统定位(元素有稳定属性)',async({page})=>{awaitpage.goto(BASE_URL);// ✅ placeholder 属性稳定,用传统定位,Token 消耗为 0awaitpage.locator('input[placeholder="请输入任务名称"]').fill('买牛奶');awaitpage.locator('button[type="submit"]').click();// 等待列表刷新awaitpage.locator('li:has-text("买牛奶")').waitFor({state:'visible'});awaitexpect(page.locator('li:has-text("买牛奶")')).toBeVisible();});test('删除任务 - AI 视觉定位(图标按钮无文案)',async({page})=>{awaitpage.goto(BASE_URL);// 先创建任务(传统定位)awaitpage.locator('input[placeholder="请输入任务名称"]').fill('买牛奶');awaitpage.locator('button[type="submit"]').click();awaitpage.locator('li:has-text("买牛奶")').waitFor({state:'visible'});// ✅ 等待页面稳定,减少 AI 重试awaitpage.waitForLoadState('networkidle');// 删除按钮是图标,无文案无 id,用 AI 视觉定位constagent=newPlaywrightAgent(page);awaitagent.aiTap('"买牛奶"这条任务右侧的删除按钮');awaitexpect(page.locator('li:has-text("买牛奶")')).not.toBeVisible();});test('标记完成 - AI 视觉断言验证状态',async({page})=>{awaitpage.goto(BASE_URL);awaitpage.locator('input[placeholder="请输入任务名称"]').fill('读一本书');awaitpage.locator('button[type="submit"]').click();awaitpage.locator('li:has-text("读一本书")').waitFor({state:'visible'});// 勾选复选框(有 type 属性,传统定位)awaitpage.locator('li:has-text("读一本书") input[type="checkbox"]').click();// ✅ 用 AI 断言验证视觉状态(文字是否出现删除线样式)constagent=newPlaywrightAgent(page);awaitagent.aiAssert('"读一本书"这条任务显示为已完成状态(文字带删除线或变灰)');});test('批量操作 - 用 aiAction 合并多步,节省 Token',async({page})=>{awaitpage.goto(BASE_URL);// 一次 AI 调用完成整个表单填写流程constagent=newPlaywrightAgent(page);awaitagent.aiAction('在任务名称输入框填入"周会准备",'+'在优先级下拉选择"高",'+'在备注输入框填入"需要准备 PPT",'+'然后点击添加按钮');awaitexpect(page.locator('li:has-text("周会准备")')).toBeVisible();});});
Step7:运行与查看报告
# 运行所有测试npx playwright test# 运行单个文件npx playwright test tests/todo.spec.ts# 有头模式运行(可以看到浏览器操作过程)npx playwright test--headed# 查看 Midscene 可视化报告(每个 AI 操作都有截图 + 推理结果)open midscene_run/report/*.html# 查看 Playwright 测试报告npx playwright show-report
Midscene 报告示例 — midscene_run/report/ 下的 HTML 报告会展示:
-
每次 aiTap/aiInput的截图 -
AI 识别到的目标元素位置(红框标注) -
模型的推理过程和置信度 -
整体执行时间和 Token 消耗
结尾
目前使用的还没有很长时间,感受上还不错,我在另一个项目里也在实践另一套 AI 驱动的方案(基于单模态而非多模态的计算机视觉方案),感受都还不错。 下次会分享另一个方案。 另外 AI 生成测试用例的提示词,各种 SKILL 的编写提示词我都记录在了星球里,感兴趣的同学可以加入一下我的星球:

MTSC2026
中国互联网测试开发大会
(深圳站)
议题征集中
欢迎成为MTSC2026大会讲师


航旅纵横APP超19小时系统故障:测试同学拆解6大技术漏洞,绝非偶然
AI自动化工具Midscene重要更新:鸿蒙、PC端全覆盖,新增模型、Skills及报告解析能力
告别脆弱测试!AI代理+Playwright,用组件感知重构E2E(端到端)测试体系
测试质量进阶|前沿趋势融合(5)智能测试体系全景总结与企业级落地路径
夜雨聆风