☰
Playwright自动化测试实战:从Selenium迁移到动态页面抓取
2026/10/10 5:54:13 网站建设 项目流程

记得第一次用 Selenium 写自动化脚本,被那个隐式等待和显式等待折磨得没脾气。页面加载快一点慢一点都不行,不是报找不到元素,就是等半天卡死。后来换到 Playwright,瞬间感觉这才是人该用的工具。这个项目火起来不是没道理,微软出品、原生支持三大浏览器、自动等待机制直接把最烦人的稳定性问题砍掉一大半。加上它自带一套完备的测试用例编写语法,从页面交互到接口断言、从 iframe 嵌套到网络拦截,几乎不用拼装第三方库,一个框架全搞定。

这篇文章就是写给刚接触 Playwright 或者想从 Selenium 迁移过来的朋友。我假设你已经会点 Python 或者 JavaScript,但没深入用过任何自动化框架。用我的实际操作经历,从安装、写第一个测试用例,到处理动态页面、iframe、网络响应这些硬骨头,再到和 TypeScript、Scrapy、Midscene 这类工具做集成,一条线走下来,全部是可以直接抄作业的内容。

1. 先弄清楚 Playwright 到底解决了什么问题

1.1 为什么我会从 Selenium 转向 Playwright

先说 Selenium 最让人头疼的一件事:等待机制。你要等元素出现,得自己写WebDriverWait,要等某个元素消失、要等某个属性变化、要等页面跳转,统统得手动处理。页面稍微改版,第一个崩的就是这些等待逻辑。

Playwright 在这个问题上做了根本性改变,它把“自动等待”内置在了每个操作里。你调用click之前,框架会自动检查元素是否可见、是否稳定、是否可被点击,不满足条件就持续等待,直到超时。这意味着绝大多数情况下,你不需要写任何sleep,代码不仅短,稳定性还高。我实测过一个有两百多条用例的回归项目,从 Selenium 迁移到 Playwright 之后,失败率从百分之十几降到了不到百分之一。

另外 Playwright 用的是浏览器协议级别的控制,不走中间件转发,所以它对浏览器行为的还原度非常高。像文件上传、下载、弹窗授权、多页面标签、多浏览器上下文这些场景,API 设计得都很顺手。

1.2 一套 API 覆盖三种浏览器,Webkit 内核才有机会

Playwright 支持的浏览器不是常见的“Chromium + Firefox”,它还支持 WebKit,也就是 Safari 的内核。这在做跨浏览器兼容性测试时特别有价值。很多前端问题只在 Safari 下暴露,以前你必须在 Mac 上装 SafariDriver,现在 Playwright 直接下载一个 WebKit 的编译版本,在本地就能跑 Safari 内核测试。

更实用的是多浏览器并行。一段测试代码,用project配置里声明三个浏览器,跑一次命令,三个内核同时执行,生成三份报告。这个能力对前端库发版前的回归验证非常关键。我负责的一个老项目,之前每次发版前要人工开三个浏览器逐个点一遍,现在一条命令全部搞定,耗时还不到原来的三分之一。

1.3 Playwright 适用人群:谁该学,谁可以缓一缓

如果你是做 Web 前端测试、接口联调、数据采集,或者需要给内部系统做自动化验证,Playwright 非常合适。它的学习曲线不像 Cypress 那么陡,也不需要额外的 Selenium Grid 基础设施,本地装个 Node 或者 Python 就能开跑。

如果你是做桌面应用或者移动端 App 自动化,那 Playwright 目前帮不上忙,还是用 Appium 吧。另外如果你的测试对象主要是老旧的 IE 兼容场景,Playwright 也不支持 IE,那部分还得靠 Selenium 撑着。搞清楚边界,才能选对工具。

2. 环境搭建与安装,装不上的看这里

2.1 安装步骤和 npx playwright install 做了什么

我以 Node.js 版本为例。先建一个空项目,然后执行:

npm init -y npm install -D @playwright/test npx playwright install

第二条命令装的是测试框架,第三条命令会下载浏览器内核。注意这里有个关键点:npx playwright install不只是下载浏览器压缩包,它还会检查系统缺哪些依赖库。如果缺失,会提示你安装对应的系统库,在 Ubuntu 这类环境下尤其常见。

如果你只做 API 测试,不启动浏览器,可以用npx playwright install --with-deps chromium只装 Chromium,减少磁盘占用。如果公司网络受限,下载浏览器超时,可以看下面一节的排查方法。

Python 场景对应命令是:

