Playwright Python 实战:一套代码跑通 Chromium、Firefox 和 WebKit 的完整手册
【免费下载链接】playwright-pythonPython version of the Playwright testing and automation library.项目地址: https://gitcode.com/GitHub_Trending/pl/playwright-python
你刚把 PR 合进 main,CI 日志里 Chromium 全绿,Firefox 却在同一条断言上挂掉,报 element is not visible。手动打开页面点一遍,什么异常都看不到。这种"只在某个引擎复现"的问题,靠肉眼排查基本无解。Playwright Python 解决的就是这类事:一套 Python API 同时驱动 Chromium、Firefox、WebKit 三个浏览器引擎,同一份测试脚本原样跑遍三端,行为差异直接暴露在日志里。
看懂支撑三引擎一致的三处机制
中间隔着一个 Node.js driver 进程
Python 进程并不直接控制浏览器。调用async_playwright()或sync_playwright()时,会先拉起一个随包分发的 Node.js 驱动子进程,两边通过 stdin/stdout 管道交换带长度前缀的 JSON 消息,这套逻辑在playwright/_impl/_transport.py的 PipeTransport 里可以逐行读到。浏览器协议本身的差异——Chromium 的 CDP、Firefox 和 WebKit 各自的私有通道——全部被 driver 消化掉,所以你在三个引擎上调用的page.click()是同一条代码路径。顺带的好处是少了一层 WebDriver 式的 HTTP 往返,延迟更低,引擎差异也被收敛到一个地方修复,不用你在测试里逐个打补丁。
每个动作都带 actionability 检查
locator.click()、fill()这类动作执行前,Playwright 会逐项确认:元素在 DOM 里、可见、几何位置稳定、能接收事件、没被禁用。任何一项不满足就内部重试,直到你给的 timeout 才抛错。直接效果是大部分手写 sleep 可以删掉,"偶发失败"的数量肉眼可见地变少。
同步 API 是异步底层的换皮
sync 版并不是另一套实现:它内部开了一个 greenlet 去跑 asyncio 事件循环,你的阻塞调用触发时把控制权交还给循环去收 driver 的消息。这就是为什么同步写法可以安心放在线程和 pytest 里,却不能放进已经运行的事件循环——这个限制踩坑部分还会再碰到一次。
这是仓库为每个引擎单独存放的基线截图之一:同一张网格测试页在 WebKit 下渲染出来的样子,后续做截图比较时拿它当参照。
从装环境到三引擎参数化
三条命令装好 Playwright Python
pip install playwright playwright install chromium playwright install firefox playwright install webkit第一条装 Python 包,里面已含 Node driver;后三条把浏览器二进制拉进本地缓存,只装你实际要跑的引擎就能省时间。CI 里不想每台机器折腾系统依赖的话,utils/docker/下按 Ubuntu 版本备好了 Dockerfile,拿来即用。
最小脚本确认三引擎都能 launch
from playwright.sync_api import sync_playwright with sync_playwright() as p: for engine in (p.chromium, p.firefox, p.webkit): browser = engine.launch() page = browser.new_page() print(page.evaluate("() => navigator.userAgent")) browser.close()with块退出时自动停掉 driver 子进程,不需要手动收尾。这段只干一件事:确认三个引擎都能启动、都能执行 JS。把 user agent 打出来,还能顺带核对每个引擎的指纹。
拦截网络请求验证 API 调用
page = context.new_page() with page.expect_response("glob:**/api/orders") as resp_info: page.click("#checkout") resp = resp_info.value assert resp.status == 200 assert resp.json()["total"] == 42expect_response是上下文管理器:进入时开始监听匹配的请求,你在块里触发点击,退出时直接拿到那个请求的 Response。断言接口行为不用轮询、不怕慢请求漏网,比事后翻请求列表干净得多。
pytest 参数化让一个用例跑三遍
ENGINES = ["chromium", "firefox", "webkit"] @pytest.fixture def page(request): with sync_playwright() as p: browser = getattr(p, request.param).launch() yield browser.new_page() browser.close() @pytest.mark.parametrize("page", ENGINES, indirect=True) def test_checkout(page): page.goto(BASE + "/cart")indirect=True把参数字符串当 fixture 参数传进去,一个测试函数就被复制成三个引擎各跑一遍。仓库自己的tests/async/conftest.py也是这个思路,只是把 browser 提升到 session 级复用,避免每条用例重新 launch。
同一张网格页在 Chromium 下的基线图。三个引擎的基线分目录存放,逐字节比较时各比各的,引擎间的渲染微差不会互相误伤。
测试开始抖时先查这三处
别把同步 API 塞进 asyncio 循环
我一开始也踩过:在 async main 里 new 了一个 sync Playwright,第一次调用就抛错,提示换 Async API——同步入口里有现成的运行中循环检查。正确姿势很简单:入口是异步的,全项目统一用 async_api;入口是同步脚本,统一用 sync_api。两套混着用,事件循环会先乱套。
截图比较别指望跨引擎逐字节相同
字体光栅化、抗锯齿在不同引擎间存在细微差别,加上页面里总有时间戳这类动态内容,截图回归最容易出假阳性。两个动作:基线按引擎分目录(仓库就是这么存的);捕获前把会变的区域 mask 掉:
shot = page.screenshot( full_page=True, mask=[page.locator(".timestamp")], )这张基线图里多出的品红色格子就是 mask 的落点:被遮蔽区域不参与比较,时间戳跳到下一秒也不会误报。
删掉固定 sleep,改等具体信号
page.wait_for_timeout(1500)在慢机器上不够、快机器上纯浪费。换成等真实信号:接口回来了、元素状态变了:
await page.wait_for_response("glob:**/api/orders") await page.locator(".cart").wait_for(state="visible")wait_for_load_state("networkidle")能用但偏保守,只适合对网络空闲要求不严格的页面。
核心收获
- 三引擎共用一套 API,靠的是中间那层 Node driver 把协议差异吞掉
- actionability 自动检查替代手写 sleep,是测试不抖的第一道保障
- 截图基线按引擎分目录、动态区 mask,视觉比较才稳
- pytest 里把引擎变成 parametrize 参数,CI 矩阵直接开跑
更多场景直接读仓库里的完整测试套件:tests/async/和tests/sync/。
【免费下载链接】playwright-pythonPython version of the Playwright testing and automation library.项目地址: https://gitcode.com/GitHub_Trending/pl/playwright-python
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考