mobile-emulation
SKILL.md
Mobile Emulation Testing with Playwright
Test responsive designs and mobile-specific features using Playwright's device emulation capabilities.
Quick Start
import { test, expect, devices } from '@playwright/test';
test.use(devices['iPhone 14']);
test('mobile navigation works', async ({ page }) => {
await page.goto('/');
// Mobile menu should be visible
await page.getByRole('button', { name: 'Menu' }).click();
await expect(page.getByRole('navigation')).toBeVisible();
});
Configuration
Project-Based Device Testing
playwright.config.ts:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
// Desktop browsers
{
name: 'Desktop Chrome',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'Desktop Firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'Desktop Safari',
use: { ...devices['Desktop Safari'] },
},
// Mobile devices
{
name: 'iPhone 14',
use: { ...devices['iPhone 14'] },
},
{
name: 'iPhone 14 Pro Max',
use: { ...devices['iPhone 14 Pro Max'] },
},
{
name: 'Pixel 7',
use: { ...devices['Pixel 7'] },
},
{
name: 'Galaxy S23',
use: { ...devices['Galaxy S III'] }, // Closest available
},
// Tablets
{
name: 'iPad Pro',
use: { ...devices['iPad Pro 11'] },
},
{
name: 'iPad Mini',
use: { ...devices['iPad Mini'] },
},
],
});
Custom Device Configuration
{
name: 'Custom Mobile',
use: {
viewport: { width: 390, height: 844 },
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)...',
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
defaultBrowserType: 'webkit',
},
},
Available Devices
Popular Devices
import { devices } from '@playwright/test';
// iPhones
devices['iPhone 14']
devices['iPhone 14 Plus']
devices['iPhone 14 Pro']
devices['iPhone 14 Pro Max']
devices['iPhone 13']
devices['iPhone 12']
devices['iPhone SE']
// Android Phones
devices['Pixel 7']
devices['Pixel 5']
devices['Galaxy S III']
devices['Galaxy S5']
devices['Galaxy Note 3']
devices['Nexus 5']
// Tablets
devices['iPad Pro 11']
devices['iPad Pro 11 landscape']
devices['iPad Mini']
devices['iPad (gen 7)']
devices['Galaxy Tab S4']
// Desktop
devices['Desktop Chrome']
devices['Desktop Firefox']
devices['Desktop Safari']
devices['Desktop Edge']
List All Devices
import { devices } from '@playwright/test';
console.log(Object.keys(devices));
// Outputs all available device names
Responsive Breakpoint Testing
Test Multiple Viewports
const breakpoints = [
{ name: 'mobile', width: 375, height: 667 },
{ name: 'tablet', width: 768, height: 1024 },
{ name: 'desktop', width: 1280, height: 720 },
{ name: 'wide', width: 1920, height: 1080 },
];
for (const bp of breakpoints) {
test(`layout at ${bp.name}`, async ({ page }) => {
await page.setViewportSize({ width: bp.width, height: bp.height });
await page.goto('/');
await expect(page).toHaveScreenshot(`layout-${bp.name}.png`);
});
}
Dynamic Viewport Changes
test('responsive navigation', async ({ page }) => {
await page.goto('/');
// Desktop - horizontal nav
await page.setViewportSize({ width: 1280, height: 720 });
await expect(page.locator('.desktop-nav')).toBeVisible();
await expect(page.locator('.mobile-menu-button')).not.toBeVisible();
// Tablet - may show hamburger
await page.setViewportSize({ width: 768, height: 1024 });
// Mobile - hamburger menu
await page.setViewportSize({ width: 375, height: 667 });
await expect(page.locator('.desktop-nav')).not.toBeVisible();
await expect(page.locator('.mobile-menu-button')).toBeVisible();
});
Touch Interactions
Tap
test('tap interaction', async ({ page }) => {
await page.goto('/');
// Tap is equivalent to click on touch devices
await page.getByRole('button').tap();
});
Swipe
test('swipe carousel', async ({ page }) => {
await page.goto('/gallery');
const carousel = page.locator('.carousel');
const box = await carousel.boundingBox();
if (box) {
// Swipe left
await page.mouse.move(box.x + box.width - 50, box.y + box.height / 2);
await page.mouse.down();
await page.mouse.move(box.x + 50, box.y + box.height / 2, { steps: 10 });
await page.mouse.up();
}
await expect(page.locator('.slide-2')).toBeVisible();
});
Pinch to Zoom
test('pinch to zoom map', async ({ page }) => {
await page.goto('/map');
const map = page.locator('#map');
const box = await map.boundingBox();
if (box) {
const centerX = box.x + box.width / 2;
const centerY = box.y + box.height / 2;
// Simulate pinch out (zoom in)
await page.touchscreen.tap(centerX, centerY);
// Note: Multi-touch pinch requires custom implementation
}
});
Long Press
test('long press context menu', async ({ page }) => {
await page.goto('/');
const element = page.locator('.long-press-target');
const box = await element.boundingBox();
if (box) {
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
await page.waitForTimeout(1000); // Hold for 1 second
await page.mouse.up();
}
await expect(page.locator('.context-menu')).toBeVisible();
});
Orientation Testing
Portrait vs Landscape
test('orientation change', async ({ page }) => {
// Portrait
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/video');
await expect(page.locator('.video-container')).toHaveCSS('width', '390px');
// Landscape
await page.setViewportSize({ width: 844, height: 390 });
await expect(page.locator('.video-container')).toHaveCSS('width', '844px');
});
Using Device Landscape Variants
test.use(devices['iPad Pro 11 landscape']);
test('tablet landscape layout', async ({ page }) => {
await page.goto('/dashboard');
// Sidebar should be visible in landscape
await expect(page.locator('.sidebar')).toBeVisible();
});
Geolocation Testing
test.use({
geolocation: { latitude: 40.7128, longitude: -74.0060 }, // NYC
permissions: ['geolocation'],
});
test('shows nearby locations', async ({ page }) => {
await page.goto('/locations');
await page.getByRole('button', { name: 'Find Nearby' }).click();
await expect(page.getByText('New York')).toBeVisible();
});
Change Location During Test
test('location change', async ({ page, context }) => {
await context.setGeolocation({ latitude: 51.5074, longitude: -0.1278 }); // London
await page.goto('/weather');
await expect(page.getByText('London')).toBeVisible();
await context.setGeolocation({ latitude: 35.6762, longitude: 139.6503 }); // Tokyo
await page.reload();
await expect(page.getByText('Tokyo')).toBeVisible();
});
Network Conditions
Slow 3G
test('works on slow network', async ({ page, context }) => {
// Emulate slow 3G
const client = await context.newCDPSession(page);
await client.send('Network.emulateNetworkConditions', {
offline: false,
downloadThroughput: (500 * 1024) / 8, // 500kb/s
uploadThroughput: (500 * 1024) / 8,
latency: 400, // 400ms
});
await page.goto('/');
// Should show skeleton loaders
await expect(page.locator('.skeleton')).toBeVisible();
// Eventually loads
await expect(page.locator('.content')).toBeVisible({ timeout: 30000 });
});
Offline Mode
test('offline functionality', async ({ page, context }) => {
await page.goto('/');
// Cache page, then go offline
await context.setOffline(true);
await page.reload();
// Should show offline message or cached content
await expect(page.getByText(/offline/i)).toBeVisible();
});
Device-Specific Features
Notch/Safe Areas
test('respects safe areas', async ({ page }) => {
// iPhone with notch
test.use(devices['iPhone 14 Pro']);
await page.goto('/');
// Header should account for notch
const header = page.locator('header');
const paddingTop = await header.evaluate(el =>
window.getComputedStyle(el).paddingTop
);
// Should have safe area inset
expect(parseInt(paddingTop)).toBeGreaterThan(20);
});
Dark Mode
test.use({
colorScheme: 'dark',
});
test('dark mode styling', async ({ page }) => {
await page.goto('/');
const body = page.locator('body');
const bgColor = await body.evaluate(el =>
window.getComputedStyle(el).backgroundColor
);
// Should have dark background
expect(bgColor).toBe('rgb(0, 0, 0)'); // or dark color
});
Reduced Motion
test.use({
reducedMotion: 'reduce',
});
test('respects reduced motion', async ({ page }) => {
await page.goto('/');
const animated = page.locator('.animated-element');
const animationDuration = await animated.evaluate(el =>
window.getComputedStyle(el).animationDuration
);
// Should have no animation
expect(animationDuration).toBe('0s');
});
Visual Regression Across Devices
const testDevices = [
'iPhone 14',
'Pixel 7',
'iPad Pro 11',
'Desktop Chrome',
];
for (const deviceName of testDevices) {
test.describe(`Visual: ${deviceName}`, () => {
test.use(devices[deviceName]);
test('homepage', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot(`homepage-${deviceName}.png`);
});
test('product page', async ({ page }) => {
await page.goto('/products/1');
await expect(page).toHaveScreenshot(`product-${deviceName}.png`);
});
});
}
Best Practices
- Test real devices too - Emulation is good but not perfect
- Cover major breakpoints - 375px, 768px, 1024px, 1280px minimum
- Test both orientations - Portrait and landscape
- Test touch vs click - Some interactions differ
- Test slow networks - Mobile users often have poor connectivity
- Test safe areas - Account for notches, home indicators
References
references/device-list.md- Complete device list with specsreferences/touch-patterns.md- Touch gesture implementations