skip to content

In a Playwright `use` block, why is a `viewport` override ignored when written before the `...devices` spread?

level: middleimportance: should knowfreq 46%

answer

  1. It is an object literal, not magic
  2. Source order decides the winner
  3. Put your override below the spread
  4. Untouched keys keep descriptor values
  5. Half-emulated devices are the real trap

basics

~10 s

Because the use block is one object literal and JavaScript applies its keys in source order. The spread comes later, so the descriptor's own viewport overwrites the earlier key and only one value survives.

solid answer

~40 s

This is plain object-literal semantics, not a Playwright rule: later keys win, and a spread expands at the position you write it. Since `devices['Pixel 7']` defines `viewport`, placing your override above the spread means the descriptor silently overwrites it. The docs call this out — define `viewport` after destructuring `devices`. The same applies to `userAgent`, `deviceScaleFactor`, `isMobile`, `hasTouch` and `browserName`; keys the descriptor does not define, like `locale` or `baseURL`, are order-independent, which is why the bug feels arbitrary. Note also that overriding one key leaves the rest of the descriptor intact: widen a `Pixel 7` viewport to 1280 and you still have a phone user agent, `isMobile: true` and touch enabled, which is a device that does not exist unless you meant it.

code

typescript · 13 lines
typescript
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  projects: [
    {
      name: 'pixel7-wide',
      use: {
        ...devices['Pixel 7'],
        viewport: { width: 480, height: 900 },
      },
    },
  ],
});

go deeper

for a junior

Learn the ordering rule as a habit: spread the descriptor first, write overrides underneath. If an override seems ignored, check where you put it before suspecting Playwright.

for a middle

Explain why, in JavaScript terms: a use block is one object literal, a spread expands at its position, and later keys overwrite earlier ones, so Playwright only ever sees one value.

for a senior

Watch for the half-emulated context. Overriding a viewport leaves the user agent, touch and mobile flags in place, and that mismatch can serve the wrong bundle or hide hover-only affordances in a way that costs real triage time.

for a principal

Set a convention for the config. Decide when overriding a registry entry is acceptable versus when a project should hand-write its options outright, and require project names that describe the resulting combination rather than the descriptor it started from.

## It is plain JavaScript, not a Playwright rule A `use` block is one object literal. When you write `{ ...devices['Pixel 7'], viewport: {...} }`, the spread expands into keys at that position and later keys overwrite earlier ones — the ordinary last-write-wins semantics of object literals. Playwright never sees the two values fight; by the time the runner reads the object, only one `viewport` exists. The Playwright docs make the point explicitly for exactly this case: define `viewport` **after** destructuring `devices`, since the descriptor also defines a viewport. Put it first and the descriptor silently wins, your override does nothing, and the test runs at the phone width you thought you had replaced. ## Which keys are at risk Any key the descriptor also defines can be clobbered this way: - `viewport` — the one people hit first, because overriding it is the common reason to reach for an override at all. - `userAgent` — a suite that wants its own UA string (a bot marker for a hotel-booking staging environment, say) must place it below the spread. - `deviceScaleFactor`, `isMobile`, `hasTouch`, `screen` — less often overridden, same rule. - `browserName` — not a context option, but subject to the same ordering against the descriptor's `defaultBrowserType`. Keys the descriptor does **not** define — `locale`, `colorScheme`, `permissions`, `baseURL` — are unaffected by order, which is why the bug feels arbitrary until you know the rule. ## Unsetting rather than replacing Sometimes you do not want a different value, you want none. The documented idiom is to assign `undefined` after the spread: ```ts const context = await browser.newContext({ ...devices['Desktop Chrome'], userAgent: undefined, }); ``` That drops the descriptor's Windows-flavoured user agent and lets the browser send its own — useful when you want the platform actually running the tests to be visible to your server-side analytics rather than a canned string. The important part is still the position: `undefined` written above the spread is overwritten by the descriptor exactly like any other value would be, so unsetting a key and replacing a key follow the same rule. ## The half-emulated device trap Overriding one key does **not** neutralise the rest of the descriptor, and that is the subtler failure. Take `Pixel 7` and override the viewport to 1280 wide, and you now have a context that is desktop-sized but still reports a Pixel user agent, still has `isMobile: true`, still has `hasTouch: true` and still reports a `deviceScaleFactor` of 2.625. No such device exists. | what you changed | what silently stayed | likely symptom | |---|---|---| | `viewport` widened | `userAgent` still a phone | server returns the mobile bundle at desktop width | | `viewport` widened | `hasTouch` still true | hover-only affordances never appear in the room gallery | | `viewport` widened | `isMobile` still true | the `meta viewport` tag still drives layout | | `deviceScaleFactor` lowered | viewport unchanged | image `srcset` picks a different asset than production | Sometimes this is exactly what you want — a deliberate "tablet-ish touch device" is a real thing to test, and a hotel-booking suite might legitimately want one project covering a wide touchscreen. The rule is that it should be a decision you wrote down in the project name, not a leftover from an override someone added to fix one flaky spec. The failure mode is quiet, too. Nothing warns you that the combination is impossible, every option is individually valid, and the suite stays green until a test depends on the interaction between two keys you never meant to pair — at which point you are debugging the config while looking at the page. ## A checklist for overriding a descriptor 1. Spread the descriptor **first**, always. 2. Write every override **below** it, and only the keys you actually mean to change. 3. Ask what the untouched keys now imply, and override those too if the combination is incoherent. 4. Name the project after the combination, not after the descriptor — `pixel7-wide` beats `pixel7`. 5. If the override list grows past two or three keys, you probably want a different descriptor, or a plain hand-written set of options with no descriptor at all. ## The one-sentence answer Because the `use` block is a single object literal and the spread comes later in source order, so the descriptor's `viewport` overwrites yours — move the override below the spread and it wins.

  • How do you remove a descriptor's user agent rather than replace it with another string?
    Assign `undefined` to `userAgent` after the spread. The documented idiom drops the canned string so the browser sends its own, which is what you want when the platform running the tests should be visible to the server rather than a hard-coded Windows or iPhone identity.
  • What is the risk of overriding only the viewport on a phone descriptor?
    Every other key survives, so you get a desktop-width context that still reports a phone user agent, still honours the meta viewport tag and still has touch enabled. That combination matches no real device, and it can serve the mobile bundle at desktop width. Make it a deliberate, named configuration or use a different descriptor.

saying these in an interview costs you the question

  • Thinks Playwright merges the two viewport values
  • Expects an explicit key to beat a later spread
  • Believes an override resets the whole descriptor
  • Assumes ordering matters for every option equally
  • Sets a desktop viewport and calls it a desktop context