Skip to main content
Back to Blog
Reference
2026-09-28

Playwright Devices List: Mobile Emulation with devices and Custom Descriptors

playwright devices guide to list, choose, extend, and run mobile emulation in local tests and CI without mistaking it for real-device coverage.

Playwright Devices List: Mobile Emulation with devices and Custom Descriptors

playwright devices is the built-in registry of desktop, tablet, and mobile browser context settings that Playwright exposes through devices from @playwright/test or playwright. In current official sources, Playwright is actively maintained, the latest stable package is 1.63.0, and the registry source lives in the Playwright repository at packages/isomorphic/deviceDescriptorsSource.json. Nothing has been renamed or discontinued, but the list changes as Playwright updates bundled browser versions and device profiles.

Use a named descriptor when you need repeatable mobile browser behavior in CI: viewport, user agent, device scale factor, touch capability, mobile viewport handling, and default browser family. Use a custom descriptor when your product analytics, support tickets, or release criteria require a size or user agent not shipped in the registry. Do not treat either option as a replacement for native device labs. Playwright emulation is browser-context emulation, not a modem, GPU, OS keyboard, thermal, camera, biometric, or hardware sensor test.

For teams building QA skills for AI coding agents, the practical win is consistency. A Claude Code, Cursor, or Copilot workflow can ask for the list, select a descriptor, generate projects, run --project filters, and collect traces without inventing mobile settings. The deeper Playwright E2E complete guide is useful when you are still designing the whole suite, while the Playwright CI GitHub Actions complete guide 2026 covers broader pipeline mechanics.

What The Device Registry Actually Contains

The registry is a map of names to context options. In code, the shape is simple: devices['iPhone 15'], devices['Pixel 7'], devices['Desktop Chrome'], and landscape variants return objects that can be spread into use or browser.newContext(). The official emulation docs describe the registry as selected device parameters, not an exhaustive catalog of every phone sold.

The source JSON confirms the recurring fields. Mobile and desktop descriptors normally include userAgent, viewport, deviceScaleFactor, isMobile, hasTouch, and defaultBrowserType. Some names use chromium by default, while iPhone and Safari-like profiles use webkit. That default is advisory for project generation and human intent; a descriptor can still be mixed incorrectly with a different browser if you override the project badly.

Descriptor fieldWhat it controlsQA consequence
viewportCSS viewport width and heightDrives responsive layout branches and screenshot baselines
userAgentBrowser UA string sent to the appInfluences UA sniffing, analytics bucketing, and server-side rendering
deviceScaleFactorDevice pixel ratioChanges screenshot dimensions and high-DPI asset selection
isMobileMobile viewport behaviorMakes meta viewport matter and affects mobile layout calculations
hasTouchTouch event availabilityExposes tap and touch paths that mouse-only desktop tests miss
defaultBrowserTypeIntended Playwright browser familyHelps pair iPhone profiles with WebKit and Android profiles with Chromium
import { devices } from '@playwright/test';

const names = Object.keys(devices)
  .filter((name) => /iPhone|Pixel|Galaxy/i.test(name))
  .sort();

for (const name of names) {
  const descriptor = devices[name];
  console.log([
    name,
    descriptor.defaultBrowserType,
    descriptor.viewport.width + 'x' + descriptor.viewport.height,
    'dpr=' + descriptor.deviceScaleFactor,
    descriptor.hasTouch ? 'touch' : 'no-touch',
  ].join(' | '));
}

That script is usually better than copying a list from a blog post. The registry changes with Playwright releases, and your lockfile controls the actual list your CI sees. If a pull request upgrades @playwright/test, run a list script before assuming that device names, user agents, or browser versions stayed fixed.

Picking A Descriptor Without Fooling Yourself

The common mistake is selecting one fashionable phone and calling the project "mobile coverage." A descriptor is only a probe. Good coverage comes from choosing probes that represent layout breakpoints, browser engines, touch behavior, and your user population. If your app is a consumer web app with iOS Safari traffic, a WebKit iPhone descriptor is valuable. If the app is an enterprise dashboard used on Android tablets, a Pixel phone alone is an attractive lie.

