diff --git a/docs/config/browser/playwright.md b/docs/config/browser/playwright.md index 7e36ded22..19d557ddd 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 `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. + ::: ::: 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 c0d6ebd49..678156abf 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,27 @@ export class PlaywrightBrowserProvider implements BrowserProvider { } debug?.('[%s] initializing the browser with launch options: %O', this.browserName, launchOptions) + + if (this.options.connectOptions) { + 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, + }) + this.browserPromise = null + return this.browser + } + let persistentContextOption = this.options.persistentContext if (persistentContextOption && openBrowserOptions.parallel) { persistentContextOption = false diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f626ac513..9cdfe6e73 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 e09518e9a..aa8dc45f3 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/docker-compose.yaml b/test/browser/docker-compose.yaml index d36ac0675..909338991 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/fixtures/playwright-connect/basic.test.js b/test/browser/fixtures/playwright-connect/basic.test.js index 107d7cee9..c140443a9 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 8e95a5e69..4796390c9 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 97318ca47..95c3ce4fc 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')('[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')('[playwright] warns if both connect a 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", + }, + } + `) })