Learn-skills.dev mobile-emulation
Mobile device emulation and responsive testing with Playwright. Use when testing mobile layouts, touch interactions, device-specific features, or responsive breakpoints.
install
source · Clone the upstream repo
git clone https://github.com/NeverSight/learn-skills.dev
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/NeverSight/learn-skills.dev "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/skills-md/adaptationio/skrillz/mobile-emulation" ~/.claude/skills/neversight-learn-skills-dev-mobile-emulation && rm -rf "$T"
manifest:
data/skills-md/adaptationio/skrillz/mobile-emulation/SKILL.mdsource content
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
- Complete device list with specsreferences/device-list.md
- Touch gesture implementationsreferences/touch-patterns.md