skip to content

In Playwright, how do you reset one page.emulateMedia override without clearing the others?

level: middleimportance: must knowfreq 48%

answer

  1. Each key is independent
  2. Three states, not two
  3. One special value clears an override
  4. Omitted keys keep their previous value
  5. Clears to browser default, not context option

basics

~20 s

Pass null for that one key. In Playwright, page.emulateMedia treats null as reset to default and leaves every key you omit at its current value, so emulateMedia with colorScheme null clears only the colour scheme.

solid answer

~40 s

`page.emulateMedia()` takes a bag of independent keys — `media`, `colorScheme`, `reducedMotion`, `forcedColors` and `contrast` in Playwright 1.63 — and each behaves the same way. A concrete value sets the override, **`null` clears that one key back to the browser default**, and a key you simply leave out is untouched. So after `page.emulateMedia({ colorScheme: 'dark', reducedMotion: 'reduce' })`, a later `page.emulateMedia({ colorScheme: null })` returns `prefers-color-scheme` to `light` while `prefers-reduced-motion: reduce` stays on. Overrides live on the page, beat the context-level `colorScheme` option, and survive navigation and reload. An invalid value is rejected rather than ignored: `colorScheme: 'bad'` throws `expected one of (dark|light|no-preference|no-override)`.

code

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

test('booking header follows the emulated media state', async ({ page }) => {
  await page.goto('/search');

  await page.emulateMedia({ colorScheme: 'dark', reducedMotion: 'reduce' });
  await expect(page.getByTestId('brand-header')).toHaveCSS('background-color', 'rgb(0, 0, 0)');

  // Clears the colour scheme only; reduced motion is still emulated.
  await page.emulateMedia({ colorScheme: null });
  await expect(page.getByTestId('brand-header')).toHaveCSS('background-color', 'rgb(255, 255, 255)');
  expect(await page.evaluate(() => matchMedia('(prefers-reduced-motion: reduce)').matches)).toBe(true);
});

go deeper

for a junior

Learn the key names and their values first: media, colorScheme, reducedMotion, forcedColors and contrast, each set on a page with emulateMedia. Remember that a null value is how you switch one of them off again.

for a middle

Explain the three states each key can be in — set, cleared with null, or untouched because you omitted it — and why omitting a key never resets it. That distinction is the whole mechanic being probed.

for a senior

Show the interaction with context-level options: emulateMedia overrides the context for one page, clearing returns to the browser default rather than the context value, and the override outlives navigation.

for a principal

Decide where media state belongs in a large suite — a project-wide context option versus a per-test call — and set a convention so no test silently inherits an override a previous line left behind.

