openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
869 lines • 36 kB
JavaScript
// Modified by OpenClaw: temporary snapshot-identity backport; see OPENCLAW_PATCH_NOTICE.md.
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
if (value !== null && value !== void 0) {
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
var dispose, inner;
if (async) {
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
dispose = value[Symbol.asyncDispose];
}
if (dispose === void 0) {
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
dispose = value[Symbol.dispose];
if (async) inner = dispose;
}
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
env.stack.push({ value: value, dispose: dispose, async: async });
}
else if (async) {
env.stack.push({ async: true });
}
return value;
};
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
return function (env) {
function fail(e) {
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
env.hasError = true;
}
var r, s = 0;
function next() {
while (r = env.stack.pop()) {
try {
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
if (r.dispose) {
var result = r.dispose.call(r.value);
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
}
else s |= 1;
}
catch (e) {
fail(e);
}
}
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
if (env.hasError) throw env.error;
}
return next();
};
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
});
export function replaceHtmlElementsWithUids(schema) {
if (typeof schema === 'boolean') {
return;
}
let isHtmlElement = false;
for (const [key, value] of Object.entries(schema)) {
if (key === 'x-mcp-type' && value === 'HTMLElement') {
isHtmlElement = true;
break;
}
}
if (isHtmlElement) {
schema.properties = { uid: { type: 'string' } };
schema.required = ['uid'];
}
if (schema.properties) {
for (const key of Object.keys(schema.properties)) {
replaceHtmlElementsWithUids(schema.properties[key]);
}
}
if (schema.items) {
if (Array.isArray(schema.items)) {
for (const item of schema.items) {
replaceHtmlElementsWithUids(item);
}
}
else {
replaceHtmlElementsWithUids(schema.items);
}
}
if (schema.anyOf) {
for (const s of schema.anyOf) {
replaceHtmlElementsWithUids(s);
}
}
if (schema.allOf) {
for (const s of schema.allOf) {
replaceHtmlElementsWithUids(s);
}
}
if (schema.oneOf) {
for (const s of schema.oneOf) {
replaceHtmlElementsWithUids(s);
}
}
}
import { DevToolsCommentBridge } from './devtools/DevToolsCommentBridge.js';
import { createTargetUniverse, } from './devtools/DevtoolsUtils.js';
import { ConsoleCollector, NetworkCollector, } from './collectors/PageCollector.js';
import { TextSnapshot } from './TextSnapshot.js';
import { PredefinedNetworkConditions, DevTools, CdpFrame, } from './third_party/index.js';
const DEFAULT_TIMEOUT = 5_000;
const NAVIGATION_TIMEOUT = 10_000;
import { logger } from './utils/logger.js';
import { getNetworkMultiplierFromString, WaitForHelper, } from './utils/WaitForHelper.js';
function isBackendNodeId(id) {
return typeof id === 'number';
}
/**
* 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 {
pptrPage;
id;
// Snapshot
textSnapshot = null;
uniqueBackendNodeIdToMcpId = new Map();
extraHandles = [];
// Emulation
emulationSettings = {};
// Metadata
isolatedContextName;
#devtoolsUniverse;
// Dialog
#dialog;
#dialogHandler;
thirdPartyDeveloperTools = [];
networkCollector;
consoleCollector;
#hasNetworkBlockOrAllowlist;
#locatorClass;
#navigationTimeout;
#sourceMaps;
#commentBridge;
#onNotification;
constructor(page, id, options) {
this.#hasNetworkBlockOrAllowlist = options.hasNetworkBlockOrAllowlist;
this.#locatorClass = options.locatorClass;
this.#navigationTimeout = options.navigationTimeout ?? NAVIGATION_TIMEOUT;
this.#sourceMaps = options.sourceMaps ?? true;
this.#onNotification = options.onNotification;
this.pptrPage = page;
this.id = id;
this.isolatedContextName = options.isolatedContextName;
this.#dialogHandler = (dialog) => {
this.#dialog = dialog;
};
page.on('dialog', this.#dialogHandler);
this.networkCollector = new NetworkCollector(page);
this.consoleCollector = new ConsoleCollector(page, collect => {
return {
console: event => {
collect(event);
},
uncaughtError: event => {
collect(event);
},
devtoolsAggregatedIssue: event => {
collect(event);
},
};
});
}
async init() {
await Promise.allSettled([
this.#initDevToolsUniverseNoThrow(),
this.#initFocusEmulationNoThrow(),
]);
}
async #initFocusEmulationNoThrow() {
// We emulate a focused page for all pages to support multi-agent workflows.
void this.pptrPage.emulateFocusedPage(true).catch(error => {
logger?.('Error turning on focused page emulation', error);
});
}
async #initDevToolsUniverseNoThrow() {
if (this.#devtoolsUniverse) {
return undefined;
}
try {
const session = await this.pptrPage.createCDPSession();
this.#devtoolsUniverse = await createTargetUniverse(session, {
sourceMaps: this.#sourceMaps,
});
}
catch (e) {
logger?.('Failed to initialize DevTools universe', e);
}
}
get devtoolsUniverse() {
return this.#devtoolsUniverse;
}
getDialog() {
return this.#dialog;
}
clearDialog() {
this.#dialog = undefined;
}
throwIfDialogOpen() {
if (this.#dialog && !this.#dialog.handled) {
throw new Error(`A dialog is open (${this.#dialog.type()}: ${this.#dialog.message()}).`);
}
}
getThirdPartyDeveloperTools() {
return this.thirdPartyDeveloperTools;
}
async getToolGroups() {
const env_1 = { stack: [], error: void 0, hasError: false };
try {
// Check if there is a `devtoolstooldiscovery` event listener
const windowHandle = __addDisposableResource(env_1, await this.pptrPage.evaluateHandle(() => window), false);
// @ts-expect-error internal API
const client = this.pptrPage._client();
const { listeners } = await client.send('DOMDebugger.getEventListeners', {
objectId: windowHandle.remoteObject().objectId,
});
if (listeners.find(l => l.type === 'devtoolstooldiscovery') === undefined) {
return [];
}
const toolGroups = await this.pptrPage.evaluate(() => {
if (window.__dtmcp) {
window.__dtmcp.toolGroups = [];
}
return new Promise(resolve => {
const event = new CustomEvent('devtoolstooldiscovery');
const groups = [];
// @ts-expect-error Adding custom property
event.respondWith = toolGroup => {
if (!window.__dtmcp) {
window.__dtmcp = {};
}
if (!window.__dtmcp.toolGroups) {
window.__dtmcp.toolGroups = [];
}
if (typeof toolGroup.name !== 'string' ||
(toolGroup.description &&
typeof toolGroup.description !== 'string') ||
!Array.isArray(toolGroup.tools)) {
console.error('Invalid toolGroup:', toolGroup);
return;
}
for (const tool of toolGroup.tools) {
if (typeof tool.name !== 'string' ||
typeof tool.description !== 'string' ||
typeof tool.inputSchema !== 'object' ||
typeof tool.execute !== 'function') {
console.error('Invalid tool:', tool);
return;
}
}
window.__dtmcp.toolGroups.push(toolGroup);
// When receiving a toolGroup for the first time, expose a simple execution helper
if (!window.__dtmcp.executeTool) {
window.__dtmcp.executeTool = async (toolName, args) => {
if (!window.__dtmcp?.toolGroups ||
window.__dtmcp.toolGroups.length === 0) {
throw new Error('No tools found on the page');
}
for (const group of window.__dtmcp.toolGroups) {
const tool = group.tools?.find(t => t.name === toolName);
if (tool) {
return await tool.execute(args);
}
}
throw new Error(`Tool ${toolName} not found`);
};
}
groups.push(toolGroup);
};
window.dispatchEvent(event);
// If at least one toolGroup was added synchronously, resolve with the array.
// Otherwise, use setTimeout to allow for any microtask/asynchronous respondWith calls, or resolve with an empty array.
if (groups.length > 0) {
resolve(groups);
}
else {
setTimeout(() => {
if (groups.length > 0) {
resolve(groups);
}
else {
resolve([]);
}
}, 0);
}
});
});
for (const group of toolGroups) {
for (const tool of group.tools ?? []) {
replaceHtmlElementsWithUids(tool.inputSchema);
}
}
this.thirdPartyDeveloperTools = toolGroups;
return toolGroups;
}
catch (e_1) {
env_1.error = e_1;
env_1.hasError = true;
}
finally {
__disposeResources(env_1);
}
}
getWebMcpTools() {
return this.pptrPage.webmcp.tools();
}
resolveCdpRequestId(cdpRequestId) {
if (!cdpRequestId) {
logger?.('no network request');
return;
}
const request = this.networkCollector.find(request => {
// @ts-expect-error id is internal.
return request.id === cdpRequestId;
});
if (!request) {
logger?.('no network request for ' + cdpRequestId);
return;
}
return this.networkCollector.getIdForResource(request);
}
resolveReqidToCdpRequestId(reqid) {
const request = this.networkCollector.getById(reqid);
if (!request) {
return undefined;
}
// @ts-expect-error id is internal.
return request.id;
}
getNetworkRequests(includePreservedRequests) {
return this.networkCollector.getData(includePreservedRequests);
}
get commentBridge() {
return this.#commentBridge;
}
async ensureDevToolsCommentBridge(devtoolsPage) {
if (!this.#commentBridge) {
this.#commentBridge = new DevToolsCommentBridge({
onNotification: this.#onNotification,
});
}
await this.#commentBridge.attach(devtoolsPage);
return this.#commentBridge;
}
async getDevToolsPage() {
try {
if (await this.pptrPage.hasDevTools()) {
return await this.pptrPage.openDevTools();
}
return undefined;
}
catch {
// Prior to Chrome 144.0.7559.59, the command fails,
// Some Electron apps still use older version
// Fall back to not exposing DevTools at all.
return undefined;
}
}
async openDevTools() {
const devtoolsPage = await this.pptrPage.openDevTools();
await this.ensureDevToolsCommentBridge(devtoolsPage);
return devtoolsPage;
}
getConsoleData(includePreservedMessages) {
return this.consoleCollector.getData(includePreservedMessages);
}
getConsoleMessageById(id) {
return this.consoleCollector.getById(id);
}
getNetworkRequestById(reqid) {
return this.networkCollector.getById(reqid);
}
get networkConditions() {
return this.emulationSettings.networkConditions ?? null;
}
get cpuThrottlingRate() {
return this.emulationSettings.cpuThrottlingRate ?? 1;
}
get geolocation() {
return this.emulationSettings.geolocation ?? null;
}
get viewport() {
return this.emulationSettings.viewport ?? null;
}
get userAgent() {
return this.emulationSettings.userAgent ?? null;
}
get colorScheme() {
return this.emulationSettings.colorScheme ?? null;
}
// Public for testability: tests spy on this method to verify throttle multipliers.
createWaitForHelper(cpuMultiplier, networkMultiplier) {
return new WaitForHelper(this.pptrPage, cpuMultiplier, networkMultiplier);
}
waitForEventsAfterAction(action, options) {
const helper = this.createWaitForHelper(this.cpuThrottlingRate, getNetworkMultiplierFromString(this.networkConditions));
return helper.waitForEventsAfterAction(action, options);
}
dispose() {
this.#commentBridge?.dispose();
this.#commentBridge = undefined;
this.pptrPage.off('dialog', this.#dialogHandler);
this.networkCollector.dispose();
this.consoleCollector.dispose();
const devtoolsUniverse = this.#devtoolsUniverse;
this.#devtoolsUniverse = undefined;
devtoolsUniverse?.universe.dispose();
void devtoolsUniverse?.session.detach().catch(e => {
logger?.('Failed to detach DevTools session', e);
});
}
async executeThirdPartyDeveloperTool(toolName, params, response) {
// Creates array of ElementHandles from the UIDs in the params.
// We do not replace the uids with the ElementsHandles yet, because
// the `evaluate` function only turns them into DOM elements if they
// are passed as non-nested arguments.
const handles = [];
for (const value of Object.values(params)) {
if (value instanceof Object &&
'uid' in value &&
typeof value.uid === 'string' &&
Object.keys(value).length === 1) {
handles.push(await this.getElementByUid(value.uid));
}
}
const result = await this.pptrPage.evaluate(async (name, args, ...elements) => {
// Replace the UIDs with DOM elements.
for (const [key, value] of Object.entries(args)) {
if (value instanceof Object &&
'uid' in value &&
typeof value.uid === 'string' &&
Object.keys(value).length === 1) {
args[key] = elements.shift();
}
}
if (!window.__dtmcp?.executeTool) {
throw new Error('No tools found on the page');
}
const toolResult = await window.__dtmcp.executeTool(name, args);
const stashDOMElement = (el) => {
if (!window.__dtmcp) {
window.__dtmcp = {};
}
if (window.__dtmcp.stashedElements === undefined) {
window.__dtmcp.stashedElements = [];
}
window.__dtmcp.stashedElements.push(el);
return {
stashedId: `stashed-${window.__dtmcp.stashedElements.length - 1}`,
};
};
const ancestors = [];
// Recursively walks the tool result:
// - Replaces DOM elements with an ID and stashes the DOM element on the window object
// - Replaces non-plain objects with a string representation of the object
// - Replaces circular references with the string '<Circular reference>'
// - Replaces functions with the string '<Function object>'
const processToolResult = (data, parentEl) => {
// 1. Handle DOM Elements
if (data instanceof Element) {
return stashDOMElement(data);
}
// 2. Handle Arrays
if (Array.isArray(data)) {
return data.map((item) => processToolResult(item, parentEl));
}
// 3. Handle Objects
if (data !== null && typeof data === 'object') {
while (ancestors.length > 0 && ancestors.at(-1) !== parentEl) {
ancestors.pop();
}
if (ancestors.includes(data)) {
return '<Circular reference>';
}
ancestors.push(data);
// If not a plain object, return a string representation of the object
if (Object.getPrototypeOf(data) !== Object.prototype) {
return `<${data.constructor.name} instance>`;
}
const processedObj = {};
for (const [key, value] of Object.entries(data)) {
processedObj[key] = processToolResult(value, data);
}
return processedObj;
}
// 4. Handle Functions
if (typeof data === 'function') {
return '<Function object>';
}
// 5. Return primitives (strings, numbers, booleans) as-is
return data;
};
return {
result: processToolResult(toolResult),
stashed: window.__dtmcp?.stashedElements?.length ?? 0,
};
}, toolName, params, ...handles);
const elementHandles = [];
for (let i = 0; i < (result.stashed ?? 0); i++) {
const elementHandle = await this.pptrPage.evaluateHandle(index => {
const el = window.__dtmcp?.stashedElements?.[index];
if (!el) {
throw new Error(`Stashed element at index ${index} not found`);
}
return el;
}, i);
elementHandles.push(elementHandle);
}
await this.pptrPage.evaluate(() => {
if (window.__dtmcp) {
window.__dtmcp.stashedElements = undefined;
}
});
if (elementHandles.length) {
const env_2 = { stack: [], error: void 0, hasError: false };
try {
const stack = __addDisposableResource(env_2, new DisposableStack(), false);
for (const handle of this.extraHandles) {
stack.use(handle);
}
this.textSnapshot = await TextSnapshot.create(this, {
extraHandles: elementHandles,
});
response.includeSnapshot();
}
catch (e_2) {
env_2.error = e_2;
env_2.hasError = true;
}
finally {
__disposeResources(env_2);
}
}
const cdpElementIds = await Promise.all(elementHandles.map(async (elementHandle, index) => {
const backendNodeId = await elementHandle.backendNodeId();
if (!backendNodeId) {
logger?.(`No backendNodeId for stashed DOM element with index ${index}`);
return `stashed-${index}`;
}
const cdpElementId = this.textSnapshot?.resolveCdpElementId(backendNodeId, elementHandle.frame);
if (!cdpElementId) {
logger?.(`Could not get cdpElementId for backend node ${backendNodeId}`);
return `stashed-${index}`;
}
return cdpElementId;
}));
const recursivelyReplaceStashedElements = (node) => {
if (Array.isArray(node)) {
return node.map(x => recursivelyReplaceStashedElements(x));
}
if (node !== null && typeof node === 'object') {
if ('stashedId' in node &&
typeof node.stashedId === 'string' &&
node.stashedId.startsWith('stashed-') &&
Object.keys(node).length === 1) {
const index = parseInt(node.stashedId.split('-')[1]);
return { uid: cdpElementIds[index] };
}
const resultObj = {};
for (const [key, value] of Object.entries(node)) {
resultObj[key] = recursivelyReplaceStashedElements(value);
}
return resultObj;
}
return node;
};
const resultWithUids = recursivelyReplaceStashedElements(result.result);
response.appendResponseLine(JSON.stringify(resultWithUids, null, 2));
}
async getElementByUid(uid) {
if (!this.textSnapshot) {
throw new Error(`No snapshot found for page ${this.id ?? '?'}. Use take_snapshot 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, uid) {
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) {
return this.textSnapshot?.idToNode.get(uid);
}
async resolveBackendNodeId(backendNodeId) {
if (!this.textSnapshot) {
this.textSnapshot = await TextSnapshot.create(this);
}
let id = this.textSnapshot.resolveCdpElementId(backendNodeId);
if (!id) {
this.textSnapshot = await TextSnapshot.create(this);
id = this.textSnapshot.resolveCdpElementId(backendNodeId);
}
return id;
}
async resolveUidToBackendNodeId(uid) {
const target = this.pptrPage.target();
const targetId = Boolean(target) &&
'_targetId' in target &&
typeof target._targetId === 'string'
? target._targetId
: undefined;
const node = this.getAXNodeByUid(uid);
if (node?.backendNodeId !== undefined) {
return { backendNodeId: node.backendNodeId, targetId };
}
try {
const handle = await this.getElementByUid(uid);
const backendNodeId = await handle.backendNodeId();
return backendNodeId ? { backendNodeId, targetId } : undefined;
}
catch {
return undefined;
}
}
async getMatchedStylesForUid(uid) {
const env_3 = { stack: [], error: void 0, hasError: false };
try {
if (!this.textSnapshot) {
throw new Error(`No snapshot found for page ${this.id ?? '?'}. Use take_snapshot to capture one.`);
}
const node = this.textSnapshot.idToNode.get(uid);
if (!node) {
throw new Error(`Element uid "${uid}" not found on page ${this.id}.`);
}
const snapshotBackendNodeId = node.backendNodeId;
if (!isBackendNodeId(snapshotBackendNodeId)) {
throw new Error(`Failed to resolve backend node ID for element with uid "${uid}".`);
}
const handle = __addDisposableResource(env_3, await this.#resolveElementHandle(node, uid), false);
const liveBackendNodeId = await handle.backendNodeId();
if (liveBackendNodeId !== snapshotBackendNodeId) {
throw new Error(`Element with uid "${uid}" was detached or no longer exists on the page. Please take a new snapshot with take_snapshot.`);
}
const backendNodeId = snapshotBackendNodeId;
if (!this.#devtoolsUniverse) {
throw new Error(`DevTools universe is not available for page ${this.id ?? '?'}.`);
}
const targetManager = this.#devtoolsUniverse.universe.context.get(DevTools.TargetManager);
const frame = handle.frame;
const mainFrame = this.pptrPage.mainFrame();
const frameTarget = frame instanceof CdpFrame ? targetManager.targetById(frame._id) : null;
const target = frameTarget ??
(frame instanceof CdpFrame &&
mainFrame instanceof CdpFrame &&
frame.client === mainFrame.client
? targetManager.primaryPageTarget()
: null);
const dom = target?.model(DevTools.DOMModel.DOMModel);
const nodeMap = await dom?.pushNodesByBackendIdsToFrontend(new Set([backendNodeId]));
const domNode = nodeMap?.get(backendNodeId);
const cssModel = dom?.target().model(DevTools.CSSModel.CSSModel);
if (!domNode || !cssModel) {
throw new Error(`Element with uid "${uid}" was detached or no longer exists on the page. Please take a new snapshot with take_snapshot.`);
}
const targetElement = domNode.enclosingElementOrSelf();
if (!targetElement) {
throw new Error(`Element with uid "${uid}" is not an element node and has no parent element.`);
}
const matchedStyles = await cssModel.getMatchedStyles(targetElement.id);
if (!matchedStyles) {
throw new Error(`Could not retrieve matched styles for element with uid "${uid}".`);
}
return matchedStyles;
}
catch (e_3) {
env_3.error = e_3;
env_3.hasError = true;
}
finally {
__disposeResources(env_3);
}
}
async getDevToolsData() {
try {
logger?.('Getting DevTools UI data');
const devtoolsPage = await this.getDevToolsPage();
if (!devtoolsPage) {
logger?.('No DevTools page detected');
return {};
}
const { cdpRequestId, cdpBackendNodeId } = await devtoolsPage.evaluate(async () => {
// @ts-expect-error no types
const UI = await import('/bundled/ui/legacy/legacy.js');
// @ts-expect-error no types
const SDK = await import('/bundled/core/sdk/sdk.js');
const request = UI.Context.Context.instance().flavor(SDK.NetworkRequest.NetworkRequest);
const node = UI.Context.Context.instance().flavor(SDK.DOMModel.DOMNode);
return {
cdpRequestId: request?.requestId(),
cdpBackendNodeId: node?.backendNodeId(),
};
});
return { cdpBackendNodeId, cdpRequestId };
}
catch (err) {
logger?.('error getting devtools data', err);
}
return {};
}
async restoreEmulation() {
const currentSetting = this.emulationSettings;
await this.emulate(currentSetting);
}
async emulate(options) {
const page = this.pptrPage;
const newSettings = { ...this.emulationSettings };
// Skip network emulation if blocklist/allowlist is configured, as it conflicts with blocking rules in Puppeteer.
if (this.#hasNetworkBlockOrAllowlist) {
if (options.networkConditions !== undefined) {
throw new Error('Network throttling is not supported when network blocking (allowlist/blocklist) is configured.');
}
}
else if (!options.networkConditions) {
await page.emulateNetworkConditions(null);
delete newSettings.networkConditions;
}
else if (options.networkConditions === 'Offline') {
await page.emulateNetworkConditions({
offline: true,
download: 0,
upload: 0,
latency: 0,
});
newSettings.networkConditions = 'Offline';
}
else if (options.networkConditions in PredefinedNetworkConditions) {
const networkCondition = PredefinedNetworkConditions[options.networkConditions];
await page.emulateNetworkConditions(networkCondition);
newSettings.networkConditions = options.networkConditions;
}
const secondarySession = this.devtoolsUniverse?.session;
if (!options.cpuThrottlingRate) {
await page.emulateCPUThrottling(1);
if (secondarySession) {
await secondarySession.send('Emulation.setCPUThrottlingRate', {
rate: 1,
});
}
delete newSettings.cpuThrottlingRate;
}
else {
await page.emulateCPUThrottling(options.cpuThrottlingRate);
if (secondarySession) {
await secondarySession.send('Emulation.setCPUThrottlingRate', {
rate: options.cpuThrottlingRate,
});
}
newSettings.cpuThrottlingRate = options.cpuThrottlingRate;
}
if (!options.geolocation) {
await page.setGeolocation({ latitude: 0, longitude: 0 });
delete newSettings.geolocation;
}
else {
await page.setGeolocation(options.geolocation);
newSettings.geolocation = options.geolocation;
}
if (!options.userAgent) {
await page.setUserAgent({ userAgent: undefined });
delete newSettings.userAgent;
}
else {
await page.setUserAgent({ userAgent: options.userAgent });
newSettings.userAgent = options.userAgent;
}
if (!options.colorScheme || options.colorScheme === 'auto') {
await page.emulateMediaFeatures([
{ name: 'prefers-color-scheme', value: '' },
]);
delete newSettings.colorScheme;
}
else {
await page.emulateMediaFeatures([
{ name: 'prefers-color-scheme', value: options.colorScheme },
]);
newSettings.colorScheme = options.colorScheme;
}
if (!options.viewport) {
delete newSettings.viewport;
}
else {
const defaults = {
deviceScaleFactor: 1,
isMobile: false,
hasTouch: false,
isLandscape: false,
};
newSettings.viewport = { ...defaults, ...options.viewport };
}
if (options.extraHttpHeaders !== undefined) {
await page.setExtraHTTPHeaders(options.extraHttpHeaders);
newSettings.extraHttpHeaders = options.extraHttpHeaders;
if (Object.keys(options.extraHttpHeaders).length === 0) {
delete newSettings.extraHttpHeaders;
}
}
this.emulationSettings = Object.keys(newSettings).length ? newSettings : {};
this.updateTimeouts();
// This should happen after updating the page timeouts.
// Setting the viewport can trigger a reload which we don't want to timeout.
await page.setViewport(newSettings.viewport ?? null);
}
updateTimeouts() {
// For waiters 5sec timeout should be sufficient.
// Increased in case we throttle the CPU
const cpuMultiplier = this.cpuThrottlingRate;
this.pptrPage.setDefaultTimeout(DEFAULT_TIMEOUT * cpuMultiplier);
// 10sec should be enough for the load event to be emitted during
// navigations.
// Increased in case we throttle the network requests or the CPU
const networkMultiplier = getNetworkMultiplierFromString(this.networkConditions);
this.pptrPage.setDefaultNavigationTimeout(this.#navigationTimeout * networkMultiplier * cpuMultiplier);
}
waitForTextOnPage(text, timeout) {
const frames = this.pptrPage.frames();
let locator = this.#locatorClass.race(frames.flatMap(frame => text.flatMap(value => [
frame.locator(`aria/${value}`),
frame.locator(`text/${value}`),
])));
if (timeout) {
locator = locator.setTimeout(timeout);
}
return locator.wait();
}
/**
* We need to ignore favicon request as they make our test flaky
*/
async setUpNetworkCollectorForTesting() {
this.networkCollector.dispose();
this.networkCollector = new NetworkCollector(this.pptrPage, undefined, collect => {
return {
request: req => {
if (req.url().includes('favicon.ico')) {
return;
}
collect(req);
},
};
});
}
}
//# sourceMappingURL=McpPage.js.map