📖 The Story: The Desktop Hubris

A SaaS company launched a highly anticipated promotional campaign. The QA team ran the Playwright suite across Chrome, Firefox, and WebKit on 24-inch desktop monitors. Everything was green. They deployed.

Two hours later, customer support was flooded. Mobile users couldn't close the promotional pop-up. The "X" button was positioned absolutely, and on screens narrower than 768px, it shifted off-screen behind a sticky header.

The QA team hadn't configured a mobile viewport. Their tests clicked the "X" perfectly at 1920x1080, completely missing the mobile CSS overflow bug. The campaign was pulled, and the company lost thousands in ad spend.

The next day, the team added a Mobile Safari project to their Playwright config using the built-in devices descriptor. The test immediately failed, revealing the hidden button. They fixed the CSS and re-launched safely.

Desktop testing is half the battle. If it doesn't work on mobile, it doesn't work.

🎯 Why Mobile Emulation?

📱

Responsive Design

Catch CSS breakpoints and layout shifts before they reach real mobile devices.

👆

Touch Events

Ensure buttons respond to taps, swipes, and long-presses, not just mouse clicks.

📍

Geolocation

Mock GPS coordinates to test location-based features like store locators.

📶

Slow Networks

Simulate 3G connections to ensure your loading spinners and timeouts behave correctly.

🌍 Real World Example

A food delivery app has a map that centers on the user's current GPS location. In standard tests, the browser prompts "Allow location access?". On mobile, this prompt often blocks the entire screen. By emulating an iPhone and pre-granting geolocation permissions with specific coordinates, you can test the map's UI without the browser prompt blocking the view.

🧒 Explain Like I'm 10

Imagine you're testing a new video game, but you only test it on a massive 80-inch TV. It looks great! But when your friend plays it on a tiny 5-inch handheld screen, the text is unreadable and the buttons are too small.

Mobile emulation is like putting a special mask over your browser. It shrinks the screen, changes the browser's ID card (User Agent), and turns your mouse into a finger. This way, you can test the "handheld screen" version without actually owning the device.

🎓 Professional Explanation

Playwright provides a devices object exported from @playwright/test. This object contains curated configurations for popular smartphones and tablets (e.g., iPhone 14, Pixel 7, iPad Pro). When applied to a BrowserContext, Playwright automatically configures the viewport, userAgent, deviceScaleFactor, and hasTouch.

This doesn't just resize the window; it tricks the web server into thinking the request is coming from an actual mobile device, triggering the correct SSR (Server-Side Rendering) or responsive CSS.

📊 The Device Matrix

Common Device Emulation Targets

💻
Desktop Chrome

1920x1080
Standard Mouse

📱
iPhone 14

390x844
Safari WebKit / Touch

📱
Pixel 7

412x915
Chrome Android / Touch

📱 Using Device Descriptors

The easiest way to emulate a device is in your playwright.config.ts using a Project.

typescript · playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  projects: [
    {
      name: 'Desktop Chrome',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'Mobile Safari',
      use: { ...devices['iPhone 14'] },
    }
  ]
});

What does devices['iPhone 14'] do? It automatically sets viewport: { width: 390, height: 844 }, userAgent: '...iPhone...', deviceScaleFactor: 3, isMobile: true, and hasTouch: true. It's a complete mobile disguise.

👆 Touch Events and Interactions

When hasTouch is enabled, Playwright maps standard mouse events (click, hover) to touch events automatically. However, you can also use explicit touch methods.

typescript
// Standard click works, Playwright sends a touch event
await page.getByRole('button').click();

// Explicit tap
await page.getByRole('button').tap();

// Simulate a swipe (e.g., to dismiss a mobile notification)
await page.getByTestId('notification').dragTo(
  page.locator('body'), 
  { force: true }
);

📍 Mocking Geolocation

Testing store locators or map apps requires GPS. You can pre-grant permissions and set coordinates.

typescript
test('finds nearest store', async ({ page, context }) => {
  // 1. Grant permission for geolocation so the browser prompt doesn't appear
  await context.grantPermissions(['geolocation']);
  
  // 2. Set the GPS coordinates (e.g., New York City)
  await context.setGeolocation({ 
    latitude: 40.7128, 
    longitude: -74.0060 
  });

  await page.goto('/store-locator');
  
  // 3. Assert the app thinks we are in New York
  await expect(page.getByText('New York Stores')).toBeVisible();
});

