Playwright Mobile Testing: How to Test on Real Devices and Emulators (2026 Guide)
Set up playwright mobile testing on emulators and real devices with working code, cloud platform tips, and CI pipeline examples.

Playwright mobile testing lets you validate web apps on mobile viewports without leaving your existing Playwright setup. Over 60% of global web traffic comes from mobile devices (Statcounter, 2024), and the mobile testing market hit $11.93 billion in 2025 (Global Growth Insights).
The pattern is familiar. Your test suite passes on Chrome desktop. Then a checkout form breaks on a Pixel 8 at 393px, or a swipe gesture dies on an iPhone 15 in Safari. Emulators catch layout problems. Real devices catch everything else.
This guide walks through playwright mobile testing from first config to production CI pipeline. Every code example runs against a live store at storedemo.testdino.com, so you can copy, paste, and see the results yourself.
Definition: Playwright mobile testing is the practice of using Playwright to validate web applications on mobile viewports. It works through built-in device emulation (spoofing viewport, user agent, and touch support) or by connecting to real physical devices via cloud providers.
What is playwright mobile testing and how does it work?
Playwright is a browser automation framework built by Microsoft. It controls Chromium, Firefox, and WebKit through a single API. For mobile testing, it operates in two modes.
Mode 1: Device emulation. Playwright ships with a registry of 100+ device descriptors. Each one defines viewport width, height, device scale factor, user agent string, and touch support. When you pick a profile like iPhone 13, Playwright configures the browser context to mimic that device.
Mode 2: Real device testing. Playwright connects to real Android and iOS hardware through cloud providers. These platforms expose devices via WebSocket or CDP (Chrome DevTools Protocol) connections. Playwright targets remote browsers the same way it targets local ones.
Note: Playwright handles mobile web testing only. It automates browsers and WebViews, not native app UIs. For native apps, use Appium or Maestro instead.
Here is what happens under the hood when you select a device profile:
-
Playwright reads the descriptor from its internal JSON registry.
-
A new browser context is created with matching viewport, userAgent, deviceScaleFactor, isMobile, and hasTouch properties.
-
The browser renders pages as if running on that device.
-
Pointer events dispatch as touch events instead of mouse events.
This is how the Playwright architecture works under the hood. Device parameters travel from the Playwright client to the browser server process, which applies them before rendering.
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
projects: [
{
name: "Mobile Chrome",
use: { ...devices["Pixel 5"] },
},
{
name: "Mobile Safari",
use: { ...devices["iPhone 13"] },
},
],
});
This config creates two test projects. Every test runs once on Pixel 5 emulation and once on iPhone 13 emulation. Playwright handles viewport resizing, user agent spoofing, and touch event routing automatically.
Setting up playwright device emulation (step-by-step)
Below is the full setup from scratch. Every test targets the TestDino Demo Store at storedemo.testdino.com, so you can run them yourself.
Step 1: Install Playwright (v1.20+ recommended).
npm init -y
npm install -D @playwright/test
npx playwright install
The last command downloads browser binaries for Chromium, Firefox, and WebKit.
Step 2: Configure mobile device profiles.
Open playwright.config.ts and add mobile projects. Following Playwright best practices keeps your config clean as device targets grow.
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./tests",
timeout: 30000,
use: {
baseURL: "https://storedemo.testdino.com",
trace: "on-first-retry",
},
projects: [
{ name: "Desktop Chrome", use: { ...devices["Desktop Chrome"] } },
{ name: "Pixel 5", use: { ...devices["Pixel 5"] } },
{ name: "iPhone 13", use: { ...devices["iPhone 13"] } },
{ name: "Galaxy S9+", use: { ...devices["Galaxy S9+"] } },
],
});
Step 3: Write a test that validates mobile navigation.
On desktop, the Demo Store shows a full nav bar. On mobile viewports, it collapses into a hamburger menu. This test verifies that behavior.
import { test, expect, devices } from '@playwright/test';
test.use({
...devices['Pixel 5'],
});
test('hamburger menu opens on mobile viewport', async ({ page }) => {
await page.goto('/');
await page.getByTestId('header-menu-icon').click();
await page.getByTestId('header-menu-all-products').nth(1).click();
});
Step 4: Write a test that adds a product to cart on mobile.
import { test, expect } from "@playwright/test";
test("add product to cart on mobile viewport", async ({ page }) => {
await page.goto("/");
// Tap "Shop Now" on the hero section
await page.getByRole("link", { name: "Shop Now" }).tap();
// Open the first product detail page
await page.locator(".product-card").first().click();
// Wait for the Add to Cart button to appear
await expect(page.getByRole("button", { name: "ADD TO CART" })).toBeVisible();
// Tap Add to Cart
await page.getByRole("button", { name: "ADD TO CART" }).tap();
// Verify the cart badge updates
const cartBadge = page.locator('[class*="badge"]').first();
await expect(cartBadge).toBeVisible();
});
Using the right Playwright assertions like toBeVisible() ensures your mobile tests wait for elements to render before interacting.
Step 5: Run the tests.
npx playwright test --project="Pixel 5"

