mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-14 21:53:35 +03:00
fix: resolve ip using grid/api/testsession endpoint (#10196)
For Selenium 4, we use se:cdp ws proxy, pointing it to the hub url. For Selenium 3, we use grid api to try and get the target node ip.
This commit is contained in:
parent
4e90eb9406
commit
75efeb1e08
@ -27,7 +27,7 @@ import { ConnectionTransport, ProtocolRequest, WebSocketTransport } from '../tra
|
||||
import { CRDevTools } from './crDevTools';
|
||||
import { Browser, BrowserOptions, BrowserProcess, PlaywrightOptions } from '../browser';
|
||||
import * as types from '../types';
|
||||
import { debugMode, fetchData, headersArrayToObject, removeFolders, streamToString } from '../../utils/utils';
|
||||
import { debugMode, fetchData, headersArrayToObject, HTTPRequestParams, removeFolders, streamToString } from '../../utils/utils';
|
||||
import { RecentLogsCollector } from '../../utils/debugLogger';
|
||||
import { Progress, ProgressController } from '../progress';
|
||||
import { TimeoutSettings } from '../../utils/timeoutSettings';
|
||||
@ -65,7 +65,7 @@ export class Chromium extends BrowserType {
|
||||
|
||||
const artifactsDir = await fs.promises.mkdtemp(ARTIFACTS_FOLDER);
|
||||
|
||||
const wsEndpoint = await urlToWSEndpoint(endpointURL);
|
||||
const wsEndpoint = await urlToWSEndpoint(progress, endpointURL);
|
||||
progress.throwIfAborted();
|
||||
|
||||
const chromeTransport = await WebSocketTransport.connect(progress, wsEndpoint, headersMap);
|
||||
@ -153,7 +153,7 @@ export class Chromium extends BrowserType {
|
||||
args.push('--remote-debugging-port=0');
|
||||
const desiredCapabilities = { 'browserName': 'chrome', 'goog:chromeOptions': { args } };
|
||||
|
||||
progress.log(`<connecting to selenium> ${hubUrl}`);
|
||||
progress.log(`<selenium> connecting to ${hubUrl}`);
|
||||
const response = await fetchData({
|
||||
url: hubUrl + 'session',
|
||||
method: 'POST',
|
||||
@ -162,41 +162,59 @@ export class Chromium extends BrowserType {
|
||||
capabilities: { alwaysMatch: desiredCapabilities }
|
||||
}),
|
||||
timeout: progress.timeUntilDeadline(),
|
||||
}, async response => {
|
||||
const body = await streamToString(response);
|
||||
let message = '';
|
||||
try {
|
||||
const json = JSON.parse(body);
|
||||
message = json.value.localizedMessage || json.value.message;
|
||||
} catch (e) {
|
||||
}
|
||||
return new Error(`Error connecting to Selenium at ${hubUrl}: ${message}`);
|
||||
});
|
||||
}, seleniumErrorHandler);
|
||||
const value = JSON.parse(response).value;
|
||||
const sessionId = value.sessionId;
|
||||
progress.log(`<connected to selenium> sessionId=${sessionId}`);
|
||||
progress.log(`<selenium> connected to sessionId=${sessionId}`);
|
||||
|
||||
const disconnectFromSelenium = async () => {
|
||||
progress.log(`<disconnecting from selenium> sessionId=${sessionId}`);
|
||||
progress.log(`<selenium> disconnecting from sessionId=${sessionId}`);
|
||||
await fetchData({
|
||||
url: hubUrl + 'session/' + sessionId,
|
||||
method: 'DELETE',
|
||||
}).catch(error => progress.log(`<error disconnecting from selenium>: ${error}`));
|
||||
progress.log(`<disconnected from selenium> sessionId=${sessionId}`);
|
||||
progress.log(`<selenium> disconnected from sessionId=${sessionId}`);
|
||||
gracefullyCloseSet.delete(disconnectFromSelenium);
|
||||
};
|
||||
gracefullyCloseSet.add(disconnectFromSelenium);
|
||||
|
||||
try {
|
||||
const capabilities = value.capabilities;
|
||||
const maybeChromeOptions = capabilities['goog:chromeOptions'];
|
||||
const chromeOptions = maybeChromeOptions && typeof maybeChromeOptions === 'object' ? maybeChromeOptions : undefined;
|
||||
const debuggerAddress = chromeOptions && typeof chromeOptions.debuggerAddress === 'string' ? chromeOptions.debuggerAddress : undefined;
|
||||
const chromeOptionsURL = typeof maybeChromeOptions === 'string' ? maybeChromeOptions : undefined;
|
||||
let endpointURL = capabilities['se:cdp'] || debuggerAddress || chromeOptionsURL;
|
||||
if (!['ws://', 'wss://', 'http://', 'https://'].some(protocol => endpointURL.startsWith(protocol)))
|
||||
endpointURL = 'http://' + endpointURL;
|
||||
return this._connectOverCDPInternal(progress, endpointURL, { slowMo: options.slowMo }, disconnectFromSelenium);
|
||||
let endpointURL: URL;
|
||||
|
||||
if (capabilities['se:cdp']) {
|
||||
// Selenium 4 - use built-in CDP websocket proxy.
|
||||
const endpointURLString = addProtocol(capabilities['se:cdp']);
|
||||
endpointURL = new URL(endpointURLString);
|
||||
endpointURL.hostname = new URL(hubUrl).hostname;
|
||||
progress.log(`<selenium> retrieved endpoint ${endpointURL.toString()} for sessionId=${sessionId}`);
|
||||
} else {
|
||||
// Selenium 3 - resolve target node IP to use instead of localhost ws url.
|
||||
const maybeChromeOptions = capabilities['goog:chromeOptions'];
|
||||
const chromeOptions = maybeChromeOptions && typeof maybeChromeOptions === 'object' ? maybeChromeOptions : undefined;
|
||||
const debuggerAddress = chromeOptions && typeof chromeOptions.debuggerAddress === 'string' ? chromeOptions.debuggerAddress : undefined;
|
||||
const chromeOptionsURL = typeof maybeChromeOptions === 'string' ? maybeChromeOptions : undefined;
|
||||
const endpointURLString = addProtocol(debuggerAddress || chromeOptionsURL);
|
||||
progress.log(`<selenium> retrieved endpoint ${endpointURLString} for sessionId=${sessionId}`);
|
||||
endpointURL = new URL(endpointURLString);
|
||||
if (endpointURL.hostname === 'localhost' || endpointURL.hostname === '127.0.0.1') {
|
||||
const sessionInfoUrl = new URL(hubUrl).origin + '/grid/api/testsession?session=' + sessionId;
|
||||
try {
|
||||
const sessionResponse = await fetchData({
|
||||
url: sessionInfoUrl,
|
||||
method: 'GET',
|
||||
timeout: progress.timeUntilDeadline(),
|
||||
}, seleniumErrorHandler);
|
||||
const proxyId = JSON.parse(sessionResponse).proxyId;
|
||||
endpointURL.hostname = new URL(proxyId).hostname;
|
||||
progress.log(`<selenium> resolved endpoint ip ${endpointURL.toString()} for sessionId=${sessionId}`);
|
||||
} catch (e) {
|
||||
progress.log(`<selenium> unable to resolve endpoint ip for sessionId=${sessionId}, running in standalone?`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await this._connectOverCDPInternal(progress, endpointURL.toString(), { slowMo: options.slowMo }, disconnectFromSelenium);
|
||||
} catch (e) {
|
||||
await disconnectFromSelenium();
|
||||
throw e;
|
||||
@ -296,9 +314,10 @@ const DEFAULT_ARGS = [
|
||||
'--no-service-autorun',
|
||||
];
|
||||
|
||||
async function urlToWSEndpoint(endpointURL: string) {
|
||||
async function urlToWSEndpoint(progress: Progress, endpointURL: string) {
|
||||
if (endpointURL.startsWith('ws'))
|
||||
return endpointURL;
|
||||
progress.log(`<ws preparing> retrieving websocket url from ${endpointURL}`);
|
||||
const httpURL = endpointURL.endsWith('/') ? `${endpointURL}json/version/` : `${endpointURL}/json/version/`;
|
||||
const request = endpointURL.startsWith('https') ? https : http;
|
||||
const json = await new Promise<string>((resolve, reject) => {
|
||||
@ -314,3 +333,20 @@ async function urlToWSEndpoint(endpointURL: string) {
|
||||
});
|
||||
return JSON.parse(json).webSocketDebuggerUrl;
|
||||
}
|
||||
|
||||
async function seleniumErrorHandler(params: HTTPRequestParams, response: http.IncomingMessage) {
|
||||
const body = await streamToString(response);
|
||||
let message = body;
|
||||
try {
|
||||
const json = JSON.parse(body);
|
||||
message = json.value.localizedMessage || json.value.message;
|
||||
} catch (e) {
|
||||
}
|
||||
return new Error(`Error connecting to Selenium at ${params.url}: ${message}`);
|
||||
}
|
||||
|
||||
function addProtocol(url: string) {
|
||||
if (!['ws://', 'wss://', 'http://', 'https://'].some(protocol => url.startsWith(protocol)))
|
||||
return 'http://' + url;
|
||||
return url;
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ const ProxyAgent = require('https-proxy-agent');
|
||||
|
||||
export const existsAsync = (path: string): Promise<boolean> => new Promise(resolve => fs.stat(path, err => resolve(!err)));
|
||||
|
||||
type HTTPRequestParams = {
|
||||
export type HTTPRequestParams = {
|
||||
url: string,
|
||||
method?: string,
|
||||
headers?: http.OutgoingHttpHeaders,
|
||||
@ -97,11 +97,11 @@ function httpRequest(params: HTTPRequestParams, onResponse: (r: http.IncomingMes
|
||||
request.end(params.data);
|
||||
}
|
||||
|
||||
export function fetchData(params: HTTPRequestParams, onError?: (response: http.IncomingMessage) => Promise<Error>): Promise<string> {
|
||||
export function fetchData(params: HTTPRequestParams, onError?: (params: HTTPRequestParams, response: http.IncomingMessage) => Promise<Error>): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
httpRequest(params, async response => {
|
||||
if (response.statusCode !== 200) {
|
||||
const error = onError ? await onError(response) : new Error(`fetch failed: server returned code ${response.statusCode}. URL: ${params.url}`);
|
||||
const error = onError ? await onError(params, response) : new Error(`fetch failed: server returned code ${response.statusCode}. URL: ${params.url}`);
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
@ -22,13 +22,13 @@ import { start } from '../packages/playwright-core/lib/outofprocess';
|
||||
|
||||
const chromeDriver = require('chromedriver').path;
|
||||
const brokenDriver = path.join(__dirname, 'assets', 'selenium-grid', 'broken-selenium-driver.js');
|
||||
const seleniumConfigStandalone = path.join(__dirname, 'assets', 'selenium-grid', 'selenium-config-standalone.json');
|
||||
const standalone_3_141_59 = path.join(__dirname, 'assets', 'selenium-grid', 'selenium-server-standalone-3.141.59.jar');
|
||||
const selenium_4_0_0_rc1 = path.join(__dirname, 'assets', 'selenium-grid', 'selenium-server-4.0.0-rc-1.jar');
|
||||
|
||||
function writeSeleniumConfig(testInfo: TestInfo, port: number) {
|
||||
const content = fs.readFileSync(seleniumConfigStandalone, 'utf8').replace(/4444/g, String(port));
|
||||
const file = testInfo.outputPath('selenium-config.json');
|
||||
const template = path.join(__dirname, 'assets', 'selenium-grid', `selenium-config-standalone.json`);
|
||||
const content = fs.readFileSync(template, 'utf8').replace(/4444/g, String(port));
|
||||
const file = testInfo.outputPath(`selenium-config-standalone.json`);
|
||||
fs.writeFileSync(file, content, 'utf8');
|
||||
return file;
|
||||
}
|
||||
@ -60,6 +60,39 @@ test('selenium grid 3.141.59 standalone chromium', async ({ browserName, childPr
|
||||
await grid.waitForOutput('Removing session');
|
||||
});
|
||||
|
||||
test('selenium grid 3.141.59 hub + node chromium', async ({ browserName, childProcess, waitForPort, browserType }, testInfo) => {
|
||||
test.skip(browserName !== 'chromium');
|
||||
|
||||
const port = testInfo.workerIndex + 15123;
|
||||
const hub = childProcess({
|
||||
command: ['java', '-jar', standalone_3_141_59, '-role', 'hub', '-port', String(port)],
|
||||
cwd: __dirname,
|
||||
});
|
||||
await waitForPort(port);
|
||||
|
||||
const node = childProcess({
|
||||
command: ['java', `-Dwebdriver.chrome.driver=${chromeDriver}`, '-jar', standalone_3_141_59, '-role', 'node', '-host', '127.0.0.1', '-hub', `http://localhost:${port}/grid/register`],
|
||||
cwd: __dirname,
|
||||
});
|
||||
await Promise.all([
|
||||
node.waitForOutput('The node is registered to the hub and ready to use'),
|
||||
hub.waitForOutput('Registered a node'),
|
||||
]);
|
||||
|
||||
const __testHookSeleniumRemoteURL = `http://localhost:${port}/wd/hub`;
|
||||
const browser = await browserType.launch({ __testHookSeleniumRemoteURL } as any);
|
||||
const page = await browser.newPage();
|
||||
await page.setContent('<title>Hello world</title><div>Get Started</div>');
|
||||
await page.click('text=Get Started');
|
||||
await expect(page).toHaveTitle('Hello world');
|
||||
await browser.close();
|
||||
|
||||
expect(hub.output).toContain('Got a request to create a new session');
|
||||
expect(node.output).toContain('Starting ChromeDriver');
|
||||
expect(node.output).toContain('Started new session');
|
||||
await node.waitForOutput('Removing session');
|
||||
});
|
||||
|
||||
test('selenium grid 4.0.0-rc-1 standalone chromium', async ({ browserName, childProcess, waitForPort, browserType }, testInfo) => {
|
||||
test.skip(browserName !== 'chromium');
|
||||
|
||||
@ -83,6 +116,38 @@ test('selenium grid 4.0.0-rc-1 standalone chromium', async ({ browserName, child
|
||||
await grid.waitForOutput('Deleted session');
|
||||
});
|
||||
|
||||
test('selenium grid 4.0.0-rc-1 hub + node chromium', async ({ browserName, childProcess, waitForPort, browserType }, testInfo) => {
|
||||
test.skip(browserName !== 'chromium');
|
||||
|
||||
const port = testInfo.workerIndex + 15123;
|
||||
const hub = childProcess({
|
||||
command: ['java', '-jar', selenium_4_0_0_rc1, 'hub', '--port', String(port)],
|
||||
cwd: __dirname,
|
||||
});
|
||||
await waitForPort(port);
|
||||
const __testHookSeleniumRemoteURL = `http://localhost:${port}/wd/hub`;
|
||||
|
||||
const node = childProcess({
|
||||
command: ['java', `-Dwebdriver.chrome.driver=${chromeDriver}`, '-jar', selenium_4_0_0_rc1, 'node', '--grid-url', `http://localhost:${port}`, '--port', String(port + 1)],
|
||||
cwd: __dirname,
|
||||
});
|
||||
await Promise.all([
|
||||
node.waitForOutput('Node has been added'),
|
||||
hub.waitForOutput('from DOWN to UP'),
|
||||
]);
|
||||
|
||||
const browser = await browserType.launch({ __testHookSeleniumRemoteURL } as any);
|
||||
const page = await browser.newPage();
|
||||
await page.setContent('<title>Hello world</title><div>Get Started</div>');
|
||||
await page.click('text=Get Started');
|
||||
await expect(page).toHaveTitle('Hello world');
|
||||
await browser.close();
|
||||
|
||||
expect(hub.output).toContain('Session request received by the distributor');
|
||||
expect(node.output).toContain('Starting ChromeDriver');
|
||||
await hub.waitForOutput('Deleted session');
|
||||
});
|
||||
|
||||
test('selenium grid 4.0.0-rc-1 standalone chromium broken driver', async ({ browserName, childProcess, waitForPort, browserType }, testInfo) => {
|
||||
test.skip(browserName !== 'chromium');
|
||||
|
||||
@ -95,7 +160,7 @@ test('selenium grid 4.0.0-rc-1 standalone chromium broken driver', async ({ brow
|
||||
|
||||
const __testHookSeleniumRemoteURL = `http://localhost:${port}/wd/hub`;
|
||||
const error = await browserType.launch({ __testHookSeleniumRemoteURL } as any).catch(e => e);
|
||||
expect(error.message).toContain(`Error connecting to Selenium at http://localhost:${port}/wd/hub/: Could not start a new session`);
|
||||
expect(error.message).toContain(`Error connecting to Selenium at http://localhost:${port}/wd/hub/session: Could not start a new session`);
|
||||
|
||||
expect(grid.output).not.toContain('Starting ChromeDriver');
|
||||
});
|
||||
@ -108,7 +173,7 @@ test('selenium grid 3.141.59 standalone non-chromium', async ({ browserName, bro
|
||||
expect(error.message).toContain('Connecting to SELENIUM_REMOTE_URL is only supported by Chromium');
|
||||
});
|
||||
|
||||
test('selenium grid 3.141.59 standalone chromium through driver', async ({ browserName, childProcess, waitForPort }, testInfo) => {
|
||||
test('selenium grid 3.141.59 standalone chromium through run-driver', async ({ browserName, childProcess, waitForPort }, testInfo) => {
|
||||
test.skip(browserName !== 'chromium');
|
||||
|
||||
const port = testInfo.workerIndex + 15123;
|
||||
|
Loading…
Reference in New Issue
Block a user