/** * @license * Copyright 2022 Google Inc. * SPDX-License-Identifier: Apache-2.0 */ import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; import type Protocol from 'devtools-protocol'; import {firstValueFrom, from, raceWith} 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 type { Credentials, GeolocationOptions, MediaFeature, PageEvents, } from '../api/Page.js'; import { Page, PageEvent, type NewDocumentScriptEvaluation, type ScreenshotOptions, } from '../api/Page.js'; import {Coverage} from '../cdp/Coverage.js'; import {EmulationManager} from '../cdp/EmulationManager.js'; import type { InternalNetworkConditions, NetworkConditions, } from '../cdp/NetworkManager.js'; import {Tracing} from '../cdp/Tracing.js'; import type { Cookie, CookieParam, CookieSameSite, DeleteCookiesRequest, } from '../common/Cookie.js'; import {UnsupportedOperation} from '../common/Errors.js'; import {EventEmitter} from '../common/EventEmitter.js'; import type {PDFOptions} from '../common/PDFOptions.js'; import type {Awaitable} from '../common/types.js'; import { evaluationString, isString, parsePDFOptions, timeout, } from '../common/util.js'; import type {Viewport} from '../common/Viewport.js'; import {assert} from '../util/assert.js'; import {bubble} from '../util/decorators.js'; import {stringToTypedArray} from '../util/encoding.js'; import {isErrorLike} from '../util/ErrorLike.js'; import type {BidiBrowser} from './Browser.js'; import type {BidiBrowserContext} from './BrowserContext.js'; import type {BidiCdpSession} from './CDPSession.js'; import type {BrowsingContext} from './core/BrowsingContext.js'; import {BidiFrame} from './Frame.js'; import type {BidiHTTPResponse} from './HTTPResponse.js'; import {BidiKeyboard, BidiMouse, BidiTouchscreen} from './Input.js'; import type {BidiJSHandle} from './JSHandle.js'; import {rewriteNavigationError} from './util.js'; import type {BidiWebWorker} from './WebWorker.js'; /** * Implements Page using WebDriver BiDi. * * @internal */ export class BidiPage extends Page { static from( browserContext: BidiBrowserContext, browsingContext: BrowsingContext ): BidiPage { const page = new BidiPage(browserContext, browsingContext); page.#initialize(); return page; } @bubble() accessor trustedEmitter = new EventEmitter<PageEvents>(); readonly #browserContext: BidiBrowserContext; readonly #frame: BidiFrame; #viewport: Viewport | null = null; readonly #workers = new Set<BidiWebWorker>(); readonly keyboard: BidiKeyboard; readonly mouse: BidiMouse; readonly touchscreen: BidiTouchscreen; readonly tracing: Tracing; readonly coverage: Coverage; readonly #cdpEmulationManager: EmulationManager; #emulatedNetworkConditions?: InternalNetworkConditions; _client(): BidiCdpSession { return this.#frame.client; } private constructor( browserContext: BidiBrowserContext, browsingContext: BrowsingContext ) { super(); this.#browserContext = browserContext; this.#frame = BidiFrame.from(this, browsingContext); this.#cdpEmulationManager = new EmulationManager(this.#frame.client); this.tracing = new Tracing(this.#frame.client); this.coverage = new Coverage(this.#frame.client); this.keyboard = new BidiKeyboard(this); this.mouse = new BidiMouse(this); this.touchscreen = new BidiTouchscreen(this); } #initialize() { this.#frame.browsingContext.on('closed', () => { this.trustedEmitter.emit(PageEvent.Close, undefined); this.trustedEmitter.removeAllListeners(); }); this.trustedEmitter.on(PageEvent.WorkerCreated, worker => { this.#workers.add(worker as BidiWebWorker); }); this.trustedEmitter.on(PageEvent.WorkerDestroyed, worker => { this.#workers.delete(worker as BidiWebWorker); }); } /** * @internal */ _userAgentHeaders: Record<string, string> = {}; #userAgentInterception?: string; #userAgentPreloadScript?: string; override async setUserAgent( userAgent: string, userAgentMetadata?: Protocol.Emulation.UserAgentMetadata ): Promise<void> { if (!this.#browserContext.browser().cdpSupported && userAgentMetadata) { throw new UnsupportedOperation( 'Current Browser does not support `userAgentMetadata`' ); } else if ( this.#browserContext.browser().cdpSupported && userAgentMetadata ) { return await this._client().send('Network.setUserAgentOverride', { userAgent: userAgent, userAgentMetadata: userAgentMetadata, }); } const enable = userAgent !== ''; userAgent = userAgent ?? (await this.#browserContext.browser().userAgent()); this._userAgentHeaders = enable ? { 'User-Agent': userAgent, } : {}; this.#userAgentInterception = await this.#toggleInterception( [Bidi.Network.InterceptPhase.BeforeRequestSent], this.#userAgentInterception, enable ); const changeUserAgent = (userAgent: string) => { Object.defineProperty(navigator, 'userAgent', { value: userAgent, }); }; const frames = [this.#frame]; for (const frame of frames) { frames.push(...frame.childFrames()); } if (this.#userAgentPreloadScript) { await this.removeScriptToEvaluateOnNewDocument( this.#userAgentPreloadScript ); } const [evaluateToken] = await Promise.all([ enable ? this.evaluateOnNewDocument(changeUserAgent, userAgent) : undefined, // When we disable the UserAgent we want to // evaluate the original value in all Browsing Contexts frames.map(frame => { return frame.evaluate(changeUserAgent, userAgent); }), ]); this.#userAgentPreloadScript = evaluateToken?.identifier; } 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.#frame.client.send('Runtime.queryObjects', { prototypeObjectId: prototypeHandle.id, }); return this.#frame.mainRealm().createHandle({ type: 'array', handle: response.objects.objectId, }) as BidiJSHandle<Prototype[]>; } override browser(): BidiBrowser { return this.browserContext().browser(); } override browserContext(): BidiBrowserContext { return this.#browserContext; } override mainFrame(): BidiFrame { return this.#frame; } async focusedFrame(): Promise<BidiFrame> { using handle = (await this.mainFrame() .isolatedRealm() .evaluateHandle(() => { let win = window; while ( win.document.activeElement instanceof win.HTMLIFrameElement || win.document.activeElement instanceof win.HTMLFrameElement ) { if (win.document.activeElement.contentWindow === null) { break; } win = win.document.activeElement.contentWindow as typeof win; } return win; })) as BidiJSHandle<Window & typeof globalThis>; const value = handle.remoteValue(); assert(value.type === 'window'); const frame = this.frames().find(frame => { return frame._id === value.value.context; }); assert(frame); return frame; } override frames(): BidiFrame[] { const frames = [this.#frame]; for (const frame of frames) { frames.push(...frame.childFrames()); } return frames; } override isClosed(): boolean { return this.#frame.detached; } override async close(options?: {runBeforeUnload?: boolean}): Promise<void> { using _guard = await this.#browserContext.waitForScreenshotOperations(); try { await this.#frame.browsingContext.close(options?.runBeforeUnload); } catch { return; } } override async reload( options: WaitForOptions = {} ): Promise<BidiHTTPResponse | null> { const [response] = await Promise.all([ this.#frame.waitForNavigation(options), this.#frame.browsingContext.reload(), ]).catch( rewriteNavigationError( this.url(), options.timeout ?? this._timeoutSettings.navigationTimeout() ) ); return response; } 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 | null): Promise<void> { if (!this.browser().cdpSupported) { await this.#frame.browsingContext.setViewport({ viewport: viewport?.width && viewport?.height ? { width: viewport.width, height: viewport.height, } : null, devicePixelRatio: viewport?.deviceScaleFactor ? viewport.deviceScaleFactor : null, }); 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<Uint8Array> { 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(', ') : []; await firstValueFrom( from( this.mainFrame() .isolatedRealm() .evaluate(() => { return document.fonts.ready; }) ).pipe(raceWith(timeout(ms))) ); const data = await firstValueFrom( from( this.#frame.browsingContext.print({ background, margin, orientation: landscape ? 'landscape' : 'portrait', page: { width, height, }, pageRanges, scale, shrinkToFit: !preferCSSPageSize, }) ).pipe(raceWith(timeout(ms))) ); const typedArray = stringToTypedArray(data, true); await this._maybeWriteTypedArrayToFile(path, typedArray); return typedArray; } override async createPDFStream( options?: PDFOptions | undefined ): Promise<ReadableStream<Uint8Array>> { const typedArray = await this.pdf(options); return new ReadableStream({ start(controller) { controller.enqueue(typedArray); controller.close(); }, }); } 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 data = await this.#frame.browsingContext.captureScreenshot({ 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> { return await this.#frame.createCDPSession(); } override async bringToFront(): Promise<void> { await this.#frame.browsingContext.activate(); } 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 script = await this.#frame.browsingContext.addPreloadScript(expression); return {identifier: script}; } override async removeScriptToEvaluateOnNewDocument( id: string ): Promise<void> { await this.#frame.browsingContext.removePreloadScript(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> { if (!this.#browserContext.browser().cdpSupported) { await this.#frame.browsingContext.setCacheBehavior( enabled ? 'default' : 'bypass' ); return; } // TODO: handle CDP-specific cases such as mprach. await this._client().send('Network.setCacheDisabled', { cacheDisabled: !enabled, }); } override async cookies(...urls: string[]): Promise<Cookie[]> { const normalizedUrls = (urls.length ? urls : [this.url()]).map(url => { return new URL(url); }); const cookies = await this.#frame.browsingContext.getCookies(); return cookies .map(cookie => { return bidiToPuppeteerCookie(cookie); }) .filter(cookie => { return normalizedUrls.some(url => { return testUrlMatchCookie(cookie, url); }); }); } override isServiceWorkerBypassed(): never { throw new UnsupportedOperation(); } override target(): never { throw new UnsupportedOperation(); } override waitForFileChooser(): never { throw new UnsupportedOperation(); } override workers(): BidiWebWorker[] { return [...this.#workers]; } #userInterception?: string; override async setRequestInterception(enable: boolean): Promise<void> { this.#userInterception = await this.#toggleInterception( [Bidi.Network.InterceptPhase.BeforeRequestSent], this.#userInterception, enable ); } /** * @internal */ _extraHTTPHeaders: Record<string, string> = {}; #extraHeadersInterception?: string; override async setExtraHTTPHeaders( headers: Record<string, string> ): Promise<void> { const extraHTTPHeaders: Record<string, string> = {}; for (const [key, value] of Object.entries(headers)) { assert( isString(value), `Expected value of header "${key}" to be String, but "${typeof value}" is found.` ); extraHTTPHeaders[key.toLowerCase()] = value; } this._extraHTTPHeaders = extraHTTPHeaders; this.#extraHeadersInterception = await this.#toggleInterception( [Bidi.Network.InterceptPhase.BeforeRequestSent], this.#extraHeadersInterception, Boolean(Object.keys(this._extraHTTPHeaders).length) ); } /** * @internal */ _credentials: Credentials | null = null; #authInterception?: string; override async authenticate(credentials: Credentials | null): Promise<void> { this.#authInterception = await this.#toggleInterception( [Bidi.Network.InterceptPhase.AuthRequired], this.#authInterception, Boolean(credentials) ); this._credentials = credentials; } async #toggleInterception( phases: [Bidi.Network.InterceptPhase, ...Bidi.Network.InterceptPhase[]], interception: string | undefined, expected: boolean ): Promise<string | undefined> { if (expected && !interception) { return await this.#frame.browsingContext.addIntercept({ phases, }); } else if (!expected && interception) { await this.#frame.browsingContext.userContext.browser.removeIntercept( interception ); return; } return interception; } override setDragInterception(): never { throw new UnsupportedOperation(); } override setBypassServiceWorker(): never { throw new UnsupportedOperation(); } override async setOfflineMode(enabled: boolean): Promise<void> { if (!this.#browserContext.browser().cdpSupported) { throw new UnsupportedOperation(); } if (!this.#emulatedNetworkConditions) { this.#emulatedNetworkConditions = { offline: false, upload: -1, download: -1, latency: 0, }; } this.#emulatedNetworkConditions.offline = enabled; return await this.#applyNetworkConditions(); } override async emulateNetworkConditions( networkConditions: NetworkConditions | null ): Promise<void> { if (!this.#browserContext.browser().cdpSupported) { throw new UnsupportedOperation(); } if (!this.#emulatedNetworkConditions) { this.#emulatedNetworkConditions = { offline: false, upload: -1, download: -1, latency: 0, }; } this.#emulatedNetworkConditions.upload = networkConditions ? networkConditions.upload : -1; this.#emulatedNetworkConditions.download = networkConditions ? networkConditions.download : -1; this.#emulatedNetworkConditions.latency = networkConditions ? networkConditions.latency : 0; return await this.#applyNetworkConditions(); } async #applyNetworkConditions(): Promise<void> { if (!this.#emulatedNetworkConditions) { return; } await this._client().send('Network.emulateNetworkConditions', { offline: this.#emulatedNetworkConditions.offline, latency: this.#emulatedNetworkConditions.latency, uploadThroughput: this.#emulatedNetworkConditions.upload, downloadThroughput: this.#emulatedNetworkConditions.download, }); } override async setCookie(...cookies: CookieParam[]): Promise<void> { const pageURL = this.url(); const pageUrlStartsWithHTTP = pageURL.startsWith('http'); for (const cookie of cookies) { let cookieUrl = cookie.url || ''; if (!cookieUrl && pageUrlStartsWithHTTP) { cookieUrl = pageURL; } assert( cookieUrl !== 'about:blank', `Blank page can not have cookie "${cookie.name}"` ); assert( !String.prototype.startsWith.call(cookieUrl || '', 'data:'), `Data URL page can not have cookie "${cookie.name}"` ); const normalizedUrl = URL.canParse(cookieUrl) ? new URL(cookieUrl) : undefined; const domain = cookie.domain ?? normalizedUrl?.hostname; assert( domain !== undefined, `At least one of the url and domain needs to be specified` ); const bidiCookie: Bidi.Storage.PartialCookie = { domain: domain, name: cookie.name, value: { type: 'string', value: cookie.value, }, ...(cookie.path !== undefined ? {path: cookie.path} : {}), ...(cookie.httpOnly !== undefined ? {httpOnly: cookie.httpOnly} : {}), ...(cookie.secure !== undefined ? {secure: cookie.secure} : {}), ...(cookie.sameSite !== undefined ? {sameSite: convertCookiesSameSiteCdpToBiDi(cookie.sameSite)} : {}), ...(cookie.expires !== undefined ? {expiry: cookie.expires} : {}), // Chrome-specific properties. ...cdpSpecificCookiePropertiesFromPuppeteerToBidi( cookie, 'sameParty', 'sourceScheme', 'priority', 'url' ), }; if (cookie.partitionKey !== undefined) { await this.browserContext().userContext.setCookie( bidiCookie, cookie.partitionKey ); } else { await this.#frame.browsingContext.setCookie(bidiCookie); } } } override async deleteCookie( ...cookies: DeleteCookiesRequest[] ): Promise<void> { await Promise.all( cookies.map(async deleteCookieRequest => { const cookieUrl = deleteCookieRequest.url ?? this.url(); const normalizedUrl = URL.canParse(cookieUrl) ? new URL(cookieUrl) : undefined; const domain = deleteCookieRequest.domain ?? normalizedUrl?.hostname; assert( domain !== undefined, `At least one of the url and domain needs to be specified` ); const filter = { domain: domain, name: deleteCookieRequest.name, ...(deleteCookieRequest.path !== undefined ? {path: deleteCookieRequest.path} : {}), }; await this.#frame.browsingContext.deleteCookie(filter); }) ); } override async removeExposedFunction(name: string): Promise<void> { await this.#frame.removeExposedFunction(name); } 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> { const controller = new AbortController(); try { const [response] = await Promise.all([ this.waitForNavigation({ ...options, signal: controller.signal, }), this.#frame.browsingContext.traverseHistory(delta), ]); return response; } catch (error) { controller.abort(); if (isErrorLike(error)) { if (error.message.includes('no such history entry')) { return null; } } throw error; } } override waitForDevicePrompt(): never { throw new UnsupportedOperation(); } } function evaluationExpression(fun: Function | string, ...args: unknown[]) { return `() => {${evaluationString(fun, ...args)}}`; } /** * Check domains match. * According to cookies spec, this check should match subdomains as well, but CDP * implementation does not do that, so this method matches only the exact domains, not * what is written in the spec: * https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.3 */ function testUrlMatchCookieHostname( cookie: Cookie, normalizedUrl: URL ): boolean { const cookieDomain = cookie.domain.toLowerCase(); const urlHostname = normalizedUrl.hostname.toLowerCase(); return cookieDomain === urlHostname; } /** * Check paths match. * Spec: https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4 */ function testUrlMatchCookiePath(cookie: Cookie, normalizedUrl: URL): boolean { const uriPath = normalizedUrl.pathname; const cookiePath = cookie.path; if (uriPath === cookiePath) { // The cookie-path and the request-path are identical. return true; } if (uriPath.startsWith(cookiePath)) { // The cookie-path is a prefix of the request-path. if (cookiePath.endsWith('/')) { // The last character of the cookie-path is %x2F ("/"). return true; } if (uriPath[cookiePath.length] === '/') { // The first character of the request-path that is not included in the cookie-path // is a %x2F ("/") character. return true; } } return false; } /** * Checks the cookie matches the URL according to the spec: */ function testUrlMatchCookie(cookie: Cookie, url: URL): boolean { const normalizedUrl = new URL(url); assert(cookie !== undefined); if (!testUrlMatchCookieHostname(cookie, normalizedUrl)) { return false; } return testUrlMatchCookiePath(cookie, normalizedUrl); } function bidiToPuppeteerCookie(bidiCookie: Bidi.Network.Cookie): Cookie { const partitionKey = bidiCookie[CDP_SPECIFIC_PREFIX + 'partitionKey']; function getParitionKey(): {partitionKey?: string} { if (typeof partitionKey === 'string') { return {partitionKey}; } if (typeof partitionKey === 'object' && partitionKey !== null) { return { // TODO: a breaking change in Puppeteer is required to change // partitionKey type and report the composite partition key. partitionKey: partitionKey.topLevelSite, }; } return {}; } return { name: bidiCookie.name, // Presents binary value as base64 string. value: bidiCookie.value.value, domain: bidiCookie.domain, path: bidiCookie.path, size: bidiCookie.size, httpOnly: bidiCookie.httpOnly, secure: bidiCookie.secure, sameSite: convertCookiesSameSiteBiDiToCdp(bidiCookie.sameSite), expires: bidiCookie.expiry ?? -1, session: bidiCookie.expiry === undefined || bidiCookie.expiry <= 0, // Extending with CDP-specific properties with `goog:` prefix. ...cdpSpecificCookiePropertiesFromBidiToPuppeteer( bidiCookie, 'sameParty', 'sourceScheme', 'partitionKeyOpaque', 'priority' ), ...getParitionKey(), }; } const CDP_SPECIFIC_PREFIX = 'goog:'; /** * Gets CDP-specific properties from the BiDi cookie and returns them as a new object. */ function cdpSpecificCookiePropertiesFromBidiToPuppeteer( bidiCookie: Bidi.Network.Cookie, ...propertyNames: Array<keyof Cookie> ): Partial<Cookie> { const result: Partial<Cookie> = {}; for (const property of propertyNames) { if (bidiCookie[CDP_SPECIFIC_PREFIX + property] !== undefined) { result[property] = bidiCookie[CDP_SPECIFIC_PREFIX + property]; } } return result; } /** * Gets CDP-specific properties from the cookie, adds CDP-specific prefixes and returns * them as a new object which can be used in BiDi. */ function cdpSpecificCookiePropertiesFromPuppeteerToBidi( cookieParam: CookieParam, ...propertyNames: Array<keyof CookieParam> ): Record<string, unknown> { const result: Record<string, unknown> = {}; for (const property of propertyNames) { if (cookieParam[property] !== undefined) { result[CDP_SPECIFIC_PREFIX + property] = cookieParam[property]; } } return result; } function convertCookiesSameSiteBiDiToCdp( sameSite: Bidi.Network.SameSite | undefined ): CookieSameSite { return sameSite === 'strict' ? 'Strict' : sameSite === 'lax' ? 'Lax' : 'None'; } function convertCookiesSameSiteCdpToBiDi( sameSite: CookieSameSite | undefined ): Bidi.Network.SameSite { return sameSite === 'Strict' ? Bidi.Network.SameSite.Strict : sameSite === 'Lax' ? Bidi.Network.SameSite.Lax : Bidi.Network.SameSite.None; }