玩转 Playwright:从 10 行脚本到一套不会半夜变红的端到端测试
玩转 Playwright:从 10 行脚本到一套不会半夜变红的端到端测试
我之前写过一篇 Playwright 自动化测试框架详解:从概念到实践,那篇回答的是”它是什么、支持哪些浏览器”,属于概念梳理。这篇不一样:**它回答”怎么玩”**——从一条十行的脚本,到一套能在 CI 上长期跑、失败了还能说清楚原因自己的测试。
版本基准:本文按 Playwright 1.64(2026-10 的最新线)撰写。文中涉及具体版本的能力都会标出来,方便你对照自己的版本。
一、是什么:一个”用代码开真实浏览器”的运行时
很多人以为 Playwright 就是”另一个 Selenium”,其实它的定位更宽:它是浏览器自动化运行时,测试只是它最重要的一种用法。
这一点从包结构就能看出来,它其实是两条线:
| 线 | 包 | 什么时候用 |
|---|---|---|
| 库 | playwright |
一次性脚本:抓数据、批量截图、给内部系统做自动化、当”网页版 cron” |
| 测试运行器 | @playwright/test |
要长期跑的回归:用例发现、并行、重试、断言、报告、trace 全给你管好了 |
@playwright/test 是跑在库上面的那层壳。所以先学库的 API,再用测试运行器,路径会顺得多;反过来只会背脚手架,遇到点非常规需求就没底。
对象模型:四层,记住就够用
1 | Browser(一个浏览器进程) |
Context 是 Playwright 最关键的设计。 在 Selenium 里,想要两份干净的会话,你得再造一个 driver、重启一个 profile,代价是秒级起步;Playwright 里 browser.newContext() 是毫秒级的,于是”每条用例一个全新浏览器状态”这种奢侈的隔离,变成了默认行为,而不是需要心疼资源的选项。并行、登录态复用、多用户交互测试,全都建立在这一点上。
Locator 是第二关键的设计。 它不是元素,是”如何找到元素的说明”。page.getByRole('button', { name: '保存' }) 返回的东西此刻在页面上根本不存在,直到你调用 .click() 它才去找——而且会一直等到能找到为止。这就是自动等待的来源。
和 Selenium / Puppeteer / Cypress 的差别在哪
不扯”快百分之多少”这种没出处的数字,机制上的差异才有决策价值:
- 对 Selenium:Playwright 不走 WebDriver 那条”HTTP 命令一问一答”的通道,而是用浏览器自身的调试协议长连接驱动,事件能主动推回来。带来的实际差别是自动等待、网络层拦截、以及多上下文并行——这三件事在 Selenium 里都要你自己造轮子。
- 对 Puppeteer:API 手感很像(Puppeteer 的不少设计后来被两边互相借鉴),但 Playwright 跨引擎(Chromium / Firefox / WebKit 都是官方维护),Puppeteer 基本绑 Chromium 生态。只做 Chrome 相关的事,Puppeteer 够用;要跨浏览器,Playwright。
- 对 Cypress:Cypress 跑在浏览器 inside,天然拿不到浏览器进程外的能力,所以多标签、多域、真实系统浏览器切换这些是它的硬边界;Playwright 在浏览器外面驱动,这些都不是问题。反过来 Cypress 的调试体验对新手更友好一些。
二、为什么:现在学它,值在哪
1. 它把 flaky 当成设计目标来消灭
端到端测试最大的成本不是写,是它今天红了你不知道是真 bug 还是运气差。Playwright 的三件套正面解决这个:
- 操作前自动等待元素可见、稳定、可点、可输入;
- 断言自带轮询重试(
toBeVisible()不是”看一眼就下结论”,而是”在超时内等到它成立”); - 用例失败可自动重试(
retries),重试时保留 trace。
2. 隔离即并行,不用改代码
每条用例一个 Context,Context 之间互不可见,于是 fullyParallel: true 打开就能吃满 CI 机器的核。这在 Selenium 体系里通常意味着重构整个测试基类。
3. 失败之后有”飞行记录仪”
trace 里有一次运行的每一步动作、动作前后的 DOM 快照、console 输出、网络请求。1.60 之后 HAR 网络捕获被折叠进了 tracing,一份 trace 文件同时是行为记录和流量记录;同期加了结构化的 errorContext,失败信息不再只是一坨给人看的字符串——这也为后面接 AI 排查留了口子。
4. 它的生态位正在变大
这条是”为什么是现在”的答案。把近一年的发布轨迹连起来看:
| 版本 | 时间 | 关键动作 |
|---|---|---|
| 1.56 | 2026 上半年 | 引入 Test Agents:planner / generator / healer |
| 1.59 | 2026 | browser.bind(),多个消费方可以挂到同一个浏览器实例上 |
| 1.60 | 2026-05 | Test Agents 转生产可用;ARIA 快照支持携带元素坐标(bounding boxes);HAR 进 trace;errorContext |
| 1.62 | 2026 | 组件测试换成 stories / galleries 模型;老的 React、Vue 组件测试包停止更新 |
| 1.63 | 2026-09 | 测试锁(协调争抢共享资源的用例)、locator.visible()、跨 frame 免选择器定位 |
| 1.64 | 2026-10 | WebMCP 支持、Locator.within()、--shuffle |
它在从”测试工程师写代码的框架”变成”AI Agent 操作浏览器的那一层运行时”:Agent 不靠截图猜坐标,而是读 ARIA 语义树(省一个数量级的 token,且定位是确定性的);MCP 服务器和 CLI + Skills 两条通道都在官方维护。哪怕你不做测试,把它当浏览器自动化底座用,也是押在一条正在变宽的路上。
三、怎么样:十分钟从零跑通
3.0 装
1 | # 只要库(写脚本用) |
Windows 上直接跑就行;公司网有代理的话,下载浏览器会失败,设 HTTPS_PROXY,或者用 PLAYWRIGHT_BROWSERS_PATH 把浏览器缓存指到可写目录。Linux CI 上别忘了 --with-deps,否则缺字体和图形库会以非常难懂的报错表现给你。
不想下载它自带的浏览器也可以——直接用你机器上已经装好的 Chrome:
1 | const { chromium } = require('playwright') |
channel: 'chrome' 或 'msedge' 走系统浏览器,省 100 多兆下载,也能验到”真实用户机器上的那个浏览器”。
3.1 线一:一条脚本把事办了
下面这个例子不依赖任何外网页面,自己塞一段 HTML 进去就能跑,适合当第一个可复现的最小样本:
1 | // demo.js —— node demo.js |
三个手感要点:
- 不写
sleep,不写waitForTimeout。 需要等的时候,等的是”条件成立”,不是”过了三秒”。 - 定位优先用语义(
getByRole/getByLabel/getByText),CSS 选择器是兜底而不是默认。 browser.close()一定要在 finally 里,否则脚本崩一次就留一个僵尸浏览器进程。生产脚本请写成try { … } finally { await browser.close() }。
顺手抓数据、批量出图,也还是这一套:
1 | const rows = await page.$$eval('table.list tr', (trs) => |
3.2 让 codegen 替你写定位器
不知道某个元素怎么定位的时候,别去猜 class:
1 | npx playwright codegen https://example.com |
它会开一个浏览器 + 一个 Inspector,你点点看,代码实时生成。想直接生成测试运行器风格的代码:
1 | npx playwright codegen --target=playwright-test https://example.com |
codegen 是”起手式”,不是”工程实践”——它生成的选择器往往很脆(一改版就挂),录完记得手工换成语义定位。
3.3 线二:第一条真正的测试
1 | npm init playwright@latest # 交互式:选语言、选测试目录、是否建 GitHub Actions |
它会生成一份 playwright.config.js 和 tests/ 目录。核心配置长这样:
1 | // playwright.config.js |
写用例:
1 | // tests/add-item.spec.js |
跑:
1 | npx playwright test # 全跑 |
--ui 是我认为它相对所有框架最”回不去”的功能:测试树、实时运行、时间旅行式的每一步快照,改完自动重跑。写测试的体感差距是数量级的。
四、核心特性:真正会用到这八个
1. 定位器:语义优先,链式收窄
1 | page.getByRole('button', { name: /保存|提交/ }) // 正则匹配可访问名 |
Playwright 默认是 strict mode:一个定位器匹配到多个元素就报错,不会”帮你选第一个”。很多人第一次被这个坑到,但它是好事——它把”我以为只点到了这一个”的隐患在执行时就拦住了。
1.63 起的跨 frame 定位也简化了:page.frameLocator() 不传选择器时,会直接遍历所有子 frame 去找元素(多个 frame 里都有同名元素时会明确报歧义,不会随便挑一个)。
2. 自动等待 + web-first 断言
click() 会等元素可见、稳定、启用、可接收事件。断言写 locator,不要先取值再断言:
1 | // ❌ 只检查一次,页面还没渲染完就挂了 |
断言不满足又不想整个用例失败时,用 expect.soft;要等一段非 DOM 的状态,用 expect.poll 或 toPass:
1 | await expect |
3. 网络层:拦截、伪造、断网
这是 Playwright 相对同类最锋利的一层,page.route() 能改请求也能造响应:
1 | // 前端还没写完,先喂一份假数据把 UI 跑起来 |
顺带一个省钱技巧:测试里把图片、字体、埋点全拦掉,跑分快一截。
1 | await page.route(/.(png|jpe?g|webp|woff2?|gif|svg)/, (r) => r.abort()) |
4. 登录态复用:storageState
端到端测试最烦的是每条用例都要重登一遍(还可能踩验证码)。Playwright 的解法是把 cookie + localStorage 存成一个 JSON,之后所有用例直接带着它起跑:
1 | // 1) 人工或脚本登录一次,存下来 |
1 | // 2) 之后所有用例免登录 |
测试运行器里更简单,config 的 use.storageState 全局设一下,或者用 globalSetup 生成它(见进阶第 2 条)。命令行也支持:npx playwright codegen --load-storage=auth.json https://example.com。
5. Context 级模拟:设备、权限、地理位置、语言时区
1 | const { devices } = require('@playwright/test') |
注意这些是 Context 级的,不是浏览器级——这正是”同一份代码,同时验桌面 Chrome 和手机 Safari 行为”能并行的原因。
6. 新标签、iframe、弹窗、文件上传下载
1 | // 新标签页 |
7. 多浏览器 + 多环境矩阵
projects 是它的矩阵机制:一个 project = 一组 use 配置。浏览器 × 视口 × 环境(本地/预发)都能这么表达,跑的时候 --project 精确选,CI 里拆成不同 job。
8. 调试三件套:UI Mode / Trace / Inspector
1 | npx playwright test --ui |
trace 打开后是”每一步 → 当时的 DOM 快照 → 当时的网络请求”的时间轴,可以逐步回放。CI 里请把 trace 归档,配置里 trace: 'on-first-retry' 是性价比最高的档位(每条都存太占地方,只在重试时存刚好覆盖 flaky 场景)。
五、进阶玩法
1. 把”登录”和”造数据”沉到 fixture 里
用例只该讲业务,不该讲准备动作。fixture 就是干这个的:
1 | // fixtures.js |
1 | // tests/x.spec.js —— 用 test 而不是内置 test,就自动获得上面的能力 |
再往前一步是 Page Object:把一页的定位器和动作封成一个类,用例读起来像验收标准。小项目不必强上,用例超过几十条以后收益才压过抽象成本。
2. globalSetup 一次登录,webServer 自动起应用
globalSetup 在整个套件开始前跑一次,天生适合”登录 + 存 storageState + 准备测试账号”:
1 | // global-setup.js |
配合 config 里的 webServer(上面 3.3 有例子),npx playwright test 一条命令会自己把开发服务器拉起来、等端口就绪、跑完再收——本地和 CI 用同一份配置,不用再写两套启动脚本。
3. 并行调优,以及 1.63 的测试锁
并行是端到端最容易翻车的地方,因为共享资源:多个 worker 同时改同一个账号的设置、同时操作同一个外部服务,单跑全过、并行必炸。
常规手段是数据隔离(每条用例自己造数据、自己清理,名字带时间戳),实在隔离不了的才排队:
workers/fullyParallel控总量;test.describe.configure({ mode: 'serial' })让一组用例串行;--shard=2/4把套件切片分到多台 CI 机器;- 1.64 提供
--shuffle打乱顺序,专门用来暴露”依赖执行顺序”的假稳定用例; - 1.63 的测试锁:给争抢同一资源的用例声明同名 lock,同名锁的用例跨文件、跨 worker、跨 project 都会串行排队,而不同名或没加锁的用例照常并行。这比”为了安全把整套件改成单 worker”精细得多。
两个提醒:锁名要在全项目内统一规范,只有标识完全相同的才会互相排队;锁不能替代数据隔离,也不会帮你清理外部服务的残留状态。升级时先给”共享账号 / 全局配置 / 单实例独占”这类用例加锁,观察时长和失败率变化,别一上来给全量套同一个锁。
4. 视觉回归:toHaveScreenshot
1 | test('首页视觉不跑偏', async ({ page }) => { |
基线用 --update-snapshots 生成。视觉回归的坑基本都不在 Playwright,在环境:字体渲染、时间戳、随机内容、动画、DPR。所以要么在 CI 里跑(环境统一),要么把这些内容用网络拦截固定掉。跨浏览器做视觉对比要谨慎——不同引擎的默认字体本来就不一样。
5. 往报告里塞自定义证据
1 | test.info().annotations.push({ type: 'issue', description: 'HOME-42' }) |
排查的人会很感谢你——比在报告里贴一屏 console 输出有用得多。
6. 接口测试和 UI 混着写
request fixture 可以不启浏览器直接打接口。正确用法是:用接口造前置数据、验后端结果,用 UI 只验用户能看见的那一段。
1 | test('删除事件后统计跟着变小', async ({ request, page }) => { |
全是 UI 操作的套件又慢又脆,全是接口的套件又验不到用户真正看到的东西。
7. 连进一个已经打开的真实浏览器
有些场景你不想让 Playwright 自己起浏览器:想复用已经登录好的日常浏览器、想绕开每次都要过的验证码、想看它操作的全过程。
1 | # 先用调试端口启动一个 Chrome(Windows) |
1 | const { chromium } = require('playwright') |
connect() / connectOverCDP() 是长期稳定的能力;1.59 的 browser.bind() 则更进一步,让多个消费方(Agent、runner、MCP)挂到同一个浏览器实例上,状态不再分裂。做 Agent 相关的事时值得留意这条线。
8. 把它当”网页版 cron”用
不一定非要测试。我自己的两个用法:
- 给自家应用做上线冒烟:部署完自动跑三条用例(能登录、记一笔能加分、兑换页能打开),比人肉点一遍可靠。
- 定时抓页面出图存档:headless 打开报表页,等关键元素出现,
page.screenshot()或page.pdf()存下来,配合storageState免登录,一台机器就能跑。
这类脚本记得三件事:browser.close() 放 finally、给整个脚本套总超时、失败时把截图一起落到日志目录。
9. 和 AI 协作:MCP 与 Test Agents
这是 Playwright 现在最有意思的一块。
Playwright MCP 把浏览器能力暴露成 Agent 可调用的工具。它走的是 ARIA 快照路线而不是截图:不跑视觉模型,直接把可访问性树转成结构化文本,Agent 读到的是”这是个 textbox,名字叫用户名,ref 是 e5”——比截图省一个数量级的 token,而且定位是确定性的。1.60 起快照还能带上元素坐标(boxes 选项),布局推理、遮挡判断这类以前必须靠视觉模型的判断,现在从语义树里直接能拿到。
1 | npx @playwright/mcp@latest |
Test Agents(1.56 引入,1.60 转生产可用)预置了三个角色,构成一个闭环:
planner探索运行中的应用,产出一份人类可读的 Markdown 测试计划;generator读计划生成 spec,生成过程中实时验证选择器和断言;healer跑失败的用例,检查运行时 UI,提出修复(通常是更新定位器或调整等待条件)。
1 | npx playwright init-agents --loop=vscode |
(这是官方发布说明里给的接入方式,具体参数请以你版本的 changelog 为准。)
但有一条边界,Playwright 自己划得很清楚,我认为这也是它比多数 Agent 工具清醒的地方:healer 能修定位器、修等待条件,但不碰断言。 因为它无法可靠区分”应用改版了”和”应用有 bug 了”——前者该修测试,后者该保持红色。所以实践上:heal 必须以提案形式出现(一个 PR 里的 diff,人 review),绝不自动合入。 允许 heal 直接进 main 的团队,最后都会发现整套测试”礼貌地绕过了一个真 bug”。同理,planner 能提覆盖方案,但”这个断言对不对”仍然是产品判断。
六、踩坑清单
waitForTimeout()是技术债的写法,等条件,别等时间。expect(await …)只断言一次,写expect(locator)才有重试。- **strict mode 报错别急着加
.first()**,先想是不是选择器写得太宽——.first()经常是在掩盖一个真 bug。 - **
page.route()记得unroute()**,或者用{ times: 1 }限制次数,否则拦截会渗到后续用例。 page.pdf()只在 Chromium 无头可用,headed 或 Firefox/WebKit 会直接报错。- headed 与 headless 行为有差异(字体、UA、扩展、视口),视觉基线要在和 CI 一致的模式下生成。
- Linux CI 缺系统依赖的报错很难懂,装浏览器时用
--with-deps。 - 企业代理下浏览器下载失败:设
HTTPS_PROXY,或PLAYWRIGHT_BROWSERS_PATH指到可写目录。 - 别给整套件加同一个锁或改成单 worker,那是把并行收益全丢了;只锁真正争抢共享资源的用例。
- 升级前看 changelog:1.62 之后老的 React/Vue 组件测试包停止更新(官方引导迁到 stories 模型),Ubuntu 20.04 也已移出官方支持范围。
- 反爬场景别用默认 headless 特征,真实 UA、语言、时区一起设,并且先确认对方站点允许你这么做、控制频率。
- trace 不归档等于没跑:CI 上
trace: 'on-first-retry'起步,Agent 驱动的自动化更是必须存 trace,否则不可审计。
总结
一句话选型:一次性的事用 playwright 库,要长期跑的事用 @playwright/test。
再往深一层,Playwright 真正的价值不是”API 比谁多”,而是它把三件麻烦事做成了默认行为——自动等待让 flaky 从”要靠经验躲”变成”框架替你兜”,Context 隔离让并行从”要重构”变成”打开开关”,trace 让失败从”贴一屏日志”变成”回放那一步”。这三件事一旦成为默认,端到端自动化才第一次具备”能长期养着”的性质。
而它现在正在做的第四件事,是把同一套能力交给 Agent:结构化的感知(ARIA 快照)、一致的执行环境(bind)、可审计的记录(trace + HAR)、预置的工作流(planner / generator / healer)。执行层的所有权在转移,判断层的归属没有——测什么、什么算对、能不能发,仍然是人的事。
好的端到端测试不是”能跑通”,而是”红的时候你能立刻知道为什么”。
想系统了解它的能力边界和多浏览器支持,可以先看前面那篇 Playwright 自动化测试框架详解;这篇里的代码按 1.64 的 API 写,示例自带一段最小 HTML,复制即可跑。
注:本文基于 Playwright 1.64(2026-10)的公开 API 与官方 changelog 撰写;文中出现的具体版本号来自官方发布记录,升级后请以对应版本的 Release Notes 为准。





