测试开发之路 AI 驱动 UI 自动化的实践心得 - 基于 Midscene

孙高飞 · 2026年04月01日 · 最后由 0x88 回复于 2026年04月01日 · 941 次阅读

前言

年后重新开始看效能相关的任务,由于去年主要在接口自动化,所以最近开始做一些 UI 自动化的东西。 目前实践了一个基于视觉驱动的 Midscene,另一个基于单模态大模型驱动的 Stagehand。 今天这篇主要介绍一下 Midscene。

PS: 我之前有过一个使用 cursor 来生成测试用例和自动化测试代码的直播录屏,还没看的同学可以看一下:https://testerhome.com/articles/43375

一、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) 从页面提取信息
const agent = new PlaywrightAgent(page);

// 不需要知道是什么 CSS,直接描述
await agent.aiInput('找到搜索输入框', { value: '提示词模板' });
await agent.aiTap('点击放大镜搜索按钮');
await agent.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:

// ✅ 搜索框 → 无稳定属性
await agent.aiInput('找到搜索输入框', { value: keyword });

// ✅ CodeMirror 富文本编辑器 → 无 value 属性
await agent.aiInput('找到提示词编辑区域,在"提示词"文案下方', { value: content });

// ✅ 图标型按钮 → 无文案
await agent.aiTap('点击放大镜搜索按钮');

// ✅ Tab 切换 → class 不稳定
await agent.aiTap('点击"自定义模板" Tab');

五、使用的几个技巧

注意:经过我得计算,每次 AI 调用(aiTap/aiInput)都需要截图并发送给视觉模型,约消耗 500-1500 token。

技巧 1:优先传统定位

最直接的节省方式。90% 的元素都有稳定属性,只有真正不稳定的才用 AI。

技巧 2:AI 调用前等待页面稳定

async fillPromptContent(content: string) {
  await this.page.waitForTimeout(5_000);  // 等待编辑器渲染完毕
  const agent = new PlaywrightAgent(this.page);
  await agent.aiInput('找到提示词编辑区', { value: content });
}

页面未稳定时 AI 可能定位失败触发重试,反而消耗更多 token。

技巧 3:封装复用,避免重复写 AI 指令,并且 midscence 有缓存,可能利用缓存(它使用自然语言的描述作为缓存的 key)

// ✅ 封装为组件层,多处复用同一 AI 指令
export class SearchInputComponent {
  async search(keyword: string) {
    const agent = new PlaywrightAgent(this.page);
    await agent.aiInput('找到搜索输入框', { value: keyword });
    await agent.aiTap('点击放大镜搜索按钮');
  }
}

技巧 4:精确描述缩小搜索范围

// ❌ 模糊描述 → 模型需要分析整个页面
await agent.aiTap('找到输入框');

// ✅ 精确描述 → 模型快速定位
await agent.aiInput('找到"提示词"文案下方的富文本编辑区域', { value: content });

技巧 5:合并连续操作用 aiAction

// ❌ 3 次截图
await agent.aiTap('点击新建按钮');
await agent.aiInput('找到标题输入框', { value: title });
await agent.aiTap('点击提交按钮');

// ✅ 1 次截图(连续操作合并)
await agent.aiAction(`点击新建按钮,填写标题"${title}",点击提交`);

七、完整实战教程:用 CodeBuddy + Midscene 从零搭建 UI 自动化测试

以一个通用 Todo 应用(增删改查)为例,完整演示:环境搭建 → 配置多模态模型 → 用 CodeBuddy 对话生成用例 → 使用 AI 视觉 API 操作页面 → 运行验证。


Step 1:安装依赖

mkdir my-ui-test && cd my-ui-test
npm init -y

# Playwright 测试框架
npm install -D @playwright/test
npx playwright install chromium

# Midscene Playwright 集成包
npm install -D @midscene/web

# 加载 .env 的工具
npm install -D dotenv

Step 2:配置多模态视觉模型

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 会跳过校验直接调用。


Step 3:配置 Playwright 和环境加载

playwright.config.ts

import { defineConfig } from '@playwright/test';
import * as dotenv from 'dotenv';

// 主进程加载 .env(worker 进程需要额外处理,见下方)
dotenv.config();

