AI 驱动的 UI 自动化测试

November 15, 2025

1. 概述

1.1 传统自动化测试的挑战

Playwright是一个现代化的端到端测试框架,由微软开发,专门用于Web应用程序的自动化测试。

// 测试登录表单
test('登录表单测试', async ({ page }) => {
  // 假设这是你的登录页面
  await page.goto('https://example.com/login');
  // 填写用户名
  await page.fill('input[name="username"]', 'testuser');
  // 填写密码
  await page.fill('input[name="password"]', 'testpassword');
  // 点击登录按钮
  await page.click('button[type="submit"]');
  // 验证登录成功后的跳转或消息
  await expect(page).toHaveURL(/.*dashboard/);
});

传统自动化测试需要指定确切的DOM节点,这样会存在以下问题:

  • 深度耦合:测试代码和业务代码的深度耦合,业务代码重构后,测试代码也需要重构
  • 学习成本高:编写用例需要学习Playwright的语法并了解业务代码,效率比较低
  • 维护成本高:UI变更时需要频繁更新选择器

1.2 AI在测试领域的发展趋势

随着AI技术的发展,测试领域出现了新的解决方案:

  • MCP集成:Playwright支持MCP服务器,使用自然语言与大模型交互,无需编写传统的自动化代码
  • 自然语言测试:Midscene使用自然语言进行自动化测试,降低了技术门槛

2. Midscene介绍

2.1 什么是Midscene

Midscene.js 是由字节跳动全新开源的 UI 自动化工具,通过引入多模态 AI 推理能力,帮助开发者打破传统 UI 自动化难于编写和维护的困境。

项目地址: https://github.com/web-infra-dev/midscene

2.2 Midscene的优势

  • 简单易用:使用自然语言,可以快速编写测试用例
  • 松耦合:与业务代码解耦,无需关注具体的代码实现,方便对代码进行重构
  • 维护性强:UI变更时测试用例通常无需修改

2.3 Midscene的局限性

  • 执行速度较慢:执行速度比传统自动化测试代码调用慢,可以通过一些方式提高运行效率,具体见文档
  • 成本考虑:调用模型有一定成本
  • 交互限制:目前仅支持点击、拖拽(只在 UI-TARS 模型中支持)、输入、键盘和滚动操作
  • 稳定性风险:AI 模型的返回值不是 100% 准确的,需要遵循编写提示词的技巧来提高稳定性
  • 验证码限制:无法绕过验证码,有些 LLM 服务会拒绝涉及验证码解决的请求

2.4 预估费用

针对 Midscene.js 样例项目,这是费用消耗数据(gpt-4o-08-06 模型,且未启用 prompting caching):

任务分辨率Prompt Tokens / 价格Completion Tokens / 价格总价
拆解(Plan)并在 eBay 执行一次搜索1280x8006005 / $0.0150125146 / $0.00146$0.02
提取(Query)eBay 搜索结果的商品信息1280x8009107 / $0.0227675122 / $0.00122$0.02

引用自: https://zhuanlan.zhihu.com/p/15458924120

2.5 快速上手

2.5.1 环境配置

配置密钥:

OPENAI_BASE_URL="https://dashscope.aliyuncs.com/compatible-mode/v1" # 或任何其他提供商的接入点
OPENAI_API_KEY="sk-xxxxxxxxxxxxxx"
MIDSCENE_MODEL_NAME="qwen-vl-max-latest"
MIDSCENE_USE_QWEN_VL=1

2.5.2 项目初始化

详细集成指南: Midscene 与 Playwright 集成

2.5.3 基础API

1. 执行交互 Action

// 基本用法
await agent.aiAction('在搜索框中输入 "JavaScript",然后点击搜索按钮');

2. 数据提取 Query

// 使用此方法,你可以直接从 UI 提取结构化的数据
const dataA = await agent.aiQuery({
    time: '左上角展示的日期和时间,string',
    userInfo: '用户信息,{name: string}',
    tableFields: '表格的字段名,string[]',
    tableDataRecord: '表格中的数据记录,{id: string, [fieldName]: string}[]',
});

3. 断言 Assert

await agent.aiAssert('"Sauce Labs Onesie" 的价格是 7.99');

2.5.4 代码示例演示

test.beforeEach(async ({ page }) => {
  await page.goto("https://todomvc.com/examples/react/dist/");
});

