如果你刚接触 Playwright,大概率会被两类问题卡住:第一类是怎么写出稳定好用的选择器,第二类是怎么让脚本在复杂页面上持续跑通。这两类痛点,脚本录制(codegen)恰好都能帮你快速跨过去。我自己在某后台管理系统补自动化覆盖时,第一个动作永远不是手写用例,而是先开录制工具把核心流程走一遍,生成一批可运行的基线脚本,再逐步精修。这篇实战指南会从录制工具的安装启动讲起,拆解录制生成代码的原理和选择器策略,并重点讲录制代码怎么改造成能上生产的工程化用例,最后附上我在实际使用中遇到过的典型问题和排查套路。
1. 为什么从脚本录制开始:给自动化测试减负的第一课
很多人觉得录制生成的代码“不优雅”,甚至有些团队直接禁用录制。但我个人的看法是:录制不是偷懒的借口,而是一个极高效的建模工具。自动化测试的本质是把用户操作抽象成可重复执行的步骤,这个建模过程才是核心,手写代码反而是次要的。录制工具把“人工操作转成代码”的时间压缩到接近零,让你把精力花在“哪些步骤需要断言、哪些元素容易变化、哪些流程值得做成公共方法”这些更有价值的思考上。
1.1 脚本录制解决的核心痛点
行业内刚刚开始做自动化的人,多数时间不是花在写用例上,而是花在找选择器上。打开开发者工具、定位元素、试各种定位语法、跑脚本报错、再回去改……一套流程下来半小时就没了。更难受的是,有些元素是动态渲染的,很多新手根本不知道用role还是text去定位。录制工具把这一步完全接管了。
另一类痛点是“脚本写了但跑不起来”。这通常是因为元素加载时机不对、点击被遮挡、iframe 里内容取不到。Playwright 录制生成的代码,天然会使用框架的自动等待机制,比如click之前会等待元素可操作,这就在起点上帮你规避了很大一部分稳定性问题。所以我一直建议新人:不要一开始就啃各种定位高级技巧,先录一条完整流程,再对照生成的代码理解 Playwright 是怎么工作的。这比空看文档高效得多。
1.2 谁适合用脚本录制这个功能
如果你是完全没写过自动化测试的新人,录制可以让你跳过选择器语法和等待策略的门槛,先获得“脚本能跑起来”的正反馈,这对建立信心特别重要。如果你是有经验的老手,录制适合做需求验证、POC 和探索式测试——新功能评审完,开个 codegen 把主流程走一遍,比写文档描述步骤直观得多,还能直接沉淀成测试资产。如果你在带团队搭建自动化体系,录制更是初期建立用例库的最佳手段,可以让不太懂代码的测试同学也参与录制工作,再由开发或自动化工程师统一做工程化改造,职责分工很清晰。
2. 录制环境搭建:两条命令跑起第一个能用的浏览器
Playwright 的录制功能内置于 CLI 工具中,不需要单独装插件。只要你的机器上有 Node.js,就能在几分钟内跑起来。
2.1 安装与基础环境准备
先创建一个空目录,比如就叫pw-demo,然后执行:
npm init -y npm i -D @playwright/test npx playwright install chromium这里我推荐装@playwright/test而不是裸的playwright包,因为前者自带测试运行器、断言库,以及我们马上要用的 codegen 命令。习惯用其他语言的也不用担心,Python 版是pip install playwright,Java 和 C# 也有对应实现,下面命令行操作换成各自语言的等价命令即可。
npx playwright install chromium这一步会把浏览器内核下载到本地缓存目录。如果是在 Linux 服务器上跑,经常会遇到系统缺少动态库的情况,这时候可以把命令改成npx playwright install --with-deps chromium,让工具尝试自动安装系统依赖。macOS 用户一般不需要额外操作,但首次运行如果系统弹安全提示,记得在“系统偏好设置-隐私与安全性”里允许对应的浏览器进程。
2.2 使用 codegen 启动录制
安装成功之后,最核心的命令就是:
npx playwright codegen https://example.com这条命令会做两件事:打开一个实时代码生成面板,同时启动一个受 Playwright 控制的浏览器窗口。你在浏览器里的每一次点击、输入、滚动,都会实时转换成代码显示在左侧面板里。这个模式对快速拿到一条可执行脚本来说,效率非常高。
如果还没想好要测试哪个网址,也可以先不带 URL 直接运行npx playwright codegen,等浏览器窗口打开后,你自己在地址栏里输入目标地址。这样录制起点更灵活,适合需要先登录跳转再操作的场景。
2.3 录制工具栏与常用参数
codegen 窗口上方有一排工具栏,里面几个功能非常重要。
- Pause / Resume:暂停录制和继续录制。当你需要处理验证码、拖动滑块这类录制效果不好的操作时,可以先暂停录制,手动处理完再继续。
- Target:选择要生成代码的目标。默认是当前页面,也可以选择新页面、新建浏览器上下文。多标签页测试会用到。
- Assert:断言模式。选中页面上的某个元素,点击 Assert 之后,代码面板会自动生成类似
expect(locator).toBeVisible()的断言。这是录制流程里最容易忽略但价值最高的功能。 - Clear:清空当前已生成的代码。流程录乱了或者想换个思路时很方便。
除了这些交互按钮,codegen 也支持命令行参数来控制浏览器环境,比如模拟移动端设备:
npx playwright codegen --device="iPhone 13" https://example.com还有指定视口尺寸、保存登录状态等参数。我常用的组合是:
npx playwright codegen --save-storage=auth.json https://example.com先把登录态保存下来,之后写脚本时用--load-storage=auth.json就能跳过重复登录,这个技巧在对多系统做回归测试时特别好用。
3. 实战录制:从登录页到首条冒烟用例的完整流程
光说不练没用,这一节我会完整走一遍登录+列表页查询的录制流程,并拆解录制过程中 Playwright 到底生成了什么、为什么这么生成。
3.1 一个真实的录制过程拆解
启动并进入某测试站点后,假设目标是录制“登录-跳转-输入关键词-查询-断言结果可见”这条流程。我的操作节奏是:
- 先不急着点,等页面元素渲染完。
- 点击用户名输入框,输入一个测试账号。
- Tab 切到密码框,输入密码。
- 点击登录按钮。
- 页面跳转到首页后,点击左侧菜单进入查询页面。
- 在输入框里输入“订单号”这类关键词。
- 点击搜索按钮。
- 等结果列表出现,找一个关键元素,点击工具栏里的 Assert,生成可见性断言。
对应的代码面板会自动生成类似这样的脚本:
import { test, expect } from '@playwright/test'; test('test', async ({ page }) => { await page.goto('https://example.com/login'); await page.getByLabel('用户名').fill('demo_user'); await page.getByLabel('密码').fill('123456'); await page.getByRole('button', { name: '登录' }).click(); await page.getByRole('link', { name: '订单查询' }).click(); await page.getByPlaceholder('请输入订单号').fill('PO20240001'); await page.getByRole('button', { name: '搜索' }).click(); await expect(page.getByText('搜索结果:共 3 条')).toBeVisible(); });注意看,录制工具生成的定位器语义化很强,getByLabel、getByRole、getByPlaceholder这些方式都比 CSS 选择器好读,而且对页面结构调整的容忍度更高。这就是录制工具第一个价值:它不仅生成“能跑的代码”,还顺手帮你选择了较优的定位策略。
3.2 录制断言:不只生成 toBeVisible
Assert 这个按钮是很多人最初会忽略的,但它恰恰是录制流程里最值得花时间的环节。默认点击 Assert 后生成的是toBeVisible(),但实际场景里往往还需要更精准的断言。
举个例子,登录后想校验用户名称是否正确,可以把断言生成在用户名元素上,但toBeVisible()只能说明元素在页面上,不能验证文本内容。这时候我们会在生成的代码基础上微调,把断言改成:
await expect(page.getByRole('heading', { name: '欢迎回来,管理员' })).toHaveText('欢迎回来,管理员');或者对输入框的值做校验:
await expect(page.getByLabel('用户名')).toHaveValue('demo_user');录制只是抛砖引玉,最终断言深度取决于你对业务的理解。我的经验是:录制过程中每完成一个关键操作,就停下来问自己“这一步如果失败,用户看到的界面应该是什么样子”,然后针对那个“界面变化”添加断言。说得直白点,断言的本质是“可验证的业务结果”,不是“元素存在”。
3.3 录制时选择器是怎么选出来的
很多人会有疑问:录制工具为什么有时候生成getByRole,有时候生成getByText,还有时候生成getByTestId?Playwright codegen 内部有一套选择器优选逻辑,会优先选择用户可感知的语义信息,比如表单的 label、按钮的角色名称、文本内容。当这些信息不足时,才会退回到 CSS 或 XPath。
这个特性其实反映了 Playwright 推荐的最佳实践:尽量按用户与页面的交互方式来定位元素,而不是按 DOM 结构。所以录制时如果能给按钮写清楚可访问名称,给输入框配好 label,生成出来的代码质量会明显更好。反过来说,如果页面上很多元素没有语义信息,录出来的代码就可能出现一堆locator('div:nth-child(3) > span')这种脆弱的定位。遇到这种情况,我建议不要继续录了,先让前端开发在元素上补>import { defineConfig } from '@playwright/test'; export default defineConfig({ testDir: './tests', timeout: 30_000, retries: 1, use: { baseURL: process.env.BASE_URL || 'https://staging.example.com', trace: 'retain-on-failure', screenshot: 'only-on-failure', }, });
这样脚本里的page.goto('/login')会自动拼接 baseURL,环境切换只需要改环境变量,脚本本身不用动。retries和trace的配置对排查偶发失败非常有帮助,后面讲问题排查时还会提到。
第二步是把录制的单段逻辑封装成可复用方法。这其实就是 Page Object 模式,不必追求教科书式的复杂抽象,哪怕只做一层函数封装也是进步:
export async function login(page: Page, username: string, password: string) { await page.goto('/login'); await page.getByLabel('用户名').fill(username); await page.getByLabel('密码').fill(password); await page.getByRole('button', { name: '登录' }).click(); }这样 20 条用例需要登录时,只调用这个函数就够了,而不是复制粘贴 20 段登录代码。凡是在多条用例里重复出现超过两次的操作,都值得这样抽出来。
4.3 数据驱动:让一批用例跑不同数据
录制产生的脚本通常是单条数据流,但回归测试经常需要对多组数据验证。最简单的数据驱动方式是直接读外部文件:
import testData from '../data/orders.json'; for (const item of testData.orders) { test(`查询订单 ${item.orderId}`, async ({ page }) => { await page.goto('/order'); await page.getByPlaceholder('请输入订单号').fill(item.orderId); await page.getByRole('button', { name: '搜索' }).click(); await expect(page.getByText(`订单状态:${item.expectedStatus}`)).toBeVisible(); }); }用循环生成一组用例,失败时能精确定位到具体数据,报告也更直观。这是把录制脚本从“一次性脚本”推向“可维护测试资产”的关键一步。
4.4 登录态复用与多账号场景
前面提到--save-storage可以保存登录态,工程化阶段更好的做法是在 fixture 里配置。把登录后的storageState保存为auth.json,然后在配置文件里给某个 project 指定:
projects: [ { name: 'logged-in', use: { storageState: 'auth.json' }, }, ],这样属于这个 project 的所有用例都不需要重复登录了,执行速度会快不少。多账号场景则建议用 fixture 做用户切换,而不是在用例里去填账号密码。
5. 常见问题与排查套路:录制流程中踩过的坑
录制工具解决了许多问题,但也不是银弹。这节把我实际使用中遇到过的高频问题整理成一张速查表,每个问题都附上排查思路和推荐解法。
5.1 定位到多个元素报 Strict Mode Violation
录制生成的定位器有时会匹配到多个元素,运行时直接报错。常见原因是页面里文案重复,比如多个区域都有“确定”按钮,getByRole('button', { name: '确定' })就会撞车。
排查思路是先用locator.count()或者运行时的报错信息确认匹配数量,然后在定位器后面追加限定条件,比如.filter({ hasText: '保存订单' }),或者把定位范围缩进某个容器:
const dialog = page.locator('.ant-modal'); await dialog.getByRole('button', { name: '确定' }).click();这个“先容器后元素”的思路在场景里很通用,比硬记nth(0)更稳,因为nth(0)对 DOM 顺序极其敏感,页面一调整就失效。
5.2 元素明明在页面上但点击不到
这个问题很多是遮罩层或元素不在视口内导致的。某些前端框架的弹窗动画还没结束就把整个页面盖了一层透明遮罩,Playwright 判断元素“可操作”失败。
解法是让断言来兜底,而不是加固定 sleep。比如点击前先等待按钮可见:
await expect(page.getByRole('button', { name: '提交' })).toBeEnabled(); await page.getByRole('button', { name: '提交' }).click();如果问题是元素在页面底部需要滚动才能看到,可以调用locator.scrollIntoViewIfNeeded()。录制过程中如果发现工具自动加了这段代码,别删。
5.3 iframe 里的元素定位不到
录制时能点到 iframe 内元素,但回放开脚本时却定位不到,多半是因为 iframe 内容加载是异步的,或者页面里同一时刻存在多个同源 iframe。Playwright 推荐使用frameLocator:
const frame = page.frameLocator('iframe[name="report"]'); await frame.getByRole('button', { name: '导出' }).click();如果你的 iframe 没有稳定的 name,可以考虑用page.frames()动态获取之后,再对frame.page()做后续操作。我个人更推荐前者,代码可读性更好。
5.4 脚本偶发失败:时好时坏到底怎么查
偶发失败是自动化维护里最磨人的问题。一份脚本昨天全部通过,今天一条挂了,重跑又好了。遇到这种情况,先别急着改代码,按这三个方向排查:
- 看trace 文件。配置文件里已经开了
trace: 'retain-on-failure',运行后会在测试结果目录生成 zip 文件,用npx playwright show-trace打开,能看到每一步的前后快照、网络请求和浏览器控制台日志,基本一目了然。 - 看请求时间线。如果元素出现很慢,多半是接口响应超时,最好在页面里等待对应接口返回,而不是等元素出现。
- 看数据依赖。如果查询结果受数据库数据影响,优先考虑每次执行前用接口造数据,保证环境稳定性。
5.5 登录状态失效导致用例连环挂
这是一个团队协作时的典型场景:用例本身没问题,但登录态过期了,几十条用例在同一处集体失败。解法分两层:第一层是保证每个用例的执行顺序独立,不要把“登录”作为某一条用例而设另一个用例依赖它;第二层是使用storageState。测试执行前如果发现登录态失效,可以用一条单独的 setup 用例刷新auth.json,或者在 CI 任务里先跑登录脚本再跑回归用例。
表格汇总如下:
| 现象 | 常见原因 | 排查手段 | 推荐解法 |
|---|---|---|---|
| 定位报多个元素 | 页面文案重复 | 打印 locator.count() | filter + 容器定位 |
| 点击不生效 | 遮盖层、未滚动 | 查看 trace 快照 | toBeEnabled + scrollIntoViewIfNeeded |
| iframe 内无法定位 | 异步加载、无稳定名 | 检查 iframe 名称与加载状态 | frameLocator |
| 偶发失败 | 网络波动、懒加载 | show-trace 查看时间线 | 接口等待 + 重试配置 |
| 登录态失效 | 会话过期 | 查看失败前序用例 | storageState + 前置 setup |
6. 把录制当成方法而非工具:进阶提升的经验心得
聊到这里,录制本身的功能边界已经讲得差不多了,但我更想说的是方法论层面的东西。录制工具可以被替代,但“快速建模、逐步精化、工程化收敛”这套思路,是自动化测试长期稳定运转的核心。
6.1 录制与手工代码混合的最佳姿势
我的做法可以用一个比例来描述:录制完成 70%-80% 的骨架,手写补全剩下的边界处理。比如录制能生成“点击上传按钮→选择文件→确认上传”,但上传成功后的文件大小校验、重复上传的异常提示,就需要手写补全。
另一种混合方式是录制时先暂停,手动处理无法生成代码的场景,再继续录制。比如短信验证码,录制没法替你收短信,你就在这一步暂停,手动输入后 resume,生成的代码里会保留你暂停之前的上下文,后续操作依然能衔接上。