← 所有文章
·playwrightmaintenanceselectors

如何让由 AI 编写的 Playwright 测试在你的 UI 变化时不崩

你上线了一处 UI 改动。下一次 CI 跑下来,十几个 spec 一片红。它们一个 bug 都没抓到,抓到的只是一个被改了名的 class。于是你一下午都在修测试,而不是写正经代码。

每个端到端套件都躲不开这一关,只是每次付出的代价不一样。有些工具在回路里常驻一个模型,在测试运行时实时重新解析 selector,于是每跑一次 CI,你都得为推理买单。下面这几条规则让你的测试始终保持为纯 Playwright,把这笔成本集中到真正有改动的那一刻。

1. 用语义化 locator,绝不用 CSS 或 XPath

这一条规则顶得上其他四条加起来还多。像这样写的测试:

await page.locator('.btn-primary.css-1x9f2 > span').click();

设计师一改 class 的名字,它立刻就废了。而像这样写的测试:

await page.getByRole('button', { name: 'Sign in' }).click();

却能扛过大多数改样式。"标着 Sign in 的那个按钮"是你对用户许下的承诺,而不是明天说不定就重构掉的实现细节。

Hover 默认只写 getByRolegetByLabelgetByTextgetByTestId,绝不用 CSS 或 XPath。你手写的测试也照这个来。就这一个习惯,决定了你的套件是能容忍 UI 变动,还是天天跟它对着干。

2. 加一段可见性序幕,让"被挪进菜单后面"快速失败

有个按钮还在 DOM 里,只是现在被塞到了 kebab 菜单后面——这是个无声的陷阱。Playwright 的 .click() 会自动等待元素变得可操作,所以它要等到 30 秒超时才失败,报错看上去就像是基础设施在抽风。

三行代码,就能把它变成一次干脆、清晰的失败:

const el = page.getByRole('button', { name: 'Submit' });
await expect(el).toBeVisible();
await el.click();

这样一来,要是那个按钮飘进了收起的抽屉里,toBeVisible() 大约五秒就会失败,报出 "Locator expected to be visible."。你一看就知道这是 UI 回归,而不是网络抖动。在 clickdblclickhoverfillselectOption 上,Hover 都会替你自动加上这段序幕。

3. 语义变了就有意地重新录制

有人把 "Sign in" 改成了 "Log in"。selector 坏了——本来就该坏。这个测试断言的那份契约已经变了,套件其实运作得好好的。

修法有两种。手动改文件——反正是纯 Playwright,一行就搞定。或者在 Hover 扩展里点 ⟳ 重新录制:agent 会读取这条测试当初的 prompt,对着当前的应用重跑一遍,再重写文件。前后大约 30 秒、花掉大约一毛钱,提交前你还能过一遍 git diff。只有当你判断确实有东西变了才这么做,而不是把它当成每次跑绿都自动触发的模型调用。

4. 只重新录制变红的,而不是全部

你八成会想要一个"全部重新录制"的按钮。忍住。去重录那些已经通过的 spec,等于为本来好好的测试白烧 token,还会把一堆没人动过的文件里纯属外观变化的 selector 改动一股脑塞进你的 diff。让 CI 告诉你哪些 spec 红了,就重录哪些,一次一个,每份 diff 都看过去。这样是慢上几分钟,但留下的是一段你能 bisect 的历史。

5. 让真正的流程断裂失败

不是每个红掉的测试都是等你去清理的杂活。如果一个测试失败,是因为某个原本走得通的步骤现在走不通了,那它就是抓到了一次回归。这时该去修的是应用。一套在真正断裂时还照样发绿的套件,对你毫无价值。

你得到的

你的套件始终是纯 Playwright:确定、快、跑起来不花钱、在 pull request 里读得懂、在任何笔记本上都能跑而不需要任何模型在场。维护的钱,你只在契约改变的那一刻花,花在一份你亲自审过的 diff 上,而不是变成一笔常年压在本就发绿的构建上的税。

看看 Hover 如何处理一个坏掉的 spec →

在你自己的应用上试试 Hover。

把 Hover 的 MCP 加到你已经在用的编码 agent。它探索你的应用,结晶出你自己拥有的纯 Playwright spec。

npm i -g @hover-dev/mcp && claude mcp add hover -- hover-mcp阅读快速开始 →