repo_description: Chrome DevTools for coding agents
repo_topics: browser chrome chrome-devtools debugging devtools mcp mcp-server puppeteer
code: /**
   * @license
   * Copyright 2025 Google LLC
   * SPDX-License-Identifier: Apache-2.0
   */
  
  import type {
    Dialog,
    ElementHandle,
    Page,
    Viewport,
    WebMCPTool,
  } from './third_party/index.js';
  import type {ToolGroup, ToolDefinition} from './tools/inPage.js';
  import {takeSnapshot} from './tools/snapshot.js';
  import type {ContextPage} from './tools/ToolDefinition.js';
  import type {
    EmulationSettings,
    GeolocationOptions,
    TextSnapshot,
    TextSnapshotNode,
  } from './types.js';
  import {
    getNetworkMultiplierFromString,
    WaitForHelper,
  } from './WaitForHelper.js';
  
  /**
   * Per-page state wrapper. Consolidates dialog, snapshot, emulation,
   * and metadata that were previously scattered across Maps in McpContext.
   *
   * Internal class consumed only by McpContext. Fields are public for direct
   * read/write access. The dialog field is private because it requires an
   * event listener lifecycle managed by the constructor/dispose pair.
   */
  export class McpPage implements ContextPage {
    readonly pptrPage: Page;
    readonly id: number;
  
    // Snapshot
    textSnapshot: TextSnapshot | null = null;
    uniqueBackendNodeIdToMcpId = new Map<string, string>();
  
    // Emulation
    emulationSettings: EmulationSettings = {};
  
    // Metadata
    isolatedContextName?: string;
    devToolsPage?: Page;
  
    // Dialog
    #dialog?: Dialog;
    #dialogHandler: (dialog: Dialog) => void;
  
    inPageTools: ToolGroup<ToolDefinition> | undefined;
  
    constructor(page: Page, id: number) {
      this.pptrPage = page;
      this.id = id;
      this.#dialogHandler = (dialog: Dialog): void => {
        this.#dialog = dialog;
      };
      page.on('dialog', this.#dialogHandler);
    }
  
    get dialog(): Dialog | undefined {
      return this.#dialog;
    }
  
    getDialog(): Dialog | undefined {
      return this.dialog;
    }
  
    clearDialog(): void {
      this.#dialog = undefined;
    }
  
    getInPageTools(): ToolGroup<ToolDefinition> | undefined {
      return this.inPageTools;
    }
  
    getWebMcpTools(): WebMCPTool[] {
      return this.pptrPage.webmcp.tools();
    }
  
    get networkConditions(): string | null {
      return this.emulationSettings.networkConditions ?? null;
    }
  
    get cpuThrottlingRate(): number {
      return this.emulationSettings.cpuThrottlingRate ?? 1;
    }
  
    get geolocation(): GeolocationOptions | null {
      return this.emulationSettings.geolocation ?? null;
    }
  
    get viewport(): Viewport | null {
      return this.emulationSettings.viewport ?? null;
    }
  
    get userAgent(): string | null {
      return this.emulationSettings.userAgent ?? null;
    }
  
    get colorScheme(): 'dark' | 'light' | null {
      return this.emulationSettings.colorScheme ?? null;
    }
  
    // Public for testability: tests spy on this method to verify throttle multipliers.
    createWaitForHelper(
      cpuMultiplier: number,
      networkMultiplier: number,
    ): WaitForHelper {
      return new WaitForHelper(this.pptrPage, cpuMultiplier, networkMultiplier);
    }
  
    waitForEventsAfterAction(
      action: () => Promise<unknown>,
      options?: {timeout?: number},
    ): Promise<void> {
      const helper = this.createWaitForHelper(
        this.cpuThrottlingRate,
        getNetworkMultiplierFromString(this.networkConditions),
      );
      return helper.waitForEventsAfterAction(action, options);
    }
  
    dispose(): void {
      this.pptrPage.off('dialog', this.#dialogHandler);
    }
  
    async getElementByUid(uid: string): Promise<ElementHandle<Element>> {
      if (!this.textSnapshot) {
        throw new Error(
          `No snapshot found for page ${this.id ?? '?'}. Use ${takeSnapshot.name} to capture one.`,
        );
      }
      const node = this.textSnapshot.idToNode.get(uid);
      if (!node) {
        throw new Error(`Element uid "${uid}" not found on page ${this.id}.`);
      }
      return this.#resolveElementHandle(node, uid);
    }
  
    async #resolveElementHandle(
      node: TextSnapshotNode,
      uid: string,
    ): Promise<ElementHandle<Element>> {
      const message = `Element with uid ${uid} no longer exists on the page.`;
      try {
        const handle = await node.elementHandle();
        if (!handle) {
          throw new Error(message);
        }
        return handle;
      } catch (error) {
        throw new Error(message, {
          cause: error,
        });
      }
    }
  
    getAXNodeByUid(uid: string) {
      return this.textSnapshot?.idToNode.get(uid);
    }
  }
  