playwright icon indicating copy to clipboard operation
playwright copied to clipboard

[Bug]: Tests fail for code using media query `display-mode: fullscreen` under new Chromium headless

Open unikitty37 opened this issue 11 months ago • 6 comments

Version

1.50.1

Steps to reproduce

Reproduction at https://github.com/unikitty37/chromium-headless-issue

  1. Opt in to new Chromium headless, as detailed in #33566
  2. Create a Svelte5 + SvelteKit site that uses a reactive media query to detect if the browser is fullscreen (const fullscreen = new MediaQuery('display-mode: fullscreen'))
  3. Add an element to a page that only displays if the browser is not fullscreen ({#if !fullscreen.current}<element />{/if})
  4. Create a test that goes to the page, calls page.evaluate(() => document.documentElement.requestFullscreen()) and then checks to see if the element from step 3 is visible

Expected behavior

Both tests should pass whether running under old Chromium headless or new Chromium headless.

Actual behavior

Both tests pass using the old Chromium headless, but the second test fails when running the new Chromium headless.

Running 4 tests using 2 workers

  ✓  1 …5:5 › Full screen control › when the browser is not full screen › is displayed (322ms)
  ✓  2 …5:5 › Full screen control › when the browser is not full screen › is displayed (356ms)
  ✓  3 …8:5 › Full screen control › when the browser is full screen › is not displayed (299ms)
  ✘  4 …18:5 › Full screen control › when the browser is full screen › is not displayed (5.3s)


  1) [new-chromium] › e2e/demo.test.js:18:5 › Full screen control › when the browser is full screen › is not displayed

    Error: Timed out 5000ms waiting for expect(locator).not.toBeVisible()

    Locator: getByRole('button', { name: 'Full Screen' })
    Expected: not visible
    Received: visible
    Call log:
      - expect.not.toBeVisible with timeout 5000ms
      - waiting for getByRole('button', { name: 'Full Screen' })
        9 × locator resolved to <button>Full Screen</button>
          - unexpected value "visible"


      17 |
      18 |     test('is not displayed', async ({ page }) => {
    > 19 |       await expect(page.getByRole('button', { name: 'Full Screen' })).not.toBeVisible()
         |                                                                           ^
      20 |     })
      21 |   })
      22 | })
        at /Users/user/Development/chromium-headless-issue/e2e/demo.test.js:19:75

  1 failed
    [new-chromium] › e2e/demo.test.js:18:5 › Full screen control › when the browser is full screen › is not displayed
  3 passed (6.5s)

Additional context

No response

Environment

System:
    OS: macOS 14.7.1
    CPU: (10) arm64 Apple M1 Max
    Memory: 241.28 MB / 64.00 GB
  Binaries:
    Node: 23.6.0 - /opt/homebrew/bin/node
    Yarn: 1.22.22 - /opt/homebrew/bin/yarn
    npm: 10.9.2 - /opt/homebrew/bin/npm
    pnpm: 9.12.1 - ~/bin/pnpm
  IDEs:
    VSCode: 1.76.1 - /Users/user/bin/code
  Languages:
    Bash: 3.2.57 - /bin/bash
  npmPackages:
    @playwright/test: ^1.49.1 => 1.50.1

The behaviour also occurs when running in a devcontainer:

  System:
    OS: Linux 6.12 Debian GNU/Linux 12 (bookworm) 12 (bookworm)
    CPU: (10) arm64 unknown
    Memory: 8.84 GB / 11.73 GB
    Container: Yes
  Binaries:
    Node: 22.13.0 - /usr/local/share/nvm/versions/node/v22.13.0/bin/node
    Yarn: 1.22.22 - /usr/bin/yarn
    npm: 10.9.2 - /usr/local/share/nvm/versions/node/v22.13.0/bin/npm
    pnpm: 9.15.4 - /usr/local/share/nvm/versions/node/v22.13.0/bin/pnpm
  IDEs:
    VSCode: 1.97.0 - /vscode/vscode-server/bin/linux-arm64/33fc5a94a3f99ebe7087e8fe79fbe1d37a251016/bin/remote-cli/code
  Languages:
    Bash: 5.2.15 - /usr/bin/bash
  npmPackages:
    @playwright/test: 1.50.1 => 1.50.1

unikitty37 avatar Feb 09 '25 13:02 unikitty37

I tried to run your repro and get:

WebServer] src/routes/+page.svelte (3:11): "MediaQuery" is not exported by "node_modules/.pnpm/[email protected]/node_modules/svelte/src/reactivity/index-server.js", imported by "src/routes/+page.svelte".

do you mind looking into it?

mxschmitt avatar Feb 10 '25 13:02 mxschmitt

@mxschmitt Thanks for taking a look — it seems to have picked a more recent version of Svelte when I built it. I've changed the package.json to use explicit version numbers, so hopefully we'll both be running the same versions of everything now…

unikitty37 avatar Feb 10 '25 15:02 unikitty37

I can reproduce. Test:

// tests/page/page-emulate-media.spec.ts
import { expect as baseExpect, test, Page } from '@playwright/test'