export default defineConfig({
  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 * as dotenv from 'dotenv';
import * as path from 'path';

dotenv.config({
  path: path.resolve(__dirname, '../../.env'),
  override: true,   // 覆盖已有变量,确保 worker 中生效
});

Step 4:用 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 }) => {
  // 打开页面
  await page.goto('http://localhost:3000');

  // 输入框有 placeholder 属性,用传统定位(稳定)
  await page.locator('input[placeholder="请输入任务名称"]').fill('买牛奶');

  // 提交按钮有 type 属性,用传统定位
  await page.locator('button[type="submit"]').click();

  // 断言列表中出现了新任务
  await expect(page.locator('li:has-text("买牛奶")')).toBeVisible();
});

你(追问):

任务列表是动态渲染的,有时候加载慢。另外"删除"按钮是一个垃圾桶图标,
没有文案也没有 id,我想补充一个删除的用例,用 AI 视觉来找到它。

CodeBuddy:

test('删除 Todo 任务', async ({ page }) => {
  await page.goto('http://localhost:3000');

  // 先创建一条任务
  await page.locator('input[placeholder="请输入任务名称"]').fill('买牛奶');
  await page.locator('button[type="submit"]').click();

  // 等待列表渲染完成
  await page.locator('li:has-text("买牛奶")').waitFor({ state: 'visible' });

  // 删除按钮是图标,没有文案,用 AI 视觉定位
  const agent = new PlaywrightAgent(page);
  await agent.aiTap('"买牛奶"这条任务右侧的删除按钮(垃圾桶图标)');

  // 断言任务已消失
  await expect(page.locator('li:has-text("买牛奶")')).not.toBeVisible();
});

你(继续):

帮我把这两个用例整理成规范的 Page Object 结构,把页面操作封装到 page 层,
用例层只调用方法和写断言。

CodeBuddy 会按你的架构规范自动生成完整的分层代码(如果你配置了 .codebuddy/rules.md,生成的代码会直接符合团队规范)。

在 AI 驱动编程的时代,学会耐心的与大模型进行沟通,通过一次次的对话来完成代码编写工作是很必要的。


Step 5:Midscene AI 视觉 API 详解

当传统定位无法胜任时,使用 PlaywrightAgent 的 AI 方法。

import { PlaywrightAgent } from '@midscene/web/playwright';

const agent = new PlaywrightAgent(page);

aiTap(prompt) — 视觉点击

// 找到元素并点击,prompt 用自然语言描述"你想点什么"
await agent.aiTap('页面右上角的用户头像');
await agent.aiTap('"买牛奶"这条任务右侧的红色删除按钮');
await agent.aiTap('弹窗底部的"确认"按钮');

// 精确描述可以减少 AI 推理时间和 Token 消耗
// ❌ 模糊:await agent.aiTap('按钮');
// ✅ 精确:await agent.aiTap('新建任务表单右侧的提交按钮');

aiInput(prompt, { value }) — 视觉输入

// 找到输入框并输入内容
await agent.aiInput('任务名称输入框', { value: '买牛奶' });
await agent.aiInput('页面顶部的搜索框', { value: 'keyword' });

// 适用场景:富文本编辑器(CodeMirror/Quill)、无 name/id 的输入框
await agent.aiInput('正文编辑区域(富文本框)', { value: '这是正文内容' });

aiAssert(prompt) — 视觉断言

// 判断页面当前状态是否符合预期
await agent.aiAssert('任务列表中存在"买牛奶"这条记录');
await agent.aiAssert('页面显示了成功提示 Toast');
await agent.aiAssert('提交按钮处于禁用状态(灰色不可点击)');

aiQuery(prompt) — 从页面提取信息

// 从页面视觉提取结构化数据
const result = await agent.aiQuery(
  '{ count: number }',
  '任务列表当前显示的任务总数'
);
console.log(result.count); // 例如:5

const status = await agent.aiQuery(
  '{ text: string }',
  '页面顶部状态栏的文字内容'
);

aiAction(prompt) — 合并多步操作(省 Token)

// 将多个连续操作合并为一次 AI 调用,只截图一次
// 适合填写表单等连续输入场景

// ❌ 低效:3 次截图 + 3 次 AI 调用
await agent.aiInput('标题输入框', { value: '我的任务' });
await agent.aiInput('描述输入框', { value: '任务描述' });
await agent.aiTap('提交按钮');

// ✅ 高效:1 次截图 + 1 次 AI 调用
await agent.aiAction(
  '在标题输入框填入"我的任务",在描述输入框填入"任务描述",然后点击提交按钮'
);

Step 6:一个完整的测试用例文件

把以上所有元素放在一起,看一个完整的可运行示例:

