PRACTICAL GUIDE / Playwright device descriptor overrides

When a Playwright device override silently undoes your mobile test

Learn to override Playwright device profiles without losing touch, screen, pixel density, or user-agent behavior, then prove the active setup in CI.

By The Testing AcademyUpdated August 4, 202623 min read
All field guides
In this guide6 sections
  1. Understand what the preset is actually changing
  2. Build an override that exposes its contract
  3. Diagnose the active profile before changing the test
  4. Separate descriptor mistakes from similar failures
  5. Roll the fix through an existing suite without hiding regressions
  6. Accept the cost, and know when not to emulate a device

What you will learn

  • Understand what the preset is actually changing
  • Build an override that exposes its contract
  • Diagnose the active profile before changing the test
  • Separate descriptor mistakes from similar failures

The mobile navigation test passes, but it is exercising the desktop header squeezed into a narrow window. A teammate changed one viewport value and assumed the rest of the phone profile followed it. The screenshot looks plausible, so the false coverage survives until a real touch device fails.

Understand what the preset is actually changing

Device presets are convenient because they configure a related set of browser-context properties together. That convenience becomes a trap when an override is treated as a cosmetic resize. The safe approach is to state which browser properties the scenario depends on, apply overrides after the preset, and prove the resulting environment inside the page.

The devices registry is a collection of descriptors, not a command that switches Playwright into a physical-phone mode. Spreading devices['iPhone 13'] into a project's use object supplies a viewport, a screen size, a user agent, a device scale factor, touch capability, and mobile behavior. Playwright then uses those values while creating the browser context. The browser still runs on the machine and browser engine selected by the project.

Each property covers a different part of the product's behavior. The viewport influences CSS media queries and values such as window.innerWidth. The screen setting affects the dimensions exposed through window.screen when a viewport is configured, and it is the one descriptor property the test runner does not accept as a top-level use option. The user agent is visible to client code and is also sent in HTTP requests, so a server that varies its response by user agent may return different markup. Device scale factor appears as window.devicePixelRatio and changes the relationship between CSS pixels and device pixels. Touch support affects whether Playwright can perform touch-oriented input. Mobile mode changes how the browser handles mobile viewport rules; Playwright documents that isMobile controls whether the meta viewport is taken into account and enables touch events, and it separately exposes hasTouch as touch capability.

Those properties are correlated in the shipped descriptor, but JavaScript object spread does not preserve that relationship for you. It copies properties in source order. If the same key appears again, the later value wins. This configuration silently ignores the intended override:

use: { viewport: { width: 390, height: 720 }, ...devices['iPhone 13'] }

The descriptor's viewport is copied after the custom value, so it replaces the custom value. There is no Playwright warning because the resulting object is valid. Reverse the order and the custom viewport wins. The same rule applies to userAgent, deviceScaleFactor, hasTouch, and isMobile.

The screen property is the exception, and the exception is easy to miss because the object still type-checks. The test runner builds its context options from a fixed list of use keys: acceptDownloads, bypassCSP, clientCertificates, colorScheme, deviceScaleFactor, extraHTTPHeaders, geolocation, hasTouch, httpCredentials, ignoreHTTPSErrors, isMobile, javaScriptEnabled, locale, offline, permissions, proxy, storageState, timezoneId, userAgent, viewport, baseURL, and serviceWorkers. That list is the _combinedContextOptions fixture in playwright/lib/index.js, and PlaywrightTestOptions in playwright/types/test.d.ts matches it. screen appears in neither. Spreading a device descriptor into use therefore assigns screen to a key nothing forwards, the context is created without it, and the engine reports window.screen as the viewport instead. On Playwright 1.61.1, a project built from ...devices['iPhone 13'] with a 390 by 720 viewport reports window.screen.height as 720, not the descriptor's 844, and the descriptor's screen is silently gone.