Start from the failure model. A responsive navigation bug usually needs width, height, and touch coverage. A payment provider redirect may care about Safari storage behavior. A download or camera flow may need a real device or a lower-level browser capability review. A visual regression suite needs stable project names and screenshot paths more than it needs five nearly identical iPhone sizes.

GoalPreferAvoid
Catch mobile layout regressionsOne small phone, one large phone, one tablet if supportedFive phones with the same breakpoint
Verify iOS browser behaviorWebKit project with an iPhone descriptorChromium project named "iPhone"
Verify Android browser behaviorChromium project with a Pixel or Galaxy descriptorAssuming WebKit approximates Android Chrome
Stabilize visual snapshotsFew descriptors, pinned Playwright version, explicit project namesUpdating descriptors and snapshots in the same blind commit
Reproduce support bugCustom descriptor based on reported viewport and UAGuessing from marketing device names
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  reporter: [['html', { open: 'never' }], ['list']],
  projects: [
    {
      name: 'desktop-chrome',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'mobile-safari',
      use: { ...devices['iPhone 15'] },
    },
    {
      name: 'mobile-chrome',
      use: { ...devices['Pixel 7'] },
    },
  ],
});

The names matter because Playwright filters by project name with --project, includes the project name in reports, and uses it in snapshot paths. A project named mobile-safari tells the next engineer what browser family is involved. A project named iPhone is vague because it hides whether the browser is WebKit, a channel override, or a custom context.

Project-Level Use Versus Test-Level Overrides

Put stable device selection in playwright.config.ts. That makes local runs, CI shards, HTML reports, and traces agree. Use test.use() only for a small number of tests that intentionally explore a variation, such as color scheme, geolocation, locale, or a special viewport. If every file calls test.use({ ...devices[...] }), an AI agent has to inspect the entire tree to know what the suite does.

One subtle rule from the official docs is order. If you spread a descriptor and then set viewport, your value wins. If you set viewport first and then spread a descriptor, the descriptor overwrites you. The same applies to isMobile, hasTouch, userAgent, and every other property.

import { test, expect, devices } from '@playwright/test';

test.use({
  ...devices['iPhone 15'],
  colorScheme: 'dark',
  locale: 'en-US',
  timezoneId: 'America/New_York',
});

test('mobile account menu exposes sign out', async ({ page }) => {
  await page.goto('/account');
  await page.getByRole('button', { name: 'Menu' }).tap();
  await expect(page.getByRole('menuitem', { name: 'Sign out' })).toBeVisible();
});

That test is intentionally narrow. It does not prove the whole account area works on every phone. It proves one mobile interaction path under a clear profile. The suite-level project should still carry the broader mobile smoke coverage so individual test files do not become a maze of hidden environment assumptions.

Custom Descriptors For Real Bug Reports

A custom descriptor is just a context options object. Build one when the registry lacks the combination you need, or when support sends a reproducible viewport and user agent from analytics. Keep it in a shared file, give it a name that includes why it exists, and make the values boring. The goal is reproducibility, not a perfect physical-device simulation.

For example, suppose your checkout breaks only for a webview-like Android browser reporting a narrow viewport. You can pin a descriptor, run a focused project, and decide later whether to buy real-device coverage for that flow.

import type { Project } from '@playwright/test';

export const androidWebviewNarrow: Project['use'] = {
  userAgent:
    'Mozilla/5.0 (Linux; Android 14; Mobile) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36',
  viewport: { width: 360, height: 740 },
  deviceScaleFactor: 3,
  isMobile: true,
  hasTouch: true,
  defaultBrowserType: 'chromium',
};
import { defineConfig, devices } from '@playwright/test';
import { androidWebviewNarrow } from './playwright.devices';

export default defineConfig({
  projects: [
    {
      name: 'mobile-chrome',
      use: { ...devices['Pixel 7'] },
    },
    {
      name: 'android-webview-narrow',
      use: androidWebviewNarrow,
      grep: /@mobile-critical/,
    },
  ],
});

Do not build custom descriptors by changing one field at a time until a test passes. That produces a profile nobody can explain. The values should come from browser devtools, request logs, real user monitoring, product analytics, or a support reproduction note. When values are illustrative, label them as illustrative in your team docs.