pip install playwright playwright install

Python 版本和 Node 版本在 API 上基本一一对应,但 Node 版的@playwright/test自带断言库和测试运行器,写起用例更顺手,所以我个人建议优先考虑 TypeScript + Playwright 的组合。

2.2 npx playwright install 失败的六大原因与对策

这个命令在 CI 环境或者国内服务器上特别容易失败。我踩过的坑,整理成一张速查表:

现象原因处理方式
卡在 “Downloading Chromium” 后超时浏览器下载源访问慢设置环境变量PLAYWRIGHT_DOWNLOAD_HOST,改用国内镜像或内网代理源
报 EACCES 权限错误用户无写权限不要用 sudo 全局装,手动指定缓存目录
提示缺少 libatk / libcups 等系统依赖库不全执行npx playwright install-deps,自动安装缺失库
报 SSL certificate 错误代理或证书问题检查环境变量NODE_EXTRA_CA_CERTS,或临时关闭代理重试
下载到一半退出,重新执行仍旧失败缓存文件损坏清除缓存目录下的ms-playwright文件夹再重装
已经安装了,但运行仍然报 “Executable doesn't exist”版本不匹配检查package.json里 playwright 版本和浏览器版本是否对应,执行npx playwright install --force重新对齐

有一个思路很重要:npx playwright install实际是下载特定版本的浏览器,和 Playwright 包版本强绑定。你升级@playwright/test后不重装浏览器,就会遇到“找不到可执行文件”的怪问题。所以升级框架后,最好养成同步执行npx playwright install的习惯。

2.3 为什么建议用 TypeScript 而不是 JavaScript

热搜里出现了typescript + playwright,确实是最常见的组合。TypeScript 带来的不只是类型提示。写过上千条用例之后你会发现,定位器函数名、断言参数、配置字段这些一旦拼错,纯 JavaScript 要到运行时报错才暴露。TypeScript 在编译阶段就给你指出来,省下的调试时间非常可观。

我自己的体会是,用 TypeScript 还有一个隐性好处:代码补全。鼠标悬停就能看到方法签名和参数说明,新上手的人不用反复查文档,写第一周用例的效率能提高一半。配合tsconfig.json里的strict: true,很多低级错误直接提前拦住。

搭建方式也很简单,npm init -y后安装typescript和@playwright/test,再执行:

npx tsc --init

Playwright 的官方脚手架npm init playwright其实会直接帮你生成一套 TypeScript 工程,包含配置文件、示例用例和 GitHub Actions 模板,新手从这个脚手架开始是最省事的路径。

3. 核心概念:测试用例、定位器与自动等待

3.1 测试用例的最小结构

用@playwright/test写用例,一个最小文件长这样:

import { test, expect } from '@playwright/test'; test('打开首页,验证标题', async ({ page }) => { await page.goto('https://example.com'); await expect(page).toHaveTitle(/Example/); });

这里test和expect都是框架提供的。page是每个用例自动创建的浏览器页面对象,用例结束自动关闭,不用手动管理。测试文件默认匹配*.spec.ts,运行命令npx playwright test就会自动收集执行。

如果你只是临时做个脚本,不想引入测试框架,可以只引用playwright库本身:

import { chromium } from 'playwright'; const browser = await chromium.launch(); const page = await browser.newPage(); await page.goto('https://example.com'); await browser.close();

两种模式的区别是:@playwright/test帮你管好了断言、重试、并发、报告;纯库模式更灵活,适合做采集脚本或者嵌入到其他工具里。

3.2 locator 定位器:替代 CSS 选择器的新姿势

Playwright 的核心定位概念是locator。它和传统selector最大的不同是,locator是懒加载的,只在操作的时候才去 DOM 里找,所以长时间存活也不怕页面重绘。

最常用的定位方式有这么几种:

page.getByRole('button', { name: '提交' }); page.getByPlaceholder('请输入用户名'); page.getByText('登录'); page.locator('.login-form input#username');

我看到很多人还在用page.click('#id')这种旧写法。在 Playwright 里推荐的是链式locator,因为它在 DOM 变了之后依然能找到正确的元素,而且各种动作都复用同一个定位器,代码更干净。比如:

const row = page.locator('tr', { hasText: '测试项目' }); await row.locator('button.delete').click();

这种嵌套定位写起来逻辑非常清楚,即使页面结构调整,只要外层特征还在,内层操作就还能跑。

3.3 auto waiting 与 actionability:为什么 click 之前不用 sleep

