Mobile device emulation and responsive testing with Playwright. Use when testing mobile layouts, touch interactions, device-specific features, or responsive breakpoints.
62
74%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./skills/mobile-emulation/SKILL.mdTest responsive designs and mobile-specific features using Playwright's device emulation capabilities.
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();
});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'] },
},
],
});{
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',
},
},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']import { devices } from '@playwright/test';
console.log(Object.keys(devices));
// Outputs all available device namesconst 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`);
});
}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();
});test('tap interaction', async ({ page }) => {
await page.goto('/');
// Tap is equivalent to click on touch devices
await page.getByRole('button').tap();
});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();
});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
}
});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();
});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');
});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();
});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();
});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();
});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 });
});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();
});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);
});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
});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');
});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`);
});
});
}references/device-list.md - Complete device list with specsreferences/touch-patterns.md - Touch gesture implementations93ed392
Also appears in
since Sep 12, 2026
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.