The supported channel for it is contextOptions, which the same fixture spreads into the options object before the named keys are written. Anything browser.newContext() accepts can travel that way, screen included. Because the named keys are applied afterwards, contextOptions is the lower-priority half of the merge, so reach for it only for properties that have no dedicated use key rather than as a general override channel. One more detail matters when writing the value: the exported DeviceDescriptor type declares only viewport, userAgent, deviceScaleFactor, isMobile, hasTouch, and defaultBrowserType, so devices['iPhone 13'].screen does not compile even though the runtime object carries it. Write the dimensions as a literal, which also makes the intended screen readable in review.

Nested values are replaced rather than deeply merged. Writing a new viewport means supplying both width and height; it does not patch one field in the preset's viewport. TypeScript normally catches a missing required dimension, but it cannot tell whether a complete replacement expresses the test author's real intent. A valid width and height can still create an incoherent device contract.

Consider a team that shortens the viewport height to keep a sticky checkout button above the fold. Keeping the preset's screen size may be deliberate: the test models the same phone with less page space available because browser chrome or an on-screen keyboard consumes room. Another team may be modeling a different class of handset. In that case, retaining the old screen while replacing the viewport could be wrong. Neither choice is universally safe. The test name and assertions need to say which one is intended.

Context options also have a lifecycle. A page-level resize can alter a page after its context exists, but it does not rebuild the context with a new user agent, device scale factor, touch capability, or mobile mode. Playwright's API documentation also warns that page.setViewportSize() resets the screen size and recommends setting both screen and viewport when creating the context if those values need independent control. Treat that method as a targeted resize, not as a late device conversion.

This distinction explains a common review mistake. A screenshot at 390 CSS pixels proves only that the captured page was narrow at that moment. It does not prove the request used a mobile user agent, that the browser honored a meta viewport tag, that touch input was available, or that a high-density asset path ran. A useful mobile test names the subset it needs and asserts that subset.

Build an override that exposes its contract

Keep the descriptor and the overrides next to each other. Hiding them in several helper layers makes spread order difficult to review and makes project names lie after later edits. A project called iphone is too vague when its viewport and screen no longer match the preset. A name such as webkit-phone-short-viewport tells a future maintainer that one dimension is intentionally nonstandard.

The following configuration retains the documented iPhone 13 profile, changes the viewport height, restores the preset's screen dimensions through contextOptions, and records traces only when a test first fails. The comments describe why the mismatch is intentional and why the screen needs a separate channel. Both the descriptor and the overrides are present in one object, and the overrides appear last.

TypeScript
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

const shortPhoneViewport = {
  ...devices['iPhone 13'],
  // Model reduced page space on the same emulated screen.
  viewport: { width: 390, height: 720 },
  // `screen` is not a test-runner `use` option, so the spread above drops it.
  // contextOptions forwards it to newContext unchanged.
  contextOptions: { screen: { width: 390, height: 844 } },
};

export default defineConfig({
  use: {
    trace: 'retain-on-failure',
  },
  projects: [
    {
      name: 'webkit-phone-short-viewport',
      use: shortPhoneViewport,
    },
    {
      name: 'webkit-desktop-control',
      use: { ...devices['Desktop Safari'] },
    },
  ],
});

This profile is appropriate only if the app's intended behavior is compatible with the retained device values. It should not be copied into a generic mobile project and forgotten. Device descriptors can change when Playwright updates its device registry. That is useful when the goal is to follow Playwright's maintained profile, but it also means a dependency upgrade can change inputs without anyone editing this configuration. The contract test below turns such a change into a reviewable failure.

Read the environment from both Playwright and the page. page.viewportSize() reports the configured viewport known to Playwright. Browser globals show what application code can observe. Attaching the fingerprint to the test result makes CI failures debuggable without asking someone to reproduce the runner's display or parse a screenshot by eye.

TypeScript
// tests/emulation-contract.spec.ts
import { devices, expect, test } from '@playwright/test';

