skills/adaptationio/skrillz/mobile-emulation

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

  1. Test real devices too - Emulation is good but not perfect
  2. Cover major breakpoints - 375px, 768px, 1024px, 1280px minimum
  3. Test both orientations - Portrait and landscape
  4. Test touch vs click - Some interactions differ
  5. Test slow networks - Mobile users often have poor connectivity
  6. Test safe areas - Account for notches, home indicators

References

  • references/device-list.md - Complete device list with specs
  • references/touch-patterns.md - Touch gesture implementations
Weekly Installs
1
Installed on
claude-code1