diff options
Diffstat (limited to 'remote/test/puppeteer/packages/puppeteer-core/src/bidi/Page.ts')
-rw-r--r-- | remote/test/puppeteer/packages/puppeteer-core/src/bidi/Page.ts | 913 |
1 files changed, 913 insertions, 0 deletions
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Page.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Page.ts new file mode 100644 index 0000000000..053d23b63a --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Page.ts @@ -0,0 +1,913 @@ +/** + * @license + * Copyright 2022 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {Readable} from 'stream'; + +import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; +import type Protocol from 'devtools-protocol'; + +import { + firstValueFrom, + from, + map, + raceWith, + zip, +} from '../../third_party/rxjs/rxjs.js'; +import type {CDPSession} from '../api/CDPSession.js'; +import type {BoundingBox} from '../api/ElementHandle.js'; +import type {WaitForOptions} from '../api/Frame.js'; +import type {HTTPResponse} from '../api/HTTPResponse.js'; +import { + Page, + PageEvent, + type GeolocationOptions, + type MediaFeature, + type NewDocumentScriptEvaluation, + type ScreenshotOptions, +} from '../api/Page.js'; +import {Accessibility} from '../cdp/Accessibility.js'; +import {Coverage} from '../cdp/Coverage.js'; +import {EmulationManager as CdpEmulationManager} from '../cdp/EmulationManager.js'; +import {FrameTree} from '../cdp/FrameTree.js'; +import {Tracing} from '../cdp/Tracing.js'; +import { + ConsoleMessage, + type ConsoleMessageLocation, +} from '../common/ConsoleMessage.js'; +import {TargetCloseError, UnsupportedOperation} from '../common/Errors.js'; +import type {Handler} from '../common/EventEmitter.js'; +import {NetworkManagerEvent} from '../common/NetworkManagerEvents.js'; +import type {PDFOptions} from '../common/PDFOptions.js'; +import type {Awaitable} from '../common/types.js'; +import { + debugError, + evaluationString, + NETWORK_IDLE_TIME, + parsePDFOptions, + timeout, + validateDialogType, +} from '../common/util.js'; +import type {Viewport} from '../common/Viewport.js'; +import {assert} from '../util/assert.js'; +import {Deferred} from '../util/Deferred.js'; +import {disposeSymbol} from '../util/disposable.js'; +import {isErrorLike} from '../util/ErrorLike.js'; + +import type {BidiBrowser} from './Browser.js'; +import type {BidiBrowserContext} from './BrowserContext.js'; +import { + BrowsingContextEvent, + CdpSessionWrapper, + type BrowsingContext, +} from './BrowsingContext.js'; +import type {BidiConnection} from './Connection.js'; +import {BidiDeserializer} from './Deserializer.js'; +import {BidiDialog} from './Dialog.js'; +import {BidiElementHandle} from './ElementHandle.js'; +import {EmulationManager} from './EmulationManager.js'; +import {BidiFrame} from './Frame.js'; +import type {BidiHTTPRequest} from './HTTPRequest.js'; +import type {BidiHTTPResponse} from './HTTPResponse.js'; +import {BidiKeyboard, BidiMouse, BidiTouchscreen} from './Input.js'; +import type {BidiJSHandle} from './JSHandle.js'; +import {getBiDiReadinessState, rewriteNavigationError} from './lifecycle.js'; +import {BidiNetworkManager} from './NetworkManager.js'; +import {createBidiHandle} from './Realm.js'; +import type {BiDiPageTarget} from './Target.js'; + +/** + * @internal + */ +export class BidiPage extends Page { + #accessibility: Accessibility; + #connection: BidiConnection; + #frameTree = new FrameTree<BidiFrame>(); + #networkManager: BidiNetworkManager; + #viewport: Viewport | null = null; + #closedDeferred = Deferred.create<never, TargetCloseError>(); + #subscribedEvents = new Map<Bidi.Event['method'], Handler<any>>([ + ['log.entryAdded', this.#onLogEntryAdded.bind(this)], + ['browsingContext.load', this.#onFrameLoaded.bind(this)], + [ + 'browsingContext.fragmentNavigated', + this.#onFrameFragmentNavigated.bind(this), + ], + [ + 'browsingContext.domContentLoaded', + this.#onFrameDOMContentLoaded.bind(this), + ], + ['browsingContext.userPromptOpened', this.#onDialog.bind(this)], + ]); + readonly #networkManagerEvents = [ + [ + NetworkManagerEvent.Request, + (request: BidiHTTPRequest) => { + this.emit(PageEvent.Request, request); + }, + ], + [ + NetworkManagerEvent.RequestServedFromCache, + (request: BidiHTTPRequest) => { + this.emit(PageEvent.RequestServedFromCache, request); + }, + ], + [ + NetworkManagerEvent.RequestFailed, + (request: BidiHTTPRequest) => { + this.emit(PageEvent.RequestFailed, request); + }, + ], + [ + NetworkManagerEvent.RequestFinished, + (request: BidiHTTPRequest) => { + this.emit(PageEvent.RequestFinished, request); + }, + ], + [ + NetworkManagerEvent.Response, + (response: BidiHTTPResponse) => { + this.emit(PageEvent.Response, response); + }, + ], + ] as const; + + readonly #browsingContextEvents = new Map<symbol, Handler<any>>([ + [BrowsingContextEvent.Created, this.#onContextCreated.bind(this)], + [BrowsingContextEvent.Destroyed, this.#onContextDestroyed.bind(this)], + ]); + #tracing: Tracing; + #coverage: Coverage; + #cdpEmulationManager: CdpEmulationManager; + #emulationManager: EmulationManager; + #mouse: BidiMouse; + #touchscreen: BidiTouchscreen; + #keyboard: BidiKeyboard; + #browsingContext: BrowsingContext; + #browserContext: BidiBrowserContext; + #target: BiDiPageTarget; + + _client(): CDPSession { + return this.mainFrame().context().cdpSession; + } + + constructor( + browsingContext: BrowsingContext, + browserContext: BidiBrowserContext, + target: BiDiPageTarget + ) { + super(); + this.#browsingContext = browsingContext; + this.#browserContext = browserContext; + this.#target = target; + this.#connection = browsingContext.connection; + + for (const [event, subscriber] of this.#browsingContextEvents) { + this.#browsingContext.on(event, subscriber); + } + + this.#networkManager = new BidiNetworkManager(this.#connection, this); + + for (const [event, subscriber] of this.#subscribedEvents) { + this.#connection.on(event, subscriber); + } + + for (const [event, subscriber] of this.#networkManagerEvents) { + // TODO: remove any + this.#networkManager.on(event, subscriber as any); + } + + const frame = new BidiFrame( + this, + this.#browsingContext, + this._timeoutSettings, + this.#browsingContext.parent + ); + this.#frameTree.addFrame(frame); + this.emit(PageEvent.FrameAttached, frame); + + // TODO: https://github.com/w3c/webdriver-bidi/issues/443 + this.#accessibility = new Accessibility( + this.mainFrame().context().cdpSession + ); + this.#tracing = new Tracing(this.mainFrame().context().cdpSession); + this.#coverage = new Coverage(this.mainFrame().context().cdpSession); + this.#cdpEmulationManager = new CdpEmulationManager( + this.mainFrame().context().cdpSession + ); + this.#emulationManager = new EmulationManager(browsingContext); + this.#mouse = new BidiMouse(this.mainFrame().context()); + this.#touchscreen = new BidiTouchscreen(this.mainFrame().context()); + this.#keyboard = new BidiKeyboard(this); + } + + /** + * @internal + */ + get connection(): BidiConnection { + return this.#connection; + } + + override async setUserAgent( + userAgent: string, + userAgentMetadata?: Protocol.Emulation.UserAgentMetadata | undefined + ): Promise<void> { + // TODO: handle CDP-specific cases such as mprach. + await this._client().send('Network.setUserAgentOverride', { + userAgent: userAgent, + userAgentMetadata: userAgentMetadata, + }); + } + + override async setBypassCSP(enabled: boolean): Promise<void> { + // TODO: handle CDP-specific cases such as mprach. + await this._client().send('Page.setBypassCSP', {enabled}); + } + + override async queryObjects<Prototype>( + prototypeHandle: BidiJSHandle<Prototype> + ): Promise<BidiJSHandle<Prototype[]>> { + assert(!prototypeHandle.disposed, 'Prototype JSHandle is disposed!'); + assert( + prototypeHandle.id, + 'Prototype JSHandle must not be referencing primitive value' + ); + const response = await this.mainFrame().client.send( + 'Runtime.queryObjects', + { + prototypeObjectId: prototypeHandle.id, + } + ); + return createBidiHandle(this.mainFrame().mainRealm(), { + type: 'array', + handle: response.objects.objectId, + }) as BidiJSHandle<Prototype[]>; + } + + _setBrowserContext(browserContext: BidiBrowserContext): void { + this.#browserContext = browserContext; + } + + override get accessibility(): Accessibility { + return this.#accessibility; + } + + override get tracing(): Tracing { + return this.#tracing; + } + + override get coverage(): Coverage { + return this.#coverage; + } + + override get mouse(): BidiMouse { + return this.#mouse; + } + + override get touchscreen(): BidiTouchscreen { + return this.#touchscreen; + } + + override get keyboard(): BidiKeyboard { + return this.#keyboard; + } + + override browser(): BidiBrowser { + return this.browserContext().browser(); + } + + override browserContext(): BidiBrowserContext { + return this.#browserContext; + } + + override mainFrame(): BidiFrame { + const mainFrame = this.#frameTree.getMainFrame(); + assert(mainFrame, 'Requesting main frame too early!'); + return mainFrame; + } + + /** + * @internal + */ + async focusedFrame(): Promise<BidiFrame> { + using frame = await this.mainFrame() + .isolatedRealm() + .evaluateHandle(() => { + let frame: HTMLIFrameElement | undefined; + let win: Window | null = window; + while (win?.document.activeElement instanceof HTMLIFrameElement) { + frame = win.document.activeElement; + win = frame.contentWindow; + } + return frame; + }); + if (!(frame instanceof BidiElementHandle)) { + return this.mainFrame(); + } + return await frame.contentFrame(); + } + + override frames(): BidiFrame[] { + return Array.from(this.#frameTree.frames()); + } + + frame(frameId?: string): BidiFrame | null { + return this.#frameTree.getById(frameId ?? '') || null; + } + + childFrames(frameId: string): BidiFrame[] { + return this.#frameTree.childFrames(frameId); + } + + #onFrameLoaded(info: Bidi.BrowsingContext.NavigationInfo): void { + const frame = this.frame(info.context); + if (frame && this.mainFrame() === frame) { + this.emit(PageEvent.Load, undefined); + } + } + + #onFrameFragmentNavigated(info: Bidi.BrowsingContext.NavigationInfo): void { + const frame = this.frame(info.context); + if (frame) { + this.emit(PageEvent.FrameNavigated, frame); + } + } + + #onFrameDOMContentLoaded(info: Bidi.BrowsingContext.NavigationInfo): void { + const frame = this.frame(info.context); + if (frame) { + frame._hasStartedLoading = true; + if (this.mainFrame() === frame) { + this.emit(PageEvent.DOMContentLoaded, undefined); + } + this.emit(PageEvent.FrameNavigated, frame); + } + } + + #onContextCreated(context: BrowsingContext): void { + if ( + !this.frame(context.id) && + (this.frame(context.parent ?? '') || !this.#frameTree.getMainFrame()) + ) { + const frame = new BidiFrame( + this, + context, + this._timeoutSettings, + context.parent + ); + this.#frameTree.addFrame(frame); + if (frame !== this.mainFrame()) { + this.emit(PageEvent.FrameAttached, frame); + } + } + } + + #onContextDestroyed(context: BrowsingContext): void { + const frame = this.frame(context.id); + + if (frame) { + if (frame === this.mainFrame()) { + this.emit(PageEvent.Close, undefined); + } + this.#removeFramesRecursively(frame); + } + } + + #removeFramesRecursively(frame: BidiFrame): void { + for (const child of frame.childFrames()) { + this.#removeFramesRecursively(child); + } + frame[disposeSymbol](); + this.#networkManager.clearMapAfterFrameDispose(frame); + this.#frameTree.removeFrame(frame); + this.emit(PageEvent.FrameDetached, frame); + } + + #onLogEntryAdded(event: Bidi.Log.Entry): void { + const frame = this.frame(event.source.context); + if (!frame) { + return; + } + if (isConsoleLogEntry(event)) { + const args = event.args.map(arg => { + return createBidiHandle(frame.mainRealm(), arg); + }); + + const text = args + .reduce((value, arg) => { + const parsedValue = arg.isPrimitiveValue + ? BidiDeserializer.deserialize(arg.remoteValue()) + : arg.toString(); + return `${value} ${parsedValue}`; + }, '') + .slice(1); + + this.emit( + PageEvent.Console, + new ConsoleMessage( + event.method as any, + text, + args, + getStackTraceLocations(event.stackTrace) + ) + ); + } else if (isJavaScriptLogEntry(event)) { + const error = new Error(event.text ?? ''); + + const messageHeight = error.message.split('\n').length; + const messageLines = error.stack!.split('\n').splice(0, messageHeight); + + const stackLines = []; + if (event.stackTrace) { + for (const frame of event.stackTrace.callFrames) { + // Note we need to add `1` because the values are 0-indexed. + stackLines.push( + ` at ${frame.functionName || '<anonymous>'} (${frame.url}:${ + frame.lineNumber + 1 + }:${frame.columnNumber + 1})` + ); + if (stackLines.length >= Error.stackTraceLimit) { + break; + } + } + } + + error.stack = [...messageLines, ...stackLines].join('\n'); + this.emit(PageEvent.PageError, error); + } else { + debugError( + `Unhandled LogEntry with type "${event.type}", text "${event.text}" and level "${event.level}"` + ); + } + } + + #onDialog(event: Bidi.BrowsingContext.UserPromptOpenedParameters): void { + const frame = this.frame(event.context); + if (!frame) { + return; + } + const type = validateDialogType(event.type); + + const dialog = new BidiDialog( + frame.context(), + type, + event.message, + event.defaultValue + ); + this.emit(PageEvent.Dialog, dialog); + } + + getNavigationResponse(id?: string | null): BidiHTTPResponse | null { + return this.#networkManager.getNavigationResponse(id); + } + + override isClosed(): boolean { + return this.#closedDeferred.finished(); + } + + override async close(options?: {runBeforeUnload?: boolean}): Promise<void> { + if (this.#closedDeferred.finished()) { + return; + } + + this.#closedDeferred.reject(new TargetCloseError('Page closed!')); + this.#networkManager.dispose(); + + await this.#connection.send('browsingContext.close', { + context: this.mainFrame()._id, + promptUnload: options?.runBeforeUnload ?? false, + }); + + this.emit(PageEvent.Close, undefined); + this.removeAllListeners(); + } + + override async reload( + options: WaitForOptions = {} + ): Promise<BidiHTTPResponse | null> { + const { + waitUntil = 'load', + timeout: ms = this._timeoutSettings.navigationTimeout(), + } = options; + + const [readiness, networkIdle] = getBiDiReadinessState(waitUntil); + + const result$ = zip( + from( + this.#connection.send('browsingContext.reload', { + context: this.mainFrame()._id, + wait: readiness, + }) + ), + ...(networkIdle !== null + ? [ + this.waitForNetworkIdle$({ + timeout: ms, + concurrency: networkIdle === 'networkidle2' ? 2 : 0, + idleTime: NETWORK_IDLE_TIME, + }), + ] + : []) + ).pipe( + map(([{result}]) => { + return result; + }), + raceWith(timeout(ms), from(this.#closedDeferred.valueOrThrow())), + rewriteNavigationError(this.url(), ms) + ); + + const result = await firstValueFrom(result$); + return this.getNavigationResponse(result.navigation); + } + + override setDefaultNavigationTimeout(timeout: number): void { + this._timeoutSettings.setDefaultNavigationTimeout(timeout); + } + + override setDefaultTimeout(timeout: number): void { + this._timeoutSettings.setDefaultTimeout(timeout); + } + + override getDefaultTimeout(): number { + return this._timeoutSettings.timeout(); + } + + override isJavaScriptEnabled(): boolean { + return this.#cdpEmulationManager.javascriptEnabled; + } + + override async setGeolocation(options: GeolocationOptions): Promise<void> { + return await this.#cdpEmulationManager.setGeolocation(options); + } + + override async setJavaScriptEnabled(enabled: boolean): Promise<void> { + return await this.#cdpEmulationManager.setJavaScriptEnabled(enabled); + } + + override async emulateMediaType(type?: string): Promise<void> { + return await this.#cdpEmulationManager.emulateMediaType(type); + } + + override async emulateCPUThrottling(factor: number | null): Promise<void> { + return await this.#cdpEmulationManager.emulateCPUThrottling(factor); + } + + override async emulateMediaFeatures( + features?: MediaFeature[] + ): Promise<void> { + return await this.#cdpEmulationManager.emulateMediaFeatures(features); + } + + override async emulateTimezone(timezoneId?: string): Promise<void> { + return await this.#cdpEmulationManager.emulateTimezone(timezoneId); + } + + override async emulateIdleState(overrides?: { + isUserActive: boolean; + isScreenUnlocked: boolean; + }): Promise<void> { + return await this.#cdpEmulationManager.emulateIdleState(overrides); + } + + override async emulateVisionDeficiency( + type?: Protocol.Emulation.SetEmulatedVisionDeficiencyRequest['type'] + ): Promise<void> { + return await this.#cdpEmulationManager.emulateVisionDeficiency(type); + } + + override async setViewport(viewport: Viewport): Promise<void> { + if (!this.#browsingContext.supportsCdp()) { + await this.#emulationManager.emulateViewport(viewport); + this.#viewport = viewport; + return; + } + const needsReload = + await this.#cdpEmulationManager.emulateViewport(viewport); + this.#viewport = viewport; + if (needsReload) { + await this.reload(); + } + } + + override viewport(): Viewport | null { + return this.#viewport; + } + + override async pdf(options: PDFOptions = {}): Promise<Buffer> { + const {timeout: ms = this._timeoutSettings.timeout(), path = undefined} = + options; + const { + printBackground: background, + margin, + landscape, + width, + height, + pageRanges: ranges, + scale, + preferCSSPageSize, + } = parsePDFOptions(options, 'cm'); + const pageRanges = ranges ? ranges.split(', ') : []; + const {result} = await firstValueFrom( + from( + this.#connection.send('browsingContext.print', { + context: this.mainFrame()._id, + background, + margin, + orientation: landscape ? 'landscape' : 'portrait', + page: { + width, + height, + }, + pageRanges, + scale, + shrinkToFit: !preferCSSPageSize, + }) + ).pipe(raceWith(timeout(ms))) + ); + + 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 async _screenshot( + options: Readonly<ScreenshotOptions> + ): Promise<string> { + const {clip, type, captureBeyondViewport, quality} = options; + if (options.omitBackground !== undefined && options.omitBackground) { + throw new UnsupportedOperation(`BiDi does not support 'omitBackground'.`); + } + if (options.optimizeForSpeed !== undefined && options.optimizeForSpeed) { + throw new UnsupportedOperation( + `BiDi does not support 'optimizeForSpeed'.` + ); + } + if (options.fromSurface !== undefined && !options.fromSurface) { + throw new UnsupportedOperation(`BiDi does not support 'fromSurface'.`); + } + if (clip !== undefined && clip.scale !== undefined && clip.scale !== 1) { + throw new UnsupportedOperation( + `BiDi does not support 'scale' in 'clip'.` + ); + } + + let box: BoundingBox | undefined; + if (clip) { + if (captureBeyondViewport) { + box = clip; + } else { + // The clip is always with respect to the document coordinates, so we + // need to convert this to viewport coordinates when we aren't capturing + // beyond the viewport. + const [pageLeft, pageTop] = await this.evaluate(() => { + if (!window.visualViewport) { + throw new Error('window.visualViewport is not supported.'); + } + return [ + window.visualViewport.pageLeft, + window.visualViewport.pageTop, + ] as const; + }); + box = { + ...clip, + x: clip.x - pageLeft, + y: clip.y - pageTop, + }; + } + } + + const { + result: {data}, + } = await this.#connection.send('browsingContext.captureScreenshot', { + context: this.mainFrame()._id, + origin: captureBeyondViewport ? 'document' : 'viewport', + format: { + type: `image/${type}`, + ...(quality !== undefined ? {quality: quality / 100} : {}), + }, + ...(box ? {clip: {type: 'box', ...box}} : {}), + }); + return data; + } + + override async createCDPSession(): Promise<CDPSession> { + const {sessionId} = await this.mainFrame() + .context() + .cdpSession.send('Target.attachToTarget', { + targetId: this.mainFrame()._id, + flatten: true, + }); + return new CdpSessionWrapper(this.mainFrame().context(), sessionId); + } + + override async bringToFront(): Promise<void> { + await this.#connection.send('browsingContext.activate', { + context: this.mainFrame()._id, + }); + } + + override async evaluateOnNewDocument< + Params extends unknown[], + Func extends (...args: Params) => unknown = (...args: Params) => unknown, + >( + pageFunction: Func | string, + ...args: Params + ): Promise<NewDocumentScriptEvaluation> { + const expression = evaluationExpression(pageFunction, ...args); + const {result} = await this.#connection.send('script.addPreloadScript', { + functionDeclaration: expression, + contexts: [this.mainFrame()._id], + }); + + return {identifier: result.script}; + } + + override async removeScriptToEvaluateOnNewDocument( + id: string + ): Promise<void> { + await this.#connection.send('script.removePreloadScript', { + script: id, + }); + } + + override async exposeFunction<Args extends unknown[], Ret>( + name: string, + pptrFunction: + | ((...args: Args) => Awaitable<Ret>) + | {default: (...args: Args) => Awaitable<Ret>} + ): Promise<void> { + return await this.mainFrame().exposeFunction( + name, + 'default' in pptrFunction ? pptrFunction.default : pptrFunction + ); + } + + override isDragInterceptionEnabled(): boolean { + return false; + } + + override async setCacheEnabled(enabled?: boolean): Promise<void> { + // TODO: handle CDP-specific cases such as mprach. + await this._client().send('Network.setCacheDisabled', { + cacheDisabled: !enabled, + }); + } + + override isServiceWorkerBypassed(): never { + throw new UnsupportedOperation(); + } + + override target(): BiDiPageTarget { + return this.#target; + } + + override waitForFileChooser(): never { + throw new UnsupportedOperation(); + } + + override workers(): never { + throw new UnsupportedOperation(); + } + + override setRequestInterception(): never { + throw new UnsupportedOperation(); + } + + override setDragInterception(): never { + throw new UnsupportedOperation(); + } + + override setBypassServiceWorker(): never { + throw new UnsupportedOperation(); + } + + override setOfflineMode(): never { + throw new UnsupportedOperation(); + } + + override emulateNetworkConditions(): never { + throw new UnsupportedOperation(); + } + + override cookies(): never { + throw new UnsupportedOperation(); + } + + override setCookie(): never { + throw new UnsupportedOperation(); + } + + override deleteCookie(): never { + throw new UnsupportedOperation(); + } + + override removeExposedFunction(): never { + // TODO: Quick win? + throw new UnsupportedOperation(); + } + + override authenticate(): never { + throw new UnsupportedOperation(); + } + + override setExtraHTTPHeaders(): never { + throw new UnsupportedOperation(); + } + + override metrics(): never { + throw new UnsupportedOperation(); + } + + override async goBack( + options: WaitForOptions = {} + ): Promise<HTTPResponse | null> { + return await this.#go(-1, options); + } + + override async goForward( + options: WaitForOptions = {} + ): Promise<HTTPResponse | null> { + return await this.#go(+1, options); + } + + async #go( + delta: number, + options: WaitForOptions + ): Promise<HTTPResponse | null> { + try { + const result = await Promise.all([ + this.waitForNavigation(options), + this.#connection.send('browsingContext.traverseHistory', { + delta, + context: this.mainFrame()._id, + }), + ]); + return result[0]; + } catch (err) { + // TODO: waitForNavigation should be cancelled if an error happens. + if (isErrorLike(err)) { + if (err.message.includes('no such history entry')) { + return null; + } + } + throw err; + } + } + + override waitForDevicePrompt(): never { + throw new UnsupportedOperation(); + } +} + +function isConsoleLogEntry( + event: Bidi.Log.Entry +): event is Bidi.Log.ConsoleLogEntry { + return event.type === 'console'; +} + +function isJavaScriptLogEntry( + event: Bidi.Log.Entry +): 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; +} + +function evaluationExpression(fun: Function | string, ...args: unknown[]) { + return `() => {${evaluationString(fun, ...args)}}`; +} |