Skip to content

Browser instance headless setting is ignored in favour of root setting #7661

Open
@delilahw

Description

@delilahw

Describe the bug

TL;DR

Vitest Browser does not seem to resolve configuration values for headless mode correctly. The root value is favoured over the instance value.

BrowserConfigOptions.headless Root Value BrowserConfigOptions.instances[].headless Instance Value Actual Behaviour Expected Behaviour
true undefined Headless Headless
undefined true Headed Headless
true false Headless Headed

Detailed Issue

Here is a more detailed description of the issue.

Headless Configuration Settings

In vitest.config.ts, the headless option can be set in two different places.

  1. In BrowserConfigOptions.headless, the root value.
export default defineConfig({
  test: {
    browser: {
      enabled: true,
      provider: "playwright", // or webdriver, etc

      headless: true,
    },
  },
});
  1. In BrowserConfigOptions.instances[], which is an array of BrowserInstanceOption.
export default defineConfig({
  test: {
    browser: {
      enabled: true,
      provider: "playwright",

      instances: [
        {
          browser: "chromium",
          headless: true,
        },
      ],
    },
  },
});

Both of the approaches above will pass type-checking, but only Scenario 1 will launch the browser with headless mode enabled. Scenario 2 will not work as expected, and the tests will run in headed mode.

Reproduction Steps

Please see https://github.com/delilahw/vitest-playwright-headless-demo for a minimal example. Clone the repository and use pnpm i to install dependencies.

Then, you can verify the behaviour by enabling debug logs in Playwright and inspecting the output for the headless option. It will be printed after the line containing "browserType.launch started".

# Launch Scenario 2
$ DEBUG='pw:*' pnpm test:browser -c vitest.config.headless-instance.ts &| tee test-instance.log

$ cat test-instance.log | grep -A3 'browserType.launch started'
# Notice the `headless` option being set to false in the below JSON.
2025-03-12T04:04:06.823Z pw:api => browserType.launch started
2025-03-12T04:04:06.823Z pw:channel SEND> {"id":1,"guid":"browser-type@6b4a60839944b8891fb59ce7131b8330","method":"launch","params":{"args":["--start-maximized"],"ignoreAllDefaultArgs":false,"headless":false}}

# Browser command/args and its PID will also be printed here.
# Notice that `--headless` is not present in the below args.
2025-03-12T04:04:06.826Z pw:browser <launching> ms-playwright/chromium-1161/chrome-mac/Chromium.app/Contents/MacOS/Chromium --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-extensions --disable-features=AcceptCHFrame,AutoExpandDetailsElement,AvoidUnnecessaryBeforeUnloadCheckSync,CertificateTransparencyComponentUpdater,DeferRendererTasksAfterInput,DestroyProfileOnBrowserClose,DialMediaRouteProvider,ExtensionManifestV2Disabled,GlobalMediaControls,HttpsUpgrades,ImprovedCookieControls,LazyFrameLoading,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --enable-automation --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --enable-use-zoom-for-dsf=false --no-sandbox --start-maximized --user-data-dir=/tmp/1/playwright_chromiumdev_profile --remote-debugging-pipe --no-startup-window
2025-03-12T04:04:06.827Z pw:browser <launched> pid=88938

You can check the headless: true | false output for various scenarios by swapping out the -c <config file> argument with the different config files below.

Config File BrowserConfigOptions.headless Root Value BrowserConfigOptions.instances[].headless Value Actual Behaviour Expected Behaviour Notes
vitest.config.headless-browser.ts true undefined Headless Headless Scenario 1
vitest.config.headless-instance.ts undefined true Headed Headless Scenario 2
vitest.config.headless-conflicting-values.ts true false Headless Headed -

Cause

Edit: See #7710 description for the cause. The below information is outdated because it outlines a potential fix that is at a lower level compared to the root cause.

What's causing this discrepancy? Simply put, the headless option in BrowserConfigOptions.instances[] is not being used in the Playwright Provider.

Let's take a look at vitest/packages/browser/src/node/providers/playwright.ts:47-54, containing the following constructor.

export class PlaywrightBrowserProvider implements BrowserProvider {
  initialize(
    project: TestProject,
    { browser, options }: PlaywrightProviderOptions,
  ): void {
    this.project = project
    this.browserName = browser
    this.options = options as any
  }
}

The this.options.headless property will be populated with BrowserConfigOptions.instances[].headless. However, it isn't consumed.

Now, let's go to Lines 66-73 (within the openBrowser() method). This snippet determines the final headless setting.

private async openBrowser() {
  ...

  const options = this.project.config.browser

  const playwright = await import('playwright')

  const launchOptions = {
    ...this.options?.launch,
    headless: options.headless,
  } satisfies LaunchOptions

  ...
}

We can see that the headless option is set to options.headless, which is populated from this.project.config.browser.headless, which itself is populated from BrowserConfigOptions.headless.

There is no usage of this.options.headless, so the value from BrowserConfigOptions.instances[].headless is ignored.

Possible Solutions

A. Use BrowserConfigOptions.headless only

If the preferred behaviour is to always use the value from BrowserConfigOptions.headless, then the current implementation is correct.

In this case, we should remove the headless option from the BrowserInstanceOption type. This would make the following configuration invalid:

export default defineConfig({
  test: {
    browser: {
      enabled: true,
      provider: "playwright",

      instances: [
        {
          browser: "chromium",
          headless: true, // Invalid
        },
      ],
    },
  },
});

B. BrowserInstanceOption.headless takes precedence over BrowserConfigOptions.headless

According to the Browser Config Reference Docs, “every browser config inherits from the root config”.

We should replace the resolution logic with

const options = this.project.config.browser

// BrowserConfigOptions.headless
const headlessFromRoot = options?.headless

// BrowserConfigOptions.instances[].headless
const headlessFromInstance = this.options?.headless

const launchOptions = {
  ...this.options?.launch,
  // Prefer headlessFromInstance if it's set
  headless: headlessFromInstance ?? headlessFromRoot,
} satisfies LaunchOptions

Other Solutions

If you have any other ideas, please feel free to discuss!

I'm happy to implement a solution and make a PR. Let me know what we wanna do. 😇

Reproduction

https://github.com/delilahw/vitest-playwright-headless-demo

System Info

System:
    OS: macOS 15.3.1
    CPU: (10) arm64 Apple M1 Max
    Memory: 105.13 MB / 64.00 GB
    Shell: 3.7.1 - /Users/delilahmsft/.nix-profile/bin/fish
  Binaries:
    Node: 22.14.0 - ~/.n/bin/node
    Yarn: 4.6.0 - ~/.npm/bin/yarn
    npm: 10.9.2 - ~/.n/bin/npm
    pnpm: 9.12.3 - ~/.npm/bin/pnpm
  Browsers:
    Chrome: 134.0.6998.45
    Edge: 134.0.3124.51
    Safari: 18.3
  npmPackages:
    @vitest/browser: ^3.0.8 => 3.0.8 
    playwright: ^1.51.0 => 1.51.0 
    vitest: ^3.0.8 => 3.0.8

Used Package Manager

pnpm

Validations

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions