Playwright端到端测试实战中那些踩过的坑和优化技巧
谁更灵活?谁更省事?Playwright 移动端测试方案实测对比
我最近在给一个 PWA 应用写 E2E 测试,目标很明确:覆盖 iOS 和 Android 的 WebView、Safari、Chrome,还要能模拟手势(比如 swipe、long press)、处理 viewport 切换、绕过某些 UA 检查。一开始我以为直接上 Playwright 的 chromium + --user-agent 就完事了,结果跑起来发现:按钮点不中、滚动卡死、touch 事件压根没触发……折腾两天后我决定不赌运气,把几个主流方案拉出来真刀真枪比一比。
结论先甩出来:我目前主力用 Playwright + mobile device emulation + context override,不是因为它完美,而是它最可控、debug 最顺、CI 最稳。如果你要快速上线、不想被 WebKit 渲染差异折磨到怀疑人生,别犹豫,就它。下面说说为啥。
方案一:“纯设备模拟”——Playwright 内置 mobile 设备预设
这是文档里第一个教你的方案,用 devices['iPhone 13'] 一行搞定:
const { chromium, devices } = require('playwright');
const iPhone = devices['iPhone 13'];
const browser = await chromium.launch();
const context = await browser.newContext({
...iPhone,
permissions: ['geolocation'],
});
const page = await context.newPage();
await page.goto('https://jztheme.com/mobile-demo');
优点是真的省事:viewport 自动设对、UA 自动注入、touch 事件默认启用。我第一次跑通 swipe 操作时甚至有点感动——await page.touchscreen.swipe(50, 300, 50, 100) 居然真滑动了。
但坑也来得快:iOS Safari 的某些 CSS 渲染(比如 position: sticky 在 overflow 容器里的行为)跟真实设备差一截;更头疼的是,devices['iPhone 13'] 本质还是 Chromium 内核,WebKit 特性(比如 webkit-overflow-scrolling: touch)根本不会生效。我们有个下拉刷新组件,本地模拟一切正常,一上真机就卡住不动——查了半小时才发现是 WebKit 的 scroll event 触发逻辑不同。
方案二:“硬切 WebKit”——用 WebKit 启动 + 手动配 mobile UA
既然 Chromium 模拟不准,那就上真 WebKit。Playwright 支持直接 launch webkit:
const { webkit } = require('playwright');
const browser = await webkit.launch();
const context = await browser.newContext({
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1',
viewport: { width: 390, height: 844 },
hasTouch: true,
isMobile: true,
});
const page = await context.newPage();
await page.goto('https://jztheme.com/mobile-demo');
这个方案的好处是:WebKit 真实渲染、touch 事件链完整(touchstart → touchmove → touchend 全都有),连 document.createEvent('TouchEvent') 都能 mock 出来。我们那个下拉刷新组件,在这里终于跑通了。
但代价是:性能明显慢(尤其 CI 上,单测多跑几秒),而且 WebKit 对某些现代 API 支持滞后——比如 navigator.permissions.query({ name: 'geolocation' }) 在旧版 Playwright 的 WebKit 中会直接 throw error,必须降级到 v1.40+ 才修复。另外,page.touchscreen 的坐标系偶尔会错位(比如 swipe 坐标偏移 20px),我最后只能加了个校准函数临时绕过。
方案三:“混合打法”——Chromium 模拟 + context.overridePermissions + 手动触发 touch
这才是我目前线上项目在用的主力方案。不追求 100% 真实,只求“够用 + 可控 + 不翻车”:
const { chromium } = require('playwright');
const browser = await chromium.launch();
const context = await browser.newContext({
viewport: { width: 414, height: 896 }, // iPhone 12 尺寸
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/125.0.6422.112 Mobile/15E148 Safari/604.1',
hasTouch: true,
isMobile: true,
// 关键:强制启用 touch event 支持
javaScriptEnabled: true,
});
// 绕过某些站点的 UA 检测逻辑
await context.addInitScript(() => {
Object.defineProperty(navigator, 'platform', { value: 'iPhone' });
Object.defineProperty(navigator, 'maxTouchPoints', { value: 5 });
});
const page = await context.newPage();
await page.goto('https://jztheme.com/mobile-demo');
// 手动模拟一次 swipe:更稳定,坐标可精确控制
await page.evaluate(() => {
const el = document.querySelector('.scroll-container');
if (!el) return;
const touchStart = new Touch({
identifier: 1,
target: el,
clientX: 200,
clientY: 400,
radiusX: 2.5,
radiusY: 2.5,
});
const touchMove = new Touch({
identifier: 1,
target: el,
clientX: 200,
clientY: 200,
radiusX: 2.5,
radiusY: 2.5,
});
el.dispatchEvent(new TouchEvent('touchstart', { touches: [touchStart] }));
el.dispatchEvent(new TouchEvent('touchmove', { touches: [touchMove], changedTouches: [touchMove] }));
el.dispatchEvent(new TouchEvent('touchend', { changedTouches: [touchMove] }));
});
这个方案的优势在于:Chromium 快、稳定、调试方便(DevTools 里直接看 touch event),配合手动 dispatch TouchEvent,反而比 page.touchscreen 更精准(至少在我这 5 个手势场景里全过了)。而且 CI 跑得飞快,失败率低于 0.3%。
缺点也很实在:你得自己补一些 WebKit 特有行为(比如 gesturestart 事件基本不用管,我们项目里也没依赖它);还有就是某些 JS 库(比如 Hammer.js)会对 TouchList 做深度校验,这时候得 patch TouchList.length 这类属性——好在就一次,写进 addInitScript 就完事。
我的选型逻辑:不选“最真实”,只选“最省心”
我以前也迷信“越接近真机越好”,直到被 WebKit 的 release cycle 折磨了三次:一次是新版 Safari 加了新的 pointer event 行为,Playwright 还没同步;一次是 iOS 17.5 更新后,document.elementFromPoint 返回 null,查了三天发现是 WebKit bug;还有一次是团队新同学配环境配了六小时,就因为本地 WebKit 版本和 CI 不一致……
现在我的标准很简单:
- 如果测试重点是 UI 布局、viewport 响应、基础交互(tap/swipe)→ 用方案三(Chromium + 手动 touch)
- 如果必须验证 WebKit 特有 API(比如
webkitRequestFullscreen)或某些 scroll 惯性行为 → 方案二(WebKit)上,但只跑核心用例,跳过耗时 case - 方案一(内置 devices)只用来做 smoke test,每天凌晨跑一遍确保主流程不崩就行
顺带提一句:别信网上那些“用 Playwright + real iOS device”的教程,除非你公司有上百台真机集群+WebDriverAgent 维护团队。我自己试过 USB 连接 iPad,光配证书和 provisioning profile 就花了两天,最后还因为 Xcode 版本冲突失败——这种成本,远不如多写两行 JS patch 来得实际。
踩坑提醒:这三点一定注意
1. viewport 和 devicePixelRatio 要匹配:比如设了 { width: 390, height: 844 },但忘了设 deviceScaleFactor: 3,结果截图模糊、元素定位偏移。我踩过两次,第二次直接写了个 wrapper 函数自动算 ratio。
2. hasTouch: true 必须显式声明,否则即使 UA 是 iPhone,ontouchstart 也会是 undefined。Playwright 不会帮你猜。
3. 不要在 beforeAll 里复用 context——移动端手势操作经常带状态(比如 scroll 位置、input focus),context 复用会导致用例互相污染。老老实实每个 test new 一个 context,内存多占 20MB,但省掉 80% 的 flaky case。
以上是我踩坑后的总结,希望对你有帮助。这个技巧的拓展用法还有很多(比如结合 page.route 拦截 API 模拟弱网、用 page.exposeFunction 注入自定义手势工具函数),后续会继续分享这类博客。有更优的实现方式欢迎评论区交流。

暂无评论