test.use({
  ...devices['iPhone 13'],
  viewport: { width: 390, height: 720 },
  contextOptions: { screen: { width: 390, height: 844 } },
});

test('exposes the short touch-device contract', async ({ page }, testInfo) => {
  const html =
    '<meta name="viewport" content="width=device-width, initial-scale=1">';
  await page.goto(`data:text/html,${encodeURIComponent(html)}`);

  const runtime = await page.evaluate(() => ({
    innerWidth: window.innerWidth,
    innerHeight: window.innerHeight,
    screenWidth: window.screen.width,
    screenHeight: window.screen.height,
    devicePixelRatio: window.devicePixelRatio,
    touchEventsExposed: 'ontouchstart' in window,
    userAgent: navigator.userAgent,
  }));

  await testInfo.attach('emulation-contract.json', {
    body: Buffer.from(JSON.stringify(runtime, null, 2)),
    contentType: 'application/json',
  });

  expect(page.viewportSize()).toEqual({ width: 390, height: 720 });
  expect(runtime.innerWidth).toBe(390);
  expect(runtime.innerHeight).toBe(720);
  expect({ width: runtime.screenWidth, height: runtime.screenHeight }).toEqual({
    width: 390,
    height: 844,
  });
  expect(runtime.devicePixelRatio).toBe(3);
  expect(runtime.touchEventsExposed).toBe(true);
  expect(runtime.userAgent).toContain('Mobile');
});

Every assertion has a real failure path. Moving the viewport before the device spread changes the viewport assertions. Deleting the contextOptions line changes the screen assertion, because the descriptor's own screen never reaches the context: on Playwright 1.61.1 the reported height drops from 844 to the viewport's 720. Setting hasTouch: false on the project changes the 'ontouchstart' in window result to false. Replacing the device user agent with a desktop value changes the user-agent assertion. The attachment is diagnostic evidence rather than the oracle; the expectations still decide whether the project satisfies its declared contract.

One property is deliberately absent from that contract: navigator.maxTouchPoints. It looks like the obvious touch probe, and under emulation it is the wrong one. WebKit reports 0 for it in every configuration worth testing, with hasTouch true, with hasTouch false, and with isMobile false, while Chromium reports 1 when touch is enabled. Since devices['iPhone 13'].defaultBrowserType is webkit, an assertion that maxTouchPoints exceeds zero can never pass in the project the descriptor itself selects, so it is not a contract check but a guaranteed red. What is observable across engines is 'ontouchstart' in window, which follows the context's touch capability, together with the fact that locator.tap() completes rather than raising the error Playwright throws when touch is not enabled on the context. Assert the capability the browser actually exposes, and keep engine-specific counters out of a cross-engine contract.

Do not assert the entire user-agent string unless the exact string is part of the feature under test. Full strings are long, browser-version-sensitive, and likely to create maintenance without protecting product behavior. A more stable server-side test can assert the application's returned variant or a controlled header classification. In a browser-only contract test, a narrow check such as Mobile is enough for this specific shipped profile. If the project intentionally unsets the descriptor's user agent to use the host platform string, that check should be removed and the project renamed to make the exception obvious.

A runtime contract is necessary, but it is not the product test. The next example checks two properties that a screenshot cannot establish: the mobile control appears under the CSS breakpoint, and Playwright can activate it with a touch gesture. The page is self-contained, so a failure comes from the configured browser behavior rather than a staging dependency.

TypeScript
// tests/mobile-interaction.spec.ts
import { devices, expect, test } from '@playwright/test';

test.use({ ...devices['iPhone 13'] });

