上个月一个测试同学跟我说,他让AI写E2E测试,生成两百行,跑起来报错,改一个冒两个,修了三个小时。最后他算了一下,自己从零手写也就四十分钟。
我问他:“你生成之前,让AI看过页面长什么样吗?”
他愣了一下:“没有。我就说‘帮我写一个登录的E2E测试’。”
这就是问题所在。AI不是在“写错的代码”,它是在“写它想象中的代码” 。它没见过你的按钮叫什么名字、class是稳定的还是哈希的,只能按训练数据里最常见的写法猜一段。
Playwright MCP解决的就是这个“想象”问题。它让Claude先操作浏览器、读取真实的页面结构,再基于它实际看到的东西生成代码。你不需要写定位器,也不需要写等待——因为定位器来自快照,等待内建在动作里。
核心顺序:从“猜页面”变成“看页面”
Anthropic在Playwright MCP里做了一个很关键的设计,叫 Reconnaissance-Then-Action——别着急点,先看看页面到底长什么样。
传统方式是:你理解页面 → 找元素 → 写Playwright代码 → 机器执行。
MCP方式是:你给目标 → Claude操作浏览器 → Claude读取页面快照 → Claude基于快照生成代码 → 执行 → 失败就重新看页面、修脚本。
这个顺序一换,AI写出来的定位器,是它实际在页面上见到的元素,不是你描述里“应该存在”的元素。
最小工作流:四步跑通
第1步:装Playwright MCP(1分钟)
前置条件只有Node.js 18+。在项目目录下执行:
claude mcp add playwright npx @playwright/mcp@latest
或者手动在项目根目录创建 .mcp.json:
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["-y", "@playwright/mcp@latest"]
}
}
}
重启Claude Code,输入 /mcp,确认 playwright 状态是connected。
第2步:告诉Claude你的项目约束(2分钟)
这一步很多人跳过,但它决定了生成代码的“风格”对不对。
在项目根目录放一个 CLAUDE.md,写清楚:
测试规范
- 优先使用 getByRole 和 getByTestId
- 禁止使用 waitForTimeout
- 断言必须包含页面可见性和业务状态
- 定位优先级:getByRole > getByLabel > getByText > getByTestId
Claude Code每次进入项目会自动读取CLAUDE.md,你写一次,它每次都按这个规范生成。
第3步:让Claude“先快照,再写”(5分钟)
现在给Claude一个有顺序的目标,而不是一句“帮我写登录测试”。
在Claude Code里输入:
打开 http://localhost:3000/login,先 snapshot 看看页面上有什么。
然后基于你看到的元素,生成一个 Playwright E2E 测试:
- 输入正确的邮箱和密码
- 点击登录
- 验证跳转到 /dashboard,且页面上出现 "Welcome"
- 再生成一个异常用例:密码错误时,应该看到错误提示
关键就在第一句“先 snapshot 看看页面上有什么”。browser_snapshot 会返回当前页面的可访问性快照——不是截图,是结构化的文本,包含每个可交互元素的角色、名称和引用标识(比如 e5)。
Claude拿到这份快照,就知道“页面上有一个textbox,名字叫Email,ref是e5”,而不是猜“这里应该有一个input”。
然后它会依次执行:browser_navigate 打开页面 → browser_snapshot 读取快照 → 基于快照中的元素生成代码 → 执行 → 如果失败,重新快照、修定位器、再试。
第4步:Review,只检查三件事(2分钟)
Claude生成完 .spec.ts 文件后,你不需要逐行看。只检查三件事:
第一,定位器稳不稳。 如果它用了CSS class或XPath,改成 getByRole。Playwright MCP的快照里本来就有可访问角色和名称,用 getByRole 不需要额外工作,而且class重构不会让它碎。
第二,有没有 waitForTimeout。 有就删掉。Playwright的自动等待会在动作执行前检查元素的可见性、稳定性和可操作性,不需要你手写时间窗口。
第三,断言够不够。 只断言“元素可见”不够。登录成功不只是页面显示了“Welcome”,可能还要看cookie或接口响应。至少加一层业务状态校验。
一个真实示例:TodoMVC,第一次跑就过
社区有一个公开的Playwright MCP实测案例,用TodoMVC演示了完整流程。Claude通过MCP打开页面、获取快照,识别出textbox “What needs to be done?” 和 checkbox “Toggle Todo”,生成了下面这段代码:
import { test, expect } from'@playwright/test';
test('add and complete a todo', async ({ page }) => {
await page.goto('https://demo.playwright.dev/todomvc');
await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Buy groceries');
await page.keyboard.press('Enter');
await expect(page.getByText('Buy groceries')).toBeVisible();
await page.getByRole('checkbox', { name: 'Toggle Todo' }).check();
await expect(page.getByText('1 item left')).toBeVisible();
});
这段代码第一次跑就通过了。 因为定位器不是猜的,是Claude在快照里实际看到的。getByRole 和 getByText 都是基于可访问信息生成的,class怎么变都不影响。
为什么不写定位器和等待,反而更稳?
因为快照里的元素信息,比你的“描述”可靠。
你告诉AI“这里应该有一个登录按钮”,AI会猜 button.login-btn。但页面上真实的按钮可能是