14Mobile & Device Emulation
What you will master here
- Device profiles (Pixel, iPhone, iPad)
- Viewport, user agent, device-scale-factor, hasTouch, isMobile
- Tap, swipe and touch events
- Geolocation, permissions, locale, timezone
- Orientation, dark mode, reduced motion
- Limits of emulation vs real devices
14.1 Device profiles
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{ name: 'desktop-chrome', 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'] } },
],
});
Each device profile bundles: viewport (375×667 etc.), userAgent, deviceScaleFactor (Retina = 2 or 3), isMobile (changes meta-viewport handling), hasTouch (enables touch events).
14.2 What emulation actually changes
| Property | Effect |
|---|---|
| viewport | Window dimensions; triggers your responsive CSS breakpoints |
| userAgent | String sent in requests; some sites serve different markup |
| deviceScaleFactor | Retina (2x) — affects screenshot resolution and CSS px sizing |
| isMobile | Mobile meta-viewport behaviour; touch events default on |
| hasTouch | Touch events available; mouse events still work |
Emulation is NOT a real deviceEmulation uses your dev machine's CPU/GPU. iOS-specific Safari bugs, Android keyboard quirks, or hardware sensors won't appear. For real coverage, pair Playwright with BrowserStack / Sauce Labs / a device farm — Playwright supports remote runners.
14.3 Touch interactions
test('mobile gesture: tap and swipe', async ({ page }) => {
await page.goto('/feed');
/* Tap (like click but fires touch events when hasTouch=true) */
await page.getByRole('button', { name: 'Like' }).tap();
/* Long-press */
const card = page.getByTestId('card-1');
const box = await card.boundingBox();
await page.touchscreen.tap(box!.x + 10, box!.y + 10);
/* Swipe via touchscreen (low-level) */
await page.evaluate(() => window.scrollTo(0, 0));
await page.touchscreen.tap(200, 600);
/* Some apps need a manual swipe path */
const start = { x: 200, y: 600 }, end = { x: 200, y: 100 };
await page.evaluate(([s, e]) => {
const make = (t: string, p: any) => new TouchEvent(t, {
bubbles: true, touches: [{ identifier: 0, clientX: p.x, clientY: p.y, target: document.body } as any],
});
document.body.dispatchEvent(make('touchstart', s));
document.body.dispatchEvent(make('touchmove', e));
document.body.dispatchEvent(make('touchend', e));
}, [start, end]);
});
14.4 Geolocation, permissions, locale, timezone
test('shows nearby stores using geo', async ({ browser }) => {
const context = await browser.newContext({
...devices['Pixel 7'],
geolocation: { latitude: 12.9716, longitude: 77.5946 }, // Bangalore
permissions: ['geolocation'],
locale: 'en-IN',
timezoneId: 'Asia/Kolkata',
});
const page = await context.newPage();
await page.goto('/stores/near-me');
await expect(page.getByText('Bengaluru')).toBeVisible();
});
14.5 Orientation
test('landscape rendering', async ({ browser }) => {
const ctx = await browser.newContext({
...devices['Pixel 7 landscape'], // pre-defined landscape profile
});
const page = await ctx.newPage();
await page.goto('/');
/* viewport is now wider than tall */
});
/* Or rotate mid-test */
test('rotate during interaction', async ({ page }) => {
await page.goto('/');
await page.setViewportSize({ width: 800, height: 360 }); // landscape
});
14.6 Color scheme, reduced motion, contrast
test('dark mode renders dark theme', async ({ browser }) => {
const ctx = await browser.newContext({ colorScheme: 'dark' });
const page = await ctx.newPage();
await page.goto('/');
await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(13, 17, 23)');
});
test('respects prefers-reduced-motion', async ({ browser }) => {
const ctx = await browser.newContext({ reducedMotion: 'reduce' });
const page = await ctx.newPage();
await page.goto('/');
/* page should skip / shorten animations */
});
test('forced-colors mode (Windows high contrast)', async ({ browser }) => {
const ctx = await browser.newContext({ forcedColors: 'active' });
/* Test that critical UI is still readable */
});
14.7 Useful built-in device list (samples)
- iPhone 14, iPhone 14 Pro Max, iPhone 14 Plus, iPhone SE
- Pixel 7, Pixel 5, Galaxy S9+
- iPad Pro 11, iPad Mini, Galaxy Tab S4
- Desktop Chrome / Edge / Firefox / Safari
- Each has a
…landscapevariant
Full list: import { devices } from '@playwright/test'; console.log(Object.keys(devices));
14.8 Picking what to test on mobile
- Critical user journeys (auth, checkout, primary CTA) — yes
- Form heavy flows — definitely (mobile keyboards are a regression source)
- Touch-only interactions (swipe carousels, pull-to-refresh) — yes
- Hover-only menus — they probably don't work on touch; explicit test
- Visual: per-viewport baselines for storybook components
Module 16 — Interview Q&A bank
How does Playwright emulate mobile devices?
Via device profiles in
@playwright/test devices — each preset bundles viewport, userAgent, deviceScaleFactor, isMobile and hasTouch. Apply per-project for cross-device runs.What's the difference between click and tap?
When
hasTouch: true is set (mobile devices), tap() fires touch events (touchstart/touchmove/touchend) plus the standard click sequence. click() fires mouse events. Use tap to verify touch-specific handlers.How do you simulate a swipe?
Either use
page.touchscreen.tap with a sequence, or dispatch TouchEvents via page.evaluate for full control. For many UIs, you can also call the underlying swipe API directly (scrollIntoView, programmatic state changes) if you don't need to exercise gesture handlers.What is deviceScaleFactor and why does it matter?
The ratio of physical pixels to CSS pixels. Retina = 2 or 3. Affects screenshot output resolution (a 375 CSS-pixel-wide screenshot at scale 2 is 750 px wide) and which @2x / @3x assets the page loads.
Is mobile emulation enough to certify mobile UX?
No — emulation runs on dev hardware. iOS-Safari specific bugs, Android keyboard behaviour, hardware sensors (camera, accelerometer), real touch lag, network differences need real-device testing on a farm (BrowserStack, Sauce Labs).
How do you test geolocation-dependent features?
Pass
geolocation: { latitude, longitude } and permissions: ['geolocation'] to newContext. The browser then reports those coordinates to the page.How do you test dark mode?
newContext({ colorScheme: 'dark' }) sets prefers-color-scheme: dark media query. Combine with visual regression to assert dark theme renders correctly.How do you test reduced-motion preference?
newContext({ reducedMotion: 'reduce' }) sets prefers-reduced-motion: reduce. Assert animations are disabled / shortened — important for vestibular-disorder users.What's the most common mobile bug functional tests miss?
Hover-only menus and other mouse-only affordances. On touch, you can't hover — the menu never opens. Always test interactions across both pointer types.
14.10 Mobile-specific test patterns
Network throttling per test
test('works on slow 3G', async ({ page, context }) => {
const cdp = await context.newCDPSession(page);
await cdp.send('Network.emulateNetworkConditions', {
offline: false,
downloadThroughput: (1.6 * 1024 * 1024) / 8, // 1.6 Mbps
uploadThroughput: (750 * 1024) / 8, // 750 Kbps
latency: 300,
});
await cdp.send('Emulation.setCPUThrottlingRate', { rate: 4 });
/* now run test under throttled conditions */
});
Mobile viewport visual snapshot
test('mobile home looks right', async ({ page }) => {
/* use a project: { use: { ...devices['Pixel 7'] } } */
await page.goto('/');
await expect(page).toHaveScreenshot('home-mobile.png', {
fullPage: true,
animations: 'disabled',
});
});
Orientation change mid-test
// Rotate by changing viewport
await page.setViewportSize({ width: 800, height: 360 }); // landscape
// Re-assert layout fits
14.11 Real-device testing (cloud farms)
- BrowserStack App Live — real iOS / Android devices, manual + automated
- Sauce Labs Real Devices — same but more enterprise certifications
- Firebase Test Lab — Google's farm, free tier, Android focused
- AWS Device Farm — pay per device-minute
- LambdaTest — competitive pricing
14.12 Mobile web vs hybrid vs native
| Type | Test tool |
|---|---|
| Mobile web (Safari/Chrome on phone) | Playwright device emulation OR cloud farm |
| Hybrid (WebView inside native shell) | Appium — switch context between NATIVE_APP and WEBVIEW |
| Native (Swift/Kotlin) | Appium with XCUITest/UiAutomator2 drivers, or XCUITest/Espresso direct |
| Cross-platform (React Native, Flutter) | Detox (RN), Maestro, Appium |
14.13 Common mobile failure modes
- Backgrounding — app paused/resumed; state preserved?
- Push notification while in app — popup interferes with flow
- Slow network mid-action — does spinner show? Action eventually completes?
- Permission prompts — geo, camera, push; test allow + deny
- Deep links — open via universal link, verify correct screen
- Battery / low power mode — animations should respect
- Cellular vs Wi-Fi — switch mid-session
- Different OS versions — iOS 15 vs 17, Android 11 vs 14
14.14 Touch gestures via Playwright
// Tap (calls touch events on touch-enabled context)
await page.getByRole('button', { name: 'Buy' }).tap();
// Swipe via touchscreen API
const box = await page.locator('.carousel').boundingBox();
const cx = box!.x + box!.width / 2;
const cy = box!.y + box!.height / 2;
await page.touchscreen.tap(cx + 100, cy);
// Or chain low-level: pointer down, move, up
Module 16 — More Mobile Q&A
Playwright mobile emulation vs Appium?
Playwright: web on mobile-like browser (viewport + UA + touch). Fast, in CI. Appium: real Android/iOS browser or native app via WebDriver. Slower, more accurate. Use Playwright for mobile-web; Appium for native + accurate iOS Safari.
How to test deep links?
Native: open
myapp://route/x URL via Appium; assert correct screen. Mobile web: navigate directly to URL; verify routing. Universal links require associated-domains config + cloud farm to test correctly.iOS Safari quirks?
100vh includes browser chrome (use 100dvh). Autoplay strict (must be muted+inline). Input zoom on focus if font < 16px. PWA limitations. Push notifications limited until iOS 16.4. Safe area insets via env() CSS.
How to test backgrounding behaviour?
Appium:
driver.runAppInBackground(Duration.ofSeconds(N)). Web: page.evaluate(() => document.dispatchEvent(new Event('visibilitychange'))) — limited; cloud farm for accurate behaviour.Real device or cloud farm — when each?
Local real device for ad-hoc debugging. Cloud farm (BrowserStack/Sauce) for CI parallel + many devices/OS versions. Combine: emulators for fast feedback, real devices for release smoke.