From 71388374a0089a769e8d147f2e5c4d4c8ce73cae Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 20 Feb 2026 15:04:31 +0900 Subject: [PATCH 1/5] feat: support `launchOptions` with `connectOptions` --- docs/config/browser/playwright.md | 4 ++- packages/browser-playwright/src/playwright.ts | 28 +++++++++---------- test/browser/README.md | 2 -- test/browser/docker-compose.yaml | 15 ++++++++++ test/browser/specs/playwright-connect.test.ts | 2 +- 5 files changed, 33 insertions(+), 18 deletions(-) diff --git a/docs/config/browser/playwright.md b/docs/config/browser/playwright.md index 7e36ded22b56..5114c788b23f 100644 --- a/docs/config/browser/playwright.md +++ b/docs/config/browser/playwright.md @@ -74,7 +74,9 @@ These options are directly passed down to `playwright[browser].connect` command. Use `connectOptions.wsEndpoint` to connect to an existing Playwright server instead of launching browsers locally. This is useful for running browsers in Docker, in CI, or on a remote machine. ::: warning -Since this command connects to an existing Playwright server, any `launch` options will be ignored. + +Vitest forwards `launchOptions` to Playwright server via the `launch-options` query parameter on the `wsEndpoint`. This works only if the remote Playwright server supports `launch-options`, for example when using the `playwright run-server` CLI. + ::: ::: details Example: Running a Playwright Server in Docker diff --git a/packages/browser-playwright/src/playwright.ts b/packages/browser-playwright/src/playwright.ts index c0d6ebd49a21..ab561433d7d1 100644 --- a/packages/browser-playwright/src/playwright.ts +++ b/packages/browser-playwright/src/playwright.ts @@ -169,20 +169,6 @@ export class PlaywrightBrowserProvider implements BrowserProvider { const playwright = await import('playwright') - if (this.options.connectOptions) { - if (this.options.launchOptions) { - this.project.vitest.logger.warn( - c.yellow(`Found both ${c.bold(c.italic(c.yellow('connect')))} and ${c.bold(c.italic(c.yellow('launch')))} options in browser instance configuration. - Ignoring ${c.bold(c.italic(c.yellow('launch')))} options and using ${c.bold(c.italic(c.yellow('connect')))} mode. - You probably want to remove one of the two options and keep only the one you want to use.`), - ) - } - const browser = await playwright[this.browserName].connect(this.options.connectOptions.wsEndpoint, this.options.connectOptions) - this.browser = browser - this.browserPromise = null - return this.browser - } - const launchOptions: LaunchOptions = { ...this.options.launchOptions, headless: options.headless, @@ -216,6 +202,20 @@ export class PlaywrightBrowserProvider implements BrowserProvider { } debug?.('[%s] initializing the browser with launch options: %O', this.browserName, launchOptions) + + if (this.options.connectOptions) { + const { wsEndpoint, ...connectOptions } = this.options.connectOptions + const endpoint = new URL(wsEndpoint) + endpoint.searchParams.set('launch-options', JSON.stringify(launchOptions)) + const browser = await playwright[this.browserName].connect( + endpoint.toString(), + connectOptions, + ) + this.browser = browser + this.browserPromise = null + return this.browser + } + let persistentContextOption = this.options.persistentContext if (persistentContextOption && openBrowserOptions.parallel) { persistentContextOption = false diff --git a/test/browser/README.md b/test/browser/README.md index e099eac7489d..043eb8724f86 100644 --- a/test/browser/README.md +++ b/test/browser/README.md @@ -2,8 +2,6 @@ ## Using docker playwright -Some test suites don't support running it remotely (`fixtures/inspect` and `fixtures/insecure-context`). - ```sh # Start playwright browser server pnpm docker up -d diff --git a/test/browser/docker-compose.yaml b/test/browser/docker-compose.yaml index d36ac0675e04..909338991c3b 100644 --- a/test/browser/docker-compose.yaml +++ b/test/browser/docker-compose.yaml @@ -7,3 +7,18 @@ services: user: pwuser ports: - '6677:6677' + + # This setup employs simpler and robust `network_mode: host` instead of port mapping. + # This supports debug inspector, which can only bind to 127.0.0.1. + # `connectOptions.exposeNetwork: ''` is also not needed for this setup. + # Note that `network_mode: host` is supported only on Linux Docker. + # Run it by: + # pnpm run docker up playwright-host + playwright-host: + image: mcr.microsoft.com/playwright:v1.58.1-noble + command: /bin/sh -c "npx -y playwright@1.58.1 run-server --port 6677" + init: true + ipc: host + user: pwuser + network_mode: host + profiles: [host] diff --git a/test/browser/specs/playwright-connect.test.ts b/test/browser/specs/playwright-connect.test.ts index 97318ca4768b..28ca001c3bfa 100644 --- a/test/browser/specs/playwright-connect.test.ts +++ b/test/browser/specs/playwright-connect.test.ts @@ -32,7 +32,7 @@ test.runIf(provider.name === 'playwright')('[playwright] runs in connect mode', expect(exitCode).toBe(0) }) -test.runIf(provider.name === 'playwright')('[playwright] warns if both connect and launch mode are configured', async () => { +test.runIf(provider.name === 'playwright').skip('[playwright] warns if both connect and launch mode are configured', async () => { const browserServer = await chromium.launchServer() const wsEndpoint = browserServer.wsEndpoint() From 4db960a6735ff4bbb4980ad16b34dbd51d2b6399 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 20 Feb 2026 17:21:15 +0900 Subject: [PATCH 2/5] fix: still respect `x-playwright-launch-options` --- docs/config/browser/playwright.md | 2 +- packages/browser-playwright/src/playwright.ts | 24 ++++++++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/docs/config/browser/playwright.md b/docs/config/browser/playwright.md index 5114c788b23f..19d557ddd622 100644 --- a/docs/config/browser/playwright.md +++ b/docs/config/browser/playwright.md @@ -75,7 +75,7 @@ Use `connectOptions.wsEndpoint` to connect to an existing Playwright server inst ::: warning -Vitest forwards `launchOptions` to Playwright server via the `launch-options` query parameter on the `wsEndpoint`. This works only if the remote Playwright server supports `launch-options`, for example when using the `playwright run-server` CLI. +Vitest forwards `launchOptions` to Playwright server via the `x-playwright-launch-options` header. This works only if the remote Playwright server supports this header, for example when using the `playwright run-server` CLI. ::: diff --git a/packages/browser-playwright/src/playwright.ts b/packages/browser-playwright/src/playwright.ts index ab561433d7d1..c7d81dcb0233 100644 --- a/packages/browser-playwright/src/playwright.ts +++ b/packages/browser-playwright/src/playwright.ts @@ -204,14 +204,22 @@ export class PlaywrightBrowserProvider implements BrowserProvider { debug?.('[%s] initializing the browser with launch options: %O', this.browserName, launchOptions) if (this.options.connectOptions) { - const { wsEndpoint, ...connectOptions } = this.options.connectOptions - const endpoint = new URL(wsEndpoint) - endpoint.searchParams.set('launch-options', JSON.stringify(launchOptions)) - const browser = await playwright[this.browserName].connect( - endpoint.toString(), - connectOptions, - ) - this.browser = browser + const { wsEndpoint, headers, ...connectOptions } = this.options.connectOptions + const customLaunchHeader = headers?.['x-playwright-launch-options'] + if (customLaunchHeader != null) { + this.project.vitest.logger.warn( + c.yellow( + 'Detected "x-playwright-launch-options" in connectOptions.headers. Provider config launchOptions is ignored.', + ), + ) + } + this.browser = await playwright[this.browserName].connect(wsEndpoint, { + ...connectOptions, + headers: { + ...headers, + 'x-playwright-launch-options': customLaunchHeader ?? JSON.stringify(launchOptions), + }, + }) this.browserPromise = null return this.browser } From 2a05a11fe73f5bfb8a9396af296837e4dc2f9a70 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 20 Feb 2026 17:24:51 +0900 Subject: [PATCH 3/5] refactor: tweak --- packages/browser-playwright/src/playwright.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/browser-playwright/src/playwright.ts b/packages/browser-playwright/src/playwright.ts index c7d81dcb0233..678156abfa37 100644 --- a/packages/browser-playwright/src/playwright.ts +++ b/packages/browser-playwright/src/playwright.ts @@ -204,21 +204,20 @@ export class PlaywrightBrowserProvider implements BrowserProvider { debug?.('[%s] initializing the browser with launch options: %O', this.browserName, launchOptions) if (this.options.connectOptions) { - const { wsEndpoint, headers, ...connectOptions } = this.options.connectOptions - const customLaunchHeader = headers?.['x-playwright-launch-options'] - if (customLaunchHeader != null) { + let { wsEndpoint, headers = {}, ...connectOptions } = this.options.connectOptions + if ('x-playwright-launch-options' in headers) { this.project.vitest.logger.warn( c.yellow( 'Detected "x-playwright-launch-options" in connectOptions.headers. Provider config launchOptions is ignored.', ), ) } + else { + headers = { ...headers, 'x-playwright-launch-options': JSON.stringify(launchOptions) } + } this.browser = await playwright[this.browserName].connect(wsEndpoint, { ...connectOptions, - headers: { - ...headers, - 'x-playwright-launch-options': customLaunchHeader ?? JSON.stringify(launchOptions), - }, + headers, }) this.browserPromise = null return this.browser From 31479af09619b67b6619c1dc68b46e53f3a2ef60 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 20 Feb 2026 19:18:22 +0900 Subject: [PATCH 4/5] test: test launchOptions --- pnpm-lock.yaml | 6 ++ pnpm-workspace.yaml | 1 + .../fixtures/playwright-connect/basic.test.js | 4 + test/browser/package.json | 1 + test/browser/specs/playwright-connect.test.ts | 75 +++++++++---------- 5 files changed, 47 insertions(+), 40 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f626ac513716..9cdfe6e7376f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -105,6 +105,9 @@ catalogs: strip-literal: specifier: ^3.1.0 version: 3.1.0 + tinyexec: + specifier: ^1.0.2 + version: 1.0.2 tinyglobby: specifier: ^0.2.15 version: 0.2.15 @@ -1206,6 +1209,9 @@ importers: react-dom: specifier: ^19.2.4 version: 19.2.4(react@19.2.4) + tinyexec: + specifier: 'catalog:' + version: 1.0.2 url: specifier: ^0.11.4 version: 0.11.4 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e09518e9a6e1..aa8dc45f38d9 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -79,6 +79,7 @@ catalog: sirv: ^3.0.2 std-env: ^3.10.0 strip-literal: ^3.1.0 + tinyexec: ^1.0.2 tinyglobby: ^0.2.15 tinyhighlight: ^0.3.2 tinyrainbow: ^3.0.3 diff --git a/test/browser/fixtures/playwright-connect/basic.test.js b/test/browser/fixtures/playwright-connect/basic.test.js index 107d7cee9cf4..c140443a98df 100644 --- a/test/browser/fixtures/playwright-connect/basic.test.js +++ b/test/browser/fixtures/playwright-connect/basic.test.js @@ -10,3 +10,7 @@ test('[playwright] Run browser-only test in browser via connect mode', () => { expect(element instanceof HTMLElement).toBe(true) expect(element instanceof HTMLInputElement).not.toBe(true) }) + +test('[playwright] applies launch options from connect header', () => { + expect(navigator.userAgent).toContain('VitestLaunchOptionsTester') +}) diff --git a/test/browser/package.json b/test/browser/package.json index 8e95a5e69b29..4796390c9305 100644 --- a/test/browser/package.json +++ b/test/browser/package.json @@ -41,6 +41,7 @@ "playwright": "^1.58.1", "react": "^19.2.4", "react-dom": "^19.2.4", + "tinyexec": "catalog:", "url": "^0.11.4", "vitest": "workspace:*", "vitest-browser-react": "^2.0.5", diff --git a/test/browser/specs/playwright-connect.test.ts b/test/browser/specs/playwright-connect.test.ts index 28ca001c3bfa..95c3ce4fca2a 100644 --- a/test/browser/specs/playwright-connect.test.ts +++ b/test/browser/specs/playwright-connect.test.ts @@ -1,42 +1,30 @@ +import { fileURLToPath } from 'node:url' import { playwright } from '@vitest/browser-playwright' -import { chromium } from 'playwright' +import { x } from 'tinyexec' import { expect, test } from 'vitest' +import { Cli } from '../../test-utils/cli' import { provider } from '../settings' import { runBrowserTests } from './utils' -test.runIf(provider.name === 'playwright')('[playwright] runs in connect mode', async () => { - const browserServer = await chromium.launchServer() - const wsEndpoint = browserServer.wsEndpoint() - - const { stdout, exitCode, stderr } = await runBrowserTests({ - root: './fixtures/playwright-connect', - browser: { - instances: [ - { - browser: 'chromium', - name: 'chromium', - provider: playwright({ - connectOptions: { - wsEndpoint, - }, - }), - }, - ], - }, +test.runIf(provider.name === 'playwright')('[playwright] runs in connect mode', async ({ onTestFinished }) => { + const cliPath = fileURLToPath(new URL('./cli.js', import.meta.resolve('@playwright/test'))) + const subprocess = x(process.execPath, [cliPath, 'run-server', '--port', '9898']).process + const cli = new Cli({ + stdin: subprocess.stdin, + stdout: subprocess.stdout, + stderr: subprocess.stderr, + }) + let setDone: (value?: unknown) => void + const isDone = new Promise(resolve => (setDone = resolve)) + subprocess.on('exit', () => setDone()) + onTestFinished(async () => { + subprocess.kill('SIGILL') + await isDone }) - await browserServer.close() - - expect(stderr).toBe('') - expect(stdout).toContain('Tests 2 passed') - expect(exitCode).toBe(0) -}) - -test.runIf(provider.name === 'playwright').skip('[playwright] warns if both connect and launch mode are configured', async () => { - const browserServer = await chromium.launchServer() - const wsEndpoint = browserServer.wsEndpoint() + await cli.waitForStdout('Listening on ws://localhost:9898') - const { stdout, exitCode, stderr } = await runBrowserTests({ + const result = await runBrowserTests({ root: './fixtures/playwright-connect', browser: { instances: [ @@ -45,18 +33,25 @@ test.runIf(provider.name === 'playwright').skip('[playwright] warns if both conn name: 'chromium', provider: playwright({ connectOptions: { - wsEndpoint, + wsEndpoint: 'ws://localhost:9898', + }, + launchOptions: { + args: [`--user-agent=VitestLaunchOptionsTester`], }, - launchOptions: {}, }), }, ], }, - }) - - await browserServer.close() - - expect(stderr).toContain('Found both connect and launch options in browser instance configuration.') - expect(stdout).toContain('Tests 2 passed') - expect(exitCode).toBe(0) + }, ['basic.test.js']) + + expect(result.stderr).toMatchInlineSnapshot(`""`) + expect(result.errorTree()).toMatchInlineSnapshot(` + { + "basic.test.js": { + "[playwright] Run basic test in browser via connect mode": "passed", + "[playwright] Run browser-only test in browser via connect mode": "passed", + "[playwright] applies launch options from connect header": "passed", + }, + } + `) }) From afd90fab27b0076743b74c218299098af8a35b65 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 20 Feb 2026 19:19:18 +0900 Subject: [PATCH 5/5] chore: readme --- test/browser/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/browser/README.md b/test/browser/README.md index 043eb8724f86..e099eac7489d 100644 --- a/test/browser/README.md +++ b/test/browser/README.md @@ -2,6 +2,8 @@ ## Using docker playwright +Some test suites don't support running it remotely (`fixtures/inspect` and `fixtures/insecure-context`). + ```sh # Start playwright browser server pnpm docker up -d