Skip to content

Chapter 12 · Device Emulation and Multi-Browser ​

Chapter goals: Learn to simulate phones/tablets with built-in device descriptors, configure geolocation, language, timezone, and dark mode, and extend the test matrix across Chromium, Firefox, and WebKit.

12.1 Device Descriptors: The devices Dictionary ​

Playwright ships a built-in registry of common device parameters (user agent, screen size, viewport, touch support) — one line to "become" an iPhone:

python
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    iphone = p.devices["iPhone 13"]          # Pull the full params dict for this device
    browser = p.webkit.launch()              # Mobile devices recommended with WebKit (iOS engine)
    context = browser.new_context(**iphone, locale="zh-CN")
    page = context.new_page()
    page.goto("https://m.example.com")

Device-engine pairing

Official examples pair iPhone with WebKit — because real iOS Safari runs on WebKit, giving the highest simulation fidelity; Android devices (Pixel, etc.) should pair with Chromium. Using the wrong engine won't error, but you'll lose simulation realism.

One detail: pre-bundled desktop device params (like "Desktop Chrome") ship with a Windows UA. To use your native platform's real UA, explicitly override to None:

python
desktop = p.devices["Desktop Chrome"]
context = browser.new_context(**desktop, user_agent=None)

12.2 Hand-Crafting Emulation Params ​

When not using presets, context params can be set directly. These params are locked at context creation; viewport can be adjusted per-page dynamically:

python
context = browser.new_context(
    viewport={"width": 1280, "height": 720},     # Viewport size
    is_mobile=True,                               # Mobile flag (affects meta viewport handling)
    has_touch=True,                               # Enable touch events
    device_scale_factor=2,                        # HiDPI screen, affects screenshot clarity
)

# Per-page viewport adjustment (responsive breakpoint testing)
page.set_viewport_size({"width": 375, "height": 812})
expect(page.get_by_test_id("mobile-menu")).to_be_visible()

The distinction between is_mobile and has_touch bears emphasis: the former tells the page to render itself as mobile (responsive layout kicks in); the latter only decides whether touch events are available — simulating iPad portrait typically requires both.

12.3 Locale, Timezone, and Permissions ​

Internationalization and localization behavior is the part manual testing most easily misses; Playwright can simulate precisely:

python
context = browser.new_context(
    locale="de-DE",                          # UI language / formatting locale
    timezone_id="Europe/Berlin",             # IANA timezone
    geolocation={"latitude": 52.52, "longitude": 13.405},
    permissions=["geolocation"],             # Pre-authorize to skip the permission dialog
)

page.goto("https://example.com")
page.evaluate("navigator.language")          # -> 'de-DE'

Common permissions also include "notifications", "camera", "microphone". To test the "user denied location" scenario, grant first then revoke with context.clear_permissions().

12.4 Dark Mode and UA Spoofing ​

Dark mode adaptation bugs are a frequent production issue; color_scheme switches it in one shot:

python
dark = browser.new_context(color_scheme="dark")
page = dark.new_page()
page.emulate_media(color_scheme="dark")      # Per-page switch at any time
body_bg = page.evaluate(
    "getComputedStyle(document.body).backgroundColor"
)
print(body_bg)                                # Assert it is not pure white → dark mode adapted

UA spoofing verifies "site graceful degradation for old browsers" logic:

python
context = browser.new_context(user_agent="Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X)")
page.goto("https://example.com")
assert page.get_by_text("Please upgrade your browser").is_visible()

UA is just a string

Changing the UA does not change engine capabilities — CSS/JS feature support is still determined by the real engine. To test engine differences, run the multi-engine matrix honestly; don't rely on spoofing.

12.5 Three-Engine Matrix Testing ​

pytest-playwright spreads the same test suite across multiple engines with a single --browser flag:

bash
pip install pytest-playwright playwright
playwright install chromium firefox webkit   # Install all three engines

Add to pytest.ini:

ini
[pytest]
addopts = --browser chromium --browser firefox --browser webkit

Or run inline:

bash
pytest --browser chromium --browser firefox --browser webkit

Each engine runs the full suite independently — failure in one engine shows up as a separate test item in the report. This is the cheapest way to catch cross-engine bugs before they reach production.

12.6 Chapter Summary ​

  • p.devices["iPhone 13"] pulls a full params dict; pair iPhones with WebKit, Android with Chromium;
  • is_mobile vs has_touch: one controls rendering, the other controls event type;
  • locale, timezone_id, geolocation, permissions configure environmental features at context creation;
  • color_scheme one-line dark mode switch; UA spoofing for legacy-browser logic;
  • --browser chromium --browser firefox --browser webkit runs the same suite on all three engines.

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. Which engine should you pair with an iPhone 13 device descriptor for the most realistic simulation?

2. What is the difference between is_mobile and has_touch?

3. How do you pre-authorize geolocation to skip the browser permission dialog?

4. To run the same test suite against all three engines, the pytest flag is?

🛠️ Hands-On Exercises ​

  1. Create contexts with p.devices["iPhone 13"] + WebKit and p.devices["Pixel 5"] + Chromium; visit a responsive site and assert the mobile menu appears on both.
  2. Set locale="ja-JP" and timezone_id="Asia/Tokyo"; verify new Date().toLocaleString() reflects Japanese formatting.
  3. Run the existing test suite with --browser chromium --browser firefox --browser webkit and compare pass rates across engines.

Proceed to Chapter 13: Data-Driven and Parameterized E2E.