Related articles: Also see testing touch gestures and device-specific interactions, a cross-browser strategy to pair with mobile emulation, and which framework handles mobile emulation best in 2026.
Mobile Web Emulation with Playwright: Testing Responsive Design and Mobile UX
As of 2026, mobile devices account for over 60% of global web traffic. On e-commerce sites, that number climbs to 75%. Yet many development teams still test primarily on desktop and only check mobile as an afterthought�often discovering critical bugs in production.
The good news? You don't need a drawer full of iPhones and Android devices to test mobile experiences. Playwright's device emulation provides a powerful, cost-effective way to test responsive design, touch interactions, and mobile-specific features�all from your local development environment or CI/CD pipeline.
In this comprehensive guide, we'll cover:
- Why mobile web testing matters and common mobile-specific issues
- Playwright's device emulation capabilities and configuration
- Testing responsive layouts and breakpoints
- Simulating touch gestures, geolocation, and network conditions
- Best practices for mobile web testing
- When to use real devices vs. emulation
Whether you're a QA engineer, frontend developer, or no-code tester, this article will help you ensure your web app delivers an exceptional experience on every device.
Why Mobile Web Testing Matters
The Mobile-First Reality
- 60% of web traffic is mobile (Statista, 2026)
- 53% of users abandon a site if it takes longer than 3 seconds to load on mobile
- Google's mobile-first indexing means your mobile site affects your SEO ranking
Common Mobile-Specific Bugs
| Issue | Example | Impact |
|---|---|---|
| Layout Breaks | Text overflows on small screens | Content unreadable |
| Touch Targets Too Small | Buttons < 44x44px | Usability issues, accidental clicks |
| Slow Performance | Images not optimized for mobile | High bounce rates |
| Unresponsive Nav | Hamburger menu doesn't work on touch | Users can't navigate |
| Form Input Issues | Wrong keyboard opens (e.g., text instead of number) | Friction, abandonment |
| Fixed Positioning Bugs | Fixed header covers content | Broken UX |
Playwright's Device Emulation: How It Works
Playwright allows you to run tests in a virtual mobile browser by emulating:
- Viewport size (e.g., 375x812 for iPhone 13)
- User agent string (identifies the browser as mobile)
- Device pixel ratio (for high-DPI displays)
- Touch support (enables touch events)
- Geolocation (simulates GPS coordinates)
- Network conditions (throttles speed to 3G/4G)
Example: Basic Device Emulation
import { test, devices } from '@playwright/test';
test.use({ ...devices['iPhone 13'] });
test('should display mobile menu', async ({ page }) => {
await page.goto('https://example.com');
const menuButton = page.locator('button[aria-label="Open menu"]');
await menuButton.click();
await page.locator('nav.mobile-menu').waitFor();
});
This test runs in an emulated iPhone 13 with a 390x844 viewport, mobile user agent, and touch events enabled.
Testing Responsive Layouts
Viewport-Based Testing
Test your site at common breakpoints:
import { test, expect } from '@playwright/test';
const viewports = [
{ name: 'Mobile', width: 375, height: 667 }, // iPhone SE
{ name: 'Tablet', width: 768, height: 1024 }, // iPad
{ name: 'Desktop', width: 1920, height: 1080 }, // Full HD
];
for (const { name, width, height } of viewports) {
test(`should render correctly on ${name}`, async ({ page }) => {
await page.setViewportSize({ width, height });
await page.goto('https://example.com');
// Take a screenshot for visual regression testing
await expect(page).toHaveScreenshot(`homepage-${name}.png`);
});
}
Testing Hide/Show Elements at Breakpoints
test('should show hamburger menu on mobile, not on desktop', async ({ page }) => {
// Mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
await page.goto('https://example.com');
await expect(page.locator('button.hamburger-menu')).toBeVisible();
await expect(page.locator('nav.desktop-nav')).toBeHidden();
// Desktop viewport
await page.setViewportSize({ width: 1920, height: 1080 });
await expect(page.locator('button.hamburger-menu')).toBeHidden();
await expect(page.locator('nav.desktop-nav')).toBeVisible();
});
Testing Touch Interactions
Mobile users interact via touch, not mouse clicks. Playwright can simulate touch gestures:
Tap
test('should open product details on tap', async ({ page }) => {
await page.goto('https://example.com/products');
await page.locator('.product-card').first().tap();
await expect(page).toHaveURL(/product\/\d+/);
});
Swipe (for carousels, sliders)
test('should swipe through image carousel', async ({ page }) => {
await page.goto('https://example.com/product/123');
const carousel = page.locator('.image-carousel');
const box = await carousel.boundingBox();
// Swipe left (from right to left)
await page.mouse.move(box.x + box.width * 0.8, box.y + box.height / 2);
await page.mouse.down();
await page.mouse.move(box.x + box.width * 0.2, box.y + box.height / 2);
await page.mouse.up();
// Verify the second image is now visible
await expect(page.locator('.carousel-item').nth(1)).toBeVisible();
});
Long Press
test('should show context menu on long press', async ({ page }) => {
await page.goto('https://example.com');
const element = page.locator('.item');
await element.tap({ delay: 1000 }); // Long press (1 second)
await expect(page.locator('.context-menu')).toBeVisible();
});
Emulating Device-Specific Features
Geolocation
test('should show nearby stores based on location', async ({ page, context }) => {
// Grant geolocation permission
await context.grantPermissions(['geolocation']);
// Set location to New York
await context.setGeolocation({ latitude: 40.7128, longitude: -74.006 });
await page.goto('https://example.com/store-locator');
await expect(page.locator('.store-list .store').first()).toContainText('New York');
});
Network Throttling (Slow 3G, Fast 3G, 4G)
test('should load gracefully on slow network', async ({ page, context }) => {
// Emulate slow 3G
await context.route('**/*', (route) =>
route.continue({
delay: 1000, // Add 1s delay to all requests
}),
);
await page.goto('https://example.com');
// Ensure loading spinner appears
await expect(page.locator('.loading-spinner')).toBeVisible();
// Wait for content to load
await expect(page.locator('h1')).toBeVisible();
});
Playwright doesn't have built-in network throttling, but you can use Chrome DevTools Protocol (CDP):
import { chromium } from 'playwright';
const browser = await chromium.launch();
const context = await browser.newContext();
const client = await context.newCDPSession(await context.newPage());
await client.send('Network.emulateNetworkConditions', {
offline: false,
downloadThroughput: 50 * 1024, // 50KB/s
uploadThroughput: 20 * 1024, // 20KB/s
latency: 100, // 100ms
});
Device Orientation (Portrait vs. Landscape)
test('should adapt layout to landscape orientation', async ({ page, context }) => {
await page.goto('https://example.com');
// Switch to landscape
await page.setViewportSize({ width: 844, height: 390 }); // iPhone 13 landscape
await expect(page.locator('.landscape-layout')).toBeVisible();
});
Testing Mobile Forms
Mobile forms have unique challenges: autocomplete, keyboard types, and input validation.
Ensure Correct Keyboard Opens
<!-- Email keyboard (@ symbol) -->
<input type="email" name="email" />
<!-- Numeric keyboard -->
<input type="tel" name="phone" />
<!-- Number keyboard with decimals -->
<input type="number" name="quantity" />
Test:
test('should open numeric keyboard for phone input', async ({ page }) => {
await page.goto('https://example.com/checkout');
const phoneInput = page.locator('input[type="tel"]');
await phoneInput.focus();
// Verify inputmode attribute or type
await expect(phoneInput).toHaveAttribute('type', 'tel');
});
Test Autofill
test('should autofill address form', async ({ page, context }) => {
await page.goto('https://example.com/checkout');
// Simulate autofill by filling multiple fields at once
await page.fill('input[name="address"]', '123 Main St');
await page.fill('input[name="city"]', 'New York');
await page.fill('input[name="zip"]', '10001');
await page.click('button[type="submit"]');
await expect(page).toHaveURL(/order-confirmation/);
});
Common Device Presets in Playwright
Playwright includes 40+ device presets:
| Device | Viewport | User Agent | Touch | DPR |
|---|---|---|---|---|
| iPhone 13 | 390x844 | Safari iOS 15 | ? | 3 |
| iPhone 13 Pro Max | 428x926 | Safari iOS 15 | ? | 3 |
| Pixel 5 | 393x851 | Chrome Android | ? | 2.75 |
| Galaxy S9+ | 320x658 | Samsung Internet | ? | 4.5 |
| iPad Pro | 1024x1366 | Safari iPadOS | ? | 2 |
Usage:
import { devices } from '@playwright/test';
test.use({ ...devices['Pixel 5'] });
Full list: Playwright Device Descriptors
Best Practices for Mobile Web Testing
1. Test on Real Breakpoints Used in CSS
Don't just test arbitrary viewports. Match your CSS media query breakpoints:
/* Tailwind CSS defaults */
@media (min-width: 640px) {
/* sm */
}
@media (min-width: 768px) {
/* md */
}
@media (min-width: 1024px) {
/* lg */
}
Test at 375px (mobile), 768px (tablet), 1024px (desktop).
2. Test Touch Interactions, Not Just Clicks
Use .tap() instead of .click() for mobile tests:
await page.locator('button').tap(); // Better for mobile
3. Check Performance on Slow Networks
Use network throttling to simulate real-world conditions (3G/4G).
4. Validate Touch Target Sizes
WCAG recommends touch targets be at least 44x44px. Test this:
test('buttons should be large enough for touch', async ({ page }) => {
await page.goto('https://example.com');
const button = page.locator('button.submit');
const box = await button.boundingBox();
expect(box.width).toBeGreaterThanOrEqual(44);
expect(box.height).toBeGreaterThanOrEqual(44);
});
5. Use Visual Regression Testing
Take screenshots at multiple viewports and compare against baselines:
await expect(page).toHaveScreenshot('homepage-mobile.png');
When to Use Real Devices vs. Emulation
Use Emulation For:
- Responsive layout testing: Quick feedback on breakpoints.
- CI/CD pipelines: Fast, automated tests.
- Early development: Iterative testing during feature development.
Use Real Devices For:
- Touch gestures: Emulation doesn't perfectly replicate swipe mechanics.
- Browser-specific bugs: Safari on iOS has quirks that WebKit emulation may miss.
- Performance testing: Real device hardware affects performance.
- Hardware features: Camera access, accelerometer, NFC.
Recommended Approach: 80% emulation (Playwright), 20% real device testing (BrowserStack, physical devices).
Mobile Testing in CI/CD
name: Mobile Web Tests
on: [pull_request, push]
jobs:
mobile-tests:
runs-on: ubuntu-latest
strategy:
matrix:
device: ['iPhone 13', 'Pixel 5', 'iPad Pro']
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '22'
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test --project="${{ matrix.device }}"
- uses: actions/upload-artifact@v3
if: failure()
with:
name: playwright-report-${{ matrix.device }}
path: playwright-report/
This workflow runs your tests on 3 different devices in parallel.
Conclusion
Mobile web testing is no longer optional�it's essential. With Playwright's powerful device emulation, you can test responsive design, touch interactions, geolocation, and mobile-specific features without needing a fleet of physical devices.
Start by emulating the most common devices (iPhone 13, Pixel 5, iPad), test at your CSS breakpoints, simulate touch gestures, and validate performance on slow networks. For critical flows, supplement with real device testing via BrowserStack or physical devices.
Ready to master mobile web testing? Sign up for ScanlyApp and integrate comprehensive mobile testing into your QA workflow.