test("todo", async ({ ai, aiQuery, aiAssert, aiTap, aiHover }) => {
  // .ai - 通用 AI 操作方法
  await ai("在任务框 input 输入 今天学习 JS,按回车键");
  await ai("在任务框 input 输入 明天学习 Rust,按回车键");
  await ai("在任务框 input 输入后天学习 AI,按回车键");

  // .aiTap, .aiHover - 即时操作接口
  await aiHover('任务列表中的第二项');
  await aiTap("第二项任务右边的删除按钮");

  await aiTap("第二条任务左边的勾选按钮");
  await aiTap("任务列表下面的 completed 状态按钮");

  await aiAssert('列表下方有一个区域显示有 "1 item left"');
});

3. 技术实现

3.1 流程图

在playwright环境执行aiAction的流程如下:

Midscene 会根据 Playwright 的截图去调用大模型,从而定位到DOM节点的位置,然后根据用户的指令再去调用模型来生成操作规划,这些操作规划最终会去调用 Playwright 的API。

3.2 模块划分

3.2.1 缓存模块

1. XPath缓存

todolist网址上"What needs to be done?"输入框的dom节点路径,缓存dom节点的XPath。XPath MDN介绍

{
  type: 'locate',
  prompt: "The input field labeled 'What needs to be done?'",
  xpaths: [ '/html/body/section[1]/header[1]/div[1]/input[1]' ]
}

2. 规划缓存

使用 YAML 来定义流程规划。"在任务框 input 输入 今天学习 JS,按回车键"这个提示词需要执行两件事情:

  • 输入"今天学习 JS"
  • 按回车键
{
  type: 'plan',
  prompt: '在任务框 input 输入 今天学习 JS,按回车键',
  yamlWorkflow: 'tasks:\n' +
    '  - name: 在任务框 input 输入 今天学习 JS,按回车键\n' +
    '    flow:\n' +
    "      - aiInput: ''\n" +
    '        value: 今天学习 JS\n' +
    "        locate: The input field labeled 'What needs to be done?'\n" +
    "      - aiKeyboardPress: ''\n" +
    '        keyName: Enter\n'
}

3.2.2 AI调用模块

提示词: https://github.com/web-infra-dev/midscene/blob/main/packages/core/src/ai-model/prompt/llm-planning.ts#L187-L233

输入:

  • 页面截图 - Playwright 捕获的当前页面截图(图像数据)
  • 用户指令 - 自然语言指令,如:"在任务框 input 输入 '今天学习 JS',然后按回车键"
  • 上下文信息 - 当前页面的状态和历史操作

输出:

// 第一次响应
{
  "what_the_user_wants_to_do_next_by_instruction": "在任务框 input 输入 '今天学习 JS',然后按回车键",
  "log": "I will use action Input to type '今天学习 JS' into the input field",
  "more_actions_needed_by_instruction": true,
  "action": {
    "type": "Input",
    "param": {
      "value": "今天学习 JS",
      "locate": {
        "bbox": [510, 132, 1060, 198],
        "prompt": "The input field labeled 'What needs to be done?'"
      }
    }
  }
}

// 第二次响应
{
  "what_the_user_wants_to_do_next_by_instruction": "在任务框 input 输入 '今天学习 JS' 后,需要按回车键确认输入",
  "log": "I will use action KeyboardPress to press the Enter key to confirm the input",
  "more_actions_needed_by_instruction": false,
  "action": {
    "type": "KeyboardPress",
    "param": {
      "keyName": "Enter"
    }
  }
}

3.2.3 调用playwright模块

对应的源码在: packages/web/src/lib/puppeteer/base-page.ts

// 调用playwright的API
page.keyboard.type("今天学习 JS", { delay: 80 })
page.keyboard.press("Enter")

4. 案例实践

import { test } from "./fixture";

test("通过magic link登录", async ({ page, ai, aiQuery, aiAction, aiAssert, aiTap, aiInput, aiHover }) => {
    await page.goto("https://app.creatify.ai/auth/magic-link/b525980660ed4d5cad94cc5169c205d0/?redirect_url=https://app.creatify.ai/home");
    await aiAction('等待10秒钟,直到页面加载完成')
    await aiTap('AI Video Ads')
    await aiAssert("页面上写着Share your product link to generate a video");
});

4.2 测试未登录态使用 url to video

import { test } from "./fixture";

test("未登录态使用 url to video", async ({ page, ai, aiQuery, aiAction, aiAssert, aiTap, aiInput, aiHover }) => {
    await page.goto("https://app.creatify.ai/tool/link-to-video");
    await aiTap("Try some links");
    await aiTap("Analyze URL");

    await aiAssert("出现登录弹窗")
});

5. 调试指南

Playwright 提供了可视化的调试界面,可以帮助我们更好地调试 Midscene 测试:

# 启动 UI 模式
npx playwright test --ui

调试界面

6. 附录