QA How-To
Playwright device emulation: Examples and Best Practices
Use Playwright device emulation examples for mobile projects, touch, landscape, locale, geolocation, permissions, media, offline mode, and CI matrices.
23 min read | 2,388 words
TL;DR
Useful Playwright device emulation examples combine a clear context profile with a user-visible assertion. Configure mobile descriptors in projects, use viewport and touch for responsive interactions, pair locale with timezone, pair coordinates with permissions, and use media or offline options only in tests that own those states.
Key Takeaways
- Model one product risk per emulation example so failures remain easy to diagnose.
- Use named projects for stable mobile profiles and small `test.use` blocks for focused variants.
- Test behavior around responsive breakpoints instead of sampling arbitrary device marketing names.
- Pair environment settings such as coordinates and permissions when the browser feature requires both.
- Use separate contexts when context creation options differ or when profiles must be compared in one test process.
- Label emulated results accurately and retain real devices for hardware and operating-system coverage.
Playwright device emulation examples should show more than a phone-shaped viewport. A credible recipe connects the browser signals to a real product behavior: navigation collapses, touch controls remain usable, prices and dates format correctly, nearby results follow permission state, dark mode retains meaning, or an offline action fails safely.
The examples below use Playwright Test and current context APIs. They are deliberately independent, so you can adapt the relevant pattern without inheriting a huge universal device fixture. Each section also states what the emulated case does not prove, which is essential when test evidence informs a release decision.
TL;DR
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{ name: 'desktop', use: { ...devices['Desktop Chrome'] } },
{ name: 'mobile-chromium', use: { ...devices['Pixel 7'] } },
{ name: 'mobile-webkit', use: { ...devices['iPhone 13'] } },
],
});
| Example | Main setting | What to assert |
|---|---|---|
| Responsive menu | Device or viewport | Correct navigation is usable |
| Touch control | hasTouch or descriptor |
Touch-triggered state changes |
| Landscape | Overridden viewport and screen | Reflow without clipping |
| Localization | locale, timezoneId |
Product dates, numbers, language |
| Nearby search | geolocation, permissions |
Results and denial fallback |
| User preference | colorScheme, emulateMedia |
Theme and motion behavior |
| Connectivity loss | offline |
Safe error and recovery path |
1. Playwright Device Emulation Examples: A Maintainable Project Matrix
Put stable profiles in playwright.config.ts so every test gets an isolated context with a known setup. Keep project names focused on coverage rather than pretending that a descriptor is physical hardware. A small baseline matrix often covers more meaningful risk than dozens of nearly identical phone presets.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
expect: { timeout: 10_000 },
use: {
baseURL: 'https://shop.example.test',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'desktop-chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'small-touch-chromium',
use: { ...devices['Pixel 7'] },
grep: /@responsive|@critical/,
},
{
name: 'small-webkit-profile',
use: { ...devices['iPhone 13'] },
grep: /@responsive|@critical/,
},
],
});
The grep expressions are ordinary JavaScript regular expressions in a TypeScript config. They limit extra projects to annotated tests, assuming titles or tags include the chosen markers. If the entire suite is genuinely mobile-relevant, remove them. Do not copy tagging syntax without matching the repository's naming convention.
Run one or all profiles explicitly:
npx playwright test --project=desktop-chromium
npx playwright test --project=small-touch-chromium
npx playwright test --project=small-webkit-profile
Check descriptor availability when Playwright is upgraded because the device registry belongs to the installed package. Keep the config and browsers on the same dependency version. The profile is a deterministic set of browser options, not an assertion that the test ran on a retail Pixel or iPhone.
2. Example: Test Both Sides of a Responsive Breakpoint
Responsive behavior is driven by layout conditions, so test around the actual design-system breakpoint. Suppose desktop navigation is visible at 768 CSS pixels and above, while a menu button represents it below 768. Two focused describe blocks express that contract more clearly than cycling through unrelated devices.
import { test, expect } from '@playwright/test';
test.describe('below the navigation breakpoint', () => {
test.use({ viewport: { width: 767, height: 900 } });
test('opens primary navigation from the menu button', async ({ page }) => {
await page.goto('/');
const menuButton = page.getByRole('button', { name: 'Open navigation' });
await expect(menuButton).toBeVisible();
await menuButton.click();
await expect(page.getByRole('navigation', { name: 'Primary' }))
.toBeVisible();
});
});
test.describe('at the navigation breakpoint', () => {
test.use({ viewport: { width: 768, height: 900 } });
test('shows primary navigation without a menu button', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('navigation', { name: 'Primary' }))
.toBeVisible();
await expect(page.getByRole('button', { name: 'Open navigation' }))
.toBeHidden();
});
});
This pair catches off-by-one media-query defects and mismatches between design tokens and component CSS. It does not need mobile user agent or touch because the contract is width-based. Add those signals only if the application deliberately branches on them.
Avoid asserting CSS classes such as md:hidden. The user contract is which navigation control is usable. For complex geometry, add a targeted screenshot and review it, but keep semantic assertions so a failure explains the behavior. The Playwright locator best practices guide shows how role locators remain stable through responsive DOM changes.
3. Example: Touch Input, Pointer Targets, and Swipe Boundaries
Use a descriptor or hasTouch: true when the component registers touch behavior or when you call tap(). The following case validates a touch-opened action sheet with accessible controls. It does not use screen coordinates because the button itself has a semantic target.
import { test, expect, devices } from '@playwright/test';
test.use({ ...devices['Pixel 7'] });
test('opens and completes a touch action sheet', async ({ page }) => {
await page.goto('/saved-items');
const card = page.getByRole('article', { name: 'API Testing Handbook' });
await card.getByRole('button', { name: 'More actions' }).tap();
const sheet = page.getByRole('dialog', { name: 'Item actions' });
await expect(sheet).toBeVisible();
await sheet.getByRole('button', { name: 'Remove from saved items' }).tap();
await expect(card).toHaveCount(0);
await expect(page.getByRole('status')).toHaveText('Item removed');
});
For a genuine swipe surface, use the supported touchscreen API only when the gesture itself is the contract. Coordinate sequences are coupled to geometry, so first locate the surface and calculate a stable path from its bounding box. Keep an alternative accessible control if the product provides one and test that separately.
Touch emulation does not reproduce finger size, device grip, palm rejection, touch-sampling rate, or native scrolling physics. Automated accessibility checks also cannot guarantee the physical target is comfortable. Combine CSS size assertions cautiously with design review and manual tests on supported phones.
If an ordinary button fails only with tap() but succeeds with click(), inspect overlays, touch handlers, pointer-event CSS, and actionability evidence. Do not apply { force: true } until you understand why a user-like action cannot reach the target.
4. Example: Portrait and Landscape Layouts
A descriptor usually represents one orientation. To create a landscape variant, spread the descriptor first, then intentionally override viewport and screen dimensions. The values should come from the product's supported layout contract, not a claim of exact hardware reproduction.
import { test, expect, devices } from '@playwright/test';
const phone = devices['iPhone 13'];
test.use({
...phone,
viewport: { width: 844, height: 390 },
screen: { width: 844, height: 390 },
});
test('keeps checkout actions visible in landscape', async ({ page }) => {
await page.goto('/checkout');
await expect(page.getByRole('heading', { name: 'Review order' }))
.toBeVisible();
await expect(page.getByRole('button', { name: 'Place order' }))
.toBeInViewport();
await expect(page.getByRole('contentinfo')).toBeVisible();
});
toBeInViewport shows whether an element intersects the viewport, but it does not prove there is no overlap. If a sticky footer can cover the action, assert clickability through a normal click in a non-destructive test state or use a focused visual comparison. Avoid inspecting raw z-index values because an apparently high value can still lose across stacking contexts.
Changing only viewport after the page loads can be useful when rotation response is the product behavior. Call page.setViewportSize, then wait for the visible reflow outcome. Remember that this changes the page viewport and resets screen size to match, which is different from a full operating-system rotation event.
Test long content and zoom-accessible layouts separately. Landscape can expose height constraints, sticky regions, and keyboard overlap, but emulation will not summon a real software keyboard. Verify keyboard-sensitive forms on physical devices.
5. Example: Locale, Timezone, and Date Boundaries
Locale and timezone frequently interact. A timestamp close to midnight can display on a different calendar day depending on the user's zone, while price formatting follows locale. Put both in context options and use fixed server fixtures so the expected result does not depend on the Node worker.
import { test, expect } from '@playwright/test';
test.use({
locale: 'en-GB',
timezoneId: 'Asia/Kolkata',
});
test('shows the scheduled interview in the browser timezone', async ({ page }) => {
await page.route('**/api/interviews/INT-18', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
id: 'INT-18',
startsAt: '2026-07-13T20:30:00Z',
fee: 1250,
currency: 'INR',
}),
});
});
await page.goto('/interviews/INT-18');
await expect(page.getByTestId('interview-date')).toHaveText('14 July 2026');
await expect(page.getByTestId('interview-time')).toHaveText('02:00');
await expect(page.getByTestId('interview-fee')).toContainText('₹');
});
This scenario proves a day rollover from the UTC fixture to the emulated browser zone. A companion project can use America/Los_Angeles to cover the other side of the boundary. Do not calculate expected strings with an unconfigured Node Intl.DateTimeFormat, because CI timezone and ICU data can make the assertion self-inconsistent.
Locale does not automatically translate application copy unless the application selects messages from browser preference. Test the actual selection mechanism, which might be a stored account setting, URL prefix, or Accept-Language behavior. For text expansion and right-to-left risks, assert layout and interaction in addition to the chosen language.
6. Example: Geolocation Granted, Denied, and Changed
Location-aware features need more than a happy coordinate. Cover permission granted, permission denied, and location changed if the product reacts while open. Keep the granted permission scoped to the application origin when possible.
import { test, expect } from '@playwright/test';
const origin = 'https://shop.example.test';
test('updates pickup stores after location changes', async ({ page, context }) => {
await context.grantPermissions(['geolocation'], { origin });
await context.setGeolocation({ latitude: 40.7128, longitude: -74.0060 });
await page.goto('/pickup');
await page.getByRole('button', { name: 'Use current location' }).click();
await expect(page.getByTestId('location-label')).toHaveText('New York area');
await context.setGeolocation({ latitude: 42.3601, longitude: -71.0589 });
await page.getByRole('button', { name: 'Refresh location' }).click();
await expect(page.getByTestId('location-label')).toHaveText('Boston area');
});
For denial, create a test without granting permission or clear permissions before the workflow:
test('allows manual search when location is unavailable', async ({ page, context }) => {
await context.clearPermissions();
await page.goto('/pickup');
await page.getByRole('button', { name: 'Use current location' }).click();
await expect(page.getByRole('alert'))
.toContainText('Enter a city or postal code');
await expect(page.getByLabel('City or postal code')).toBeVisible();
});
Browser behavior for permission prompts and supported permission names varies. Playwright grants or clears context permission state, but it does not automate native operating-system location settings. A real-device check is still needed where the app integrates installed-browser prompts, precise-location toggles, or native wrappers.
7. Example: Dark Mode, Reduced Motion, and Print CSS
User preference examples should assert how the product adapts. A dark-mode test can verify a semantic theme marker and a visual snapshot for a high-value surface. A reduced-motion test should verify that autoplay or transition behavior changes without depending on a stopwatch.
import { test, expect } from '@playwright/test';
test('renders a readable dark analytics card', async ({ page }) => {
await page.emulateMedia({ colorScheme: 'dark' });
await page.goto('/analytics');
const card = page.getByRole('region', { name: 'Weekly applications' });
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
await expect(card).toHaveScreenshot('weekly-applications-dark.png', {
animations: 'disabled',
});
});
test('offers static controls when reduced motion is requested', async ({ page }) => {
await page.emulateMedia({ reducedMotion: 'reduce' });
await page.goto('/onboarding');
await expect(page.getByTestId('intro-animation')).toHaveAttribute(
'data-playback',
'paused',
);
await expect(page.getByRole('button', { name: 'Continue' })).toBeVisible();
});
When media preference affects initial JavaScript execution, call emulateMedia before navigation. Alternatively, configure colorScheme or other supported options in test.use so the context begins in that state.
Print CSS uses await page.emulateMedia({ media: 'print' }). Assert that navigation and interactive controls are hidden while the printable content and identifiers remain visible. This tests CSS print media, not the native print dialog or printer output.
For visual baselines, stabilize fonts, data, viewport, and browser project. See Playwright screenshot comparison practices before adding broad page snapshots.
8. Example: Offline State and Reconnection
A context can start offline through test.use({ offline: true }), but many product scenarios begin online and then lose connectivity. Playwright exposes context connectivity control in current APIs through browserContext.setOffline, which can model that transition for all pages in the context.
import { test, expect } from '@playwright/test';
test('queues a draft when connectivity is lost', async ({ page, context }) => {
await page.goto('/notes/new');
await page.getByLabel('Note').fill('Review flaky test evidence');
await context.setOffline(true);
await page.getByRole('button', { name: 'Save note' }).click();
await expect(page.getByRole('status'))
.toHaveText('Saved on this device. Waiting to sync.');
await context.setOffline(false);
await page.getByRole('button', { name: 'Sync now' }).click();
await expect(page.getByRole('status')).toHaveText('All changes synced');
});
This example assumes the application provides deterministic local queuing and an explicit sync control. Adapt assertions to the real product. Verify the final server state through an API or UI result, especially if data loss is a release risk.
Offline is binary. It does not emulate high latency, low bandwidth, packet loss, or intermittent radio transitions. Use routing to delay or abort targeted requests when a UI failure state depends on one endpoint, and use a network-conditioning environment for performance claims. Service workers can cache requests or respond while offline, so understand whether they are part of the scenario.
Reset connectivity in cleanup if you create and manage your own context. Test fixtures ordinarily dispose their isolated context, but a custom shared context must never leak offline state into later cases.
9. Example: Compare Profiles with Explicit Browser Contexts
Most suites should use projects, which produce separate tests and clearer reports. Occasionally a diagnostic or library-level test must create two contexts in one process to compare server responses or markup under profiles. Use the browser fixture and close every context.
import { test, expect, devices } from '@playwright/test';
test('serves usable navigation to desktop and mobile profiles', async ({ browser }) => {
const desktopContext = await browser.newContext({
...devices['Desktop Chrome'],
});
const mobileContext = await browser.newContext({
...devices['Pixel 7'],
});
try {
const desktopPage = await desktopContext.newPage();
const mobilePage = await mobileContext.newPage();
await Promise.all([
desktopPage.goto('https://shop.example.test'),
mobilePage.goto('https://shop.example.test'),
]);
await expect(desktopPage.getByRole('navigation', { name: 'Primary' }))
.toBeVisible();
await expect(mobilePage.getByRole('button', { name: 'Open navigation' }))
.toBeVisible();
} finally {
await desktopContext.close();
await mobileContext.close();
}
});
This pattern consumes more resources and combines two profiles into one result. If only one side fails, project-based cases are easier to retry, shard, and diagnose. Use concurrent navigation only when the test environment and data are safe for it.
New pages within a context inherit its context-level emulation. A popup opened from the mobile page therefore shares the profile, permissions, locale, and connectivity. Cross-origin permission scope still matters. Assert the popup workflow with page.waitForEvent('popup') or a context page event as appropriate, not with a fixed wait.
For clean reusable setup, read Playwright fixtures for test isolation.
10. Best Practices for Playwright Device Emulation Examples
Give every example one reason to exist. A breakpoint test proves layout selection. A touch test proves pointer handling. A locale test proves formatting. A permission test proves fallback. Combining all settings into one super-mobile test makes a failure ambiguous and creates a profile no real user may have.
Spread a device descriptor before overrides. Keep creation-only options in projects or test.use. Use runtime methods such as setGeolocation, setOffline, setViewportSize, and emulateMedia only when state transition is the scenario. Restore mutable state when custom contexts outlive a test, and close contexts in finally.
Assert user-visible behavior. Configuration introspection is useful for a diagnostic smoke test, but navigator.userAgent matching a string does not prove navigation, checkout, or accessibility works. Use roles and labels across desktop and mobile, then include targeted visual evidence for clipping or overlap risks.
Select the matrix from analytics, supported-browser policy, defect history, accessibility needs, and product architecture. Do not fabricate support based on Playwright's descriptor list. A descriptor means the browser can emulate those signals, not that product management supports the model.
Finally, state limitations in reports. Emulated WebKit is not physical Mobile Safari. Touch emulation is not a real finger and virtual keyboard. Offline is not poor bandwidth. Honest labels allow emulation to remain a powerful, fast signal while the real-device and network-performance layers cover what it cannot.
Interview Questions and Answers
Q: Show a correct way to override a device descriptor.
I spread the descriptor first and write the override afterward: { ...devices['iPhone 13'], viewport: { width: 360, height: 740 } }. JavaScript's later property wins. I document why the custom size exists so it is not mistaken for the stock descriptor.
Q: How would you test a responsive breakpoint?
I create isolated tests immediately below and at the documented breakpoint. Each test asserts which accessible navigation or control is usable. I do not add user agent or touch unless the product behavior depends on those signals.
Q: When should a test use tap() instead of click()?
I use tap() when touch-specific event handling or pointer modality is the risk and ensure touch is enabled. For ordinary accessible controls intended for any primary pointer, click() is usually the clearer cross-input action. Both should lead to a user-visible assertion.
Q: How do you test a location permission denial?
I create the context without the grant or call clearPermissions, trigger the location workflow, and assert the manual fallback. I avoid trying to click native permission UI because Playwright controls browser-context permission state directly.
Q: How would you model connectivity loss after a page loads?
I load the page online, call context.setOffline(true), trigger the relevant operation, and assert safe pending or error behavior. Then I restore connectivity with setOffline(false) and verify recovery. This models binary connectivity, not slow-network performance.
Q: Why prefer projects over creating multiple contexts in one test?
Projects produce separate results, retries, traces, and sharding units for each profile. Multiple contexts are useful for a focused comparison but consume more resources and combine failures. I use projects for routine coverage.
Q: What evidence still requires a real device?
Installed mobile browsers, operating-system prompts, software keyboards, safe areas, hardware APIs, device performance, scrolling physics, and vendor-specific rendering deserve physical validation. Emulation is excellent for repeatable browser signals but cannot claim those hardware outcomes.
Common Mistakes
- Copying every device descriptor into CI without a risk-based reason.
- Testing arbitrary popular widths instead of documented breakpoint boundaries.
- Spreading a descriptor after custom values and silently losing the overrides.
- Calling
tap()in a context without touch enabled. - Calculating localized expectations with the uncontrolled Node worker timezone.
- Setting coordinates but forgetting geolocation permission.
- Treating dark mode as a color-only screenshot and skipping functional contrast or asset behavior.
- Calling offline mode a slow-network simulation.
- Creating manual contexts without closing them in
finally. - Labeling an emulated project as a physical iPhone or Android device test.
Conclusion
The best Playwright device emulation examples are small experiments with clear product outcomes. Use projects for durable profiles, context options for starting conditions, runtime APIs for deliberate transitions, and accessible assertions for behavior. Keep screenshots focused and configuration overrides explicit.
Adopt the examples that map to your supported experience, then add physical devices for the remaining operating-system, hardware, input, and performance risks. Review the matrix after browser upgrades and major responsive changes, then remove profiles that no longer expose a distinct risk. Clear ownership keeps emulation evidence useful instead of letting the suite become an unmaintained catalog of device names. That balanced strategy produces faster feedback without confusing an emulated browser profile with the device in a user's hand.
Interview Questions and Answers
Write the pattern for a mobile Playwright project.
Import `defineConfig` and `devices` from `@playwright/test`, then add a project whose use options spread a named descriptor. I choose a project name that describes the coverage and run only relevant responsive or critical tests on extra profiles.
How do you test both sides of a responsive breakpoint?
I use isolated describe blocks or projects at one pixel below and at the breakpoint. Each case asserts the accessible behavior selected by the layout. This detects boundary mistakes without coupling to CSS class names.
What is the correct order for descriptor and overrides?
The descriptor is spread first, followed by explicit properties such as viewport, locale, or user agent. Later JavaScript object properties win. Reversing the order is a subtle configuration bug.
How would you test online to offline recovery?
I navigate and prepare data online, set the context offline, attempt the operation, and assert safe local or error state. I restore connectivity and assert synchronization or retry. The final server result receives separate verification when data integrity matters.
How do locale and timezone settings interact?
Locale influences formatting and language preferences, while timezone changes the browser's temporal zone. A timestamp can cross a calendar boundary before locale formats it. I use explicit server fixtures and fixed expected output rather than relying on the worker environment.
When is a manual browser context useful?
It is useful for library-mode control, side-by-side diagnostics, or multiple isolated profiles within one process. I close contexts reliably and recognize that project-based tests provide clearer routine reports, retries, and sharding.
What makes a device emulation example interview-credible?
It connects a specific option to a user-visible risk and names the limitation. I can explain why touch, locale, permission, or offline state is configured, what assertion proves, and which physical-device or integration test remains necessary.
Frequently Asked Questions
What is a simple Playwright mobile emulation example?
Import `devices`, spread a named phone descriptor into a Playwright project, run a responsive test in that project, and assert the mobile navigation or layout behavior. The descriptor configures a browser context, not a real phone.
How do I test a breakpoint with Playwright?
Create isolated tests with viewport widths immediately below and at the documented breakpoint. Assert the user-visible control or layout mode on each side rather than a CSS framework class.
How do I emulate landscape mode in Playwright?
Spread the chosen descriptor first, then override `viewport` and, when relevant, `screen` with landscape dimensions. This models the browser dimensions but does not reproduce every operating-system rotation event.
Can Playwright change geolocation during a test?
Yes. Call `context.setGeolocation` with new coordinates. The change applies to all pages in the context, and the context still needs suitable geolocation permission.
Can Playwright go offline after navigation?
Yes. Use `context.setOffline(true)` to change the context to offline and `setOffline(false)` to restore connectivity. Assert the application's pending, fallback, and recovery behavior.
Should responsive tests use click or tap?
Use `click` for controls that should work with any primary pointer. Use `tap` when touch-specific behavior is the risk, and configure a context with touch enabled.
How do I compare two device profiles in Playwright?
Prefer separate projects for routine coverage. For a focused comparison, use the `browser` fixture to create two explicit contexts, open a page in each, assert the intended behavior, and close both contexts in `finally`.
Related Guides
- Playwright drag and drop: Examples and Best Practices
- Playwright geolocation emulation: Examples and Best Practices
- Playwright mock date and time: Examples and Best Practices
- Playwright popup and new tab handling: Examples and Best Practices
- Playwright tag and grep filters: Examples and Best Practices
- Playwright APIRequestContext: Examples and Best Practices