UNPKG

chromium-bidi

Version:

An implementation of the WebDriver BiDi protocol for Chromium implemented as a JavaScript layer translating between BiDi and CDP, running inside a Chrome tab.

1,235 lines (1,223 loc) 835 kB
(function () { 'use strict'; function mitt(n){return {all:n=n||new Map,on:function(t,e){var i=n.get(t);i?i.push(e):n.set(t,[e]);},off:function(t,e){var i=n.get(t);i&&(e?i.splice(i.indexOf(e)>>>0,1):n.set(t,[]));},emit:function(t,e){var i=n.get(t);i&&i.slice().map(function(n){n(e);}),(i=n.get("*"))&&i.slice().map(function(n){n(t,e);});}}} /** * Copyright 2022 Google LLC. * Copyright (c) Microsoft Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ class EventEmitter { #emitter = mitt(); on(type, handler) { this.#emitter.on(type, handler); return this; } once(event, handler) { const onceHandler = (eventData) => { handler(eventData); this.off(event, onceHandler); }; return this.on(event, onceHandler); } off(type, handler) { this.#emitter.off(type, handler); return this; } emit(event, eventData) { this.#emitter.emit(event, eventData); } removeAllListeners(event) { if (event) { this.#emitter.all.delete(event); } else { this.#emitter.all.clear(); } return this; } } /** * Copyright 2021 Google LLC. * Copyright (c) Microsoft Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var LogType; (function (LogType) { LogType["bidi"] = "bidi"; LogType["cdp"] = "cdp"; LogType["debug"] = "debug"; LogType["debugError"] = "debug:error"; LogType["debugInfo"] = "debug:info"; LogType["debugWarn"] = "debug:warn"; })(LogType || (LogType = {})); /** * Copyright 2022 Google LLC. * Copyright (c) Microsoft Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var _a$6; class ProcessingQueue { static LOGGER_PREFIX = `${LogType.debug}:queue`; #logger; #processor; #queue = []; #isProcessing = false; constructor(processor, logger) { this.#processor = processor; this.#logger = logger; } add(entry, name) { this.#queue.push([entry, name]); void this.#processIfNeeded(); } async #processIfNeeded() { if (this.#isProcessing) { return; } this.#isProcessing = true; while (this.#queue.length > 0) { const arrayEntry = this.#queue.shift(); if (!arrayEntry) { continue; } const [entryPromise, name] = arrayEntry; this.#logger?.(_a$6.LOGGER_PREFIX, 'Processing event:', name); await entryPromise .then((entry) => { if (entry.kind === 'error') { this.#logger?.(LogType.debugError, 'Event threw before sending:', entry.error.message, entry.error.stack); return; } return this.#processor(entry.value); }) .catch((error) => { this.#logger?.(LogType.debugError, 'Event was not processed:', error?.message); }); } this.#isProcessing = false; } } _a$6 = ProcessingQueue; /** * Copyright 2023 Google LLC. * Copyright (c) Microsoft Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var BiDiModule; (function (BiDiModule) { BiDiModule["Bluetooth"] = "bluetooth"; BiDiModule["Browser"] = "browser"; BiDiModule["BrowsingContext"] = "browsingContext"; BiDiModule["Cdp"] = "goog:cdp"; BiDiModule["Input"] = "input"; BiDiModule["Log"] = "log"; BiDiModule["Network"] = "network"; BiDiModule["Script"] = "script"; BiDiModule["Session"] = "session"; BiDiModule["Speculation"] = "speculation"; })(BiDiModule || (BiDiModule = {})); var Script$2; (function (Script) { (function (EventNames) { EventNames["Message"] = "script.message"; EventNames["RealmCreated"] = "script.realmCreated"; EventNames["RealmDestroyed"] = "script.realmDestroyed"; })(Script.EventNames || (Script.EventNames = {})); })(Script$2 || (Script$2 = {})); var Log$1; (function (Log) { (function (EventNames) { EventNames["LogEntryAdded"] = "log.entryAdded"; })(Log.EventNames || (Log.EventNames = {})); })(Log$1 || (Log$1 = {})); var BrowsingContext$2; (function (BrowsingContext) { (function (EventNames) { EventNames["ContextCreated"] = "browsingContext.contextCreated"; EventNames["ContextDestroyed"] = "browsingContext.contextDestroyed"; EventNames["DomContentLoaded"] = "browsingContext.domContentLoaded"; EventNames["DownloadEnd"] = "browsingContext.downloadEnd"; EventNames["DownloadWillBegin"] = "browsingContext.downloadWillBegin"; EventNames["FragmentNavigated"] = "browsingContext.fragmentNavigated"; EventNames["HistoryUpdated"] = "browsingContext.historyUpdated"; EventNames["Load"] = "browsingContext.load"; EventNames["NavigationAborted"] = "browsingContext.navigationAborted"; EventNames["NavigationCommitted"] = "browsingContext.navigationCommitted"; EventNames["NavigationFailed"] = "browsingContext.navigationFailed"; EventNames["NavigationStarted"] = "browsingContext.navigationStarted"; EventNames["UserPromptClosed"] = "browsingContext.userPromptClosed"; EventNames["UserPromptOpened"] = "browsingContext.userPromptOpened"; })(BrowsingContext.EventNames || (BrowsingContext.EventNames = {})); })(BrowsingContext$2 || (BrowsingContext$2 = {})); var Input$2; (function (Input) { (function (EventNames) { EventNames["FileDialogOpened"] = "input.fileDialogOpened"; })(Input.EventNames || (Input.EventNames = {})); })(Input$2 || (Input$2 = {})); var Network$2; (function (Network) { (function (EventNames) { EventNames["AuthRequired"] = "network.authRequired"; EventNames["BeforeRequestSent"] = "network.beforeRequestSent"; EventNames["FetchError"] = "network.fetchError"; EventNames["ResponseCompleted"] = "network.responseCompleted"; EventNames["ResponseStarted"] = "network.responseStarted"; })(Network.EventNames || (Network.EventNames = {})); })(Network$2 || (Network$2 = {})); var Bluetooth$2; (function (Bluetooth) { (function (EventNames) { EventNames["RequestDevicePromptUpdated"] = "bluetooth.requestDevicePromptUpdated"; EventNames["GattConnectionAttempted"] = "bluetooth.gattConnectionAttempted"; EventNames["CharacteristicEventGenerated"] = "bluetooth.characteristicEventGenerated"; EventNames["DescriptorEventGenerated"] = "bluetooth.descriptorEventGenerated"; })(Bluetooth.EventNames || (Bluetooth.EventNames = {})); })(Bluetooth$2 || (Bluetooth$2 = {})); var Speculation; (function (Speculation) { (function (EventNames) { EventNames["PrefetchStatusUpdated"] = "speculation.prefetchStatusUpdated"; })(Speculation.EventNames || (Speculation.EventNames = {})); })(Speculation || (Speculation = {})); const EVENT_NAMES = new Set([ ...Object.values(BiDiModule), ...Object.values(Bluetooth$2.EventNames), ...Object.values(BrowsingContext$2.EventNames), ...Object.values(Input$2.EventNames), ...Object.values(Log$1.EventNames), ...Object.values(Network$2.EventNames), ...Object.values(Script$2.EventNames), ...Object.values(Speculation.EventNames), ]); class Exception extends Error { error; message; stacktrace; constructor(error, message, stacktrace) { super(); this.error = error; this.message = message; this.stacktrace = stacktrace; } toErrorResponse(commandId) { return { type: 'error', id: commandId, error: this.error, message: this.message, stacktrace: this.stacktrace, }; } } class InvalidArgumentException extends Exception { constructor(message, stacktrace) { super("invalid argument" , message, stacktrace); } } class InvalidSelectorException extends Exception { constructor(message, stacktrace) { super("invalid selector" , message, stacktrace); } } class MoveTargetOutOfBoundsException extends Exception { constructor(message, stacktrace) { super("move target out of bounds" , message, stacktrace); } } class NoSuchAlertException extends Exception { constructor(message, stacktrace) { super("no such alert" , message, stacktrace); } } class NoSuchElementException extends Exception { constructor(message, stacktrace) { super("no such element" , message, stacktrace); } } class NoSuchFrameException extends Exception { constructor(message, stacktrace) { super("no such frame" , message, stacktrace); } } class NoSuchHandleException extends Exception { constructor(message, stacktrace) { super("no such handle" , message, stacktrace); } } class NoSuchHistoryEntryException extends Exception { constructor(message, stacktrace) { super("no such history entry" , message, stacktrace); } } class NoSuchInterceptException extends Exception { constructor(message, stacktrace) { super("no such intercept" , message, stacktrace); } } class NoSuchNodeException extends Exception { constructor(message, stacktrace) { super("no such node" , message, stacktrace); } } class NoSuchRequestException extends Exception { constructor(message, stacktrace) { super("no such request" , message, stacktrace); } } class NoSuchScriptException extends Exception { constructor(message, stacktrace) { super("no such script" , message, stacktrace); } } class NoSuchUserContextException extends Exception { constructor(message, stacktrace) { super("no such user context" , message, stacktrace); } } class UnknownCommandException extends Exception { constructor(message, stacktrace) { super("unknown command" , message, stacktrace); } } class UnknownErrorException extends Exception { constructor(message, stacktrace = new Error().stack) { super("unknown error" , message, stacktrace); } } class UnableToCaptureScreenException extends Exception { constructor(message, stacktrace) { super("unable to capture screen" , message, stacktrace); } } class UnsupportedOperationException extends Exception { constructor(message, stacktrace) { super("unsupported operation" , message, stacktrace); } } class UnableToSetCookieException extends Exception { constructor(message, stacktrace) { super("unable to set cookie" , message, stacktrace); } } class UnableToSetFileInputException extends Exception { constructor(message, stacktrace) { super("unable to set file input" , message, stacktrace); } } class InvalidWebExtensionException extends Exception { constructor(message, stacktrace) { super("invalid web extension" , message, stacktrace); } } class NoSuchWebExtensionException extends Exception { constructor(message, stacktrace) { super("no such web extension" , message, stacktrace); } } class NoSuchNetworkCollectorException extends Exception { constructor(message, stacktrace) { super("no such network collector" , message, stacktrace); } } class NoSuchNetworkDataException extends Exception { constructor(message, stacktrace) { super("no such network data" , message, stacktrace); } } /** * Copyright 2023 Google LLC. * Copyright (c) Microsoft Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ class BidiNoOpParser { parseDisableSimulationParameters(params) { return params; } parseHandleRequestDevicePromptParams(params) { return params; } parseSimulateAdapterParameters(params) { return params; } parseSimulateAdvertisementParameters(params) { return params; } parseSimulateCharacteristicParameters(params) { return params; } parseSimulateCharacteristicResponseParameters(params) { return params; } parseSimulateDescriptorParameters(params) { return params; } parseSimulateDescriptorResponseParameters(params) { return params; } parseSimulateGattConnectionResponseParameters(params) { return params; } parseSimulateGattDisconnectionParameters(params) { return params; } parseSimulatePreconnectedPeripheralParameters(params) { return params; } parseSimulateServiceParameters(params) { return params; } parseCreateUserContextParameters(params) { return params; } parseRemoveUserContextParameters(params) { return params; } parseSetClientWindowStateParameters(params) { return params; } parseSetDownloadBehaviorParameters(params) { return params; } parseActivateParams(params) { return params; } parseCaptureScreenshotParams(params) { return params; } parseCloseParams(params) { return params; } parseCreateParams(params) { return params; } parseGetTreeParams(params) { return params; } parseHandleUserPromptParams(params) { return params; } parseLocateNodesParams(params) { return params; } parseNavigateParams(params) { return params; } parsePrintParams(params) { return params; } parseReloadParams(params) { return params; } parseSetBypassCspParams(params) { return params; } parseSetViewportParams(params) { return params; } parseTraverseHistoryParams(params) { return params; } parseGetSessionParams(params) { return params; } parseResolveRealmParams(params) { return params; } parseSendCommandParams(params) { return params; } parseSetClientHintsOverrideParams(params) { return params; } parseSetForcedColorsModeThemeOverrideParams(params) { return params; } parseSetGeolocationOverrideParams(params) { return params; } parseSetLocaleOverrideParams(params) { return params; } parseSetNetworkConditionsParams(params) { return params; } parseSetScreenOrientationOverrideParams(params) { return params; } parseSetScreenSettingsOverrideParams(params) { return params; } parseSetScriptingEnabledParams(params) { return params; } parseSetScrollbarTypeOverrideParams(params) { return params; } parseSetTimezoneOverrideParams(params) { return params; } parseSetTouchOverrideParams(params) { return params; } parseSetUserAgentOverrideParams(params) { return params; } parseAddPreloadScriptParams(params) { return params; } parseCallFunctionParams(params) { return params; } parseDisownParams(params) { return params; } parseEvaluateParams(params) { return params; } parseGetRealmsParams(params) { return params; } parseRemovePreloadScriptParams(params) { return params; } parsePerformActionsParams(params) { return params; } parseReleaseActionsParams(params) { return params; } parseSetFilesParams(params) { return params; } parseAddDataCollectorParams(params) { return params; } parseAddInterceptParams(params) { return params; } parseContinueRequestParams(params) { return params; } parseContinueResponseParams(params) { return params; } parseContinueWithAuthParams(params) { return params; } parseDisownDataParams(params) { return params; } parseFailRequestParams(params) { return params; } parseGetDataParams(params) { return params; } parseProvideResponseParams(params) { return params; } parseRemoveDataCollectorParams(params) { return params; } parseRemoveInterceptParams(params) { return params; } parseSetCacheBehaviorParams(params) { return params; } parseSetExtraHeadersParams(params) { return params; } parseSetPermissionsParams(params) { return params; } parseSubscribeParams(params) { return params; } parseUnsubscribeParams(params) { return params; } parseDeleteCookiesParams(params) { return params; } parseGetCookiesParams(params) { return params; } parseSetCookieParams(params) { return params; } parseInstallParams(params) { return params; } parseUninstallParams(params) { return params; } } /** * Copyright 2023 Google LLC. * Copyright (c) Microsoft Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ class BrowserProcessor { #browserCdpClient; #browsingContextStorage; #configStorage; #userContextStorage; constructor(browserCdpClient, browsingContextStorage, configStorage, userContextStorage) { this.#browserCdpClient = browserCdpClient; this.#browsingContextStorage = browsingContextStorage; this.#configStorage = configStorage; this.#userContextStorage = userContextStorage; } close() { setTimeout(() => this.#browserCdpClient.sendCommand('Browser.close').catch(() => { }), 0); return {}; } async createUserContext(params) { const w3cParams = params; const globalConfig = this.#configStorage.getGlobalConfig(); if (w3cParams.acceptInsecureCerts !== undefined) { if (w3cParams.acceptInsecureCerts === false && globalConfig.acceptInsecureCerts === true) throw new UnknownErrorException(`Cannot set user context's "acceptInsecureCerts" to false, when a capability "acceptInsecureCerts" is set to true`); } const request = {}; if (w3cParams.proxy) { const proxyStr = getProxyStr(w3cParams.proxy); if (proxyStr) { request.proxyServer = proxyStr; } if (w3cParams.proxy.noProxy) { request.proxyBypassList = w3cParams.proxy.noProxy.join(','); } } else { if (params['goog:proxyServer'] !== undefined) { request.proxyServer = params['goog:proxyServer']; } const proxyBypassList = params['goog:proxyBypassList'] ?? undefined; if (proxyBypassList) { request.proxyBypassList = proxyBypassList.join(','); } } const context = await this.#browserCdpClient.sendCommand('Target.createBrowserContext', request); await this.#applyDownloadBehavior(globalConfig.downloadBehavior ?? null, context.browserContextId); this.#configStorage.updateUserContextConfig(context.browserContextId, { acceptInsecureCerts: params['acceptInsecureCerts'], userPromptHandler: params['unhandledPromptBehavior'], }); return { userContext: context.browserContextId, }; } async removeUserContext(params) { const userContext = params.userContext; if (userContext === 'default') { throw new InvalidArgumentException('`default` user context cannot be removed'); } try { await this.#browserCdpClient.sendCommand('Target.disposeBrowserContext', { browserContextId: userContext, }); } catch (err) { if (err.message.startsWith('Failed to find context with id')) { throw new NoSuchUserContextException(err.message); } throw err; } return {}; } async getUserContexts() { return { userContexts: await this.#userContextStorage.getUserContexts(), }; } async #getWindowInfo(targetId) { const windowInfo = await this.#browserCdpClient.sendCommand('Browser.getWindowForTarget', { targetId }); return { active: false, clientWindow: `${windowInfo.windowId}`, state: windowInfo.bounds.windowState ?? 'normal', height: windowInfo.bounds.height ?? 0, width: windowInfo.bounds.width ?? 0, x: windowInfo.bounds.left ?? 0, y: windowInfo.bounds.top ?? 0, }; } async setClientWindowState(params) { const { clientWindow } = params; const bounds = { windowState: params.state, }; if (params.state === 'normal') { if (params.width !== undefined) { bounds.width = params.width; } if (params.height !== undefined) { bounds.height = params.height; } if (params.x !== undefined) { bounds.left = params.x; } if (params.y !== undefined) { bounds.top = params.y; } } const windowId = Number.parseInt(clientWindow); if (isNaN(windowId)) { throw new InvalidArgumentException('no such client window'); } await this.#browserCdpClient.sendCommand('Browser.setWindowBounds', { windowId, bounds, }); const result = await this.#browserCdpClient.sendCommand('Browser.getWindowBounds', { windowId, }); return { active: false, clientWindow: `${windowId}`, state: result.bounds.windowState ?? 'normal', height: result.bounds.height ?? 0, width: result.bounds.width ?? 0, x: result.bounds.left ?? 0, y: result.bounds.top ?? 0, }; } async getClientWindows() { const topLevelTargetIds = this.#browsingContextStorage .getTopLevelContexts() .map((b) => b.cdpTarget.id); const clientWindows = await Promise.all(topLevelTargetIds.map(async (targetId) => await this.#getWindowInfo(targetId))); const uniqueClientWindowIds = new Set(); const uniqueClientWindows = new Array(); for (const window of clientWindows) { if (!uniqueClientWindowIds.has(window.clientWindow)) { uniqueClientWindowIds.add(window.clientWindow); uniqueClientWindows.push(window); } } return { clientWindows: uniqueClientWindows }; } #toCdpDownloadBehavior(downloadBehavior) { if (downloadBehavior === null) return { behavior: 'default', }; if (downloadBehavior?.type === 'denied') return { behavior: 'deny', }; if (downloadBehavior?.type === 'allowed') { return { behavior: 'allow', downloadPath: downloadBehavior.destinationFolder, }; } throw new UnknownErrorException('Unexpected download behavior'); } async #applyDownloadBehavior(downloadBehavior, userContext) { await this.#browserCdpClient.sendCommand('Browser.setDownloadBehavior', { ...this.#toCdpDownloadBehavior(downloadBehavior), browserContextId: userContext === 'default' ? undefined : userContext, eventsEnabled: true, }); } async setDownloadBehavior(params) { let userContexts; if (params.userContexts === undefined) { userContexts = (await this.#userContextStorage.getUserContexts()).map((c) => c.userContext); } else { userContexts = Array.from(await this.#userContextStorage.verifyUserContextIdList(params.userContexts)); } if (params.userContexts === undefined) { this.#configStorage.updateGlobalConfig({ downloadBehavior: params.downloadBehavior, }); } else { params.userContexts.map((userContext) => this.#configStorage.updateUserContextConfig(userContext, { downloadBehavior: params.downloadBehavior, })); } await Promise.all(userContexts.map(async (userContext) => { const downloadBehavior = this.#configStorage.getActiveConfig(undefined, userContext) .downloadBehavior ?? null; await this.#applyDownloadBehavior(downloadBehavior, userContext); })); return {}; } } function getProxyStr(proxyConfig) { if (proxyConfig.proxyType === 'direct' || proxyConfig.proxyType === 'system') { return undefined; } if (proxyConfig.proxyType === 'pac') { throw new UnsupportedOperationException(`PAC proxy configuration is not supported per user context`); } if (proxyConfig.proxyType === 'autodetect') { throw new UnsupportedOperationException(`Autodetect proxy is not supported per user context`); } if (proxyConfig.proxyType === 'manual') { const servers = []; if (proxyConfig.httpProxy !== undefined) { servers.push(`http=${proxyConfig.httpProxy}`); } if (proxyConfig.sslProxy !== undefined) { servers.push(`https=${proxyConfig.sslProxy}`); } if (proxyConfig.socksProxy !== undefined || proxyConfig.socksVersion !== undefined) { if (proxyConfig.socksProxy === undefined) { throw new InvalidArgumentException(`'socksVersion' cannot be set without 'socksProxy'`); } if (proxyConfig.socksVersion === undefined || typeof proxyConfig.socksVersion !== 'number' || !Number.isInteger(proxyConfig.socksVersion) || proxyConfig.socksVersion < 0 || proxyConfig.socksVersion > 255) { throw new InvalidArgumentException(`'socksVersion' must be between 0 and 255`); } servers.push(`socks=socks${proxyConfig.socksVersion}://${proxyConfig.socksProxy}`); } if (servers.length === 0) { return undefined; } return servers.join(';'); } throw new UnknownErrorException(`Unknown proxy type`); } /** * Copyright 2023 Google LLC. * Copyright (c) Microsoft Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ class CdpProcessor { #browsingContextStorage; #realmStorage; #cdpConnection; #browserCdpClient; constructor(browsingContextStorage, realmStorage, cdpConnection, browserCdpClient) { this.#browsingContextStorage = browsingContextStorage; this.#realmStorage = realmStorage; this.#cdpConnection = cdpConnection; this.#browserCdpClient = browserCdpClient; } getSession(params) { const context = params.context; const sessionId = this.#browsingContextStorage.getContext(context).cdpTarget.cdpSessionId; if (sessionId === undefined) { return {}; } return { session: sessionId }; } resolveRealm(params) { const context = params.realm; const realm = this.#realmStorage.getRealm({ realmId: context }); if (realm === undefined) { throw new UnknownErrorException(`Could not find realm ${params.realm}`); } return { executionContextId: realm.executionContextId }; } async sendCommand(params) { const client = params.session ? this.#cdpConnection.getCdpClient(params.session) : this.#browserCdpClient; const result = await client.sendCommand(params.method, params.params); return { result, session: params.session, }; } } class BrowsingContextProcessor { #browserCdpClient; #browsingContextStorage; #contextConfigStorage; #eventManager; #userContextStorage; constructor(browserCdpClient, browsingContextStorage, userContextStorage, contextConfigStorage, eventManager) { this.#contextConfigStorage = contextConfigStorage; this.#userContextStorage = userContextStorage; this.#browserCdpClient = browserCdpClient; this.#browsingContextStorage = browsingContextStorage; this.#eventManager = eventManager; this.#eventManager.addSubscribeHook(BrowsingContext$2.EventNames.ContextCreated, this.#onContextCreatedSubscribeHook.bind(this)); } getTree(params) { const resultContexts = params.root === undefined ? this.#browsingContextStorage.getTopLevelContexts() : [this.#browsingContextStorage.getContext(params.root)]; return { contexts: resultContexts.map((c) => c.serializeToBidiValue(params.maxDepth ?? Number.MAX_VALUE)), }; } async create(params) { let referenceContext; let userContext = 'default'; if (params.referenceContext !== undefined) { referenceContext = this.#browsingContextStorage.getContext(params.referenceContext); if (!referenceContext.isTopLevelContext()) { throw new InvalidArgumentException(`referenceContext should be a top-level context`); } userContext = referenceContext.userContext; } if (params.userContext !== undefined) { userContext = params.userContext; } const existingContexts = this.#browsingContextStorage .getAllContexts() .filter((context) => context.userContext === userContext); let newWindow = false; switch (params.type) { case "tab" : newWindow = false; break; case "window" : newWindow = true; break; } if (!existingContexts.length) { newWindow = true; } let result; try { result = await this.#browserCdpClient.sendCommand('Target.createTarget', { url: 'about:blank', newWindow, browserContextId: userContext === 'default' ? undefined : userContext, background: params.background === true, }); } catch (err) { if ( err.message.startsWith('Failed to find browser context with id') || err.message === 'browserContextId') { throw new NoSuchUserContextException(`The context ${userContext} was not found`); } throw err; } const context = await this.#browsingContextStorage.waitForContext(result.targetId); await context.lifecycleLoaded(); return { context: context.id }; } navigate(params) { const context = this.#browsingContextStorage.getContext(params.context); return context.navigate(params.url, params.wait ?? "none" ); } reload(params) { const context = this.#browsingContextStorage.getContext(params.context); return context.reload(params.ignoreCache ?? false, params.wait ?? "none" ); } async activate(params) { const context = this.#browsingContextStorage.getContext(params.context); if (!context.isTopLevelContext()) { throw new InvalidArgumentException('Activation is only supported on the top-level context'); } await context.activate(); return {}; } async captureScreenshot(params) { const context = this.#browsingContextStorage.getContext(params.context); return await context.captureScreenshot(params); } async print(params) { const context = this.#browsingContextStorage.getContext(params.context); return await context.print(params); } async setViewport(params) { const maxDimensionSize = 10_000_000; if ((params.viewport?.height ?? 0) > maxDimensionSize || (params.viewport?.width ?? 0) > maxDimensionSize) { throw new UnsupportedOperationException(`Viewport dimension over ${maxDimensionSize} are not supported`); } const config = {}; if (params.devicePixelRatio !== undefined) { config.devicePixelRatio = params.devicePixelRatio; } if (params.viewport !== undefined) { config.viewport = params.viewport; } const impactedTopLevelContexts = await this.#getRelatedTopLevelBrowsingContexts(params.context, params.userContexts); for (const userContextId of params.userContexts ?? []) { this.#contextConfigStorage.updateUserContextConfig(userContextId, config); } if (params.context !== undefined) { this.#contextConfigStorage.updateBrowsingContextConfig(params.context, config); } await Promise.all(impactedTopLevelContexts.map(async (context) => { const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext); await context.setViewport(config.viewport ?? null, config.devicePixelRatio ?? null, config.screenOrientation ?? null); })); return {}; } async #getRelatedTopLevelBrowsingContexts(browsingContextId, userContextIds) { if (browsingContextId === undefined && userContextIds === undefined) { throw new InvalidArgumentException('Either userContexts or context must be provided'); } if (browsingContextId !== undefined && userContextIds !== undefined) { throw new InvalidArgumentException('userContexts and context are mutually exclusive'); } if (browsingContextId !== undefined) { const context = this.#browsingContextStorage.getContext(browsingContextId); if (!context.isTopLevelContext()) { throw new InvalidArgumentException('Emulating viewport is only supported on the top-level context'); } return [context]; } await this.#userContextStorage.verifyUserContextIdList(userContextIds); const result = []; for (const userContextId of userContextIds) { const topLevelBrowsingContexts = this.#browsingContextStorage .getTopLevelContexts() .filter((browsingContext) => browsingContext.userContext === userContextId); result.push(...topLevelBrowsingContexts); } return [...new Set(result).values()]; } async traverseHistory(params) { const context = this.#browsingContextStorage.getContext(params.context); if (!context) { throw new InvalidArgumentException(`No browsing context with id ${params.context}`); } if (!context.isTopLevelContext()) { throw new InvalidArgumentException('Traversing history is only supported on the top-level context'); } await context.traverseHistory(params.delta); return {}; } async handleUserPrompt(params) { const context = this.#browsingContextStorage.getContext(params.context); try { await context.handleUserPrompt(params.accept, params.userText); } catch (error) { if (error.message?.includes('No dialog is showing')) { throw new NoSuchAlertException('No dialog is showing'); } throw error; } return {}; } async close(params) { const context = this.#browsingContextStorage.getContext(params.context); if (!context.isTopLevelContext()) { throw new InvalidArgumentException(`Non top-level browsing context ${context.id} cannot be closed.`); } const parentCdpClient = context.cdpTarget.parentCdpClient; try { const detachedFromTargetPromise = new Promise((resolve) => { const onContextDestroyed = (event) => { if (event.targetId === params.context) { parentCdpClient.off('Target.detachedFromTarget', onContextDestroyed); resolve(); } }; parentCdpClient.on('Target.detachedFromTarget', onContextDestroyed); }); try { if (params.promptUnload) { await context.close(); } else { await parentCdpClient.sendCommand('Target.closeTarget', { targetId: params.context, }); } } catch (error) { if (!parentCdpClient.isCloseError(error)) { throw error; } } await detachedFromTargetPromise; } catch (error) { if (!(error.code === -32e3 && error.message === 'Not attached to an active page')) { throw error; } } return {}; } async locateNodes(params) { const context = this.#browsingContextStorage.getContext(params.context); return await context.locateNodes(params); } #onContextCreatedSubscribeHook(contextId) { const context = this.#browsingContextStorage.getContext(contextId); const contextsToReport = [ context, ...this.#browsingContextStorage.getContext(contextId).allChildren, ]; contextsToReport.forEach((context) => { this.#eventManager.registerEvent({ type: 'event', method: BrowsingContext$2.EventNames.ContextCreated, params: context.serializeToBidiValue(), }, context.id); }); return Promise.resolve(); } } /** * Copyright 2025 Google LLC. * Copyright (c) Microsoft Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ class EmulationProcessor { #userContextStorage; #browsingContextStorage; #contextConfigStorage; constructor(browsingContextStorage, userContextStorage, contextConfigStorage) { this.#userContextStorage = userContextStorage; this.#browsingContextStorage = browsingContextStorage; this.#contextConfigStorage = contextConfigStorage; } async setGeolocationOverride(params) { if ('coordinates' in params && 'error' in params) { throw new InvalidArgumentException('Coordinates and error cannot be set at the same time'); } let geolocation = null; if ('coordinates' in params) { if ((params.coordinates?.altitude ?? null) === null && (params.coordinates?.altitudeAccuracy ?? null) !== null) { throw new InvalidArgumentException('Geolocation altitudeAccuracy can be set only with altitude'); } geolocation = params.coordinates; } else if ('error' in params) { if (params.error.type !== 'positionUnavailable') { throw new InvalidArgumentException(`Unknown geolocation error ${params.error.type}`); } geolocation = params.error; } else { throw new InvalidArgumentException(`Coordinates or error should be set`); } const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts); for (const browsingContextId of params.contexts ?? []) { this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, { geolocation, }); } for (const userContextId of params.userContexts ?? []) { this.#contextConfigStorage.updateUserContextConfig(userContextId, { geolocation, }); } await Promise.all(browsingContexts.map(async (context) => { const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext); await context.setGeolocationOverride(config.geolocation ?? null); })); return {}; } async setLocaleOverride(params) { const locale = params.locale ?? null; if (locale !== null && !isValidLocale(locale)) { throw new InvalidArgumentException(`Invalid locale "${locale}"`); } const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts); for (const browsingContextId of params.contexts ?? []) { this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, { locale, }); } for (const userContextId of params.userContexts ?? []) { this.#contextConfigStorage.updateUserContextConfig(userContextId, { locale, }); } await Promise.all(browsingContexts.map(async (context) => {