## The shape of the call `page.emulateMedia()` takes a single options object whose keys are independent switches, each mapping to one CSS media feature the page can query: | Key | Values | Default with no emulation | |---|---|---| | `media` | `'screen'`, `'print'` | `screen` | | `colorScheme` | `'light'`, `'dark'`, `'no-preference'` | `light` | | `reducedMotion` | `'reduce'`, `'no-preference'` | `no-preference` | | `forcedColors` | `'active'`, `'none'` | `none` | | `contrast` | `'more'`, `'no-preference'` | `no-preference` | Every key also accepts `null`, and that is the whole answer to the question: **`null` means "stop overriding this one"**, not "set it to something falsy". ## Three states per key, not two Each key is in one of three states after a call, and keeping them apart is what makes the API predictable: 1. **Set** — you passed a value, so the page reports that value to `matchMedia()` from now on. 2. **Cleared** — you passed `null`, so the override is dropped and the browser default applies again. 3. **Untouched** — you did not mention the key, so whatever it was before the call it still is. That third state is the one people trip over. `await page.emulateMedia({ media: 'print' })` after a dark-mode override does not reset the colour scheme; the page is now both `print` and `dark`. If you want a clean slate you have to say so key by key: `page.emulateMedia({ media: null, colorScheme: null, reducedMotion: null, forcedColors: null, contrast: null })`. ## How it relates to the context options The same features exist as **browser-context options** (`colorScheme`, `reducedMotion`, `forcedColors`, `contrast`), which you set once for a whole context or via `test.use()`. `page.emulateMedia()` is the per-page, mid-test counterpart: - The context option decides what the page starts with when it is created. - `page.emulateMedia()` overrides it afterwards, for that page only, so two pages in one context can sit on different colour schemes. - Clearing with `null` returns the feature to the **browser default**, not to the context option you originally passed. On a hotel-booking site whose project runs with `colorScheme: 'dark'`, a `page.emulateMedia({ colorScheme: null })` inside one test leaves that page on `light`. - The override is page state, not navigation state: it survives `page.goto()`, a reload, and even a cross-document navigation, so you can set it once and then walk the search → room detail → checkout flow. ## `media` is a different kind of key Four of the five keys emulate a **user preference** the browser normally reads from the operating system. `media` is not one of those: it swaps the CSS **media type** the document is evaluated against, so `matchMedia('print').matches` becomes `true`, `matchMedia('screen').matches` becomes `false`, and `@media print` rules take effect while screen-only rules stop applying. - That makes it the way to test a print stylesheet — a booking confirmation's printable summary — without producing a PDF or opening a print dialog. - It is still just an override on the same object, so it obeys the same three-state rule: `media: null` restores `screen`. - Because it swaps rather than adds, a layout that only exists under `screen` genuinely disappears; assertions written against the screen layout will fail while print emulation is on, which is usually the bug the test is looking for. ## Validation and failure modes Playwright validates the strings rather than passing them through, which turns typos into immediate errors: - `page.emulateMedia({ media: 'bad' })` rejects with `media: expected one of (screen|print|no-override)`. - `page.emulateMedia({ colorScheme: 'bad' })` rejects with `colorScheme: expected one of (dark|light|no-preference|no-override)`. `no-override` in those messages is the wire-level name for what `null` compiles to — useful to recognise in a stack trace, but `null` is what you write. Two more practical traps: - **`no-preference` is not the same as `null` for colour scheme.** `'no-preference'` is still an override; `null` removes emulation entirely. - **Emulating the feature does not make the app respond.** `forcedColors: 'active'` flips `matchMedia('(forced-colors: active)')`, so the booking site's high-contrast CSS branch is reachable; whether that branch is correct is a separate assertion. ## Using it in a test The natural shape is: navigate, emulate, assert the branch, then clear the one key you changed if the rest of the test needs the default back. - Assert the feature reached the page with `matchMedia(...)` inside `page.evaluate()` when you are debugging the emulation itself. - Assert the rendered consequence — a computed colour, a hidden animation, a `print`-only summary block — when you are testing the application, because that is the part that can regress. Because each key is independent and sticky, a helper that always passes the full set explicitly is a reasonable convention in a large suite: it removes any doubt about which overrides a given test inherited from an earlier line.

  • After clearing colorScheme with null, does the page fall back to the context's colorScheme option?
    No — it falls back to the browser default, which is `light`. The context option only decides the page's starting state; once `page.emulateMedia()` has taken over that feature, `null` removes emulation altogether rather than restoring the value the context was created with.
  • Does a page.emulateMedia override survive a navigation?
    Yes. The override is page-level state, not per-document, so it persists across `page.goto()`, a reload and cross-document navigations. That is what lets you emulate once and then walk a whole booking flow without re-applying it on every step.
  • How would you assert that forcedColors emulation actually reached the page?
    Read the media query back: `await page.evaluate(() => matchMedia('(forced-colors: active)').matches)` should be true. For a product assertion, prefer checking the rendered consequence — a computed colour or a visible high-contrast element — since that proves the CSS branch works, not just that the flag flipped.

saying these in an interview costs you the question

  • Thinks an empty options object resets every media override
  • Believes null throws because it is not a listed value
  • Treats no-preference and null as the same thing
  • Assumes the override is lost on the next navigation
  • Expects clearing to restore the context's original option value
  • Thinks emulating a feature also proves the CSS branch is correct