Tip: Run npx playwright test --project="iPhone 13" --headed to watch the test visually. This helps debug layout issues that only appear on specific viewports.
Step 6: Define custom device profiles.
If the built-in profiles do not match your target, create a custom one.
{
name: 'Custom Android Tablet',
use: {
viewport: { width: 800, height: 1280 },
userAgent: 'Mozilla/5.0 (Linux; Android 13; SM-X200) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
},
}
Custom profiles make playwright mobile testing adaptable to any hardware target, including tablets and foldable devices. For more config options, Playwright test automation covers the full landscape.
Emulation vs real device testing: what actually changes?
This is the core question every team faces. Emulation is fast and free. Real device testing is accurate but costs money. The answer depends on what you are validating.
| Factor | Emulation | Real device testing |
|---|---|---|
| Speed | Fast. Runs locally, no network latency. | Slower. Network round-trip to cloud device. |
| Cost | Free. Included with Playwright. | Paid. Cloud platform subscription required. |
| Rendering accuracy | Desktop browser engine. WebKit on desktop differs from Safari on iOS. | Actual device browser and rendering engine. |
| Touch/gesture fidelity | Simulated. Cannot replicate multi-touch hardware behavior. | Real touch screen and gesture processing. |
| Performance metrics | Misleading. Uses host machine CPU/GPU. | Accurate. Real CPU, RAM, and GPU constraints. |
| Hardware features | Cannot test GPS, camera, or biometrics. | Full access to device hardware. |
| CI/CD integration | Simple. No external dependencies. | Requires API keys and cloud config. |
| Device coverage | 100+ built-in profiles. Custom profiles possible. | Thousands of real devices across OS versions. |

Tip: A practical ratio: run emulation for 80% of your playwright mobile testing runs (layout, responsive, functional flows). Reserve real device testing for the remaining 20% (Safari rendering, performance benchmarks, hardware features).
The biggest gap appears with iOS Safari. Playwright uses desktop WebKit to emulate Safari, but that is not the same engine as mobile Safari on an iPhone. Apple's mobile Safari has unique scrolling behavior, fixed positioning quirks, and viewport handling that desktop WebKit cannot match.
Teams that skip real device validation for iOS regularly ship Safari-only bugs to production. Playwright visual testing catches visual differences between emulated and real device renders.
A real-world example from our testing: an emulated test on storedemo.testdino.com shows a product grid rendering in 2 columns on an iPhone 13 viewport. The test passes. On a real iPhone 13, the grid renders in 1 column because mobile Safari interprets CSS gap with flex-wrap differently. Emulation cannot catch this.
Here is a test that validates the product grid column layout on mobile:
import { test, expect } from "@playwright/test";
test("product grid shows 2-column layout on mobile", async ({ page }) => {
await page.goto("/");
// Scroll to the product section
const firstCard = page.locator(".product-card").first();
await firstCard.scrollIntoViewIfNeeded();
const firstBox = await firstCard.boundingBox();
const secondBox = await page.locator(".product-card").nth(1).boundingBox();
// On mobile, cards should be side-by-side or stacked
if (firstBox && secondBox) {
const isTwoColumn = Math.abs(secondBox.y - firstBox.y) < 10;
const isStacked = secondBox.y > firstBox.y + firstBox.height / 2;
expect(isTwoColumn || isStacked).toBeTruthy();
}
});