test('opens the compact navigation with touch input', async ({ page }) => {
  await page.setContent(`
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
      #menu { display: none; }
      @media (max-width: 600px) { #menu { display: inline-block; } }
    </style>
    <button id="menu" type="button">Open menu</button>
    <p id="status">closed</p>
    <script>
      document.querySelector('#menu').addEventListener('click', () => {
        document.querySelector('#status').textContent = 'open';
      });
    </script>
  `);

  const menu = page.getByRole('button', { name: 'Open menu' });
  await expect(menu).toBeVisible();
  await menu.tap();
  await expect(page.locator('#status')).toHaveText('open');
});

The control test still does not claim that a browser engine on a build agent has become an iPhone. It checks a responsive layout and a browser touch path under a documented emulation profile. Hardware behaviors such as a real virtual keyboard, operating-system permission surfaces, sensor accuracy, browser chrome, thermal pressure, and mobile-network conditions remain outside that claim.

Diagnose the active profile before changing the test

Start with the earliest observable mismatch. If page.viewportSize() is wrong, inspect project selection and spread order before opening application code. If Playwright reports the expected viewport but window.innerWidth differs, check whether the page has the viewport metadata the scenario assumes and whether the test resized the page later. If dimensions are correct but a touch action fails, compare 'ontouchstart' in window, the project's hasTouch value, and the input method the test actually used, remembering that navigator.maxTouchPoints stays at zero under WebKit emulation whatever hasTouch says. If only server-rendered markup is wrong, inspect the request user agent or the server's variant decision instead of tuning CSS waits.

The fingerprint attachment gives those branches separate evidence. A failure report with the expected viewport and no ontouchstart on window is not a responsive-layout failure. A report with touch capability and dimensions correct but the wrong navigation variant may indicate user-agent-based rendering or an application breakpoint. A report with the correct browser contract and a locator resolving to no elements points toward the product or test data. This separation prevents a broad label such as “mobile flake” from absorbing unrelated causes.

Run the small contract test by itself under the same named project as the failing suite. Force one worker while investigating so log order stays easy to read, and retain a trace for the reproduction. These commands do not fix the profile; they reduce the amount of application behavior involved.

Shell
pnpm exec playwright test tests/emulation-contract.spec.ts \
  --project=webkit-phone-short-viewport \
  --workers=1 \
  --trace=on

pnpm exec playwright show-trace test-results/**/trace.zip

In the trace, use the action timeline to verify which navigation and resize calls happened before the failure. Look at the screenshot associated with the failing assertion for layout evidence, and open the attachment named emulation-contract.json for browser-observable values. Do not infer touch capability or user agent from screenshot dimensions. Do not infer the context's initial viewport from the final screenshot if the test calls page.setViewportSize() midway through the case.

Trace evidence is especially useful when a fixture changes the page after project creation. Search fixtures and hooks for test.use, page.setViewportSize, and manual browser.newContext calls. The built-in page fixture uses the selected project's context options, but a test that creates its own context is responsible for forwarding them. A helper that calls browser.newContext() with no options does not inherit the test project's emulation merely because the test also requests the standard page fixture. Follow the page object used by the failing locator back to its owning context.

Project selection is another frequent cause. A developer runs --project=webkit-phone-short-viewport locally, while CI uses a regular expression or a different configuration file that excludes it. The most reliable evidence is the project name printed by the reporter together with the attached runtime fingerprint. A mobile-looking snapshot from a previous retry or a differently named project is not evidence for the current attempt.

When an expectation says the viewport should be 390 by 720, that message is generated from your explicit contract and therefore has a clear owner. Compare that with waiting longer for a menu locator. Extra time cannot change a context created with the wrong descriptor. Retries can accidentally select a path that renders after different data or cache state, but they cannot make an incoherent profile coherent. Keep the environment check early and cheap so a profile problem fails before a long checkout flow.

One diagnostic deserves caution: navigator.userAgent confirms the value exposed to page JavaScript, but it does not prove how every intermediary treated the request. A reverse proxy, CDN, or test server can normalize headers or cache a variant incorrectly. When server rendering is the suspected boundary, add application-owned diagnostics such as a response header in a test environment or assert a stable marker in the returned HTML. Do not claim a Playwright override is broken solely because the server returned an unexpected component.

