乐于分享
好东西不私藏

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

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('搜索结果列表已显示');

运行原理

  1. 截图捕获
     → page.screenshot() 获取当前视口图像
  2. DOM 提取
     → 同时抓取可访问性树,提供双重上下文
  3. 模型推理
     → 截图 + 操作意图发送给视觉大模型(Qwen-VL 等)
  4. 坐标反推
     → 模型识别目标元素位置,转换为可执行 locator
  5. 执行操作
     → Playwright 真实点击/输入,生成带截图的可视化报告

二、用CodeBuddy/cursor/codex/CC + MCP + Skills编写测试用例

传统方式:手动打开浏览器 → 看 DOM 结构 → 手写选择器 → 写用例代码,流程割裂、效率低。

新方式:AI IDE 全程辅助,从需求到代码一气呵成。

IDE辅助编写UI自动化的整体架构

再看这张图展示了完整的工具链分层架构:

层次
组成
职责
AI Agent 层
Claude Code / Cursor / CodeBuddy
理解测试需求,生成符合规范的测试代码
MCP 协议层
Playwright MCP Server(探索/调试)Playwright CLI + Skills(批量/CI)
连接 AI 与浏览器,提供工具调用能力
浏览器层
Chromium / Firefox / WebKit
真实浏览器执行,支持截图与 DOM 分析
被测平台
登录页 · 应用列表 · 对话页 · 工作流编辑页
实际被测系统,AI 直接感知页面状态

两条路径的分工:

  • 探索性测试/调试
    :通过 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 控件原子操作封装
不得依赖 pages/services
页面层
单页面操作组合
不得跨页面,不得写 expect
业务逻辑层
跨页面完整流程
不得直接操作 DOM
用例层
业务编排 + 断言
不得直接 new XxxPage()

这里可以给出,我给大模型的提示词:

我现在要涉及一个专业的,企业级的 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
阿里通义千问 Qwen-VL 系列
gemini
Google Gemini 系列
glm-v
智谱 GLM-4V 系列
doubao-vision
字节豆包视觉系列
gpt-5
OpenAI 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及报告解析能力

传统QA测不动LLM:你的AI应用正在默默“撒谎”

从“盯日志”到“看结论”:用AI重构测试失败分析流程

告别脆弱测试!AI代理+Playwright,用组件感知重构E2E(端到端)测试体系

测试质量进阶|前沿趋势融合(5)智能测试体系全景总结与企业级落地路径

必更!Playwright新版布v1.59解锁新特性,Screencast赋能AI代理测试

AI IDE驱动测试革命:Cursor、Trae、Kiro 如何让软件测试从“负担”变为“生产力引擎”