Source: Perfecto 2024 "Mobile Testing Coverage Report" comparing emulation vs physical device defect discovery across 12,000 test suites
How do you run playwright tests on real mobile devices?
Playwright does not ship with built-in real device connectivity. For playwright real device testing, you connect to a remote browser session hosted by a cloud provider.
Three approaches work.
Approach 1: Cloud SDK integration (LambdaTest example)
Most providers offer a WebSocket-based connection. Here is a LambdaTest setup for playwright android testing and playwright ios testing.
Step 1: Install the SDK.
npm install -D lambdatest-node-sdk
Step 2: Configure device capabilities and connect.
import { test, expect, chromium } from "@playwright/test";
const capabilities = {
browserName: "Chrome",
browserVersion: "latest",
"LT:Options": {
platform: "Android",
deviceName: "Pixel 7",
platformVersion: "13.0",
user: process.env.LT_USERNAME,
accessKey: process.env.LT_ACCESS_KEY,
network: true,
console: true,
},
};
test("verify product page loads on real Pixel 7", async () => {
const browser = await chromium.connect(
`wss://cdp.lambdatest.com/playwright?capabilities=${encodeURIComponent(JSON.stringify(capabilities))}`
);
const context = await browser.newContext();
const page = await context.newPage();
await page.goto("https://storedemo.testdino.com");
await expect(page.getByText("Demo E-commerce Testing Store")).toBeVisible();
await page.getByText("All Products").click();
await expect(page.getByPlaceholder("Search products...")).toBeVisible();
await context.close();
await browser.close();
});
Approach 2: BrowserStack SDK integration
BrowserStack uses a YAML config file for device targets.
userName: YOUR_USERNAME
accessKey: YOUR_ACCESS_KEY
platforms:
- deviceName: Samsung Galaxy S23
osVersion: 13.0
browserName: chrome
browserVersion: latest
- deviceName: iPhone 15
osVersion: 17
browserName: safari
browserVersion: latest
parallelsPerPlatform: 2
npm install -D browserstack-node-sdk
npx browserstack-node-sdk playwright test
Approach 3: Experimental Android support (local USB)
Playwright has experimental Android support via ADB (v1.20+). No cloud subscription needed. The Android web testing guide covers this in depth.
import { _android as android } from "playwright";
(async () => {
const [device] = await android.devices();
console.log(`Connected to: ${device.model()}`);
await device.shell("am force-stop com.android.chrome");
const context = await device.launchBrowser();
const page = await context.newPage();
await page.goto("https://storedemo.testdino.com");
const heading = page.getByText("Demo E-commerce Testing Store");
console.log(`Heading visible: ${await heading.isVisible()}`);
await context.close();
await device.close();
})();
Note: The _android API requires: Playwright v1.20 or later, physical Android device connected via USB, ADB daemon running with USB debugging enabled, and Chrome 87+ installed on the device. Screenshots only work when the device screen is awake.
For iOS, Playwright cannot connect to Safari on physical iPhones locally. Apple restricts third-party browser automation. Cloud platforms are the only path for playwright ios testing on real hardware.
Understanding how Playwright locators work is important here. Locators like getByRole and getByText stay consistent across emulation and real devices, so your test code works in both.
Cloud platform comparison: choosing a real device provider
Picking a cloud provider depends on device coverage, budget, and CI integration needs.
| Feature | LambdaTest | BrowserStack | PCloudy |
|---|---|---|---|
| Real device count | 3,000+ (Android & iOS) | 3,500+ (Android & iOS) | 500+ (Android & iOS) |
| Playwright support | WebSocket-based connection | Native SDK integration | CDP-based connection |
| iOS Safari on real iPhone | Yes (full support) | Limited | |
| Parallel execution | Yes (plan-based limits) | Yes (plan-based limits) | Yes (limited) |
| Video recording | Automatic | Automatic | Manual trigger |
| CI/CD integration | GitHub Actions, GitLab, Jenkins, Azure DevOps | GitHub Actions, GitLab, Jenkins, CircleCI | Jenkins, CircleCI |
| Pricing (as of June 2025) | Starts at $15/month | Starts at $29/month | Starts at $100/month |
| Free trial | 100 minutes | 100 minutes | Free trial available |
LambdaTest is the most budget-friendly. BrowserStack has the widest iOS device catalog. PCloudy works well for teams already using Appium for native testing.
When running Playwright parallel execution on cloud platforms, match your worker count to your subscription tier. Too many parallel sessions queue up and slow down your pipeline.
Handling touch events and mobile gestures in playwright
When a device profile sets hasTouch: true, Playwright automatically routes pointer events as touch events. This means page.click() dispatches a TouchEvent on mobile contexts, not a MouseEvent.
Here are working gesture examples against the TestDino Demo Store.
Swipe to scroll through products:
import { test, expect, devices } from '@playwright/test';
test.use({ ...devices['Pixel 5'] });
test('swipe down to load more products', async ({ page }) => {
await page.goto('/');
// Get initial scroll position
const initialScroll = await page.evaluate(() => window.scrollY);
// Perform a swipe-down gesture
await page.touchscreen.tap(200, 400);
await page.mouse.move(200, 400);
await page.mouse.down();
await page.mouse.move(200, 100, { steps: 10 });
await page.mouse.up();
// Verify page scrolled
const newScroll = await page.evaluate(() => window.scrollY);
expect(newScroll).toBeGreaterThan(initialScroll);
});
Tap and hold for product quick-view:
import { test, expect, devices } from '@playwright/test';
test.use({ ...devices['iPhone 13'] });
test('long-press on product card triggers context action', async ({ page }) => {
await page.goto('/');
const productCard = page.locator('.product-card').first();
await productCard.scrollIntoViewIfNeeded();
// Simulate a long press (tap + hold)
const box = await productCard.boundingBox();
if (box) {
await page.touchscreen.tap(box.x + box.width / 2, box.y + box.height / 2);
}
// Verify the product card is interactive
await expect(productCard).toBeVisible();
});

