summaryrefslogtreecommitdiffstats
path: root/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi
diff options
context:
space:
mode:
Diffstat (limited to 'remote/test/puppeteer/packages/puppeteer-core/src/common/bidi')
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/BidiOverCDP.ts190
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Browser.ts89
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/BrowserContext.ts59
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Connection.ts215
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Context.ts282
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/ElementHandle.ts52
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/JSHandle.ts159
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Page.ts345
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Serializer.ts273
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/bidi.ts21
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/utils.ts47
11 files changed, 1732 insertions, 0 deletions
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/BidiOverCDP.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/BidiOverCDP.ts
new file mode 100644
index 0000000000..1f965c56ab
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/BidiOverCDP.ts
@@ -0,0 +1,190 @@
+/**
+ * Copyright 2023 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 * as BidiMapper from 'chromium-bidi/lib/cjs/bidiMapper/bidiMapper.js';
+import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+import type {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js';
+
+import {CDPSession, Connection as CDPPPtrConnection} from '../Connection.js';
+import {Handler} from '../EventEmitter.js';
+
+import {Connection as BidiPPtrConnection} from './Connection.js';
+
+type CdpEvents = {
+ [Property in keyof ProtocolMapping.Events]: ProtocolMapping.Events[Property][0];
+};
+
+/**
+ * @internal
+ */
+export async function connectBidiOverCDP(
+ cdp: CDPPPtrConnection
+): Promise<BidiPPtrConnection> {
+ const transportBiDi = new NoOpTransport();
+ const cdpConnectionAdapter = new CDPConnectionAdapter(cdp);
+ const pptrTransport = {
+ send(message: string): void {
+ // Forwards a BiDi command sent by Puppeteer to the input of the BidiServer.
+ transportBiDi.emitMessage(JSON.parse(message));
+ },
+ close(): void {
+ bidiServer.close();
+ cdpConnectionAdapter.close();
+ },
+ onmessage(_message: string): void {
+ // The method is overridden by the Connection.
+ },
+ };
+ transportBiDi.on('bidiResponse', (message: object) => {
+ // Forwards a BiDi event sent by BidiServer to Puppeteer.
+ pptrTransport.onmessage(JSON.stringify(message));
+ });
+ const pptrBiDiConnection = new BidiPPtrConnection(pptrTransport);
+ const bidiServer = await BidiMapper.BidiServer.createAndStart(
+ transportBiDi,
+ cdpConnectionAdapter,
+ ''
+ );
+ return pptrBiDiConnection;
+}
+
+/**
+ * Manages CDPSessions for BidiServer.
+ * @internal
+ */
+class CDPConnectionAdapter {
+ #cdp: CDPPPtrConnection;
+ #adapters = new Map<CDPSession, CDPClientAdapter<CDPSession>>();
+ #browser: CDPClientAdapter<CDPPPtrConnection>;
+
+ constructor(cdp: CDPPPtrConnection) {
+ this.#cdp = cdp;
+ this.#browser = new CDPClientAdapter(cdp);
+ }
+
+ browserClient(): CDPClientAdapter<CDPPPtrConnection> {
+ return this.#browser;
+ }
+
+ getCdpClient(id: string) {
+ const session = this.#cdp.session(id);
+ if (!session) {
+ throw new Error('Unknown CDP session with id' + id);
+ }
+ if (!this.#adapters.has(session)) {
+ const adapter = new CDPClientAdapter(session);
+ this.#adapters.set(session, adapter);
+ return adapter;
+ }
+ return this.#adapters.get(session)!;
+ }
+
+ close() {
+ this.#browser.close();
+ for (const adapter of this.#adapters.values()) {
+ adapter.close();
+ }
+ }
+}
+
+/**
+ * Wrapper on top of CDPSession/CDPConnection to satisfy CDP interface that
+ * BidiServer needs.
+ *
+ * @internal
+ */
+class CDPClientAdapter<T extends Pick<CDPPPtrConnection, 'send' | 'on' | 'off'>>
+ extends BidiMapper.EventEmitter<CdpEvents>
+ implements BidiMapper.CdpClient
+{
+ #closed = false;
+ #client: T;
+
+ constructor(client: T) {
+ super();
+ this.#client = client;
+ this.#client.on('*', this.#forwardMessage as Handler<any>);
+ }
+
+ #forwardMessage = <T extends keyof CdpEvents>(
+ method: T,
+ event: CdpEvents[T]
+ ) => {
+ this.emit(method, event);
+ };
+
+ async sendCommand<T extends keyof ProtocolMapping.Commands>(
+ method: T,
+ ...params: ProtocolMapping.Commands[T]['paramsType']
+ ): Promise<ProtocolMapping.Commands[T]['returnType']> {
+ if (this.#closed) {
+ return;
+ }
+ try {
+ return await this.#client.send(method, ...params);
+ } catch (err) {
+ if (this.#closed) {
+ return;
+ }
+ throw err;
+ }
+ }
+
+ close() {
+ this.#client.off('*', this.#forwardMessage as Handler<any>);
+ this.#closed = true;
+ }
+}
+
+/**
+ * This transport is given to the BiDi server instance and allows Puppeteer
+ * to send and receive commands to the BiDiServer.
+ * @internal
+ */
+class NoOpTransport
+ extends BidiMapper.EventEmitter<any>
+ implements BidiMapper.BidiTransport
+{
+ #onMessage: (
+ message: Bidi.Message.RawCommandRequest
+ ) => Promise<void> | void = async (
+ _m: Bidi.Message.RawCommandRequest
+ ): Promise<void> => {
+ return;
+ };
+
+ emitMessage(message: Bidi.Message.RawCommandRequest) {
+ void this.#onMessage(message);
+ }
+
+ setOnMessage(
+ onMessage: (message: Bidi.Message.RawCommandRequest) => Promise<void> | void
+ ): void {
+ this.#onMessage = onMessage;
+ }
+
+ async sendMessage(message: Bidi.Message.OutgoingMessage): Promise<void> {
+ this.emit('bidiResponse', message);
+ }
+
+ close() {
+ this.#onMessage = async (
+ _m: Bidi.Message.RawCommandRequest
+ ): Promise<void> => {
+ return;
+ };
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Browser.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Browser.ts
new file mode 100644
index 0000000000..9741ce7129
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Browser.ts
@@ -0,0 +1,89 @@
+/**
+ * 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 {ChildProcess} from 'child_process';
+
+import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import {
+ Browser as BrowserBase,
+ BrowserCloseCallback,
+ BrowserContextOptions,
+} from '../../api/Browser.js';
+import {BrowserContext as BrowserContextBase} from '../../api/BrowserContext.js';
+import {Viewport} from '../PuppeteerViewport.js';
+
+import {BrowserContext} from './BrowserContext.js';
+import {Connection} from './Connection.js';
+
+/**
+ * @internal
+ */
+export class Browser extends BrowserBase {
+ static async create(opts: Options): Promise<Browser> {
+ // TODO: await until the connection is established.
+ try {
+ await opts.connection.send('session.new', {});
+ } catch {}
+ await opts.connection.send('session.subscribe', {
+ events: [
+ 'browsingContext.contextCreated',
+ ] as Bidi.Session.SubscribeParametersEvent[],
+ });
+ return new Browser(opts);
+ }
+
+ #process?: ChildProcess;
+ #closeCallback?: BrowserCloseCallback;
+ #connection: Connection;
+ #defaultViewport: Viewport | null;
+
+ constructor(opts: Options) {
+ super();
+ this.#process = opts.process;
+ this.#closeCallback = opts.closeCallback;
+ this.#connection = opts.connection;
+ this.#defaultViewport = opts.defaultViewport;
+ }
+
+ override async close(): Promise<void> {
+ this.#connection.dispose();
+ await this.#closeCallback?.call(null);
+ }
+
+ override isConnected(): boolean {
+ return !this.#connection.closed;
+ }
+
+ override process(): ChildProcess | null {
+ return this.#process ?? null;
+ }
+
+ override async createIncognitoBrowserContext(
+ _options?: BrowserContextOptions
+ ): Promise<BrowserContextBase> {
+ return new BrowserContext(this.#connection, {
+ defaultViewport: this.#defaultViewport,
+ });
+ }
+}
+
+interface Options {
+ process?: ChildProcess;
+ closeCallback?: BrowserCloseCallback;
+ connection: Connection;
+ defaultViewport: Viewport | null;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/BrowserContext.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/BrowserContext.ts
new file mode 100644
index 0000000000..92950b87b0
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/BrowserContext.ts
@@ -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 {BrowserContext as BrowserContextBase} from '../../api/BrowserContext.js';
+import {Page as PageBase} from '../../api/Page.js';
+import {Viewport} from '../PuppeteerViewport.js';
+
+import {Connection} from './Connection.js';
+import {Context} from './Context.js';
+import {Page} from './Page.js';
+
+interface BrowserContextOptions {
+ defaultViewport: Viewport | null;
+}
+
+/**
+ * @internal
+ */
+export class BrowserContext extends BrowserContextBase {
+ #connection: Connection;
+ #defaultViewport: Viewport | null;
+
+ constructor(connection: Connection, options: BrowserContextOptions) {
+ super();
+ this.#connection = connection;
+ this.#defaultViewport = options.defaultViewport;
+ }
+
+ override async newPage(): Promise<PageBase> {
+ const {result} = await this.#connection.send('browsingContext.create', {
+ type: 'tab',
+ });
+ const context = this.#connection.context(result.context) as Context;
+ const page = new Page(context);
+ if (this.#defaultViewport) {
+ try {
+ await page.setViewport(this.#defaultViewport);
+ } catch {
+ // No support for setViewport in Firefox.
+ }
+ }
+ return page;
+ }
+
+ override async close(): Promise<void> {}
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Connection.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Connection.ts
new file mode 100644
index 0000000000..5f26ee00fb
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Connection.ts
@@ -0,0 +1,215 @@
+/**
+ * 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 * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import {CallbackRegistry} from '../Connection.js';
+import {ConnectionTransport} from '../ConnectionTransport.js';
+import {debug} from '../Debug.js';
+import {EventEmitter} from '../EventEmitter.js';
+
+import {Context} from './Context.js';
+
+const debugProtocolSend = debug('puppeteer:webDriverBiDi:SEND ►');
+const debugProtocolReceive = debug('puppeteer:webDriverBiDi:RECV ◀');
+
+/**
+ * @internal
+ */
+interface Commands {
+ 'script.evaluate': {
+ params: Bidi.Script.EvaluateParameters;
+ returnType: Bidi.Script.EvaluateResult;
+ };
+ 'script.callFunction': {
+ params: Bidi.Script.CallFunctionParameters;
+ returnType: Bidi.Script.CallFunctionResult;
+ };
+ 'script.disown': {
+ params: Bidi.Script.DisownParameters;
+ returnType: Bidi.Script.DisownResult;
+ };
+
+ 'browsingContext.create': {
+ params: Bidi.BrowsingContext.CreateParameters;
+ returnType: Bidi.BrowsingContext.CreateResult;
+ };
+ 'browsingContext.close': {
+ params: Bidi.BrowsingContext.CloseParameters;
+ returnType: Bidi.BrowsingContext.CloseResult;
+ };
+ 'browsingContext.navigate': {
+ params: Bidi.BrowsingContext.NavigateParameters;
+ returnType: Bidi.BrowsingContext.NavigateResult;
+ };
+ 'browsingContext.print': {
+ params: Bidi.BrowsingContext.PrintParameters;
+ returnType: Bidi.BrowsingContext.PrintResult;
+ };
+ 'browsingContext.captureScreenshot': {
+ params: Bidi.BrowsingContext.CaptureScreenshotParameters;
+ returnType: Bidi.BrowsingContext.CaptureScreenshotResult;
+ };
+
+ 'session.new': {
+ params: {capabilities?: Record<any, unknown>}; // TODO: Update Types in chromium bidi
+ returnType: {sessionId: string};
+ };
+ 'session.status': {
+ params: object;
+ returnType: Bidi.Session.StatusResult;
+ };
+ 'session.subscribe': {
+ params: Bidi.Session.SubscribeParameters;
+ returnType: Bidi.Session.SubscribeResult;
+ };
+ 'session.unsubscribe': {
+ params: Bidi.Session.SubscribeParameters;
+ returnType: Bidi.Session.UnsubscribeResult;
+ };
+ 'cdp.sendCommand': {
+ params: Bidi.CDP.SendCommandParams;
+ returnType: Bidi.CDP.SendCommandResult;
+ };
+ 'cdp.getSession': {
+ params: Bidi.CDP.GetSessionParams;
+ returnType: Bidi.CDP.GetSessionResult;
+ };
+}
+
+/**
+ * @internal
+ */
+export class Connection extends EventEmitter {
+ #transport: ConnectionTransport;
+ #delay: number;
+ #timeout? = 0;
+ #closed = false;
+ #callbacks = new CallbackRegistry();
+ #contexts: Map<string, Context> = new Map();
+
+ constructor(transport: ConnectionTransport, delay = 0, timeout?: number) {
+ super();
+ this.#delay = delay;
+ this.#timeout = timeout ?? 180_000;
+
+ this.#transport = transport;
+ this.#transport.onmessage = this.onMessage.bind(this);
+ this.#transport.onclose = this.#onClose.bind(this);
+ }
+
+ get closed(): boolean {
+ return this.#closed;
+ }
+
+ context(contextId: string): Context | null {
+ return this.#contexts.get(contextId) || null;
+ }
+
+ send<T extends keyof Commands>(
+ method: T,
+ params: Commands[T]['params']
+ ): Promise<Commands[T]['returnType']> {
+ return this.#callbacks.create(method, this.#timeout, id => {
+ const stringifiedMessage = JSON.stringify({
+ id,
+ method,
+ params,
+ } as Bidi.Message.CommandRequest);
+ debugProtocolSend(stringifiedMessage);
+ this.#transport.send(stringifiedMessage);
+ }) as Promise<Commands[T]['returnType']>;
+ }
+
+ /**
+ * @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
+ | Bidi.Message.CommandResponse
+ | Bidi.Message.EventMessage;
+
+ if ('id' in object) {
+ if ('error' in object) {
+ this.#callbacks.reject(
+ object.id,
+ createProtocolError(object),
+ object.message
+ );
+ } else {
+ this.#callbacks.resolve(object.id, object);
+ }
+ } else {
+ this.#handleSpecialEvents(object);
+ this.#maybeEmitOnContext(object);
+ this.emit(object.method, object.params);
+ }
+ }
+
+ #maybeEmitOnContext(event: Bidi.Message.EventMessage) {
+ let context: Context | undefined;
+ // Context specific events
+ if ('context' in event.params && event.params.context) {
+ context = this.#contexts.get(event.params.context);
+ // `log.entryAdded` specific context
+ } else if ('source' in event.params && event.params.source.context) {
+ context = this.#contexts.get(event.params.source.context);
+ }
+ context?.emit(event.method, event.params);
+ }
+
+ #handleSpecialEvents(event: Bidi.Message.EventMessage) {
+ switch (event.method) {
+ case 'browsingContext.contextCreated':
+ this.#contexts.set(
+ event.params.context,
+ new Context(this, event.params)
+ );
+ }
+ }
+
+ #onClose(): void {
+ if (this.#closed) {
+ return;
+ }
+ this.#closed = true;
+ this.#transport.onmessage = undefined;
+ this.#transport.onclose = undefined;
+ this.#callbacks.clear();
+ }
+
+ dispose(): void {
+ this.#onClose();
+ this.#transport.close();
+ }
+}
+
+/**
+ * @internal
+ */
+function createProtocolError(object: Bidi.Message.ErrorResult): string {
+ let message = `${object.error} ${object.message}`;
+ if (object.stacktrace) {
+ message += ` ${object.stacktrace}`;
+ }
+ return message;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Context.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Context.ts
new file mode 100644
index 0000000000..4d3711d6aa
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Context.ts
@@ -0,0 +1,282 @@
+/**
+ * Copyright 2023 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 * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import {HTTPResponse} from '../../api/HTTPResponse.js';
+import {WaitForOptions} from '../../api/Page.js';
+import {assert} from '../../util/assert.js';
+import {stringifyFunction} from '../../util/Function.js';
+import {ProtocolMapping} from '../Connection.js';
+import {ProtocolError, TimeoutError} from '../Errors.js';
+import {EventEmitter} from '../EventEmitter.js';
+import {PuppeteerLifeCycleEvent} from '../LifecycleWatcher.js';
+import {TimeoutSettings} from '../TimeoutSettings.js';
+import {EvaluateFunc, HandleFor} from '../types.js';
+import {isString, setPageContent, waitWithTimeout} from '../util.js';
+
+import {Connection} from './Connection.js';
+import {ElementHandle} from './ElementHandle.js';
+import {JSHandle} from './JSHandle.js';
+import {BidiSerializer} from './Serializer.js';
+
+/**
+ * @internal
+ */
+const lifeCycleToReadinessState = new Map<
+ PuppeteerLifeCycleEvent,
+ Bidi.BrowsingContext.ReadinessState
+>([
+ ['load', 'complete'],
+ ['domcontentloaded', 'interactive'],
+]);
+
+/**
+ * @internal
+ */
+const lifeCycleToSubscribedEvent = new Map<PuppeteerLifeCycleEvent, string>([
+ ['load', 'browsingContext.load'],
+ ['domcontentloaded', 'browsingContext.domContentLoaded'],
+]);
+
+/**
+ * @internal
+ */
+export class Context extends EventEmitter {
+ #connection: Connection;
+ #url: string;
+ _contextId: string;
+ _timeoutSettings = new TimeoutSettings();
+
+ constructor(connection: Connection, result: Bidi.BrowsingContext.Info) {
+ super();
+ this.#connection = connection;
+ this._contextId = result.context;
+ this.#url = result.url;
+ }
+
+ get connection(): Connection {
+ return this.#connection;
+ }
+
+ get id(): string {
+ return this._contextId;
+ }
+
+ async evaluateHandle<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
+ >(
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
+ return this.#evaluate(false, pageFunction, ...args);
+ }
+
+ async evaluate<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
+ >(
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<Awaited<ReturnType<Func>>> {
+ return this.#evaluate(true, pageFunction, ...args);
+ }
+
+ async #evaluate<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
+ >(
+ returnByValue: true,
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<Awaited<ReturnType<Func>>>;
+ async #evaluate<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
+ >(
+ returnByValue: false,
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<HandleFor<Awaited<ReturnType<Func>>>>;
+ async #evaluate<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
+ >(
+ returnByValue: boolean,
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<HandleFor<Awaited<ReturnType<Func>>> | Awaited<ReturnType<Func>>> {
+ let responsePromise;
+ const resultOwnership = returnByValue ? 'none' : 'root';
+ if (isString(pageFunction)) {
+ responsePromise = this.#connection.send('script.evaluate', {
+ expression: pageFunction,
+ target: {context: this._contextId},
+ resultOwnership,
+ awaitPromise: true,
+ });
+ } else {
+ responsePromise = this.#connection.send('script.callFunction', {
+ functionDeclaration: stringifyFunction(pageFunction),
+ arguments: await Promise.all(
+ args.map(arg => {
+ return BidiSerializer.serialize(arg, this);
+ })
+ ),
+ target: {context: this._contextId},
+ resultOwnership,
+ awaitPromise: true,
+ });
+ }
+
+ const {result} = await responsePromise;
+
+ if ('type' in result && result.type === 'exception') {
+ throw new Error(result.exceptionDetails.text);
+ }
+
+ return returnByValue
+ ? BidiSerializer.deserialize(result.result)
+ : getBidiHandle(this, result.result);
+ }
+
+ async goto(
+ url: string,
+ options: WaitForOptions & {
+ referer?: string | undefined;
+ referrerPolicy?: string | undefined;
+ } = {}
+ ): Promise<HTTPResponse | null> {
+ const {
+ waitUntil = 'load',
+ timeout = this._timeoutSettings.navigationTimeout(),
+ } = options;
+
+ const readinessState = lifeCycleToReadinessState.get(
+ getWaitUntilSingle(waitUntil)
+ ) as Bidi.BrowsingContext.ReadinessState;
+
+ try {
+ const response = await waitWithTimeout(
+ this.connection.send('browsingContext.navigate', {
+ url: url,
+ context: this.id,
+ wait: readinessState,
+ }),
+ 'Navigation',
+ timeout
+ );
+ this.#url = response.result.url;
+
+ return null;
+ } catch (error) {
+ if (error instanceof ProtocolError) {
+ error.message += ` at ${url}`;
+ } else if (error instanceof TimeoutError) {
+ error.message = 'Navigation timeout of ' + timeout + ' ms exceeded';
+ }
+ throw error;
+ }
+ }
+
+ url(): string {
+ return this.#url;
+ }
+
+ async setContent(
+ html: string,
+ options: WaitForOptions | undefined = {}
+ ): Promise<void> {
+ const {
+ waitUntil = 'load',
+ timeout = this._timeoutSettings.navigationTimeout(),
+ } = options;
+
+ const waitUntilCommand = lifeCycleToSubscribedEvent.get(
+ getWaitUntilSingle(waitUntil)
+ ) as string;
+
+ await Promise.all([
+ setPageContent(this, html),
+ waitWithTimeout(
+ new Promise<void>(resolve => {
+ this.once(waitUntilCommand, () => {
+ resolve();
+ });
+ }),
+ waitUntilCommand,
+ timeout
+ ),
+ ]);
+ }
+
+ async sendCDPCommand(
+ method: keyof ProtocolMapping.Commands,
+ params: object = {}
+ ): Promise<unknown> {
+ const session = await this.#connection.send('cdp.getSession', {
+ context: this._contextId,
+ });
+ // TODO: remove any once chromium-bidi types are updated.
+ const sessionId = (session.result as any).cdpSession;
+ return await this.#connection.send('cdp.sendCommand', {
+ cdpMethod: method,
+ cdpParams: params,
+ cdpSession: sessionId,
+ });
+ }
+}
+
+/**
+ * @internal
+ */
+function getWaitUntilSingle(
+ event: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]
+): Extract<PuppeteerLifeCycleEvent, 'load' | 'domcontentloaded'> {
+ if (Array.isArray(event) && event.length > 1) {
+ throw new Error('BiDi support only single `waitUntil` argument');
+ }
+ const waitUntilSingle = Array.isArray(event)
+ ? (event.find(lifecycle => {
+ return lifecycle === 'domcontentloaded' || lifecycle === 'load';
+ }) as PuppeteerLifeCycleEvent)
+ : event;
+
+ if (
+ waitUntilSingle === 'networkidle0' ||
+ waitUntilSingle === 'networkidle2'
+ ) {
+ throw new Error(`BiDi does not support 'waitUntil' ${waitUntilSingle}`);
+ }
+
+ assert(waitUntilSingle, `Invalid waitUntil option ${waitUntilSingle}`);
+
+ return waitUntilSingle;
+}
+
+/**
+ * @internal
+ */
+export function getBidiHandle(
+ context: Context,
+ result: Bidi.CommonDataTypes.RemoteValue
+): JSHandle | ElementHandle<Node> {
+ if (result.type === 'node' || result.type === 'window') {
+ return new ElementHandle(context, result);
+ }
+ return new JSHandle(context, result);
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/ElementHandle.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/ElementHandle.ts
new file mode 100644
index 0000000000..21e69e3e9b
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/ElementHandle.ts
@@ -0,0 +1,52 @@
+/**
+ * Copyright 2023 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 * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import {ElementHandle as BaseElementHandle} from '../../api/ElementHandle.js';
+
+import {Connection} from './Connection.js';
+import {Context} from './Context.js';
+import {JSHandle} from './JSHandle.js';
+
+/**
+ * @internal
+ */
+export class ElementHandle<
+ ElementType extends Node = Element
+> extends BaseElementHandle<ElementType> {
+ declare handle: JSHandle<ElementType>;
+
+ constructor(context: Context, remoteValue: Bidi.CommonDataTypes.RemoteValue) {
+ super(new JSHandle(context, remoteValue));
+ }
+
+ context(): Context {
+ return this.handle.context();
+ }
+
+ get connection(): Connection {
+ return this.handle.connection;
+ }
+
+ get isPrimitiveValue(): boolean {
+ return this.handle.isPrimitiveValue;
+ }
+
+ remoteValue(): Bidi.CommonDataTypes.RemoteValue {
+ return this.handle.remoteValue();
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/JSHandle.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/JSHandle.ts
new file mode 100644
index 0000000000..2cd2876622
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/JSHandle.ts
@@ -0,0 +1,159 @@
+/**
+ * Copyright 2023 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 * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import {ElementHandle} from '../../api/ElementHandle.js';
+import {JSHandle as BaseJSHandle} from '../../api/JSHandle.js';
+import {EvaluateFuncWith, HandleFor, HandleOr} from '../../common/types.js';
+
+import {Connection} from './Connection.js';
+import {Context} from './Context.js';
+import {BidiSerializer} from './Serializer.js';
+import {releaseReference} from './utils.js';
+
+export class JSHandle<T = unknown> extends BaseJSHandle<T> {
+ #disposed = false;
+ #context;
+ #remoteValue;
+
+ constructor(context: Context, remoteValue: Bidi.CommonDataTypes.RemoteValue) {
+ super();
+ this.#context = context;
+ this.#remoteValue = remoteValue;
+ }
+
+ context(): Context {
+ return this.#context;
+ }
+
+ get connection(): Connection {
+ return this.#context.connection;
+ }
+
+ override get disposed(): boolean {
+ return this.#disposed;
+ }
+
+ override async evaluate<
+ Params extends unknown[],
+ Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params>
+ >(
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<Awaited<ReturnType<Func>>> {
+ return await this.context().evaluate(pageFunction, this, ...args);
+ }
+
+ override async evaluateHandle<
+ Params extends unknown[],
+ Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params>
+ >(
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
+ return await this.context().evaluateHandle(pageFunction, this, ...args);
+ }
+
+ override async getProperty<K extends keyof T>(
+ propertyName: HandleOr<K>
+ ): Promise<HandleFor<T[K]>>;
+ override async getProperty(propertyName: string): Promise<HandleFor<unknown>>;
+ override async getProperty<K extends keyof T>(
+ propertyName: HandleOr<K>
+ ): Promise<HandleFor<T[K]>> {
+ return await this.evaluateHandle((object, propertyName) => {
+ return object[propertyName as K];
+ }, propertyName);
+ }
+
+ override async getProperties(): Promise<Map<string, BaseJSHandle>> {
+ // TODO(lightning00blade): Either include return of depth Handles in RemoteValue
+ // or new BiDi command that returns array of remote value
+ const keys = await this.evaluate(object => {
+ return Object.getOwnPropertyNames(object);
+ });
+ const map: Map<string, BaseJSHandle> = new Map();
+ const results = await Promise.all(
+ keys.map(key => {
+ return this.getProperty(key);
+ })
+ );
+
+ for (const [key, value] of Object.entries(keys)) {
+ const handle = results[key as any];
+ if (handle) {
+ map.set(value, handle);
+ }
+ }
+
+ return map;
+ }
+
+ override async jsonValue(): Promise<T> {
+ const value = BidiSerializer.deserialize(this.#remoteValue);
+
+ if (this.#remoteValue.type !== 'undefined' && value === undefined) {
+ throw new Error('Could not serialize referenced object');
+ }
+ return value;
+ }
+
+ override asElement(): ElementHandle<Node> | null {
+ return null;
+ }
+
+ override async dispose(): Promise<void> {
+ if (this.#disposed) {
+ return;
+ }
+ this.#disposed = true;
+ if ('handle' in this.#remoteValue) {
+ await releaseReference(this.#context, this.#remoteValue);
+ }
+ }
+
+ get isPrimitiveValue(): boolean {
+ switch (this.#remoteValue.type) {
+ case 'string':
+ case 'number':
+ case 'bigint':
+ case 'boolean':
+ case 'undefined':
+ case 'null':
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ override toString(): string {
+ if (this.isPrimitiveValue) {
+ return 'JSHandle:' + BidiSerializer.deserialize(this.#remoteValue);
+ }
+
+ return 'JSHandle@' + this.#remoteValue.type;
+ }
+
+ override get id(): string | undefined {
+ return 'handle' in this.#remoteValue ? this.#remoteValue.handle : undefined;
+ }
+
+ remoteValue(): Bidi.CommonDataTypes.RemoteValue {
+ return this.#remoteValue;
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Page.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Page.ts
new file mode 100644
index 0000000000..524f5ed122
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Page.ts
@@ -0,0 +1,345 @@
+/**
+ * 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 type {Readable} from 'stream';
+
+import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import {HTTPResponse} from '../../api/HTTPResponse.js';
+import {
+ Page as PageBase,
+ PageEmittedEvents,
+ ScreenshotOptions,
+ WaitForOptions,
+} from '../../api/Page.js';
+import {isErrorLike} from '../../util/ErrorLike.js';
+import {ConsoleMessage, ConsoleMessageLocation} from '../ConsoleMessage.js';
+import {Handler} from '../EventEmitter.js';
+import {PDFOptions} from '../PDFOptions.js';
+import {Viewport} from '../PuppeteerViewport.js';
+import {EvaluateFunc, HandleFor} from '../types.js';
+import {debugError, waitWithTimeout} from '../util.js';
+
+import {Context, getBidiHandle} from './Context.js';
+import {BidiSerializer} from './Serializer.js';
+
+/**
+ * @internal
+ */
+export class Page extends PageBase {
+ #context: Context;
+ #subscribedEvents = new Map<string, Handler<any>>([
+ ['log.entryAdded', this.#onLogEntryAdded.bind(this)],
+ ['browsingContext.load', this.#onLoad.bind(this)],
+ ['browsingContext.domContentLoaded', this.#onDOMLoad.bind(this)],
+ ]) as Map<Bidi.Session.SubscribeParametersEvent, Handler>;
+ #viewport: Viewport | null = null;
+
+ constructor(context: Context) {
+ super();
+ this.#context = context;
+
+ this.#context.connection
+ .send('session.subscribe', {
+ events: [
+ ...this.#subscribedEvents.keys(),
+ ] as Bidi.Session.SubscribeParameters['events'],
+ contexts: [this.#context.id],
+ })
+ .catch(error => {
+ if (isErrorLike(error) && !error.message.includes('Target closed')) {
+ throw error;
+ }
+ });
+
+ for (const [event, subscriber] of this.#subscribedEvents) {
+ this.#context.on(event, subscriber);
+ }
+ }
+
+ #onLogEntryAdded(event: Bidi.Log.LogEntry): void {
+ if (isConsoleLogEntry(event)) {
+ const args = event.args.map(arg => {
+ return getBidiHandle(this.#context, arg);
+ });
+
+ const text = args
+ .reduce((value, arg) => {
+ const parsedValue = arg.isPrimitiveValue
+ ? BidiSerializer.deserialize(arg.remoteValue())
+ : arg.toString();
+ return `${value} ${parsedValue}`;
+ }, '')
+ .slice(1);
+
+ this.emit(
+ PageEmittedEvents.Console,
+ new ConsoleMessage(
+ event.method as any,
+ text,
+ args,
+ getStackTraceLocations(event.stackTrace)
+ )
+ );
+ } else if (isJavaScriptLogEntry(event)) {
+ let message = event.text ?? '';
+
+ if (event.stackTrace) {
+ for (const callFrame of event.stackTrace.callFrames) {
+ const location =
+ callFrame.url +
+ ':' +
+ callFrame.lineNumber +
+ ':' +
+ callFrame.columnNumber;
+ const functionName = callFrame.functionName || '<anonymous>';
+ message += `\n at ${functionName} (${location})`;
+ }
+ }
+
+ const error = new Error(message);
+ error.stack = ''; // Don't capture Puppeteer stacktrace.
+
+ this.emit(PageEmittedEvents.PageError, error);
+ } else {
+ debugError(
+ `Unhandled LogEntry with type "${event.type}", text "${event.text}" and level "${event.level}"`
+ );
+ }
+ }
+
+ #onLoad(_event: Bidi.BrowsingContext.NavigationInfo): void {
+ this.emit(PageEmittedEvents.Load);
+ }
+
+ #onDOMLoad(_event: Bidi.BrowsingContext.NavigationInfo): void {
+ this.emit(PageEmittedEvents.DOMContentLoaded);
+ }
+
+ override async close(): Promise<void> {
+ await this.#context.connection.send('session.unsubscribe', {
+ events: [...this.#subscribedEvents.keys()],
+ contexts: [this.#context.id],
+ });
+
+ await this.#context.connection.send('browsingContext.close', {
+ context: this.#context.id,
+ });
+
+ for (const [event, subscriber] of this.#subscribedEvents) {
+ this.#context.off(event, subscriber);
+ }
+ }
+
+ override async evaluateHandle<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
+ >(
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
+ return this.#context.evaluateHandle(pageFunction, ...args);
+ }
+
+ override async evaluate<
+ Params extends unknown[],
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
+ >(
+ pageFunction: Func | string,
+ ...args: Params
+ ): Promise<Awaited<ReturnType<Func>>> {
+ return this.#context.evaluate(pageFunction, ...args);
+ }
+
+ override async goto(
+ url: string,
+ options?: WaitForOptions & {
+ referer?: string | undefined;
+ referrerPolicy?: string | undefined;
+ }
+ ): Promise<HTTPResponse | null> {
+ return this.#context.goto(url, options);
+ }
+
+ override url(): string {
+ return this.#context.url();
+ }
+
+ override setDefaultNavigationTimeout(timeout: number): void {
+ this.#context._timeoutSettings.setDefaultNavigationTimeout(timeout);
+ }
+
+ override setDefaultTimeout(timeout: number): void {
+ this.#context._timeoutSettings.setDefaultTimeout(timeout);
+ }
+
+ override async setContent(
+ html: string,
+ options: WaitForOptions = {}
+ ): Promise<void> {
+ await this.#context.setContent(html, options);
+ }
+
+ override async content(): Promise<string> {
+ return await this.evaluate(() => {
+ let retVal = '';
+ if (document.doctype) {
+ retVal = new XMLSerializer().serializeToString(document.doctype);
+ }
+ if (document.documentElement) {
+ retVal += document.documentElement.outerHTML;
+ }
+ return retVal;
+ });
+ }
+
+ override async setViewport(viewport: Viewport): Promise<void> {
+ // TODO: use BiDi commands when available.
+ const mobile = false;
+ const width = viewport.width;
+ const height = viewport.height;
+ const deviceScaleFactor = 1;
+ const screenOrientation = {angle: 0, type: 'portraitPrimary'};
+
+ await this.#context.sendCDPCommand('Emulation.setDeviceMetricsOverride', {
+ mobile,
+ width,
+ height,
+ deviceScaleFactor,
+ screenOrientation,
+ });
+
+ this.#viewport = viewport;
+ }
+
+ override viewport(): Viewport | null {
+ return this.#viewport;
+ }
+
+ override async pdf(options: PDFOptions = {}): Promise<Buffer> {
+ const {path = undefined} = options;
+ const {
+ printBackground: background,
+ margin,
+ landscape,
+ width,
+ height,
+ pageRanges,
+ scale,
+ preferCSSPageSize,
+ timeout,
+ } = this._getPDFOptions(options, 'cm');
+ const {result} = await waitWithTimeout(
+ this.#context.connection.send('browsingContext.print', {
+ context: this.#context._contextId,
+ background,
+ margin,
+ orientation: landscape ? 'landscape' : 'portrait',
+ page: {
+ width,
+ height,
+ },
+ pageRanges: pageRanges.split(', '),
+ scale,
+ shrinkToFit: !preferCSSPageSize,
+ }),
+ 'browsingContext.print',
+ timeout
+ );
+
+ const buffer = Buffer.from(result.data, 'base64');
+
+ await this._maybeWriteBufferToFile(path, buffer);
+
+ return buffer;
+ }
+
+ override async createPDFStream(
+ options?: PDFOptions | undefined
+ ): Promise<Readable> {
+ const buffer = await this.pdf(options);
+ try {
+ const {Readable} = await import('stream');
+ return Readable.from(buffer);
+ } catch (error) {
+ if (error instanceof TypeError) {
+ throw new Error(
+ 'Can only pass a file path in a Node-like environment.'
+ );
+ }
+ throw error;
+ }
+ }
+
+ override screenshot(
+ options: ScreenshotOptions & {encoding: 'base64'}
+ ): Promise<string>;
+ override screenshot(
+ options?: ScreenshotOptions & {encoding?: 'binary'}
+ ): never;
+ override async screenshot(
+ options: ScreenshotOptions = {}
+ ): Promise<Buffer | string> {
+ const {path = undefined, encoding, ...args} = options;
+ if (Object.keys(args).length >= 1) {
+ throw new Error('BiDi only supports "encoding" and "path" options');
+ }
+
+ const {result} = await this.#context.connection.send(
+ 'browsingContext.captureScreenshot',
+ {
+ context: this.#context._contextId,
+ }
+ );
+
+ if (encoding === 'base64') {
+ return result.data;
+ }
+
+ const buffer = Buffer.from(result.data, 'base64');
+ await this._maybeWriteBufferToFile(path, buffer);
+
+ return buffer;
+ }
+}
+
+function isConsoleLogEntry(
+ event: Bidi.Log.LogEntry
+): event is Bidi.Log.ConsoleLogEntry {
+ return event.type === 'console';
+}
+
+function isJavaScriptLogEntry(
+ event: Bidi.Log.LogEntry
+): event is Bidi.Log.JavascriptLogEntry {
+ return event.type === 'javascript';
+}
+
+function getStackTraceLocations(
+ stackTrace?: Bidi.Script.StackTrace
+): ConsoleMessageLocation[] {
+ const stackTraceLocations: ConsoleMessageLocation[] = [];
+ if (stackTrace) {
+ for (const callFrame of stackTrace.callFrames) {
+ stackTraceLocations.push({
+ url: callFrame.url,
+ lineNumber: callFrame.lineNumber,
+ columnNumber: callFrame.columnNumber,
+ });
+ }
+ }
+ return stackTraceLocations;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Serializer.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Serializer.ts
new file mode 100644
index 0000000000..f28b0e7318
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Serializer.ts
@@ -0,0 +1,273 @@
+/**
+ * Copyright 2023 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 * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import {debugError, isDate, isPlainObject, isRegExp} from '../util.js';
+
+import {Context} from './Context.js';
+import {ElementHandle} from './ElementHandle.js';
+import {JSHandle} from './JSHandle.js';
+
+/**
+ * @internal
+ */
+class UnserializableError extends Error {}
+
+/**
+ * @internal
+ */
+export class BidiSerializer {
+ static serializeNumber(arg: number): Bidi.CommonDataTypes.LocalOrRemoteValue {
+ let value: Bidi.CommonDataTypes.SpecialNumber | number;
+ if (Object.is(arg, -0)) {
+ value = '-0';
+ } else if (Object.is(arg, Infinity)) {
+ value = 'Infinity';
+ } else if (Object.is(arg, -Infinity)) {
+ value = '-Infinity';
+ } else if (Object.is(arg, NaN)) {
+ value = 'NaN';
+ } else {
+ value = arg;
+ }
+ return {
+ type: 'number',
+ value,
+ };
+ }
+
+ static serializeObject(
+ arg: object | null
+ ): Bidi.CommonDataTypes.LocalOrRemoteValue {
+ if (arg === null) {
+ return {
+ type: 'null',
+ };
+ } else if (Array.isArray(arg)) {
+ const parsedArray = arg.map(subArg => {
+ return BidiSerializer.serializeRemoveValue(subArg);
+ });
+
+ return {
+ type: 'array',
+ value: parsedArray,
+ };
+ } else if (isPlainObject(arg)) {
+ try {
+ JSON.stringify(arg);
+ } catch (error) {
+ if (
+ error instanceof TypeError &&
+ error.message.startsWith('Converting circular structure to JSON')
+ ) {
+ error.message += ' Recursive objects are not allowed.';
+ }
+ throw error;
+ }
+
+ const parsedObject: Bidi.CommonDataTypes.MappingLocalValue = [];
+ for (const key in arg) {
+ parsedObject.push([
+ BidiSerializer.serializeRemoveValue(key),
+ BidiSerializer.serializeRemoveValue(arg[key]),
+ ]);
+ }
+
+ return {
+ type: 'object',
+ value: parsedObject,
+ };
+ } else if (isRegExp(arg)) {
+ return {
+ type: 'regexp',
+ value: {
+ pattern: arg.source,
+ flags: arg.flags,
+ },
+ };
+ } else if (isDate(arg)) {
+ return {
+ type: 'date',
+ value: arg.toISOString(),
+ };
+ }
+
+ throw new UnserializableError(
+ 'Custom object sterilization not possible. Use plain objects instead.'
+ );
+ }
+
+ static serializeRemoveValue(
+ arg: unknown
+ ): Bidi.CommonDataTypes.LocalOrRemoteValue {
+ switch (typeof arg) {
+ case 'symbol':
+ case 'function':
+ throw new UnserializableError(`Unable to serializable ${typeof arg}`);
+ case 'object':
+ return BidiSerializer.serializeObject(arg);
+
+ case 'undefined':
+ return {
+ type: 'undefined',
+ };
+ case 'number':
+ return BidiSerializer.serializeNumber(arg);
+ case 'bigint':
+ return {
+ type: 'bigint',
+ value: arg.toString(),
+ };
+ case 'string':
+ return {
+ type: 'string',
+ value: arg,
+ };
+ case 'boolean':
+ return {
+ type: 'boolean',
+ value: arg,
+ };
+ }
+ }
+
+ static serialize(
+ arg: unknown,
+ context: Context
+ ): Bidi.CommonDataTypes.LocalOrRemoteValue {
+ // TODO: See use case of LazyArgs
+ const objectHandle =
+ arg && (arg instanceof JSHandle || arg instanceof ElementHandle)
+ ? arg
+ : null;
+ if (objectHandle) {
+ if (objectHandle.context() !== context) {
+ throw new Error(
+ 'JSHandles can be evaluated only in the context they were created!'
+ );
+ }
+ if (objectHandle.disposed) {
+ throw new Error('JSHandle is disposed!');
+ }
+ return objectHandle.remoteValue();
+ }
+
+ return BidiSerializer.serializeRemoveValue(arg);
+ }
+
+ static deserializeNumber(
+ value: Bidi.CommonDataTypes.SpecialNumber | number
+ ): number {
+ switch (value) {
+ case '-0':
+ return -0;
+ case 'NaN':
+ return NaN;
+ case 'Infinity':
+ case '+Infinity':
+ return Infinity;
+ case '-Infinity':
+ return -Infinity;
+ default:
+ return value;
+ }
+ }
+
+ static deserializeLocalValue(
+ result: Bidi.CommonDataTypes.RemoteValue
+ ): unknown {
+ switch (result.type) {
+ case 'array':
+ // TODO: Check expected output when value is undefined
+ return result.value?.map(value => {
+ return BidiSerializer.deserializeLocalValue(value);
+ });
+ case 'set':
+ // TODO: Check expected output when value is undefined
+ return result.value.reduce((acc: Set<unknown>, value) => {
+ return acc.add(BidiSerializer.deserializeLocalValue(value));
+ }, new Set());
+ case 'object':
+ if (result.value) {
+ return result.value.reduce((acc: Record<any, unknown>, tuple) => {
+ const {key, value} = BidiSerializer.deserializeTuple(tuple);
+ acc[key as any] = value;
+ return acc;
+ }, {});
+ }
+ break;
+ case 'map':
+ return result.value.reduce((acc: Map<unknown, unknown>, tuple) => {
+ const {key, value} = BidiSerializer.deserializeTuple(tuple);
+ return acc.set(key, value);
+ }, new Map());
+ case 'promise':
+ return {};
+ case 'regexp':
+ return new RegExp(result.value.pattern, result.value.flags);
+ case 'date':
+ return new Date(result.value);
+
+ case 'undefined':
+ return undefined;
+ case 'null':
+ return null;
+ case 'number':
+ return BidiSerializer.deserializeNumber(result.value);
+ case 'bigint':
+ return BigInt(result.value);
+ case 'boolean':
+ return Boolean(result.value);
+ case 'string':
+ return result.value;
+ }
+
+ throw new UnserializableError(
+ `Deserialization of type ${result.type} not supported.`
+ );
+ }
+
+ static deserializeTuple([serializedKey, serializedValue]: [
+ Bidi.CommonDataTypes.RemoteValue | string,
+ Bidi.CommonDataTypes.RemoteValue
+ ]): {key: unknown; value: unknown} {
+ const key =
+ typeof serializedKey === 'string'
+ ? serializedKey
+ : BidiSerializer.deserializeLocalValue(serializedKey);
+ const value = BidiSerializer.deserializeLocalValue(serializedValue);
+
+ return {key, value};
+ }
+
+ static deserialize(result: Bidi.CommonDataTypes.RemoteValue): any {
+ if (!result) {
+ debugError('Service did not produce a result.');
+ return undefined;
+ }
+
+ try {
+ return BidiSerializer.deserializeLocalValue(result);
+ } catch (error) {
+ if (error instanceof UnserializableError) {
+ debugError(error.message);
+ return undefined;
+ }
+ throw error;
+ }
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/bidi.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/bidi.ts
new file mode 100644
index 0000000000..c980168aaa
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/bidi.ts
@@ -0,0 +1,21 @@
+/**
+ * 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.
+ */
+
+export * from './Browser.js';
+export * from './BrowserContext.js';
+export * from './Page.js';
+export * from './Connection.js';
+export * from './BidiOverCDP.js';
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/utils.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/utils.ts
new file mode 100644
index 0000000000..ad4a590c5a
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/utils.ts
@@ -0,0 +1,47 @@
+/**
+ * Copyright 2023 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 * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+
+import {debug} from '../Debug.js';
+
+import {Context} from './Context.js';
+
+/**
+ * @internal
+ */
+export const debugError = debug('puppeteer:error');
+/**
+ * @internal
+ */
+export async function releaseReference(
+ client: Context,
+ remoteReference: Bidi.CommonDataTypes.RemoteReference
+): Promise<void> {
+ if (!remoteReference.handle) {
+ return;
+ }
+ await client.connection
+ .send('script.disown', {
+ target: {context: client._contextId},
+ handles: [remoteReference.handle],
+ })
+ .catch((error: any) => {
+ // Exceptions might happen in case of a page been navigated or closed.
+ // Swallow these since they are harmless and we don't leak anything in this case.
+ debugError(error);
+ });
+}