diff options
Diffstat (limited to 'remote/test/puppeteer/packages/puppeteer-core/src/common/bidi')
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); + }); +} |