Separate descriptor mistakes from similar failures

A CSS breakpoint defect can look exactly like a bad viewport override in the screenshot. The menu remains hidden at a phone width, so the test author assumes the preset was lost. The distinction is direct: the contract attachment shows the expected innerWidth, and window.matchMedia('(max-width: 600px)').matches returns true, yet the computed style still hides the control. That evidence moves the investigation into selector specificity, container queries, stylesheet loading, or application state. Changing the descriptor would only disguise the product bug.

Container queries create a particularly convincing near-miss. The viewport is narrow, but the relevant component sits inside a fixed-width preview pane or transformed shell. A viewport media query and a container query answer different questions. Inspect the element's bounding box and its query container rather than repeatedly shaving pixels from the global viewport. If the component should adapt to its container, a custom device descriptor is not the repair.

Server-side user-agent branching creates the opposite mismatch. Runtime dimensions and touch capability are correct, but the server sent desktop markup because someone overrode userAgent, unset it, or served a cached response produced for another request. The page can still look narrow after CSS applies, which makes a screenshot deceptively mobile. Network evidence and a server-rendered variant marker distinguish this from spread order. If the application is not supposed to branch on user agent, the test may have revealed an architecture problem rather than a profile configuration problem.

A later call to page.setViewportSize() is another near-miss. The context starts with the intended descriptor, then a shared helper resizes the page for a visual test. Playwright documents that this method also resets screen size. A test that subsequently compares screen.width with the original device can fail even though the initial descriptor was applied correctly. The trace timeline shows the resize call, and a fingerprint captured immediately before the product action shows the changed state. Move the resize into a dedicated test or create the correct context up front.

Visual regression failures often get blamed on deviceScaleFactor before font and rendering differences are eliminated. Device scale factor changes the device-pixel density, so it belongs in the baseline contract, but matching it does not make screenshots identical across browser engines, operating systems, or font installations. If the runtime fingerprint matches and only antialiasing or text metrics differ, investigate the image environment and font loading. Do not distort the descriptor until the baseline happens to pass.

Orientation is also more than swapping two viewport numbers. A product may read screen orientation, viewport dimensions, CSS orientation queries, or resize events. A one-time landscape project created with deliberate screen and viewport values is easier to reason about than rotating a page mid-test and assuming every device characteristic changed. When the feature is specifically responsive to resizing, call the page resize explicitly and assert the resize outcome. When the feature is a landscape startup path, create a landscape context and navigate once.

Touch support does not mean every event sequence matches physical hardware. Playwright can drive touch-oriented input when the context supports it, but browser emulation does not recreate the operating system's gesture arbitration, edge gestures, virtual keyboard, or accessibility services. If a failure appears only with a real stylus, multi-touch gesture, or mobile browser chrome, preserve a smaller Playwright check for the web contract and move the hardware claim to a device lab or manual exploratory session.

The final near-miss is a locator problem. Responsive layouts commonly render desktop and mobile controls in the DOM at the same time while hiding one with CSS. A broad text locator may resolve to both or interact with the wrong one. The environment can be perfect and the test still target the desktop control. Use role, accessible name, and a scoped navigation landmark, then assert visibility before action. A descriptor change is not a substitute for locator identity.

Roll the fix through an existing suite without hiding regressions

Inventory every place that composes a device descriptor before changing a shared helper. Configuration files are only the beginning. File-level test.use calls, custom fixtures, and direct browser.newContext calls can create independent profiles. A read-only search gives you the review set and also exposes property order.

Shell
rg -n "devices\[|test\.use\(|newContext\(|setViewportSize\(" \
  playwright.config.ts tests

Classify each match by intent. A layout-only test may need a named viewport and no device preset. A touch interaction needs touch capability in addition to width. A server-rendering test may care about user agent but not device scale factor. A screenshot project needs a stable browser, viewport, scale factor, fonts, and baseline environment. This classification usually reduces the number of places that require a full device descriptor.

