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 { CRDevTools } from './crDevTools';
|
||||||
import { Browser, BrowserOptions, BrowserProcess, PlaywrightOptions } from '../browser';
|
import { Browser, BrowserOptions, BrowserProcess, PlaywrightOptions } from '../browser';
|
||||||
import * as types from '../types';
|
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 { RecentLogsCollector } from '../../utils/debugLogger';
|
||||||
import { Progress, ProgressController } from '../progress';
|
import { Progress, ProgressController } from '../progress';
|
||||||
import { TimeoutSettings } from '../../utils/timeoutSettings';
|
import { TimeoutSettings } from '../../utils/timeoutSettings';
|
||||||
@ -65,7 +65,7 @@ export class Chromium extends BrowserType {
|
|||||||
|
|
||||||
const artifactsDir = await fs.promises.mkdtemp(ARTIFACTS_FOLDER);
|
const artifactsDir = await fs.promises.mkdtemp(ARTIFACTS_FOLDER);
|
||||||
|
|
||||||
const wsEndpoint = await urlToWSEndpoint(endpointURL);
|
const wsEndpoint = await urlToWSEndpoint(progress, endpointURL);
|
||||||
progress.throwIfAborted();
|
progress.throwIfAborted();
|
||||||
|
|
||||||
const chromeTransport = await WebSocketTransport.connect(progress, wsEndpoint, headersMap);
|
const chromeTransport = await WebSocketTransport.connect(progress, wsEndpoint, headersMap);
|
||||||
@ -153,7 +153,7 @@ export class Chromium extends BrowserType {
|
|||||||
args.push('--remote-debugging-port=0');
|
args.push('--remote-debugging-port=0');
|
||||||
const desiredCapabilities = { 'browserName': 'chrome', 'goog:chromeOptions': { args } };
|
const desiredCapabilities = { 'browserName': 'chrome', 'goog:chromeOptions': { args } };
|
||||||
|
|
||||||
progress.log(`<connecting to selenium> ${hubUrl}`);
|
progress.log(`<selenium> connecting to ${hubUrl}`);
|
||||||
const response = await fetchData({
|
const response = await fetchData({
|
||||||
url: hubUrl + 'session',
|
url: hubUrl + 'session',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -162,41 +162,59 @@ export class Chromium extends BrowserType {
|
|||||||
capabilities: { alwaysMatch: desiredCapabilities }
|
capabilities: { alwaysMatch: desiredCapabilities }
|
||||||
}),
|
}),
|
||||||
timeout: progress.timeUntilDeadline(),
|
timeout: progress.timeUntilDeadline(),
|
||||||
}, async response => {
|
}, seleniumErrorHandler);
|
||||||
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}`);
|
|
||||||
});
|
|
||||||
const value = JSON.parse(response).value;
|
const value = JSON.parse(response).value;
|
||||||
const sessionId = value.sessionId;
|
const sessionId = value.sessionId;
|
||||||
progress.log(`<connected to selenium> sessionId=${sessionId}`);
|
progress.log(`<selenium> connected to sessionId=${sessionId}`);
|
||||||
|
|
||||||
const disconnectFromSelenium = async () => {
|
const disconnectFromSelenium = async () => {
|
||||||
progress.log(`<disconnecting from selenium> sessionId=${sessionId}`);
|
progress.log(`<selenium> disconnecting from sessionId=${sessionId}`);
|
||||||
await fetchData({
|
await fetchData({
|
||||||
url: hubUrl + 'session/' + sessionId,
|
url: hubUrl + 'session/' + sessionId,
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
}).catch(error => progress.log(`<error disconnecting from selenium>: ${error}`));
|
}).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.delete(disconnectFromSelenium);
|
||||||
};
|
};
|
||||||
gracefullyCloseSet.add(disconnectFromSelenium);
|
gracefullyCloseSet.add(disconnectFromSelenium);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const capabilities = value.capabilities;
|
const capabilities = value.capabilities;
|
||||||
const maybeChromeOptions = capabilities['goog:chromeOptions'];
|
let endpointURL: URL;
|
||||||
const chromeOptions = maybeChromeOptions && typeof maybeChromeOptions === 'object' ? maybeChromeOptions : undefined;
|
|
||||||
const debuggerAddress = chromeOptions && typeof chromeOptions.debuggerAddress === 'string' ? chromeOptions.debuggerAddress : undefined;
|
if (capabilities['se:cdp']) {
|
||||||
const chromeOptionsURL = typeof maybeChromeOptions === 'string' ? maybeChromeOptions : undefined;
|
// Selenium 4 - use built-in CDP websocket proxy.
|
||||||
let endpointURL = capabilities['se:cdp'] || debuggerAddress || chromeOptionsURL;
|
const endpointURLString = addProtocol(capabilities['se:cdp']);
|
||||||
if (!['ws://', 'wss://', 'http://', 'https://'].some(protocol => endpointURL.startsWith(protocol)))
|
endpointURL = new URL(endpointURLString);
|
||||||
endpointURL = 'http://' + endpointURL;
|
endpointURL.hostname = new URL(hubUrl).hostname;
|
||||||
return this._connectOverCDPInternal(progress, endpointURL, { slowMo: options.slowMo }, disconnectFromSelenium);
|
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) {
|
} catch (e) {
|
||||||
await disconnectFromSelenium();
|
await disconnectFromSelenium();
|
||||||
throw e;
|
throw e;
|
||||||
@ -296,9 +314,10 @@ const DEFAULT_ARGS = [
|
|||||||
'--no-service-autorun',
|
'--no-service-autorun',
|
||||||
];
|
];
|
||||||
|
|
||||||
async function urlToWSEndpoint(endpointURL: string) {
|
async function urlToWSEndpoint(progress: Progress, endpointURL: string) {
|
||||||
if (endpointURL.startsWith('ws'))
|
if (endpointURL.startsWith('ws'))
|
||||||
return endpointURL;
|
return endpointURL;
|
||||||
|
progress.log(`<ws preparing> retrieving websocket url from ${endpointURL}`);
|
||||||
const httpURL = endpointURL.endsWith('/') ? `${endpointURL}json/version/` : `${endpointURL}/json/version/`;
|
const httpURL = endpointURL.endsWith('/') ? `${endpointURL}json/version/` : `${endpointURL}/json/version/`;
|
||||||
const request = endpointURL.startsWith('https') ? https : http;
|
const request = endpointURL.startsWith('https') ? https : http;
|
||||||
const json = await new Promise<string>((resolve, reject) => {
|
const json = await new Promise<string>((resolve, reject) => {
|
||||||
@ -314,3 +333,20 @@ async function urlToWSEndpoint(endpointURL: string) {
|
|||||||
});
|
});
|
||||||
return JSON.parse(json).webSocketDebuggerUrl;
|
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)));
|
export const existsAsync = (path: string): Promise<boolean> => new Promise(resolve => fs.stat(path, err => resolve(!err)));
|
||||||
|
|
||||||
type HTTPRequestParams = {
|
export type HTTPRequestParams = {
|
||||||
url: string,
|
url: string,
|
||||||
method?: string,
|
method?: string,
|
||||||
headers?: http.OutgoingHttpHeaders,
|
headers?: http.OutgoingHttpHeaders,
|
||||||
@ -97,11 +97,11 @@ function httpRequest(params: HTTPRequestParams, onResponse: (r: http.IncomingMes
|
|||||||
request.end(params.data);
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
httpRequest(params, async response => {
|
httpRequest(params, async response => {
|
||||||
if (response.statusCode !== 200) {
|
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);
|
reject(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -22,13 +22,13 @@ import { start } from '../packages/playwright-core/lib/outofprocess';
|
|||||||
|
|
||||||
const chromeDriver = require('chromedriver').path;
|
const chromeDriver = require('chromedriver').path;
|
||||||
const brokenDriver = path.join(__dirname, 'assets', 'selenium-grid', 'broken-selenium-driver.js');
|
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 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');
|
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) {
|
function writeSeleniumConfig(testInfo: TestInfo, port: number) {
|
||||||
const content = fs.readFileSync(seleniumConfigStandalone, 'utf8').replace(/4444/g, String(port));
|
const template = path.join(__dirname, 'assets', 'selenium-grid', `selenium-config-standalone.json`);
|
||||||
const file = testInfo.outputPath('selenium-config.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');
|
fs.writeFileSync(file, content, 'utf8');
|
||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
@ -60,6 +60,39 @@ test('selenium grid 3.141.59 standalone chromium', async ({ browserName, childPr
|
|||||||
await grid.waitForOutput('Removing session');
|
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('selenium grid 4.0.0-rc-1 standalone chromium', async ({ browserName, childProcess, waitForPort, browserType }, testInfo) => {
|
||||||
test.skip(browserName !== 'chromium');
|
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');
|
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('selenium grid 4.0.0-rc-1 standalone chromium broken driver', async ({ browserName, childProcess, waitForPort, browserType }, testInfo) => {
|
||||||
test.skip(browserName !== 'chromium');
|
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 __testHookSeleniumRemoteURL = `http://localhost:${port}/wd/hub`;
|
||||||
const error = await browserType.launch({ __testHookSeleniumRemoteURL } as any).catch(e => e);
|
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');
|
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');
|
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');
|
test.skip(browserName !== 'chromium');
|
||||||
|
|
||||||
const port = testInfo.workerIndex + 15123;
|
const port = testInfo.workerIndex + 15123;
|
||||||
|
Loading…
Reference in New Issue
Block a user