Playwright's device emulation sets viewport, pixel ratio, user agent, and touch events together, but it does not test real mobile browser engines: you're still running Chromium regardless of whether you've selected devices['iPhone 14']. The setViewportSize shortcut changes only the viewport, not the user agent or touch behavior, which is sufficient for responsive layout tests but wrong for anything that checks mobile-specific CSS or user-agent sniffing. This article covers device presets, per-test emulation, the difference between setViewportSize and full newContext emulation, touch events, screenshot regression for mobile, and which scenarios warrant a dedicated mobile test project.
What mobile emulation does (and doesn't do)
Playwright emulation simulates:
- Viewport dimensions
- Device pixel ratio (retina screens)
- User agent string
- Touch events (replaces mouse events)
- Geolocation (optional)
- Locale and timezone (optional)
It does not test:
- Real device performance
- Actual mobile browser engines (you're still on Chromium, Firefox, or WebKit)
- Hardware-specific rendering
- Native mobile apps
For real device testing you need services like BrowserStack or Sauce Labs. Emulation covers the most common need: verifying your app behaves correctly at mobile viewport sizes and responds to touch.
Using built-in device presets
Playwright includes a library of device presets. Use them in your config:
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'chromium-desktop',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'mobile-chrome',
use: { ...devices['Pixel 7'] },
},
{
name: 'mobile-safari',
use: { ...devices['iPhone 14'] },
},
{
name: 'tablet',
use: { ...devices['iPad Pro 11'] },
},
],
});Run mobile tests only:
npx playwright test --project=mobile-chromeSee all available devices:
npx playwright --list-devicesThe list includes ~80 presets: Pixel devices, iPhones, Galaxy phones, iPads, Surface tablets.
Per-test device emulation
Override emulation for a single test without changing config:
import { test, expect, devices } from '@playwright/test';
test('mobile navigation works', async ({ browser }) => {
const context = await browser.newContext({
...devices['iPhone 14'],
});
const page = await context.newPage();
await page.goto('/');
// Hamburger menu should be visible on mobile
await expect(page.getByRole('button', { name: 'Menu' })).toBeVisible();
await page.getByRole('button', { name: 'Menu' }).click();
await expect(page.getByRole('navigation')).toBeVisible();
await context.close();
});Touch events
On mobile devices, Playwright uses touch events instead of mouse events. Most interactions work identically: click(), fill(), type() all translate correctly. Where touch matters:
// Tap (same as click on mobile)
await page.getByRole('button').tap();
// Swipe gesture
await page.touchscreen.tap(100, 200);
// Drag on touchscreen
await page.touchscreen.tap(100, 400);
// then simulate swipe by tapping a series of pointsFor swipe carousels, pull-to-refresh, or complex gestures, you may need page.evaluate() to dispatch custom touch events. Most simple mobile interactions work with regular locator methods.
Testing responsive behavior
The most practical use: verify your layout at different breakpoints.
test.describe('responsive layout', () => {
test('desktop shows full nav', async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 720 });
await page.goto('/');
await expect(page.getByRole('navigation')).toBeVisible();
await expect(page.getByRole('button', { name: 'Menu' })).not.toBeVisible();
});
test('mobile shows hamburger', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 });
await page.goto('/');
await expect(page.getByRole('button', { name: 'Menu' })).toBeVisible();
await expect(page.getByRole('navigation')).not.toBeVisible();
});
});setViewportSize changes only the viewport: it doesn't change the user agent. For full device simulation, use devices presets or a context with viewport + userAgent + deviceScaleFactor set together.
Viewport vs. use configuration
// Viewport only — doesn't change user agent or touch events
await page.setViewportSize({ width: 375, height: 812 });
// Full device emulation — viewport + user agent + touch + pixel ratio
const context = await browser.newContext({
viewport: { width: 390, height: 844 },
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X)...',
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
});For responsive tests that check layout only, setViewportSize is fine. For tests that check mobile-specific behavior (touch events, mobile-specific CSS, user-agent sniffing), use full device emulation via devices presets.
Screenshot comparison for responsive testing
Visual regression is especially valuable for mobile: layout bugs are often invisible in test assertions but obvious in a screenshot:
test('mobile homepage matches baseline', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 });
await page.goto('/');
await expect(page).toHaveScreenshot('mobile-homepage.png', {
fullPage: true,
});
});Run once to generate the baseline. Subsequent runs compare against it. A layout shift of a single pixel fails the test.
What to test on mobile specifically
Not every test needs a mobile version. Prioritize:
- Navigation (hamburger menus, mobile drawers)
- Forms (virtual keyboard doesn't cover submit button, tap targets are large enough)
- Tables (horizontal scroll vs. stacked layout)
- Images (responsive
srcset, no overflow) - Payment flows (critical path, touch-friendly buttons)
- Modals (scrollable content, close button accessible)
Skip mobile tests for:
- Admin dashboards that explicitly don't support mobile
- Tests that check identical behavior regardless of viewport
- API tests and unit tests