Tip: Always set isMobile: true in your device config. This property controls whether the browser respects the <meta name="viewport"> tag. Without it, pages render at desktop width even if the viewport dimensions are correct.
A common debugging pitfall with playwright mobile automation: page.tap() fails with "element not visible" because a sticky header covers the target. Fix this by scrolling first:
await page.locator(".target-element").scrollIntoViewIfNeeded();
await page.locator(".target-element").tap();
Common mobile test errors and fixes:
| Error | Cause | Fix |
|---|---|---|
| page.tap is not a function | hasTouch not set to true | Use a device profile or set hasTouch: true manually |
| Element is outside of the viewport | Sticky header or fixed nav covering the element | Call scrollIntoViewIfNeeded() before tapping |
| Timeout waiting for selector | Mobile layout uses different selectors or lazy-loads content | Use responsive-safe locators like getByRole or getByTestId |
| Navigation failed on real device | Network timeout on cloud device connection | Increase navigationTimeout in config to 60000ms |

Running mobile tests in CI/CD pipelines
Mobile emulation tests run identically in CI. No special config needed because emulation uses the same browser binaries you install locally.
GitHub Actions example:
Setting up Playwright in GitHub Actions for playwright mobile testing is straightforward.
name: Mobile Tests
on: [push, pull_request]
jobs:
mobile-emulation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test --project="Pixel 5" --project="iPhone 13"
- uses: actions/upload-artifact@v4
if: always()
with:
name: mobile-test-report
path: playwright-report/
real-device-tests:
runs-on: ubuntu-latest
needs: mobile-emulation
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run test:cloud-devices
env:
CLOUD_USERNAME: ${{ secrets.CLOUD_USERNAME }}
CLOUD_ACCESS_KEY: ${{ secrets.CLOUD_ACCESS_KEY }}
This creates a two-stage pipeline:
-
Stage 1 runs on mobile emulation profiles. Fast, free, catches layout and functional regressions.
-
Stage 2 runs on real devices via your cloud provider. Validates rendering on actual hardware.
The needs: mobile-emulation directive gates real device tests behind emulation. If emulation fails, real device runs are skipped, saving cloud minutes.
Note: Store cloud provider credentials as encrypted secrets in your CI system. Never hardcode API keys in config files or commit them to version control.
GitLab CI example:
mobile-tests:
image: mcr.microsoft.com/playwright:v1.49.0-noble
stage: test
script:
- npm ci
- npx playwright test --project="Pixel 5" --project="iPhone 13"
artifacts:
when: always
paths:
- playwright-report/
expire_in: 7 days
When investigating flaky tests in mobile CI runs, pay attention to viewport-dependent selectors and animation timing. The flaky test benchmark report shows mobile viewport tests have a 12% higher flake rate than desktop, primarily from animation timing and touch event delays.
Teams using TestDino alongside their playwright mobile testing CI setup can track pass/fail trends across devices from a single observability dashboard.
Playwright vs Appium vs Maestro vs Detox: picking the right mobile testing tool
Should you use Playwright for mobile testing, or pick a dedicated mobile framework? Four tools dominate the space, each targeting a different slice.
Definition: Detox is a gray-box E2E testing framework built by Wix for React Native apps. Unlike black-box tools, Detox runs inside the app process and auto-syncs with animations, network requests, and async operations.

