/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {
Dialog,
ElementHandle,
Page,
Viewport,
} from './third_party/index.js';
import {takeSnapshot} from './tools/snapshot.js';
import type {ContextPage} from './tools/ToolDefinition.js';
import type {
EmulationSettings,
GeolocationOptions,
TextSnapshot,
TextSnapshotNode,
} from './types.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;
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;
}
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;
}
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);
}
}