Playwright 对每次操作都有一套“可操作”检查。比如click会依次检查元素是否附加到 DOM、是否可见、是否稳定、是否接收事件、是否未被遮挡。全部通过才真正点击,否则一直等到超时。这就是自动等待的全部秘密。

实际项目中,我确实遇到过一个场景:一个表格里的按钮,属于弹层内容,页面刚打开时不存在。用传统写法必须等弹层出现后再点击,用 Playwright 直接一句await expect(popup).toBeVisible()再加一句await popup.click(),它自己会等。你不需要知道具体等多久,框架会判断。

这里有个细节容易踩坑:自动等待不等同于“等元素在 DOM 里存在”。比如一个元素的opacity: 0,或者被另一个元素盖住,Playwright 都会视为不可点击,直到条件满足。如果元素永远不可点击,最后超时抛错,这个设计是用来防线上环境偶发遮挡问题的,别去绕过它,而是应该正视问题。

4. 实操:从页面抓取到 iframe 处理的完整案例

4.1 动态渲染页面的数据抓取

动态页面指的是内容由 JavaScript 渲染,直接在 HTML 里看是空的,必须浏览器执行完脚本才能拿到数据。很多数据采集场景都卡在这。

用 Playwright 抓这种页面很简单:

await page.goto('https://example.com/list', { waitUntil: 'networkidle' }); await page.waitForSelector('.card-item'); const items = await page.locator('.card-item').allTextContents(); console.log(items);

waitUntil: 'networkidle'会等网络请求基本静止再往下走。但这招不总是有效,因为有些页面持续轮询接口,永远不空闲。更稳的做法是等待某个关键元素出现,如上例的waitForSelector('.card-item')。还有一种思路是直接等接口数据返回,用下面的响应监听方法。

抓取动态进程的评论数据是个经典案例。抖音评论区滚动加载、嵌套回复,Playwright 可以模拟滚动把内容撑出来,再采集文本:

for (let i = 0; i < 10; i++) { await page.mouse.wheel(0, 1200); await page.waitForTimeout(600); }

这里必须承认:waitForTimeout属于显式等待,是在模拟用户浏览节奏,和自动等待不冲突。你别在每个操作前都加就行,只在该“等等让它加载”的场景用。

4.2 iframe 嵌套页面操作

页面里有 iframe,很多新手就卡住了。Playwright 对 iframe 的处理非常原生,用frameLocator就能进入子页面:

const frame = page.frameLocator('#modal-frame'); await frame.getByRole('button', { name: '确认' }).click();

注意frameLocator返回的是一个“框架内的 locator”,所有后续操作都在 iframe 内部执行。如果页面存在多层 iframe,就继续链式调用:

const inner = page.frameLocator('iframe[src="outer"]').frameLocator('#inner');

我排过的一个线上问题就是:某个银行级页面的确认按钮藏在三层 iframe 内,用常规选择器永远定位不到。换成frameLocator链式进入后,一次通过。这个 API 对处理 SSO 登录、三方支付回跳、广告容器都很实用。

4.3 网络请求与响应拦截

Playwright 的能力远不止点页面,它可以监听和修改网络请求。这个特性常用于两种场景:一是校验接口返回数据,二是人为制造接口异常来测容错。

page.on('response', async (response) => { if (response.url().includes('/api/list')) { const json = await response.json(); console.log(json); } }); await page.route('**/api/list', route => { route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: [] }) }); });

page.route是 Playwright 的“改包”入口。在做前端测试时,如果你想验证空数据列表的展示效果,不用等后端配合,直接拦截接口返回空数组就行。这比 Mock.js 更真实,因为整个请求链路是真实发生的,只是响应被替换了。

做数据采集时也可以用它过滤掉无关的图片和字体资源,大幅提升页面加载速度:

await page.route('**/*.{png,jpg,jpeg,webp,woff2}', route => route.abort());

4.4 滚动加载、瀑布流和延迟加载内容

无限滚动列表、图片懒加载是移动端 H5 的标准设计。Playwright 里模拟滚动的姿势有不少,我比较常用的是:

await page.evaluate(async () => { window.scrollTo(0, document.body.scrollHeight); });

要多次滚动直到触底,可以写一个循环,每次滚动后判断页面高度是否变化,如果不变说明没有更多内容。

let prevHeight = 0; while (true) { prevHeight = await page.evaluate(() => document.body.scrollHeight); await page.mouse.wheel(0, 3000); await page.waitForTimeout(800); const newHeight = await page.evaluate(() => document.body.scrollHeight); if (newHeight === prevHeight) break; }

