Playwright MCP实战:5分钟搞定AI驱动的表单自动化测试(含代码示例)

最近在重构一个老项目的自动化测试套件时,我又一次被那些脆弱的CSS选择器折磨得够呛。页面结构稍微调整一下,整个测试脚本就崩溃了,维护成本高得吓人。就在我几乎要放弃的时候,团队里一个同事提到了Playwright MCP,说它用AI的方式重新定义了浏览器自动化。抱着试试看的心态,我花了一个下午研究,结果发现这玩意儿简直是前端测试工程师的“作弊器”——特别是对于表单测试这种高频又繁琐的场景。今天我就把自己踩坑和实战的经验整理出来,分享给那些同样被动态元素定位、复杂表单验证困扰的开发者们。

1. 环境配置与核心概念速览

如果你和我一样,第一次听到“MCP”这个词时有点懵,别担心,它没有听起来那么复杂。MCP全称是Model Context Protocol,你可以把它理解为一个标准化的通信桥梁,专门为了让大语言模型(LLM)能够更自然、更稳定地与外部工具(比如浏览器)对话。Playwright MCP则是微软基于这个协议,为浏览器自动化量身打造的一套工具集。

传统的自动化测试脚本,无论是用Selenium还是Playwright原生API,都严重依赖于开发者手动编写精确的元素选择器。当页面DOM结构发生变化,或者某些元素是动态生成的时候,这些选择器就会失效。Playwright MCP的核心创新在于,它不再依赖传统的DOM路径或CSS选择器进行元素定位,而是转向了更稳定、语义更丰富的“可访问性树”(Accessibility Tree)。

提示:可访问性树是浏览器为辅助技术(如屏幕阅读器)构建的页面语义化表示。它包含了元素的角色(role)、名称(name)、状态等属性,远比DOM结构稳定,因为前端框架再怎么变,一个“提交按钮”的语义角色通常不会改变。

安装过程异常简单,尤其是如果你已经习惯了Node.js生态。最快捷的方式是通过npx直接运行:

# 安装并启动一个基础的Playwright MCP服务器
npx @playwright/mcp@latest

不过,为了在项目中持续使用,我推荐创建一个配置文件。下面是我在项目中使用的 .mcp.config.json 基础配置,它定义了两个不同的服务器实例,分别用于常规测试和需要视觉识别的场景:

{
  "mcpServers": {
    "default": {
      "command": "npx",
      "args": ["@playwright/mcp@latest", "--browser=chrome", "--headless"]
    },
    "vision": {
      "command": "npx",
      "args": ["@playwright/mcp@latest", "--browser=chrome", "--vision"]
    }
  }
}
  • default 服务器使用默认的Snapshot模式,基于可访问性树工作,速度快,适合绝大多数逻辑交互。
  • vision 服务器启用了视觉模式,当页面元素无法通过可访问性树有效识别时(比如验证码、自定义绘制的图形),可以尝试用这个。

配置好后,你的测试脚本就可以通过标准的MCP客户端(比如LangChain、Claude Desktop等)连接到这个服务器,开始发送指令了。整个搭建过程,从零到可以运行第一个指令,真的用不了五分钟。

2. 告别选择器地狱:基于AI的智能元素定位

