视口、设备与移动端模拟 Emulation
本教程共 59 篇 · 第 27 篇 · 更新于 2026-08-04 · 约 10 分钟阅读
27. 视口、设备与移动端模拟 Emulation
本节目标:会用内置设备清单和视口配置,在桌面浏览器里跑出手机端的测试效果。
响应式布局要测,移动端交互也要测。买一堆真机显然不现实。
Playwright 的做法是模拟(Emulation)——改视口大小、改 User Agent、开启触摸事件、调整设备像素比,让页面以为自己跑在手机上。
这一章讲怎么用,也讲它的边界在哪。
内置设备清单
Playwright 自带一份设备参数表,通过 devices(设备描述清单)拿:
import { devices } from '@playwright/test';
console.log(devices['iPhone 13']);
// {
// userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 ...)',
// viewport: { width: 390, height: 664 },
// deviceScaleFactor: 3,
// isMobile: true,
// hasTouch: true,
// defaultBrowserType: 'webkit'
// }
一个设备描述就是这么几个字段的组合。用的时候展开进配置:
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 13'] },
},
],
});
这样配好之后,同一套测试会在两种设备上各跑一遍。项目(Projects)机制在第 6 章讲过,这里只是它的一个用法。
库模式(Library)下写法类似,只是 context 得自己建:
import { chromium, devices } from 'playwright';
const browser = await chromium.launch();
const iphone13 = devices['iPhone 13'];
const context = await browser.newContext({ ...iphone13 });
也可以只在某个测试文件里用:
import { test, devices } from '@playwright/test';
test.use({ ...devices['Pixel 7'] });
test('移动端首页', async ({ page }) => {
// 这个文件里的测试都跑在 Pixel 7 模拟环境下
});
User Agent 的平台陷阱
内置设备描述里的 userAgent 是写死的,包含具体的操作系统信息。
比如 Desktop Chrome 的 UA 里带的是 Windows。你在 macOS 上跑测试,页面收到的还是 Windows 的 UA。
多数时候无所谓。但如果你的应用会根据 UA 里的操作系统做事(比如显示不同的下载链接、快捷键提示),这就会出问题。
官方给的建议是把 UA 置空:
const context = await browser.newContext({
...devices['Desktop Chrome'],
userAgent: undefined, // 用运行平台的真实 UA
});
也可以自己指定:
test.use({ userAgent: '我的自定义 UA' });
视口配置
视口就是页面的可视区域大小,不含浏览器工具栏。
在配置里设置(对整个项目生效):
export default defineConfig({
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
// 必须写在展开之后,否则会被设备自带的 viewport 覆盖
viewport: { width: 1280, height: 720 },
},
},
],
});
Warning顺序很重要。
...devices['Desktop Chrome']里已经有viewport了。写在它前面会被覆盖,写在后面才生效。这个坑对
isMobile、hasTouch、userAgent同样适用——想覆盖设备默认值,一律放在展开之后。
在测试文件里设置:
test.use({ viewport: { width: 1600, height: 1200 } });
test('宽屏布局', async ({ page }) => {
// ...
});
只对一组测试生效:
test.describe('小屏场景', () => {
test.use({ viewport: { width: 375, height: 667 } });
test('菜单折叠成汉堡按钮', async ({ page }) => {
// ...
});
});
测试运行中动态改:
await page.setViewportSize({ width: 1600, height: 1200 });
这个方法只影响当前 page。同一 context 下的其他页面不变。
Tip测响应式断点时,
setViewportSize()在一个测试里连续改几次很方便:改一次断言一次,比开三个测试省事。
deviceScaleFactor:高分屏
设备像素比,也就是 1 个 CSS 像素对应几个物理像素。普通屏是 1,Retina 屏是 2 或 3。
const context = await browser.newContext({
viewport: { width: 2560, height: 1440 },
deviceScaleFactor: 2,
});
它主要影响两件事:
- 页面里
window.devicePixelRatio的值,响应式图片会据此选择@2x资源。 - 截图的实际像素尺寸——
deviceScaleFactor: 2时,1280 宽的视口截出来是 2560 像素宽。
做视觉对比测试(第 48 章)时要特别注意这个值,不同环境下不一致会导致大量误报。
isMobile 与 hasTouch
这两个开关容易混,分开说。
isMobile 控制两件事:
- 是否解析页面的
<meta name="viewport">标签。 - 是否启用触摸事件。
移动端布局很多是靠 meta viewport 生效的。isMobile: false 时这个标签被忽略,页面会按桌面方式渲染,哪怕视口只有 375 宽。
hasTouch 只控制一件事:是否上报触摸支持。它影响 'ontouchstart' in window 这类特性检测,很多库据此决定走触摸逻辑还是鼠标逻辑。
test.use({
viewport: { width: 375, height: 667 },
isMobile: true,
hasTouch: true,
});
Warning
isMobile不被 Firefox 支持。跨浏览器项目里配了它,Firefox 那条会直接报错。要么在 Firefox 项目里显式关掉,要么把移动端测试限定在 Chromium 和 WebKit 上。
覆盖设备默认值时同样注意顺序:
export default defineConfig({
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
isMobile: false, // 写在展开之后
},
},
],
});
触摸操作
开了 hasTouch 之后,可以用 tap() 代替 click():
await page.getByRole('button', { name: '提交' }).tap();
tap() 派发的是触摸事件序列,不是鼠标事件。测触摸专属逻辑时才需要它,普通按钮 click() 就够。
复杂手势(滑动、双指缩放)没有现成 API。思路是用 dispatchEvent() 手动派发 touchstart / touchmove / touchend,自己构造触点坐标:
const touches = [{ identifier: 0, clientX: 100, clientY: 200 }];
await locator.dispatchEvent('touchstart', {
touches,
changedTouches: touches,
targetTouches: touches,
});
滑动就是在 touchmove 里逐步改坐标,缩放就是给两个触点让它们靠近或远离。写起来不难,但比较啰嗦。
Note
dispatchEvent()派发的事件isTrusted为false。如果被测页面会检查这个属性,模拟手势就会被忽略——测试时得先把那个检查关掉。
常用设备清单
内置设备有上百个,实际常用的就这几类:
devices['Desktop Chrome']
devices['Desktop Safari']
devices['Desktop Firefox']
devices['Desktop Edge']
devices['iPhone 13']
devices['iPhone 13 Pro Max']
devices['Pixel 7']
devices['Galaxy S9+']
devices['iPad Pro 11']
devices['iPad Mini']
每个还有横屏版本,名字后面加 landscape:
devices['iPhone 13 landscape']
完整列表在 Playwright 源码的 deviceDescriptorsSource.json 里,也可以直接打印 Object.keys(devices) 看。
Tip别贪多。设备矩阵配三四个代表性的就够:一个桌面、一个 iOS、一个 Android。全配上会让 CI 时间翻好几倍,收益却很有限。
模拟不等于真机
必须说清楚这一点。设备模拟改的是浏览器暴露给页面的信息,不是真的换了一台设备。
模拟能覆盖的:
- 视口尺寸和响应式断点
- User Agent 判断分支
- 触摸事件与
hasTouch特性检测 - 设备像素比与
@2x资源选择 - meta viewport 的解析行为
模拟不能覆盖的:
- 真实的渲染引擎差异(iOS Safari 只能用 WebKit 近似,不完全等价)
- 真机性能(低端机的卡顿、内存限制)
- 系统级行为(软键盘弹出、返回手势、状态栏)
- 硬件能力(相机、蓝牙、传感器)
所以模拟适合做布局和逻辑的日常回归,发布前的兼容性验证还是得上真机或云真机平台。
一个完整配置
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'desktop',
use: {
...devices['Desktop Chrome'],
userAgent: undefined,
viewport: { width: 1440, height: 900 },
},
},
{
name: 'mobile-ios',
use: { ...devices['iPhone 13'] },
},
{
name: 'mobile-android',
use: { ...devices['Pixel 7'] },
},
],
});
配合测试里的条件跳过,可以让某些用例只在特定设备跑:
test('汉堡菜单', async ({ page, isMobile }) => {
test.skip(!isMobile, '这个用例只在移动端有意义');
await page.getByRole('button', { name: '菜单' }).tap();
await expect(page.getByRole('navigation')).toBeVisible();
});
isMobile 是内置夹具,可以直接注入用来做条件判断。
小结
- 设备模拟用
devices清单,展开进use或test.use()。 - 想覆盖设备自带的 viewport / isMobile / userAgent,必须写在展开之后。
viewport在 context 层,setViewportSize()在 page 层且可以中途改。deviceScaleFactor影响响应式图片选择和截图尺寸,视觉对比时要固定。isMobile管 meta viewport 和触摸,Firefox 不支持;hasTouch只管触摸特性检测。- 触摸点击用
tap(),复杂手势要手动派发 touch 事件。 - 设备矩阵配三四个就够,配多了 CI 时间失控。
- 模拟能覆盖布局和逻辑,覆盖不了渲染引擎、性能和硬件。
下一章继续模拟,但换成权限、地理位置和颜色方案这些环境属性。