Mobile & Device

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

PropertyEffect
viewportWindow dimensions; triggers your responsive CSS breakpoints
userAgentString sent in requests; some sites serve different markup
deviceScaleFactorRetina (2x) — affects screenshot resolution and CSS px sizing
isMobileMobile meta-viewport behaviour; touch events default on
hasTouchTouch 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)

Full list: import { devices } from '@playwright/test'; console.log(Object.keys(devices));

14.8 Picking what to test on mobile

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)

14.12 Mobile web vs hybrid vs native

TypeTest 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

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.