UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

869 lines • 36 kB
// 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