CI Matrix That Keeps Mobile Honest

In GitHub Actions, use project filters instead of separate config files. Keep the Playwright version in package-lock.json or pnpm-lock.yaml, run npx playwright install --with-deps, and upload reports even on failure. Current official GitHub action majors include actions/checkout@v7, actions/setup-node@v7, and actions/upload-artifact@v7.

name: playwright-mobile

on:
  pull_request:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        project: [desktop-chrome, mobile-safari, mobile-chrome]
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test --project=${{ matrix.project }}
      - uses: actions/upload-artifact@v7
        if: always()
        with:
          name: playwright-report-${{ matrix.project }}-${{ github.run_id }}
          path: playwright-report/
          if-no-files-found: ignore

For fast feedback, add targeted scripts that use Playwright's real filters. Use --project for a device project, -g or --grep for title and tag matching, and --grep-invert when excluding a known expensive tag. Do not ask agents to invent a --device flag for playwright test; device selection belongs in projects. The --device flag is useful for playwright codegen, where Playwright can record under a device profile.

{
  "scripts": {
    "test:e2e": "playwright test",
    "test:e2e:mobile": "playwright test --project=mobile-safari --project=mobile-chrome",
    "test:e2e:mobile-critical": "playwright test --project=mobile-safari -g @mobile-critical",
    "codegen:iphone": "playwright codegen --device='iPhone 15' http://localhost:3000"
  }
}

Ready-made QA skills install from qaskills.sh with the qaskills CLI, but the repo still needs these explicit project names. Agents become much more reliable when the command surface is pinned in package.json instead of buried in chat history.

Assertions That Survive Mobile Differences

Mobile emulation changes layout, timing, and sometimes server responses. Assertions should validate user-observable outcomes without binding to irrelevant pixel trivia. Prefer roles, labels, URLs, response side effects, and semantic state. Use screenshots for high-value visual risks, not as the only proof that checkout, auth, or search works.

Here is a test that checks a mobile navigation flow and settles async work before asserting the side effect. The regex is anchored, and the test first confirms the cart badge exists before comparing its text.

import { test, expect } from '@playwright/test';

test('mobile cart updates after adding a product @mobile-critical', async ({ page }) => {
  await page.goto('/products/sku-123');
  await expect(page).toHaveURL(/\/products\/sku-123$/);

  const badge = page.getByLabel('Cart items');
  await expect(badge).toBeVisible();
  await expect(badge).toHaveText('0');

  await page.getByRole('button', { name: 'Add to cart' }).tap();

  await expect
    .poll(async () => badge.textContent(), {
      message: 'cart badge should reflect the add-to-cart side effect',
    })
    .toBe('1');

  await page.getByRole('link', { name: 'Cart' }).tap();
  await expect(page).toHaveURL(/\/cart$/);
  await expect(page.getByRole('heading', { name: 'Cart' })).toBeVisible();
});

What people get wrong is testing the mobile menu by checking only that a click returns status 200 or that a hamburger button is visible. The risk is not the presence of an icon. The risk is whether the touch path opens the menu, focus lands somewhere useful, the intended target is reachable, and the next page reflects the state change.

Failure Mode: Passing Locally, Broken In CI

A realistic mobile failure looks like this: the iPhone project passes on a developer laptop, but CI fails because the layout chooses the desktop breakpoint and the mobile menu button is missing. The test error says the button with name Menu is not visible. The root cause is usually not Playwright being flaky. It is often a descriptor override order problem or a CI project filter running a desktop project by mistake.

Diagnose it in layers. First, print testInfo.project.name in a temporary trace attachment or inspect the HTML report to confirm the project. Second, capture page.viewportSize() in the trace or test output. Third, check the config order: { viewport: ..., ...devices['iPhone 15'] } does not mean what many people think. The descriptor wins because it comes later. Fourth, verify the app has a correct meta viewport tag, because isMobile makes that tag matter.