这是Playwright MCP最让我兴奋的部分。我们再也不需要去写 page.locator(‘#root > div > form > div:nth-child(2) > input’) 这种脆弱无比的表达式了。MCP的工作流程可以概括为“先看后动”。

第一步,获取页面快照(Snapshot)。 这个快照不是截图,而是一个包含了当前页面所有可交互元素的、结构化的JSON描述。每个元素都有唯一的引用标识(ref)和清晰的语义化属性。

// 假设我们已经有了一个连接到MCP服务器的客户端实例 `mcp`
const snapshot = await mcp.execute(‘browser_snapshot’);

// 查看快照结构,它会列出所有可交互元素
console.log(snapshot.elements);

快照的输出可能类似于这样(简化版):

{
  “elements”: {
    “usernameInput”: { “ref”: “a1b2c3”, “role”: “textbox”, “name”: “用户名” },
    “passwordInput”: { “ref”: “d4e5f6”, “role”: “textbox”, “name”: “密码”, “attributes”: {“type”:“password”} },
    “loginButton”: { “ref”: “g7h8i9”, “role”: “button”, “name”: “登录” },
    “rememberMeCheckbox”: { “ref”: “j0k1l2”, “role”: “checkbox”, “name”: “记住我” }
  }
}

第二步,使用自然语言或语义属性进行操作。 拿到元素的 ref 之后,后续的所有操作都基于这个稳定的引用。即使页面重新渲染,只要这个元素的语义角色没变,ref 在单次会话中通常是稳定的。

传统方式与MCP方式对比:

操作传统 Playwright (依赖选择器)Playwright MCP (依赖语义引用)
定位登录输入框page.locator(‘input[type=“text”][name=“user”]’)使用快照中 usernameInputref
定位动态生成的按钮page.locator(‘button:has-text(“提交”)’).last()使用快照中 submitButtonref
优势精细控制,可直接操作DOM属性稳定性高,不受DOM结构微调影响;意图清晰,直接对应功能。
劣势选择器易碎,维护成本高;意图不直观。需要先获取快照;对元素语义化要求高。

这种模式的转变,意味着我们的测试脚本从“描述如何找到元素”变成了“声明要操作什么元素”。测试的逻辑更贴近用户的实际行为:“在叫做‘用户名’的框里输入文字”,而不是“在某个div下的第三个input里输入文字”。当开发同事修改了前端框架,把div换成了section,或者调整了CSS类名时,只要按钮的 accessible name 还是“登录”,我的测试就完全不受影响。

3. 表单自动化实战:从登录到多步提交

理论讲完了,我们来点实际的。我以一个常见的用户注册表单为例,这个表单包含文本框、密码框、下拉选择、单选框、复选框和文件上传。用Playwright MCP,我们可以在几分钟内写出健壮的测试脚本。

首先,导航到目标页面并获取快照:

// 1. 导航到注册页面
await mcp.execute(‘browser_navigate’, { url: ‘https://example.com/register’ });
// 等待页面加载稳定
await mcp.execute(‘browser_wait’, { time: 2 });

// 2. 获取页面语义快照,这是所有操作的基础
const snap = await mcp.execute(‘browser_snapshot’);
console.log(‘可用的元素:’, Object.keys(snap.elements)); // 查看有哪些元素

接下来,开始填充表单。你会发现,代码读起来就像在口述测试步骤:

// 3. 填写基础文本字段
await mcp.execute(‘browser_type’, {
  ref: snap.elements.fullNameInput.ref, // 引用快照中的‘全名’输入框
  text: “张三”
});

await mcp.execute(‘browser_type’, {
  ref: snap.elements.emailInput.ref,
  text: “zhangsan@example.com”
});

// 对于密码框,MCP同样能识别其类型
await mcp.execute(‘browser_type’, {
  ref: snap.elements.passwordInput.ref,
  text: “MySecurePass123!”
});

// 4. 操作下拉选择框
await mcp.execute(‘browser_select_option’, {
  ref: snap.elements.countrySelect.ref,
  values: [“CN”] // 选择值为中国
});

// 5. 操作单选按钮和复选框
await mcp.execute(‘browser_click’, {
  ref: snap.elements.genderMaleRadio.ref // 选择‘男性’
});

await mcp.execute(‘browser_click’, {
  ref: snap.elements.agreeToTermsCheckbox.ref // 勾选‘同意条款’
});

// 6. 文件上传(这是传统自动化中比较麻烦的一环)
await mcp.execute(‘browser_file_upload’, {
  ref: snap.elements.avatarUpload.ref, // 指向文件上传输入框
  paths: [“/Users/me/avatar.jpg”] // 本地文件路径
});

最后,提交表单并验证结果。我们可以通过检查页面URL变化或寻找成功提示元素来断言:

// 7. 提交表单
await mcp.execute(‘browser_click’, {
  ref: snap.elements.submitButton.ref
});

// 8. 等待跳转或成功消息出现
await mcp.execute(‘browser_wait’, { time: 3 });

// 9. 获取提交后的页面快照,验证是否包含成功元素
const resultSnap = await mcp.execute(‘browser_snapshot’);
if (resultSnap.elements.registrationSuccessMessage) {
  console.log(‘✅ 注册成功!’);
} else {
  console.log(‘❌ 注册可能失败,未找到成功提示。’);
}

整个流程写下来,代码里没有出现一个CSS选择器,全部通过元素的语义角色和名称来引用。这不仅让脚本更容易编写,也让后来阅读代码的人一眼就能看懂每一步在做什么。

4. 处理复杂与动态场景:Vision模式与高级技巧

当然,真实的项目不会总是这么理想。我们会遇到一些“顽固”的元素,比如:

  • 完全自定义绘制、没有可访问性信息的UI组件。
  • 基于Canvas或WebGL的图形界面。
  • 某些前端框架生成的不太规范的DOM。

这时,Snapshot模式可能就力不从心了。Playwright MCP的 Vision模式 就是为这些场景准备的备选方案。它通过计算机视觉技术来分析屏幕像素,让你可以通过坐标、图像匹配等方式进行操作。

启用Vision模式后,你可以进行一些“像素级”操作:

// 切换到Vision模式的服务连接
const visionMcp = await connectToServer(‘vision’); // 假设的连接函数

// 方法A:直接坐标点击(知道精确位置时)
await visionMcp.execute(‘browser_screen_click’, {
  x: 450,
  y: 320
});

// 方法B:更常见的是,先截图,然后基于图像查找
// 注意:MCP当前版本可能将视觉查找功能集成在更高层的AI Agent中
// 一种模式是,你可以用自然语言描述:“点击那个蓝色的圆形图标”
// MCP服务器会结合视觉模型理解你的指令并执行

注意:Vision模式通常比Snapshot模式慢,且受屏幕分辨率、缩放比例影响。它应该是Snapshot模式失效后的补充手段,而非首选。最佳实践是优先利用好可访问性树。

除了视觉问题,表单测试中还有几个高级场景值得关注:

1. 表单验证与错误处理: 一个健壮的测试需要模拟错误输入,并验证错误提示是否正确显示。

// 故意输入无效邮箱,触发前端验证
await mcp.execute(‘browser_type’, {
  ref: snap.elements.emailInput.ref,
  text: “invalid-email”
});
await mcp.execute(‘browser_click’, { ref: snap.elements.submitButton.ref });

// 短暂等待错误信息渲染
await mcp.execute(‘browser_wait’, { time: 1 });
const errorSnap = await mcp.execute(‘browser_snapshot’);

// 检查是否存在错误提示元素
if (errorSnap.elements.emailErrorAlert && errorSnap.elements.emailErrorAlert.name.includes(‘无效’)) {
  console.log(‘✅ 前端邮箱验证功能正常。’);
}

2. 键盘事件模拟: 测试表单的键盘交互,比如按Tab键切换焦点,按Enter提交。

// 在某个输入框内按Tab键,焦点应移动到下一个元素
await mcp.execute(‘browser_press_key’, {
  ref: snap.elements.fullNameInput.ref,
  key: “Tab”
});

// 直接按回车键提交表单(如果表单支持)
await mcp.execute(‘browser_press_key’, {
  key: “Enter”
});

3. 与现有测试框架集成: 你不需要完全抛弃Jest、Mocha、Playwright Test Runner。你可以将MCP作为这些框架内的一个强大工具来使用。

// 示例:在Jest测试用例中使用Playwright MCP
import { createMcpClient } from ‘./mcp-client’; // 你自己封装的MCP客户端

describe(‘用户注册表单’, () => {
  let mcp;

  beforeAll(async () => {
    mcp = await createMcpClient(); // 启动并连接MCP服务器
    await mcp.execute(‘browser_navigate’, { url: process.env.TEST_URL });
  });

  afterAll(async () => {
    await mcp.execute(‘browser_close’);
  });

  test(‘应该能成功注册新用户’, async () => {
    const snap = await mcp.execute(‘browser_snapshot’);
    // … 填写表单的代码 …
    await mcp.execute(‘browser_click’, { ref: snap.elements.submitButton.ref });

    const resultSnap = await mcp.execute(‘browser_snapshot’);
    expect(resultSnap.elements.registrationSuccessMessage).toBeDefined();
  });
});

通过将MCP与现有框架结合,你可以继续使用你熟悉的断言库、生命周期钩子和报告系统,同时享受AI驱动定位带来的稳定性红利。

5. 构建可维护的测试体系:模式与最佳实践

引入一项新技术,如果只是零星使用,其价值是有限的。要真正提升团队效率,我们需要思考如何将Playwright MCP规模化、工程化地应用到测试体系中。下面是我总结的几个模式和最佳实践。

模式一:页面对象模式(Page Object Model)的MCP变体 传统的POM封装的是选择器和方法。在MCP世界里,我们封装的是“语义快照”和“意图操作”。

// LoginPage.mcp.js
class LoginPage {
  constructor(mcpClient) {
    this.mcp = mcpClient;
    this.snapshot = null;
  }

  async navigate() {
    await this.mcp.execute(‘browser_navigate’, { url: ‘/login’ });
    await this.refreshSnapshot();
  }

  async refreshSnapshot() {
    this.snapshot = await this.mcp.execute(‘browser_snapshot’);
  }

  async login(username, password) {
    await this.mcp.execute(‘browser_type’, {
      ref: this.snapshot.elements.usernameInput.ref,
      text: username
    });
    await this.mcp.execute(‘browser_type’, {
      ref: this.snapshot.elements.passwordInput.ref,
      text: password
    });
    await this.mcp.execute(‘browser_click’, {
      ref: this.snapshot.elements.loginButton.ref
    });
    // 登录后页面可能变化,刷新快照
    await this.mcp.execute(‘browser_wait’, { time: 2 });
    await this.refreshSnapshot();
  }

  isLoginSuccessful() {
    return !!this.snapshot.elements.userDashboardHeader;
  }
}

// 在测试中使用
const loginPage = new LoginPage(mcp);
await loginPage.navigate();
await loginPage.login(‘testuser’, ‘password’);
expect(loginPage.isLoginSuccessful()).toBeTruthy();

模式二:快照稳定性策略 虽然基于可访问性树的ref很稳定,但在某些单页面应用(SPA)中,页面剧烈更新后ref也可能失效。我们需要一个简单的重试和刷新机制。

async function clickWithRetry(mcp, elementName, maxRetries = 2) {
  let snap = await mcp.execute(‘browser_snapshot’);
  let retries = 0;

  while (retries <= maxRetries) {
    if (snap.elements[elementName]) {
      try {
        await mcp.execute(‘browser_click’, { ref: snap.elements[elementName].ref });
        return true; // 点击成功
      } catch (clickError) {
        console.warn(`点击 ${elementName} 失败,尝试刷新快照重试…`);
      }
    }
    // 如果元素不存在或点击失败,等待片刻后刷新快照
    await mcp.execute(‘browser_wait’, { time: 1 });
    snap = await mcp.execute(‘browser_snapshot’);
    retries++;
  }
  throw new Error(`无法点击元素: ${elementName}, 重试 ${maxRetries} 次后失败`);
}

最佳实践清单:

  • 优先使用Snapshot模式:Vision模式是备胎,只在必要时启用。
  • 为元素提供良好的可访问性属性:这不仅是MCP的要求,也是提升产品本身无障碍体验的好习惯。确保按钮有aria-label,表单字段有name
  • 将MCP指令与业务断言分离:用MCP执行操作,用你熟悉的测试框架(Jest, Mocha)做断言,保持关注点分离。
  • 在CI/CD流水线中运行:MCP服务器可以无头模式运行,完美集成到GitHub Actions、Jenkins等CI工具中。记得在配置中加上 --headless 参数。
  • 管理测试数据:和所有自动化测试一样,准备独立的测试账号和数据,避免污染生产环境。

玩转Playwright MCP这几周,最大的感受是测试脚本的“心智负担”减轻了。我不再需要像个侦探一样去分析前端代码结构,而是可以更专注于测试用例本身的设计和业务逻辑的验证。它确实解决了我之前关于“动态元素定位不稳定”的核心痛点。当然,它也不是银弹,对于极度定制化、毫无语义的UI,或者对执行速度有极端要求的场景,你可能还需要结合传统方法。但对于绝大多数中后台系统、官网、标准Web应用的表单测试来说,用MCP来快速搭建和维护自动化用例,效率的提升是实实在在的。下次当你再为又一个失败的选择器头疼时,不妨花上五分钟,试试这种新的思路。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