const expect = baseExpect.extend({
  async toMatchMedia(page: Page, mediaQuery: string) {
    const pass = await page.evaluate(mediaQuery => matchMedia(mediaQuery).matches, mediaQuery).catch(() => false);
    return {
      message() {
        if (pass)
          return `Expected "${mediaQuery}" not to match, but it did`;
        else
          return `Expected "${mediaQuery}" to match, but it did not`;
      },
      pass,
      name: 'toMatchMedia',
    };
  },
});

test('Full screen control is displayed', async ({ page }) => {
  await page.goto('https://example.com')
  await expect(page).not.toMatchMedia('(display-mode: fullscreen)')
})

test('Full screen control is not displayed', async ({ page }) => {
  await page.goto('https://example.com')
  await page.evaluate(() => document.documentElement.requestFullscreen())
  await expect(page).toMatchMedia('(display-mode: fullscreen)')
})

Passes in Chromium Headless Shell but not with 'channel': 'chromium'.

mxschmitt avatar Feb 11 '25 12:02 mxschmitt

Minimal Puppeteer reproduction:

import puppeteer from 'puppeteer';

(async () => {
  for (const headless of [true, 'shell']) {
    for (const callSetFocusEmulationEnabled of [true, false]) {
      const browser = await puppeteer.launch({ headless });
      const page = await browser.newPage();

      const props = [
        headless === true ? 'Headless New' : 'Headless Shell',
        callSetFocusEmulationEnabled ? 'Emulation.setFocusEmulationEnabled' : 'no Emulation.setFocusEmulationEnabled',
        `Browser: ${await browser.version()}`,
        `Platform: ${process.platform}`,
      ]
      console.log()
      console.log(props.join(' | '));

      // Problematic code: Emulation.setFocusEmulationEnabled is not getting used in Puppeteer
      // by default hence the issue does not surface there. Playwright uses this always.
      const session = await page.createCDPSession();
      if (callSetFocusEmulationEnabled)
        await session.send(`Emulation.setFocusEmulationEnabled`, { enabled: true });

      const verifyFullscreenMode = async (mode, expected) => {
        const pass = await page.evaluate(
          (mediaQuery) => matchMedia(mediaQuery).matches,
          '(display-mode: fullscreen)'
        );
        const passString = pass === expected ? 'passed' : 'failed';
        console.log(`- ${mode}: (${passString}) - Expected: ${expected}, Actual: ${pass}`);
      };

      await page.goto('https://example.com');
      await verifyFullscreenMode('Without fullscreen mode', false);

      await page.evaluate(() => document.documentElement.requestFullscreen());
      await verifyFullscreenMode('With fullscreen mode', true);

      await browser.close();
    }
  }
})();

Expected: Pass on all 4 variations.

Actual: Headless New with Emulation.setFocusEmulationEnabled behaves differently and '(display-mode: fullscreen)' does not return true after calling document.documentElement.requestFullscreen().

Headless New | Emulation.setFocusEmulationEnabled | Browser: Chrome/135.0.7049.42 | Platform: darwin
- Without fullscreen mode: (passed) - Expected: false, Actual: false
- With fullscreen mode: (failed) - Expected: true, Actual: false

Headless New | no Emulation.setFocusEmulationEnabled | Browser: Chrome/135.0.7049.42 | Platform: darwin
- Without fullscreen mode: (passed) - Expected: false, Actual: false
- With fullscreen mode: (passed) - Expected: true, Actual: true

Headless Shell | Emulation.setFocusEmulationEnabled | Browser: HeadlessChrome/135.0.7049.42 | Platform: darwin
- Without fullscreen mode: (passed) - Expected: false, Actual: false
- With fullscreen mode: (passed) - Expected: true, Actual: true

Headless Shell | no Emulation.setFocusEmulationEnabled | Browser: HeadlessChrome/135.0.7049.42 | Platform: darwin
- Without fullscreen mode: (passed) - Expected: false, Actual: false
- With fullscreen mode: (passed) - Expected: true, Actual: true

mxschmitt avatar Mar 04 '25 13:03 mxschmitt

Bad headless shell version:

  • "version": "133.0.6862.0"
  • "revision": "1388591"
  • https://crrev.com/1388591

Good headless shell version:

  • "version": "133.0.6863.0"
  • "revision": "1388808"
  • https://crrev.com/1388808

RANGE: https://chromium.googlesource.com/chromium/src/+log/30886f7231b93c289fffcfc85dc2f2dca59f4430..bfbe6e71d59705ae9366e2efbc9f44b8cad8d2e8

Suspect which makes the test pass: https://chromium.googlesource.com/chromium/src/+/e616fc75b5ce7aa8e9d4db3bf19d4ca3307146d1

Goal: We'd like to have something similar in Headless New.

mxschmitt avatar Apr 09 '25 11:04 mxschmitt

Filed as https://issues.chromium.org/u/1/issues/409579376

mxschmitt avatar Apr 10 '25 12:04 mxschmitt

Investigation notes: as per upstream Chromium issue, this is a side-effect of not actually making page fullscreen. This is intended behavior in most cases, because making headed (or new headless) browser fullscreen during testing prevents user for doing anything. It is unclear whether there is a proper solution. I'll merge this into the larger fullscreen issue.

dgozman avatar Jul 31 '25 09:07 dgozman