diff --git a/docs/src/selenium-grid.md b/docs/src/selenium-grid.md index 6e5f146198c2c..facab65eb230b 100644 --- a/docs/src/selenium-grid.md +++ b/docs/src/selenium-grid.md @@ -44,6 +44,36 @@ You don't have to change your code, just use your testing harness or [`method: B When using Selenium Grid Hub, you can [skip browser downloads](./browsers.md#skip-browser-downloads). +Besides this, you can specify the url using the launch options (has a lower priority than the environment variable). + +```js tab=js-test title="playwright.config.ts" +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + projects: [ + { + name: 'chromium', + use: { + launchOptions: { + selenium: { + url: 'http://localhost:4444/wd/hub', + }, + }, + }, + }, + ], +}); +``` + +```js tab=js-library +const { chromium } = require('playwright'); +const browser = await chromium.launch({ + selenium: { + url: 'http://localhost:4444/wd/hub', + }, +}); +``` + ### Passing additional capabilities If your grid requires additional capabilities to be set (for example, you use an external service), you can set `SELENIUM_REMOTE_CAPABILITIES` environment variable to provide JSON-serialized capabilities. @@ -64,6 +94,50 @@ SELENIUM_REMOTE_URL=http://:4444 SELENIUM_REMOTE_CAPABILITIES=" SELENIUM_REMOTE_URL=http://:4444 SELENIUM_REMOTE_CAPABILITIES="{'mygrid:options':{os:'windows',username:'John',password:'secure'}}" dotnet test ``` +Also can be specified using the launch options (has a lower priority than the environment variable): + +```js tab=js-test title="playwright.config.ts" +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + projects: [ + { + name: 'chromium', + use: { + launchOptions: { + selenium: { + url: 'http://localhost:4444/wd/hub', + capabilities: { + 'mygrid:options': { + os: 'windows', + username: 'John', + password: 'secure', + } + }, + }, + }, + }, + }, + ], +}); +``` + +```js tab=js-library +const { chromium } = require('playwright'); +const browser = await chromium.launch({ + selenium: { + url: 'http://localhost:4444/wd/hub', + capabilities: { + 'mygrid:options': { + os: 'windows', + username: 'John', + password: 'secure', + }, + }, + }, +}); +``` + ### Passing additional headers If your grid requires additional headers to be set (for example, you should provide authorization token to use browsers in your cloud), you can set `SELENIUM_REMOTE_HEADERS` environment variable to provide JSON-serialized headers. @@ -84,6 +158,42 @@ SELENIUM_REMOTE_URL=http://:4444 SELENIUM_REMOTE_HEADERS="{'Aut SELENIUM_REMOTE_URL=http://:4444 SELENIUM_REMOTE_HEADERS="{'Authorization':'OAuth 12345'}" dotnet test ``` +Also can be specified using the launch options (has a lower priority than the environment variable): + +```js tab=js-test title="playwright.config.ts" +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + projects: [ + { + name: 'chromium', + use: { + launchOptions: { + selenium: { + url: 'http://localhost:4444/wd/hub', + headers: { + 'Authorization': 'OAuth 12345', + }, + }, + }, + }, + }, + ], +}); +``` + +```js tab=js-library +const { chromium } = require('playwright'); +const browser = await chromium.launch({ + selenium: { + url: 'http://localhost:4444/wd/hub', + headers: { + 'Authorization': 'OAuth 12345', + }, + }, +}); +``` + ### Detailed logs Run with `DEBUG=pw:browser*` environment variable to see how Playwright is connecting to Selenium Grid. @@ -106,8 +216,6 @@ DEBUG=pw:browser* SELENIUM_REMOTE_URL=http://internal.grid:4444 dotnet test If you file an issue, please include this log. - - ## Using Selenium Docker One easy way to use Selenium Grid is to run official docker containers. Read more in [selenium docker images](https://github.com/SeleniumHQ/docker-selenium) documentation. For experimental arm images, see [docker-seleniarm](https://github.com/seleniumhq-community/docker-seleniarm). @@ -188,7 +296,6 @@ SELENIUM_REMOTE_URL=http://:4444 mvn test SELENIUM_REMOTE_URL=http://:4444 dotnet test ``` - ## Selenium 3 Internally, Playwright connects to the browser using [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) websocket. Selenium 4 exposes this capability, while Selenium 3 does not. diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 575784da91958..3f72f85f776a8 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -492,6 +492,11 @@ scheme.BrowserTypeLaunchParams = tObject({ downloadsPath: tOptional(tString), tracesDir: tOptional(tString), chromiumSandbox: tOptional(tBoolean), + selenium: tOptional(tObject({ + url: tOptional(tString), + capabilities: tOptional(tAny), + headers: tOptional(tAny), + })), firefoxUserPrefs: tOptional(tAny), slowMo: tOptional(tNumber), }); @@ -520,6 +525,11 @@ scheme.BrowserTypeLaunchPersistentContextParams = tObject({ downloadsPath: tOptional(tString), tracesDir: tOptional(tString), chromiumSandbox: tOptional(tBoolean), + selenium: tOptional(tObject({ + url: tOptional(tString), + capabilities: tOptional(tAny), + headers: tOptional(tAny), + })), noDefaultViewport: tOptional(tBoolean), viewport: tOptional(tObject({ width: tNumber, diff --git a/packages/playwright-core/src/server/browserType.ts b/packages/playwright-core/src/server/browserType.ts index dedba5df0a744..50ef94e1f77c6 100644 --- a/packages/playwright-core/src/server/browserType.ts +++ b/packages/playwright-core/src/server/browserType.ts @@ -65,7 +65,8 @@ export abstract class BrowserType extends SdkObject { const controller = new ProgressController(metadata, this); controller.setLogName('browser'); const browser = await controller.run(progress => { - const seleniumHubUrl = (options as any).__testHookSeleniumRemoteURL || process.env.SELENIUM_REMOTE_URL; + const seleniumHubUrl = process.env.SELENIUM_REMOTE_URL || options.selenium?.url; + if (seleniumHubUrl) return this._launchWithSeleniumHub(progress, seleniumHubUrl, options); return this._innerLaunchWithRetries(progress, options, undefined, helper.debugProtocolLogger(protocolLogger)).catch(e => { throw this._rewriteStartupError(e); }); diff --git a/packages/playwright-core/src/server/chromium/chromium.ts b/packages/playwright-core/src/server/chromium/chromium.ts index f4d9ec01001fd..f9210c05243c7 100644 --- a/packages/playwright-core/src/server/chromium/chromium.ts +++ b/packages/playwright-core/src/server/chromium/chromium.ts @@ -175,9 +175,11 @@ export class Chromium extends BrowserType { const args = this._innerDefaultArgs(options); args.push('--remote-debugging-port=0'); const isEdge = options.channel && options.channel.startsWith('msedge'); + let desiredCapabilities = { 'browserName': isEdge ? 'MicrosoftEdge' : 'chrome', - [isEdge ? 'ms:edgeOptions' : 'goog:chromeOptions']: { args } + [isEdge ? 'ms:edgeOptions' : 'goog:chromeOptions']: { args }, + ...options.selenium?.capabilities }; if (process.env.SELENIUM_REMOTE_CAPABILITIES) { @@ -186,7 +188,8 @@ export class Chromium extends BrowserType { desiredCapabilities = { ...desiredCapabilities, ...remoteCapabilities }; } - let headers: { [key: string]: string } = {}; + let headers = { ...options.selenium?.headers }; + if (process.env.SELENIUM_REMOTE_HEADERS) { const remoteHeaders = parseSeleniumRemoteParams({ name: 'headers', value: process.env.SELENIUM_REMOTE_HEADERS }, progress); if (remoteHeaders) diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index bcfbeaac7c22c..e136af7384348 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -19607,6 +19607,26 @@ export interface LaunchOptions { * If specified, traces are saved into this directory. */ tracesDir?: string; + + /** + * Selenium settings + */ + selenium?: { + /** + * Url to connect to selenium server + */ + url?: string; + + /** + * Browser desired capabilities. Check out the [WebDriver Protocol](https://w3c.github.io/webdriver/#capabilities) for more details. + */ + capabilities?: any; + + /** + * Custom headers to pass into every request + */ + headers?: { [key: string]: string; }; + }, } export interface ConnectOverCDPOptions { diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index 1dbc4e19fb37f..8ea392b6c42ce 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -878,6 +878,11 @@ export type BrowserTypeLaunchParams = { downloadsPath?: string, tracesDir?: string, chromiumSandbox?: boolean, + selenium?: { + url?: string, + capabilities?: any, + headers?: any, + }, firefoxUserPrefs?: any, slowMo?: number, }; @@ -903,6 +908,11 @@ export type BrowserTypeLaunchOptions = { downloadsPath?: string, tracesDir?: string, chromiumSandbox?: boolean, + selenium?: { + url?: string, + capabilities?: any, + headers?: any, + }, firefoxUserPrefs?: any, slowMo?: number, }; @@ -931,6 +941,11 @@ export type BrowserTypeLaunchPersistentContextParams = { downloadsPath?: string, tracesDir?: string, chromiumSandbox?: boolean, + selenium?: { + url?: string, + capabilities?: any, + headers?: any, + }, noDefaultViewport?: boolean, viewport?: { width: number, @@ -1002,6 +1017,11 @@ export type BrowserTypeLaunchPersistentContextOptions = { downloadsPath?: string, tracesDir?: string, chromiumSandbox?: boolean, + selenium?: { + url?: string, + capabilities?: any, + headers?: any, + }, noDefaultViewport?: boolean, viewport?: { width: number, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index b6178f60501c0..9f5e18be5f374 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -411,6 +411,12 @@ LaunchOptions: downloadsPath: string? tracesDir: string? chromiumSandbox: boolean? + selenium: + type: object? + properties: + url: string? + capabilities: json? + headers: json? ContextOptions: diff --git a/tests/library/browsertype-launch-selenium.spec.ts b/tests/library/browsertype-launch-selenium.spec.ts index d46ca0c5f81ec..74499c31fa59e 100644 --- a/tests/library/browsertype-launch-selenium.spec.ts +++ b/tests/library/browsertype-launch-selenium.spec.ts @@ -52,8 +52,11 @@ test('selenium grid 3.141.59 standalone chromium', async ({ browserName, childPr }); await waitForPort(port); - const __testHookSeleniumRemoteURL = `http://127.0.0.1:${port}/wd/hub`; - const browser = await browserType.launch({ __testHookSeleniumRemoteURL } as any); + const browser = await browserType.launch({ + selenium: { + url: `http://127.0.0.1:${port}/wd/hub`, + }, + }); const page = await browser.newPage(); await page.setContent('Hello world
Get Started
'); await page.click('text=Get Started'); @@ -84,8 +87,11 @@ test('selenium grid 3.141.59 hub + node chromium', async ({ browserName, childPr hub.waitForOutput('Registered a node'), ]); - const __testHookSeleniumRemoteURL = `http://127.0.0.1:${port}/wd/hub`; - const browser = await browserType.launch({ __testHookSeleniumRemoteURL } as any); + const browser = await browserType.launch({ + selenium: { + url: `http://127.0.0.1:${port}/wd/hub`, + }, + }); const page = await browser.newPage(); await page.setContent('Hello world
Get Started
'); await page.click('text=Get Started'); @@ -108,8 +114,11 @@ test('selenium grid 4.8.3 standalone chromium', async ({ browserName, childProce }); await waitForPort(port); - const __testHookSeleniumRemoteURL = `http://127.0.0.1:${port}/`; - const browser = await browserType.launch({ __testHookSeleniumRemoteURL } as any); + const browser = await browserType.launch({ + selenium: { + url: `http://127.0.0.1:${port}/`, + }, + }); const page = await browser.newPage(); await page.setContent('Hello world
Get Started
'); await page.click('text=Get Started'); @@ -130,7 +139,6 @@ test('selenium grid 4.8.3 hub + node chromium', async ({ browserName, childProce cwd: __dirname, }); await waitForPort(port); - const __testHookSeleniumRemoteURL = `http://127.0.0.1:${port}/`; const node = childProcess({ command: ['java', `-Dwebdriver.chrome.driver=${chromeDriver}`, '-jar', selenium_4_8_3, 'node', '--grid-url', `http://127.0.0.1:${port}`, '--port', String(port + 1)], @@ -141,7 +149,11 @@ test('selenium grid 4.8.3 hub + node chromium', async ({ browserName, childProce hub.waitForOutput('from DOWN to UP'), ]); - const browser = await browserType.launch({ __testHookSeleniumRemoteURL } as any); + const browser = await browserType.launch({ + selenium: { + url: `http://127.0.0.1:${port}/`, + }, + }); const page = await browser.newPage(); await page.setContent('Hello world
Get Started
'); await page.click('text=Get Started'); @@ -163,8 +175,11 @@ test('selenium grid 4.8.3 standalone chromium broken driver', async ({ browserNa }); await waitForPort(port); - const __testHookSeleniumRemoteURL = `http://127.0.0.1:${port}/`; - const error = await browserType.launch({ __testHookSeleniumRemoteURL } as any).catch(e => e); + const error = await browserType.launch({ + selenium: { + url: `http://127.0.0.1:${port}/`, + }, + }).catch(e => e); expect(error.message).toContain(`Error connecting to Selenium at http://127.0.0.1:${port}/session: Could not start a new session`); expect(grid.output).not.toContain('Starting ChromeDriver'); @@ -173,8 +188,11 @@ test('selenium grid 4.8.3 standalone chromium broken driver', async ({ browserNa test('selenium grid 3.141.59 standalone non-chromium', async ({ browserName, browserType }, testInfo) => { test.skip(browserName === 'chromium'); - const __testHookSeleniumRemoteURL = `http://127.0.0.1:4444/wd/hub`; - const error = await browserType.launch({ __testHookSeleniumRemoteURL } as any).catch(e => e); + const error = await browserType.launch({ + selenium: { + url: `http://127.0.0.1:4444/wd/hub`, + }, + }).catch(e => e); expect(error.message).toContain('Connecting to SELENIUM_REMOTE_URL is only supported by Chromium'); });