chore: support bidi connection to chromium (#32474)
This commit is contained in:
parent
1989589edd
commit
752b171a13
43
package-lock.json
generated
43
package-lock.json
generated
|
|
@ -38,6 +38,7 @@
|
|||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"@zip.js/zip.js": "^2.7.29",
|
||||
"chokidar": "^3.5.3",
|
||||
"chromium-bidi": "^0.6.4",
|
||||
"colors": "^1.4.0",
|
||||
"concurrently": "^6.2.1",
|
||||
"cross-env": "^7.0.3",
|
||||
|
|
@ -2828,6 +2829,20 @@
|
|||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/chromium-bidi": {
|
||||
"version": "0.6.5",
|
||||
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.5.tgz",
|
||||
"integrity": "sha512-RuLrmzYrxSb0s9SgpB+QN5jJucPduZQ/9SIe76MDxYJuecPW5mxMdacJ1f4EtgiV+R0p3sCkznTMvH0MPGFqjA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mitt": "3.0.1",
|
||||
"urlpattern-polyfill": "10.0.0",
|
||||
"zod": "3.23.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"devtools-protocol": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
|
||||
|
|
@ -3258,6 +3273,13 @@
|
|||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/devtools-protocol": {
|
||||
"version": "0.0.1349977",
|
||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1349977.tgz",
|
||||
"integrity": "sha512-5JcwlDKinshGSm+4AVLFCkokJUAKTgjmiorNmrGgYYKix1h8Ts9/fplQeK1xg/rACYw1JlEM2PwIEvny5QswKQ==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/dezalgo": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
|
||||
|
|
@ -5454,6 +5476,12 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/mitt": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||
|
|
@ -7120,6 +7148,12 @@
|
|||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/urlpattern-polyfill": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz",
|
||||
"integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/util-extend": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz",
|
||||
|
|
@ -7905,6 +7939,15 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.23.8",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
|
||||
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"packages/html-reporter": {
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@
|
|||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"@zip.js/zip.js": "^2.7.29",
|
||||
"chokidar": "^3.5.3",
|
||||
"chromium-bidi": "^0.6.4",
|
||||
"colors": "^1.4.0",
|
||||
"concurrently": "^6.2.1",
|
||||
"cross-env": "^7.0.3",
|
||||
|
|
|
|||
|
|
@ -3,3 +3,9 @@
|
|||
../
|
||||
../isomorphic/
|
||||
./third_party/
|
||||
|
||||
[bidiOverCdp.ts]
|
||||
***
|
||||
|
||||
[bidiBrowserType.ts]
|
||||
../chromium/chromiumSwitches.ts
|
||||
|
|
|
|||
|
|
@ -43,9 +43,6 @@ export class BidiBrowser extends Browser {
|
|||
const browser = new BidiBrowser(parent, transport, options);
|
||||
if ((options as any).__testHookOnConnectToBrowser)
|
||||
await (options as any).__testHookOnConnectToBrowser();
|
||||
const sessionStatus = await browser._browserSession.send('session.status', {});
|
||||
if (!sessionStatus.ready)
|
||||
throw new Error('Bidi session is not ready. ' + sessionStatus.message);
|
||||
|
||||
let proxy: bidi.Session.ManualProxyConfiguration | undefined;
|
||||
if (options.proxy) {
|
||||
|
|
|
|||
206
packages/playwright-core/src/server/bidi/bidiBrowserType.ts
Normal file
206
packages/playwright-core/src/server/bidi/bidiBrowserType.ts
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the 'License');
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an 'AS IS' BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { assert, wrapInASCIIBox } from '../../utils';
|
||||
import type { Env } from '../../utils/processLauncher';
|
||||
import type { BrowserOptions } from '../browser';
|
||||
import { BrowserReadyState } from '../browserType';
|
||||
import { BrowserType, kNoXServerRunningError } from '../browserType';
|
||||
import type { SdkObject } from '../instrumentation';
|
||||
import type { ProtocolError } from '../protocolError';
|
||||
import type { ConnectionTransport } from '../transport';
|
||||
import type * as types from '../types';
|
||||
import { BidiBrowser } from './bidiBrowser';
|
||||
import { kBrowserCloseMessageId } from './bidiConnection';
|
||||
import { chromiumSwitches } from '../chromium/chromiumSwitches';
|
||||
|
||||
export class BidiBrowserType extends BrowserType {
|
||||
constructor(parent: SdkObject) {
|
||||
super(parent, 'bidi');
|
||||
this._useBidi = true;
|
||||
}
|
||||
|
||||
override async connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<BidiBrowser> {
|
||||
if (options.channel?.includes('chrome')) {
|
||||
// Chrome doesn't support Bidi, we create Bidi over CDP which is used by Chrome driver.
|
||||
// bidiOverCdp depends on chromium-bidi which we only have in devDependencies, so
|
||||
// we load bidiOverCdp dynamically.
|
||||
const bidiTransport = await require('./bidiOverCdp').connectBidiOverCdp(transport);
|
||||
(transport as any)[kBidiOverCdpWrapper] = bidiTransport;
|
||||
transport = bidiTransport;
|
||||
}
|
||||
return BidiBrowser.connect(this.attribution.playwright, transport, options);
|
||||
}
|
||||
|
||||
override doRewriteStartupLog(error: ProtocolError): ProtocolError {
|
||||
if (!error.logs)
|
||||
return error;
|
||||
// https://github.com/microsoft/playwright/issues/6500
|
||||
if (error.logs.includes(`as root in a regular user's session is not supported.`))
|
||||
error.logs = '\n' + wrapInASCIIBox(`Firefox is unable to launch if the $HOME folder isn't owned by the current user.\nWorkaround: Set the HOME=/root environment variable${process.env.GITHUB_ACTION ? ' in your GitHub Actions workflow file' : ''} when running Playwright.`, 1);
|
||||
if (error.logs.includes('no DISPLAY environment variable specified'))
|
||||
error.logs = '\n' + wrapInASCIIBox(kNoXServerRunningError, 1);
|
||||
return error;
|
||||
}
|
||||
|
||||
override amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env {
|
||||
if (!path.isAbsolute(os.homedir()))
|
||||
throw new Error(`Cannot launch Firefox with relative home directory. Did you set ${os.platform() === 'win32' ? 'USERPROFILE' : 'HOME'} to a relative path?`);
|
||||
if (os.platform() === 'linux') {
|
||||
// Always remove SNAP_NAME and SNAP_INSTANCE_NAME env variables since they
|
||||
// confuse Firefox: in our case, builds never come from SNAP.
|
||||
// See https://github.com/microsoft/playwright/issues/20555
|
||||
return { ...env, SNAP_NAME: undefined, SNAP_INSTANCE_NAME: undefined };
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
override attemptToGracefullyCloseBrowser(transport: ConnectionTransport): void {
|
||||
const bidiTransport = (transport as any)[kBidiOverCdpWrapper];
|
||||
if (bidiTransport)
|
||||
transport = bidiTransport;
|
||||
transport.send({ method: 'browser.close', params: {}, id: kBrowserCloseMessageId });
|
||||
}
|
||||
|
||||
override defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[] {
|
||||
if (options.channel === 'bidi-firefox-stable')
|
||||
return this._defaultFirefoxArgs(options, isPersistent, userDataDir);
|
||||
else if (options.channel === 'bidi-chrome-canary')
|
||||
return this._defaultChromiumArgs(options, isPersistent, userDataDir);
|
||||
throw new Error(`Unknown Bidi channel "${options.channel}"`);
|
||||
}
|
||||
|
||||
override readyState(options: types.LaunchOptions): BrowserReadyState | undefined {
|
||||
assert(options.useWebSocket);
|
||||
if (options.channel?.includes('firefox'))
|
||||
return new FirefoxReadyState();
|
||||
if (options.channel?.includes('chrome'))
|
||||
return new ChromiumReadyState();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _defaultFirefoxArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[] {
|
||||
const { args = [], headless } = options;
|
||||
const userDataDirArg = args.find(arg => arg.startsWith('-profile') || arg.startsWith('--profile'));
|
||||
if (userDataDirArg)
|
||||
throw this._createUserDataDirArgMisuseError('--profile');
|
||||
const firefoxArguments = ['--remote-debugging-port=0'];
|
||||
if (headless)
|
||||
firefoxArguments.push('--headless');
|
||||
else
|
||||
firefoxArguments.push('--foreground');
|
||||
firefoxArguments.push(`--profile`, userDataDir);
|
||||
firefoxArguments.push(...args);
|
||||
// TODO: make ephemeral context work without this argument.
|
||||
firefoxArguments.push('about:blank');
|
||||
// if (isPersistent)
|
||||
// firefoxArguments.push('about:blank');
|
||||
// else
|
||||
// firefoxArguments.push('-silent');
|
||||
return firefoxArguments;
|
||||
}
|
||||
|
||||
private _defaultChromiumArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[] {
|
||||
const chromeArguments = this._innerDefaultArgs(options);
|
||||
chromeArguments.push(`--user-data-dir=${userDataDir}`);
|
||||
chromeArguments.push('--remote-debugging-port=0');
|
||||
if (isPersistent)
|
||||
chromeArguments.push('about:blank');
|
||||
else
|
||||
chromeArguments.push('--no-startup-window');
|
||||
return chromeArguments;
|
||||
}
|
||||
|
||||
private _innerDefaultArgs(options: types.LaunchOptions): string[] {
|
||||
const { args = [], proxy } = options;
|
||||
const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir'));
|
||||
if (userDataDirArg)
|
||||
throw this._createUserDataDirArgMisuseError('--user-data-dir');
|
||||
if (args.find(arg => arg.startsWith('--remote-debugging-pipe')))
|
||||
throw new Error('Playwright manages remote debugging connection itself.');
|
||||
if (args.find(arg => !arg.startsWith('-')))
|
||||
throw new Error('Arguments can not specify page to be opened');
|
||||
const chromeArguments = [...chromiumSwitches];
|
||||
|
||||
if (os.platform() === 'darwin') {
|
||||
// See https://github.com/microsoft/playwright/issues/7362
|
||||
chromeArguments.push('--enable-use-zoom-for-dsf=false');
|
||||
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1407025.
|
||||
if (options.headless)
|
||||
chromeArguments.push('--use-angle');
|
||||
}
|
||||
|
||||
if (options.devtools)
|
||||
chromeArguments.push('--auto-open-devtools-for-tabs');
|
||||
if (options.headless) {
|
||||
if (process.env.PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW)
|
||||
chromeArguments.push('--headless=new');
|
||||
else
|
||||
chromeArguments.push('--headless=old');
|
||||
|
||||
chromeArguments.push(
|
||||
'--hide-scrollbars',
|
||||
'--mute-audio',
|
||||
'--blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4',
|
||||
);
|
||||
}
|
||||
if (options.chromiumSandbox !== true)
|
||||
chromeArguments.push('--no-sandbox');
|
||||
if (proxy) {
|
||||
const proxyURL = new URL(proxy.server);
|
||||
const isSocks = proxyURL.protocol === 'socks5:';
|
||||
// https://www.chromium.org/developers/design-documents/network-settings
|
||||
if (isSocks && !this.attribution.playwright.options.socksProxyPort) {
|
||||
// https://www.chromium.org/developers/design-documents/network-stack/socks-proxy
|
||||
chromeArguments.push(`--host-resolver-rules="MAP * ~NOTFOUND , EXCLUDE ${proxyURL.hostname}"`);
|
||||
}
|
||||
chromeArguments.push(`--proxy-server=${proxy.server}`);
|
||||
const proxyBypassRules = [];
|
||||
// https://source.chromium.org/chromium/chromium/src/+/master:net/docs/proxy.md;l=548;drc=71698e610121078e0d1a811054dcf9fd89b49578
|
||||
if (this.attribution.playwright.options.socksProxyPort)
|
||||
proxyBypassRules.push('<-loopback>');
|
||||
if (proxy.bypass)
|
||||
proxyBypassRules.push(...proxy.bypass.split(',').map(t => t.trim()).map(t => t.startsWith('.') ? '*' + t : t));
|
||||
if (!process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK && !proxyBypassRules.includes('<-loopback>'))
|
||||
proxyBypassRules.push('<-loopback>');
|
||||
if (proxyBypassRules.length > 0)
|
||||
chromeArguments.push(`--proxy-bypass-list=${proxyBypassRules.join(';')}`);
|
||||
}
|
||||
chromeArguments.push(...args);
|
||||
return chromeArguments;
|
||||
}
|
||||
}
|
||||
|
||||
class FirefoxReadyState extends BrowserReadyState {
|
||||
override onBrowserOutput(message: string): void {
|
||||
// Bidi WebSocket in Firefox.
|
||||
const match = message.match(/WebDriver BiDi listening on (ws:\/\/.*)$/);
|
||||
if (match)
|
||||
this._wsEndpoint.resolve(match[1] + '/session');
|
||||
}
|
||||
}
|
||||
|
||||
class ChromiumReadyState extends BrowserReadyState {
|
||||
override onBrowserOutput(message: string): void {
|
||||
const match = message.match(/DevTools listening on (.*)/);
|
||||
if (match)
|
||||
this._wsEndpoint.resolve(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
const kBidiOverCdpWrapper = Symbol('kBidiConnectionWrapper');
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the 'License');
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an 'AS IS' BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { assert, ManualPromise, wrapInASCIIBox } from '../../utils';
|
||||
import type { Env } from '../../utils/processLauncher';
|
||||
import type { BrowserOptions } from '../browser';
|
||||
import type { BrowserReadyState } from '../browserType';
|
||||
import { BrowserType, kNoXServerRunningError } from '../browserType';
|
||||
import type { SdkObject } from '../instrumentation';
|
||||
import type { ProtocolError } from '../protocolError';
|
||||
import type { ConnectionTransport } from '../transport';
|
||||
import type * as types from '../types';
|
||||
import { BidiBrowser } from './bidiBrowser';
|
||||
import { kBrowserCloseMessageId } from './bidiConnection';
|
||||
|
||||
export class BidiFirefox extends BrowserType {
|
||||
constructor(parent: SdkObject) {
|
||||
super(parent, 'bidi');
|
||||
this._useBidi = true;
|
||||
}
|
||||
|
||||
override async connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<BidiBrowser> {
|
||||
return BidiBrowser.connect(this.attribution.playwright, transport, options);
|
||||
}
|
||||
|
||||
override doRewriteStartupLog(error: ProtocolError): ProtocolError {
|
||||
if (!error.logs)
|
||||
return error;
|
||||
// https://github.com/microsoft/playwright/issues/6500
|
||||
if (error.logs.includes(`as root in a regular user's session is not supported.`))
|
||||
error.logs = '\n' + wrapInASCIIBox(`Firefox is unable to launch if the $HOME folder isn't owned by the current user.\nWorkaround: Set the HOME=/root environment variable${process.env.GITHUB_ACTION ? ' in your GitHub Actions workflow file' : ''} when running Playwright.`, 1);
|
||||
if (error.logs.includes('no DISPLAY environment variable specified'))
|
||||
error.logs = '\n' + wrapInASCIIBox(kNoXServerRunningError, 1);
|
||||
return error;
|
||||
}
|
||||
|
||||
override amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env {
|
||||
if (!path.isAbsolute(os.homedir()))
|
||||
throw new Error(`Cannot launch Firefox with relative home directory. Did you set ${os.platform() === 'win32' ? 'USERPROFILE' : 'HOME'} to a relative path?`);
|
||||
if (os.platform() === 'linux') {
|
||||
// Always remove SNAP_NAME and SNAP_INSTANCE_NAME env variables since they
|
||||
// confuse Firefox: in our case, builds never come from SNAP.
|
||||
// See https://github.com/microsoft/playwright/issues/20555
|
||||
return { ...env, SNAP_NAME: undefined, SNAP_INSTANCE_NAME: undefined };
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
override attemptToGracefullyCloseBrowser(transport: ConnectionTransport): void {
|
||||
transport.send({ method: 'browser.close', params: {}, id: kBrowserCloseMessageId });
|
||||
}
|
||||
|
||||
override defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[] {
|
||||
const { args = [], headless } = options;
|
||||
const userDataDirArg = args.find(arg => arg.startsWith('-profile') || arg.startsWith('--profile'));
|
||||
if (userDataDirArg)
|
||||
throw this._createUserDataDirArgMisuseError('--profile');
|
||||
const firefoxArguments = ['--remote-debugging-port=0'];
|
||||
if (headless)
|
||||
firefoxArguments.push('--headless');
|
||||
else
|
||||
firefoxArguments.push('--foreground');
|
||||
firefoxArguments.push(`--profile`, userDataDir);
|
||||
firefoxArguments.push(...args);
|
||||
// TODO: make ephemeral context work without this argument.
|
||||
firefoxArguments.push('about:blank');
|
||||
// if (isPersistent)
|
||||
// firefoxArguments.push('about:blank');
|
||||
// else
|
||||
// firefoxArguments.push('-silent');
|
||||
return firefoxArguments;
|
||||
}
|
||||
|
||||
override readyState(options: types.LaunchOptions): BrowserReadyState | undefined {
|
||||
assert(options.useWebSocket);
|
||||
return new BidiReadyState();
|
||||
}
|
||||
}
|
||||
|
||||
class BidiReadyState implements BrowserReadyState {
|
||||
private readonly _wsEndpoint = new ManualPromise<string|undefined>();
|
||||
|
||||
onBrowserOutput(message: string): void {
|
||||
// Bidi WebSocket in Firefox.
|
||||
const match = message.match(/WebDriver BiDi listening on (ws:\/\/.*)$/);
|
||||
if (match)
|
||||
this._wsEndpoint.resolve(match[1] + '/session');
|
||||
}
|
||||
onBrowserExit(): void {
|
||||
// Unblock launch when browser prematurely exits.
|
||||
this._wsEndpoint.resolve(undefined);
|
||||
}
|
||||
async waitUntilReady(): Promise<{ wsEndpoint?: string }> {
|
||||
const wsEndpoint = await this._wsEndpoint;
|
||||
return { wsEndpoint };
|
||||
}
|
||||
}
|
||||
100
packages/playwright-core/src/server/bidi/bidiOverCdp.ts
Normal file
100
packages/playwright-core/src/server/bidi/bidiOverCdp.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the 'License');
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an 'AS IS' BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as bidiMapper from 'chromium-bidi/lib/cjs/bidiMapper/BidiMapper';
|
||||
import * as bidiCdpConnection from 'chromium-bidi/lib/cjs/cdp/CdpConnection';
|
||||
import type * as bidiTransport from 'chromium-bidi/lib/cjs/utils/transport';
|
||||
import type { ChromiumBidi } from 'chromium-bidi/lib/cjs/protocol/protocol';
|
||||
import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
|
||||
import { debugLogger } from '../../utils/debugLogger';
|
||||
|
||||
const bidiServerLogger = (prefix: string, ...args: unknown[]): void => {
|
||||
debugLogger.log(prefix as any, args);
|
||||
};
|
||||
|
||||
export async function connectBidiOverCdp(cdp: ConnectionTransport): Promise<ConnectionTransport> {
|
||||
let server: bidiMapper.BidiServer | undefined = undefined;
|
||||
const bidiTransport = new BidiTransportImpl();
|
||||
const bidiConnection = new BidiConnection(bidiTransport, () => server?.close());
|
||||
const cdpTransportImpl = new CdpTransportImpl(cdp);
|
||||
const cdpConnection = new bidiCdpConnection.MapperCdpConnection(cdpTransportImpl, bidiServerLogger);
|
||||
// Make sure onclose event is propagated.
|
||||
cdp.onclose = () => bidiConnection.onclose?.();
|
||||
server = await bidiMapper.BidiServer.createAndStart(
|
||||
bidiTransport,
|
||||
cdpConnection,
|
||||
await cdpConnection.createBrowserSession(),
|
||||
/* selfTargetId= */ '',
|
||||
undefined,
|
||||
bidiServerLogger);
|
||||
return bidiConnection;
|
||||
}
|
||||
|
||||
class BidiTransportImpl implements bidiMapper.BidiTransport {
|
||||
_handler?: (message: ChromiumBidi.Command) => Promise<void> | void;
|
||||
_bidiConnection!: BidiConnection;
|
||||
|
||||
setOnMessage(handler: (message: ChromiumBidi.Command) => Promise<void> | void) {
|
||||
this._handler = handler;
|
||||
}
|
||||
sendMessage(message: ChromiumBidi.Message): Promise<void> | void {
|
||||
return this._bidiConnection.onmessage?.(message as any);
|
||||
}
|
||||
close() {
|
||||
this._bidiConnection.onclose?.();
|
||||
}
|
||||
}
|
||||
|
||||
class BidiConnection implements ConnectionTransport {
|
||||
private _bidiTransport: BidiTransportImpl;
|
||||
private _closeCallback: () => void;
|
||||
|
||||
constructor(bidiTransport: BidiTransportImpl, closeCallback: () => void) {
|
||||
this._bidiTransport = bidiTransport;
|
||||
this._bidiTransport._bidiConnection = this;
|
||||
this._closeCallback = closeCallback;
|
||||
}
|
||||
send(s: ProtocolRequest): void {
|
||||
this._bidiTransport._handler?.(s as any);
|
||||
}
|
||||
close(): void {
|
||||
this._closeCallback();
|
||||
}
|
||||
onmessage?: ((message: ProtocolResponse) => void) | undefined;
|
||||
onclose?: ((reason?: string) => void) | undefined;
|
||||
}
|
||||
|
||||
class CdpTransportImpl implements bidiTransport.Transport {
|
||||
private _connection: ConnectionTransport;
|
||||
private _handler?: (message: string) => Promise<void> | void;
|
||||
_bidiConnection!: BidiConnection;
|
||||
|
||||
constructor(connection: ConnectionTransport) {
|
||||
this._connection = connection;
|
||||
this._connection.onmessage = message => {
|
||||
this._handler?.(JSON.stringify(message));
|
||||
};
|
||||
}
|
||||
setOnMessage(handler: (message: string) => Promise<void> | void) {
|
||||
this._handler = handler;
|
||||
}
|
||||
sendMessage(message: string): Promise<void> | void {
|
||||
return this._connection.send(JSON.parse(message));
|
||||
}
|
||||
close(): void {
|
||||
this._connection.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -86,8 +86,8 @@ export class BidiPage implements PageDelegate {
|
|||
}
|
||||
|
||||
private async _initialize() {
|
||||
const { contexts } = await this._session.send('browsingContext.getTree', { root: this._session.sessionId });
|
||||
this._handleFrameTree(contexts[0]);
|
||||
// Initialize main frame.
|
||||
this._onFrameAttached(this._session.sessionId, null);
|
||||
await Promise.all([
|
||||
this.updateHttpCredentials(),
|
||||
this.updateRequestInterception(),
|
||||
|
|
@ -95,15 +95,6 @@ export class BidiPage implements PageDelegate {
|
|||
]);
|
||||
}
|
||||
|
||||
private _handleFrameTree(frameTree: bidi.BrowsingContext.Info) {
|
||||
this._onFrameAttached(frameTree.context, frameTree.parent || null);
|
||||
if (!frameTree.children)
|
||||
return;
|
||||
|
||||
for (const child of frameTree.children)
|
||||
this._handleFrameTree(child);
|
||||
}
|
||||
|
||||
potentiallyUninitializedPage(): Page {
|
||||
return this._page;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import { ProgressController } from './progress';
|
|||
import type * as types from './types';
|
||||
import type * as channels from '@protocol/channels';
|
||||
import { DEFAULT_TIMEOUT, TimeoutSettings } from '../common/timeoutSettings';
|
||||
import { debugMode } from '../utils';
|
||||
import { debugMode, ManualPromise } from '../utils';
|
||||
import { existsAsync } from '../utils/fileUtils';
|
||||
import { helper } from './helper';
|
||||
import { RecentLogsCollector } from '../utils/debugLogger';
|
||||
|
|
@ -44,10 +44,19 @@ export const kNoXServerRunningError = 'Looks like you launched a headed browser
|
|||
'Set either \'headless: true\' or use \'xvfb-run <your-playwright-app>\' before running Playwright.\n\n<3 Playwright Team';
|
||||
|
||||
|
||||
export interface BrowserReadyState {
|
||||
onBrowserOutput(message: string): void;
|
||||
onBrowserExit(): void;
|
||||
waitUntilReady(): Promise<{ wsEndpoint?: string }>;
|
||||
export abstract class BrowserReadyState {
|
||||
protected readonly _wsEndpoint = new ManualPromise<string|undefined>();
|
||||
|
||||
onBrowserExit(): void {
|
||||
// Unblock launch when browser prematurely exits.
|
||||
this._wsEndpoint.resolve(undefined);
|
||||
}
|
||||
async waitUntilReady(): Promise<{ wsEndpoint?: string }> {
|
||||
const wsEndpoint = await this._wsEndpoint;
|
||||
return { wsEndpoint };
|
||||
}
|
||||
|
||||
abstract onBrowserOutput(message: string): void;
|
||||
}
|
||||
|
||||
export abstract class BrowserType extends SdkObject {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import type { Env } from '../../utils/processLauncher';
|
|||
import { gracefullyCloseSet } from '../../utils/processLauncher';
|
||||
import { kBrowserCloseMessageId } from './crConnection';
|
||||
import { BrowserType, kNoXServerRunningError } from '../browserType';
|
||||
import type { BrowserReadyState } from '../browserType';
|
||||
import { BrowserReadyState } from '../browserType';
|
||||
import type { ConnectionTransport, ProtocolRequest } from '../transport';
|
||||
import { WebSocketTransport } from '../transport';
|
||||
import { CRDevTools } from './crDevTools';
|
||||
|
|
@ -352,21 +352,12 @@ export class Chromium extends BrowserType {
|
|||
}
|
||||
}
|
||||
|
||||
class ChromiumReadyState implements BrowserReadyState {
|
||||
private readonly _wsEndpoint = new ManualPromise<string|undefined>();
|
||||
|
||||
onBrowserOutput(message: string): void {
|
||||
class ChromiumReadyState extends BrowserReadyState {
|
||||
override onBrowserOutput(message: string): void {
|
||||
const match = message.match(/DevTools listening on (.*)/);
|
||||
if (match)
|
||||
this._wsEndpoint.resolve(match[1]);
|
||||
}
|
||||
onBrowserExit(): void {
|
||||
this._wsEndpoint.resolve(undefined);
|
||||
}
|
||||
async waitUntilReady(): Promise<{ wsEndpoint?: string }> {
|
||||
const wsEndpoint = await this._wsEndpoint;
|
||||
return { wsEndpoint };
|
||||
}
|
||||
}
|
||||
|
||||
async function urlToWSEndpoint(progress: Progress, endpointURL: string, headers: { [key: string]: string; }) {
|
||||
|
|
|
|||
|
|
@ -20,12 +20,12 @@ import path from 'path';
|
|||
import { FFBrowser } from './ffBrowser';
|
||||
import { kBrowserCloseMessageId } from './ffConnection';
|
||||
import { BrowserType, kNoXServerRunningError } from '../browserType';
|
||||
import type { BrowserReadyState } from '../browserType';
|
||||
import { BrowserReadyState } from '../browserType';
|
||||
import type { Env } from '../../utils/processLauncher';
|
||||
import type { ConnectionTransport } from '../transport';
|
||||
import type { BrowserOptions } from '../browser';
|
||||
import type * as types from '../types';
|
||||
import { ManualPromise, wrapInASCIIBox } from '../../utils';
|
||||
import { wrapInASCIIBox } from '../../utils';
|
||||
import type { SdkObject } from '../instrumentation';
|
||||
import type { ProtocolError } from '../protocolError';
|
||||
|
||||
|
|
@ -95,20 +95,10 @@ export class Firefox extends BrowserType {
|
|||
}
|
||||
}
|
||||
|
||||
class JugglerReadyState implements BrowserReadyState {
|
||||
private readonly _jugglerPromise = new ManualPromise<void>();
|
||||
|
||||
onBrowserOutput(message: string): void {
|
||||
class JugglerReadyState extends BrowserReadyState {
|
||||
override onBrowserOutput(message: string): void {
|
||||
if (message.includes('Juggler listening to the pipe'))
|
||||
this._jugglerPromise.resolve();
|
||||
}
|
||||
onBrowserExit(): void {
|
||||
// Unblock launch when browser prematurely exits.
|
||||
this._jugglerPromise.resolve();
|
||||
}
|
||||
async waitUntilReady(): Promise<{ wsEndpoint?: string }> {
|
||||
await this._jugglerPromise;
|
||||
return { };
|
||||
this._wsEndpoint.resolve(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import { debugLogger, type Language } from '../utils';
|
|||
import type { Page } from './page';
|
||||
import { DebugController } from './debugController';
|
||||
import type { BrowserType } from './browserType';
|
||||
import { BidiFirefox } from './bidi/bidiFirefox';
|
||||
import { BidiBrowserType } from './bidi/bidiBrowserType';
|
||||
|
||||
type PlaywrightOptions = {
|
||||
socksProxyPort?: number;
|
||||
|
|
@ -64,7 +64,7 @@ export class Playwright extends SdkObject {
|
|||
}
|
||||
}, null);
|
||||
this.chromium = new Chromium(this);
|
||||
this.bidi = new BidiFirefox(this);
|
||||
this.bidi = new BidiBrowserType(this);
|
||||
this.firefox = new Firefox(this);
|
||||
this.webkit = new WebKit(this);
|
||||
this.electron = new Electron(this);
|
||||
|
|
|
|||
|
|
@ -354,7 +354,7 @@ function readDescriptors(browsersJSON: BrowsersJSON) {
|
|||
|
||||
export type BrowserName = 'chromium' | 'firefox' | 'webkit' | 'bidi';
|
||||
type InternalTool = 'ffmpeg' | 'firefox-beta' | 'chromium-tip-of-tree' | 'android';
|
||||
type BidiChannel = 'bidi-firefox-stable';
|
||||
type BidiChannel = 'bidi-firefox-stable' | 'bidi-chrome-canary';
|
||||
type ChromiumChannel = 'chrome' | 'chrome-beta' | 'chrome-dev' | 'chrome-canary' | 'msedge' | 'msedge-beta' | 'msedge-dev' | 'msedge-canary';
|
||||
const allDownloadable = ['chromium', 'firefox', 'webkit', 'ffmpeg', 'firefox-beta', 'chromium-tip-of-tree'];
|
||||
|
||||
|
|
@ -530,6 +530,11 @@ export class Registry {
|
|||
'darwin': '/Applications/Firefox.app/Contents/MacOS/firefox',
|
||||
'win32': '\\Mozilla Firefox\\firefox.exe',
|
||||
}));
|
||||
this._executables.push(this._createBidiChannel('bidi-chrome-canary', {
|
||||
'linux': '',
|
||||
'darwin': '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
|
||||
'win32': `\\Google\\Chrome SxS\\Application\\chrome.exe`,
|
||||
}));
|
||||
|
||||
const firefox = descriptors.find(d => d.name === 'firefox')!;
|
||||
const firefoxExecutable = findExecutablePath(firefox.dir, 'firefox');
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ const getExecutablePath = () => {
|
|||
};
|
||||
|
||||
const headed = process.argv.includes('--headed');
|
||||
const channel = process.env.PWTEST_CHANNEL as any;
|
||||
const trace = !!process.env.PWTEST_TRACE;
|
||||
|
||||
const outputDir = path.join(__dirname, '..', '..', 'test-results');
|
||||
|
|
@ -64,32 +63,34 @@ const executablePath = getExecutablePath();
|
|||
if (executablePath && !process.env.TEST_WORKER_INDEX)
|
||||
console.error(`Using executable at ${executablePath}`);
|
||||
const testIgnore: RegExp[] = [];
|
||||
for (const folder of ['library', 'page']) {
|
||||
config.projects.push({
|
||||
name: `${browserName}-${folder}`,
|
||||
testDir: path.join(testDir, folder),
|
||||
testIgnore,
|
||||
snapshotPathTemplate: `{testDir}/{testFileDir}/{testFileName}-snapshots/{arg}-${browserName}{ext}`,
|
||||
use: {
|
||||
browserName,
|
||||
headless: !headed,
|
||||
channel,
|
||||
video: 'off',
|
||||
launchOptions: {
|
||||
channel: 'bidi-firefox-stable',
|
||||
executablePath,
|
||||
for (const channel of ['bidi-chrome-canary', 'bidi-firefox-stable']) {
|
||||
for (const folder of ['library', 'page']) {
|
||||
config.projects.push({
|
||||
name: `${channel}-${folder}`,
|
||||
testDir: path.join(testDir, folder),
|
||||
testIgnore,
|
||||
snapshotPathTemplate: `{testDir}/{testFileDir}/{testFileName}-snapshots/{arg}-${channel}{ext}`,
|
||||
use: {
|
||||
browserName,
|
||||
headless: !headed,
|
||||
channel,
|
||||
video: 'off',
|
||||
launchOptions: {
|
||||
channel: 'bidi-chrome-canary',
|
||||
executablePath,
|
||||
},
|
||||
trace: trace ? 'on' : undefined,
|
||||
},
|
||||
trace: trace ? 'on' : undefined,
|
||||
},
|
||||
metadata: {
|
||||
platform: process.platform,
|
||||
docker: !!process.env.INSIDE_DOCKER,
|
||||
headless: !headed,
|
||||
browserName,
|
||||
channel,
|
||||
trace: !!trace,
|
||||
},
|
||||
});
|
||||
metadata: {
|
||||
platform: process.platform,
|
||||
docker: !!process.env.INSIDE_DOCKER,
|
||||
headless: !headed,
|
||||
browserName,
|
||||
channel,
|
||||
trace: !!trace,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default config;
|
||||
|
|
|
|||
Loading…
Reference in a new issue