
做 Web 自动化测试的同学多少都经历过这样的场景本地明明跑得好好的 Selenium 脚本换一台机器就报chromedriver版本不匹配页面多了一个弹窗脚本就开始乱点处理多标签页要手动切 handle切来切去把自己绕晕。这些问题不是你不会写而是工具本身把大量成本留给了开发者。Playwright 正是冲着这些历史遗留问题来的。它由微软团队维护最初服务于 Chromium 自动化测试后来扩展成支持 Chromium、Firefox、WebKit 三大内核的统一自动化框架。它把浏览器驱动内置到了安装包里提供自动等待、多页面上下文隔离、录制脚本、网络拦截等能力让 Web 自动化的开发和维护成本明显下降。这篇文章不打算做泛泛的框架介绍而是从 Selenium 迁移者的视角出发把 Playwright 真正核心的用法、最容易踩的坑、可落地的工程化思路讲透。读完你会明白为什么不少团队在把 Web 自动化往 Playwright 上迁移以及迁移之后脚本的稳定度能提升多少。文章包含可直接运行的代码示例、录屏调试方法和一套常见问题排查表建议先收藏再阅读。1. 为什么 Selenium 用户会考虑迁移先给一个判断Selenium 并不会一夜之间消失大量存量项目还在用它维护这是事实。但新项目、新团队、新用例越来越多地选择 Playwright这也是事实。迁移的驱动力不是“新框架更时髦”而是 Selenium 在几个核心场景上确实长期没有解决好。第一个痛点是驱动管理。Selenium 需要你手动下载 chromedriver、匹配浏览器版本还要考虑环境变量。本地能用CI 机器上又容易出问题。Playwright 的安装命令会把对应浏览器内核一起下载启动时自动匹配不需要手工管理驱动这一条就省掉了很多维护成本。第二个痛点是自动等待。Selenium 里time.sleep()是新手最爱也是脚本不稳的直接原因之一。隐式等待解决了一部分问题但对“元素出现了但不可点击”“元素被遮挡”“异步渲染慢半拍”这类场景仍然乏力。Playwright 默认对每个操作执行自动重试和可见性判断大多数情况下你不需要自己写 sleep。第三个痛点是多页面和上下文隔离。Selenium 处理新标签页要切 window handle处理登录态复杂项目时经常互相串。Playwright 用 Context 概念把浏览器会话隔离起来每个 Context 相当于一个独立用户环境互不干扰。这在多账号、多场景并行测试时非常有用。下面是两个框架的直观对比对比维度SeleniumPlaywright驱动管理手动下载需匹配浏览器版本安装时自动下载内置驱动自动等待主要靠显式/隐式等待默认自动等待失败可重试多标签处理需要切换 window handleContext 直接管理多个 Pageiframe 处理需要 switch_to 切换frame_locator 链式定位脚本录制IDE 工具相对独立内置 codegen直接生成定位器和代码网络拦截依赖第三方代理或插件原生支持 route 和 request/response 事件截图与视频需要额外配置内置截图、录屏、trace 录制这张表格列到第 7 行就已经足够说明问题不是“Selenium 不能做”而是“每个功能做起来都更费劲”。自动化测试真正贵的地方不是写脚本的时间而是脚本跑挂了以后人去排查的时间。Playwright 在帮助开发者少踩坑这件事上设计思路是明确的。2. 环境准备与安装Playwright 支持 Python、JavaScript/TypeScript、Java 和 .NET。下面以 Python 为例但核心概念在所有语言版本里都是一致的。2.1 安装 Python 依赖建议使用虚拟环境避免污染系统 Pythonmkdir playwright-demo cd playwright-demo python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate pip install playwright安装完成后执行playwright install这一步会下载 Chromium、Firefox、WebKit 三个内核的可执行文件。如果只是做 Web 自动化可以先只装 Chromiumplaywright install chromium2.2 离线安装场景部分公司内网环境无法直接访问外网这时候有两种思路。第一种是在可以联网的机器上先执行playwright install然后把浏览器缓存目录整体打包拷到内网。Linux 和 macOS 下默认路径是~/Library/Caches/ms-playwrightWindows 下在%USERPROFILE%\AppData\Local\ms-playwright。设置环境变量PLAYWRIGHT_BROWSERS_PATH可以自定义存放位置。第二种是先下载 zip 包再离线安装。Playwright 提供了对应版本的浏览器下载链接通常格式为https://playwright.azureedge.net/builds/chromium/版本号/chromium-linux.zip将下载好的 zip 解压到指定目录再设置PLAYWRIGHT_BROWSERS_PATH环境变量指向该目录即可。2.3 Linux 系统依赖问题在 CentOS 或 Ubuntu 服务器上跑 Playwright经常会遇到缺少系统库的问题。一个常见处理方式是安装系统级依赖playwright install-deps chromium这条命令会调用系统的包管理器安装 Chromium 所需的共享库。如果公司安全策略不允许执行该命令可以手动安装libnss3、libatk等基础依赖但更稳妥的做法还是用官方命令解决。3. 核心概念Browser、Context、Page 三层结构Playwright 有一个非常清晰的三层对象模型理解它之后写脚本的思路会顺畅很多。Browser 是浏览器进程等价于你打开了一个 Chrome 应用。Context 是浏览器上下文等价于 Chrome 里的一个“用户会话”。不同 Context 之间 cookies、localStorage、缓存完全隔离。这就是为什么 Playwright 可以轻松实现多账号并行每个账号一个 Context互不干扰。Page 则是 Context 里的一个标签页所有 UI 操作都发生在 Page 上。用代码解释这三层from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch(headlessTrue) context_a browser.new_context() context_b browser.new_context() page_a context_a.new_page() page_b context_b.new_page() page_a.goto(https://example.com) page_b.goto(https://example.com) browser.close()这个设计对自动化测试最大的好处是不需要像 Selenium 那样反复切换 window handle也不会因为两个用例共享浏览器状态而互相干扰。在实际项目中建议每个测试用例创建一个新的 Context测试结束直接关闭 Context等价于自动清理所有状态。4. 使用 Codegen 快速录制脚本很多读者一开始接触 Playwright 是被它的录制功能吸引的。确实Playwright 的 codegen 比传统录制工具更进一步它生成的是标准的、可维护的定位器代码而不是一串坐标点。启动录制playwright codegen https://example.com命令执行后会打开一个浏览器窗口和一个 Playwright Inspector 面板。你在浏览器里做的点击、输入、选择操作都会自动转换成代码。如果点击启动录制后在页面上操作了某个输入框并输入文本Inspector 会生成类似下面的代码page.get_by_placeholder(请输入用户名).click() page.get_by_placeholder(请输入用户名).fill(testuser)注意它默认生成的是基于可访问属性的定位器比如get_by_role、get_by_label、get_by_placeholder而不是脆弱的 CSS 绝对路径。这是 Playwright 官方推荐的定位方式因为它更接近用户查看页面的方式按钮叫什么角色、输入框的 label 是什么、占位符文本是什么。Codegen 的适用场景包括快速梳理业务流程生成脚本雏形然后再人工完善。排查页面元素定位问题时通过点击获取精确的定位器表达式。在团队内部做用例评审时录一段操作流程作为可视化参考。需要强调的是录制只是起点。好的自动化脚本还必须处理异常分支、等待条件和断言录完直接扔到 CI 里的做法是不建议的。5. 核心选择器与完整交互示例Playwright 提供了多套定位方式正式项目里最常用的是下面几种。5.1 get_by_role按角色定位例如按钮、链接、文本框page.get_by_role(button, name登录).click() page.get_by_role(link, name注册).click()5.2 get_by_label 与 get_by_placeholder表单场景最方便page.get_by_label(用户名).fill(testuser) page.get_by_placeholder(请输入密码).fill(123456)5.3 get_by_text根据文本内容定位page.get_by_text(退款成功).click()5.4 locator 链式定位先定位到某个容器再在容器内部查找元素适合复杂页面product_card page.locator(.product-item).filter(has_textiPhone 15) product_card.get_by_role(button, name加入购物车).click()5.5 一个完整的最小登录场景写一个可以跑通的完整脚本它模拟了打开登录页、填写表单、点击登录、等待跳转的完整流程# login_demo.py from playwright.sync_api import sync_playwright def run(): with sync_playwright() as p: browser p.chromium.launch(headlessFalse) page browser.new_page() # 访问页面这里替换成你的测试地址 page.goto(https://your-test-site.com/login) # 填写表单 page.get_by_placeholder(用户名).fill(demo_user) page.get_by_placeholder(密码).fill(demo_password) # 点击登录 page.get_by_role(button, name登 录).click() # 等待页面出现“欢迎”文本自动重试 page.get_by_text(欢迎回来).wait_for(statevisible) print(登录成功当前 URL:, page.url) browser.close() if __name__ __main__: run()这个脚本的关键点在于wait_for(statevisible)。它不会像sleep(5)那样固定等待而是每隔一段时间检查一次目标元素是否可见既稳定又高效。如果需要处理下拉框、多选框、文件上传Playwright 也提供了比较直接的方式# 下拉框选择 page.select_option(#city, label杭州) # 勾选复选框 page.get_by_label(我已阅读并同意协议).check() # 上传文件 page.set_input_files(input[typefile], test_data.csv)如果你是在 Java 项目中使用 Playwright思路是相同的只是 API 变成链式调用Page page browser.newPage(); page.navigate(https://your-test-site.com/login); page.getByPlaceholder(用户名).fill(demo_user); page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName(登录)).click(); page.getByText(欢迎回来).waitFor();6. 动态页面、iframe 和等待机制动态页面是 Web 自动化的主要难点。页面组件是异步渲染的脚本执行快了找不到元素执行慢了浪费时间。Playwright 的做法是“智能等待”它会在执行点击、填写、获取文本等操作前自动判断目标元素是否可被交互如果暂时不可用就重试直到超时。这种机制已经很好地解决了大部分场景。但如果你需要精确控制等待逻辑可以显式使用wait_for_selector或expect轮询。其中expect是 Playwright 官方推荐的轮询方式from playwright.sync_api import expect # 断言元素可见最多等待 10 秒 expect(page.get_by_text(订单支付成功)).to_be_visible(timeout10000) # 断言输入框的值 expect(page.get_by_placeholder(搜索)).to_have_value(Playwright)iframe 场景在 Selenium 里需要driver.switch_to.frame()在 Playwright 中直接使用 frame_locator# 先定位 iframe再在 iframe 内部查找元素 frame page.frame_locator(#main-iframe) frame.get_by_placeholder(请输入内容).fill(hello) frame.get_by_role(button, name提交).click()frame_locator 返回的对象支持普通 page 上可用的绝大多数定位方法你可以把它当作一个局部 page 来理解。这比 Selenium 的上下文切换直观很多写代码的时候大脑不需要记住“现在在哪一个 frame 里”。处理多层嵌套 iframe 时frame_locator 也支持链式调用inner_frame page.frame_locator(#outer-iframe).frame_locator(#inner-iframe) inner_frame.get_by_text(确认).click()关于等待的另一类场景是页面加载时发送了大量请求需要等待网络空闲。Playwright 有page.wait_for_load_state(networkidle)但要注意这个状态在某些长期轮询页面上可能永远等不到。更稳妥的方法是等待业务结果元素出现而不是等待网络状态。7. 网络拦截、截图、下载与移动端模拟Playwright 的优势不仅仅是 UI 操作它对网络层的控制能力也非常强。这在模拟弱网环境、提取接口数据、手动 mock 接口时特别有用。7.1 监听并读取接口响应有时候脚本需要校验前端是否展示了正确的接口数据。可以注册一个 response 监听器def handle_response(response): if /api/user/info in response.url: data response.json() print(用户昵称:, data.get(nickname)) page.on(response, handle_response) page.goto(https://your-test-site.com/user)这段代码不会中断页面请求只是在响应返回时读取数据。适合做接口与 UI 联动断言。7.2 拦截并修改请求在某些测试场景下我们希望前端请求指向一个 mock 地址或者直接返回固定数据。可以用 route 方法def mock_response(route): route.fulfill( status200, content_typeapplication/json, body{data: mocked} ) page.route(**/api/promotion, mock_response) page.goto(https://your-test-site.com/home)这样在测试环境里不需要后端真正提供促销接口也能验证前端在接口返回特定数据时的展示逻辑。7.3 截图与录屏截图在排查 CI 失败时非常有用page.screenshot(pathscreenshots/login_success.png, full_pageTrue)如果想要记录完整操作过程可以使用 Context 的 record_video 参数context browser.new_context( record_video_dirvideos/, record_video_size{width: 1280, height: 720} )测试结束后在videos/目录下会生成对应录制文件方便回看真实操作过程。7.4 文件下载处理处理下载不需要手工管理临时目录with page.expect_download() as download_info: page.get_by_role(button, name导出报表).click() download download_info.value download.save_as(reports/export.xlsx)如果页面点击后弹出了系统级下载框这个写法也能捕获因为 Playwright 对下载事件做了统一管理。7.5 移动端浏览器模拟Playwright 内置了很多设备的 user agent 和 viewport 配置可以让桌面浏览器模拟出手机浏览器效果from playwright.sync_api import sync_playwright with sync_playwright() as p: iphone p.devices[iPhone 13] browser p.chromium.launch(headlessFalse) context browser.new_context(**iphone) page context.new_page() page.goto(https://your-mobile-site.com) page.screenshot(pathscreenshots/mobile_home.png) browser.close()p.devices[iPhone 13]是一组预设参数包括浏览器内核、viewport 尺寸、触摸事件等。这比手动配置 user agent 和窗口大小要省事得多。8. 运行结果与验证方式脚本写完以后怎么确认它真的正常很多人直接跑一遍 CtrlC 看到不报错就结束了但自动化测试的价值在于“每一次运行都能给出可信结果”。8.1 命令行运行如果是简单的 Python 脚本直接运行python login_demo.py预期结果是终端打印“登录成功当前 URL: ...”并且浏览器窗口完成登录跳转。如果希望无界面运行把 launch 参数改为browser p.chromium.launch(headlessTrue)8.2 是否应该使用 pytest单文件脚本适合学习和排查问题但工程化之后我更推荐用 pytest 配合 pytest-playwright 来组织用例。这样可以获得断言失败自动截图、用例级隔离、并发执行等能力。安装pip install pytest pytest-playwright一个最小测试用例# test_login.py from playwright.sync_api import Page def test_login_success(page: Page): page.goto(https://your-test-site.com/login) page.get_by_placeholder(用户名).fill(demo_user) page.get_by_placeholder(密码).fill(demo_password) page.get_by_role(button, name登录).click() page.get_by_text(欢迎回来).wait_for(statevisible) assert /profile in page.url不用显式启动浏览器pytest-playwright 插件会在用例执行前自动创建 fixture。运行pytest test_login.py -v8.3 Trace 查看器脚本跑挂了第一步不是瞎猜而是打开 trace 查看器。在启动浏览器时加上录制参数context browser.new_context( record_video_dirvideos/ )但如果想要更完整的操作历史包括每个步骤的 DOM 快照和网络请求可以用 tracecontext browser.new_context() context.tracing.start(screenshotsTrue, snapshotsTrue) # 执行业务操作 page.goto(https://your-test-site.com/login) page.get_by_placeholder(用户名).fill(demo_user) # 结束录制并保存 trace 文件 context.tracing.stop(pathtrace.zip)然后查看playwright show-trace trace.zip打开 trace 查看器后你可以逐步回放每个操作点击了哪里、页面上元素处于什么状态、网络请求什么时候发出。排查“脚本在我这跑得好好的为什么 CI 上失败”这类问题时trace 的作用甚至比日志更大。8.4 判断成功的标准自动化用例通过不只看最后没有抛异常还应该包含明确断言。至少要验证关键页面元素是否出现。页面 URL 是否符合预期。关键接口是否被请求返回是否正常。页面关键文本是否为预期值。没有断言的自动化脚本运行绿了也只是“没报错”不能代表业务真的正确。9. 常见问题与排查思路下面整理的是 Playwright 使用过程中最高频的几个问题。问题现象可能原因排查方式解决方案启动时提示Executable doesnt exist浏览器内核未安装或路径不对查看 PLAYWRIGHT_BROWSERS_PATH 环境变量执行 playwright install chromium或离线安装内核到正确目录运行时报target closed: target page, context or browser has been closed在某个 Page 或 Browser 关闭后继续操作了它检查代码中是否有显式 close或 pages 列表引用了过期 Page避免在 with 块结束后再操作 page多页面场景随时检查 context.pages 获取最新 Page元素定位不到报 timeout选择器不正确或元素处于不可见/遮挡状态打开 trace 查看该步骤的 DOM 快照改用 get_by_role、get_by_text 等接近用户视角的定位器iframe 内元素一直找不到frame_locator 名称不对或 iframe 动态加载使用 page.frames 查看当前页面所有 frame 的信息优先使用 frame_locator避免切换上下文Linux 下启动浏览器报缺少共享库系统缺少 Chromium 运行依赖查看缺少的 so 文件名称执行 playwright install-deps chromium点击按钮无效但页面没报错按钮被 loading 状态或遮罩覆盖截屏确认点击瞬间页面状态在点击前增加 expect(button).to_be_enabled() 或 wait_for页面一直停在等待 networkidle页面有常年轮询请求网络空闲不代表页面交互完成改用业务元素等待不要依赖 networkidle特别说一下target closed这是初学者最容易遇到也最容易被吓到的错误。它的含义很直白你在某个浏览器上下文或页面已经关闭之后还尝试调用它的方法。常见场景是在循环遍历多个标签页时不小心把某个 Page 关了但循环里还在使用它的引用。解决思路是每次操作前都从context.pages中重新获取有效 Page 对象或者为每个业务步骤单独创建新的 Page。10. 工程化最佳实践与团队协作建议10.1 用例分层不要把所有的步骤全部写在一个测试函数里。建议至少分出三层页面对象层封装页面元素定位和操作例如 LoginPage、OrderPage。业务流程层封装一组操作例如 “用户完成登录并进入个人中心”。测试用例层只描述场景和断言。这样做的好处是页面结构调整时只需要修改页面对象层测试用例层基本不动。10.2 用例隔离每个测试用例都创建新的 Context测试结束时关闭。不要尝试复用浏览器状态。即使两个用例访问同一个网站、使用同一个账号也应该各自新建 Context。这能避免由于前一个用例的残留状态导致后一个用例失败。10.3 谨慎处理登录态如果每个用例都重新走一遍登录流程执行时间会比较长。可以考虑在 fixture 中先登录一次然后保存 storage state 文件context browser.new_context() page context.new_page() # 执行登录操作 page.get_by_placeholder(用户名).fill(demo_user) page.get_by_placeholder(密码).fill(demo_password) page.get_by_role(button, name登录).click() # 保存登录状态 context.storage_state(pathstate.json)后续用例加载这个文件即可跳过重复登录context browser.new_context(storage_statestate.json)但要注意storage_state 保存的是 cookies 和本地存储如果产品做了短期会话失效机制这种方案需要配合定期更新 state 文件。10.4 稳定优先于速度不要在一开始就追求并行和提速。稳定跑通是第一位。盲目把用例并行起来反而可能引入资源竞争导致的假失败。建议先串行稳定再考虑 pytest-xdist 等并发方案。10.5 合理使用 mock 与网络拦截前端异常分支如接口超时、接口返回空数组很难通过真实环境构造。用 route 做 mock 是效率最高的方式。但需要注意mock 数据要尽量接近真实接口结构否则会出现测试环境通过、线上环境失败的情况。10.6 安全与合规提醒Web 自动化一定要在授权范围内使用。针对登录页加验证码、拖动拼图验证码这类安全机制自动化脚本不应该尝试绕过大多数平台都将其视为异常访问。更合理的做法是在测试环境通过测试凭证进入系统或者与平台方沟通获取白名单机制。相关的敏感点为本文不讨论绕过瑞数等风控服务的方案也不讨论各类所谓的“反检测”技巧。这类技术属于攻防对抗领域且常常违反平台用户协议存在合规风险。10.7 CI 集成在 CI 中运行 Playwright 用例时注意安装依赖和浏览器内核。建议在流水线中显式执行pip install -r requirements.txt playwright install --with-deps chromium--with-deps参数会同时安装系统和浏览器依赖减少 CI 环境缺库导致的问题。11. 总结与后续学习方向Playwright 真正解决的不是“有没有一个工具能点按钮”的问题而是把 Web 自动化里的不稳定因素一个一个收编驱动管理、等待机制、iframe 切换、多页面协作、网络控制、调试回放。这些能力叠加起来脚本写起来更顺手跑起来更稳排查起来更直观。这也是它能在短时间内获得大量关注的原因。如果你正准备迁移建议按三条路线推进先用 codegen 录制现有业务感受定位器生成方式再把公司里最痛的一个回归用例迁到 Playwright 上验证稳定度是否提升最后再考虑引入 pytest-playwright、CI 集成和并行执行。不要一上来就要求全量迁移先在一个用例上跑通闭环团队才会真正信任这个工具。后续值得深入的方向包括pytest-playwright 的 fixture 机制、浏览器并行测试的进程模型、Playwright 与 CI 平台结合时的高清截图和 trace 上传、移动端真机与模拟器的差异处理以及在微前端架构下的多应用容器定位方式。技术工具永远在迭代但“让自动化脚本更稳定、更可维护、更可追溯”这个目标不会变。如果你看完这篇文章能少写一个time.sleep()少踩一次驱动不匹配的坑那这一篇就没有白写。