Add the contract test before editing the helper. Run it against the current main branch or release baseline and retain the fingerprint. If it fails, that is useful information: the project name already promised more than the configuration delivered. Decide whether to change the name, the profile, or the test claim. Do not quietly adjust expectations to whatever CI currently reports.

Next, create one explicit project beside the existing one rather than mutating every mobile job at once. Put the descriptor first and overrides after it. Run a small group containing the environment contract, a responsive layout case, a touch interaction, and one server-rendered case if user-agent branching exists. This group samples different properties without running the entire suite.

Compare failures by boundary. A changed screenshot with a matching contract needs visual review. A product flow that now fails because touch input reaches a different handler may expose real missing coverage. A server response that changes after restoring the device user agent may reveal a cache key or rendering dependency. These are not migration noise to be waived automatically.

Once the new project is trusted, move tests by scenario rather than directory. Tests that rely on the full phone contract should import no local resize helper. Tests that only need a breakpoint should use a simpler viewport project. Remove ambiguous names such as mobile-small if two projects with different touch or user-agent behavior share them. A project name is part of the diagnostic output, so precision there shortens every future investigation.

Keep the old project for a short comparison window if it protects a release path, but do not run two nearly identical matrices forever. Parallel profiles cost browser startup time, worker capacity, trace storage, screenshot baselines, and review attention. Define the exit condition in the change: for example, all tagged touch tests have moved and the old project's unique failure count is zero across the agreed observation period. The period is a team decision, not a fabricated quality threshold.

Review descriptor changes during Playwright upgrades. The registry is maintained by Playwright, which is a benefit, but a preset is still test input. The contract attachment makes differences visible, and a focused pull-request job can run just the environment tests for each named device project. When an upstream descriptor changes, decide whether following it improves realism or whether the suite requires a pinned custom contract. Copying the entire descriptor into local code freezes behavior but transfers maintenance to your team.

Avoid mass replacement with page.setViewportSize() as a migration shortcut. It may make width assertions pass while removing the evidence that touch, mobile mode, user agent, and scale factor are intentional. It also changes screen size according to the documented API behavior. Use it only in tests whose subject is resizing or a viewport-only layout transition.

Accept the cost, and know when not to emulate a device

An explicit profile costs maintenance. The more properties you assert, the more often browser or Playwright updates require review. The answer is not to assert nothing. Protect only properties that can change the product path. A checkout test that depends on a mobile navigation and touch activation needs viewport and touch evidence. It probably does not need an exact screen height or full user-agent string. A visual test may need device scale factor but no assertion about server classification.

Keeping Playwright's descriptor and adding a small override gives you upstream maintenance plus local intent. The trade-off is that registry updates can move the baseline. Copying every field into a local constant gives a stable contract, but it ages. New browser behavior, user-agent changes, and updated device definitions will not arrive automatically. Whichever model you choose, make it visible in code review and test the properties that matter.

Broader device matrices increase coverage and latency together. Three viewport variants across three browser engines create nine project combinations before locale, permissions, color scheme, or authenticated state enter the picture. More combinations also multiply screenshots and traces. Build the matrix from distinct risk, not from a desire to list popular phones. Two descriptors that take the same application path add less value than one descriptor plus a real-device check for an unsupported hardware behavior.

Do not use a phone descriptor when the requirement is simply “the component collapses below 600 pixels.” A viewport-specific desktop context is clearer and avoids accidental assertions about touch, user agent, or pixel density. Test the breakpoint with boundary values that matter to the CSS, then keep one separate mobile-profile test for the end-to-end interaction.

Do not override a descriptor to force a failing locator into view. If a sticky banner covers a button or two responsive controls share a name, fix the product layout or locator. Changing viewport height until the click succeeds creates a test-only device and removes coverage from the troublesome dimensions. The runtime contract will be consistent, but the scenario will be dishonest.