📶 Simulating Network Conditions

Mobile users often have slow connections. Test how your app handles latency.

typescript
test('shows loading spinner on slow 3G', async ({ page, context }) => {
  // Simulate Slow 3G
  await context.setOffline(false); // Ensure we are online
  
  // Playwright doesn't have a native setNetworkCondition, but we can mock delays
  await page.route('**/api/data', async route => {
    await new Promise(r => setTimeout(r, 3000)); // 3 second delay
    await route.continue();
  });

  await page.goto('/dashboard');
  // Assert the spinner appears and stays visible during the delay
  await expect(page.getByTestId('loading-spinner')).toBeVisible();
});

⚠️ Common Mistakes

Mistake 1: Resizing the Window Instead of Emulating

typescript · ❌ Wrong Approach
// ❌ BAD: Only changes size. Server still thinks it's a desktop.
await page.setViewportSize({ width: 390, height: 844 });

// ✅ GOOD: Changes size, user agent, and touch capabilities.
await context.newPage({ ...devices['iPhone 14'] });

If you just resize the window, the backend won't serve the mobile HTML bundle because the User-Agent header still says "Windows". Always use device descriptors.

✅ Best Practices

  1. Use the built-in devices registry. Don't hardcode user agents; they change frequently.
  2. Test mobile interactions explicitly. If a menu requires a swipe to close, use dragTo rather than a click.
  3. Grant permissions early. Grant geolocation in beforeEach to avoid prompt flakiness.
  4. Don't emulate every device. Test one iOS device (WebKit) and one Android device (Chromium) to cover 95% of bases.

🏗️ Senior Engineer Deep Dive

User Agent vs Client Hints

Historically, mobile detection relied on parsing the User-Agent string. Modern browsers are moving towards User-Agent Client Hints (Sec-CH-UA-Mobile). Playwright's device descriptors set both automatically, ensuring compatibility with both legacy server checks and modern frameworks.

Device Scale Factor (Retina Displays)

Why does deviceScaleFactor: 3 matter? On a real iPhone, one CSS pixel equals 3 physical pixels. This affects image rendering (srcset) and canvas resolution. If your app serves 1x images to a 3x screen, it will look blurry. Emulating the scale factor ensures you catch missing high-res assets.

💼 Interview Questions

Beginner
How do you run a Playwright test as if it were on an iPhone?
Show Answer
I import devices from @playwright/test and create a project in the config file. I spread devices['iPhone 14'] into the project's use object. This configures the viewport, user agent, and touch events for that project.
Intermediate
What is the difference between using setViewportSize and a device descriptor?
Show Answer
setViewportSize only changes the dimensions of the browser window. A device descriptor changes the dimensions, but also changes the User-Agent string, enables touch events (hasTouch), sets the isMobile flag, and configures the device scale factor. This is necessary for backend SSR to serve mobile templates.
Advanced
How do you test a location-based feature without triggering the browser's "Allow Location" prompt?
Show Answer
I use context.grantPermissions(['geolocation']) before navigating to the page. Then I use context.setGeolocation({ latitude, longitude }) to set the specific GPS coordinates. This bypasses the UI prompt and feeds the mock location directly to the Geolocation API.
Scenario
A mobile user reports that a dropdown menu closes immediately when they try to open it. How would you debug this with Playwright?
Show Answer
I would run the test using an Android or iOS device descriptor. I would use the Trace Viewer to inspect the touch events. Likely, the dropdown opens on touchstart but an overlapping element is immediately triggering a touchend or click event on the body, closing it. I'd use page.tap() in slow motion to see the event sequence.

🏋️ Practical Exercise

🎯 Hands-On Practice Easy

Add a Mobile Project

  1. Open playwright.config.ts.
  2. Add a new project named "Mobile" that uses devices['iPhone 14'].
  3. Run the suite using --project=Mobile.
  4. Watch the test run. Notice the browser window is now shaped like a phone.
  5. Take a screenshot in the test to see the mobile layout.

🚀 Mini Project

