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:
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:
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:
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:
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:
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 adaptedUA spoofing verifies "site graceful degradation for old browsers" logic:
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:
pip install pytest-playwright playwright
playwright install chromium firefox webkit # Install all three enginesAdd to pytest.ini:
[pytest]
addopts = --browser chromium --browser firefox --browser webkitOr run inline:
pytest --browser chromium --browser firefox --browser webkitEach 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_mobilevshas_touch: one controls rendering, the other controls event type;locale,timezone_id,geolocation,permissionsconfigure environmental features at context creation;color_schemeone-line dark mode switch; UA spoofing for legacy-browser logic;--browser chromium --browser firefox --browser webkitruns 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
- Create contexts with
p.devices["iPhone 13"]+ WebKit andp.devices["Pixel 5"]+ Chromium; visit a responsive site and assert the mobile menu appears on both. - Set
locale="ja-JP"andtimezone_id="Asia/Tokyo"; verifynew Date().toLocaleString()reflects Japanese formatting. - Run the existing test suite with
--browser chromium --browser firefox --browser webkitand compare pass rates across engines.
Proceed to Chapter 13: Data-Driven and Parameterized E2E.