chore: support WebDriver BiDi browser instances (#8932)

This PR adds a basic support for WebDriver BiDi that currently includes only the ability to establish a connection and shutdown the browser. Therefore, the implementation is marked as internal and won't show up in the changelog as it's barely useful at the moment.

The API classes are kept as classes instead of interfaces so that clients relying on instanceof checks still work.
This commit is contained in:
Alex Rudenko 2022-09-14 16:40:58 +02:00 committed by Randolf J
parent cfaaa5e2c0
commit db28203e64
30 changed files with 1093 additions and 336 deletions

View File

@ -18,7 +18,7 @@ module.exports = {
reporter: 'dot', reporter: 'dot',
logLevel: 'debug', logLevel: 'debug',
require: ['./test/build/mocha-utils.js', 'source-map-support/register'], require: ['./test/build/mocha-utils.js', 'source-map-support/register'],
spec: 'test/build/*.spec.js', spec: 'test/build/**/*.spec.js',
exit: !!process.env.CI, exit: !!process.env.CI,
retries: process.env.CI ? 2 : 0, retries: process.env.CI ? 2 : 0,
parallel: !!process.env.PARALLEL, parallel: !!process.env.PARALLEL,

View File

@ -16,5 +16,5 @@ export interface BrowserContextOptions
| Property | Modifiers | Type | Description | | Property | Modifiers | Type | Description |
| ------------------------------------------------------------------------ | --------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | ------------------------------------------------------------------------ | --------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| [proxyBypassList?](./puppeteer.browsercontextoptions.proxybypasslist.md) | | string\[\] | <i>(Optional)</i> Bypass the proxy for the given semi-colon-separated list of hosts. | | [proxyBypassList?](./puppeteer.browsercontextoptions.proxybypasslist.md) | | string\[\] | <i>(Optional)</i> Bypass the proxy for the given list of hosts. |
| [proxyServer?](./puppeteer.browsercontextoptions.proxyserver.md) | | string | <i>(Optional)</i> Proxy server with optional port to use for all requests. Username and password can be set in <code>Page.authenticate</code>. | | [proxyServer?](./puppeteer.browsercontextoptions.proxyserver.md) | | string | <i>(Optional)</i> Proxy server with optional port to use for all requests. Username and password can be set in <code>Page.authenticate</code>. |

View File

@ -4,7 +4,7 @@ sidebar_label: BrowserContextOptions.proxyBypassList
# BrowserContextOptions.proxyBypassList property # BrowserContextOptions.proxyBypassList property
Bypass the proxy for the given semi-colon-separated list of hosts. Bypass the proxy for the given list of hosts.
**Signature:** **Signature:**

628
src/api/Browser.ts Normal file
View File

@ -0,0 +1,628 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* 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.
*/
/* eslint-disable @typescript-eslint/no-unused-vars */
import {ChildProcess} from 'child_process';
import {Protocol} from 'devtools-protocol';
import {EventEmitter} from '../common/EventEmitter.js';
import type {Page} from '../common/Page.js'; // TODO: move to ./api
import type {Target} from '../common/Target.js'; // TODO: move to ./api
/**
* BrowserContext options.
*
* @public
*/
export interface BrowserContextOptions {
/**
* Proxy server with optional port to use for all requests.
* Username and password can be set in `Page.authenticate`.
*/
proxyServer?: string;
/**
* Bypass the proxy for the given list of hosts.
*/
proxyBypassList?: string[];
}
/**
* @internal
*/
export type BrowserCloseCallback = () => Promise<void> | void;
/**
* @public
*/
export type TargetFilterCallback = (
target: Protocol.Target.TargetInfo
) => boolean;
/**
* @internal
*/
export type IsPageTargetCallback = (
target: Protocol.Target.TargetInfo
) => boolean;
/**
* @internal
*/
export const WEB_PERMISSION_TO_PROTOCOL_PERMISSION = new Map<
Permission,
Protocol.Browser.PermissionType
>([
['geolocation', 'geolocation'],
['midi', 'midi'],
['notifications', 'notifications'],
// TODO: push isn't a valid type?
// ['push', 'push'],
['camera', 'videoCapture'],
['microphone', 'audioCapture'],
['background-sync', 'backgroundSync'],
['ambient-light-sensor', 'sensors'],
['accelerometer', 'sensors'],
['gyroscope', 'sensors'],
['magnetometer', 'sensors'],
['accessibility-events', 'accessibilityEvents'],
['clipboard-read', 'clipboardReadWrite'],
['clipboard-write', 'clipboardReadWrite'],
['payment-handler', 'paymentHandler'],
['persistent-storage', 'durableStorage'],
['idle-detection', 'idleDetection'],
// chrome-specific permissions we have.
['midi-sysex', 'midiSysex'],
]);
/**
* @public
*/
export type Permission =
| 'geolocation'
| 'midi'
| 'notifications'
| 'camera'
| 'microphone'
| 'background-sync'
| 'ambient-light-sensor'
| 'accelerometer'
| 'gyroscope'
| 'magnetometer'
| 'accessibility-events'
| 'clipboard-read'
| 'clipboard-write'
| 'payment-handler'
| 'persistent-storage'
| 'idle-detection'
| 'midi-sysex';
/**
* @public
*/
export interface WaitForTargetOptions {
/**
* Maximum wait time in milliseconds. Pass `0` to disable the timeout.
* @defaultValue 30 seconds.
*/
timeout?: number;
}
/**
* All the events a {@link Browser | browser instance} may emit.
*
* @public
*/
export const enum BrowserEmittedEvents {
/**
* Emitted when Puppeteer gets disconnected from the Chromium instance. This
* might happen because of one of the following:
*
* - Chromium is closed or crashed
*
* - The {@link Browser.disconnect | browser.disconnect } method was called.
*/
Disconnected = 'disconnected',
/**
* Emitted when the url of a target changes. Contains a {@link Target} instance.
*
* @remarks
*
* Note that this includes target changes in incognito browser contexts.
*/
TargetChanged = 'targetchanged',
/**
* Emitted when a target is created, for example when a new page is opened by
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/open | window.open}
* or by {@link Browser.newPage | browser.newPage}
*
* Contains a {@link Target} instance.
*
* @remarks
*
* Note that this includes target creations in incognito browser contexts.
*/
TargetCreated = 'targetcreated',
/**
* Emitted when a target is destroyed, for example when a page is closed.
* Contains a {@link Target} instance.
*
* @remarks
*
* Note that this includes target destructions in incognito browser contexts.
*/
TargetDestroyed = 'targetdestroyed',
}
/**
* A Browser is created when Puppeteer connects to a Chromium instance, either through
* {@link PuppeteerNode.launch} or {@link Puppeteer.connect}.
*
* @remarks
*
* The Browser class extends from Puppeteer's {@link EventEmitter} class and will
* emit various events which are documented in the {@link BrowserEmittedEvents} enum.
*
* @example
* An example of using a {@link Browser} to create a {@link Page}:
*
* ```ts
* const puppeteer = require('puppeteer');
*
* (async () => {
* const browser = await puppeteer.launch();
* const page = await browser.newPage();
* await page.goto('https://example.com');
* await browser.close();
* })();
* ```
*
* @example
* An example of disconnecting from and reconnecting to a {@link Browser}:
*
* ```ts
* const puppeteer = require('puppeteer');
*
* (async () => {
* const browser = await puppeteer.launch();
* // Store the endpoint to be able to reconnect to Chromium
* const browserWSEndpoint = browser.wsEndpoint();
* // Disconnect puppeteer from Chromium
* browser.disconnect();
*
* // Use the endpoint to reestablish a connection
* const browser2 = await puppeteer.connect({browserWSEndpoint});
* // Close Chromium
* await browser2.close();
* })();
* ```
*
* @public
*/
export class Browser extends EventEmitter {
/**
* @internal
*/
constructor() {
super();
}
/**
* @internal
*/
_attach(): Promise<void> {
throw new Error('Not implemented');
}
/**
* @internal
*/
_detach(): void {
throw new Error('Not implemented');
}
/**
* @internal
*/
get _targets(): Map<string, Target> {
throw new Error('Not implemented');
}
/**
* The spawned browser process. Returns `null` if the browser instance was created with
* {@link Puppeteer.connect}.
*/
process(): ChildProcess | null {
throw new Error('Not implemented');
}
/**
* @internal
*/
_getIsPageTargetCallback(): IsPageTargetCallback | undefined {
throw new Error('Not implemented');
}
/**
* Creates a new incognito browser context. This won't share cookies/cache with other
* browser contexts.
*
* @example
*
* ```ts
* (async () => {
* const browser = await puppeteer.launch();
* // Create a new incognito browser context.
* const context = await browser.createIncognitoBrowserContext();
* // Create a new page in a pristine context.
* const page = await context.newPage();
* // Do stuff
* await page.goto('https://example.com');
* })();
* ```
*/
createIncognitoBrowserContext(
options?: BrowserContextOptions
): Promise<BrowserContext>;
createIncognitoBrowserContext(): Promise<BrowserContext> {
throw new Error('Not implemented');
}
/**
* Returns an array of all open browser contexts. In a newly created browser, this will
* return a single instance of {@link BrowserContext}.
*/
browserContexts(): BrowserContext[] {
throw new Error('Not implemented');
}
/**
* Returns the default browser context. The default browser context cannot be closed.
*/
defaultBrowserContext(): BrowserContext {
throw new Error('Not implemented');
}
/**
* @internal
*/
_disposeContext(contextId?: string): Promise<void>;
_disposeContext(): Promise<void> {
throw new Error('Not implemented');
}
/**
* The browser websocket endpoint which can be used as an argument to
* {@link Puppeteer.connect}.
*
* @returns The Browser websocket url.
*
* @remarks
*
* The format is `ws://${host}:${port}/devtools/browser/<id>`.
*
* You can find the `webSocketDebuggerUrl` from `http://${host}:${port}/json/version`.
* Learn more about the
* {@link https://chromedevtools.github.io/devtools-protocol | devtools protocol} and
* the {@link
* https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target
* | browser endpoint}.
*/
wsEndpoint(): string {
throw new Error('Not implemented');
}
/**
* Promise which resolves to a new {@link Page} object. The Page is created in
* a default browser context.
*/
newPage(): Promise<Page> {
throw new Error('Not implemented');
}
/**
* @internal
*/
_createPageInContext(contextId?: string): Promise<Page>;
_createPageInContext(): Promise<Page> {
throw new Error('Not implemented');
}
/**
* All active targets inside the Browser. In case of multiple browser contexts, returns
* an array with all the targets in all browser contexts.
*/
targets(): Target[] {
throw new Error('Not implemented');
}
/**
* The target associated with the browser.
*/
target(): Target {
throw new Error('Not implemented');
}
/**
* Searches for a target in all browser contexts.
*
* @param predicate - A function to be run for every target.
* @returns The first target found that matches the `predicate` function.
*
* @example
*
* An example of finding a target for a page opened via `window.open`:
*
* ```ts
* await page.evaluate(() => window.open('https://www.example.com/'));
* const newWindowTarget = await browser.waitForTarget(
* target => target.url() === 'https://www.example.com/'
* );
* ```
*/
waitForTarget(
predicate: (x: Target) => boolean | Promise<boolean>,
options?: WaitForTargetOptions
): Promise<Target>;
waitForTarget(): Promise<Target> {
throw new Error('Not implemented');
}
/**
* An array of all open pages inside the Browser.
*
* @remarks
*
* In case of multiple browser contexts, returns an array with all the pages in all
* browser contexts. Non-visible pages, such as `"background_page"`, will not be listed
* here. You can find them using {@link Target.page}.
*/
pages(): Promise<Page[]> {
throw new Error('Not implemented');
}
/**
* A string representing the browser name and version.
*
* @remarks
*
* For headless Chromium, this is similar to `HeadlessChrome/61.0.3153.0`. For
* non-headless, this is similar to `Chrome/61.0.3153.0`.
*
* The format of browser.version() might change with future releases of Chromium.
*/
version(): Promise<string> {
throw new Error('Not implemented');
}
/**
* The browser's original user agent. Pages can override the browser user agent with
* {@link Page.setUserAgent}.
*/
userAgent(): Promise<string> {
throw new Error('Not implemented');
}
/**
* Closes Chromium and all of its pages (if any were opened). The {@link Browser} object
* itself is considered to be disposed and cannot be used anymore.
*/
close(): Promise<void> {
throw new Error('Not implemented');
}
/**
* Disconnects Puppeteer from the browser, but leaves the Chromium process running.
* After calling `disconnect`, the {@link Browser} object is considered disposed and
* cannot be used anymore.
*/
disconnect(): void {
throw new Error('Not implemented');
}
/**
* Indicates that the browser is connected.
*/
isConnected(): boolean {
throw new Error('Not implemented');
}
}
/**
* @public
*/
export const enum BrowserContextEmittedEvents {
/**
* Emitted when the url of a target inside the browser context changes.
* Contains a {@link Target} instance.
*/
TargetChanged = 'targetchanged',
/**
* Emitted when a target is created within the browser context, for example
* when a new page is opened by
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/open | window.open}
* or by {@link BrowserContext.newPage | browserContext.newPage}
*
* Contains a {@link Target} instance.
*/
TargetCreated = 'targetcreated',
/**
* Emitted when a target is destroyed within the browser context, for example
* when a page is closed. Contains a {@link Target} instance.
*/
TargetDestroyed = 'targetdestroyed',
}
/**
* BrowserContexts provide a way to operate multiple independent browser
* sessions. When a browser is launched, it has a single BrowserContext used by
* default. The method {@link Browser.newPage | Browser.newPage} creates a page
* in the default browser context.
*
* @remarks
*
* The Browser class extends from Puppeteer's {@link EventEmitter} class and
* will emit various events which are documented in the
* {@link BrowserContextEmittedEvents} enum.
*
* If a page opens another page, e.g. with a `window.open` call, the popup will
* belong to the parent page's browser context.
*
* Puppeteer allows creation of "incognito" browser contexts with
* {@link Browser.createIncognitoBrowserContext | Browser.createIncognitoBrowserContext}
* method. "Incognito" browser contexts don't write any browsing data to disk.
*
* @example
*
* ```ts
* // Create a new incognito browser context
* const context = await browser.createIncognitoBrowserContext();
* // Create a new page inside context.
* const page = await context.newPage();
* // ... do stuff with page ...
* await page.goto('https://example.com');
* // Dispose context once it's no longer needed.
* await context.close();
* ```
*
* @public
*/
export class BrowserContext extends EventEmitter {
/**
* @internal
*/
constructor() {
super();
}
/**
* An array of all active targets inside the browser context.
*/
targets(): Target[] {
throw new Error('Not implemented');
}
/**
* This searches for a target in this specific browser context.
*
* @example
* An example of finding a target for a page opened via `window.open`:
*
* ```ts
* await page.evaluate(() => window.open('https://www.example.com/'));
* const newWindowTarget = await browserContext.waitForTarget(
* target => target.url() === 'https://www.example.com/'
* );
* ```
*
* @param predicate - A function to be run for every target
* @param options - An object of options. Accepts a timout,
* which is the maximum wait time in milliseconds.
* Pass `0` to disable the timeout. Defaults to 30 seconds.
* @returns Promise which resolves to the first target found
* that matches the `predicate` function.
*/
waitForTarget(
predicate: (x: Target) => boolean | Promise<boolean>,
options?: {timeout?: number}
): Promise<Target>;
waitForTarget(): Promise<Target> {
throw new Error('Not implemented');
}
/**
* An array of all pages inside the browser context.
*
* @returns Promise which resolves to an array of all open pages.
* Non visible pages, such as `"background_page"`, will not be listed here.
* You can find them using {@link Target.page | the target page}.
*/
pages(): Promise<Page[]> {
throw new Error('Not implemented');
}
/**
* Returns whether BrowserContext is incognito.
* The default browser context is the only non-incognito browser context.
*
* @remarks
* The default browser context cannot be closed.
*/
isIncognito(): boolean {
throw new Error('Not implemented');
}
/**
* @example
*
* ```ts
* const context = browser.defaultBrowserContext();
* await context.overridePermissions('https://html5demos.com', [
* 'geolocation',
* ]);
* ```
*
* @param origin - The origin to grant permissions to, e.g. "https://example.com".
* @param permissions - An array of permissions to grant.
* All permissions that are not listed here will be automatically denied.
*/
overridePermissions(origin: string, permissions: Permission[]): Promise<void>;
overridePermissions(): Promise<void> {
throw new Error('Not implemented');
}
/**
* Clears all permission overrides for the browser context.
*
* @example
*
* ```ts
* const context = browser.defaultBrowserContext();
* context.overridePermissions('https://example.com', ['clipboard-read']);
* // do stuff ..
* context.clearPermissionOverrides();
* ```
*/
clearPermissionOverrides(): Promise<void> {
throw new Error('Not implemented');
}
/**
* Creates a new page in the browser context.
*/
newPage(): Promise<Page> {
throw new Error('Not implemented');
}
/**
* The browser this browser context belongs to.
*/
browser(): Browser {
throw new Error('Not implemented');
}
/**
* Closes the browser context. All the targets that belong to the browser context
* will be closed.
*
* @remarks
* Only incognito browser contexts can be closed.
*/
close(): Promise<void> {
throw new Error('Not implemented');
}
}

View File

@ -18,7 +18,6 @@ import {ChildProcess} from 'child_process';
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {CDPSession, Connection, ConnectionEmittedEvents} from './Connection.js'; import {CDPSession, Connection, ConnectionEmittedEvents} from './Connection.js';
import {EventEmitter} from './EventEmitter.js';
import {waitWithTimeout} from './util.js'; import {waitWithTimeout} from './util.js';
import {Page} from './Page.js'; import {Page} from './Page.js';
import {Viewport} from './PuppeteerViewport.js'; import {Viewport} from './PuppeteerViewport.js';
@ -27,196 +26,24 @@ import {TaskQueue} from './TaskQueue.js';
import {TargetManager, TargetManagerEmittedEvents} from './TargetManager.js'; import {TargetManager, TargetManagerEmittedEvents} from './TargetManager.js';
import {ChromeTargetManager} from './ChromeTargetManager.js'; import {ChromeTargetManager} from './ChromeTargetManager.js';
import {FirefoxTargetManager} from './FirefoxTargetManager.js'; import {FirefoxTargetManager} from './FirefoxTargetManager.js';
import {
/** Browser as BrowserBase,
* BrowserContext options. BrowserContext,
* BrowserCloseCallback,
* @public TargetFilterCallback,
*/ IsPageTargetCallback,
export interface BrowserContextOptions { BrowserEmittedEvents,
/** BrowserContextEmittedEvents,
* Proxy server with optional port to use for all requests. BrowserContextOptions,
* Username and password can be set in `Page.authenticate`. WEB_PERMISSION_TO_PROTOCOL_PERMISSION,
*/ WaitForTargetOptions,
proxyServer?: string;
/**
* Bypass the proxy for the given semi-colon-separated list of hosts.
*/
proxyBypassList?: string[];
}
/**
* @internal
*/
export type BrowserCloseCallback = () => Promise<void> | void;
/**
* @public
*/
export type TargetFilterCallback = (
target: Protocol.Target.TargetInfo
) => boolean;
/**
* @internal
*/
export type IsPageTargetCallback = (
target: Protocol.Target.TargetInfo
) => boolean;
const WEB_PERMISSION_TO_PROTOCOL_PERMISSION = new Map<
Permission, Permission,
Protocol.Browser.PermissionType } from '../api/Browser.js';
>([
['geolocation', 'geolocation'],
['midi', 'midi'],
['notifications', 'notifications'],
// TODO: push isn't a valid type?
// ['push', 'push'],
['camera', 'videoCapture'],
['microphone', 'audioCapture'],
['background-sync', 'backgroundSync'],
['ambient-light-sensor', 'sensors'],
['accelerometer', 'sensors'],
['gyroscope', 'sensors'],
['magnetometer', 'sensors'],
['accessibility-events', 'accessibilityEvents'],
['clipboard-read', 'clipboardReadWrite'],
['clipboard-write', 'clipboardReadWrite'],
['payment-handler', 'paymentHandler'],
['persistent-storage', 'durableStorage'],
['idle-detection', 'idleDetection'],
// chrome-specific permissions we have.
['midi-sysex', 'midiSysex'],
]);
/** /**
* @public * @internal
*/ */
export type Permission = export class CDPBrowser extends BrowserBase {
| 'geolocation'
| 'midi'
| 'notifications'
| 'camera'
| 'microphone'
| 'background-sync'
| 'ambient-light-sensor'
| 'accelerometer'
| 'gyroscope'
| 'magnetometer'
| 'accessibility-events'
| 'clipboard-read'
| 'clipboard-write'
| 'payment-handler'
| 'persistent-storage'
| 'idle-detection'
| 'midi-sysex';
/**
* @public
*/
export interface WaitForTargetOptions {
/**
* Maximum wait time in milliseconds. Pass `0` to disable the timeout.
* @defaultValue 30 seconds.
*/
timeout?: number;
}
/**
* All the events a {@link Browser | browser instance} may emit.
*
* @public
*/
export const enum BrowserEmittedEvents {
/**
* Emitted when Puppeteer gets disconnected from the Chromium instance. This
* might happen because of one of the following:
*
* - Chromium is closed or crashed
*
* - The {@link Browser.disconnect | browser.disconnect } method was called.
*/
Disconnected = 'disconnected',
/**
* Emitted when the url of a target changes. Contains a {@link Target} instance.
*
* @remarks
*
* Note that this includes target changes in incognito browser contexts.
*/
TargetChanged = 'targetchanged',
/**
* Emitted when a target is created, for example when a new page is opened by
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/open | window.open}
* or by {@link Browser.newPage | browser.newPage}
*
* Contains a {@link Target} instance.
*
* @remarks
*
* Note that this includes target creations in incognito browser contexts.
*/
TargetCreated = 'targetcreated',
/**
* Emitted when a target is destroyed, for example when a page is closed.
* Contains a {@link Target} instance.
*
* @remarks
*
* Note that this includes target destructions in incognito browser contexts.
*/
TargetDestroyed = 'targetdestroyed',
}
/**
* A Browser is created when Puppeteer connects to a Chromium instance, either through
* {@link PuppeteerNode.launch} or {@link Puppeteer.connect}.
*
* @remarks
*
* The Browser class extends from Puppeteer's {@link EventEmitter} class and will
* emit various events which are documented in the {@link BrowserEmittedEvents} enum.
*
* @example
* An example of using a {@link Browser} to create a {@link Page}:
*
* ```ts
* const puppeteer = require('puppeteer');
*
* (async () => {
* const browser = await puppeteer.launch();
* const page = await browser.newPage();
* await page.goto('https://example.com');
* await browser.close();
* })();
* ```
*
* @example
* An example of disconnecting from and reconnecting to a {@link Browser}:
*
* ```ts
* const puppeteer = require('puppeteer');
*
* (async () => {
* const browser = await puppeteer.launch();
* // Store the endpoint to be able to reconnect to Chromium
* const browserWSEndpoint = browser.wsEndpoint();
* // Disconnect puppeteer from Chromium
* browser.disconnect();
*
* // Use the endpoint to reestablish a connection
* const browser2 = await puppeteer.connect({browserWSEndpoint});
* // Close Chromium
* await browser2.close();
* })();
* ```
*
* @public
*/
export class Browser extends EventEmitter {
/** /**
* @internal * @internal
*/ */
@ -230,8 +57,8 @@ export class Browser extends EventEmitter {
closeCallback?: BrowserCloseCallback, closeCallback?: BrowserCloseCallback,
targetFilterCallback?: TargetFilterCallback, targetFilterCallback?: TargetFilterCallback,
isPageTargetCallback?: IsPageTargetCallback isPageTargetCallback?: IsPageTargetCallback
): Promise<Browser> { ): Promise<CDPBrowser> {
const browser = new Browser( const browser = new CDPBrowser(
product, product,
connection, connection,
contextIds, contextIds,
@ -252,15 +79,15 @@ export class Browser extends EventEmitter {
#closeCallback: BrowserCloseCallback; #closeCallback: BrowserCloseCallback;
#targetFilterCallback: TargetFilterCallback; #targetFilterCallback: TargetFilterCallback;
#isPageTargetCallback!: IsPageTargetCallback; #isPageTargetCallback!: IsPageTargetCallback;
#defaultContext: BrowserContext; #defaultContext: CDPBrowserContext;
#contexts: Map<string, BrowserContext>; #contexts: Map<string, CDPBrowserContext>;
#screenshotTaskQueue: TaskQueue; #screenshotTaskQueue: TaskQueue;
#targetManager: TargetManager; #targetManager: TargetManager;
/** /**
* @internal * @internal
*/ */
get _targets(): Map<string, Target> { override get _targets(): Map<string, Target> {
return this.#targetManager.getAvailableTargets(); return this.#targetManager.getAvailableTargets();
} }
@ -305,12 +132,12 @@ export class Browser extends EventEmitter {
this.#targetFilterCallback this.#targetFilterCallback
); );
} }
this.#defaultContext = new BrowserContext(this.#connection, this); this.#defaultContext = new CDPBrowserContext(this.#connection, this);
this.#contexts = new Map(); this.#contexts = new Map();
for (const contextId of contextIds) { for (const contextId of contextIds) {
this.#contexts.set( this.#contexts.set(
contextId, contextId,
new BrowserContext(this.#connection, this, contextId) new CDPBrowserContext(this.#connection, this, contextId)
); );
} }
} }
@ -322,7 +149,7 @@ export class Browser extends EventEmitter {
/** /**
* @internal * @internal
*/ */
async _attach(): Promise<void> { override async _attach(): Promise<void> {
this.#connection.on( this.#connection.on(
ConnectionEmittedEvents.Disconnected, ConnectionEmittedEvents.Disconnected,
this.#emitDisconnected this.#emitDisconnected
@ -349,7 +176,7 @@ export class Browser extends EventEmitter {
/** /**
* @internal * @internal
*/ */
_detach(): void { override _detach(): void {
this.#connection.off( this.#connection.off(
ConnectionEmittedEvents.Disconnected, ConnectionEmittedEvents.Disconnected,
this.#emitDisconnected this.#emitDisconnected
@ -376,7 +203,7 @@ export class Browser extends EventEmitter {
* The spawned browser process. Returns `null` if the browser instance was created with * The spawned browser process. Returns `null` if the browser instance was created with
* {@link Puppeteer.connect}. * {@link Puppeteer.connect}.
*/ */
process(): ChildProcess | null { override process(): ChildProcess | null {
return this.#process ?? null; return this.#process ?? null;
} }
@ -402,7 +229,7 @@ export class Browser extends EventEmitter {
/** /**
* @internal * @internal
*/ */
_getIsPageTargetCallback(): IsPageTargetCallback | undefined { override _getIsPageTargetCallback(): IsPageTargetCallback | undefined {
return this.#isPageTargetCallback; return this.#isPageTargetCallback;
} }
@ -424,9 +251,9 @@ export class Browser extends EventEmitter {
* })(); * })();
* ``` * ```
*/ */
async createIncognitoBrowserContext( override async createIncognitoBrowserContext(
options: BrowserContextOptions = {} options: BrowserContextOptions = {}
): Promise<BrowserContext> { ): Promise<CDPBrowserContext> {
const {proxyServer, proxyBypassList} = options; const {proxyServer, proxyBypassList} = options;
const {browserContextId} = await this.#connection.send( const {browserContextId} = await this.#connection.send(
@ -436,7 +263,7 @@ export class Browser extends EventEmitter {
proxyBypassList: proxyBypassList && proxyBypassList.join(','), proxyBypassList: proxyBypassList && proxyBypassList.join(','),
} }
); );
const context = new BrowserContext( const context = new CDPBrowserContext(
this.#connection, this.#connection,
this, this,
browserContextId browserContextId
@ -449,21 +276,21 @@ export class Browser extends EventEmitter {
* Returns an array of all open browser contexts. In a newly created browser, this will * Returns an array of all open browser contexts. In a newly created browser, this will
* return a single instance of {@link BrowserContext}. * return a single instance of {@link BrowserContext}.
*/ */
browserContexts(): BrowserContext[] { override browserContexts(): CDPBrowserContext[] {
return [this.#defaultContext, ...Array.from(this.#contexts.values())]; return [this.#defaultContext, ...Array.from(this.#contexts.values())];
} }
/** /**
* Returns the default browser context. The default browser context cannot be closed. * Returns the default browser context. The default browser context cannot be closed.
*/ */
defaultBrowserContext(): BrowserContext { override defaultBrowserContext(): CDPBrowserContext {
return this.#defaultContext; return this.#defaultContext;
} }
/** /**
* @internal * @internal
*/ */
async _disposeContext(contextId?: string): Promise<void> { override async _disposeContext(contextId?: string): Promise<void> {
if (!contextId) { if (!contextId) {
return; return;
} }
@ -564,7 +391,7 @@ export class Browser extends EventEmitter {
* https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target * https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target
* | browser endpoint}. * | browser endpoint}.
*/ */
wsEndpoint(): string { override wsEndpoint(): string {
return this.#connection.url(); return this.#connection.url();
} }
@ -572,14 +399,14 @@ export class Browser extends EventEmitter {
* Promise which resolves to a new {@link Page} object. The Page is created in * Promise which resolves to a new {@link Page} object. The Page is created in
* a default browser context. * a default browser context.
*/ */
async newPage(): Promise<Page> { override async newPage(): Promise<Page> {
return this.#defaultContext.newPage(); return this.#defaultContext.newPage();
} }
/** /**
* @internal * @internal
*/ */
async _createPageInContext(contextId?: string): Promise<Page> { override async _createPageInContext(contextId?: string): Promise<Page> {
const {targetId} = await this.#connection.send('Target.createTarget', { const {targetId} = await this.#connection.send('Target.createTarget', {
url: 'about:blank', url: 'about:blank',
browserContextId: contextId || undefined, browserContextId: contextId || undefined,
@ -605,7 +432,7 @@ export class Browser extends EventEmitter {
* All active targets inside the Browser. In case of multiple browser contexts, returns * All active targets inside the Browser. In case of multiple browser contexts, returns
* an array with all the targets in all browser contexts. * an array with all the targets in all browser contexts.
*/ */
targets(): Target[] { override targets(): Target[] {
return Array.from( return Array.from(
this.#targetManager.getAvailableTargets().values() this.#targetManager.getAvailableTargets().values()
).filter(target => { ).filter(target => {
@ -616,7 +443,7 @@ export class Browser extends EventEmitter {
/** /**
* The target associated with the browser. * The target associated with the browser.
*/ */
target(): Target { override target(): Target {
const browserTarget = this.targets().find(target => { const browserTarget = this.targets().find(target => {
return target.type() === 'browser'; return target.type() === 'browser';
}); });
@ -643,7 +470,7 @@ export class Browser extends EventEmitter {
* ); * );
* ``` * ```
*/ */
async waitForTarget( override async waitForTarget(
predicate: (x: Target) => boolean | Promise<boolean>, predicate: (x: Target) => boolean | Promise<boolean>,
options: WaitForTargetOptions = {} options: WaitForTargetOptions = {}
): Promise<Target> { ): Promise<Target> {
@ -683,7 +510,7 @@ export class Browser extends EventEmitter {
* browser contexts. Non-visible pages, such as `"background_page"`, will not be listed * browser contexts. Non-visible pages, such as `"background_page"`, will not be listed
* here. You can find them using {@link Target.page}. * here. You can find them using {@link Target.page}.
*/ */
async pages(): Promise<Page[]> { override async pages(): Promise<Page[]> {
const contextPages = await Promise.all( const contextPages = await Promise.all(
this.browserContexts().map(context => { this.browserContexts().map(context => {
return context.pages(); return context.pages();
@ -705,7 +532,7 @@ export class Browser extends EventEmitter {
* *
* The format of browser.version() might change with future releases of Chromium. * The format of browser.version() might change with future releases of Chromium.
*/ */
async version(): Promise<string> { override async version(): Promise<string> {
const version = await this.#getVersion(); const version = await this.#getVersion();
return version.product; return version.product;
} }
@ -714,26 +541,27 @@ export class Browser extends EventEmitter {
* The browser's original user agent. Pages can override the browser user agent with * The browser's original user agent. Pages can override the browser user agent with
* {@link Page.setUserAgent}. * {@link Page.setUserAgent}.
*/ */
async userAgent(): Promise<string> { override async userAgent(): Promise<string> {
const version = await this.#getVersion(); const version = await this.#getVersion();
return version.userAgent; return version.userAgent;
} }
/** /**
* Closes Chromium and all of its pages (if any were opened). The {@link Browser} object * Closes Chromium and all of its pages (if any were opened). The
* itself is considered to be disposed and cannot be used anymore. * {@link CDPBrowser} object itself is considered to be disposed and cannot be
* used anymore.
*/ */
async close(): Promise<void> { override async close(): Promise<void> {
await this.#closeCallback.call(null); await this.#closeCallback.call(null);
this.disconnect(); this.disconnect();
} }
/** /**
* Disconnects Puppeteer from the browser, but leaves the Chromium process running. * Disconnects Puppeteer from the browser, but leaves the Chromium process running.
* After calling `disconnect`, the {@link Browser} object is considered disposed and * After calling `disconnect`, the {@link CDPBrowser} object is considered disposed and
* cannot be used anymore. * cannot be used anymore.
*/ */
disconnect(): void { override disconnect(): void {
this.#targetManager.dispose(); this.#targetManager.dispose();
this.#connection.dispose(); this.#connection.dispose();
} }
@ -741,7 +569,7 @@ export class Browser extends EventEmitter {
/** /**
* Indicates that the browser is connected. * Indicates that the browser is connected.
*/ */
isConnected(): boolean { override isConnected(): boolean {
return !this.#connection._closed; return !this.#connection._closed;
} }
@ -749,75 +577,19 @@ export class Browser extends EventEmitter {
return this.#connection.send('Browser.getVersion'); return this.#connection.send('Browser.getVersion');
} }
} }
/**
* @public
*/
export const enum BrowserContextEmittedEvents {
/**
* Emitted when the url of a target inside the browser context changes.
* Contains a {@link Target} instance.
*/
TargetChanged = 'targetchanged',
/**
* Emitted when a target is created within the browser context, for example
* when a new page is opened by
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/open | window.open}
* or by {@link BrowserContext.newPage | browserContext.newPage}
*
* Contains a {@link Target} instance.
*/
TargetCreated = 'targetcreated',
/**
* Emitted when a target is destroyed within the browser context, for example
* when a page is closed. Contains a {@link Target} instance.
*/
TargetDestroyed = 'targetdestroyed',
}
/** /**
* BrowserContexts provide a way to operate multiple independent browser * @internal
* sessions. When a browser is launched, it has a single BrowserContext used by
* default. The method {@link Browser.newPage | Browser.newPage} creates a page
* in the default browser context.
*
* @remarks
*
* The Browser class extends from Puppeteer's {@link EventEmitter} class and
* will emit various events which are documented in the
* {@link BrowserContextEmittedEvents} enum.
*
* If a page opens another page, e.g. with a `window.open` call, the popup will
* belong to the parent page's browser context.
*
* Puppeteer allows creation of "incognito" browser contexts with
* {@link Browser.createIncognitoBrowserContext | Browser.createIncognitoBrowserContext}
* method. "Incognito" browser contexts don't write any browsing data to disk.
*
* @example
*
* ```ts
* // Create a new incognito browser context
* const context = await browser.createIncognitoBrowserContext();
* // Create a new page inside context.
* const page = await context.newPage();
* // ... do stuff with page ...
* await page.goto('https://example.com');
* // Dispose context once it's no longer needed.
* await context.close();
* ```
*
* @public
*/ */
export class BrowserContext extends EventEmitter { export class CDPBrowserContext extends BrowserContext {
#connection: Connection; #connection: Connection;
#browser: Browser; #browser: CDPBrowser;
#id?: string; #id?: string;
/** /**
* @internal * @internal
*/ */
constructor(connection: Connection, browser: Browser, contextId?: string) { constructor(connection: Connection, browser: CDPBrowser, contextId?: string) {
super(); super();
this.#connection = connection; this.#connection = connection;
this.#browser = browser; this.#browser = browser;
@ -827,7 +599,7 @@ export class BrowserContext extends EventEmitter {
/** /**
* An array of all active targets inside the browser context. * An array of all active targets inside the browser context.
*/ */
targets(): Target[] { override targets(): Target[] {
return this.#browser.targets().filter(target => { return this.#browser.targets().filter(target => {
return target.browserContext() === this; return target.browserContext() === this;
}); });
@ -853,7 +625,7 @@ export class BrowserContext extends EventEmitter {
* @returns Promise which resolves to the first target found * @returns Promise which resolves to the first target found
* that matches the `predicate` function. * that matches the `predicate` function.
*/ */
waitForTarget( override waitForTarget(
predicate: (x: Target) => boolean | Promise<boolean>, predicate: (x: Target) => boolean | Promise<boolean>,
options: {timeout?: number} = {} options: {timeout?: number} = {}
): Promise<Target> { ): Promise<Target> {
@ -869,7 +641,7 @@ export class BrowserContext extends EventEmitter {
* Non visible pages, such as `"background_page"`, will not be listed here. * Non visible pages, such as `"background_page"`, will not be listed here.
* You can find them using {@link Target.page | the target page}. * You can find them using {@link Target.page | the target page}.
*/ */
async pages(): Promise<Page[]> { override async pages(): Promise<Page[]> {
const pages = await Promise.all( const pages = await Promise.all(
this.targets() this.targets()
.filter(target => { .filter(target => {
@ -897,7 +669,7 @@ export class BrowserContext extends EventEmitter {
* @remarks * @remarks
* The default browser context cannot be closed. * The default browser context cannot be closed.
*/ */
isIncognito(): boolean { override isIncognito(): boolean {
return !!this.#id; return !!this.#id;
} }
@ -915,7 +687,7 @@ export class BrowserContext extends EventEmitter {
* @param permissions - An array of permissions to grant. * @param permissions - An array of permissions to grant.
* All permissions that are not listed here will be automatically denied. * All permissions that are not listed here will be automatically denied.
*/ */
async overridePermissions( override async overridePermissions(
origin: string, origin: string,
permissions: Permission[] permissions: Permission[]
): Promise<void> { ): Promise<void> {
@ -946,7 +718,7 @@ export class BrowserContext extends EventEmitter {
* context.clearPermissionOverrides(); * context.clearPermissionOverrides();
* ``` * ```
*/ */
async clearPermissionOverrides(): Promise<void> { override async clearPermissionOverrides(): Promise<void> {
await this.#connection.send('Browser.resetPermissions', { await this.#connection.send('Browser.resetPermissions', {
browserContextId: this.#id || undefined, browserContextId: this.#id || undefined,
}); });
@ -955,14 +727,14 @@ export class BrowserContext extends EventEmitter {
/** /**
* Creates a new page in the browser context. * Creates a new page in the browser context.
*/ */
newPage(): Promise<Page> { override newPage(): Promise<Page> {
return this.#browser._createPageInContext(this.#id); return this.#browser._createPageInContext(this.#id);
} }
/** /**
* The browser this browser context belongs to. * The browser this browser context belongs to.
*/ */
browser(): Browser { override browser(): CDPBrowser {
return this.#browser; return this.#browser;
} }
@ -973,7 +745,7 @@ export class BrowserContext extends EventEmitter {
* @remarks * @remarks
* Only incognito browser contexts can be closed. * Only incognito browser contexts can be closed.
*/ */
async close(): Promise<void> { override async close(): Promise<void> {
assert(this.#id, 'Non-incognito profiles cannot be closed!'); assert(this.#id, 'Non-incognito profiles cannot be closed!');
await this.#browser._disposeContext(this.#id); await this.#browser._disposeContext(this.#id);
} }

View File

@ -18,11 +18,8 @@ import {debugError} from './util.js';
import {isErrorLike} from '../util/ErrorLike.js'; import {isErrorLike} from '../util/ErrorLike.js';
import {isNode} from '../environment.js'; import {isNode} from '../environment.js';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import { import {IsPageTargetCallback, TargetFilterCallback} from '../api/Browser.js';
Browser, import {CDPBrowser} from './Browser.js';
IsPageTargetCallback,
TargetFilterCallback,
} from './Browser.js';
import {Connection} from './Connection.js'; import {Connection} from './Connection.js';
import {ConnectionTransport} from './ConnectionTransport.js'; import {ConnectionTransport} from './ConnectionTransport.js';
import {getFetch} from './fetch.js'; import {getFetch} from './fetch.js';
@ -55,6 +52,11 @@ export interface BrowserConnectOptions {
* @internal * @internal
*/ */
_isPageTarget?: IsPageTargetCallback; _isPageTarget?: IsPageTargetCallback;
/**
* @defaultValue 'cdp'
* @internal
*/
protocol?: 'cdp' | 'webDriverBiDi';
} }
const getWebSocketTransportClass = async () => { const getWebSocketTransportClass = async () => {
@ -70,13 +72,13 @@ const getWebSocketTransportClass = async () => {
* *
* @internal * @internal
*/ */
export async function _connectToBrowser( export async function _connectToCDPBrowser(
options: BrowserConnectOptions & { options: BrowserConnectOptions & {
browserWSEndpoint?: string; browserWSEndpoint?: string;
browserURL?: string; browserURL?: string;
transport?: ConnectionTransport; transport?: ConnectionTransport;
} }
): Promise<Browser> { ): Promise<CDPBrowser> {
const { const {
browserWSEndpoint, browserWSEndpoint,
browserURL, browserURL,
@ -118,7 +120,7 @@ export async function _connectToBrowser(
const {browserContextIds} = await connection.send( const {browserContextIds} = await connection.send(
'Target.getBrowserContexts' 'Target.getBrowserContexts'
); );
const browser = await Browser._create( const browser = await CDPBrowser._create(
product || 'chrome', product || 'chrome',
connection, connection,
browserContextIds, browserContextIds,

View File

@ -20,7 +20,7 @@ import {CDPSession, Connection} from './Connection.js';
import {EventEmitter} from './EventEmitter.js'; import {EventEmitter} from './EventEmitter.js';
import {Target} from './Target.js'; import {Target} from './Target.js';
import {debugError} from './util.js'; import {debugError} from './util.js';
import {TargetFilterCallback} from './Browser.js'; import {TargetFilterCallback} from '../api/Browser.js';
import { import {
TargetInterceptor, TargetInterceptor,
TargetFactory, TargetFactory,

View File

@ -18,7 +18,7 @@ import Protocol from 'devtools-protocol';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {CDPSession, Connection} from './Connection.js'; import {CDPSession, Connection} from './Connection.js';
import {Target} from './Target.js'; import {Target} from './Target.js';
import {TargetFilterCallback} from './Browser.js'; import {TargetFilterCallback} from '../api/Browser.js';
import { import {
TargetFactory, TargetFactory,
TargetInterceptor, TargetInterceptor,

View File

@ -23,7 +23,7 @@ import {
} from '../util/DeferredPromise.js'; } from '../util/DeferredPromise.js';
import {isErrorLike} from '../util/ErrorLike.js'; import {isErrorLike} from '../util/ErrorLike.js';
import {Accessibility} from './Accessibility.js'; import {Accessibility} from './Accessibility.js';
import {Browser, BrowserContext} from './Browser.js'; import type {Browser, BrowserContext} from '../api/Browser.js';
import { import {
CDPSession, CDPSession,
CDPSessionEmittedEvents, CDPSessionEmittedEvents,

View File

@ -13,8 +13,11 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import {Browser} from './Browser.js'; import {Browser} from '../api/Browser.js';
import {BrowserConnectOptions, _connectToBrowser} from './BrowserConnector.js'; import {
BrowserConnectOptions,
_connectToCDPBrowser,
} from './BrowserConnector.js';
import {ConnectionTransport} from './ConnectionTransport.js'; import {ConnectionTransport} from './ConnectionTransport.js';
import {devices} from './DeviceDescriptors.js'; import {devices} from './DeviceDescriptors.js';
import {errors} from './Errors.js'; import {errors} from './Errors.js';
@ -81,7 +84,7 @@ export class Puppeteer {
* @returns Promise which resolves to browser instance. * @returns Promise which resolves to browser instance.
*/ */
connect(options: ConnectOptions): Promise<Browser> { connect(options: ConnectOptions): Promise<Browser> {
return _connectToBrowser(options); return _connectToCDPBrowser(options);
} }
/** /**

View File

@ -17,7 +17,11 @@
import {Page, PageEmittedEvents} from './Page.js'; import {Page, PageEmittedEvents} from './Page.js';
import {WebWorker} from './WebWorker.js'; import {WebWorker} from './WebWorker.js';
import {CDPSession} from './Connection.js'; import {CDPSession} from './Connection.js';
import {Browser, BrowserContext, IsPageTargetCallback} from './Browser.js'; import type {
Browser,
BrowserContext,
IsPageTargetCallback,
} from '../api/Browser.js';
import {Viewport} from './PuppeteerViewport.js'; import {Viewport} from './PuppeteerViewport.js';
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {TaskQueue} from './TaskQueue.js'; import {TaskQueue} from './TaskQueue.js';

View File

@ -0,0 +1,52 @@
import {
Browser as BrowserBase,
BrowserCloseCallback,
} from '../../api/Browser.js';
import {Connection} from './Connection.js';
import {ChildProcess} from 'child_process';
/**
* @internal
*/
export class Browser extends BrowserBase {
/**
* @internal
*/
static async create(opts: Options): Promise<Browser> {
// TODO: await until the connection is established.
return new Browser(opts);
}
#process?: ChildProcess;
#closeCallback?: BrowserCloseCallback;
#connection: Connection;
/**
* @internal
*/
constructor(opts: Options) {
super();
this.#process = opts.process;
this.#closeCallback = opts.closeCallback;
this.#connection = opts.connection;
}
override async close(): Promise<void> {
await this.#closeCallback?.call(null);
this.#connection.dispose();
}
override isConnected(): boolean {
return !this.#connection.closed;
}
override process(): ChildProcess | null {
return this.#process ?? null;
}
}
interface Options {
process?: ChildProcess;
closeCallback?: BrowserCloseCallback;
connection: Connection;
}

View File

@ -0,0 +1,167 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* 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 {debug} from '../Debug.js';
const debugProtocolSend = debug('puppeteer:webDriverBiDi:SEND ►');
const debugProtocolReceive = debug('puppeteer:webDriverBiDi:RECV ◀');
import {ConnectionTransport} from '../ConnectionTransport.js';
import {EventEmitter} from '../EventEmitter.js';
import {ProtocolError} from '../Errors.js';
import {ConnectionCallback} from '../Connection.js';
interface Command {
id: number;
method: string;
params: object;
}
interface CommandResponse {
id: number;
result: object;
}
interface ErrorResponse {
id: number;
error: string;
message: string;
stacktrace?: string;
}
interface Event {
method: string;
params: object;
}
/**
* @internal
*/
export class Connection extends EventEmitter {
#transport: ConnectionTransport;
#delay: number;
#lastId = 0;
#closed = false;
#callbacks: Map<number, ConnectionCallback> = new Map();
constructor(transport: ConnectionTransport, delay = 0) {
super();
this.#delay = delay;
this.#transport = transport;
this.#transport.onmessage = this.onMessage.bind(this);
this.#transport.onclose = this.#onClose.bind(this);
}
get closed(): boolean {
return this.#closed;
}
send(method: string, params: object): Promise<any> {
const id = ++this.#lastId;
const stringifiedMessage = JSON.stringify({
id,
method,
params,
} as Command);
debugProtocolSend(stringifiedMessage);
this.#transport.send(stringifiedMessage);
return new Promise((resolve, reject) => {
this.#callbacks.set(id, {
resolve,
reject,
error: new ProtocolError(),
method,
});
});
}
/**
* @internal
*/
protected async onMessage(message: string): Promise<void> {
if (this.#delay) {
await new Promise(f => {
return setTimeout(f, this.#delay);
});
}
debugProtocolReceive(message);
const object = JSON.parse(message) as
| Event
| ErrorResponse
| CommandResponse;
if ('id' in object) {
const callback = this.#callbacks.get(object.id);
// Callbacks could be all rejected if someone has called `.dispose()`.
if (callback) {
this.#callbacks.delete(object.id);
if ('error' in object) {
callback.reject(
createProtocolError(callback.error, callback.method, object)
);
} else {
callback.resolve(object.result);
}
}
} else {
this.emit(object.method, object.params);
}
}
#onClose(): void {
if (this.#closed) {
return;
}
this.#closed = true;
this.#transport.onmessage = undefined;
this.#transport.onclose = undefined;
for (const callback of this.#callbacks.values()) {
callback.reject(
rewriteError(
callback.error,
`Protocol error (${callback.method}): Connection closed.`
)
);
}
this.#callbacks.clear();
}
dispose(): void {
this.#onClose();
this.#transport.close();
}
}
function rewriteError(
error: ProtocolError,
message: string,
originalMessage?: string
): Error {
error.message = message;
error.originalMessage = originalMessage ?? error.originalMessage;
return error;
}
function createProtocolError(
error: ProtocolError,
method: string,
object: ErrorResponse
): Error {
let message = `Protocol error (${method}): ${object.error} ${object.message}`;
if (object.stacktrace) {
message += ` ${object.stacktrace}`;
}
return rewriteError(error, message, object.message);
}

View File

@ -22,6 +22,7 @@ import removeFolder from 'rimraf';
import {promisify} from 'util'; import {promisify} from 'util';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {Connection} from '../common/Connection.js'; import {Connection} from '../common/Connection.js';
import {Connection as BiDiConnection} from '../common/bidi/Connection.js';
import {debug} from '../common/Debug.js'; import {debug} from '../common/Debug.js';
import {TimeoutError} from '../common/Errors.js'; import {TimeoutError} from '../common/Errors.js';
import { import {
@ -245,6 +246,25 @@ export class BrowserRunner {
removeEventListeners(this.#listeners); removeEventListeners(this.#listeners);
} }
async setupWebDriverBiDiConnection(options: {
timeout: number;
slowMo: number;
preferredRevision: string;
}): Promise<BiDiConnection> {
assert(this.proc, 'BrowserRunner not started.');
const {timeout, slowMo, preferredRevision} = options;
let browserWSEndpoint = await waitForWSEndpoint(
this.proc,
timeout,
preferredRevision,
/^WebDriver BiDi listening on (ws:\/\/.*)$/
);
browserWSEndpoint += '/session';
const transport = await WebSocketTransport.create(browserWSEndpoint);
return new BiDiConnection(transport, slowMo);
}
async setupConnection(options: { async setupConnection(options: {
usePipe?: boolean; usePipe?: boolean;
timeout: number; timeout: number;
@ -279,7 +299,8 @@ export class BrowserRunner {
function waitForWSEndpoint( function waitForWSEndpoint(
browserProcess: childProcess.ChildProcess, browserProcess: childProcess.ChildProcess,
timeout: number, timeout: number,
preferredRevision: string preferredRevision: string,
regex = /^DevTools listening on (ws:\/\/.*)$/
): Promise<string> { ): Promise<string> {
assert(browserProcess.stderr, '`browserProcess` does not have stderr.'); assert(browserProcess.stderr, '`browserProcess` does not have stderr.');
const rl = readline.createInterface(browserProcess.stderr); const rl = readline.createInterface(browserProcess.stderr);
@ -327,7 +348,7 @@ function waitForWSEndpoint(
function onLine(line: string): void { function onLine(line: string): void {
stderr += line + '\n'; stderr += line + '\n';
const match = line.match(/^DevTools listening on (ws:\/\/.*)$/); const match = line.match(regex);
if (!match) { if (!match) {
return; return;
} }

View File

@ -1,7 +1,7 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {Browser} from '../common/Browser.js'; import {CDPBrowser} from '../common/Browser.js';
import {Product} from '../common/Product.js'; import {Product} from '../common/Product.js';
import {BrowserRunner} from './BrowserRunner.js'; import {BrowserRunner} from './BrowserRunner.js';
import { import {
@ -43,7 +43,7 @@ export class ChromeLauncher implements ProductLauncher {
this._isPuppeteerCore = isPuppeteerCore; this._isPuppeteerCore = isPuppeteerCore;
} }
async launch(options: PuppeteerNodeLaunchOptions = {}): Promise<Browser> { async launch(options: PuppeteerNodeLaunchOptions = {}): Promise<CDPBrowser> {
const { const {
ignoreDefaultArgs = false, ignoreDefaultArgs = false,
args = [], args = [],
@ -154,7 +154,7 @@ export class ChromeLauncher implements ProductLauncher {
slowMo, slowMo,
preferredRevision: this._preferredRevision, preferredRevision: this._preferredRevision,
}); });
browser = await Browser._create( browser = await CDPBrowser._create(
this.product, this.product,
connection, connection,
[], [],

View File

@ -2,7 +2,9 @@ import fs from 'fs';
import os from 'os'; import os from 'os';
import path from 'path'; import path from 'path';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {Browser} from '../common/Browser.js'; import {Browser} from '../api/Browser.js';
import {CDPBrowser as CDPBrowser} from '../common/Browser.js';
import {Browser as BiDiBrowser} from '../common/bidi/Browser.js';
import {Product} from '../common/Product.js'; import {Product} from '../common/Product.js';
import {BrowserFetcher} from './BrowserFetcher.js'; import {BrowserFetcher} from './BrowserFetcher.js';
import {BrowserRunner} from './BrowserRunner.js'; import {BrowserRunner} from './BrowserRunner.js';
@ -58,6 +60,7 @@ export class FirefoxLauncher implements ProductLauncher {
extraPrefsFirefox = {}, extraPrefsFirefox = {},
waitForInitialPage = true, waitForInitialPage = true,
debuggingPort = null, debuggingPort = null,
protocol = 'cdp',
} = options; } = options;
const firefoxArguments = []; const firefoxArguments = [];
@ -145,6 +148,27 @@ export class FirefoxLauncher implements ProductLauncher {
pipe, pipe,
}); });
if (protocol === 'webDriverBiDi') {
let browser;
try {
const connection = await runner.setupWebDriverBiDiConnection({
timeout,
slowMo,
preferredRevision: this._preferredRevision,
});
browser = await BiDiBrowser.create({
connection,
closeCallback: runner.close.bind(runner),
process: runner.proc,
});
} catch (error) {
runner.kill();
throw error;
}
return browser;
}
let browser; let browser;
try { try {
const connection = await runner.setupConnection({ const connection = await runner.setupConnection({
@ -153,7 +177,7 @@ export class FirefoxLauncher implements ProductLauncher {
slowMo, slowMo,
preferredRevision: this._preferredRevision, preferredRevision: this._preferredRevision,
}); });
browser = await Browser._create( browser = await CDPBrowser._create(
this.product, this.product,
connection, connection,
[], [],

View File

@ -15,7 +15,7 @@
*/ */
import os from 'os'; import os from 'os';
import {Browser} from '../common/Browser.js'; import {Browser} from '../api/Browser.js';
import {BrowserFetcher} from './BrowserFetcher.js'; import {BrowserFetcher} from './BrowserFetcher.js';
import { import {

View File

@ -22,7 +22,7 @@ import {
import {BrowserFetcher, BrowserFetcherOptions} from './BrowserFetcher.js'; import {BrowserFetcher, BrowserFetcherOptions} from './BrowserFetcher.js';
import {LaunchOptions, BrowserLaunchArgumentOptions} from './LaunchOptions.js'; import {LaunchOptions, BrowserLaunchArgumentOptions} from './LaunchOptions.js';
import {BrowserConnectOptions} from '../common/BrowserConnector.js'; import {BrowserConnectOptions} from '../common/BrowserConnector.js';
import {Browser} from '../common/Browser.js'; import {Browser} from '../api/Browser.js';
import {createLauncher, ProductLauncher} from './ProductLauncher.js'; import {createLauncher, ProductLauncher} from './ProductLauncher.js';
import {PUPPETEER_REVISIONS} from '../revisions.js'; import {PUPPETEER_REVISIONS} from '../revisions.js';
import {Product} from '../common/Product.js'; import {Product} from '../common/Product.js';

View File

@ -1,5 +1,6 @@
// AUTOGENERATED - Use `npm run generate:sources` to regenerate. // AUTOGENERATED - Use `npm run generate:sources` to regenerate.
export * from './api/Browser.js';
export * from './common/Accessibility.js'; export * from './common/Accessibility.js';
export * from './common/AriaQueryHandler.js'; export * from './common/AriaQueryHandler.js';
export * from './common/Browser.js'; export * from './common/Browser.js';

View File

@ -1942,5 +1942,23 @@
"platforms": ["darwin", "win32", "linux"], "platforms": ["darwin", "win32", "linux"],
"parameters": ["firefox"], "parameters": ["firefox"],
"expectations": ["SKIP"] "expectations": ["SKIP"]
},
{
"testIdPattern": "",
"platforms": ["darwin", "win32", "linux"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["SKIP"]
},
{
"testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch can launch and close the browser",
"platforms": ["darwin", "win32", "linux"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[Connection.spec] WebDriver BiDi",
"platforms": ["darwin", "win32", "linux"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["PASS"]
} }
] ]

View File

@ -19,6 +19,11 @@
"id": "firefox-headless", "id": "firefox-headless",
"platforms": ["linux"], "platforms": ["linux"],
"parameters": ["firefox", "headless"] "parameters": ["firefox", "headless"]
},
{
"id": "firefox-bidi",
"platforms": ["linux"],
"parameters": ["firefox", "headless", "webDriverBiDi"]
} }
], ],
"parameterDefinitons": { "parameterDefinitons": {
@ -36,6 +41,9 @@
}, },
"chrome-headless": { "chrome-headless": {
"HEADLESS": "chrome" "HEADLESS": "chrome"
},
"webDriverBiDi": {
"PUPPETEER_PROTOCOL": "webDriverBiDi"
} }
} }
} }

View File

@ -20,18 +20,18 @@ import utils from './utils.js';
import expect from 'expect'; import expect from 'expect';
import { import {
Browser, CDPBrowser,
BrowserContext, CDPBrowserContext,
} from '../../lib/cjs/puppeteer/common/Browser.js'; } from '../../lib/cjs/puppeteer/common/Browser.js';
describe('TargetManager', () => { describe('TargetManager', () => {
/* We use a special browser for this test as we need the --site-per-process flag */ /* We use a special browser for this test as we need the --site-per-process flag */
let browser: Browser; let browser: CDPBrowser;
let context: BrowserContext; let context: CDPBrowserContext;
before(async () => { before(async () => {
const {puppeteer, defaultBrowserOptions} = getTestState(); const {puppeteer, defaultBrowserOptions} = getTestState();
browser = await puppeteer.launch( browser = (await puppeteer.launch(
Object.assign({}, defaultBrowserOptions, { Object.assign({}, defaultBrowserOptions, {
args: (defaultBrowserOptions.args || []).concat([ args: (defaultBrowserOptions.args || []).concat([
'--site-per-process', '--site-per-process',
@ -39,7 +39,7 @@ describe('TargetManager', () => {
'--host-rules=MAP * 127.0.0.1', '--host-rules=MAP * 127.0.0.1',
]), ]),
}) })
); )) as CDPBrowser;
}); });
beforeEach(async () => { beforeEach(async () => {

View File

@ -0,0 +1,59 @@
/**
* Copyright 2022 Google Inc. All rights reserved.
*
* 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 expect from 'expect';
import {Connection} from '../../../lib/cjs/puppeteer/common/bidi/Connection.js';
import {ConnectionTransport} from '../../../lib/cjs/puppeteer/common/ConnectionTransport.js';
describe('WebDriver BiDi', () => {
describe('Connection', () => {
class TestConnectionTransport implements ConnectionTransport {
sent: string[] = [];
closed = false;
send(message: string) {
this.sent.push(message);
}
close(): void {
this.closed = true;
}
}
it('should work', async () => {
const transport = new TestConnectionTransport();
const connection = new Connection(transport);
const responsePromise = connection.send('session.status', {
context: 'context',
});
expect(transport.sent).toEqual([
`{"id":1,"method":"session.status","params":{"context":"context"}}`,
]);
const id = JSON.parse(transport.sent[0]!).id;
const rawResponse = {
id,
result: {ready: false, message: 'already connected'},
};
(transport as ConnectionTransport).onmessage?.(
JSON.stringify(rawResponse)
);
const response = await responsePromise;
expect(response).toEqual(rawResponse.result);
connection.dispose();
expect(transport.closed).toBeTruthy();
});
});
});

View File

@ -16,10 +16,7 @@
import expect from 'expect'; import expect from 'expect';
import {TLSSocket} from 'tls'; import {TLSSocket} from 'tls';
import { import {Browser, BrowserContext} from '../../lib/cjs/puppeteer/api/Browser.js';
Browser,
BrowserContext,
} from '../../lib/cjs/puppeteer/common/Browser.js';
import {Page} from '../../lib/cjs/puppeteer/common/Page.js'; import {Page} from '../../lib/cjs/puppeteer/common/Page.js';
import {HTTPResponse} from '../../lib/cjs/puppeteer/common/HTTPResponse.js'; import {HTTPResponse} from '../../lib/cjs/puppeteer/common/HTTPResponse.js';
import {getTestState} from './mocha-utils.js'; import {getTestState} from './mocha-utils.js';

View File

@ -203,6 +203,11 @@ describe('Launcher specs', function () {
}); });
}); });
describe('Puppeteer.launch', function () { describe('Puppeteer.launch', function () {
it('can launch and close the browser', async () => {
const {defaultBrowserOptions, puppeteer} = getTestState();
const browser = await puppeteer.launch(defaultBrowserOptions);
await browser.close();
});
it('should reject all promises when browser is closed', async () => { it('should reject all promises when browser is closed', async () => {
const {defaultBrowserOptions, puppeteer} = getTestState(); const {defaultBrowserOptions, puppeteer} = getTestState();
const browser = await puppeteer.launch(defaultBrowserOptions); const browser = await puppeteer.launch(defaultBrowserOptions);

View File

@ -20,10 +20,7 @@ import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import rimraf from 'rimraf'; import rimraf from 'rimraf';
import sinon from 'sinon'; import sinon from 'sinon';
import { import {Browser, BrowserContext} from '../../lib/cjs/puppeteer/api/Browser.js';
Browser,
BrowserContext,
} from '../../lib/cjs/puppeteer/common/Browser.js';
import {Page} from '../../lib/cjs/puppeteer/common/Page.js'; import {Page} from '../../lib/cjs/puppeteer/common/Page.js';
import {isErrorLike} from '../../lib/cjs/puppeteer/util/ErrorLike.js'; import {isErrorLike} from '../../lib/cjs/puppeteer/util/ErrorLike.js';
import { import {
@ -71,6 +68,7 @@ const headless = (process.env['HEADLESS'] || 'true').trim().toLowerCase();
const isHeadless = headless === 'true' || headless === 'chrome'; const isHeadless = headless === 'true' || headless === 'chrome';
const isFirefox = product === 'firefox'; const isFirefox = product === 'firefox';
const isChrome = product === 'chrome'; const isChrome = product === 'chrome';
const protocol = process.env['PUPPETEER_PROTOCOL'] || 'cdp';
let extraLaunchOptions = {}; let extraLaunchOptions = {};
try { try {
@ -91,6 +89,7 @@ const defaultBrowserOptions = Object.assign(
executablePath: process.env['BINARY'], executablePath: process.env['BINARY'],
headless: headless === 'chrome' ? ('chrome' as const) : isHeadless, headless: headless === 'chrome' ? ('chrome' as const) : isHeadless,
dumpio: !!process.env['DUMPIO'], dumpio: !!process.env['DUMPIO'],
protocol: protocol as 'cdp' | 'webDriverBiDi',
}, },
extraLaunchOptions extraLaunchOptions
); );

View File

@ -17,10 +17,7 @@
import utils from './utils.js'; import utils from './utils.js';
import expect from 'expect'; import expect from 'expect';
import {getTestState} from './mocha-utils.js'; import {getTestState} from './mocha-utils.js';
import { import {Browser, BrowserContext} from '../../lib/cjs/puppeteer/api/Browser.js';
Browser,
BrowserContext,
} from '../../lib/cjs/puppeteer/common/Browser.js';
import {Page} from '../../lib/cjs/puppeteer/common/Page.js'; import {Page} from '../../lib/cjs/puppeteer/common/Page.js';
describe('OOPIF', function () { describe('OOPIF', function () {

View File

@ -19,7 +19,7 @@ import http from 'http';
import os from 'os'; import os from 'os';
import {getTestState} from './mocha-utils.js'; import {getTestState} from './mocha-utils.js';
import type {Server, IncomingMessage, ServerResponse} from 'http'; import type {Server, IncomingMessage, ServerResponse} from 'http';
import type {Browser} from '../../lib/cjs/puppeteer/common/Browser.js'; import type {Browser} from '../../lib/cjs/puppeteer/api/Browser.js';
import type {AddressInfo} from 'net'; import type {AddressInfo} from 'net';
import {TestServer} from '../../utils/testserver/lib/index.js'; import {TestServer} from '../../utils/testserver/lib/index.js';

View File

@ -18,7 +18,7 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import expect from 'expect'; import expect from 'expect';
import {getTestState} from './mocha-utils.js'; import {getTestState} from './mocha-utils.js';
import {Browser} from '../../lib/cjs/puppeteer/common/Browser.js'; import {Browser} from '../../lib/cjs/puppeteer/api/Browser.js';
import {Page} from '../../lib/cjs/puppeteer/common/Page.js'; import {Page} from '../../lib/cjs/puppeteer/common/Page.js';
describe('Tracing', function () { describe('Tracing', function () {

View File

@ -6,7 +6,7 @@ import {sync as glob} from 'glob';
import path from 'path'; import path from 'path';
import {job} from './internal/job.js'; import {job} from './internal/job.js';
const INCLUDED_FOLDERS = ['common', 'node', 'generated', 'util']; const INCLUDED_FOLDERS = ['common', 'node', 'generated', 'util', 'api'];
(async () => { (async () => {
await job('', async ({outputs}) => { await job('', async ({outputs}) => {