1. 为什么我最终选了 Playwright codegen 加 Skills 这条路线
UI 自动化测试这件事,做过的人都懂,最磨人的不是写断言,而是那些重复到令人发指的定位和操作代码。一个中等规模的 Web 系统,登录、导航、表单填写、列表查询、弹窗确认,这些动作翻来覆去地写,一个页面对象文件动辄几百行,写完还要调试选择器,调完还要维护。前几年我试过纯手写、试过录制回放工具、也试过让大模型直接根据页面描述生成脚本,各有各的坑。直到我把 Playwright 的 codegen 录制能力和一套自己整理的 Skills 提示词体系结合起来,才算找到一条真正能落地的路子。
这套方案的核心逻辑其实一句话就能说清楚:人工用 codegen 录制关键操作路径,拿到一份“半成品”脚本,再用结构化的 Skills 提示词让 AI 补全断言、参数化、异常处理和页面对象封装。这样做的好处是,录制阶段由人来把控业务语义和操作顺序,AI 只负责它最擅长的代码补全和模式化改写,既避免了纯 AI 生成脚本时“一本正经胡说八道”的定位错误,又省掉了大量手写样板代码的时间。更重要的是,录制出来的脚本本身就是真实可运行的,AI 是在一个正确的地基上做增量,而不是从零开始猜。
我之所以强调“省 Token”,是因为如果你直接让大模型根据一段自然语言描述去生成完整的 Playwright 脚本,它需要理解页面结构、猜测选择器、推断等待策略,输出动辄上千行,Token 消耗非常可观,而且生成结果往往需要反复修正。而 codegen 录制出来的脚本,已经把最占篇幅的定位器和操作序列固化了,AI 只需要在关键位置做补充和重构,输入输出都短得多,成本自然就下来了。这套打法特别适合那些有一定自动化基础、但苦于脚本编写效率低的测试开发同学,也适合想引入 AI 辅助但又担心生成质量不可控的团队。
2. 整体设计思路与方案选型拆解
2.1 为什么不让 AI 从零生成脚本
我最早尝试的就是让大模型直接根据页面功能描述生成 Playwright 脚本。给一段“打开登录页,输入用户名密码,点击登录,验证跳转到首页”的描述,模型确实能吐出一份看起来像模像样的代码。但实际跑起来问题一大堆:选择器是它编的,#username这种 ID 在真实页面里可能根本不存在;等待策略是它猜的,waitForTimeout硬编码几秒,网络慢一点就挂;断言是它想的,验证文本和实际渲染对不上。每次都要人工逐行核对修改,改完发现还不如自己写。
根本原因在于,大模型没有“看见”真实页面,它只能根据训练数据里的常见模式去推测。UI 自动化的核心难点恰恰在于每个系统的 DOM 结构、组件库、渲染时机都不一样,这些信息不在模型的先验知识里。所以让 AI 从零生成,等于让它闭着眼睛画地图,出错是必然的。
2.2 codegen 录制解决了什么问题
Playwright 自带的codegen命令是我认为这个工具链里被低估的功能。执行playwright codegen <url>之后,会弹出一个浏览器窗口和一个代码生成面板,你在浏览器里的每一次点击、输入、导航,都会被实时翻译成 Playwright 的 API 调用。它生成的选择器是基于 Playwright 的定位引擎自动计算的,优先使用getByRole、getByLabel、getByText这些语义化定位器,比手写的 CSS 选择器稳定得多。
录制出来的脚本有几个特点:操作序列完全真实、定位器经过引擎验证、等待逻辑由 Playwright 的自动等待机制处理。这就相当于把“页面理解”这个最难的部分交给了工具本身,人只需要负责走对业务流程。我实测下来,一个包含登录加三个页面跳转的流程,录制加简单整理,五分钟就能拿到一份可运行的脚本,比手写快了三到五倍。
2.3 Skills 提示词体系扮演的角色
录制的脚本虽然能跑,但它是“线性流水账”,缺乏工程化结构。比如所有操作都堆在一个test块里,没有页面对象封装,没有参数化,没有断言,异常处理基本为零。这时候就需要 AI 介入做重构和补全,但关键在于怎么给 AI 下指令。
我整理了一套叫 Skills 的提示词模板,本质上是把“一个资深测试开发在重构脚本时会考虑的所有维度”结构化地告诉模型。这套模板包含几个固定模块:角色设定(你是一个 Playwright 测试专家)、输入说明(这是一份 codegen 录制的原始脚本)、重构要求(提取页面对象、参数化测试数据、补充断言、增加错误处理)、输出格式(分文件输出,标注每个文件的用途)。有了这套结构,AI 的输出质量比随便丢一句“帮我优化这段代码”要高出一个档次,而且每次输出风格一致,方便团队统一规范。
2.4 人机分工的边界在哪里
这套方案能成立的前提,是人和 AI 各司其职。人负责的是:确定测试场景和业务路径、在 codegen 里执行正确的操作、审核 AI 补全后的断言是否符合业务预期、决定哪些步骤需要参数化。AI 负责的是:把线性脚本拆成页面对象、生成参数化数据结构、补充常见的断言模板、添加 try-catch 和截图逻辑、统一代码风格。
这条边界很重要。我见过有人试图让 AI 连业务路径都自己决定,结果生成了一堆无意义的操作组合。也见过有人录制完之后完全不管,直接让 AI 输出,结果断言写错了业务含义也没发现。记住,AI 是加速器,不是决策者,业务正确性永远要人来把关。
3. 核心细节解析与实操要点
3.1 codegen 录制的正确姿势
很多人用 codegen 就是打开、随便点几下、复制代码,这样录出来的脚本质量很差。我总结了一套录制规范,能显著提升后续 AI 补全的效果。
首先是启动参数。我一般会用--target playwright-test指定输出为测试框架格式,而不是默认的库调用格式,这样录出来直接就是test('...', async ({ page }) => {...})的结构,省去后续转换。如果项目用 TypeScript,加上--lang ts。设备模拟可以用--device "iPhone 13",视口大小用--viewport-size "1280,720",这些参数在录制移动端或特定分辨率场景时很有用。
playwright codegen --target playwright-test --lang ts --viewport-size "1440,900" https://example.com其次是录制过程中的操作纪律。每完成一个逻辑上独立的业务动作,比如“登录成功进入首页”,就稍微停顿一下,在脑子里标记这是一个断点。输入文本时尽量用真实的测试数据,不要用aaa、123这种,因为后续 AI 参数化时会参考这些值来推断字段含义。遇到需要等待异步加载的地方,不要手动加waitForTimeout,让 Playwright 的自动等待去处理,录出来的脚本里如果出现了硬等待,说明操作太快了,重新录一遍。
注意:录制时如果页面有 iframe 或者 shadow DOM,codegen 有时会生成比较复杂的定位链,这时候不要急着删,先保留,等 AI 重构阶段再处理,因为原始定位信息对 AI 理解页面结构有帮助。
3.2 Skills 提示词模板的完整结构
这套模板我迭代了七八个版本,现在稳定下来的结构包含五个部分。第一部分是角色和任务定义,明确告诉模型它在做什么。第二部分是输入脚本的粘贴区,用分隔符标出来。第三部分是重构要求清单,这是核心,我列了十二条具体要求。第四部分是输出格式约定。第五部分是示例参考,给一两个输入输出的对照样例。
重构要求清单里,我重点强调这几条:把每个页面的操作提取成独立的 Page Object 类,类名用页面业务含义命名;测试数据抽取到单独的 fixtures 文件,用 TypeScript 接口定义类型;每个关键操作后补充expect断言,断言要基于可见的 UI 元素而不是内部状态;所有网络请求相关的操作加上waitForResponse或waitForLoadState;异常处理统一用 try-catch 包裹并在 catch 里截图;最后生成一个 README 说明文件结构。
输出格式我要求模型按文件路径分块输出,每个文件用代码块包裹并标注语言,文件之间用注释分隔。这样我复制出来直接就能按路径创建文件,不用再手动拆分。
3.3 定位器策略的取舍
codegen 默认生成的定位器优先级是:getByRole>getByLabel>getByPlaceholder>getByText>getByTestId> CSS 选择器。这个优先级是有道理的,语义化定位器对 UI 改动的容忍度更高。但实际项目里,很多老系统没有良好的 ARIA 标注,getByRole可能定位不到,codegen 就会退化成 CSS 选择器甚至 XPath。
我的处理原则是:如果录制出来是语义化定位器,保留不动;如果是 CSS 选择器,在 AI 重构阶段让它尝试替换为getByTestId,前提是项目里确实有>mkdir ui-auto-demo && cd ui-auto-demo pnpm init pnpm add -D @playwright/test pnpm exec playwright install chromium
安装完成后,项目根目录执行pnpm exec playwright codegen --help确认命令可用。如果团队需要跨浏览器测试,把chromium换成chromium firefox webkit,但首次安装会下载三个浏览器内核,体积较大,按需选择。
4.2 录制一个完整的登录加查询流程
假设我们要测试一个后台管理系统的登录和用户列表查询功能。启动 codegen:
pnpm exec playwright codegen --target playwright-test --lang ts --viewport-size "1440,900" https://demo.example.com/login浏览器打开后,依次执行:在用户名输入框输入testuser,在密码框输入Test@1234,点击登录按钮,等待跳转到首页,点击左侧菜单的“用户管理”,在搜索框输入张三,点击查询按钮,等待列表刷新。完成后关闭浏览器,codegen 面板里的代码会自动保存到你指定的文件,或者直接复制出来。
录出来的原始脚本大概长这样:
import { test, expect } from '@playwright/test'; test('test', async ({ page }) => { await page.goto('https://demo.example.com/login'); await page.getByLabel('用户名').fill('testuser'); await page.getByLabel('密码').fill('Test@1234'); await page.getByRole('button', { name: '登录' }).click(); await page.getByRole('link', { name: '用户管理' }).click(); await page.getByPlaceholder('请输入姓名').fill('张三'); await page.getByRole('button', { name: '查询' }).click(); });这份脚本能跑,但没有断言、没有参数化、没有页面对象,直接放进项目里维护性很差。接下来就是 AI 补全环节。
4.3 用 Skills 提示词驱动 AI 重构
把上面这段脚本粘贴到 Skills 模板的输入区,然后发送给模型。我在模板里会明确要求输出以下文件结构:
pages/LoginPage.ts pages/UserManagePage.ts fixtures/testData.ts tests/login-and-search.spec.ts模型返回的结果里,LoginPage.ts会把登录相关的定位器和操作封装成类方法,UserManagePage.ts封装用户管理页的操作,testData.ts定义测试数据的接口和默认值,测试文件则调用这些封装并补充断言。
我拿到 AI 输出后,重点检查三件事:断言是否符合业务预期(比如登录后应该验证首页的欢迎语而不是随便一个元素)、参数化是否合理(测试数据是否抽到了 fixtures)、定位器是否被错误替换(AI 有时会把能跑的 CSS 选择器改成定位不到的语义化定位器)。检查完做少量手动修正,一份工程化的测试脚本就成型了。
4.4 断言补充的关键位置
AI 补全断言时,我会在 Skills 模板里指定几个必须加断言的位置:页面跳转后验证 URL 或关键元素、表单提交后验证成功提示或数据变化、列表查询后验证结果数量或首行内容。这样模型不会漏掉关键验证点。
以登录为例,AI 生成的断言通常是await expect(page).toHaveURL(/dashboard/)加上await expect(page.getByText('欢迎回来')).toBeVisible()。这两个断言一个验证路由跳转,一个验证页面渲染,组合起来比较可靠。查询场景的断言则是await expect(page.getByRole('row')).toHaveCount(1)或者验证首行单元格文本包含“张三”。
提示:断言不要写得太死,比如验证具体的时间戳或随机 ID,这种断言维护成本极高。优先验证稳定的业务语义,比如状态文本、数量范围、元素可见性。
4.5 异常处理与截图机制
原始录制脚本没有任何异常处理,一旦某一步失败,测试直接中断,排查时只能看日志。我在 Skills 模板里要求 AI 在每个测试用例外层加 try-catch,catch 块里执行await page.screenshot({ path: 'screenshots/xxx.png', fullPage: true })并重新抛出错误。
这样测试失败时,截图会自动保存到指定目录,配合 Playwright 自带的 trace 功能,排查效率提升非常明显。trace 的开启方式是在playwright.config.ts里配置use: { trace: 'on-first-retry' },失败重试时自动记录完整操作轨迹,可以用playwright show-trace命令回放。
5. 常见问题与排查技巧实录
5.1 codegen 录制时定位器生成不稳定怎么办
这是最常见的问题。同一个按钮,有时录出来是getByRole('button', { name: '提交' }),有时变成locator('div > button:nth-child(3)')。原因通常是页面渲染时机不同,codegen 在元素还没完全挂载时抓取,就只能退化成结构选择器。
解决办法有两个:一是录制时放慢操作节奏,等页面稳定后再点击;二是在录制前先手动在浏览器控制台确认目标元素有没有稳定的>