SymptomLikely causeFast check
Mobile menu missing in CIWrong project ranConfirm --project and report project name
Screenshot size changed after upgradeDescriptor changed with PlaywrightCompare printed device list before and after lockfile update
Taps fail but clicks passTarget is covered or not touch-readyUse trace viewer and role locators, then inspect hit target
Server sends desktop HTMLUA override missing or overwrittenLog request headers in test environment
Safari-only bug not reproducedRunning iPhone descriptor on ChromiumCheck defaultBrowserType and project browser
import { test, expect } from '@playwright/test';

test('diagnose mobile profile', async ({ page }, testInfo) => {
  await page.goto('/');
  const viewport = page.viewportSize();

  expect(testInfo.project.name).toMatch(/mobile/);
  expect(viewport).not.toBeNull();
  expect(viewport?.width).toBeLessThanOrEqual(430);
});

Remove diagnostic tests after the incident, or mark them as a small smoke check if they protect a known release gate. Permanent diagnostics should assert a real contract, not merely print facts.

What Emulation Cannot Prove

Playwright's device descriptors are powerful because they are cheap, deterministic, and integrated with the same traces and reporters as the rest of the suite. They are limited for the same reason. They run in desktop-hosted browser engines. They do not make macOS WebKit identical to every iOS Safari build, and they do not create Android OEM browser quirks, mobile network loss, real keyboards, push notification behavior, camera permission surfaces, Bluetooth, NFC, battery pressure, or thermal throttling.

Use emulation as your first line of defense. Escalate to real devices when the risk lives below the browser context or when revenue-critical flows depend on platform integration. A mature mobile strategy often has a broad emulated smoke suite on every pull request, a narrower real-device suite on scheduled or pre-release runs, and manual exploratory testing for brand-new flows.

RiskEmulation fitReal-device trigger
CSS breakpoint regressionStrongOnly if device-specific rendering is suspected
Touch navigationGoodComplex gestures, native scrolling issues, or overlays
Safari storage behaviorUseful with WebKitiOS-specific webview or installed PWA behavior
Camera or biometric promptWeakAlways test on hardware or a platform simulator
Performance under heat or poor networkWeakUse device lab plus network shaping
Push notificationsWeakUse real browser and OS notification stack

Maintaining The List Over Time

Treat device descriptors like any other dependency-owned contract. When you upgrade Playwright, skim release notes, run the list script, update snapshots separately from behavior changes, and keep a changelog entry for project additions or removals. If your team uses AI coding agents, put the accepted project names and commands in AGENTS.md or package scripts so an agent does not search stale examples and introduce a non-existent device name.

The registry is intentionally selected, not complete. If marketing asks for "all iPhones," push back with a coverage matrix: small iOS, large iOS, Android Chrome, tablet if it matters, and real-device coverage for platform integrations. More descriptors can reduce confidence when they multiply slow, redundant screenshots and nobody investigates failures.

Frequently Asked Questions

How do I print the current Playwright devices list?

Use Object.keys(devices).sort() from @playwright/test or playwright, and run it against the installed version in your repo. That matters because the registry is versioned with Playwright. A website list may be stale the moment your lockfile changes. For useful output, print the name, default browser type, viewport, scale factor, and touch flag. Store the script as a developer utility if device selection is part of your release checklist.

Is Playwright mobile emulation the same as testing on an iPhone?

No. An iPhone descriptor gives Playwright a WebKit-oriented browser context with mobile viewport, user agent, device scale factor, and touch settings. It does not give you iOS hardware, mobile Safari on every supported OS release, sensors, native keyboards, device memory pressure, or carrier network behavior. It is excellent for deterministic web regression coverage. It is incomplete for native platform integrations and device-specific performance risks.

Should I use one project per device or one test per device?

Prefer one project per stable device profile. Projects make reports, traces, retries, snapshots, and CI matrices readable. Test-level test.use() is best for small, intentional overrides inside a file, such as color scheme or geolocation. If every test owns its own descriptor, maintainers and AI agents must inspect many files to know the actual coverage, and filtering with --project becomes much less useful.

Why did my mobile screenshots change after a Playwright upgrade?

Device descriptors and bundled browser versions can change when Playwright changes. A screenshot diff after an upgrade may reflect a browser rendering change, a descriptor value change, or a real app regression. Print the device descriptor values before and after the upgrade, review the release notes, and update baselines separately from app code when possible. That separation keeps visual approvals from hiding behavior changes.