import './helpers/setup';

import { test, expect } from '@playwright/test';
import { PlaywrightAgent } from '@midscene/web/playwright';

const BASE_URL = 'http://localhost:3000';

test.describe('Todo 应用', () => {

  test('新建任务 - 传统定位(元素有稳定属性)', async ({ page }) => {
    await page.goto(BASE_URL);

    // ✅ placeholder 属性稳定,用传统定位,Token 消耗为 0
    await page.locator('input[placeholder="请输入任务名称"]').fill('买牛奶');
    await page.locator('button[type="submit"]').click();

    // 等待列表刷新
    await page.locator('li:has-text("买牛奶")').waitFor({ state: 'visible' });

    await expect(page.locator('li:has-text("买牛奶")')).toBeVisible();
  });


  test('删除任务 - AI 视觉定位(图标按钮无文案)', async ({ page }) => {
    await page.goto(BASE_URL);

    // 先创建任务(传统定位)
    await page.locator('input[placeholder="请输入任务名称"]').fill('买牛奶');
    await page.locator('button[type="submit"]').click();
    await page.locator('li:has-text("买牛奶")').waitFor({ state: 'visible' });

    // ✅ 等待页面稳定,减少 AI 重试
    await page.waitForLoadState('networkidle');

    // 删除按钮是图标,无文案无 id,用 AI 视觉定位
    const agent = new PlaywrightAgent(page);
    await agent.aiTap('"买牛奶"这条任务右侧的删除按钮');

    await expect(page.locator('li:has-text("买牛奶")')).not.toBeVisible();
  });


  test('标记完成 - AI 视觉断言验证状态', async ({ page }) => {
    await page.goto(BASE_URL);

    await page.locator('input[placeholder="请输入任务名称"]').fill('读一本书');
    await page.locator('button[type="submit"]').click();
    await page.locator('li:has-text("读一本书")').waitFor({ state: 'visible' });

    // 勾选复选框(有 type 属性,传统定位)
    await page.locator('li:has-text("读一本书") input[type="checkbox"]').click();

    // ✅ 用 AI 断言验证视觉状态(文字是否出现删除线样式)
    const agent = new PlaywrightAgent(page);
    await agent.aiAssert('"读一本书"这条任务显示为已完成状态(文字带删除线或变灰)');
  });


  test('批量操作 - 用 aiAction 合并多步,节省 Token', async ({ page }) => {
    await page.goto(BASE_URL);

    // 一次 AI 调用完成整个表单填写流程
    const agent = new PlaywrightAgent(page);
    await agent.aiAction(
      '在任务名称输入框填入"周会准备",' +
      '在优先级下拉选择"高",' +
      '在备注输入框填入"需要准备 PPT",' +
      '然后点击添加按钮'
    );

    await expect(page.locator('li:has-text("周会准备")')).toBeVisible();
  });

});

Step 7:运行与查看报告

# 运行所有测试
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 的编写提示词我都记录在了星球里,感兴趣的同学可以加入一下我得星球:

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 9 条回复 时间 点赞

Midscene 难道只能使用 js 吗?

这样每次回归一轮下来,当前使用的 Token 消耗可控么?

。。。。 回复

它是基于 playwright 的, 有 python sdk 版本,但我没用过。

每次定位,差不多是 1000 到 1500 的 token 消耗。 大多数控件还是建议用传统定位, 实在定位困难或者变化比较大的控件再用 AI 定位

APP 的 UI 自动化也能这么搞吗?

用 midscenejs 的 chrome 的插件,完成了个识别验证码登录的流程,用了将近 4W 多 Token🎃

吹落如雨 回复

自己写一个 app 的 mcp 可以实现,在 ide 或 CC 里都可以,我是用的 CC,必须要用多模态的模型,连接之后等待前端渲染完成然后截图再识别定位,进行操作,点击滑动输入长按都可以,现在就是比较慢,极其依赖模型,我用 gpt 给出一个任务后视觉识别几秒钟就找到了,国内的模型都比较慢还经常识别不到,耗 token 方面其实还好,视觉视觉本身就比较消耗 token,截图之后再识别会好很多,然后就是可以让 AI 执行后生成脚本存起来,Web 的 playwright 也同理,写个脚本存起来方便后续回归不用再次走 agent

吹落如雨 回复

文档里说它是支持全平台的,你可以试试

回复内容未通过审核,暂不显示
需要 登录 後方可回應,如果你還沒有帳號按這裡 注册