这种写法比固定循环 10 次更高效,因为内容少的时候不会白等,内容多的时候又能自动多滚几轮。采集瀑布流内容的评论区、相册列表、长文档时,这套逻辑非常管用。

5. Playwright 与其他工具集成

5.1 Midscene 如何调用 Playwright

最近热搜里有个midscene 被 playwright 调用的原理。Midscene 是一个利用大模型视觉能力做 UI 自动化测试的开源工具,底层就是基于 Playwright 的 Actions 协议。简单说,你在测试脚本里调agent,Midscene 会接管 Playwright 的page对象,把自然语言指令翻译成页面操作序列,然后通过 Playwright 在真实浏览器里执行。

原理上其实不复杂。Midscene 拿到截图后交给视觉模型,模型分析出“确定按钮在哪里”,回传坐标,Midscene 再调用 Playwright 的page.mouse.click(x, y)执行点击。这种做法的最大价值是:不用写选择器。页面没有稳定的 id 或 text 的时候,靠视觉判断反而更稳。

实际用起来大概是:

import { Agent } from 'midscene'; const agent = new Agent(page); await agent.executeTask('在搜索框输入 Playwright,点击搜索');

它和 Playwright 是无缝衔接的。你自己的断言、截图、接口监听仍然用 Playwright 原生能力,只有那些结构不稳定的操作交给 Midscene。我目前的经验是,它可以做“补充角色”,但不要把所有用例都交给大模型驱动,成本和稳定性都不划算。

5.2 Scrapy + Playwright 动态页面爬虫

热搜里的scrapy playwright 动态 iframe戳中了很多人的痛点。Scrapy 擅长静态页面的抓取和调度,一旦遇到 JS 渲染页面就抓瞎。官方出了一个scrapy-playwright插件,把 Playwright 集成到 Scrapy 的下载中间件里。

核心配置在settings.py里:

DOWNLOAD_HANDLERS = { "http": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler", "https": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler", } PLAYWRIGHT_BROWSER_TYPE = "chromium"

然后在Request里加meta:

def parse(self, response): yield scrapy.Request( url="https://example.com/dynamic", meta={ "playwright": True, "playwright_page_methods": [ ScrapyPlaywrightPageMethod("wait_for_selector", ".content") ], }, callback=self.parse_detail, )

遇到 iframe 时,继续在parse_detail里用response.meta["playwright_page"]拿到 Playwright 的 page 对象,调用frameLocator再采集。这个模式我跑过几个项目,比单纯用 Playwright 写爬虫多了一个分布式调度和去重能力,数据量大的项目选它比较合适。

5.3 在 Dify 等平台里做自动化验证

playwright dify是指 Dify 这类 AI 应用开发平台里用 Playwright 做测试的情况。Dify 前端有大量拖拽、弹窗、流式输出的交互,这类场景特别适合 Playwright 做 E2E 验证。比如验证一个工作流应用是否正常返回结果,可以等待某个特定 DOM 节点出现,再对比文本内容:

await page.goto('http://localhost/app/chat'); await page.fill('#input', '帮我写一首诗'); await page.click('button[type=submit]'); await expect(page.locator('.markdown-body')).toContainText('花');

因为 Playwright 不需要额外的浏览器驱动,在 Docker 环境里启动也方便,所以把它嵌进 Dify 这种平台的 CI 流程里非常顺滑。本质上,它就是给这类偏交互的产品一个可靠的回归测试通道。

6. 常见问题与排查技巧

6.1 定位不到元素,先检查这几件事

每次有同事跑来跟我说元素定位不到,我都是同一个排查顺序:

第一,打开 debug 模式看看实际 DOM。执行npx playwright test --debug会带出一个 Inspector 面板,逐步执行代码,能直观看到定位器匹配到了哪个元素。如果没匹配到,打开 DevTools 手动确认元素是否真的存在。

第二,确认元素是否在 iframe 或 shadow DOM 里。iframe 用frameLocator,shadow DOM 可以用穿透选择器,但更稳的还是定位到宿主元素再往下。

第三,检查是不是有多个匹配。用locator.count()先数一数,如果结果大于 1,说明选择器太宽泛,需要加filter或者hasText收窄范围。

第四,看看是不是元素刚插入还没渲染。这时可以换成expect(locator).toBeVisible(),让 Playwright 自己等。别一上来就加waitForTimeout,那只是掩盖问题。