| Capability | Playwright | Appium | Maestro | Detox |
|---|---|---|---|---|
| Primary use case | Mobile web, PWA, hybrid WebView | Native, hybrid, and mobile web apps | Native mobile UI (iOS & Android) | React Native E2E testing |
| Testing approach | Black-box (browser automation) | Black-box (WebDriver protocol) | Black-box (UI-level) | Gray-box (in-process, app-aware) |
| Language support | JS/TS, Python, Java, C# | Java, Python, Ruby, C#, JS | YAML (declarative) | JS/TS only (Jest integration) |
| Setup complexity | Low (npm install) | High (server, drivers, SDKs) | Low (CLI install) | Medium (native build config) |
| Execution speed | Fast (direct browser protocol) | Moderate (WebDriver overhead) | Fast (no compilation) | Very fast (in-process) |
| Flaky test handling | Auto-wait + retry on assertion | Explicit waits needed, flake-prone | Intelligent UI wait, auto-retry | Auto-sync with animations and network |
| iOS real device | Via cloud platforms only | Yes (XCUITest driver) | Yes (native support) | Yes (simulators + real devices) |
| Android real device | Experimental (_android API) + cloud | Yes (UIAutomator2/Espresso) | Yes (native support) | Yes (emulators + real devices) |
| Cross-browser testing | Chromium, Firefox, WebKit | Limited to device browser | No (app-focused) | No (app-focused) |
| CI/CD friendliness | Excellent (Docker, GitHub Actions) | Good (requires Appium server) | Good (Maestro Cloud) | Excellent (built for CI) |
| Community (GitHub stars) | 70k+ | 18k+ | 7k+ | 11k+ |
When Playwright is the right choice:
-
You are testing a responsive web app, PWA, or hybrid app with WebViews.
-
You need cross-browser coverage (Chromium + Firefox + WebKit) on mobile viewports.
-
Your team already uses Playwright for desktop and wants one framework for playwright mobile browser testing too.
-
You want fast CI/CD feedback with zero external dependencies.
When Appium is the right choice:
-
You are testing a native Android or iOS app with complex interactions.
-
You need hardware access: GPS, camera, biometrics, push notifications.
-
Your team works across multiple platforms and languages.
When Maestro is the right choice:
-
You want fast, no-code UI validation using YAML.
-
Your QA team is not comfortable writing JavaScript.
-
You are testing Flutter, React Native, or SwiftUI apps.
When Detox is the right choice:
-
Your product runs on React Native and needs E2E tests coupled to the app lifecycle.
-
Test flakiness is a major issue due to heavy animations or complex async operations.
-
CI reliability matters more than language flexibility.
A combination many teams use: playwright mobile testing for the web layer, Detox for React Native E2E, and a cloud provider for real device validation. This covers the full spectrum without overloading one tool.
According to Appium market share data, Appium is still the most adopted mobile framework in 2026. But Playwright adoption is growing fast among mobile web teams, driven by the Playwright AI ecosystem and lower setup friction.

Source: Based on JetBrains "State of Developer Ecosystem" surveys 2022-2024 and Stack Overflow Developer Surveys 2022-2025 adoption trend data
Conclusion
Playwright mobile testing gives you two clear paths:
-
Emulation for fast, free validation of responsive layouts and functional flows.
-
Real device clouds for Safari rendering accuracy and hardware-level performance testing.
The same test code runs on both. CI integration needs a YAML config and API credentials.
For responsive web apps, PWAs, or hybrid apps with WebViews, playwright mobile testing covers your needs without a separate framework. For native mobile apps, pair it with Appium, Maestro, or Detox based on your stack.
Start with emulation. Add real devices for critical user flows. Build a BDD-driven test structure so your suite stays maintainable as device targets grow.
Your checkout page does not care which tool found the bug. Ship it working on every screen your users touch.
FAQs

Ayush Mania
Forward Development Engineer



