Playwright 实战:自动等待、拦截、Trace 与并行¶
Playwright 减少 flaky 的核心不是“更快的浏览器”,而是动作前做可操作性检查。理解这套机制,才能判断什么时候该等、什么时候是页面真的坏了。
1. 安装与 CI 依赖¶
CI 里直接用官方镜像,省掉系统依赖:
版本要与 pip install playwright 的版本一致,否则报:
2. 自动等待到底等什么¶
每次 click() / fill() 前,Playwright 对目标元素做 actionability 检查:
默认超时 30s(expect 默认 5s)。这意味着:
- 大多数场景不需要手写
sleep - 但“元素存在”不等于“业务数据已返回”,接口数据渲染仍需等断言
# 等元素出现
page.get_by_role("button", name="提交").click()
# 等业务状态(推荐:用 expect 轮询)
from playwright.sync_api import expect
expect(page.get_by_test_id("order-status")).to_have_text("待支付")
不要用 page.wait_for_timeout(3000) 代替断言。固定等待在慢环境仍会挂,在快环境纯浪费。
3. 定位器:Role 优先,testid 兜底¶
page.get_by_role("button", name="提交订单").click()
page.get_by_label("收货地址").fill("北京市海淀区")
page.get_by_placeholder("请输入手机号").fill("13800000000")
page.get_by_test_id("pay-now").click()
严格模式报错:
这不是 bug,是提示你定位不够精确。用 .nth(0) 或更具体的容器限定:
4. 登录态复用:只登录一次¶
# conftest.py
import pytest
from playwright.sync_api import Browser, Page
@pytest.fixture(scope="session")
def auth_state(browser: Browser, tmp_path_factory):
state = tmp_path_factory.mktemp("auth") / "state.json"
ctx = browser.new_context()
page = ctx.new_page()
page.goto(f"{BASE_URL}/login")
page.get_by_label("用户名").fill(USER)
page.get_by_label("密码").fill(PWD)
page.get_by_role("button", name="登录").click()
page.wait_for_url("**/dashboard")
ctx.storage_state(path=str(state))
ctx.close()
return str(state)
@pytest.fixture
def page(browser: Browser, auth_state):
ctx = browser.new_context(storage_state=auth_state)
yield ctx.new_page()
ctx.close()
价值:省掉每条用例 3-5 秒登录,并把“登录”这个不稳定点从所有用例里移走。注意 token 过期时间长于套件执行时间,否则中途集体 401。
5. 网络拦截:Mock 与故障注入¶
# 返回固定数据
page.route("**/api/orders", lambda route: route.fulfill(
status=200,
content_type="application/json",
body='{"code":0,"data":[{"orderId":1,"status":"PAID"}]}'))
# 模拟 500
page.route("**/api/coupons", lambda route: route.fulfill(status=500, body="boom"))
# 模拟超时/断网
page.route("**/api/recommend", lambda route: route.abort("timedout"))
# 改写真实响应
def patch(route):
resp = route.fetch()
data = resp.json()
data["data"]["amount"] = "0.01"
route.fulfill(response=resp, json=data)
page.route("**/api/orders/*", patch)
常见错误:先点击后注册 route。请求已经发出,拦截不到。正确顺序是先 route,再触发动作。
6. 等接口:expect_response 的时序¶
with page.expect_response("**/api/orders") as info:
page.get_by_role("button", name="提交").click()
resp = info.value
assert resp.status == 200
body = resp.json()
assert body["code"] == 0
expect_response 必须在触发动作之前进入上下文,否则会漏掉响应。
如果接口是轮询(每 2 秒一次),用 expect.poll 等业务状态更稳:
from playwright.sync_api import expect
expect.poll(lambda: page.request.get(f"{API}/orders/1").json()["data"]["status"],
timeout=30_000).to_equal("PAID")
7. Trace:失败复盘的核心¶
# 或手动控制
context.tracing.start(screenshots=True, snapshots=True, sources=True)
...
context.tracing.stop(path="trace.zip")
Trace 里能直接看到:每步操作前后的 DOM 快照、网络请求与响应、console 日志、源码位置。定位“为什么点不到”比截屏高效得多。
配置参考:
# pytest.ini
[pytest]
addopts =
--tracing=retain-on-failure
--screenshot=only-on-failure
--video=retain-on-failure
--output=test-results
8. 并行与分片¶
# 本地并行
pytest -n 4
# CI 分片:3 台机器各跑 1/3
# 用 pytest + pytest-playwright 时,pytest 没有 --shard,需装 pytest-split
pip install pytest-split
pytest --splits 3 --group 1
pytest --splits 3 --group 2
pytest --splits 3 --group 3
# 若直接用 Playwright 自带的 test runner(npx playwright test)
npx playwright test --shard=1/3
分片默认按用例数切,可能某片更重。pytest-split 可以先用 --store-durations 记录耗时,下次按 durations 文件均衡分配;Playwright runner 的 --shard 只按用例数均分,不读历史耗时,想均衡要靠自己拆分或调整用例分布。
并行注意:
- 每个 worker 独立
browser_context,不要共享 page - 测试数据按
worker_id隔离(同 pytest-xdist) - 视频与 trace 会显著增加磁盘 IO,CI 上只保留失败
9. 时区、语言与移动端¶
ctx = browser.new_context(
locale="zh-CN",
timezone_id="Asia/Shanghai",
viewport={"width": 1440, "height": 900},
user_agent="Mozilla/5.0 ...",
)
移动端:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
iphone = p.devices["iPhone 13"]
browser = p.webkit.launch()
ctx = browser.new_context(**iphone)
时区不设置时,CI 默认 UTC,涉及“当天”“本月”的用例会跨天失败——这是很常见的跨时区假失败。
10. 与 Selenium 的取舍¶
| 维度 | Selenium | Playwright |
|---|---|---|
| 旧版浏览器 | 支持 | 不支持 IE |
| 自动等待 | 需封装 | 内置 |
| 网络 Mock | 需代理工具 | 原生 |
| 调试 | 截图/日志 | Trace 全链路 |
| 多用户并发 | 多 driver | 多 context,更轻 |
| 存量项目 | 已投入大 | 迁移成本高 |
新项目优先 Playwright;需要兼容旧浏览器或已有 Selenium 体系时,不必强迁。
11. 验收¶
□ 同一用例连跑 10 次全绿
□ 断网/接口 500 场景能被 Mock 稳定复现
□ 失败构建自动产出 trace,可直接 show-trace
□ 登录只执行一次(看耗时分布)
□ 4 并行下无账号/数据冲突
□ CI 时区固定为 Asia/Shanghai