level 17 / device-emulation
Device Emulation
Use Playwright's built-in device emulation to test mobile viewports, pixel ratios, user agents, and geolocation.
What Device Emulation Does
Playwright can emulate mobile devices at the browser level:
- Viewport size and pixel ratio
- User-agent string (browser reports as mobile)
- Touch event support
- Geolocation
- Network throttling
This lets you test responsive layouts and mobile-specific features without a physical device or emulator.
Built-in Device Descriptors
Playwright ships descriptors for 50+ devices:
import { test, devices } from '@playwright/test';
// Use a specific device
test.use({ ...devices['iPhone 15'] });
test('login works on iPhone 15', async ({ page }) => {
await page.goto('/login');
await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible();
});
// playwright.config.ts — multi-device projects
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'mobile-safari', use: { ...devices['iPhone 15'] } },
{ name: 'mobile-chrome', use: { ...devices['Pixel 7'] } },
{ name: 'tablet', use: { ...devices['iPad Pro 11'] } },
],
});
Checking Available Devices
import { devices } from '@playwright/test';
// All available device names
console.log(Object.keys(devices));
// → ['iPhone 15', 'iPhone 15 Pro', 'Pixel 7', 'Galaxy S9+', 'iPad Pro 11', ...]
Custom Viewport Configuration
// Custom viewport without a device preset
test.use({
viewport: { width: 390, height: 844 },
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0...',
});
// Or override viewport per-test
test('narrow viewport layout', async ({ page }) => {
await page.setViewportSize({ width: 320, height: 568 });
await page.goto('/');
await expect(page.locator('.mobile-menu')).toBeVisible();
});
Testing Mobile-Specific Behaviour
// Test hamburger menu (only visible on mobile)
test('mobile nav opens on tap', async ({ page }) => {
await page.goto('/');
// Only visible at mobile breakpoint
const hamburger = page.getByRole('button', { name: 'Menu' });
await expect(hamburger).toBeVisible();
await hamburger.tap(); // use tap() not click() on mobile
await expect(page.getByRole('navigation')).toBeVisible();
});
// Test geolocation-dependent feature
test('shows local store based on location', async ({ page, context }) => {
await context.setGeolocation({ latitude: 51.5074, longitude: -0.1278 });
await context.grantPermissions(['geolocation']);
await page.goto('/find-store');
await page.getByRole('button', { name: 'Use my location' }).tap();
await expect(page.getByText('London')).toBeVisible();
});
Device Descriptor Contents
import { devices } from '@playwright/test';
console.log(devices['iPhone 15']);
// {
// userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5...',
// viewport: { width: 393, height: 852 },
// deviceScaleFactor: 3,
// isMobile: true,
// hasTouch: true,
// defaultBrowserType: 'webkit',
// }
defaultBrowserType: iPhone descriptors default to webkit (Safari engine). Use { ...devices['iPhone 15'], defaultBrowserType: 'chromium' } to test iPhone viewport on Chrome if needed.
Responsive Breakpoint Matrix
const BREAKPOINTS = [
{ name: 'mobile-sm', width: 320, height: 568 },
{ name: 'mobile', width: 390, height: 844 },
{ name: 'tablet', width: 768, height: 1024 },
{ name: 'desktop', width: 1280, height: 800 },
{ name: 'wide', width: 1920, height: 1080 },
];
for (const bp of BREAKPOINTS) {
test(`layout correct at ${bp.name}`, async ({ page }) => {
await page.setViewportSize({ width: bp.width, height: bp.height });
await page.goto('/');
await expect(page).toHaveScreenshot(`home-${bp.name}.png`);
});
}