Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/config/browser/playwright.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 21 additions & 14 deletions packages/browser-playwright/src/playwright.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions test/browser/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: '<loopback>'` 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]
4 changes: 4 additions & 0 deletions test/browser/fixtures/playwright-connect/basic.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})
1 change: 1 addition & 0 deletions test/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
75 changes: 35 additions & 40 deletions test/browser/specs/playwright-connect.test.ts
Original file line number Diff line number Diff line change
@@ -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: [
Expand All @@ -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",
},
}
`)
})
Loading