tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

Page.ts (36648B)


      1 /**
      2 * @license
      3 * Copyright 2017 Google Inc.
      4 * SPDX-License-Identifier: Apache-2.0
      5 */
      6 
      7 import type {Protocol} from 'devtools-protocol';
      8 
      9 import {firstValueFrom, from, raceWith} from '../../third_party/rxjs/rxjs.js';
     10 import type {Browser} from '../api/Browser.js';
     11 import type {BrowserContext} from '../api/BrowserContext.js';
     12 import {CDPSessionEvent, type CDPSession} from '../api/CDPSession.js';
     13 import type {ElementHandle} from '../api/ElementHandle.js';
     14 import type {Frame, WaitForOptions} from '../api/Frame.js';
     15 import type {HTTPResponse} from '../api/HTTPResponse.js';
     16 import type {JSHandle} from '../api/JSHandle.js';
     17 import type {Credentials} from '../api/Page.js';
     18 import {
     19  Page,
     20  PageEvent,
     21  type GeolocationOptions,
     22  type MediaFeature,
     23  type Metrics,
     24  type NewDocumentScriptEvaluation,
     25  type ScreenshotClip,
     26  type ScreenshotOptions,
     27  type WaitTimeoutOptions,
     28 } from '../api/Page.js';
     29 import {
     30  ConsoleMessage,
     31  type ConsoleMessageType,
     32 } from '../common/ConsoleMessage.js';
     33 import type {
     34  Cookie,
     35  DeleteCookiesRequest,
     36  CookieParam,
     37  CookiePartitionKey,
     38 } from '../common/Cookie.js';
     39 import {TargetCloseError} from '../common/Errors.js';
     40 import {EventEmitter} from '../common/EventEmitter.js';
     41 import {FileChooser} from '../common/FileChooser.js';
     42 import {NetworkManagerEvent} from '../common/NetworkManagerEvents.js';
     43 import type {PDFOptions} from '../common/PDFOptions.js';
     44 import type {BindingPayload, HandleFor} from '../common/types.js';
     45 import {
     46  debugError,
     47  evaluationString,
     48  getReadableAsTypedArray,
     49  getReadableFromProtocolStream,
     50  parsePDFOptions,
     51  timeout,
     52  validateDialogType,
     53 } from '../common/util.js';
     54 import type {Viewport} from '../common/Viewport.js';
     55 import {assert} from '../util/assert.js';
     56 import {Deferred} from '../util/Deferred.js';
     57 import {AsyncDisposableStack} from '../util/disposable.js';
     58 import {isErrorLike} from '../util/ErrorLike.js';
     59 
     60 import {Binding} from './Binding.js';
     61 import {CdpCDPSession} from './CdpSession.js';
     62 import {isTargetClosedError} from './Connection.js';
     63 import {Coverage} from './Coverage.js';
     64 import type {DeviceRequestPrompt} from './DeviceRequestPrompt.js';
     65 import {CdpDialog} from './Dialog.js';
     66 import {EmulationManager} from './EmulationManager.js';
     67 import type {CdpFrame} from './Frame.js';
     68 import {FrameManager} from './FrameManager.js';
     69 import {FrameManagerEvent} from './FrameManagerEvents.js';
     70 import {CdpKeyboard, CdpMouse, CdpTouchscreen} from './Input.js';
     71 import type {IsolatedWorld} from './IsolatedWorld.js';
     72 import {MAIN_WORLD} from './IsolatedWorlds.js';
     73 import {releaseObject} from './JSHandle.js';
     74 import type {NetworkConditions} from './NetworkManager.js';
     75 import type {CdpTarget} from './Target.js';
     76 import {TargetManagerEvent} from './TargetManageEvents.js';
     77 import type {TargetManager} from './TargetManager.js';
     78 import {Tracing} from './Tracing.js';
     79 import {
     80  createClientError,
     81  pageBindingInitString,
     82  valueFromRemoteObject,
     83 } from './utils.js';
     84 import {CdpWebWorker} from './WebWorker.js';
     85 
     86 function convertConsoleMessageLevel(method: string): ConsoleMessageType {
     87  switch (method) {
     88    case 'warning':
     89      return 'warn';
     90    default:
     91      return method as ConsoleMessageType;
     92  }
     93 }
     94 
     95 /**
     96 * @internal
     97 */
     98 export class CdpPage extends Page {
     99  static async _create(
    100    client: CdpCDPSession,
    101    target: CdpTarget,
    102    defaultViewport: Viewport | null,
    103  ): Promise<CdpPage> {
    104    const page = new CdpPage(client, target);
    105    await page.#initialize();
    106    if (defaultViewport) {
    107      try {
    108        await page.setViewport(defaultViewport);
    109      } catch (err) {
    110        if (isErrorLike(err) && isTargetClosedError(err)) {
    111          debugError(err);
    112        } else {
    113          throw err;
    114        }
    115      }
    116    }
    117    return page;
    118  }
    119 
    120  #closed = false;
    121  readonly #targetManager: TargetManager;
    122 
    123  #primaryTargetClient: CdpCDPSession;
    124  #primaryTarget: CdpTarget;
    125  #tabTargetClient: CDPSession;
    126  #tabTarget: CdpTarget;
    127  #keyboard: CdpKeyboard;
    128  #mouse: CdpMouse;
    129  #touchscreen: CdpTouchscreen;
    130  #frameManager: FrameManager;
    131  #emulationManager: EmulationManager;
    132  #tracing: Tracing;
    133  #bindings = new Map<string, Binding>();
    134  #exposedFunctions = new Map<string, string>();
    135  #coverage: Coverage;
    136  #viewport: Viewport | null;
    137  #workers = new Map<string, CdpWebWorker>();
    138  #fileChooserDeferreds = new Set<Deferred<FileChooser>>();
    139  #sessionCloseDeferred = Deferred.create<never, TargetCloseError>();
    140  #serviceWorkerBypassed = false;
    141  #userDragInterceptionEnabled = false;
    142 
    143  constructor(client: CdpCDPSession, target: CdpTarget) {
    144    super();
    145    this.#primaryTargetClient = client;
    146    this.#tabTargetClient = client.parentSession()!;
    147    assert(this.#tabTargetClient, 'Tab target session is not defined.');
    148    this.#tabTarget = (this.#tabTargetClient as CdpCDPSession).target();
    149    assert(this.#tabTarget, 'Tab target is not defined.');
    150    this.#primaryTarget = target;
    151    this.#targetManager = target._targetManager();
    152    this.#keyboard = new CdpKeyboard(client);
    153    this.#mouse = new CdpMouse(client, this.#keyboard);
    154    this.#touchscreen = new CdpTouchscreen(client, this.#keyboard);
    155    this.#frameManager = new FrameManager(client, this, this._timeoutSettings);
    156    this.#emulationManager = new EmulationManager(client);
    157    this.#tracing = new Tracing(client);
    158    this.#coverage = new Coverage(client);
    159    this.#viewport = null;
    160 
    161    const frameManagerEmitter = new EventEmitter(this.#frameManager);
    162    frameManagerEmitter.on(FrameManagerEvent.FrameAttached, frame => {
    163      this.emit(PageEvent.FrameAttached, frame);
    164    });
    165    frameManagerEmitter.on(FrameManagerEvent.FrameDetached, frame => {
    166      this.emit(PageEvent.FrameDetached, frame);
    167    });
    168    frameManagerEmitter.on(FrameManagerEvent.FrameNavigated, frame => {
    169      this.emit(PageEvent.FrameNavigated, frame);
    170    });
    171    frameManagerEmitter.on(
    172      FrameManagerEvent.ConsoleApiCalled,
    173      ([world, event]) => {
    174        this.#onConsoleAPI(world, event);
    175      },
    176    );
    177    frameManagerEmitter.on(
    178      FrameManagerEvent.BindingCalled,
    179      ([world, event]) => {
    180        void this.#onBindingCalled(world, event);
    181      },
    182    );
    183 
    184    const networkManagerEmitter = new EventEmitter(
    185      this.#frameManager.networkManager,
    186    );
    187    networkManagerEmitter.on(NetworkManagerEvent.Request, request => {
    188      this.emit(PageEvent.Request, request);
    189    });
    190    networkManagerEmitter.on(
    191      NetworkManagerEvent.RequestServedFromCache,
    192      request => {
    193        this.emit(PageEvent.RequestServedFromCache, request!);
    194      },
    195    );
    196    networkManagerEmitter.on(NetworkManagerEvent.Response, response => {
    197      this.emit(PageEvent.Response, response);
    198    });
    199    networkManagerEmitter.on(NetworkManagerEvent.RequestFailed, request => {
    200      this.emit(PageEvent.RequestFailed, request);
    201    });
    202    networkManagerEmitter.on(NetworkManagerEvent.RequestFinished, request => {
    203      this.emit(PageEvent.RequestFinished, request);
    204    });
    205 
    206    this.#tabTargetClient.on(
    207      CDPSessionEvent.Swapped,
    208      this.#onActivation.bind(this),
    209    );
    210 
    211    this.#tabTargetClient.on(
    212      CDPSessionEvent.Ready,
    213      this.#onSecondaryTarget.bind(this),
    214    );
    215 
    216    this.#targetManager.on(
    217      TargetManagerEvent.TargetGone,
    218      this.#onDetachedFromTarget,
    219    );
    220 
    221    this.#tabTarget._isClosedDeferred
    222      .valueOrThrow()
    223      .then(() => {
    224        this.#targetManager.off(
    225          TargetManagerEvent.TargetGone,
    226          this.#onDetachedFromTarget,
    227        );
    228 
    229        this.emit(PageEvent.Close, undefined);
    230        this.#closed = true;
    231      })
    232      .catch(debugError);
    233 
    234    this.#setupPrimaryTargetListeners();
    235    this.#attachExistingTargets();
    236  }
    237 
    238  #attachExistingTargets(): void {
    239    const queue = [];
    240    for (const childTarget of this.#targetManager.getChildTargets(
    241      this.#primaryTarget,
    242    )) {
    243      queue.push(childTarget);
    244    }
    245    let idx = 0;
    246    while (idx < queue.length) {
    247      const next = queue[idx] as CdpTarget;
    248      idx++;
    249      const session = next._session();
    250      if (session) {
    251        this.#onAttachedToTarget(session);
    252      }
    253      for (const childTarget of this.#targetManager.getChildTargets(next)) {
    254        queue.push(childTarget);
    255      }
    256    }
    257  }
    258 
    259  async #onActivation(newSession: CDPSession): Promise<void> {
    260    // TODO: Remove assert once we have separate Event type for CdpCDPSession.
    261    assert(
    262      newSession instanceof CdpCDPSession,
    263      'CDPSession is not instance of CdpCDPSession',
    264    );
    265    this.#primaryTargetClient = newSession;
    266    this.#primaryTarget = newSession.target();
    267    assert(this.#primaryTarget, 'Missing target on swap');
    268    this.#keyboard.updateClient(newSession);
    269    this.#mouse.updateClient(newSession);
    270    this.#touchscreen.updateClient(newSession);
    271    this.#emulationManager.updateClient(newSession);
    272    this.#tracing.updateClient(newSession);
    273    this.#coverage.updateClient(newSession);
    274    await this.#frameManager.swapFrameTree(newSession);
    275    this.#setupPrimaryTargetListeners();
    276  }
    277 
    278  async #onSecondaryTarget(session: CDPSession): Promise<void> {
    279    assert(session instanceof CdpCDPSession);
    280    if (session.target()._subtype() !== 'prerender') {
    281      return;
    282    }
    283    this.#frameManager.registerSpeculativeSession(session).catch(debugError);
    284    this.#emulationManager
    285      .registerSpeculativeSession(session)
    286      .catch(debugError);
    287  }
    288 
    289  /**
    290   * Sets up listeners for the primary target. The primary target can change
    291   * during a navigation to a prerended page.
    292   */
    293  #setupPrimaryTargetListeners() {
    294    const clientEmitter = new EventEmitter(this.#primaryTargetClient);
    295    clientEmitter.on(CDPSessionEvent.Ready, this.#onAttachedToTarget);
    296    clientEmitter.on(CDPSessionEvent.Disconnected, () => {
    297      this.#sessionCloseDeferred.reject(new TargetCloseError('Target closed'));
    298    });
    299    clientEmitter.on('Page.domContentEventFired', () => {
    300      this.emit(PageEvent.DOMContentLoaded, undefined);
    301    });
    302    clientEmitter.on('Page.loadEventFired', () => {
    303      this.emit(PageEvent.Load, undefined);
    304    });
    305    clientEmitter.on('Page.javascriptDialogOpening', this.#onDialog.bind(this));
    306    clientEmitter.on(
    307      'Runtime.exceptionThrown',
    308      this.#handleException.bind(this),
    309    );
    310    clientEmitter.on(
    311      'Inspector.targetCrashed',
    312      this.#onTargetCrashed.bind(this),
    313    );
    314    clientEmitter.on('Performance.metrics', this.#emitMetrics.bind(this));
    315    clientEmitter.on('Log.entryAdded', this.#onLogEntryAdded.bind(this));
    316    clientEmitter.on('Page.fileChooserOpened', this.#onFileChooser.bind(this));
    317  }
    318 
    319  #onDetachedFromTarget = (target: CdpTarget) => {
    320    const sessionId = target._session()?.id();
    321    const worker = this.#workers.get(sessionId!);
    322    if (!worker) {
    323      return;
    324    }
    325    this.#workers.delete(sessionId!);
    326    this.emit(PageEvent.WorkerDestroyed, worker);
    327  };
    328 
    329  #onAttachedToTarget = (session: CDPSession) => {
    330    assert(session instanceof CdpCDPSession);
    331    this.#frameManager.onAttachedToTarget(session.target());
    332    if (session.target()._getTargetInfo().type === 'worker') {
    333      const worker = new CdpWebWorker(
    334        session,
    335        session.target().url(),
    336        session.target()._targetId,
    337        session.target().type(),
    338        this.#addConsoleMessage.bind(this),
    339        this.#handleException.bind(this),
    340        this.#frameManager.networkManager,
    341      );
    342      this.#workers.set(session.id(), worker);
    343      this.emit(PageEvent.WorkerCreated, worker);
    344    }
    345    session.on(CDPSessionEvent.Ready, this.#onAttachedToTarget);
    346  };
    347 
    348  async #initialize(): Promise<void> {
    349    try {
    350      await Promise.all([
    351        this.#frameManager.initialize(this.#primaryTargetClient),
    352        this.#primaryTargetClient.send('Performance.enable'),
    353        this.#primaryTargetClient.send('Log.enable'),
    354      ]);
    355    } catch (err) {
    356      if (isErrorLike(err) && isTargetClosedError(err)) {
    357        debugError(err);
    358      } else {
    359        throw err;
    360      }
    361    }
    362  }
    363 
    364  async #onFileChooser(
    365    event: Protocol.Page.FileChooserOpenedEvent,
    366  ): Promise<void> {
    367    if (!this.#fileChooserDeferreds.size) {
    368      return;
    369    }
    370 
    371    const frame = this.#frameManager.frame(event.frameId);
    372    assert(frame, 'This should never happen.');
    373 
    374    // This is guaranteed to be an HTMLInputElement handle by the event.
    375    using handle = (await frame.worlds[MAIN_WORLD].adoptBackendNode(
    376      event.backendNodeId,
    377    )) as ElementHandle<HTMLInputElement>;
    378 
    379    const fileChooser = new FileChooser(
    380      handle.move(),
    381      event.mode !== 'selectSingle',
    382    );
    383    for (const promise of this.#fileChooserDeferreds) {
    384      promise.resolve(fileChooser);
    385    }
    386    this.#fileChooserDeferreds.clear();
    387  }
    388 
    389  _client(): CDPSession {
    390    return this.#primaryTargetClient;
    391  }
    392 
    393  override isServiceWorkerBypassed(): boolean {
    394    return this.#serviceWorkerBypassed;
    395  }
    396 
    397  override isDragInterceptionEnabled(): boolean {
    398    return this.#userDragInterceptionEnabled;
    399  }
    400 
    401  override isJavaScriptEnabled(): boolean {
    402    return this.#emulationManager.javascriptEnabled;
    403  }
    404 
    405  override async waitForFileChooser(
    406    options: WaitTimeoutOptions = {},
    407  ): Promise<FileChooser> {
    408    const needsEnable = this.#fileChooserDeferreds.size === 0;
    409    const {timeout = this._timeoutSettings.timeout()} = options;
    410    const deferred = Deferred.create<FileChooser>({
    411      message: `Waiting for \`FileChooser\` failed: ${timeout}ms exceeded`,
    412      timeout,
    413    });
    414 
    415    if (options.signal) {
    416      options.signal.addEventListener(
    417        'abort',
    418        () => {
    419          deferred.reject(options.signal?.reason);
    420        },
    421        {once: true},
    422      );
    423    }
    424 
    425    this.#fileChooserDeferreds.add(deferred);
    426    let enablePromise: Promise<void> | undefined;
    427    if (needsEnable) {
    428      enablePromise = this.#primaryTargetClient.send(
    429        'Page.setInterceptFileChooserDialog',
    430        {
    431          enabled: true,
    432        },
    433      );
    434    }
    435    try {
    436      const [result] = await Promise.all([
    437        deferred.valueOrThrow(),
    438        enablePromise,
    439      ]);
    440      return result;
    441    } catch (error) {
    442      this.#fileChooserDeferreds.delete(deferred);
    443      throw error;
    444    }
    445  }
    446 
    447  override async setGeolocation(options: GeolocationOptions): Promise<void> {
    448    return await this.#emulationManager.setGeolocation(options);
    449  }
    450 
    451  override target(): CdpTarget {
    452    return this.#primaryTarget;
    453  }
    454 
    455  override browser(): Browser {
    456    return this.#primaryTarget.browser();
    457  }
    458 
    459  override browserContext(): BrowserContext {
    460    return this.#primaryTarget.browserContext();
    461  }
    462 
    463  #onTargetCrashed(): void {
    464    this.emit(PageEvent.Error, new Error('Page crashed!'));
    465  }
    466 
    467  #onLogEntryAdded(event: Protocol.Log.EntryAddedEvent): void {
    468    const {level, text, args, source, url, lineNumber} = event.entry;
    469    if (args) {
    470      args.map(arg => {
    471        void releaseObject(this.#primaryTargetClient, arg);
    472      });
    473    }
    474    if (source !== 'worker') {
    475      this.emit(
    476        PageEvent.Console,
    477        new ConsoleMessage(
    478          convertConsoleMessageLevel(level),
    479          text,
    480          [],
    481          [{url, lineNumber}],
    482        ),
    483      );
    484    }
    485  }
    486 
    487  override mainFrame(): CdpFrame {
    488    return this.#frameManager.mainFrame();
    489  }
    490 
    491  override get keyboard(): CdpKeyboard {
    492    return this.#keyboard;
    493  }
    494 
    495  override get touchscreen(): CdpTouchscreen {
    496    return this.#touchscreen;
    497  }
    498 
    499  override get coverage(): Coverage {
    500    return this.#coverage;
    501  }
    502 
    503  override get tracing(): Tracing {
    504    return this.#tracing;
    505  }
    506 
    507  override frames(): Frame[] {
    508    return this.#frameManager.frames();
    509  }
    510 
    511  override workers(): CdpWebWorker[] {
    512    return Array.from(this.#workers.values());
    513  }
    514 
    515  override async setRequestInterception(value: boolean): Promise<void> {
    516    return await this.#frameManager.networkManager.setRequestInterception(
    517      value,
    518    );
    519  }
    520 
    521  override async setBypassServiceWorker(bypass: boolean): Promise<void> {
    522    this.#serviceWorkerBypassed = bypass;
    523    return await this.#primaryTargetClient.send(
    524      'Network.setBypassServiceWorker',
    525      {bypass},
    526    );
    527  }
    528 
    529  override async setDragInterception(enabled: boolean): Promise<void> {
    530    this.#userDragInterceptionEnabled = enabled;
    531    return await this.#primaryTargetClient.send('Input.setInterceptDrags', {
    532      enabled,
    533    });
    534  }
    535 
    536  override async setOfflineMode(enabled: boolean): Promise<void> {
    537    return await this.#frameManager.networkManager.setOfflineMode(enabled);
    538  }
    539 
    540  override async emulateNetworkConditions(
    541    networkConditions: NetworkConditions | null,
    542  ): Promise<void> {
    543    return await this.#frameManager.networkManager.emulateNetworkConditions(
    544      networkConditions,
    545    );
    546  }
    547 
    548  override setDefaultNavigationTimeout(timeout: number): void {
    549    this._timeoutSettings.setDefaultNavigationTimeout(timeout);
    550  }
    551 
    552  override setDefaultTimeout(timeout: number): void {
    553    this._timeoutSettings.setDefaultTimeout(timeout);
    554  }
    555 
    556  override getDefaultTimeout(): number {
    557    return this._timeoutSettings.timeout();
    558  }
    559 
    560  override getDefaultNavigationTimeout(): number {
    561    return this._timeoutSettings.navigationTimeout();
    562  }
    563 
    564  override async queryObjects<Prototype>(
    565    prototypeHandle: JSHandle<Prototype>,
    566  ): Promise<JSHandle<Prototype[]>> {
    567    assert(!prototypeHandle.disposed, 'Prototype JSHandle is disposed!');
    568    assert(
    569      prototypeHandle.id,
    570      'Prototype JSHandle must not be referencing primitive value',
    571    );
    572    const response = await this.mainFrame().client.send(
    573      'Runtime.queryObjects',
    574      {
    575        prototypeObjectId: prototypeHandle.id,
    576      },
    577    );
    578    return this.mainFrame()
    579      .mainRealm()
    580      .createCdpHandle(response.objects) as HandleFor<Prototype[]>;
    581  }
    582 
    583  override async cookies(...urls: string[]): Promise<Cookie[]> {
    584    const originalCookies = (
    585      await this.#primaryTargetClient.send('Network.getCookies', {
    586        urls: urls.length ? urls : [this.url()],
    587      })
    588    ).cookies;
    589 
    590    const unsupportedCookieAttributes = ['sourcePort'];
    591    const filterUnsupportedAttributes = (
    592      cookie: Protocol.Network.Cookie,
    593    ): Protocol.Network.Cookie => {
    594      for (const attr of unsupportedCookieAttributes) {
    595        delete (cookie as unknown as Record<string, unknown>)[attr];
    596      }
    597      return cookie;
    598    };
    599    return originalCookies.map(filterUnsupportedAttributes).map(cookie => {
    600      return {
    601        ...cookie,
    602        // TODO: a breaking change is needed in Puppeteer types to support other
    603        // partition keys.
    604        partitionKey: cookie.partitionKey
    605          ? cookie.partitionKey.topLevelSite
    606          : undefined,
    607      };
    608    });
    609  }
    610 
    611  override async deleteCookie(
    612    ...cookies: DeleteCookiesRequest[]
    613  ): Promise<void> {
    614    const pageURL = this.url();
    615    for (const cookie of cookies) {
    616      const item = {
    617        ...cookie,
    618        partitionKey: convertCookiesPartitionKeyFromPuppeteerToCdp(
    619          cookie.partitionKey,
    620        ),
    621      };
    622      if (!cookie.url && pageURL.startsWith('http')) {
    623        item.url = pageURL;
    624      }
    625      await this.#primaryTargetClient.send('Network.deleteCookies', item);
    626      if (pageURL.startsWith('http') && !item.partitionKey) {
    627        const url = new URL(pageURL);
    628        // Delete also cookies from the page's partition.
    629        await this.#primaryTargetClient.send('Network.deleteCookies', {
    630          ...item,
    631          partitionKey: {
    632            topLevelSite: url.origin.replace(`:${url.port}`, ''),
    633            hasCrossSiteAncestor: false,
    634          },
    635        });
    636      }
    637    }
    638  }
    639 
    640  override async setCookie(...cookies: CookieParam[]): Promise<void> {
    641    const pageURL = this.url();
    642    const startsWithHTTP = pageURL.startsWith('http');
    643    const items = cookies.map(cookie => {
    644      const item = Object.assign({}, cookie);
    645      if (!item.url && startsWithHTTP) {
    646        item.url = pageURL;
    647      }
    648      assert(
    649        item.url !== 'about:blank',
    650        `Blank page can not have cookie "${item.name}"`,
    651      );
    652      assert(
    653        !String.prototype.startsWith.call(item.url || '', 'data:'),
    654        `Data URL page can not have cookie "${item.name}"`,
    655      );
    656      return item;
    657    });
    658    await this.deleteCookie(...items);
    659    if (items.length) {
    660      await this.#primaryTargetClient.send('Network.setCookies', {
    661        cookies: items.map(cookieParam => {
    662          return {
    663            ...cookieParam,
    664            partitionKey: convertCookiesPartitionKeyFromPuppeteerToCdp(
    665              cookieParam.partitionKey,
    666            ),
    667          };
    668        }),
    669      });
    670    }
    671  }
    672 
    673  override async exposeFunction(
    674    name: string,
    675    // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
    676    pptrFunction: Function | {default: Function},
    677  ): Promise<void> {
    678    if (this.#bindings.has(name)) {
    679      throw new Error(
    680        `Failed to add page binding with name ${name}: window['${name}'] already exists!`,
    681      );
    682    }
    683    const source = pageBindingInitString('exposedFun', name);
    684    let binding: Binding;
    685    switch (typeof pptrFunction) {
    686      case 'function':
    687        binding = new Binding(
    688          name,
    689          pptrFunction as (...args: unknown[]) => unknown,
    690          source,
    691        );
    692        break;
    693      default:
    694        binding = new Binding(
    695          name,
    696          pptrFunction.default as (...args: unknown[]) => unknown,
    697          source,
    698        );
    699        break;
    700    }
    701    this.#bindings.set(name, binding);
    702    const [{identifier}] = await Promise.all([
    703      this.#frameManager.evaluateOnNewDocument(source),
    704      this.#frameManager.addExposedFunctionBinding(binding),
    705    ]);
    706    this.#exposedFunctions.set(name, identifier);
    707  }
    708 
    709  override async removeExposedFunction(name: string): Promise<void> {
    710    const exposedFunctionId = this.#exposedFunctions.get(name);
    711    if (!exposedFunctionId) {
    712      throw new Error(`Function with name "${name}" does not exist`);
    713    }
    714    // #bindings must be updated together with #exposedFunctions.
    715    const binding = this.#bindings.get(name)!;
    716    this.#exposedFunctions.delete(name);
    717    this.#bindings.delete(name);
    718    await Promise.all([
    719      this.#frameManager.removeScriptToEvaluateOnNewDocument(exposedFunctionId),
    720      this.#frameManager.removeExposedFunctionBinding(binding),
    721    ]);
    722  }
    723 
    724  override async authenticate(credentials: Credentials | null): Promise<void> {
    725    return await this.#frameManager.networkManager.authenticate(credentials);
    726  }
    727 
    728  override async setExtraHTTPHeaders(
    729    headers: Record<string, string>,
    730  ): Promise<void> {
    731    return await this.#frameManager.networkManager.setExtraHTTPHeaders(headers);
    732  }
    733 
    734  override async setUserAgent(
    735    userAgent: string,
    736    userAgentMetadata?: Protocol.Emulation.UserAgentMetadata,
    737  ): Promise<void> {
    738    return await this.#frameManager.networkManager.setUserAgent(
    739      userAgent,
    740      userAgentMetadata,
    741    );
    742  }
    743 
    744  override async metrics(): Promise<Metrics> {
    745    const response = await this.#primaryTargetClient.send(
    746      'Performance.getMetrics',
    747    );
    748    return this.#buildMetricsObject(response.metrics);
    749  }
    750 
    751  #emitMetrics(event: Protocol.Performance.MetricsEvent): void {
    752    this.emit(PageEvent.Metrics, {
    753      title: event.title,
    754      metrics: this.#buildMetricsObject(event.metrics),
    755    });
    756  }
    757 
    758  #buildMetricsObject(metrics?: Protocol.Performance.Metric[]): Metrics {
    759    const result: Record<
    760      Protocol.Performance.Metric['name'],
    761      Protocol.Performance.Metric['value']
    762    > = {};
    763    for (const metric of metrics || []) {
    764      if (supportedMetrics.has(metric.name)) {
    765        result[metric.name] = metric.value;
    766      }
    767    }
    768    return result;
    769  }
    770 
    771  #handleException(exception: Protocol.Runtime.ExceptionThrownEvent): void {
    772    this.emit(
    773      PageEvent.PageError,
    774      createClientError(exception.exceptionDetails),
    775    );
    776  }
    777 
    778  #onConsoleAPI(
    779    world: IsolatedWorld,
    780    event: Protocol.Runtime.ConsoleAPICalledEvent,
    781  ): void {
    782    const values = event.args.map(arg => {
    783      return world.createCdpHandle(arg);
    784    });
    785    this.#addConsoleMessage(
    786      convertConsoleMessageLevel(event.type),
    787      values,
    788      event.stackTrace,
    789    );
    790  }
    791 
    792  async #onBindingCalled(
    793    world: IsolatedWorld,
    794    event: Protocol.Runtime.BindingCalledEvent,
    795  ): Promise<void> {
    796    let payload: BindingPayload;
    797    try {
    798      payload = JSON.parse(event.payload);
    799    } catch {
    800      // The binding was either called by something in the page or it was
    801      // called before our wrapper was initialized.
    802      return;
    803    }
    804    const {type, name, seq, args, isTrivial} = payload;
    805    if (type !== 'exposedFun') {
    806      return;
    807    }
    808 
    809    const context = world.context;
    810    if (!context) {
    811      return;
    812    }
    813 
    814    const binding = this.#bindings.get(name);
    815    await binding?.run(context, seq, args, isTrivial);
    816  }
    817 
    818  #addConsoleMessage(
    819    eventType: string,
    820    args: JSHandle[],
    821    stackTrace?: Protocol.Runtime.StackTrace,
    822  ): void {
    823    if (!this.listenerCount(PageEvent.Console)) {
    824      args.forEach(arg => {
    825        return arg.dispose();
    826      });
    827      return;
    828    }
    829    const textTokens = [];
    830    // eslint-disable-next-line max-len -- The comment is long.
    831    // eslint-disable-next-line rulesdir/use-using -- These are not owned by this function.
    832    for (const arg of args) {
    833      const remoteObject = arg.remoteObject();
    834      if (remoteObject.objectId) {
    835        textTokens.push(arg.toString());
    836      } else {
    837        textTokens.push(valueFromRemoteObject(remoteObject));
    838      }
    839    }
    840    const stackTraceLocations = [];
    841    if (stackTrace) {
    842      for (const callFrame of stackTrace.callFrames) {
    843        stackTraceLocations.push({
    844          url: callFrame.url,
    845          lineNumber: callFrame.lineNumber,
    846          columnNumber: callFrame.columnNumber,
    847        });
    848      }
    849    }
    850    const message = new ConsoleMessage(
    851      convertConsoleMessageLevel(eventType),
    852      textTokens.join(' '),
    853      args,
    854      stackTraceLocations,
    855    );
    856    this.emit(PageEvent.Console, message);
    857  }
    858 
    859  #onDialog(event: Protocol.Page.JavascriptDialogOpeningEvent): void {
    860    const type = validateDialogType(event.type);
    861    const dialog = new CdpDialog(
    862      this.#primaryTargetClient,
    863      type,
    864      event.message,
    865      event.defaultPrompt,
    866    );
    867    this.emit(PageEvent.Dialog, dialog);
    868  }
    869 
    870  override async reload(
    871    options?: WaitForOptions,
    872  ): Promise<HTTPResponse | null> {
    873    const [result] = await Promise.all([
    874      this.waitForNavigation({
    875        ...options,
    876        ignoreSameDocumentNavigation: true,
    877      }),
    878      this.#primaryTargetClient.send('Page.reload'),
    879    ]);
    880 
    881    return result;
    882  }
    883 
    884  override async createCDPSession(): Promise<CDPSession> {
    885    return await this.target().createCDPSession();
    886  }
    887 
    888  override async goBack(
    889    options: WaitForOptions = {},
    890  ): Promise<HTTPResponse | null> {
    891    return await this.#go(-1, options);
    892  }
    893 
    894  override async goForward(
    895    options: WaitForOptions = {},
    896  ): Promise<HTTPResponse | null> {
    897    return await this.#go(+1, options);
    898  }
    899 
    900  async #go(
    901    delta: number,
    902    options: WaitForOptions,
    903  ): Promise<HTTPResponse | null> {
    904    const history = await this.#primaryTargetClient.send(
    905      'Page.getNavigationHistory',
    906    );
    907    const entry = history.entries[history.currentIndex + delta];
    908    if (!entry) {
    909      return null;
    910    }
    911    const result = await Promise.all([
    912      this.waitForNavigation(options),
    913      this.#primaryTargetClient.send('Page.navigateToHistoryEntry', {
    914        entryId: entry.id,
    915      }),
    916    ]);
    917    return result[0];
    918  }
    919 
    920  override async bringToFront(): Promise<void> {
    921    await this.#primaryTargetClient.send('Page.bringToFront');
    922  }
    923 
    924  override async setJavaScriptEnabled(enabled: boolean): Promise<void> {
    925    return await this.#emulationManager.setJavaScriptEnabled(enabled);
    926  }
    927 
    928  override async setBypassCSP(enabled: boolean): Promise<void> {
    929    await this.#primaryTargetClient.send('Page.setBypassCSP', {enabled});
    930  }
    931 
    932  override async emulateMediaType(type?: string): Promise<void> {
    933    return await this.#emulationManager.emulateMediaType(type);
    934  }
    935 
    936  override async emulateCPUThrottling(factor: number | null): Promise<void> {
    937    return await this.#emulationManager.emulateCPUThrottling(factor);
    938  }
    939 
    940  override async emulateMediaFeatures(
    941    features?: MediaFeature[],
    942  ): Promise<void> {
    943    return await this.#emulationManager.emulateMediaFeatures(features);
    944  }
    945 
    946  override async emulateTimezone(timezoneId?: string): Promise<void> {
    947    return await this.#emulationManager.emulateTimezone(timezoneId);
    948  }
    949 
    950  override async emulateIdleState(overrides?: {
    951    isUserActive: boolean;
    952    isScreenUnlocked: boolean;
    953  }): Promise<void> {
    954    return await this.#emulationManager.emulateIdleState(overrides);
    955  }
    956 
    957  override async emulateVisionDeficiency(
    958    type?: Protocol.Emulation.SetEmulatedVisionDeficiencyRequest['type'],
    959  ): Promise<void> {
    960    return await this.#emulationManager.emulateVisionDeficiency(type);
    961  }
    962 
    963  override async setViewport(viewport: Viewport | null): Promise<void> {
    964    const needsReload = await this.#emulationManager.emulateViewport(viewport);
    965    this.#viewport = viewport;
    966    if (needsReload) {
    967      await this.reload();
    968    }
    969  }
    970 
    971  override viewport(): Viewport | null {
    972    return this.#viewport;
    973  }
    974 
    975  override async evaluateOnNewDocument<
    976    Params extends unknown[],
    977    Func extends (...args: Params) => unknown = (...args: Params) => unknown,
    978  >(
    979    pageFunction: Func | string,
    980    ...args: Params
    981  ): Promise<NewDocumentScriptEvaluation> {
    982    const source = evaluationString(pageFunction, ...args);
    983    return await this.#frameManager.evaluateOnNewDocument(source);
    984  }
    985 
    986  override async removeScriptToEvaluateOnNewDocument(
    987    identifier: string,
    988  ): Promise<void> {
    989    return await this.#frameManager.removeScriptToEvaluateOnNewDocument(
    990      identifier,
    991    );
    992  }
    993 
    994  override async setCacheEnabled(enabled = true): Promise<void> {
    995    await this.#frameManager.networkManager.setCacheEnabled(enabled);
    996  }
    997 
    998  override async _screenshot(
    999    options: Readonly<ScreenshotOptions>,
   1000  ): Promise<string> {
   1001    const {
   1002      fromSurface,
   1003      omitBackground,
   1004      optimizeForSpeed,
   1005      quality,
   1006      clip: userClip,
   1007      type,
   1008      captureBeyondViewport,
   1009    } = options;
   1010 
   1011    await using stack = new AsyncDisposableStack();
   1012    if (omitBackground && (type === 'png' || type === 'webp')) {
   1013      await this.#emulationManager.setTransparentBackgroundColor();
   1014      stack.defer(async () => {
   1015        await this.#emulationManager
   1016          .resetDefaultBackgroundColor()
   1017          .catch(debugError);
   1018      });
   1019    }
   1020 
   1021    let clip = userClip;
   1022    if (clip && !captureBeyondViewport) {
   1023      const viewport = await this.mainFrame()
   1024        .isolatedRealm()
   1025        .evaluate(() => {
   1026          const {
   1027            height,
   1028            pageLeft: x,
   1029            pageTop: y,
   1030            width,
   1031          } = window.visualViewport!;
   1032          return {x, y, height, width};
   1033        });
   1034      clip = getIntersectionRect(clip, viewport);
   1035    }
   1036 
   1037    const {data} = await this.#primaryTargetClient.send(
   1038      'Page.captureScreenshot',
   1039      {
   1040        format: type,
   1041        optimizeForSpeed,
   1042        fromSurface,
   1043        ...(quality !== undefined ? {quality: Math.round(quality)} : {}),
   1044        ...(clip ? {clip: {...clip, scale: clip.scale ?? 1}} : {}),
   1045        captureBeyondViewport,
   1046      },
   1047    );
   1048    return data;
   1049  }
   1050 
   1051  override async createPDFStream(
   1052    options: PDFOptions = {},
   1053  ): Promise<ReadableStream<Uint8Array>> {
   1054    const {timeout: ms = this._timeoutSettings.timeout()} = options;
   1055    const {
   1056      landscape,
   1057      displayHeaderFooter,
   1058      headerTemplate,
   1059      footerTemplate,
   1060      printBackground,
   1061      scale,
   1062      width: paperWidth,
   1063      height: paperHeight,
   1064      margin,
   1065      pageRanges,
   1066      preferCSSPageSize,
   1067      omitBackground,
   1068      tagged: generateTaggedPDF,
   1069      outline: generateDocumentOutline,
   1070      waitForFonts,
   1071    } = parsePDFOptions(options);
   1072 
   1073    if (omitBackground) {
   1074      await this.#emulationManager.setTransparentBackgroundColor();
   1075    }
   1076 
   1077    if (waitForFonts) {
   1078      await firstValueFrom(
   1079        from(
   1080          this.mainFrame()
   1081            .isolatedRealm()
   1082            .evaluate(() => {
   1083              return document.fonts.ready;
   1084            }),
   1085        ).pipe(raceWith(timeout(ms))),
   1086      );
   1087    }
   1088 
   1089    const printCommandPromise = this.#primaryTargetClient.send(
   1090      'Page.printToPDF',
   1091      {
   1092        transferMode: 'ReturnAsStream',
   1093        landscape,
   1094        displayHeaderFooter,
   1095        headerTemplate,
   1096        footerTemplate,
   1097        printBackground,
   1098        scale,
   1099        paperWidth,
   1100        paperHeight,
   1101        marginTop: margin.top,
   1102        marginBottom: margin.bottom,
   1103        marginLeft: margin.left,
   1104        marginRight: margin.right,
   1105        pageRanges,
   1106        preferCSSPageSize,
   1107        generateTaggedPDF,
   1108        generateDocumentOutline,
   1109      },
   1110    );
   1111 
   1112    const result = await firstValueFrom(
   1113      from(printCommandPromise).pipe(raceWith(timeout(ms))),
   1114    );
   1115 
   1116    if (omitBackground) {
   1117      await this.#emulationManager.resetDefaultBackgroundColor();
   1118    }
   1119 
   1120    assert(result.stream, '`stream` is missing from `Page.printToPDF');
   1121    return await getReadableFromProtocolStream(
   1122      this.#primaryTargetClient,
   1123      result.stream,
   1124    );
   1125  }
   1126 
   1127  override async pdf(options: PDFOptions = {}): Promise<Uint8Array> {
   1128    const {path = undefined} = options;
   1129    const readable = await this.createPDFStream(options);
   1130    const typedArray = await getReadableAsTypedArray(readable, path);
   1131    assert(typedArray, 'Could not create typed array');
   1132    return typedArray;
   1133  }
   1134 
   1135  override async close(
   1136    options: {runBeforeUnload?: boolean} = {runBeforeUnload: undefined},
   1137  ): Promise<void> {
   1138    using _guard = await this.browserContext().waitForScreenshotOperations();
   1139    const connection = this.#primaryTargetClient.connection();
   1140    assert(
   1141      connection,
   1142      'Protocol error: Connection closed. Most likely the page has been closed.',
   1143    );
   1144    const runBeforeUnload = !!options.runBeforeUnload;
   1145    if (runBeforeUnload) {
   1146      await this.#primaryTargetClient.send('Page.close');
   1147    } else {
   1148      await connection.send('Target.closeTarget', {
   1149        targetId: this.#primaryTarget._targetId,
   1150      });
   1151      await this.#tabTarget._isClosedDeferred.valueOrThrow();
   1152    }
   1153  }
   1154 
   1155  override isClosed(): boolean {
   1156    return this.#closed;
   1157  }
   1158 
   1159  override get mouse(): CdpMouse {
   1160    return this.#mouse;
   1161  }
   1162 
   1163  /**
   1164   * This method is typically coupled with an action that triggers a device
   1165   * request from an api such as WebBluetooth.
   1166   *
   1167   * :::caution
   1168   *
   1169   * This must be called before the device request is made. It will not return a
   1170   * currently active device prompt.
   1171   *
   1172   * :::
   1173   *
   1174   * @example
   1175   *
   1176   * ```ts
   1177   * const [devicePrompt] = Promise.all([
   1178   *   page.waitForDevicePrompt(),
   1179   *   page.click('#connect-bluetooth'),
   1180   * ]);
   1181   * await devicePrompt.select(
   1182   *   await devicePrompt.waitForDevice(({name}) => name.includes('My Device')),
   1183   * );
   1184   * ```
   1185   */
   1186  override async waitForDevicePrompt(
   1187    options: WaitTimeoutOptions = {},
   1188  ): Promise<DeviceRequestPrompt> {
   1189    return await this.mainFrame().waitForDevicePrompt(options);
   1190  }
   1191 }
   1192 
   1193 const supportedMetrics = new Set<string>([
   1194  'Timestamp',
   1195  'Documents',
   1196  'Frames',
   1197  'JSEventListeners',
   1198  'Nodes',
   1199  'LayoutCount',
   1200  'RecalcStyleCount',
   1201  'LayoutDuration',
   1202  'RecalcStyleDuration',
   1203  'ScriptDuration',
   1204  'TaskDuration',
   1205  'JSHeapUsedSize',
   1206  'JSHeapTotalSize',
   1207 ]);
   1208 
   1209 /** @see https://w3c.github.io/webdriver-bidi/#rectangle-intersection */
   1210 function getIntersectionRect(
   1211  clip: Readonly<ScreenshotClip>,
   1212  viewport: Readonly<Protocol.DOM.Rect>,
   1213 ): ScreenshotClip {
   1214  // Note these will already be normalized.
   1215  const x = Math.max(clip.x, viewport.x);
   1216  const y = Math.max(clip.y, viewport.y);
   1217  return {
   1218    x,
   1219    y,
   1220    width: Math.max(
   1221      Math.min(clip.x + clip.width, viewport.x + viewport.width) - x,
   1222      0,
   1223    ),
   1224    height: Math.max(
   1225      Math.min(clip.y + clip.height, viewport.y + viewport.height) - y,
   1226      0,
   1227    ),
   1228  };
   1229 }
   1230 
   1231 export function convertCookiesPartitionKeyFromPuppeteerToCdp(
   1232  partitionKey: CookiePartitionKey | string | undefined,
   1233 ): Protocol.Network.CookiePartitionKey | undefined {
   1234  if (partitionKey === undefined) {
   1235    return undefined;
   1236  }
   1237  if (typeof partitionKey === 'string') {
   1238    return {
   1239      topLevelSite: partitionKey,
   1240      hasCrossSiteAncestor: false,
   1241    };
   1242  }
   1243  return {
   1244    topLevelSite: partitionKey.sourceOrigin,
   1245    hasCrossSiteAncestor: partitionKey.hasCrossSiteAncestor ?? false,
   1246  };
   1247 }