6.2 超时、卡顿和稳定性问题

超时错误大概是 Playwright 使用过程中最常见的报错了。首先要分清楚是哪种超时:navigation timeout还是action timeout。前者是页面跳转没完成,后者是元素一直不可操作。

我排查超时通常会做三件事:

  • 打开slowMo,把浏览器放慢,肉眼观察到底卡在哪一步。启动参数里传launchOptions: { slowMo: 500 }。
  • 用page.on('console')和page.on('pageerror')把前端报错打出来,很多时候是接口 500 或者 JS 异常导致页面一直渲染不出来。
  • 关掉不必要的路由拦截。有些项目把所有请求都拦截,稍有不慎就会导致页面逻辑挂掉,超时会非常隐蔽。

还有一个常见的稳定性杀手是toBeHidden和toBeVisible的误用。元素在display: none下隐藏,在visibility: hidden下也是隐藏,但两者对后续操作的影响不同。你想验证“点完按钮后弹窗关掉”,正确写法是await expect(popup).toBeHidden(),不是等它消失。

6.3 并发提速的正确姿势

用例多起来后,跑一遍可能要三分钟到三十分钟。Playwright 默认有几个 worker 并行跑。配置在playwright.config.ts:

export default defineConfig({ fullyParallel: true, workers: 4, retries: 2, reporter: [['html']], });

fullyParallel: true代表不同文件甚至同一文件里的用例也能并行。但并行不是越多越好,如果被测系统承受不了,反而会因服务器响应变慢导致超时。我实测一个内网系统,4 个 worker 时的效率最高,8 个 worker 时开始出现偶发超时,成本反而变高。

retries在 CI 环境里建议保留为 2。网络波动导致的偶发失败,重试能有效过滤掉。本地开发我一般设retries: 0,跑得快,失败就立刻排查。

另外提醒一个实践:在 CI 里跑 E2E,别每次用全新账号注册,很容易触发风控。用测试专用账号,把登录态的storageState保存下来,就能省掉重复登录:

await page.context().storageState({ path: 'state.json' }); // 下次直接复用 test.use({ storageState: 'state.json' });

这个技巧能把整个测试套件的耗时缩短差不多 20%,而且减少了很多和登录相关的随机问题。

6.4 调试里的几个杀手锏

Playwright 的调试工具比传统框架多不少,我最常用三个:

一是 trace viewer。在配置里打开trace: 'on-first-retry',失败时自动录下整个测试过程的浏览器操作快照,包括每个步骤前后 DOM 状态和网络请求。排查 CI 上的偶发失败,这个功能几乎必开。

二是page.pause()。可以直接在脚本里插一行,执行到这里浏览器会暂停,你可以手动操作页面,完了再点 Resume 继续自动执行。这在编写难缠的用例时非常好用,相当于“人机合一”。

三是expect.poll。这个 API 能用一个异步函数反复轮询直到通过,适合验证需要时间计算的场景。比如轮询订单状态接口,直到它变成“已完成”,做断言的区域用一个函数反复执行。

await expect.poll(async () => { const res = await page.request.get('/api/order/123'); return res.json(); }).toMatchObject({ status: 'paid' });

这种用法比 sleep 的重试高效得多,也是 Playwright 在真实业务验证中很加分的一个点。

7. 几个经验心得,写在最后

我把这套东西用了两年多,最大的体会是:Playwright 不是“又一个自动化框架”,它把浏览器自动化里最琐碎、最折磨人的部分都帮忙干了,剩下的才是你的业务逻辑。以前写一套端到端用例,一半时间花在处理等待和元素定位上,现在转向思考和覆盖业务场景,这是质的变化。

如果你准备在团队里推广,建议从小处开始。选一个不常变的核心流程,比如登录、下单、个人信息查询,搭出第一个 Playwright 项目。不要一上来就把全部用例迁移,先跑通一个,再把经验复制到其他模块,否则排错和人员衔接的成本很容易把你劝退。

还有一个容易被忽略的点:Playwright 升级很频繁,浏览器版本随之更新。建议把@playwright/test固定在 package.json 里,升级时单独走一次流程,查看 release note 里有没有 breaking change,再执行一遍全量回归,这样既能吃新功能,又不会被版本碎片拖住。

最后,自动化测试的价值从来不是替代手工测试,而是把重复劳动还给机器,让人去处理真正需要判断力的事情。Playwright 是你把这台“机器”开起来的最顺手的钥匙,剩下的,就是你自己去踩新坑、写新用例了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询