🏗️ The GPS Store Locator

  1. Write a test that navigates to a site with a map (e.g., Google Maps or a local store locator).
  2. Configure the context to emulate a Pixel 7.
  3. Grant geolocation permissions.
  4. Set the GPS coordinates to your actual home city.
  5. Assert that the map centers on those coordinates (or that the text "Stores near [Your City]" appears).

📝 Quiz

Test your understanding. Click an option to check your answer.

1. Which object from @playwright/test contains pre-configured mobile device settings?
A) browsers
B) phones
C) devices
D) mobile
Answer: C — The devices object exports configurations for popular phones and tablets.
2. What does devices['iPhone 14'] automatically configure?
A) Viewport size and User Agent
B) hasTouch: true and isMobile: true
C) Device Scale Factor (Retina)
D) All of the above
Answer: D — It configures viewport, UA, touch capabilities, mobile flag, and scale factor.
3. How do you mock the user's GPS coordinates?
A) page.setGPS(lat, long)
B) context.setGeolocation({ latitude, longitude })
C) page.mockLocation()
D) devices.setLocation()
Answer: B — The context's setGeolocation method sets the coordinates.
4. Why is setViewportSize not enough for mobile testing?
A) It doesn't actually resize the window.
B) It doesn't change the User-Agent or enable touch events.
C) It is deprecated in Playwright.
D) It only works in headless mode.
Answer: B — Resizing the window doesn't trick the backend into serving mobile HTML, and it keeps mouse events instead of touch.
5. How do you prevent the browser's "Allow Location Access" prompt from blocking your test?
A) Click "Allow" really fast
B) Use context.grantPermissions(['geolocation'])
C) Turn off notifications in the OS
D) Use page.route to block the prompt
Answer: B — Granting permissions in the context pre-approves the prompt, so it never appears.
6. When hasTouch is enabled, what happens when you call page.click()?
A) It throws an error; you must use tap().
B) Playwright automatically maps it to a touch event.
C) It performs a right-click.
D) It scrolls the page instead of clicking.
Answer: B — Playwright handles the mapping, so click() works seamlessly on touch devices.
7. What does deviceScaleFactor: 3 mean?
A) The viewport is 3 times wider than normal.
B) One CSS pixel equals 3 physical pixels (Retina display).
C> The test runs 3 times faster.
D) It zooms out by 300%.
Answer: B — It emulates high-DPI screens, ensuring you test high-resolution image rendering.
8. Can you simulate a slow network in Playwright?
A) Yes, using context.setNetworkCondition('Slow 3G')
B) Yes, by using page.route and adding a setTimeout before fulfilling
C) No, Playwright only runs on localhost
D) Yes, but only in Chromium
Answer: B — While Playwright doesn't have a native throttling flag like Chrome DevTools, you can easily mock latency by delaying route.continue().
9. Where is the best place to configure mobile emulation?
A) Inside every individual test file
B) In playwright.config.ts as a Project
C) In the CI pipeline script
D) In the HTML reporter
Answer: B — Defining it as a project allows you to run mobile tests independently via --project=Mobile.
10. How can you test a "swipe to delete" gesture?
A) page.swipe()
B) element.dragTo(target) or mouse drag with coordinates
C) page.click() with force: true
D) You cannot test swipes in Playwright
Answer: B — You simulate the swipe by dragging the element to another locator or by using specific X/Y coordinates.

❓ FAQ

Q: Does Playwright mobile emulation replace testing on real devices (BrowserStack)?

A: No. Emulation mimics the viewport and user agent, but it doesn't run the actual iOS Safari engine or Android Chrome engine perfectly. It uses the desktop engine disguised as mobile. For 100% accuracy, you still need real device clouds for final regression.

Q: Can I emulate dark mode for mobile?

A: Yes. Use colorScheme: 'dark' in the use object alongside the device descriptor.

📦 Summary

🎯 Key Takeaways

  • Use the devices registry to emulate phones and tablets accurately.
  • Emulation changes viewport, user agent, touch, and scale factor.
  • Use context.grantPermissions and setGeolocation to test GPS features.
  • Simulate latency by delaying mocked API routes.
  • Configure devices in playwright.config.ts projects to run them in parallel.