Description
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.
- In
BrowserConfigOptions.headless
, the root value.
export default defineConfig({
test: {
browser: {
enabled: true,
provider: "playwright", // or webdriver, etc
headless: true,
},
},
});
- In
BrowserConfigOptions.instances[]
, which is an array ofBrowserInstanceOption
.
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
- Follow our Code of Conduct
- Read the Contributing Guidelines.
- Read the docs.
- Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
- Check that this is a concrete bug. For Q&A open a GitHub Discussion or join our Discord Chat Server.
- The provided reproduction is a minimal reproducible example of the bug.