你是不是也在写自动化测试的时候被sleep(2)折磨过?页面等久了超时,等短了元素还没渲染,最后只能靠玄学调等待时间。如果你还在用这种方式维护测试脚本,我强烈建议你看看 Playwright。它是微软开源的自动化测试框架,核心能力是跨浏览器(Chromium、Firefox、WebKit)驱动、自动等待和一套非常贴近用户视角的选择器体系。这篇文章我会把这些年在生产环境里用 Playwright 踩过的坑、沉淀的方法论全部整理出来,从环境搭建、元素定位、动态页面处理、网络拦截,到 AI 时代大家都在讨论的 Playwright MCP 集成、离线部署,以及脚本挂起这类疑难杂症的排查思路,一次性讲透。适合正在做 Web 自动化测试、想从 Selenium 迁移过来、或者打算用 AI Agent 操作浏览器的工程师参考。
1. 为什么是 Playwright:它解决了我最头疼的几件事
1.1 和 Selenium、Cypress 的本质差异
很多人第一次接触 Playwright 都会问:Selenium 那么成熟,我为什么要换?我曾经在好几个项目里用 Selenium 维护了几百条用例,最大的痛点不是写脚本,而是不稳定。Selenium 走的是 WebDriver 协议,浏览器和脚本之间隔了一层驱动服务,元素明明在页面上,脚本就是找不到,或者点击被其他元素挡住,然后就要加各种显式等待,代码里全是expected_conditions。
Playwright 不跟你玩这套。它直接通过 Chrome DevTools Protocol(CDP)和浏览器通信,浏览器启动、页面渲染、事件监听全都在一个进程内完成。这意味着 Playwright 的响应速度更快、稳定性也更好。更重要的是,Playwright 内置了一套Actionability 检查:在点击、填写、拖拽之前,它会自动等待元素可见、稳定、可交互。比如你要点击一个按钮,框架会等这个按钮渲染出来、位置不再跳动、没有被其他元素遮挡,然后才执行点击。这一套机制直接把 Selenium 时代最折磨人的“WebDriverWait + EC 组合拳”给干掉了。
Cypress 其实也做了类似的事,但 Cypress 有个致命限制:它只能跑在 Chromium 系浏览器上,而且是在页面所在的进程里执行脚本,遇到跨域 iframe、多标签页场景非常吃力。Playwright 天然支持多浏览器、多上下文、多标签页,在处理复杂业务系统时几乎不受限制。
1.2 自动等待到底是怎么工作的
理解 Playwright 的自动等待机制,是写出稳定测试脚本的前提。它不像 Selenium 那样“查找元素”就完了,Playwright 的每个动作都会执行一套可操作性检查,你可以理解成“动作前体检”。比如click()会依次检查:元素是否 attach 到 DOM、是否可见、是否稳定(没有动画/位移)、是否接收事件(没有被其他元素覆盖)、是否 enabled。这些检查默认在 30 秒内完成,超时就会抛异常。
这里有个关键细节:自动等待是动作级别的,不是查找级别的。你用page.locator('button')拿到的只是一个选择器,不会触发任何等待;真正触发等待的是.click()、.fill()、.press()这些动作。很多新手会写await page.locator('#login').is_visible(),然后发现返回 False 就断言失败,这是因为is_visible()不会自动等待。如果你只是要判断元素最终是否出现,应该用expect(locator).to_be_visible(),它是带自动重试的断言。
我有一次排查线上用例失败,原因是页面有一个 loading 遮罩层,透明度动画持续几百毫秒,Playwright 的稳定性检查认为元素还在“移动”,于是反复重试直到超时。解决办法也很简单:先等 loading 元素消失,或者直接等待需要点击的元素稳定。自动等待不是玄学,它只是一套可配置的轮询机制,理解了这个原理,遇到超时就不会慌。
1.3 先搞清楚边界:写测试还是写工具
Playwright 经常被拿来写爬虫、写自动化脚本,但我在团队里一直强调一个边界:测试代码和工具代码要分层。测试代码追求的是可读性、稳定性、可维护性,工具代码追求的是效率、容错、反检测。两者混在一起,最终一定是一团乱麻。
如果你是做端到端测试,请严格使用 Playwright 提供的断言库(@playwright/test或pytest-playwright),利用它的 fixture 机制、失败截图、trace 回放。如果你是做页面自动化工具(比如定时上报数据、批量导出文件),那可以只用 Playwright 的库 API,配合自定义重试和业务逻辑。这篇博文主要以测试场景为主线,但在涉及网络拦截、iframe、下载上传这些能力时,工具场景同样适用。
2. 环境搭建与工具链选型
2.1 安装与浏览器管理(含离线安装教程)
玩转 Playwright 的第一步是装对东西。Node.js 项目直接npm init playwright@latest,它会帮你创建测试目录、配置文件和一个示例用例。Python 项目用pip install pytest-playwright,然后在项目根目录执行playwright install来下载浏览器。
这里有个最常见的坑:playwright install下载的是 Chromium、Firefox、WebKit 三套浏览器,体积非常大,在国内网络环境下经常慢到令人崩溃。解决思路有两条。
第一,设置国内镜像环境变量。在 Linux/Mac 上执行:
export PLAYWRIGHT_DOWNLOAD_HOST=https://npmmirror.com/mirrors/playwright playwright install chromium第二,如果你所在的服务器完全无法访问外网,那就走离线安装。找一台能联网的机器,先playwright install chromium,然后把浏览器缓存目录整个打包拷过去。缓存目录默认是~/Library/Caches/ms-playwright(Mac)或~/.cache/ms-playwright(Linux),也可以用环境变量PLAYWRIGHT_BROWSERS_PATH指定到你想要的位置。拷贝到目标机器后,同样把PLAYWRIGHT_BROWSERS_PATH指向解压目录,脚本就能直接找到浏览器了。
注意:不同版本的 Playwright 对应的浏览器版本不同,离线拷贝时一定要保证目标机器上的
playwrightnpm 包或 Python 包的版本和下载浏览器时的版本一致,否则会报“Executable doesn't exist”之类的错误。这个坑我踩过不止一次,最好在 CI 里固定版本号。
2.2 codegen:写选择器的“作弊器”
新手问的最多的问题就是“选择器怎么定位”。我的建议永远是:别手写,用playwright codegen录。执行:
playwright codegen https://example.com它会打开一个带操作记录的浏览器窗口,你在页面上点了什么、填了什么,它都会实时生成对应的代码。你还能直接在录制器里点击某个元素,它会展示这个元素的所有候选定位方式,包括text=、role=、css=等,你可以挑最稳定的一条用。
我录制的时候有个习惯:录完不是直接复制就完事,而是把自动生成的 CSS 选择器改成更贴近用户语义的定位方式,比如把.btn-primary改成getByRole('button', { name: '登录' })。这样做一方面是为了抗 UI 变动,另一方面是让测试报告里的失败信息更可读。codegen 最大的价值不是省写代码的时间,而是让你快速了解一个陌生页面的 DOM 结构和可访问性属性。
2.3 配置文件:timeout、workers、trace 是不可忽视的
Playwright 的配置中心是playwright.config.ts(或者pytest.ini里的相关配置)。我建议每个项目都显式设置这几个参数:
- timeout:全局超时,一般设置为 30_000ms。如果你页面加载慢,可以调到 60_000ms,但千万别无脑调大,否则用例失败时等待成本很高。
- workers:并行执行数。测试多的话并行能省很多时间,但也要看 CI 机器的 CPU 和内存,我曾经在 2 核 4G 的机器上开 4 个 worker,结果浏览器进程频繁崩溃,后来老老实实改成 2。
- trace:失败时自动录制 trace,可配置为
'on-first-retry',也就是第一次重试失败时保留完整执行轨迹。平常跑不录制,节省空间,一旦失败就能打开 Trace Viewer 精确查看每一步发生了什么。
Python 项目里可以在pytest.ini中配置:
[pytest] addopts = -v --tracing=on-first-retry --video=retain-on-failure --screenshot=only-on-failure截图、视频、trace 三个一起配置,失败了就能拿到现场证据。线上问题排查的时候,这个配置救命的频率非常高。
2.4 Playwright MCP 和 Chrome DevTools MCP 到底有什么区别
最近 AI 圈子里 MCP(Model Context Protocol)特别火,热词里也有 playwright mcp、chrome devtools mcp。很多人分不清这些概念,我直接说结论。
MCP 是一个为 AI 模型提供工具能力的开放协议,你可以理解成“AI 的 USB 接口”。Playwright MCP 是把 Playwright 封装成一个 MCP Server,让 AI 助手能够通过自然语言直接调用 Playwright 的能力去打开页面、点击按钮、提取数据。Chrome DevTools MCP 则走的是 Chrome DevTools Protocol,它连接的是已有的 Chrome 实例,主要用于“让 AI 帮你看一下当前页面”。后者更像是给开发调试用的辅助工具,前者更像是给自动化测试和网页任务执行用的完整工具箱。
如果你要用 AI 驱动浏览器做端到端测试,选 Playwright MCP;如果你只是想让 AI 看看你已经打开的网页、帮忙调一下样式,选 Chrome DevTools MCP。至于 Browser Use MCP,它更偏重 Agent 自主完成网页任务(比如自动填表、抓取数据),和 Playwright MCP 相比,后者在测试断言、选择器、多浏览器支持上更专业。我个人的建议:现阶段 AI 写测试脚本还是只能做辅助,真正上线跑用例,依然要依赖你写的确定性断言和稳定选择器。
3. 核心 API 实战:稳定可靠的元素定位
3.1 选择器优先级:从 getByRole 到 test-id
定位元素是整个自动化测试的命门。元素定不准,后面全是白搭。我总结了一个优先级经验:
- 用户可见属性优先:
getByRole、getByLabel、getByPlaceholder、getByText。这些 API 对应的是屏幕阅读器、无障碍访问看到的页面,靠近用户的真实感知,UI 变动时稳定性最高。 - 显式测试标识:
getByTestId("login-submit")。需要开发配合在 DOM 上加>await page.click('#app > div.container > div.row > button.submit-btn');后来发现这个页面表单有好几个按钮,类名还经常变,改成:
await page.getByRole('button', { name: '提交订单' }).click();失败率直接降了一个数量级。因为按钮的名称在视觉上几乎不会变,而 class 和 DOM 层级说改就改。
3.2 动态页面与 iframe 处理
动态页面是自动化的重灾区,尤其是那些内容异步渲染、列表不断刷新的后台系统。Playwright 处理动态内容的核心思路是不要等待“时间”,要等待“状态”。比如等某个请求完成、等某个元素出现、等某个文本变成预期值。
如果你要等一个异步加载的数据表格渲染完成,不要
sleep(3),而是:await expect(page.locator('.data-table tbody tr')).to_have_count(10);或者等某条业务数据出现:
await expect(page.getByText('订单号: A10086')).to_be_visible();iframe 是另一个高频问题。很多嵌入的地图、支付组件、第三方登录都是 iframe 渲染的,用普通 locator 永远找不到里面的元素。Playwright 的解决方式是
frameLocator:const frame = page.frameLocator('#pay-frame'); await frame.getByPlaceholder('请输入银行卡号').fill('6222...'); await frame.getByRole('button', { name: '确认支付' }).click();注意
frameLocator用的是 CSS 选择器来定位 iframe 本身,然后它的后续查找会自动进入 iframe 内部,不需要你手动切换上下文——这正是比 Selenium 舒服的地方。如果页面有嵌套 iframe,一层一层套frameLocator就行。还有一个经典的坑:iframe 内部的元素在 iframe 重新加载后会失效。如果你在 iframe 里定位到了元素,然后页面触发了 iframe 的
src切换,之前的 locator 就作废了。解决办法是重新获取 frameLocator,或者断言等 iframe 出现后再操作。3.3 弹窗、新标签页、网络事件监控
页面弹窗(alert/confirm/prompt)在 Selenium 里要
switch_to.alert,切来切去很麻烦。Playwright 里更推荐直接监听:page.on('dialog', dialog => dialog.accept());这样弹窗出现时自动点确定,测试该干嘛干嘛。如果你想验证弹窗文案,也可以在监听器里把
dialog.message()记下来再断言。新标签页(比如
target="_blank"的链接)更简单。用waitForEvent('popup'):const [popup] = await Promise.all([ page.waitForEvent('popup'), page.click('a[target="_blank"]') ]); await popup.waitForLoadState(); console.log(await popup.title());这里的
Promise.all非常关键,因为它先把监听挂上,再触发点击,避免事件发生在监听之前导致丢失。这是 Playwright 异步模型的一个核心套路:先 wait 再 act。凡是会引起页面跳转、新窗口、下载的动作,我都建议写成Promise.all([promise, action])的结构。网络事件监控方面,最常用的是
page.on('request')、page.on('response')和page.on('requestfailed')。我经常用它在测试里做网络层的“探针”,比如断言某个埋点请求确实发出了:let trackingSent = false; page.on('request', req => { if (req.url().includes('/api/track') && req.method() === 'POST') { trackingSent = true; } }); await page.click('#buy-now'); expect(trackingSent).toBeTruthy();这种做法的好处是验证业务链路是否完整,而不仅仅是看 UI 有没有变化。有些前端页面在点击后灰了、按钮变了,但埋点请求其实没发出去,靠 UI 断言根本发现不了问题。
4. 高频场景攻坚实录
4.1 滚动加载与 scrollIntoView
无限滚动列表(电商商品流、信息流、后台日志)是常见场景。想在 Playwright 里滚动页面,我见过很多新手直接
page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)),这没错,但对一个具体的业务元素来说,更优雅的做法是让目标元素滚进视野:await page.locator('.item-50').scroll_into_view_if_needed(); await expect(page.locator('.item-50')).to_be_visible();这种基于元素的位置来滚动,比计算像素值靠谱得多,因为不同窗口高度、不同屏幕尺寸下,需要的滚动距离完全不一样。
如果你就是要把整个页面滚到底,也有一个更贴近真实用户的操作方式:
await page.mouse.wheel(0, 1000);重点说一下持续滚动加载的用例怎么写。我通常搭配循环和超时控制:
for (let i = 0; i < 20; i++) { await page.mouse.wheel(0, 1200); await page.wait_for_timeout(500); if (await page.getByText('没有更多了').is_visible().catch(() => false)) { break; } }注意这里我用了
is_visible().catch(() => false),因为元素不存在时,is_visible()会直接抛异常,加个 catch 能让循环更稳定。滚动加载测试最忌讳的是固定等一个时间,因为你根本不知道网络快慢,正确做法是“滚动一下,等状态,判断是否继续”。4.2 下载文件与上传文件
下载文件是管理后台、报表系统的高频功能。Playwright 里下载不是直接拿个路径,而是通过事件获取下载对象:
const downloadPromise = page.waitForEvent('download'); await page.getByRole('button', { name: '导出报表' }).click(); const download = await downloadPromise; await download.saveAs('/data/reports/2024-export.xlsx');这个过程中有几件事值得注意。第一,不要监听
page.on('download')然后自己去拼路径,download.saveAs()才是确定性的保存方式,它会处理临时文件。第二,下载文件名建议用download.suggested_filename()重新拼一个你自己的规范名,因为后端返回的文件名经常是乱码或者带时间戳。第三,如果下载的是大文件,要设置合适的超时,比如page.setDefaultTimeout(120000),否则默认 30 秒不够用。上传文件反而更简单。
set_input_files可以直接传本地路径:await page.locator('input[type=file]').set_input_files([ '/data/avatar.jpg', '/data/banner.png' ]);关键点在于,这个 input 元素可能是个隐藏元素(
display:none),普通click()点不了,但set_input_files是允许直接操作隐藏输入框的。如果上传组件是自定义的可拖拽区域,没有原生 input,那就要先读一下文档,看它是否暴露了可供DataTransfer操作的接口。4.3 滑块验证码与校验类控件的测试思路
滑块验证码是自动化测试里绕不开的烦人东西。我要先强调一句:所有验证码测试的前提是你在被测系统自己的测试环境里,且有授权。把绕过验证码用在未授权的生产系统、抢票、薅羊毛上,既违反规则也可能触犯法律,这条底线不能碰。
在被测环境里,处理滑块的技术方案基本固定:识别滑块和缺口位置,然后模拟人类拖拽轨迹。我的一套可行思路是这样的。先用 Playwright 截取滑块区域图片,然后用图像处理(比如 OpenCV 模板匹配或像素边缘检测)找到缺口位置,最后用
page.mouse完成拖拽:const start = await page.locator('.slider-handle').bounding_box(); const target = await calculateGapPosition(); // 自定义识别函数 const steps = 25; for (let i = 1; i <= steps; i++) { const x = start.x + (target.x - start.x) * (i / steps) + Math.random() * 2; const y = start.y + Math.random() * 3; await page.mouse.move(x, y); } await page.mouse.down(); await page.mouse.up();轨迹里的随机抖动是关键,匀速直线拖拽很容易被判为机器操作。真实的拖动轨迹是中间快、两端慢,还带一点上下抖动,所以在位移函数里我通常用缓动曲线而不是线性插值。
但我必须坦白讲:滑块验证码是一个军备竞赛的领域,服务端会做轨迹拟合、速度检测、浏览器指纹、上下文行为分析,纯前端模拟很难做到 100% 通过率。在测试环境里,更靠谱的方案是让开发同学提供一个“万能验证码”或者临时关闭验证的开关,把滑块验证码作为单独的安全测试专项去处理,而不是让每条业务用例都去跟滑块死磕。
4.4 网络拦截与 mock 数据
这是 Playwright 最强大的能力之一:你可以完全拦截浏览器的网络请求,返回你想返回的数据。这样测试就不依赖后端环境,前端联调和测试都能并行。
拦截并把某个接口替换成 mock 数据:
await page.route('**/api/user/info', route => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ id: 1, name: 'Test User', level: 'vip' }) }));拦截并继续到真实网络,只记录请求:
await page.route('**/api/order', route => { console.log(route.request().url()); route.continue(); });拦截请求,把 POST 数据替换掉再放行(适合修改表单请求验证边界):
await page.route('**/api/submit', async route => { const postData = JSON.parse(route.request().post_data() || '{}'); postData.amount = -1; await route.continue({ postData: JSON.stringify(postData) }); });这种“改请求参数再放行”的玩法,用来测优惠金额为负数、数量超限这些边界条件,特别高效。你不需要费劲在 UI 上凑出非法状态,直接在请求层把数据改了。
还有一个实用场景:遇到慢接口导致前端 loading 转个不停,你可以故意让某个请求延迟返回,验证前端是否有超时兜底和异常提示。在 mock 数据时加一个
delay参数,就能模拟各种网络抖动。5. 测试用例组织与断言设计
5.1 fixture 与用例隔离
测试用例最怕的就是互相影响:这个用例登录了,那个用例没登录,跑起来全乱了。Playwright 里的 fixture(夹具)机制就是解决这个问题的。你可以把“启动浏览器、创建上下文、登录系统、打开首页”这些公共步骤放进 fixture,每个用例拿到一个干净的环境。
Python 项目的
pytest-playwright写法:@pytest.fixture def logged_in_page(page): page.goto("https://admin.example.com/login") page.get_by_label("用户名").fill("tester") page.get_by_label("密码").fill("password") page.get_by_role("button", name="登录").click() page.wait_for_url("**/dashboard") return page用例只需要声明
logged_in_page参数,pytest 就会自动执行这套登录流程。这里要记住:fixture 是函数级就函数级,是模块级就模块级,不要所有 fixture 都搞成 session 级共享浏览器状态,否则用例之间就有了隐式依赖。我踩过一个真实的教训:当时为了省时间,fixture 里登录一次,所有用例共用同一个页面对象,结果某个用例把用户信息改成了测试数据,后面所有依赖原数据的用例全挂了。从那以后我严格要求用例之间“无共享可变状态”,宁可多花几秒登录,也不要排查一晚上的脏数据问题。
5.2 合理使用断言与等待
Playwright 的断言也是带自动重试的,这一点和很多测试框架不一样。比如
expect(locator).to_have_text("已支付"),它不是立即判断,而是一遍一遍地去检查,默认 5 秒内满足就算通过。这和sleep完全不同,它把“等待”内置到了断言里,所以脚本会尽量快地通过,又不怕慢渲染。写断言的一个原则是:断言业务结果,而不是断言过程。举个例子,提交订单后不要断言“按钮文字变成了提交中”,而是等请求完成、页面跳转后,断言“订单列表里出现了一条新纪录”。后者才是业务真正成功的结果,前者只是 UI 的中间态。
另一个容易忽视的点:文本断言的顺序和正则匹配。
to_have_text(/订单号:\d{6}/)比to_have_text("订单号:123456")更抗变动,因为你只关心格式,不关心具体值。当你测的是一串动态生成的流水号时,正则断言是唯一解。5.3 并行执行与资源控制
Playwright 的并行能力很强,但并行不是免费午餐。我在 CI 上遇到过浏览器进程把内存打满,导致 OOM 的情况。控制并发的最直接手段是设置 workers。Node 项目在
playwright.config.ts里:export default defineConfig({ workers: process.env.CI ? 2 : undefined, });Python 项目在 pytest 命令行里加
-n 2(需要安装pytest-xdist)。这里有个很容易踩的坑:并行跑用例时,浏览器上下文之间要彻底隔离,不要共享存储状态。Playwright 的
browser.new_context()每次都会创建一个完全独立的浏览器环境,这本身就很好,但你千万别手动在 context 之间共享什么全局变量。我见过有人把 token 存在 Python 模块级变量里,然后多条用例并行时互相覆盖 token,测试结果随机失败。正确的做法是:把 token 写进当前 context 的 storage_state,或者每次登录都重新获取。并行还有一个隐藏收益:它能暴露出你代码里的“时序依赖”。如果那些用例串行跑没问题、并行跑就失败,那几乎可以断定用例之间存在隐藏的共享状态,这时候早发现问题反而是好事。
6. 常见问题与排查技巧实录
6.1 npx playwright install 慢与离线方案
前面讲环境的时候提过这个坑,但很多读者都问细节,这里再详细展开一下。
playwright install慢的根源是它要从微软的 CDN 下载浏览器压缩包,单个 Chromium 大约 150MB 左右,国内网络直连经常龟速。最省事的方案是设置镜像:# Linux / Mac export PLAYWRIGHT_DOWNLOAD_HOST=https://npmmirror.com/mirrors/playwright npx playwright install chromiumPython 项目也可以设环境变量:
export PLAYWRIGHT_DOWNLOAD_HOST=https://npmmirror.com/mirrors/playwright playwright install chromium如果你遇到的是“服务器完全离线”的场景,那就按我在 2.1 节说的离线拷贝方案:在联网机器上
playwright install,然后把整个缓存目录打包。但要注意,打包之前确认两边的 playwright 版本一致,因为版本变了浏览器版本也会变。还有一个小技巧,如果你只需要跑 UI 测试,其实没必要装三套浏览器,只装 Chromium 就够:
npx playwright install chromium --with-deps--with-deps会顺手把 Linux 上浏览器运行所需的系统依赖都装好,这个参数在干净的 CI 镜像上尤其重要。6.2 定位不到元素:先怀疑 frame 和 shadow DOM
“选择器明明没问题,但就是找不到元素”,这是测试群里日经级别的问题。我的排查顺序是这样的:先在代码里打印当前页面的
url和title,确认没跑错页面;打开 Trace 看失败瞬间的截图,确认页面长什么样;然后打开浏览器的开发者工具,按Ctrl+F在 Elements 面板里试一下你的选择器能不能匹配到。如果 DOM 里确实有这个元素,但 Playwright 就是拿不到,90% 的情况是有 iframe。处理方式前面说过了,用
frame_locator。剩下的 10% 大多是Shadow DOM。很多现代前端组件(比如某些 Web Components)把元素藏在#shadow-root里,普通的 CSS 选择器进入不了 shadow 内部。Playwright 对 shadow DOM 有支持,但定位依然是个技术活,尤其是嵌套的 open shadow root,你要用穿透查找的方式,或者直接在page.evaluate里操作 shadow root 的元素。另外,动态 id 的问题也经常被忽略。有些前端框架会给元素生成随机 id,比如
id="u_513f8a2d",每次刷新都变。你要是用这种 id 写选择器,下一次跑用例必挂。这种场景请务必改用文本、角色或者>await page.goto('https://example.com', { wait_until: 'domcontentloaded', timeout: 60000 });不用等所有图片和 CSS 加载完,只要 DOM 解析完成就可以开始操作了,这对慢页面提速非常明显。不过要注意,某些页面的功能依赖 window 的
load事件,改成domcontentloaded后可能出现事件还没绑定好就操作失败的情况,需要你根据场景权衡。如果
click超时,先看是不是元素被遮挡。Playwright 的报错信息会直接告诉你“element is not stable”或者“element intercepted by another element”。被遮挡最常见的元凶是 position 为 fixed 的悬浮窗、底部弹窗、cookie 同意条。这时候不要让脚本去硬点,先把遮挡元素关掉,或者用locator.click({ force: true })跳过可操作性检查。但我要提醒你,force: true是最后手段,它相当于告诉 Playwright “别检查了,给我点”,如果点不到,脚本会直接抛异常,不会重试。如果整个脚本莫名其妙挂了几分钟才超时,而每一步单独看都很快,那大概率是有隐藏的对话框或者无限循环的动画。我曾经排查过一个 case:页面里有个
setInterval的动画一直改变元素位置,导致 Playwright 稳定性检查永远不通过。这类问题就要靠 trace 的 timeline 分析,看那段时间页面到底发生了什么。6.4 面对复杂防护策略的测试调优
最后说一个很多人关心的话题:遇到上了比较严的防护策略的网站(指纹检测、JS 挑战、行为分析之类的),测试脚本老是失败怎么办。注意,我这里说的是在有授权的测试环境中,想办法让你的自动化测试更稳定,不是教你去突破未授权系统的防护。
真实世界里的情况是,很多内部系统虽然挂了防护,但测试环境并不需要 100% 模拟真实用户。我会从这几个维度做调优:第一,固定浏览器指纹参数,比如
viewport、user_agent、locale,确保每次运行时环境一致,避免因为默认 UA 不一致被识别。第二,降低操作速度和频次,把click、type之间加一点随机间隔,模拟人工操作节奏,但只加短延迟,不要无脑 sleep。第三,合理使用 Playwright 的context.storage_state保持登录态,减少重复登录触发的风控。这几步做下来,大多数测试环境的稳定性都能提升不少。需要强调的是,测试代码的首要目标是确定性和可重复,而不是“隐蔽性”。防护越严格的场景,更应该靠测试后门、白名单、专用测试账号来解决,而不是在自动化脚本这个层面反复对抗。
我个人这几年最深的体会是:Playwright 解决了“怎么稳定操作浏览器”的问题,但测试稳定性的最终决定因素,依然是用例设计是否合理、断言是否抓得住业务本质、fixture 隔离是否干净。工具降低了门槛,但也放大了设计的好坏。如果你今天刚开始接触 Playwright,先把自动等待机制和选择器优先级吃透,然后把 trace 和截图配置好,再慢慢往复杂场景扩展。这样一步步来,比追求跑通所有花哨功能要扎实得多。
最后再送一个小技巧:调试用例时,不要只会
--debug单步走。试试在 Python 代码里加入page.pause(),它在有头模式下会进入 Playwright Inspector,你可以在那个界面里实时看到当前选择器匹配了哪个元素,也能手动跳转下一步。这个工具用熟了,排查定位问题的速度能快一倍。