Do not claim physical-device coverage from browser emulation. Camera capture, Bluetooth, biometric prompts, virtual keyboards, safe areas controlled by browser chrome, operating-system accessibility, and multi-touch gestures require other evidence. Playwright can still validate the surrounding web flow, but the report should state the boundary. A concise limitation is more credible than a green test carrying an inflated device label.

Avoid user-agent overrides when the feature is not supposed to depend on user agent. They can steer server rendering and caching in ways that a viewport-only requirement never requested. If a user-agent branch is part of the product, test that decision explicitly and include a desktop control. If it is accidental, preserve the failure and fix the application rather than teaching the suite to send the string that makes it pass.

Finally, do not combine unrelated emulation concerns in one overloaded project. A phone profile with a custom locale, timezone, geolocation, dark color scheme, offline mode, and granted permissions may be valid for one customer journey, but it is a poor general mobile baseline. When it fails, too many environment changes compete as causes. Keep a stable core profile, then add focused projects or file-level options for risks that genuinely interact.

The useful outcome is not a perfectly realistic fictional phone. It is a browser contract that another engineer can read, reproduce, and challenge. When the contract matches and the application still fails, the failure belongs to the feature boundary rather than to a screenshot-sized guess about the environment.

// FIELD DISPATCH

Get the QA Field Notes

Weekly QA battles, AI testing guides, and interview drills. Free on Substack.

// LIVE COURSE / THE TESTING ACADEMY

Playwright Automation Mastery

Go beyond Selenium. Master Playwright with JS/TS in 90 days.

From the instructor behind this guide.

Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.

Code PROMODE / 10% offJoin the batch

The Testing Academy editorial desk

Practical QA guidance built around test evidence, production tradeoffs, and interview-ready explanations.

Published July 25, 2026 / Reviewed August 4, 2026

PRIMARY REFERENCES

Verify the details at the source

QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.

  1. 01
    Official playwright.dev reference

    playwright.dev

    Primary documentation selected and verified for the claims in this guide.

  2. 02
    Official playwright.dev reference

    playwright.dev

    Primary documentation selected and verified for the claims in this guide.

  3. 03
    Official playwright.dev reference

    playwright.dev

    Primary documentation selected and verified for the claims in this guide.

  4. 04
    Official playwright.dev reference

    playwright.dev

    Primary documentation selected and verified for the claims in this guide.

FAQ / QUICK ANSWERS

Questions testers ask

Why is my Playwright mobile viewport override ignored?

Object spread order is the first thing to inspect. A device descriptor contains its own viewport, so a later `...devices[name]` replaces a viewport written earlier in the same object; put deliberate overrides after the spread and assert the runtime dimensions.

Should I change both screen and viewport in a device profile?

That depends on the scenario your product reads, and keeping the screen takes deliberate work. `screen` is not a test-runner `use` option, so spreading a descriptor drops it and `window.screen` falls back to the viewport. Pass it as `contextOptions: { screen: { width, height } }` to keep the device screen behind a shorter viewport, or set both when the contract requires a different screen as well.

What is the difference between isMobile and hasTouch in Playwright?

According to Playwright's emulation documentation, `isMobile` controls mobile viewport behavior, including how the meta viewport is handled, while `hasTouch` declares touch support. Neither setting is proven merely by making the page narrow.

Can page.setViewportSize turn a desktop context into a mobile device?

No. It resizes the page and resets the reported screen size, but it does not replace the context's user agent, touch capability, device scale factor, or mobile setting. Use a context-level profile when those properties matter.

How can I prove which Playwright device settings CI used?

Attach a small runtime fingerprint that records `innerWidth`, `window.screen`, `devicePixelRatio`, whether `'ontouchstart' in window` is true, and the user agent. Compare that evidence with the named project's contract before investigating the application assertion.