Playwright自动化测试实战:从自动等待到AI驱动浏览器的完整指南
发布时间:2026/9/28 7:46:09 作者:尧图编辑部 阅读量:1,286

你是不是也在写自动化测试的时候被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 ProtocolCDP和浏览器通信浏览器启动、页面渲染、事件监听全都在一个进程内完成。这意味着 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 playwrightlatest它会帮你创建测试目录、配置文件和一个示例用例。Python 项目用pip install pytest-playwright然后在项目根目录执行playwright install来下载浏览器。这里有个最常见的坑playwright install下载的是 Chromium、Firefox、WebKit 三套浏览器体积非常大在国内网络环境下经常慢到令人崩溃。解决思路有两条。第一设置国内镜像环境变量。在 Linux/Mac 上执行export PLAYWRIGHT_DOWNLOAD_HOSThttps://npmmirror.com/mirrors/playwright playwright install chromium第二如果你所在的服务器完全无法访问外网那就走离线安装。找一台能联网的机器先playwright install chromium然后把浏览器缓存目录整个打包拷过去。缓存目录默认是~/Library/Caches/ms-playwrightMac或~/.cache/ms-playwrightLinux也可以用环境变量PLAYWRIGHT_BROWSERS_PATH指定到你想要的位置。拷贝到目标机器后同样把PLAYWRIGHT_BROWSERS_PATH指向解压目录脚本就能直接找到浏览器了。注意不同版本的 Playwright 对应的浏览器版本不同离线拷贝时一定要保证目标机器上的playwrightnpm 包或 Python 包的版本和下载浏览器时的版本一致否则会报“Executable doesnt 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 --tracingon-first-retry --videoretain-on-failure --screenshotonly-on-failure截图、视频、trace 三个一起配置失败了就能拿到现场证据。线上问题排查的时候这个配置救命的频率非常高。2.4 Playwright MCP 和 Chrome DevTools MCP 到底有什么区别最近 AI 圈子里 MCPModel 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 的解决方式是frameLocatorconst 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[typefile]).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_HOSThttps://npmmirror.com/mirrors/playwright npx playwright install chromiumPython 项目也可以设环境变量export PLAYWRIGHT_DOWNLOAD_HOSThttps://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 看失败瞬间的截图确认页面长什么样然后打开浏览器的开发者工具按CtrlF在 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比如idu_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你可以在那个界面里实时看到当前选择器匹配了哪个元素也能手动跳转下一步。这个工具用熟了排查定位问题的速度能快一倍。