首页 / Playwright 入门教程 / 视口、设备与移动端模拟 Emulation

Playwright 入门教程

视口、设备与移动端模拟 Emulation

本教程共 59 篇 · 第 27 篇 · 更新于 2026-08-04 · 约 10 分钟阅读

PlaywrightEmulationviewportdevices移动端响应式deviceScaleFactor

本节目标:会用内置设备清单和视口配置,在桌面浏览器里跑出手机端的测试效果。

响应式布局要测,移动端交互也要测。买一堆真机显然不现实。

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 了。写在它前面会被覆盖,写在后面才生效。

这个坑对 isMobilehasTouchuserAgent 同样适用——想覆盖设备默认值,一律放在展开之后。

在测试文件里设置

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,
});

它主要影响两件事:

  1. 页面里 window.devicePixelRatio 的值,响应式图片会据此选择 @2x 资源。
  2. 截图的实际像素尺寸——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() 派发的事件 isTrustedfalse。如果被测页面会检查这个属性,模拟手势就会被忽略——测试时得先把那个检查关掉。

常用设备清单

内置设备有上百个,实际常用的就这几类:

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 清单,展开进 usetest.use()
  • 想覆盖设备自带的 viewport / isMobile / userAgent,必须写在展开之后
  • viewport 在 context 层,setViewportSize() 在 page 层且可以中途改。
  • deviceScaleFactor 影响响应式图片选择和截图尺寸,视觉对比时要固定。
  • isMobile 管 meta viewport 和触摸,Firefox 不支持;hasTouch 只管触摸特性检测。
  • 触摸点击用 tap(),复杂手势要手动派发 touch 事件。
  • 设备矩阵配三四个就够,配多了 CI 时间失控。
  • 模拟能覆盖布局和逻辑,覆盖不了渲染引擎、性能和硬件。

下一章继续模拟,但换成权限、地理位置和颜色方案这些环境属性。