做 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 相当于一个独立用户环境,互不干扰。这在多账号、多场景并行测试时非常有用。
下面是两个框架的直观对比:
| 对比维度 | Selenium | Playwright |
|---|---|---|
| 驱动管理 | 手动下载,需匹配浏览器版本 | 安装时自动下载,内置驱动 |
| 自动等待 | 主要靠显式/隐式等待 | 默认自动等待,失败可重试 |
| 多标签处理 | 需要切换 window handle | Context 直接管理多个 Page |
| iframe 处理 | 需要 switch_to 切换 | frame_locator 链式定位 |
| 脚本录制 | IDE 工具相对独立 | 内置 codegen,直接生成定位器和代码 |
| 网络拦截 | 依赖第三方代理或插件 | 原生支持 route 和 request/response 事件 |
| 截图与视频 | 需要额外配置 | 内置截图、录屏、trace 录制 |
这张表格列到第 7 行就已经足够说明问题:不是“Selenium 不能做”,而是“每个功能做起来都更费劲”。自动化测试真正贵的地方不是写脚本的时间,而是脚本跑挂了以后人去排查的时间。Playwright 在帮助开发者少踩坑这件事上,设计思路是明确的。
2. 环境准备与安装
Playwright 支持 Python、JavaScript/TypeScript、Java 和 .NET。下面以 Python 为例,但核心概念在所有语言版本里都是一致的。
2.1 安装 Python 依赖
建议使用虚拟环境,避免污染系统 Python:
mkdir 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 自动化,可以先只装 Chromium:
playwright install chromium2.2 离线安装场景
部分公司内网环境无法直接访问外网,这时候有两种思路。
第一种是在可以联网的机器上先执行playwright install,然后把浏览器缓存目录整体打包拷到内网。Linux 和 macOS 下默认路径是~/Library/Caches/ms-playwright,Windows 下在%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(headless=True) 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_text="iPhone 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(headless=False) 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(state="visible") print("登录成功,当前 URL:", page.url) browser.close() if __name__ == "__main__": run()这个脚本的关键点在于wait_for(state="visible")。它不会像sleep(5)那样固定等待,而是每隔一段时间检查一次目标元素是否可见,既稳定又高效。
如果需要处理下拉框、多选框、文件上传,Playwright 也提供了比较直接的方式:
# 下拉框选择 page.select_option("#city", label="杭州") # 勾选复选框 page.get_by_label("我已阅读并同意协议").check() # 上传文件 page.set_input_files('input[type="file"]', "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(timeout=10000) # 断言输入框的值 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( status=200, content_type="application/json", body='{"data": "mocked"}' ) page.route("**/api/promotion", mock_response) page.goto("https://your-test-site.com/home")这样在测试环境里,不需要后端真正提供促销接口,也能验证前端在接口返回特定数据时的展示逻辑。
7.3 截图与录屏
截图在排查 CI 失败时非常有用:
page.screenshot(path="screenshots/login_success.png", full_page=True)如果想要记录完整操作过程,可以使用 Context 的 record_video 参数:
context = browser.new_context( record_video_dir="videos/", 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(headless=False) context = browser.new_context(**iphone) page = context.new_page() page.goto("https://your-mobile-site.com") page.screenshot(path="screenshots/mobile_home.png") browser.close()p.devices["iPhone 13"]是一组预设参数,包括浏览器内核、viewport 尺寸、触摸事件等。这比手动配置 user agent 和窗口大小要省事得多。
8. 运行结果与验证方式
脚本写完以后,怎么确认它真的正常?很多人直接跑一遍 Ctrl+C 看到不报错就结束了,但自动化测试的价值在于“每一次运行都能给出可信结果”。
8.1 命令行运行
如果是简单的 Python 脚本,直接运行:
python login_demo.py预期结果是终端打印“登录成功,当前 URL: ...”,并且浏览器窗口完成登录跳转。如果希望无界面运行,把 launch 参数改为:
browser = p.chromium.launch(headless=True)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(state="visible") assert "/profile" in page.url不用显式启动浏览器,pytest-playwright 插件会在用例执行前自动创建 fixture。
运行:
pytest test_login.py -v8.3 Trace 查看器
脚本跑挂了,第一步不是瞎猜,而是打开 trace 查看器。在启动浏览器时加上录制参数:
context = browser.new_context( record_video_dir="videos/" )但如果想要更完整的操作历史,包括每个步骤的 DOM 快照和网络请求,可以用 trace:
context = browser.new_context() context.tracing.start(screenshots=True, snapshots=True) # 执行业务操作 page.goto("https://your-test-site.com/login") page.get_by_placeholder("用户名").fill("demo_user") # 结束录制并保存 trace 文件 context.tracing.stop(path="trace.zip")然后查看:
playwright show-trace trace.zip打开 trace 查看器后,你可以逐步回放每个操作:点击了哪里、页面上元素处于什么状态、网络请求什么时候发出。排查“脚本在我这跑得好好的,为什么 CI 上失败”这类问题时,trace 的作用甚至比日志更大。
8.4 判断成功的标准
自动化用例通过不只看最后没有抛异常,还应该包含明确断言。至少要验证:
- 关键页面元素是否出现。
- 页面 URL 是否符合预期。
- 关键接口是否被请求,返回是否正常。
- 页面关键文本是否为预期值。
没有断言的自动化脚本,运行绿了也只是“没报错”,不能代表业务真的正确。
9. 常见问题与排查思路
下面整理的是 Playwright 使用过程中最高频的几个问题。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
启动时提示Executable doesn't 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(path="state.json")后续用例加载这个文件即可跳过重复登录:
context = browser.new_context(storage_state="state.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(),少踩一次驱动不匹配的坑,那这一篇就没有白写。