chore: support bidi connection to chromium (#32474)

This commit is contained in:
Yury Semikhatsky 2024-09-05 14:56:07 -07:00 committed by GitHub
parent 1989589edd
commit 752b171a13
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 415 additions and 187 deletions

43
package-lock.json generated
View file

@ -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": {

View file

@ -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",

View file

@ -3,3 +3,9 @@
../
../isomorphic/
./third_party/
[bidiOverCdp.ts]
***
[bidiBrowserType.ts]
../chromium/chromiumSwitches.ts

View file

@ -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) {

View 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');

View file

@ -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 };
}
}

View 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();
}
}

View file

@ -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;
}

View file

@ -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 {

View file

@ -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; }) {

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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');

View file

@ -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;