diff --git a/docs/src/test-api/class-testoptions.md b/docs/src/test-api/class-testoptions.md index 749d5e260d..5453b1d7bd 100644 --- a/docs/src/test-api/class-testoptions.md +++ b/docs/src/test-api/class-testoptions.md @@ -110,6 +110,13 @@ Learn more about [various timeouts](./test-timeouts.md). ## property: TestOptions.colorScheme = %%-context-option-colorscheme-%% +## property: TestOptions.connectOptions +- type: <[void]|[Object]> + - `wsEndpoint` <[string]> A browser websocket endpoint to connect to. + - `headers` <[void]|[Object]<[string], [string]>> Additional HTTP headers to be sent with web socket connect request. Optional. + +When connect options are specified, default [`property: Fixtures.browser`], [`property: Fixtures.context`] and [`property: Fixtures.page`] use the remote browser instead of launching a browser locally, and any launch options like [`property: TestOptions.headless`] or [`property: TestOptions.channel`] are ignored. + ## property: TestOptions.contextOptions - type: <[Object]> diff --git a/packages/playwright-core/src/dispatchers/browserTypeDispatcher.ts b/packages/playwright-core/src/dispatchers/browserTypeDispatcher.ts index f3bce05a1d..22d459e49b 100644 --- a/packages/playwright-core/src/dispatchers/browserTypeDispatcher.ts +++ b/packages/playwright-core/src/dispatchers/browserTypeDispatcher.ts @@ -63,6 +63,7 @@ export class BrowserTypeDispatcher extends Dispatcher(); diff --git a/packages/playwright-test/src/index.ts b/packages/playwright-test/src/index.ts index 6b80507641..88f3276d40 100644 --- a/packages/playwright-test/src/index.ts +++ b/packages/playwright-test/src/index.ts @@ -70,6 +70,7 @@ export const test = _baseTest.extend({ headless: [ undefined, { scope: 'worker', option: true } ], channel: [ undefined, { scope: 'worker', option: true } ], launchOptions: [ {}, { scope: 'worker', option: true } ], + connectOptions: [ undefined, { scope: 'worker', option: true } ], screenshot: [ 'off', { scope: 'worker', option: true } ], video: [ 'off', { scope: 'worker', option: true } ], trace: [ 'off', { scope: 'worker', option: true } ], @@ -100,9 +101,16 @@ export const test = _baseTest.extend({ await use(options); }, { scope: 'worker' }], - browser: [async ({ playwright, browserName }, use) => { + browser: [async ({ playwright, browserName, connectOptions }, use) => { if (!['chromium', 'firefox', 'webkit'].includes(browserName)) throw new Error(`Unexpected browserName "${browserName}", must be one of "chromium", "firefox" or "webkit"`); + if (connectOptions) { + const browser = await playwright[browserName].connect(connectOptions); + await use(browser); + await browser.close(); + return; + } + const browser = await playwright[browserName].launch(); await use(browser); await browser.close(); diff --git a/packages/playwright-test/types/test.d.ts b/packages/playwright-test/types/test.d.ts index e2da6cb791..acbcef581b 100644 --- a/packages/playwright-test/types/test.d.ts +++ b/packages/playwright-test/types/test.d.ts @@ -2658,6 +2658,17 @@ type ColorScheme = Exclude; type ExtraHTTPHeaders = Exclude; type Proxy = Exclude; type StorageState = Exclude; +type ConnectOptions = { + /** + * A browser websocket endpoint to connect to. + */ + wsEndpoint: string; + + /** + * Additional HTTP headers to be sent with web socket connect request. + */ + headers?: { [key: string]: string; }; +}; /** * Playwright Test provides many options to configure test environment, [Browser], [BrowserContext] and more. @@ -2735,6 +2746,16 @@ export interface PlaywrightWorkerOptions { * [testOptions.channel](https://playwright.dev/docs/api/class-testoptions#test-options-channel) take priority over this. */ launchOptions: LaunchOptions; + /** + * When connect options are specified, default + * [fixtures.browser](https://playwright.dev/docs/api/class-fixtures#fixtures-browser), + * [fixtures.context](https://playwright.dev/docs/api/class-fixtures#fixtures-context) and + * [fixtures.page](https://playwright.dev/docs/api/class-fixtures#fixtures-page) use the remote browser instead of + * launching a browser locally, and any launch options like + * [testOptions.headless](https://playwright.dev/docs/api/class-testoptions#test-options-headless) or + * [testOptions.channel](https://playwright.dev/docs/api/class-testoptions#test-options-channel) are ignored. + */ + connectOptions: ConnectOptions | undefined; /** * Whether to automatically capture a screenshot after each test. Defaults to `'off'`. * - `'off'`: Do not capture screenshots. diff --git a/tests/playwright-test/playwright.spec.ts b/tests/playwright-test/playwright.spec.ts index 8a14eb4a11..6d9a95f8c7 100644 --- a/tests/playwright-test/playwright.spec.ts +++ b/tests/playwright-test/playwright.spec.ts @@ -546,3 +546,58 @@ test('should work with video.path() throwing', async ({ runInlineTest }, testInf const video = fs.readdirSync(dir).find(file => file.endsWith('webm')); expect(video).toBeTruthy(); }); + +test('should work with connectOptions', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.js': ` + module.exports = { + globalSetup: './global-setup', + use: { + connectOptions: { + wsEndpoint: process.env.CONNECT_WS_ENDPOINT, + }, + }, + }; + `, + 'global-setup.ts': ` + module.exports = async () => { + const server = await pwt.chromium.launchServer(); + process.env.CONNECT_WS_ENDPOINT = server.wsEndpoint(); + return () => server.close(); + }; + `, + 'a.test.ts': ` + const { test } = pwt; + test('pass', async ({ page }) => { + await page.setContent('
PASS
'); + await expect(page.locator('div')).toHaveText('PASS'); + }); + `, + }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); +}); + +test('should throw with bad connectOptions', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.js': ` + module.exports = { + use: { + connectOptions: { + wsEndpoint: 'http://does-not-exist-bad-domain.oh-no-should-not-work', + }, + }, + }; + `, + 'a.test.ts': ` + const { test } = pwt; + test('pass', async ({ page }) => { + await page.setContent('
PASS
'); + await expect(page.locator('div')).toHaveText('PASS'); + }); + `, + }); + expect(result.exitCode).toBe(1); + expect(result.passed).toBe(0); + expect(result.output).toContain('browserType.connect:'); +}); diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index b553255e2e..3f35f1b7b5 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -297,6 +297,17 @@ type ColorScheme = Exclude; type ExtraHTTPHeaders = Exclude; type Proxy = Exclude; type StorageState = Exclude; +type ConnectOptions = { + /** + * A browser websocket endpoint to connect to. + */ + wsEndpoint: string; + + /** + * Additional HTTP headers to be sent with web socket connect request. + */ + headers?: { [key: string]: string; }; +}; export interface PlaywrightWorkerOptions { browserName: BrowserName; @@ -304,6 +315,7 @@ export interface PlaywrightWorkerOptions { headless: boolean | undefined; channel: BrowserChannel | undefined; launchOptions: LaunchOptions; + connectOptions: ConnectOptions | undefined; screenshot: 'off' | 'on' | 'only-on-failure'; trace: TraceMode | /** deprecated */ 'retry-with-trace' | { mode: TraceMode, snapshots?: boolean, screenshots?: boolean, sources?: boolean }; video: VideoMode | /** deprecated */ 'retry-with-video' | { mode: VideoMode, size?: ViewportSize };