Add Brave CDP automation, replace Oracle browser mode

Connects to user's running Brave via Chrome DevTools Protocol
to automate ChatGPT interaction. Uses puppeteer-core to open a
tab, send the prompt, wait for response, and extract the result.

No cookies, no separate profiles, no copy/paste. Just connects
to the browser where the user is already logged in.

One-time setup: relaunch Brave with --remote-debugging-port=9222

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Taylor Eernisse
2026-02-07 16:16:41 -05:00
parent d776a266a8
commit e7882b917b
4163 changed files with 782828 additions and 148 deletions

56
node_modules/chromium-bidi/lib/THIRD_PARTY_NOTICES generated vendored Normal file
View File

@@ -0,0 +1,56 @@
Name: mitt
URL: https://github.com/developit/mitt
Version: 3.0.1
License: MIT
MIT License
Copyright (c) 2021 Jason Miller
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-------------------- DEPENDENCY DIVIDER --------------------
Name: zod
URL: https://zod.dev
Version: 3.25.76
License: MIT
MIT License
Copyright (c) 2025 Colin McDonnell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,29 @@
/**
* 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.
*/
/**
* @fileoverview The entry point to the BiDi Mapper namespace.
* Other modules should only access exports defined in this file.
* XXX: Add ESlint rule for this (https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-restricted-paths.md)
*/
export { BidiServer } from './BidiServer.js';
export { MapperOptions } from './MapperOptions.js';
export type { CdpConnection } from '../cdp/CdpConnection.js';
export type { CdpClient } from '../cdp/CdpClient.js';
export { EventEmitter } from '../utils/EventEmitter.js';
export type { BidiTransport } from './BidiTransport.js';
export { OutgoingMessage } from './OutgoingMessage.js';
export type { BidiCommandParameterParser } from './BidiParser.js';

View File

@@ -0,0 +1,31 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.OutgoingMessage = exports.EventEmitter = exports.BidiServer = void 0;
/**
* @fileoverview The entry point to the BiDi Mapper namespace.
* Other modules should only access exports defined in this file.
* XXX: Add ESlint rule for this (https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-restricted-paths.md)
*/
var BidiServer_js_1 = require("./BidiServer.js");
Object.defineProperty(exports, "BidiServer", { enumerable: true, get: function () { return BidiServer_js_1.BidiServer; } });
var EventEmitter_js_1 = require("../utils/EventEmitter.js");
Object.defineProperty(exports, "EventEmitter", { enumerable: true, get: function () { return EventEmitter_js_1.EventEmitter; } });
var OutgoingMessage_js_1 = require("./OutgoingMessage.js");
Object.defineProperty(exports, "OutgoingMessage", { enumerable: true, get: function () { return OutgoingMessage_js_1.OutgoingMessage; } });
//# sourceMappingURL=BidiMapper.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"BidiMapper.js","sourceRoot":"","sources":["../../../src/bidiMapper/BidiMapper.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;;AAEH;;;;GAIG;AACH,iDAA2C;AAAnC,2GAAA,UAAU,OAAA;AAIlB,4DAAsD;AAA9C,+GAAA,YAAY,OAAA;AAEpB,2DAAqD;AAA7C,qHAAA,eAAe,OAAA"}

View File

@@ -0,0 +1,92 @@
/**
* 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.
*/
import type { Browser, BrowsingContext, Cdp, Emulation, Input, Network, Script, Session, Storage, Permissions, Bluetooth, WebExtension, UAClientHints } from '../protocol/protocol.js';
import type { BidiCommandParameterParser } from './BidiParser.js';
export declare class BidiNoOpParser implements BidiCommandParameterParser {
parseDisableSimulationParameters(params: unknown): Bluetooth.DisableSimulationParameters;
parseHandleRequestDevicePromptParams(params: unknown): Bluetooth.HandleRequestDevicePromptParameters;
parseSimulateAdapterParameters(params: unknown): Bluetooth.SimulateAdapterParameters;
parseSimulateAdvertisementParameters(params: unknown): Bluetooth.SimulateAdvertisementParameters;
parseSimulateCharacteristicParameters(params: unknown): Bluetooth.SimulateCharacteristicParameters;
parseSimulateCharacteristicResponseParameters(params: unknown): Bluetooth.SimulateCharacteristicResponseParameters;
parseSimulateDescriptorParameters(params: unknown): Bluetooth.SimulateDescriptorParameters;
parseSimulateDescriptorResponseParameters(params: unknown): Bluetooth.SimulateDescriptorResponseParameters;
parseSimulateGattConnectionResponseParameters(params: unknown): Bluetooth.SimulateGattConnectionResponseParameters;
parseSimulateGattDisconnectionParameters(params: unknown): Bluetooth.SimulateGattDisconnectionParameters;
parseSimulatePreconnectedPeripheralParameters(params: unknown): Bluetooth.SimulatePreconnectedPeripheralParameters;
parseSimulateServiceParameters(params: unknown): Bluetooth.SimulateServiceParameters;
parseCreateUserContextParameters(params: unknown): Browser.CreateUserContextParameters;
parseRemoveUserContextParameters(params: unknown): Browser.RemoveUserContextParameters;
parseSetClientWindowStateParameters(params: unknown): Browser.SetClientWindowStateParameters;
parseSetDownloadBehaviorParameters(params: unknown): Browser.SetDownloadBehaviorParameters;
parseActivateParams(params: unknown): BrowsingContext.ActivateParameters;
parseCaptureScreenshotParams(params: unknown): BrowsingContext.CaptureScreenshotParameters;
parseCloseParams(params: unknown): BrowsingContext.CloseParameters;
parseCreateParams(params: unknown): BrowsingContext.CreateParameters;
parseGetTreeParams(params: unknown): BrowsingContext.GetTreeParameters;
parseHandleUserPromptParams(params: unknown): BrowsingContext.HandleUserPromptParameters;
parseLocateNodesParams(params: unknown): BrowsingContext.LocateNodesParameters;
parseNavigateParams(params: unknown): BrowsingContext.NavigateParameters;
parsePrintParams(params: unknown): BrowsingContext.PrintParameters;
parseReloadParams(params: unknown): BrowsingContext.ReloadParameters;
parseSetViewportParams(params: unknown): BrowsingContext.SetViewportParameters;
parseTraverseHistoryParams(params: unknown): BrowsingContext.TraverseHistoryParameters;
parseGetSessionParams(params: unknown): Cdp.GetSessionParameters;
parseResolveRealmParams(params: unknown): Cdp.ResolveRealmParameters;
parseSendCommandParams(params: unknown): Cdp.SendCommandParameters;
parseSetClientHintsOverrideParams(params: unknown): UAClientHints.Emulation.SetClientHintsOverrideParameters;
parseSetForcedColorsModeThemeOverrideParams(params: unknown): Emulation.SetForcedColorsModeThemeOverrideParameters;
parseSetGeolocationOverrideParams(params: unknown): Emulation.SetGeolocationOverrideParameters;
parseSetLocaleOverrideParams(params: unknown): Emulation.SetLocaleOverrideParameters;
parseSetNetworkConditionsParams(params: unknown): Emulation.SetNetworkConditionsParameters;
parseSetScreenOrientationOverrideParams(params: unknown): Emulation.SetScreenOrientationOverrideParameters;
parseSetScreenSettingsOverrideParams(params: unknown): Emulation.SetScreenSettingsOverrideParameters;
parseSetScriptingEnabledParams(params: unknown): Emulation.SetScriptingEnabledParameters;
parseSetTimezoneOverrideParams(params: unknown): Emulation.SetTimezoneOverrideParameters;
parseSetTouchOverrideParams(params: unknown): Emulation.SetTouchOverrideParameters;
parseSetUserAgentOverrideParams(params: unknown): Emulation.SetUserAgentOverrideParameters;
parseAddPreloadScriptParams(params: unknown): Script.AddPreloadScriptParameters;
parseCallFunctionParams(params: unknown): Script.CallFunctionParameters;
parseDisownParams(params: unknown): Script.DisownParameters;
parseEvaluateParams(params: unknown): Script.EvaluateParameters;
parseGetRealmsParams(params: unknown): Script.GetRealmsParameters;
parseRemovePreloadScriptParams(params: unknown): Script.RemovePreloadScriptParameters;
parsePerformActionsParams(params: unknown): Input.PerformActionsParameters;
parseReleaseActionsParams(params: unknown): Input.ReleaseActionsParameters;
parseSetFilesParams(params: unknown): Input.SetFilesParameters;
parseAddDataCollectorParams(params: unknown): Network.AddDataCollectorParameters;
parseAddInterceptParams(params: unknown): Network.AddInterceptParameters;
parseContinueRequestParams(params: unknown): Network.ContinueRequestParameters;
parseContinueResponseParams(params: unknown): Network.ContinueResponseParameters;
parseContinueWithAuthParams(params: unknown): Network.ContinueWithAuthParameters;
parseDisownDataParams(params: unknown): Network.DisownDataParameters;
parseFailRequestParams(params: unknown): Network.FailRequestParameters;
parseGetDataParams(params: unknown): Network.GetDataParameters;
parseProvideResponseParams(params: unknown): Network.ProvideResponseParameters;
parseRemoveDataCollectorParams(params: unknown): Network.RemoveDataCollectorParameters;
parseRemoveInterceptParams(params: unknown): Network.RemoveInterceptParameters;
parseSetCacheBehaviorParams(params: unknown): Network.SetCacheBehaviorParameters;
parseSetExtraHeadersParams(params: unknown): Network.SetExtraHeadersParameters;
parseSetPermissionsParams(params: unknown): Permissions.SetPermissionParameters;
parseSubscribeParams(params: unknown): Session.SubscribeParameters;
parseUnsubscribeParams(params: unknown): Session.UnsubscribeByAttributesRequest | Session.UnsubscribeByIdRequest;
parseDeleteCookiesParams(params: unknown): Storage.DeleteCookiesParameters;
parseGetCookiesParams(params: unknown): Storage.GetCookiesParameters;
parseSetCookieParams(params: unknown): Storage.SetCookieParameters;
parseInstallParams(params: unknown): WebExtension.InstallParameters;
parseUninstallParams(params: unknown): WebExtension.UninstallParameters;
}

View File

@@ -0,0 +1,274 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.BidiNoOpParser = void 0;
class BidiNoOpParser {
// Bluetooth module
// keep-sorted start block=yes
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;
}
// keep-sorted end
// Browser module
// keep-sorted start block=yes
parseCreateUserContextParameters(params) {
return params;
}
parseRemoveUserContextParameters(params) {
return params;
}
parseSetClientWindowStateParameters(params) {
return params;
}
parseSetDownloadBehaviorParameters(params) {
return params;
}
// keep-sorted end
// Browsing Context module
// keep-sorted start block=yes
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;
}
parseSetViewportParams(params) {
return params;
}
parseTraverseHistoryParams(params) {
return params;
}
// keep-sorted end
// CDP module
// keep-sorted start block=yes
parseGetSessionParams(params) {
return params;
}
parseResolveRealmParams(params) {
return params;
}
parseSendCommandParams(params) {
return params;
}
// keep-sorted end
// Emulation module
// keep-sorted start block=yes
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;
}
parseSetTimezoneOverrideParams(params) {
return params;
}
parseSetTouchOverrideParams(params) {
return params;
}
parseSetUserAgentOverrideParams(params) {
return params;
}
// keep-sorted end
// Script module
// keep-sorted start block=yes
parseAddPreloadScriptParams(params) {
return params;
}
parseCallFunctionParams(params) {
return params;
}
parseDisownParams(params) {
return params;
}
parseEvaluateParams(params) {
return params;
}
parseGetRealmsParams(params) {
return params;
}
parseRemovePreloadScriptParams(params) {
return params;
}
// keep-sorted end
// Input module
// keep-sorted start block=yes
parsePerformActionsParams(params) {
return params;
}
parseReleaseActionsParams(params) {
return params;
}
parseSetFilesParams(params) {
return params;
}
// keep-sorted end
// Network module
// keep-sorted start block=yes
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;
}
// keep-sorted end
// Permissions module
// keep-sorted start block=yes
parseSetPermissionsParams(params) {
return params;
}
// keep-sorted end
// Session module
// keep-sorted start block=yes
parseSubscribeParams(params) {
return params;
}
parseUnsubscribeParams(params) {
return params;
}
// keep-sorted end
// Storage module
// keep-sorted start block=yes
parseDeleteCookiesParams(params) {
return params;
}
parseGetCookiesParams(params) {
return params;
}
parseSetCookieParams(params) {
return params;
}
// keep-sorted end
// WebExtenstion module
// keep-sorted start block=yes
parseInstallParams(params) {
return params;
}
parseUninstallParams(params) {
return params;
}
}
exports.BidiNoOpParser = BidiNoOpParser;
//# sourceMappingURL=BidiNoOpParser.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"BidiNoOpParser.js","sourceRoot":"","sources":["../../../src/bidiMapper/BidiNoOpParser.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,91 @@
/**
* 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.
*/
import type { Bluetooth, Browser, BrowsingContext, Cdp, Emulation, Input, Network, Permissions, Script, Session, Storage, WebExtension, UAClientHints } from '../protocol/protocol.js';
export interface BidiCommandParameterParser {
parseDisableSimulationParameters(params: unknown): Bluetooth.DisableSimulationParameters;
parseHandleRequestDevicePromptParams(params: unknown): Bluetooth.HandleRequestDevicePromptParameters;
parseSimulateAdapterParameters(params: unknown): Bluetooth.SimulateAdapterParameters;
parseSimulateAdvertisementParameters(params: unknown): Bluetooth.SimulateAdvertisementParameters;
parseSimulateCharacteristicParameters(params: unknown): Bluetooth.SimulateCharacteristicParameters;
parseSimulateCharacteristicResponseParameters(params: unknown): Bluetooth.SimulateCharacteristicResponseParameters;
parseSimulateDescriptorParameters(params: unknown): Bluetooth.SimulateDescriptorParameters;
parseSimulateDescriptorResponseParameters(params: unknown): Bluetooth.SimulateDescriptorResponseParameters;
parseSimulateGattConnectionResponseParameters(params: unknown): Bluetooth.SimulateGattConnectionResponseParameters;
parseSimulateGattDisconnectionParameters(params: unknown): Bluetooth.SimulateGattDisconnectionParameters;
parseSimulatePreconnectedPeripheralParameters(params: unknown): Bluetooth.SimulatePreconnectedPeripheralParameters;
parseSimulateServiceParameters(params: unknown): Bluetooth.SimulateServiceParameters;
parseCreateUserContextParameters(params: unknown): Browser.CreateUserContextParameters;
parseRemoveUserContextParameters(params: unknown): Browser.RemoveUserContextParameters;
parseSetClientWindowStateParameters(params: unknown): Browser.SetClientWindowStateParameters;
parseSetDownloadBehaviorParameters(params: unknown): Browser.SetDownloadBehaviorParameters;
parseActivateParams(params: unknown): BrowsingContext.ActivateParameters;
parseCaptureScreenshotParams(params: unknown): BrowsingContext.CaptureScreenshotParameters;
parseCloseParams(params: unknown): BrowsingContext.CloseParameters;
parseCreateParams(params: unknown): BrowsingContext.CreateParameters;
parseGetTreeParams(params: unknown): BrowsingContext.GetTreeParameters;
parseHandleUserPromptParams(params: unknown): BrowsingContext.HandleUserPromptParameters;
parseLocateNodesParams(params: unknown): BrowsingContext.LocateNodesParameters;
parseNavigateParams(params: unknown): BrowsingContext.NavigateParameters;
parsePrintParams(params: unknown): BrowsingContext.PrintParameters;
parseReloadParams(params: unknown): BrowsingContext.ReloadParameters;
parseSetViewportParams(params: unknown): BrowsingContext.SetViewportParameters;
parseTraverseHistoryParams(params: unknown): BrowsingContext.TraverseHistoryParameters;
parseGetSessionParams(params: unknown): Cdp.GetSessionParameters;
parseResolveRealmParams(params: unknown): Cdp.ResolveRealmParameters;
parseSendCommandParams(params: unknown): Cdp.SendCommandParameters;
parseSetClientHintsOverrideParams(params: unknown): UAClientHints.Emulation.SetClientHintsOverrideParameters;
parseSetForcedColorsModeThemeOverrideParams(params: unknown): Emulation.SetForcedColorsModeThemeOverrideParameters;
parseSetGeolocationOverrideParams(params: unknown): Emulation.SetGeolocationOverrideParameters;
parseSetLocaleOverrideParams(params: unknown): Emulation.SetLocaleOverrideParameters;
parseSetNetworkConditionsParams(params: unknown): Emulation.SetNetworkConditionsParameters;
parseSetScreenOrientationOverrideParams(params: unknown): Emulation.SetScreenOrientationOverrideParameters;
parseSetScreenSettingsOverrideParams(params: unknown): Emulation.SetScreenSettingsOverrideParameters;
parseSetScriptingEnabledParams(params: unknown): Emulation.SetScriptingEnabledParameters;
parseSetTimezoneOverrideParams(params: unknown): Emulation.SetTimezoneOverrideParameters;
parseSetTouchOverrideParams(params: unknown): Emulation.SetTouchOverrideParameters;
parseSetUserAgentOverrideParams(params: unknown): Emulation.SetUserAgentOverrideParameters;
parsePerformActionsParams(params: unknown): Input.PerformActionsParameters;
parseReleaseActionsParams(params: unknown): Input.ReleaseActionsParameters;
parseSetFilesParams(params: unknown): Input.SetFilesParameters;
parseSetPermissionsParams(params: unknown): Permissions.SetPermissionParameters;
parseAddDataCollectorParams(params: unknown): Network.AddDataCollectorParameters;
parseAddInterceptParams(params: unknown): Network.AddInterceptParameters;
parseContinueRequestParams(params: unknown): Network.ContinueRequestParameters;
parseContinueResponseParams(params: unknown): Network.ContinueResponseParameters;
parseContinueWithAuthParams(params: unknown): Network.ContinueWithAuthParameters;
parseDisownDataParams(params: unknown): Network.DisownDataParameters;
parseFailRequestParams(params: unknown): Network.FailRequestParameters;
parseGetDataParams(params: unknown): Network.GetDataParameters;
parseProvideResponseParams(params: unknown): Network.ProvideResponseParameters;
parseRemoveDataCollectorParams(params: unknown): Network.RemoveDataCollectorParameters;
parseRemoveInterceptParams(params: unknown): Network.RemoveInterceptParameters;
parseSetCacheBehaviorParams(params: unknown): Network.SetCacheBehaviorParameters;
parseSetExtraHeadersParams(params: unknown): Network.SetExtraHeadersParameters;
parseAddPreloadScriptParams(params: unknown): Script.AddPreloadScriptParameters;
parseCallFunctionParams(params: unknown): Script.CallFunctionParameters;
parseDisownParams(params: unknown): Script.DisownParameters;
parseEvaluateParams(params: unknown): Script.EvaluateParameters;
parseGetRealmsParams(params: unknown): Script.GetRealmsParameters;
parseRemovePreloadScriptParams(params: unknown): Script.RemovePreloadScriptParameters;
parseSubscribeParams(params: unknown): Session.SubscribeParameters;
parseUnsubscribeParams(params: unknown): Session.UnsubscribeParameters;
parseDeleteCookiesParams(params: unknown): Storage.DeleteCookiesParameters;
parseGetCookiesParams(params: unknown): Storage.GetCookiesParameters;
parseSetCookieParams(params: unknown): Storage.SetCookieParameters;
parseInstallParams(params: unknown): WebExtension.InstallParameters;
parseUninstallParams(params: unknown): WebExtension.UninstallParameters;
}

View File

@@ -0,0 +1,19 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=BidiParser.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"BidiParser.js","sourceRoot":"","sources":["../../../src/bidiMapper/BidiParser.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG"}

View File

@@ -0,0 +1,42 @@
/**
* 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.
*/
import type { CdpClient } from '../cdp/CdpClient.js';
import type { CdpConnection } from '../cdp/CdpConnection.js';
import type { ChromiumBidi } from '../protocol/protocol.js';
import { EventEmitter } from '../utils/EventEmitter.js';
import { type LoggerFn } from '../utils/log.js';
import type { Result } from '../utils/result.js';
import type { BidiCommandParameterParser } from './BidiParser.js';
import type { BidiTransport } from './BidiTransport.js';
import type { OutgoingMessage } from './OutgoingMessage.js';
interface BidiServerEvent extends Record<string | symbol, unknown> {
message: ChromiumBidi.Command;
}
export declare class BidiServer extends EventEmitter<BidiServerEvent> {
#private;
private constructor();
/**
* Creates and starts BiDi Mapper instance.
*/
static createAndStart(bidiTransport: BidiTransport, cdpConnection: CdpConnection, browserCdpClient: CdpClient, selfTargetId: string, parser?: BidiCommandParameterParser, logger?: LoggerFn): Promise<BidiServer>;
/**
* Sends BiDi message.
*/
emitOutgoingMessage(messageEntry: Promise<Result<OutgoingMessage>>, event: string): void;
close(): void;
}
export {};

View File

@@ -0,0 +1,169 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.BidiServer = void 0;
const EventEmitter_js_1 = require("../utils/EventEmitter.js");
const log_js_1 = require("../utils/log.js");
const ProcessingQueue_js_1 = require("../utils/ProcessingQueue.js");
const CommandProcessor_js_1 = require("./CommandProcessor.js");
const BluetoothProcessor_js_1 = require("./modules/bluetooth/BluetoothProcessor.js");
const ContextConfigStorage_js_1 = require("./modules/browser/ContextConfigStorage.js");
const UserContextStorage_js_1 = require("./modules/browser/UserContextStorage.js");
const CdpTargetManager_js_1 = require("./modules/cdp/CdpTargetManager.js");
const BrowsingContextStorage_js_1 = require("./modules/context/BrowsingContextStorage.js");
const NetworkStorage_js_1 = require("./modules/network/NetworkStorage.js");
const PreloadScriptStorage_js_1 = require("./modules/script/PreloadScriptStorage.js");
const RealmStorage_js_1 = require("./modules/script/RealmStorage.js");
const EventManager_js_1 = require("./modules/session/EventManager.js");
const SpeculationProcessor_js_1 = require("./modules/speculation/SpeculationProcessor.js");
class BidiServer extends EventEmitter_js_1.EventEmitter {
#messageQueue;
#transport;
#commandProcessor;
#eventManager;
#browsingContextStorage = new BrowsingContextStorage_js_1.BrowsingContextStorage();
#realmStorage = new RealmStorage_js_1.RealmStorage();
#preloadScriptStorage = new PreloadScriptStorage_js_1.PreloadScriptStorage();
#bluetoothProcessor;
#speculationProcessor;
#logger;
#handleIncomingMessage = (message) => {
void this.#commandProcessor.processCommand(message).catch((error) => {
this.#logger?.(log_js_1.LogType.debugError, error);
});
};
#processOutgoingMessage = async (messageEntry) => {
const message = messageEntry.message;
if (messageEntry.googChannel !== null) {
message['goog:channel'] = messageEntry.googChannel;
}
await this.#transport.sendMessage(message);
};
constructor(bidiTransport, cdpConnection, browserCdpClient, selfTargetId, defaultUserContextId, defaultUserAgent, parser, logger) {
super();
this.#logger = logger;
this.#messageQueue = new ProcessingQueue_js_1.ProcessingQueue(this.#processOutgoingMessage, this.#logger);
this.#transport = bidiTransport;
this.#transport.setOnMessage(this.#handleIncomingMessage);
const contextConfigStorage = new ContextConfigStorage_js_1.ContextConfigStorage();
const userContextStorage = new UserContextStorage_js_1.UserContextStorage(browserCdpClient);
this.#eventManager = new EventManager_js_1.EventManager(this.#browsingContextStorage, userContextStorage);
const networkStorage = new NetworkStorage_js_1.NetworkStorage(this.#eventManager, this.#browsingContextStorage, browserCdpClient, logger);
this.#bluetoothProcessor = new BluetoothProcessor_js_1.BluetoothProcessor(this.#eventManager, this.#browsingContextStorage);
this.#speculationProcessor = new SpeculationProcessor_js_1.SpeculationProcessor(this.#eventManager, this.#logger);
this.#commandProcessor = new CommandProcessor_js_1.CommandProcessor(cdpConnection, browserCdpClient, this.#eventManager, this.#browsingContextStorage, this.#realmStorage, this.#preloadScriptStorage, networkStorage, contextConfigStorage, this.#bluetoothProcessor, userContextStorage, parser, async (options) => {
// This is required to ignore certificate errors when service worker is fetched.
await browserCdpClient.sendCommand('Security.setIgnoreCertificateErrors', {
ignore: options.acceptInsecureCerts ?? false,
});
contextConfigStorage.updateGlobalConfig({
acceptInsecureCerts: options.acceptInsecureCerts ?? false,
userPromptHandler: options.unhandledPromptBehavior,
prerenderingDisabled: options?.['goog:prerenderingDisabled'] ?? false,
disableNetworkDurableMessages: options?.['goog:disableNetworkDurableMessages'],
});
new CdpTargetManager_js_1.CdpTargetManager(cdpConnection, browserCdpClient, selfTargetId, this.#eventManager, this.#browsingContextStorage, this.#realmStorage, networkStorage, contextConfigStorage, this.#bluetoothProcessor, this.#speculationProcessor, this.#preloadScriptStorage, defaultUserContextId, defaultUserAgent, logger);
// Needed to get events about new targets.
await browserCdpClient.sendCommand('Target.setDiscoverTargets', {
discover: true,
});
// Needed to automatically attach to new targets.
await browserCdpClient.sendCommand('Target.setAutoAttach', {
autoAttach: true,
waitForDebuggerOnStart: true,
flatten: true,
// Browser session should attach to tab instead of the page, so that
// prerendering is not blocked.
filter: [
{
type: 'page',
exclude: true,
},
{},
],
});
await this.#topLevelContextsLoaded();
}, this.#logger);
this.#eventManager.on("event" /* EventManagerEvents.Event */, ({ message, event }) => {
this.emitOutgoingMessage(message, event);
});
this.#commandProcessor.on("response" /* CommandProcessorEvents.Response */, ({ message, event }) => {
this.emitOutgoingMessage(message, event);
});
}
/**
* Creates and starts BiDi Mapper instance.
*/
static async createAndStart(bidiTransport, cdpConnection, browserCdpClient, selfTargetId, parser, logger) {
const [defaultUserContextId, version] = await Promise.all([
this.#getDefaultUserContextId(browserCdpClient),
// Fetch the default User Agent to be used in `CdpTarget`. This allows to avoid
// round trips to the browser for every target override.
browserCdpClient.sendCommand('Browser.getVersion'),
// Required for `Browser.downloadWillBegin` events.
browserCdpClient.sendCommand('Browser.setDownloadBehavior', {
behavior: 'default',
eventsEnabled: true,
}),
]);
const server = new BidiServer(bidiTransport, cdpConnection, browserCdpClient, selfTargetId, defaultUserContextId, version.userAgent, parser, logger);
return server;
}
static async #getDefaultUserContextId(browserCdpClient) {
// In chromium before `145.0.7578.0`, the default context is not exposed in
// `Target.getBrowserContexts`, but can be observed via `Target.getTargets`.
// If so, try to determine the default browser context by checking which one
// is mentioned in `Target.getTargets` and not in
// `Target.getBrowserContexts`.
// TODO(after 2026-02-24): rely only on `defaultBrowserContextId` from
// `Target.getBrowserContexts` after Chromium 145 reaches stable.
const [{ defaultBrowserContextId, browserContextIds }, { targetInfos }] = await Promise.all([
browserCdpClient.sendCommand('Target.getBrowserContexts'),
browserCdpClient.sendCommand('Target.getTargets'),
]);
if (defaultBrowserContextId) {
return defaultBrowserContextId;
}
for (const info of targetInfos) {
if (info.browserContextId &&
!browserContextIds.includes(info.browserContextId)) {
// The target belongs to a browser context that is not mentioned in
// `Target.getBrowserContexts`. This is the default browser context.
return info.browserContextId;
}
}
// The browser context is unknown.
return 'default';
}
/**
* Sends BiDi message.
*/
emitOutgoingMessage(messageEntry, event) {
this.#messageQueue.add(messageEntry, event);
}
close() {
this.#transport.close();
}
async #topLevelContextsLoaded() {
await Promise.all(this.#browsingContextStorage
.getTopLevelContexts()
.map((c) => c.lifecycleLoaded()));
}
}
exports.BidiServer = BidiServer;
//# sourceMappingURL=BidiServer.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,22 @@
/**
* 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.
*/
import type { ChromiumBidi } from '../protocol/protocol.js';
export interface BidiTransport {
setOnMessage: (handler: (message: ChromiumBidi.Command) => Promise<void> | void) => void;
sendMessage: (message: ChromiumBidi.Message) => Promise<void> | void;
close(): void;
}

View File

@@ -0,0 +1,19 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=BidiTransport.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"BidiTransport.js","sourceRoot":"","sources":["../../../src/bidiMapper/BidiTransport.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG"}

View File

@@ -0,0 +1,48 @@
/**
* 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.
*/
import type { CdpClient } from '../cdp/CdpClient.js';
import type { CdpConnection } from '../cdp/CdpConnection.js';
import { type ChromiumBidi } from '../protocol/protocol.js';
import { EventEmitter } from '../utils/EventEmitter.js';
import { type LoggerFn } from '../utils/log.js';
import type { Result } from '../utils/result.js';
import type { BidiCommandParameterParser } from './BidiParser.js';
import type { MapperOptions } from './MapperOptions.js';
import type { BluetoothProcessor } from './modules/bluetooth/BluetoothProcessor.js';
import type { ContextConfigStorage } from './modules/browser/ContextConfigStorage.js';
import type { UserContextStorage } from './modules/browser/UserContextStorage.js';
import type { BrowsingContextStorage } from './modules/context/BrowsingContextStorage.js';
import type { NetworkStorage } from './modules/network/NetworkStorage.js';
import type { PreloadScriptStorage } from './modules/script/PreloadScriptStorage.js';
import type { RealmStorage } from './modules/script/RealmStorage.js';
import type { EventManager } from './modules/session/EventManager.js';
import { OutgoingMessage } from './OutgoingMessage.js';
export declare const enum CommandProcessorEvents {
Response = "response"
}
interface CommandProcessorEventsMap extends Record<string | symbol, unknown> {
[CommandProcessorEvents.Response]: {
message: Promise<Result<OutgoingMessage>>;
event: string;
};
}
export declare class CommandProcessor extends EventEmitter<CommandProcessorEventsMap> {
#private;
constructor(cdpConnection: CdpConnection, browserCdpClient: CdpClient, eventManager: EventManager, browsingContextStorage: BrowsingContextStorage, realmStorage: RealmStorage, preloadScriptStorage: PreloadScriptStorage, networkStorage: NetworkStorage, contextConfigStorage: ContextConfigStorage, bluetoothProcessor: BluetoothProcessor, userContextStorage: UserContextStorage, parser: BidiCommandParameterParser | undefined, initConnection: (options: MapperOptions) => Promise<void>, logger?: LoggerFn);
processCommand(command: ChromiumBidi.Command): Promise<void>;
}
export {};

View File

@@ -0,0 +1,326 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.CommandProcessor = void 0;
const protocol_js_1 = require("../protocol/protocol.js");
const EventEmitter_js_1 = require("../utils/EventEmitter.js");
const log_js_1 = require("../utils/log.js");
const BidiNoOpParser_js_1 = require("./BidiNoOpParser.js");
const BrowserProcessor_js_1 = require("./modules/browser/BrowserProcessor.js");
const CdpProcessor_js_1 = require("./modules/cdp/CdpProcessor.js");
const BrowsingContextProcessor_js_1 = require("./modules/context/BrowsingContextProcessor.js");
const EmulationProcessor_js_1 = require("./modules/emulation/EmulationProcessor.js");
const InputProcessor_js_1 = require("./modules/input/InputProcessor.js");
const NetworkProcessor_js_1 = require("./modules/network/NetworkProcessor.js");
const PermissionsProcessor_js_1 = require("./modules/permissions/PermissionsProcessor.js");
const ScriptProcessor_js_1 = require("./modules/script/ScriptProcessor.js");
const SessionProcessor_js_1 = require("./modules/session/SessionProcessor.js");
const StorageProcessor_js_1 = require("./modules/storage/StorageProcessor.js");
const WebExtensionProcessor_js_1 = require("./modules/webExtension/WebExtensionProcessor.js");
const OutgoingMessage_js_1 = require("./OutgoingMessage.js");
class CommandProcessor extends EventEmitter_js_1.EventEmitter {
// keep-sorted start
#bluetoothProcessor;
#browserCdpClient;
#browserProcessor;
#browsingContextProcessor;
#cdpProcessor;
#emulationProcessor;
#inputProcessor;
#networkProcessor;
#permissionsProcessor;
#scriptProcessor;
#sessionProcessor;
#storageProcessor;
#webExtensionProcessor;
// keep-sorted end
#parser;
#logger;
constructor(cdpConnection, browserCdpClient, eventManager, browsingContextStorage, realmStorage, preloadScriptStorage, networkStorage, contextConfigStorage, bluetoothProcessor, userContextStorage, parser = new BidiNoOpParser_js_1.BidiNoOpParser(), initConnection, logger) {
super();
this.#browserCdpClient = browserCdpClient;
this.#parser = parser;
this.#logger = logger;
this.#bluetoothProcessor = bluetoothProcessor;
// keep-sorted start block=yes
this.#browserProcessor = new BrowserProcessor_js_1.BrowserProcessor(browserCdpClient, browsingContextStorage, contextConfigStorage, userContextStorage);
this.#browsingContextProcessor = new BrowsingContextProcessor_js_1.BrowsingContextProcessor(browserCdpClient, browsingContextStorage, userContextStorage, contextConfigStorage, eventManager);
this.#cdpProcessor = new CdpProcessor_js_1.CdpProcessor(browsingContextStorage, realmStorage, cdpConnection, browserCdpClient);
this.#emulationProcessor = new EmulationProcessor_js_1.EmulationProcessor(browsingContextStorage, userContextStorage, contextConfigStorage);
this.#inputProcessor = new InputProcessor_js_1.InputProcessor(browsingContextStorage);
this.#networkProcessor = new NetworkProcessor_js_1.NetworkProcessor(browsingContextStorage, networkStorage, userContextStorage, contextConfigStorage);
this.#permissionsProcessor = new PermissionsProcessor_js_1.PermissionsProcessor(browserCdpClient);
this.#scriptProcessor = new ScriptProcessor_js_1.ScriptProcessor(eventManager, browsingContextStorage, realmStorage, preloadScriptStorage, userContextStorage, logger);
this.#sessionProcessor = new SessionProcessor_js_1.SessionProcessor(eventManager, browserCdpClient, initConnection);
this.#storageProcessor = new StorageProcessor_js_1.StorageProcessor(browserCdpClient, browsingContextStorage, logger);
this.#webExtensionProcessor = new WebExtensionProcessor_js_1.WebExtensionProcessor(browserCdpClient);
// keep-sorted end
}
async #processCommand(command) {
switch (command.method) {
// Bluetooth module
// keep-sorted start block=yes
case 'bluetooth.disableSimulation':
return await this.#bluetoothProcessor.disableSimulation(this.#parser.parseDisableSimulationParameters(command.params));
case 'bluetooth.handleRequestDevicePrompt':
return await this.#bluetoothProcessor.handleRequestDevicePrompt(this.#parser.parseHandleRequestDevicePromptParams(command.params));
case 'bluetooth.simulateAdapter':
return await this.#bluetoothProcessor.simulateAdapter(this.#parser.parseSimulateAdapterParameters(command.params));
case 'bluetooth.simulateAdvertisement':
return await this.#bluetoothProcessor.simulateAdvertisement(this.#parser.parseSimulateAdvertisementParameters(command.params));
case 'bluetooth.simulateCharacteristic':
return await this.#bluetoothProcessor.simulateCharacteristic(this.#parser.parseSimulateCharacteristicParameters(command.params));
case 'bluetooth.simulateCharacteristicResponse':
return await this.#bluetoothProcessor.simulateCharacteristicResponse(this.#parser.parseSimulateCharacteristicResponseParameters(command.params));
case 'bluetooth.simulateDescriptor':
return await this.#bluetoothProcessor.simulateDescriptor(this.#parser.parseSimulateDescriptorParameters(command.params));
case 'bluetooth.simulateDescriptorResponse':
return await this.#bluetoothProcessor.simulateDescriptorResponse(this.#parser.parseSimulateDescriptorResponseParameters(command.params));
case 'bluetooth.simulateGattConnectionResponse':
return await this.#bluetoothProcessor.simulateGattConnectionResponse(this.#parser.parseSimulateGattConnectionResponseParameters(command.params));
case 'bluetooth.simulateGattDisconnection':
return await this.#bluetoothProcessor.simulateGattDisconnection(this.#parser.parseSimulateGattDisconnectionParameters(command.params));
case 'bluetooth.simulatePreconnectedPeripheral':
return await this.#bluetoothProcessor.simulatePreconnectedPeripheral(this.#parser.parseSimulatePreconnectedPeripheralParameters(command.params));
case 'bluetooth.simulateService':
return await this.#bluetoothProcessor.simulateService(this.#parser.parseSimulateServiceParameters(command.params));
// keep-sorted end
// Browser module
// keep-sorted start block=yes
case 'browser.close':
return this.#browserProcessor.close();
case 'browser.createUserContext':
return await this.#browserProcessor.createUserContext(this.#parser.parseCreateUserContextParameters(command.params));
case 'browser.getClientWindows':
return await this.#browserProcessor.getClientWindows();
case 'browser.getUserContexts':
return await this.#browserProcessor.getUserContexts();
case 'browser.removeUserContext':
return await this.#browserProcessor.removeUserContext(this.#parser.parseRemoveUserContextParameters(command.params));
case 'browser.setClientWindowState':
return await this.#browserProcessor.setClientWindowState(this.#parser.parseSetClientWindowStateParameters(command.params));
case 'browser.setDownloadBehavior':
return await this.#browserProcessor.setDownloadBehavior(this.#parser.parseSetDownloadBehaviorParameters(command.params));
// keep-sorted end
// Browsing Context module
// keep-sorted start block=yes
case 'browsingContext.activate':
return await this.#browsingContextProcessor.activate(this.#parser.parseActivateParams(command.params));
case 'browsingContext.captureScreenshot':
return await this.#browsingContextProcessor.captureScreenshot(this.#parser.parseCaptureScreenshotParams(command.params));
case 'browsingContext.close':
return await this.#browsingContextProcessor.close(this.#parser.parseCloseParams(command.params));
case 'browsingContext.create':
return await this.#browsingContextProcessor.create(this.#parser.parseCreateParams(command.params));
case 'browsingContext.getTree':
return this.#browsingContextProcessor.getTree(this.#parser.parseGetTreeParams(command.params));
case 'browsingContext.handleUserPrompt':
return await this.#browsingContextProcessor.handleUserPrompt(this.#parser.parseHandleUserPromptParams(command.params));
case 'browsingContext.locateNodes':
return await this.#browsingContextProcessor.locateNodes(this.#parser.parseLocateNodesParams(command.params));
case 'browsingContext.navigate':
return await this.#browsingContextProcessor.navigate(this.#parser.parseNavigateParams(command.params));
case 'browsingContext.print':
return await this.#browsingContextProcessor.print(this.#parser.parsePrintParams(command.params));
case 'browsingContext.reload':
return await this.#browsingContextProcessor.reload(this.#parser.parseReloadParams(command.params));
case 'browsingContext.setViewport':
return await this.#browsingContextProcessor.setViewport(this.#parser.parseSetViewportParams(command.params));
case 'browsingContext.traverseHistory':
return await this.#browsingContextProcessor.traverseHistory(this.#parser.parseTraverseHistoryParams(command.params));
// keep-sorted end
// CDP module
// keep-sorted start block=yes
case 'goog:cdp.getSession':
return this.#cdpProcessor.getSession(this.#parser.parseGetSessionParams(command.params));
case 'goog:cdp.resolveRealm':
return this.#cdpProcessor.resolveRealm(this.#parser.parseResolveRealmParams(command.params));
case 'goog:cdp.sendCommand':
return await this.#cdpProcessor.sendCommand(this.#parser.parseSendCommandParams(command.params));
// keep-sorted end
// Emulation module
// keep-sorted start block=yes
case 'emulation.setClientHintsOverride':
return await this.#emulationProcessor.setClientHintsOverride(this.#parser.parseSetClientHintsOverrideParams(command.params));
case 'emulation.setForcedColorsModeThemeOverride':
this.#parser.parseSetForcedColorsModeThemeOverrideParams(command.params);
throw new protocol_js_1.UnsupportedOperationException(`Method ${command.method} is not implemented.`);
case 'emulation.setGeolocationOverride':
return await this.#emulationProcessor.setGeolocationOverride(this.#parser.parseSetGeolocationOverrideParams(command.params));
case 'emulation.setLocaleOverride':
return await this.#emulationProcessor.setLocaleOverride(this.#parser.parseSetLocaleOverrideParams(command.params));
case 'emulation.setNetworkConditions':
return await this.#emulationProcessor.setNetworkConditions(this.#parser.parseSetNetworkConditionsParams(command.params));
case 'emulation.setScreenOrientationOverride':
return await this.#emulationProcessor.setScreenOrientationOverride(this.#parser.parseSetScreenOrientationOverrideParams(command.params));
case 'emulation.setScreenSettingsOverride':
return await this.#emulationProcessor.setScreenSettingsOverride(this.#parser.parseSetScreenSettingsOverrideParams(command.params));
case 'emulation.setScriptingEnabled':
return await this.#emulationProcessor.setScriptingEnabled(this.#parser.parseSetScriptingEnabledParams(command.params));
case 'emulation.setTimezoneOverride':
return await this.#emulationProcessor.setTimezoneOverride(this.#parser.parseSetTimezoneOverrideParams(command.params));
case 'emulation.setTouchOverride':
return await this.#emulationProcessor.setTouchOverride(this.#parser.parseSetTouchOverrideParams(command.params));
case 'emulation.setUserAgentOverride':
return await this.#emulationProcessor.setUserAgentOverrideParams(this.#parser.parseSetUserAgentOverrideParams(command.params));
// keep-sorted end
// Input module
// keep-sorted start block=yes
case 'input.performActions':
return await this.#inputProcessor.performActions(this.#parser.parsePerformActionsParams(command.params));
case 'input.releaseActions':
return await this.#inputProcessor.releaseActions(this.#parser.parseReleaseActionsParams(command.params));
case 'input.setFiles':
return await this.#inputProcessor.setFiles(this.#parser.parseSetFilesParams(command.params));
// keep-sorted end
// Network module
// keep-sorted start block=yes
case 'network.addDataCollector':
return await this.#networkProcessor.addDataCollector(this.#parser.parseAddDataCollectorParams(command.params));
case 'network.addIntercept':
return await this.#networkProcessor.addIntercept(this.#parser.parseAddInterceptParams(command.params));
case 'network.continueRequest':
return await this.#networkProcessor.continueRequest(this.#parser.parseContinueRequestParams(command.params));
case 'network.continueResponse':
return await this.#networkProcessor.continueResponse(this.#parser.parseContinueResponseParams(command.params));
case 'network.continueWithAuth':
return await this.#networkProcessor.continueWithAuth(this.#parser.parseContinueWithAuthParams(command.params));
case 'network.disownData':
return this.#networkProcessor.disownData(this.#parser.parseDisownDataParams(command.params));
case 'network.failRequest':
return await this.#networkProcessor.failRequest(this.#parser.parseFailRequestParams(command.params));
case 'network.getData':
return await this.#networkProcessor.getData(this.#parser.parseGetDataParams(command.params));
case 'network.provideResponse':
return await this.#networkProcessor.provideResponse(this.#parser.parseProvideResponseParams(command.params));
case 'network.removeDataCollector':
return await this.#networkProcessor.removeDataCollector(this.#parser.parseRemoveDataCollectorParams(command.params));
case 'network.removeIntercept':
return await this.#networkProcessor.removeIntercept(this.#parser.parseRemoveInterceptParams(command.params));
case 'network.setCacheBehavior':
return await this.#networkProcessor.setCacheBehavior(this.#parser.parseSetCacheBehaviorParams(command.params));
case 'network.setExtraHeaders':
return await this.#networkProcessor.setExtraHeaders(this.#parser.parseSetExtraHeadersParams(command.params));
// keep-sorted end
// Permissions module
// keep-sorted start block=yes
case 'permissions.setPermission':
return await this.#permissionsProcessor.setPermissions(this.#parser.parseSetPermissionsParams(command.params));
// keep-sorted end
// Script module
// keep-sorted start block=yes
case 'script.addPreloadScript':
return await this.#scriptProcessor.addPreloadScript(this.#parser.parseAddPreloadScriptParams(command.params));
case 'script.callFunction':
return await this.#scriptProcessor.callFunction(this.#parser.parseCallFunctionParams(this.#processTargetParams(command.params)));
case 'script.disown':
return await this.#scriptProcessor.disown(this.#parser.parseDisownParams(this.#processTargetParams(command.params)));
case 'script.evaluate':
return await this.#scriptProcessor.evaluate(this.#parser.parseEvaluateParams(this.#processTargetParams(command.params)));
case 'script.getRealms':
return this.#scriptProcessor.getRealms(this.#parser.parseGetRealmsParams(command.params));
case 'script.removePreloadScript':
return await this.#scriptProcessor.removePreloadScript(this.#parser.parseRemovePreloadScriptParams(command.params));
// keep-sorted end
// Session module
// keep-sorted start block=yes
case 'session.end':
throw new protocol_js_1.UnsupportedOperationException(`Method ${command.method} is not implemented.`);
case 'session.new':
return await this.#sessionProcessor.new(command.params);
case 'session.status':
return this.#sessionProcessor.status();
case 'session.subscribe':
return await this.#sessionProcessor.subscribe(this.#parser.parseSubscribeParams(command.params), command['goog:channel']);
case 'session.unsubscribe':
return await this.#sessionProcessor.unsubscribe(this.#parser.parseUnsubscribeParams(command.params), command['goog:channel']);
// keep-sorted end
// Storage module
// keep-sorted start block=yes
case 'storage.deleteCookies':
return await this.#storageProcessor.deleteCookies(this.#parser.parseDeleteCookiesParams(command.params));
case 'storage.getCookies':
return await this.#storageProcessor.getCookies(this.#parser.parseGetCookiesParams(command.params));
case 'storage.setCookie':
return await this.#storageProcessor.setCookie(this.#parser.parseSetCookieParams(command.params));
// keep-sorted end
// WebExtension module
// keep-sorted start block=yes
case 'webExtension.install':
return await this.#webExtensionProcessor.install(this.#parser.parseInstallParams(command.params));
case 'webExtension.uninstall':
return await this.#webExtensionProcessor.uninstall(this.#parser.parseUninstallParams(command.params));
// keep-sorted end
}
// Intentionally kept outside the switch statement to ensure that
// ESLint @typescript-eslint/switch-exhaustiveness-check triggers if a new
// command is added.
throw new protocol_js_1.UnknownCommandException(`Unknown command '${command?.method}'.`);
}
// Workaround for as zod.union always take the first schema
// https://github.com/w3c/webdriver-bidi/issues/635
#processTargetParams(params) {
if (typeof params === 'object' &&
params &&
'target' in params &&
typeof params.target === 'object' &&
params.target &&
'context' in params.target) {
delete params.target['realm'];
}
return params;
}
async processCommand(command) {
try {
const result = await this.#processCommand(command);
const response = {
type: 'success',
id: command.id,
result,
};
this.emit("response" /* CommandProcessorEvents.Response */, {
message: OutgoingMessage_js_1.OutgoingMessage.createResolved(response, command['goog:channel']),
event: command.method,
});
}
catch (e) {
if (e instanceof protocol_js_1.Exception) {
this.emit("response" /* CommandProcessorEvents.Response */, {
message: OutgoingMessage_js_1.OutgoingMessage.createResolved(e.toErrorResponse(command.id), command['goog:channel']),
event: command.method,
});
}
else {
const error = e;
this.#logger?.(log_js_1.LogType.bidi, error);
// Heuristic required for processing cases when a browsing context is gone
// during the command processing, e.g. like in test
// `test_input_keyDown_closes_browsing_context`.
const errorException = this.#browserCdpClient.isCloseError(e)
? new protocol_js_1.NoSuchFrameException(`Browsing context is gone`)
: new protocol_js_1.UnknownErrorException(error.message, error.stack);
this.emit("response" /* CommandProcessorEvents.Response */, {
message: OutgoingMessage_js_1.OutgoingMessage.createResolved(errorException.toErrorResponse(command.id), command['goog:channel']),
event: command.method,
});
}
}
}
}
exports.CommandProcessor = CommandProcessor;
//# sourceMappingURL=CommandProcessor.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,7 @@
import type { Session } from '../protocol/generated/webdriver-bidi.js';
export interface MapperOptions {
acceptInsecureCerts?: boolean;
unhandledPromptBehavior?: Session.UserPromptHandler;
'goog:prerenderingDisabled'?: boolean;
'goog:disableNetworkDurableMessages'?: true;
}

View File

@@ -0,0 +1,20 @@
"use strict";
/*
* 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.
*
*/
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=MapperOptions.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"MapperOptions.js","sourceRoot":"","sources":["../../../src/bidiMapper/MapperOptions.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;GAgBG"}

View File

@@ -0,0 +1,27 @@
/**
* 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.
*/
import type { GoogChannel } from '../protocol/chromium-bidi.js';
import type { ChromiumBidi } from '../protocol/protocol.js';
import type { Result } from '../utils/result.js';
export declare class OutgoingMessage {
#private;
private constructor();
static createFromPromise(messagePromise: Promise<Result<ChromiumBidi.Message>>, googChannel: GoogChannel): Promise<Result<OutgoingMessage>>;
static createResolved(message: ChromiumBidi.Message, googChannel?: GoogChannel): Promise<Result<OutgoingMessage>>;
get message(): ChromiumBidi.Message;
get googChannel(): GoogChannel;
}

View File

@@ -0,0 +1,52 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.OutgoingMessage = void 0;
class OutgoingMessage {
#message;
#googChannel;
constructor(message, googChannel = null) {
this.#message = message;
this.#googChannel = googChannel;
}
static createFromPromise(messagePromise, googChannel) {
return messagePromise.then((message) => {
if (message.kind === 'success') {
return {
kind: 'success',
value: new OutgoingMessage(message.value, googChannel),
};
}
return message;
});
}
static createResolved(message, googChannel = null) {
return Promise.resolve({
kind: 'success',
value: new OutgoingMessage(message, googChannel),
});
}
get message() {
return this.#message;
}
get googChannel() {
return this.#googChannel;
}
}
exports.OutgoingMessage = OutgoingMessage;
//# sourceMappingURL=OutgoingMessage.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"OutgoingMessage.js","sourceRoot":"","sources":["../../../src/bidiMapper/OutgoingMessage.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;;AAMH,MAAa,eAAe;IACjB,QAAQ,CAAuB;IAC/B,YAAY,CAAc;IAEnC,YACE,OAA6B,EAC7B,cAA2B,IAAI;QAE/B,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;QACxB,IAAI,CAAC,YAAY,GAAG,WAAW,CAAC;IAClC,CAAC;IAED,MAAM,CAAC,iBAAiB,CACtB,cAAqD,EACrD,WAAwB;QAExB,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE;YACrC,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;gBAC/B,OAAO;oBACL,IAAI,EAAE,SAAS;oBACf,KAAK,EAAE,IAAI,eAAe,CAAC,OAAO,CAAC,KAAK,EAAE,WAAW,CAAC;iBACvD,CAAC;YACJ,CAAC;YACD,OAAO,OAAO,CAAC;QACjB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,MAAM,CAAC,cAAc,CACnB,OAA6B,EAC7B,cAA2B,IAAI;QAE/B,OAAO,OAAO,CAAC,OAAO,CAAC;YACrB,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,IAAI,eAAe,CAAC,OAAO,EAAE,WAAW,CAAC;SACjD,CAAC,CAAC;IACL,CAAC;IAED,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;IAED,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,YAAY,CAAC;IAC3B,CAAC;CACF;AA5CD,0CA4CC"}

View File

@@ -0,0 +1,37 @@
/**
* Copyright 2024 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.
*/
import { type Bluetooth, type EmptyResult } from '../../../protocol/protocol.js';
import type { CdpTarget } from '../cdp/CdpTarget.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
import type { EventManager } from '../session/EventManager.js';
export declare class BluetoothProcessor {
#private;
constructor(eventManager: EventManager, browsingContextStorage: BrowsingContextStorage);
simulateAdapter(params: Bluetooth.SimulateAdapterParameters): Promise<EmptyResult>;
disableSimulation(params: Bluetooth.DisableSimulationParameters): Promise<EmptyResult>;
simulatePreconnectedPeripheral(params: Bluetooth.SimulatePreconnectedPeripheralParameters): Promise<EmptyResult>;
simulateAdvertisement(params: Bluetooth.SimulateAdvertisementParameters): Promise<EmptyResult>;
simulateCharacteristic(params: Bluetooth.SimulateCharacteristicParameters): Promise<EmptyResult>;
simulateCharacteristicResponse(params: Bluetooth.SimulateCharacteristicResponseParameters): Promise<EmptyResult>;
simulateDescriptor(params: Bluetooth.SimulateDescriptorParameters): Promise<EmptyResult>;
simulateDescriptorResponse(params: Bluetooth.SimulateDescriptorResponseParameters): Promise<EmptyResult>;
simulateGattConnectionResponse(params: Bluetooth.SimulateGattConnectionResponseParameters): Promise<EmptyResult>;
simulateGattDisconnection(params: Bluetooth.SimulateGattDisconnectionParameters): Promise<EmptyResult>;
simulateService(params: Bluetooth.SimulateServiceParameters): Promise<EmptyResult>;
onCdpTargetCreated(cdpTarget: CdpTarget): void;
handleRequestDevicePrompt(params: Bluetooth.HandleRequestDevicePromptParameters): Promise<EmptyResult>;
}

View File

@@ -0,0 +1,411 @@
"use strict";
/**
* Copyright 2024 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.BluetoothProcessor = void 0;
const protocol_js_1 = require("../../../protocol/protocol.js");
/** Represents a base Bluetooth GATT item. */
class BluetoothGattItem {
id;
uuid;
constructor(id, uuid) {
this.id = id;
this.uuid = uuid;
}
}
/** Represents a Bluetooth descriptor. */
class BluetoothDescriptor extends BluetoothGattItem {
characteristic;
constructor(id, uuid, characteristic) {
super(id, uuid);
this.characteristic = characteristic;
}
}
/** Represents a Bluetooth characteristic. */
class BluetoothCharacteristic extends BluetoothGattItem {
descriptors = new Map();
service;
constructor(id, uuid, service) {
super(id, uuid);
this.service = service;
}
}
/** Represents a Bluetooth service. */
class BluetoothService extends BluetoothGattItem {
characteristics = new Map();
device;
constructor(id, uuid, device) {
super(id, uuid);
this.device = device;
}
}
/** Represents a Bluetooth device. */
class BluetoothDevice {
address;
services = new Map();
constructor(address) {
this.address = address;
}
}
class BluetoothProcessor {
#eventManager;
#browsingContextStorage;
#bluetoothDevices = new Map();
// A map from a characteristic id from CDP to its BluetoothCharacteristic object.
#bluetoothCharacteristics = new Map();
// A map from a descriptor id from CDP to its BluetoothDescriptor object.
#bluetoothDescriptors = new Map();
constructor(eventManager, browsingContextStorage) {
this.#eventManager = eventManager;
this.#browsingContextStorage = browsingContextStorage;
}
#getDevice(address) {
const device = this.#bluetoothDevices.get(address);
if (!device) {
throw new protocol_js_1.InvalidArgumentException(`Bluetooth device with address ${address} does not exist`);
}
return device;
}
#getService(device, serviceUuid) {
const service = device.services.get(serviceUuid);
if (!service) {
throw new protocol_js_1.InvalidArgumentException(`Service with UUID ${serviceUuid} on device ${device.address} does not exist`);
}
return service;
}
#getCharacteristic(service, characteristicUuid) {
const characteristic = service.characteristics.get(characteristicUuid);
if (!characteristic) {
throw new protocol_js_1.InvalidArgumentException(`Characteristic with UUID ${characteristicUuid} does not exist for service ${service.uuid} on device ${service.device.address}`);
}
return characteristic;
}
#getDescriptor(characteristic, descriptorUuid) {
const descriptor = characteristic.descriptors.get(descriptorUuid);
if (!descriptor) {
throw new protocol_js_1.InvalidArgumentException(`Descriptor with UUID ${descriptorUuid} does not exist for characteristic ${characteristic.uuid} on service ${characteristic.service.uuid} on device ${characteristic.service.device.address}`);
}
return descriptor;
}
async simulateAdapter(params) {
if (params.state === undefined) {
// The bluetooth.simulateAdapter Command
// Step 4.2. If params["state"] does not exist, return error with error code invalid argument.
// https://webbluetoothcg.github.io/web-bluetooth/#bluetooth-simulateAdapter-command
throw new protocol_js_1.InvalidArgumentException(`Parameter "state" is required for creating a Bluetooth adapter`);
}
const context = this.#browsingContextStorage.getContext(params.context);
// Bluetooth spec requires overriding the existing adapter (step 6). From the CDP
// perspective, we need to disable the emulation first.
// https://webbluetoothcg.github.io/web-bluetooth/#bluetooth-simulateAdapter-command
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.disable');
this.#bluetoothDevices.clear();
this.#bluetoothCharacteristics.clear();
this.#bluetoothDescriptors.clear();
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.enable', {
state: params.state,
leSupported: params.leSupported ?? true,
});
return {};
}
async disableSimulation(params) {
const context = this.#browsingContextStorage.getContext(params.context);
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.disable');
this.#bluetoothDevices.clear();
this.#bluetoothCharacteristics.clear();
this.#bluetoothDescriptors.clear();
return {};
}
async simulatePreconnectedPeripheral(params) {
if (this.#bluetoothDevices.has(params.address)) {
throw new protocol_js_1.InvalidArgumentException(`Bluetooth device with address ${params.address} already exists`);
}
const context = this.#browsingContextStorage.getContext(params.context);
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulatePreconnectedPeripheral', {
address: params.address,
name: params.name,
knownServiceUuids: params.knownServiceUuids,
manufacturerData: params.manufacturerData,
});
this.#bluetoothDevices.set(params.address, new BluetoothDevice(params.address));
return {};
}
async simulateAdvertisement(params) {
const context = this.#browsingContextStorage.getContext(params.context);
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulateAdvertisement', {
entry: params.scanEntry,
});
return {};
}
async simulateCharacteristic(params) {
const device = this.#getDevice(params.address);
const service = this.#getService(device, params.serviceUuid);
const context = this.#browsingContextStorage.getContext(params.context);
switch (params.type) {
case 'add': {
if (params.characteristicProperties === undefined) {
throw new protocol_js_1.InvalidArgumentException(`Parameter "characteristicProperties" is required for adding a Bluetooth characteristic`);
}
if (service.characteristics.has(params.characteristicUuid)) {
throw new protocol_js_1.InvalidArgumentException(`Characteristic with UUID ${params.characteristicUuid} already exists`);
}
const response = await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.addCharacteristic', {
serviceId: service.id,
characteristicUuid: params.characteristicUuid,
properties: params.characteristicProperties,
});
const characteristic = new BluetoothCharacteristic(response.characteristicId, params.characteristicUuid, service);
service.characteristics.set(params.characteristicUuid, characteristic);
this.#bluetoothCharacteristics.set(characteristic.id, characteristic);
return {};
}
case 'remove': {
if (params.characteristicProperties !== undefined) {
throw new protocol_js_1.InvalidArgumentException(`Parameter "characteristicProperties" should not be provided for removing a Bluetooth characteristic`);
}
const characteristic = this.#getCharacteristic(service, params.characteristicUuid);
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.removeCharacteristic', {
characteristicId: characteristic.id,
});
service.characteristics.delete(params.characteristicUuid);
this.#bluetoothCharacteristics.delete(characteristic.id);
return {};
}
default:
throw new protocol_js_1.InvalidArgumentException(`Parameter "type" of ${params.type} is not supported`);
}
}
async simulateCharacteristicResponse(params) {
const context = this.#browsingContextStorage.getContext(params.context);
const device = this.#getDevice(params.address);
const service = this.#getService(device, params.serviceUuid);
const characteristic = this.#getCharacteristic(service, params.characteristicUuid);
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulateCharacteristicOperationResponse', {
characteristicId: characteristic.id,
type: params.type,
code: params.code,
...(params.data && {
data: btoa(String.fromCharCode(...params.data)),
}),
});
return {};
}
async simulateDescriptor(params) {
const device = this.#getDevice(params.address);
const service = this.#getService(device, params.serviceUuid);
const characteristic = this.#getCharacteristic(service, params.characteristicUuid);
const context = this.#browsingContextStorage.getContext(params.context);
switch (params.type) {
case 'add': {
if (characteristic.descriptors.has(params.descriptorUuid)) {
throw new protocol_js_1.InvalidArgumentException(`Descriptor with UUID ${params.descriptorUuid} already exists`);
}
const response = await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.addDescriptor', {
characteristicId: characteristic.id,
descriptorUuid: params.descriptorUuid,
});
const descriptor = new BluetoothDescriptor(response.descriptorId, params.descriptorUuid, characteristic);
characteristic.descriptors.set(params.descriptorUuid, descriptor);
this.#bluetoothDescriptors.set(descriptor.id, descriptor);
return {};
}
case 'remove': {
const descriptor = this.#getDescriptor(characteristic, params.descriptorUuid);
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.removeDescriptor', {
descriptorId: descriptor.id,
});
characteristic.descriptors.delete(params.descriptorUuid);
this.#bluetoothDescriptors.delete(descriptor.id);
return {};
}
default:
throw new protocol_js_1.InvalidArgumentException(`Parameter "type" of ${params.type} is not supported`);
}
}
async simulateDescriptorResponse(params) {
const context = this.#browsingContextStorage.getContext(params.context);
const device = this.#getDevice(params.address);
const service = this.#getService(device, params.serviceUuid);
const characteristic = this.#getCharacteristic(service, params.characteristicUuid);
const descriptor = this.#getDescriptor(characteristic, params.descriptorUuid);
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulateDescriptorOperationResponse', {
descriptorId: descriptor.id,
type: params.type,
code: params.code,
...(params.data && {
data: btoa(String.fromCharCode(...params.data)),
}),
});
return {};
}
async simulateGattConnectionResponse(params) {
const context = this.#browsingContextStorage.getContext(params.context);
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulateGATTOperationResponse', {
address: params.address,
type: 'connection',
code: params.code,
});
return {};
}
async simulateGattDisconnection(params) {
const context = this.#browsingContextStorage.getContext(params.context);
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulateGATTDisconnection', {
address: params.address,
});
return {};
}
async simulateService(params) {
const device = this.#getDevice(params.address);
const context = this.#browsingContextStorage.getContext(params.context);
switch (params.type) {
case 'add': {
if (device.services.has(params.uuid)) {
throw new protocol_js_1.InvalidArgumentException(`Service with UUID ${params.uuid} already exists`);
}
const response = await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.addService', {
address: params.address,
serviceUuid: params.uuid,
});
device.services.set(params.uuid, new BluetoothService(response.serviceId, params.uuid, device));
return {};
}
case 'remove': {
const service = this.#getService(device, params.uuid);
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.removeService', {
serviceId: service.id,
});
device.services.delete(params.uuid);
return {};
}
default:
throw new protocol_js_1.InvalidArgumentException(`Parameter "type" of ${params.type} is not supported`);
}
}
onCdpTargetCreated(cdpTarget) {
cdpTarget.cdpClient.on('DeviceAccess.deviceRequestPrompted', (event) => {
this.#eventManager.registerEvent({
type: 'event',
method: 'bluetooth.requestDevicePromptUpdated',
params: {
context: cdpTarget.id,
prompt: event.id,
devices: event.devices,
},
}, cdpTarget.id);
});
cdpTarget.browserCdpClient.on('BluetoothEmulation.gattOperationReceived', async (event) => {
switch (event.type) {
case 'connection':
this.#eventManager.registerEvent({
type: 'event',
method: 'bluetooth.gattConnectionAttempted',
params: {
context: cdpTarget.id,
address: event.address,
},
}, cdpTarget.id);
return;
case 'discovery':
// Chromium Web Bluetooth simulation generates this GATT discovery event when
// a page attempts to get services for a given Bluetooth device for the first time.
// This 'get services' operation is put on hold until a GATT discovery response
// is sent to the simulation.
// Note: Web Bluetooth automation (see https://webbluetoothcg.github.io/web-bluetooth/#automated-testing)
// does not support simulating a GATT discovery response. This is because simulated services, characteristics,
// or descriptors are immediately visible to the simulation, meaning it doesn't have a distinct
// DISCOVERY state. Therefore, this code simulates a successful GATT discovery
// response upon receiving this event.
await cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulateGATTOperationResponse', {
address: event.address,
type: 'discovery',
code: 0x0,
});
}
});
cdpTarget.browserCdpClient.on('BluetoothEmulation.characteristicOperationReceived', (event) => {
if (!this.#bluetoothCharacteristics.has(event.characteristicId)) {
return;
}
let type;
if (event.type === 'write') {
// write-default-deprecated comes from
// https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetoothremotegattcharacteristic-writevalue,
// which is deprecated so not supported.
if (event.writeType === 'write-default-deprecated') {
return;
}
type = event.writeType;
}
else {
type = event.type;
}
const characteristic = this.#bluetoothCharacteristics.get(event.characteristicId);
this.#eventManager.registerEvent({
type: 'event',
method: 'bluetooth.characteristicEventGenerated',
params: {
context: cdpTarget.id,
address: characteristic.service.device.address,
serviceUuid: characteristic.service.uuid,
characteristicUuid: characteristic.uuid,
type,
...(event.data && {
data: Array.from(atob(event.data), (c) => c.charCodeAt(0)),
}),
},
}, cdpTarget.id);
});
cdpTarget.browserCdpClient.on('BluetoothEmulation.descriptorOperationReceived', (event) => {
if (!this.#bluetoothDescriptors.has(event.descriptorId)) {
return;
}
const descriptor = this.#bluetoothDescriptors.get(event.descriptorId);
this.#eventManager.registerEvent({
type: 'event',
method: 'bluetooth.descriptorEventGenerated',
params: {
context: cdpTarget.id,
address: descriptor.characteristic.service.device.address,
serviceUuid: descriptor.characteristic.service.uuid,
characteristicUuid: descriptor.characteristic.uuid,
descriptorUuid: descriptor.uuid,
type: event.type,
...(event.data && {
data: Array.from(atob(event.data), (c) => c.charCodeAt(0)),
}),
},
}, cdpTarget.id);
});
}
async handleRequestDevicePrompt(params) {
const context = this.#browsingContextStorage.getContext(params.context);
if (params.accept) {
await context.cdpTarget.cdpClient.sendCommand('DeviceAccess.selectPrompt', {
id: params.prompt,
deviceId: params.device,
});
}
else {
await context.cdpTarget.cdpClient.sendCommand('DeviceAccess.cancelPrompt', {
id: params.prompt,
});
}
return {};
}
}
exports.BluetoothProcessor = BluetoothProcessor;
//# sourceMappingURL=BluetoothProcessor.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,37 @@
/**
* 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.
*/
import { type Browser, type EmptyResult, type Session } from '../../../protocol/protocol.js';
import type { CdpClient } from '../../BidiMapper.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
import type { ContextConfigStorage } from './ContextConfigStorage.js';
import type { UserContextStorage } from './UserContextStorage.js';
export declare class BrowserProcessor {
#private;
constructor(browserCdpClient: CdpClient, browsingContextStorage: BrowsingContextStorage, configStorage: ContextConfigStorage, userContextStorage: UserContextStorage);
close(): EmptyResult;
createUserContext(params: Record<string, any>): Promise<Browser.CreateUserContextResult>;
removeUserContext(params: Browser.RemoveUserContextParameters): Promise<EmptyResult>;
getUserContexts(): Promise<Browser.GetUserContextsResult>;
setClientWindowState(params: Browser.SetClientWindowStateParameters): Promise<Browser.SetClientWindowStateResult>;
getClientWindows(): Promise<Browser.GetClientWindowsResult>;
setDownloadBehavior(params: Browser.SetDownloadBehaviorParameters): Promise<EmptyResult>;
}
/**
* Proxy config parse implementation:
* https://source.chromium.org/chromium/chromium/src/+/main:net/proxy_resolution/proxy_config.h;drc=743a82d08e59d803c94ee1b8564b8b11dd7b462f;l=107
*/
export declare function getProxyStr(proxyConfig: Session.ProxyConfiguration): string | undefined;

View File

@@ -0,0 +1,294 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.BrowserProcessor = void 0;
exports.getProxyStr = getProxyStr;
const protocol_js_1 = require("../../../protocol/protocol.js");
class BrowserProcessor {
#browserCdpClient;
#browsingContextStorage;
#configStorage;
#userContextStorage;
constructor(browserCdpClient, browsingContextStorage, configStorage, userContextStorage) {
this.#browserCdpClient = browserCdpClient;
this.#browsingContextStorage = browsingContextStorage;
this.#configStorage = configStorage;
this.#userContextStorage = userContextStorage;
}
close() {
// Ensure that it is put at the end of the event loop.
// This way we send back the response before closing the tab.
// Always catch uncaught exceptions.
setTimeout(() => this.#browserCdpClient.sendCommand('Browser.close').catch(() => { }), 0);
return {};
}
async createUserContext(params) {
// `params` is a record to provide legacy `goog:` parameters. Now as the `proxy`
// parameter is specified, we should get rid of `goog:proxyServer` and
// `goog:proxyBypassList` and make the params of type
// `Browser.CreateUserContextParameters`.
const w3cParams = params;
const globalConfig = this.#configStorage.getGlobalConfig();
if (w3cParams.acceptInsecureCerts !== undefined) {
if (w3cParams.acceptInsecureCerts === false &&
globalConfig.acceptInsecureCerts === true)
// TODO: https://github.com/GoogleChromeLabs/chromium-bidi/issues/3398
throw new protocol_js_1.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 {
// TODO: remove after Puppeteer stops using it.
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 protocol_js_1.InvalidArgumentException('`default` user context cannot be removed');
}
try {
await this.#browserCdpClient.sendCommand('Target.disposeBrowserContext', {
browserContextId: userContext,
});
}
catch (err) {
// https://source.chromium.org/chromium/chromium/src/+/main:content/browser/devtools/protocol/target_handler.cc;l=1424;drc=c686e8f4fd379312469fe018f5c390e9c8f20d0d
if (err.message.startsWith('Failed to find context with id')) {
throw new protocol_js_1.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` is not supported in CDP yet.
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 protocol_js_1.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();
// Filter out duplicated client windows.
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)
// CDP "default" behavior.
return {
behavior: 'default',
};
if (downloadBehavior?.type === 'denied')
// Deny all the downloads.
return {
behavior: 'deny',
};
if (downloadBehavior?.type === 'allowed') {
// CDP behavior "allow" means "save downloaded files to the specific download path".
return {
behavior: 'allow',
downloadPath: downloadBehavior.destinationFolder,
};
}
// Unreachable. Handled by params parser.
throw new protocol_js_1.UnknownErrorException('Unexpected download behavior');
}
async #applyDownloadBehavior(downloadBehavior, userContext) {
await this.#browserCdpClient.sendCommand('Browser.setDownloadBehavior', {
...this.#toCdpDownloadBehavior(downloadBehavior),
browserContextId: userContext === 'default' ? undefined : userContext,
// Required for enabling download events.
eventsEnabled: true,
});
}
async setDownloadBehavior(params) {
let userContexts;
if (params.userContexts === undefined) {
// Global download behavior.
userContexts = (await this.#userContextStorage.getUserContexts()).map((c) => c.userContext);
}
else {
// Download behavior for the specific user contexts.
userContexts = Array.from(await this.#userContextStorage.verifyUserContextIdList(params.userContexts));
}
if (params.userContexts === undefined) {
// Store the global setting to be applied for the future user contexts.
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) => {
// Download behavior can be already set per user context, in which case the global
// one should not be applied.
const downloadBehavior = this.#configStorage.getActiveConfig(undefined, userContext)
.downloadBehavior ?? null;
await this.#applyDownloadBehavior(downloadBehavior, userContext);
}));
return {};
}
}
exports.BrowserProcessor = BrowserProcessor;
/**
* Proxy config parse implementation:
* https://source.chromium.org/chromium/chromium/src/+/main:net/proxy_resolution/proxy_config.h;drc=743a82d08e59d803c94ee1b8564b8b11dd7b462f;l=107
*/
function getProxyStr(proxyConfig) {
if (proxyConfig.proxyType === 'direct' ||
proxyConfig.proxyType === 'system') {
// These types imply that Chrome should use its default behavior (e.g., direct
// connection or system-configured proxy). No specific `proxyServer` string is
// needed.
return undefined;
}
if (proxyConfig.proxyType === 'pac') {
throw new protocol_js_1.UnsupportedOperationException(`PAC proxy configuration is not supported per user context`);
}
if (proxyConfig.proxyType === 'autodetect') {
throw new protocol_js_1.UnsupportedOperationException(`Autodetect proxy is not supported per user context`);
}
if (proxyConfig.proxyType === 'manual') {
const servers = [];
// HTTP Proxy
if (proxyConfig.httpProxy !== undefined) {
// servers.push(proxyConfig.httpProxy);
servers.push(`http=${proxyConfig.httpProxy}`);
}
// SSL Proxy (uses 'https' scheme)
if (proxyConfig.sslProxy !== undefined) {
// servers.push(proxyConfig.sslProxy);
servers.push(`https=${proxyConfig.sslProxy}`);
}
// SOCKS Proxy
if (proxyConfig.socksProxy !== undefined ||
proxyConfig.socksVersion !== undefined) {
// socksVersion is mandatory and must be a valid integer if socksProxy is
// specified.
if (proxyConfig.socksProxy === undefined) {
throw new protocol_js_1.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 protocol_js_1.InvalidArgumentException(`'socksVersion' must be between 0 and 255`);
}
servers.push(`socks=socks${proxyConfig.socksVersion}://${proxyConfig.socksProxy}`);
}
if (servers.length === 0) {
// If 'manual' proxyType is chosen but no specific proxy servers (http, ssl, socks)
// are provided, it means no proxy server should be configured.
return undefined;
}
return servers.join(';');
}
// Unreachable.
throw new protocol_js_1.UnknownErrorException(`Unknown proxy type`);
}
//# sourceMappingURL=BrowserProcessor.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,50 @@
/**
* 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.
*/
import type { Protocol } from 'devtools-protocol';
import type { Browser, BrowsingContext, Emulation, Session, UAClientHints } from '../../../protocol/protocol.js';
/**
* Represents a context configurations. It can be global, per User Context, or per
* Browsing Context. The undefined value means the config will be taken from the upstream
* config. `null` values means the value should be default regardless of the upstream.
*/
export declare class ContextConfig {
acceptInsecureCerts?: boolean;
clientHints?: UAClientHints.Emulation.ClientHintsMetadata | null;
devicePixelRatio?: number | null;
disableNetworkDurableMessages?: true;
downloadBehavior?: Browser.DownloadBehavior | null;
emulatedNetworkConditions?: Emulation.NetworkConditions | null;
extraHeaders?: Protocol.Network.Headers;
geolocation?: Emulation.GeolocationCoordinates | Emulation.GeolocationPositionError | null;
locale?: string | null;
maxTouchPoints?: number | null;
prerenderingDisabled?: boolean;
screenArea?: Emulation.ScreenArea | null;
screenOrientation?: Emulation.ScreenOrientation | null;
scriptingEnabled?: false | null;
timezone?: string | null;
userAgent?: string | null;
userPromptHandler?: Session.UserPromptHandler;
viewport?: BrowsingContext.Viewport | null;
/**
* Merges multiple `ContextConfig` objects. The configs are merged in the order they are
* provided. For each property, the value from the last config that defines it will be
* used. The final result will not contain any `undefined` or `null` properties.
* `undefined` values are ignored. `null` values remove the already set value.
*/
static merge(...configs: (ContextConfig | undefined)[]): ContextConfig;
}

View File

@@ -0,0 +1,74 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ContextConfig = void 0;
/**
* Represents a context configurations. It can be global, per User Context, or per
* Browsing Context. The undefined value means the config will be taken from the upstream
* config. `null` values means the value should be default regardless of the upstream.
*/
class ContextConfig {
// keep-sorted start block=yes
acceptInsecureCerts;
clientHints;
devicePixelRatio;
disableNetworkDurableMessages;
downloadBehavior;
emulatedNetworkConditions;
// Extra headers are kept in CDP format.
extraHeaders;
geolocation;
locale;
maxTouchPoints;
prerenderingDisabled;
screenArea;
screenOrientation;
scriptingEnabled;
// Timezone is kept in CDP format with GMT prefix for offset values.
timezone;
userAgent;
userPromptHandler;
viewport;
// keep-sorted end
/**
* Merges multiple `ContextConfig` objects. The configs are merged in the order they are
* provided. For each property, the value from the last config that defines it will be
* used. The final result will not contain any `undefined` or `null` properties.
* `undefined` values are ignored. `null` values remove the already set value.
*/
static merge(...configs) {
const result = new ContextConfig();
for (const config of configs) {
if (!config) {
continue;
}
for (const key in config) {
const value = config[key];
if (value === null) {
delete result[key];
}
else if (value !== undefined) {
result[key] = value;
}
}
}
return result;
}
}
exports.ContextConfig = ContextConfig;
//# sourceMappingURL=ContextConfig.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ContextConfig.js","sourceRoot":"","sources":["../../../../../src/bidiMapper/modules/browser/ContextConfig.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;;AAYH;;;;GAIG;AACH,MAAa,aAAa;IACxB,8BAA8B;IAC9B,mBAAmB,CAAW;IAC9B,WAAW,CAAsD;IACjE,gBAAgB,CAAiB;IACjC,6BAA6B,CAAQ;IACrC,gBAAgB,CAAmC;IACnD,yBAAyB,CAAsC;IAC/D,wCAAwC;IACxC,YAAY,CAA4B;IACxC,WAAW,CAGF;IACT,MAAM,CAAiB;IACvB,cAAc,CAAiB;IAC/B,oBAAoB,CAAW;IAC/B,UAAU,CAA+B;IACzC,iBAAiB,CAAsC;IACvD,gBAAgB,CAAgB;IAChC,oEAAoE;IACpE,QAAQ,CAAiB;IACzB,SAAS,CAAiB;IAC1B,iBAAiB,CAA6B;IAC9C,QAAQ,CAAmC;IAC3C,kBAAkB;IAElB;;;;;OAKG;IACH,MAAM,CAAC,KAAK,CAAC,GAAG,OAAsC;QACpD,MAAM,MAAM,GAAG,IAAI,aAAa,EAAE,CAAC;QAEnC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,SAAS;YACX,CAAC;YACD,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;gBACzB,MAAM,KAAK,GAAG,MAAM,CAAC,GAA0B,CAAC,CAAC;gBACjD,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;oBACnB,OAAQ,MAAc,CAAC,GAAG,CAAC,CAAC;gBAC9B,CAAC;qBAAM,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;oBAC9B,MAAc,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;gBAC/B,CAAC;YACH,CAAC;QACH,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;CACF;AAnDD,sCAmDC"}

View File

@@ -0,0 +1,42 @@
import { ContextConfig } from './ContextConfig.js';
/**
* Manages context-specific configurations. This class allows setting
* configurations at three levels: global, user context, and browsing context.
*
* When `getActiveConfig` is called, it merges the configurations in a specific
* order of precedence: `global -> user context -> browsing context`. For each
* configuration property, the value from the highest-precedence level that has a
* non-`undefined` value is used.
*
* The `update` methods (`updateGlobalConfig`, `updateUserContextConfig`,
* `updateBrowsingContextConfig`) merge the provided configuration with the
* existing one at the corresponding level. Properties with `undefined` values in
* the provided configuration are ignored, preserving the existing value.
*/
export declare class ContextConfigStorage {
#private;
/**
* Updates the global configuration. Properties with `undefined` values in the
* provided `config` are ignored.
*/
updateGlobalConfig(config: ContextConfig): void;
/**
* Updates the configuration for a specific browsing context. Properties with
* `undefined` values in the provided `config` are ignored.
*/
updateBrowsingContextConfig(browsingContextId: string, config: ContextConfig): void;
/**
* Updates the configuration for a specific user context. Properties with
* `undefined` values in the provided `config` are ignored.
*/
updateUserContextConfig(userContext: string, config: ContextConfig): void;
/**
* Returns the current global configuration.
*/
getGlobalConfig(): ContextConfig;
/**
* Calculates the active configuration by merging global, user context, and
* browsing context settings.
*/
getActiveConfig(topLevelBrowsingContextId: string | undefined, userContext: string): ContextConfig;
}

View File

@@ -0,0 +1,96 @@
"use strict";
/*
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ContextConfigStorage = void 0;
const ContextConfig_js_1 = require("./ContextConfig.js");
/**
* Manages context-specific configurations. This class allows setting
* configurations at three levels: global, user context, and browsing context.
*
* When `getActiveConfig` is called, it merges the configurations in a specific
* order of precedence: `global -> user context -> browsing context`. For each
* configuration property, the value from the highest-precedence level that has a
* non-`undefined` value is used.
*
* The `update` methods (`updateGlobalConfig`, `updateUserContextConfig`,
* `updateBrowsingContextConfig`) merge the provided configuration with the
* existing one at the corresponding level. Properties with `undefined` values in
* the provided configuration are ignored, preserving the existing value.
*/
class ContextConfigStorage {
#global = new ContextConfig_js_1.ContextConfig();
#userContextConfigs = new Map();
#browsingContextConfigs = new Map();
/**
* Updates the global configuration. Properties with `undefined` values in the
* provided `config` are ignored.
*/
updateGlobalConfig(config) {
this.#global = ContextConfig_js_1.ContextConfig.merge(this.#global, config);
}
/**
* Updates the configuration for a specific browsing context. Properties with
* `undefined` values in the provided `config` are ignored.
*/
updateBrowsingContextConfig(browsingContextId, config) {
this.#browsingContextConfigs.set(browsingContextId, ContextConfig_js_1.ContextConfig.merge(this.#browsingContextConfigs.get(browsingContextId), config));
}
/**
* Updates the configuration for a specific user context. Properties with
* `undefined` values in the provided `config` are ignored.
*/
updateUserContextConfig(userContext, config) {
this.#userContextConfigs.set(userContext, ContextConfig_js_1.ContextConfig.merge(this.#userContextConfigs.get(userContext), config));
}
/**
* Returns the current global configuration.
*/
getGlobalConfig() {
return this.#global;
}
/**
* Extra headers is a special case. The headers from the different levels have to be
* merged instead of being overridden.
*/
#getExtraHeaders(topLevelBrowsingContextId, userContext) {
const globalHeaders = this.#global.extraHeaders ?? {};
const userContextHeaders = this.#userContextConfigs.get(userContext)?.extraHeaders ?? {};
const browsingContextHeaders = topLevelBrowsingContextId === undefined
? {}
: (this.#browsingContextConfigs.get(topLevelBrowsingContextId)
?.extraHeaders ?? {});
return { ...globalHeaders, ...userContextHeaders, ...browsingContextHeaders };
}
/**
* Calculates the active configuration by merging global, user context, and
* browsing context settings.
*/
getActiveConfig(topLevelBrowsingContextId, userContext) {
let result = ContextConfig_js_1.ContextConfig.merge(this.#global, this.#userContextConfigs.get(userContext));
if (topLevelBrowsingContextId !== undefined) {
result = ContextConfig_js_1.ContextConfig.merge(result, this.#browsingContextConfigs.get(topLevelBrowsingContextId));
}
// Extra headers is a special case which have to be treated in a special way.
const extraHeaders = this.#getExtraHeaders(topLevelBrowsingContextId, userContext);
result.extraHeaders =
Object.keys(extraHeaders).length > 0 ? extraHeaders : undefined;
return result;
}
}
exports.ContextConfigStorage = ContextConfigStorage;
//# sourceMappingURL=ContextConfigStorage.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ContextConfigStorage.js","sourceRoot":"","sources":["../../../../../src/bidiMapper/modules/browser/ContextConfigStorage.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;;AAEH,yDAAiD;AAEjD;;;;;;;;;;;;;GAaG;AACH,MAAa,oBAAoB;IAC/B,OAAO,GAAG,IAAI,gCAAa,EAAE,CAAC;IAC9B,mBAAmB,GAAG,IAAI,GAAG,EAAyB,CAAC;IACvD,uBAAuB,GAAG,IAAI,GAAG,EAAyB,CAAC;IAE3D;;;OAGG;IACH,kBAAkB,CAAC,MAAqB;QACtC,IAAI,CAAC,OAAO,GAAG,gCAAa,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC3D,CAAC;IAED;;;OAGG;IACH,2BAA2B,CACzB,iBAAyB,EACzB,MAAqB;QAErB,IAAI,CAAC,uBAAuB,CAAC,GAAG,CAC9B,iBAAiB,EACjB,gCAAa,CAAC,KAAK,CACjB,IAAI,CAAC,uBAAuB,CAAC,GAAG,CAAC,iBAAiB,CAAC,EACnD,MAAM,CACP,CACF,CAAC;IACJ,CAAC;IAED;;;OAGG;IACH,uBAAuB,CAAC,WAAmB,EAAE,MAAqB;QAChE,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAC1B,WAAW,EACX,gCAAa,CAAC,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC,CACvE,CAAC;IACJ,CAAC;IAED;;OAEG;IACH,eAAe;QACb,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED;;;OAGG;IACH,gBAAgB,CACd,yBAA6C,EAC7C,WAAmB;QAEnB,MAAM,aAAa,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC;QACtD,MAAM,kBAAkB,GACtB,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,YAAY,IAAI,EAAE,CAAC;QAChE,MAAM,sBAAsB,GAC1B,yBAAyB,KAAK,SAAS;YACrC,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,CAAC,IAAI,CAAC,uBAAuB,CAAC,GAAG,CAAC,yBAAyB,CAAC;gBAC1D,EAAE,YAAY,IAAI,EAAE,CAAC,CAAC;QAE9B,OAAO,EAAC,GAAG,aAAa,EAAE,GAAG,kBAAkB,EAAE,GAAG,sBAAsB,EAAC,CAAC;IAC9E,CAAC;IAED;;;OAGG;IACH,eAAe,CACb,yBAA6C,EAC7C,WAAmB;QAEnB,IAAI,MAAM,GAAG,gCAAa,CAAC,KAAK,CAC9B,IAAI,CAAC,OAAO,EACZ,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,WAAW,CAAC,CAC1C,CAAC;QACF,IAAI,yBAAyB,KAAK,SAAS,EAAE,CAAC;YAC5C,MAAM,GAAG,gCAAa,CAAC,KAAK,CAC1B,MAAM,EACN,IAAI,CAAC,uBAAuB,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAC5D,CAAC;QACJ,CAAC;QAED,6EAA6E;QAC7E,MAAM,YAAY,GAAG,IAAI,CAAC,gBAAgB,CACxC,yBAAyB,EACzB,WAAW,CACZ,CAAC;QACF,MAAM,CAAC,YAAY;YACjB,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC;QAElE,OAAO,MAAM,CAAC;IAChB,CAAC;CACF;AAjGD,oDAiGC"}

View File

@@ -0,0 +1,27 @@
/**
* 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.
*/
import type { CdpClient } from '../../../cdp/CdpClient.js';
import { type Browser } from '../../../protocol/protocol.js';
export declare class UserContextStorage {
#private;
constructor(browserClient: CdpClient);
getUserContexts(): Promise<[
Browser.UserContextInfo,
...Browser.UserContextInfo[]
]>;
verifyUserContextIdList(userContextIds: Browser.UserContext[]): Promise<Set<string>>;
}

View File

@@ -0,0 +1,56 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.UserContextStorage = void 0;
const protocol_js_1 = require("../../../protocol/protocol.js");
class UserContextStorage {
#browserClient;
constructor(browserClient) {
this.#browserClient = browserClient;
}
async getUserContexts() {
const result = await this.#browserClient.sendCommand('Target.getBrowserContexts');
return [
{
userContext: 'default',
},
...result.browserContextIds.map((id) => {
return {
userContext: id,
};
}),
];
}
async verifyUserContextIdList(userContextIds) {
const foundContexts = new Set();
if (!userContextIds.length) {
return foundContexts;
}
const userContexts = await this.getUserContexts();
const knownUserContextIds = new Set(userContexts.map((userContext) => userContext.userContext));
for (const userContextId of userContextIds) {
if (!knownUserContextIds.has(userContextId)) {
throw new protocol_js_1.NoSuchUserContextException(`User context ${userContextId} not found`);
}
foundContexts.add(userContextId);
}
return foundContexts;
}
}
exports.UserContextStorage = UserContextStorage;
//# sourceMappingURL=UserContextStorage.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"UserContextStorage.js","sourceRoot":"","sources":["../../../../../src/bidiMapper/modules/browser/UserContextStorage.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;;AAGH,+DAGuC;AAEvC,MAAa,kBAAkB;IAC7B,cAAc,CAAY;IAC1B,YAAY,aAAwB;QAClC,IAAI,CAAC,cAAc,GAAG,aAAa,CAAC;IACtC,CAAC;IAED,KAAK,CAAC,eAAe;QAGnB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,WAAW,CAClD,2BAA2B,CAC5B,CAAC;QACF,OAAO;YACL;gBACE,WAAW,EAAE,SAAS;aACvB;YACD,GAAG,MAAM,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE;gBACrC,OAAO;oBACL,WAAW,EAAE,EAAE;iBAChB,CAAC;YACJ,CAAC,CAAC;SACH,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,uBAAuB,CAAC,cAAqC;QACjE,MAAM,aAAa,GAAG,IAAI,GAAG,EAAuB,CAAC;QACrD,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,CAAC;YAC3B,OAAO,aAAa,CAAC;QACvB,CAAC;QAED,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,eAAe,EAAE,CAAC;QAClD,MAAM,mBAAmB,GAAG,IAAI,GAAG,CACjC,YAAY,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,WAAW,CAAC,WAAW,CAAC,CAC3D,CAAC;QACF,KAAK,MAAM,aAAa,IAAI,cAAc,EAAE,CAAC;YAC3C,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,CAAC;gBAC5C,MAAM,IAAI,wCAA0B,CAClC,gBAAgB,aAAa,YAAY,CAC1C,CAAC;YACJ,CAAC;YACD,aAAa,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QACnC,CAAC;QAED,OAAO,aAAa,CAAC;IACvB,CAAC;CACF;AA7CD,gDA6CC"}

View File

@@ -0,0 +1,27 @@
/**
* 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.
*/
import { type Cdp } from '../../../protocol/protocol.js';
import type { CdpClient, CdpConnection } from '../../BidiMapper.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
import type { RealmStorage } from '../script/RealmStorage.js';
export declare class CdpProcessor {
#private;
constructor(browsingContextStorage: BrowsingContextStorage, realmStorage: RealmStorage, cdpConnection: CdpConnection, browserCdpClient: CdpClient);
getSession(params: Cdp.GetSessionParameters): Cdp.GetSessionResult;
resolveRealm(params: Cdp.ResolveRealmParameters): Cdp.ResolveRealmResult;
sendCommand(params: Cdp.SendCommandParameters): Promise<Cdp.SendCommandResult>;
}

View File

@@ -0,0 +1,60 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.CdpProcessor = void 0;
const protocol_js_1 = require("../../../protocol/protocol.js");
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 protocol_js_1.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,
};
}
}
exports.CdpProcessor = CdpProcessor;
//# sourceMappingURL=CdpProcessor.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"CdpProcessor.js","sourceRoot":"","sources":["../../../../../src/bidiMapper/modules/cdp/CdpProcessor.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;;AAEH,+DAA8E;AAK9E,MAAa,YAAY;IACd,uBAAuB,CAAyB;IAChD,aAAa,CAAe;IAC5B,cAAc,CAAgB;IAC9B,iBAAiB,CAAY;IAEtC,YACE,sBAA8C,EAC9C,YAA0B,EAC1B,aAA4B,EAC5B,gBAA2B;QAE3B,IAAI,CAAC,uBAAuB,GAAG,sBAAsB,CAAC;QACtD,IAAI,CAAC,aAAa,GAAG,YAAY,CAAC;QAClC,IAAI,CAAC,cAAc,GAAG,aAAa,CAAC;QACpC,IAAI,CAAC,iBAAiB,GAAG,gBAAgB,CAAC;IAC5C,CAAC;IAED,UAAU,CAAC,MAAgC;QACzC,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;QAC/B,MAAM,SAAS,GACb,IAAI,CAAC,uBAAuB,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,YAAY,CAAC;QAC1E,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC5B,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,OAAO,EAAC,OAAO,EAAE,SAAS,EAAC,CAAC;IAC9B,CAAC;IAED,YAAY,CAAC,MAAkC;QAC7C,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC;QAC7B,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,EAAC,OAAO,EAAE,OAAO,EAAC,CAAC,CAAC;QAC9D,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,MAAM,IAAI,mCAAqB,CAAC,wBAAwB,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;QAC1E,CAAC;QACD,OAAO,EAAC,kBAAkB,EAAE,KAAK,CAAC,kBAAkB,EAAC,CAAC;IACxD,CAAC;IAED,KAAK,CAAC,WAAW,CACf,MAAiC;QAEjC,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO;YAC3B,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,YAAY,CAAC,MAAM,CAAC,OAAO,CAAC;YAClD,CAAC,CAAC,IAAI,CAAC,iBAAiB,CAAC;QAC3B,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QACtE,OAAO;YACL,MAAM;YACN,OAAO,EAAE,MAAM,CAAC,OAAO;SACxB,CAAC;IACJ,CAAC;CACF;AAjDD,oCAiDC"}

View File

@@ -0,0 +1,57 @@
import type { Protocol } from 'devtools-protocol';
import type { CdpClient } from '../../../cdp/CdpClient.js';
import { type Browser, type BrowsingContext, type ChromiumBidi, Emulation, type UAClientHints } from '../../../protocol/protocol.js';
import { Deferred } from '../../../utils/Deferred.js';
import type { LoggerFn } from '../../../utils/log.js';
import type { Result } from '../../../utils/result.js';
import type { ContextConfigStorage } from '../browser/ContextConfigStorage.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
import { type NetworkStorage } from '../network/NetworkStorage.js';
import type { ChannelProxy } from '../script/ChannelProxy.js';
import type { PreloadScriptStorage } from '../script/PreloadScriptStorage.js';
import type { RealmStorage } from '../script/RealmStorage.js';
import type { EventManager } from '../session/EventManager.js';
export declare class CdpTarget {
#private;
readonly userContext: Browser.UserContext;
readonly contextConfigStorage: ContextConfigStorage;
static create(targetId: Protocol.Target.TargetID, cdpClient: CdpClient, browserCdpClient: CdpClient, parentCdpClient: CdpClient, realmStorage: RealmStorage, eventManager: EventManager, preloadScriptStorage: PreloadScriptStorage, browsingContextStorage: BrowsingContextStorage, networkStorage: NetworkStorage, configStorage: ContextConfigStorage, userContext: Browser.UserContext, defaultUserAgent: string, logger?: LoggerFn): CdpTarget;
constructor(targetId: Protocol.Target.TargetID, cdpClient: CdpClient, browserCdpClient: CdpClient, parentCdpClient: CdpClient, eventManager: EventManager, realmStorage: RealmStorage, preloadScriptStorage: PreloadScriptStorage, browsingContextStorage: BrowsingContextStorage, configStorage: ContextConfigStorage, networkStorage: NetworkStorage, userContext: Browser.UserContext, defaultUserAgent: string, logger: LoggerFn | undefined);
/** Returns a deferred that resolves when the target is unblocked. */
get unblocked(): Deferred<Result<void>>;
get id(): Protocol.Target.TargetID;
get cdpClient(): CdpClient;
get parentCdpClient(): CdpClient;
get browserCdpClient(): CdpClient;
/** Needed for CDP escape path. */
get cdpSessionId(): Protocol.Target.SessionID;
/**
* Window id the target belongs to. If not known, returns 0.
*/
get windowId(): number;
toggleFetchIfNeeded(): Promise<void>;
/**
* Toggles CDP "Fetch" domain and enable/disable network cache.
*/
toggleNetworkIfNeeded(): Promise<void>;
toggleSetCacheDisabled(disable?: boolean): Promise<void>;
toggleDeviceAccessIfNeeded(): Promise<void>;
togglePreloadIfNeeded(): Promise<void>;
toggleNetwork(): Promise<void>;
/**
* All the ProxyChannels from all the preload scripts of the given
* BrowsingContext.
*/
getChannels(): ChannelProxy[];
setDeviceMetricsOverride(viewport: BrowsingContext.Viewport | null, devicePixelRatio: number | null, screenOrientation: Emulation.ScreenOrientation | null, screenArea: Emulation.ScreenArea | null): Promise<void>;
get topLevelId(): string;
isSubscribedTo(moduleOrEvent: ChromiumBidi.EventNames): boolean;
setGeolocationOverride(geolocation: Emulation.GeolocationCoordinates | Emulation.GeolocationPositionError | null): Promise<void>;
setTouchOverride(maxTouchPoints: number | null): Promise<void>;
setLocaleOverride(locale: string | null): Promise<void>;
setScriptingEnabled(scriptingEnabled: false | null): Promise<void>;
setTimezoneOverride(timezone: string | null): Promise<void>;
setExtraHeaders(headers: Protocol.Network.Headers): Promise<void>;
setUserAgentAndAcceptLanguage(userAgent: string | null | undefined, acceptLanguage: string | null | undefined, clientHints?: UAClientHints.Emulation.ClientHintsMetadata | null): Promise<void>;
setEmulatedNetworkConditions(networkConditions: Emulation.NetworkConditions | null): Promise<void>;
}

View File

@@ -0,0 +1,695 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CdpTarget = void 0;
const chromium_bidi_js_1 = require("../../../protocol/chromium-bidi.js");
const protocol_js_1 = require("../../../protocol/protocol.js");
const Deferred_js_1 = require("../../../utils/Deferred.js");
const log_js_1 = require("../../../utils/log.js");
const BrowsingContextImpl_js_1 = require("../context/BrowsingContextImpl.js");
const LogManager_js_1 = require("../log/LogManager.js");
const NetworkStorage_js_1 = require("../network/NetworkStorage.js");
class CdpTarget {
#id;
userContext;
#cdpClient;
#browserCdpClient;
#parentCdpClient;
#realmStorage;
#eventManager;
#preloadScriptStorage;
#browsingContextStorage;
#networkStorage;
contextConfigStorage;
#unblocked = new Deferred_js_1.Deferred();
// Default user agent for the target. Required, as emulating client hints without user
// agent is not possible. Cache it to avoid round trips to the browser for every target override.
#defaultUserAgent;
#logger;
/**
* Target's window id. Is filled when the CDP target is created and do not reflect
* moving targets from one window to another. The actual values
* will be set during `#unblock`.
* */
#windowId;
#deviceAccessEnabled = false;
#cacheDisableState = false;
#preloadEnabled = false;
#fetchDomainStages = {
request: false,
response: false,
auth: false,
};
static create(targetId, cdpClient, browserCdpClient, parentCdpClient, realmStorage, eventManager, preloadScriptStorage, browsingContextStorage, networkStorage, configStorage, userContext, defaultUserAgent, logger) {
const cdpTarget = new CdpTarget(targetId, cdpClient, browserCdpClient, parentCdpClient, eventManager, realmStorage, preloadScriptStorage, browsingContextStorage, configStorage, networkStorage, userContext, defaultUserAgent, logger);
LogManager_js_1.LogManager.create(cdpTarget, realmStorage, eventManager, logger);
cdpTarget.#setEventListeners();
// No need to await.
// Deferred will be resolved when the target is unblocked.
void cdpTarget.#unblock();
return cdpTarget;
}
constructor(targetId, cdpClient, browserCdpClient, parentCdpClient, eventManager, realmStorage, preloadScriptStorage, browsingContextStorage, configStorage, networkStorage, userContext, defaultUserAgent, logger) {
this.#defaultUserAgent = defaultUserAgent;
this.userContext = userContext;
this.#id = targetId;
this.#cdpClient = cdpClient;
this.#browserCdpClient = browserCdpClient;
this.#parentCdpClient = parentCdpClient;
this.#eventManager = eventManager;
this.#realmStorage = realmStorage;
this.#preloadScriptStorage = preloadScriptStorage;
this.#networkStorage = networkStorage;
this.#browsingContextStorage = browsingContextStorage;
this.contextConfigStorage = configStorage;
this.#logger = logger;
}
/** Returns a deferred that resolves when the target is unblocked. */
get unblocked() {
return this.#unblocked;
}
get id() {
return this.#id;
}
get cdpClient() {
return this.#cdpClient;
}
get parentCdpClient() {
return this.#parentCdpClient;
}
get browserCdpClient() {
return this.#browserCdpClient;
}
/** Needed for CDP escape path. */
get cdpSessionId() {
// SAFETY we got the client by it's id for creating
return this.#cdpClient.sessionId;
}
/**
* Window id the target belongs to. If not known, returns 0.
*/
get windowId() {
if (this.#windowId === undefined) {
this.#logger?.(log_js_1.LogType.debugError, 'Getting windowId before it was set, returning 0');
}
return this.#windowId ?? 0;
}
/**
* Enables all the required CDP domains and unblocks the target.
*/
async #unblock() {
const config = this.contextConfigStorage.getActiveConfig(this.topLevelId, this.userContext);
const results = await Promise.allSettled([
this.#cdpClient.sendCommand('Page.enable', {
enableFileChooserOpenedEvent: true,
}),
...(this.#ignoreFileDialog()
? []
: [
this.#cdpClient.sendCommand('Page.setInterceptFileChooserDialog', {
enabled: true,
// The intercepted dialog should be canceled.
cancel: true,
}),
]),
// There can be some existing frames in the target, if reconnecting to an
// existing browser instance, e.g. via Puppeteer. Need to restore the browsing
// contexts for the frames to correctly handle further events, like
// `Runtime.executionContextCreated`.
// It's important to schedule this task together with enabling domains commands to
// prepare the tree before the events (e.g. Runtime.executionContextCreated) start
// coming.
// https://github.com/GoogleChromeLabs/chromium-bidi/issues/2282
this.#cdpClient
.sendCommand('Page.getFrameTree')
.then((frameTree) => this.#restoreFrameTreeState(frameTree.frameTree)),
this.#cdpClient.sendCommand('Runtime.enable'),
this.#cdpClient.sendCommand('Page.setLifecycleEventsEnabled', {
enabled: true,
}),
// Enabling CDP Network domain is required for navigation detection:
// https://github.com/GoogleChromeLabs/chromium-bidi/issues/2856.
this.#cdpClient
.sendCommand('Network.enable', {
// If `googDisableNetworkDurableMessages` flag is set, do not enable durable
// messages.
enableDurableMessages: config.disableNetworkDurableMessages !== true,
maxTotalBufferSize: NetworkStorage_js_1.MAX_TOTAL_COLLECTED_SIZE,
})
.then(() => this.toggleNetworkIfNeeded()),
this.#cdpClient.sendCommand('Target.setAutoAttach', {
autoAttach: true,
waitForDebuggerOnStart: true,
flatten: true,
}),
this.#updateWindowId(),
this.#setUserContextConfig(config),
this.#initAndEvaluatePreloadScripts(),
this.#cdpClient.sendCommand('Runtime.runIfWaitingForDebugger'),
// Resume tab execution as well if it was paused by the debugger.
this.#parentCdpClient.sendCommand('Runtime.runIfWaitingForDebugger'),
this.toggleDeviceAccessIfNeeded(),
this.togglePreloadIfNeeded(),
]);
for (const result of results) {
if (result instanceof Error) {
// Ignore errors during configuring targets, just log them.
this.#logger?.(log_js_1.LogType.debugError, 'Error happened when configuring a new target', result);
}
}
this.#unblocked.resolve({
kind: 'success',
value: undefined,
});
}
#restoreFrameTreeState(frameTree) {
const frame = frameTree.frame;
const maybeContext = this.#browsingContextStorage.findContext(frame.id);
if (maybeContext !== undefined) {
// Restoring parent of already known browsing context. This means the target is
// OOPiF and the BiDi session was connected to already existing browser instance.
if (maybeContext.parentId === null &&
frame.parentId !== null &&
frame.parentId !== undefined) {
maybeContext.parentId = frame.parentId;
}
}
if (maybeContext === undefined && frame.parentId !== undefined) {
// Restore not yet known nested frames. The top-level frame is created when the
// target is attached.
const parentBrowsingContext = this.#browsingContextStorage.getContext(frame.parentId);
BrowsingContextImpl_js_1.BrowsingContextImpl.create(frame.id, frame.parentId, this.userContext, parentBrowsingContext.cdpTarget, this.#eventManager, this.#browsingContextStorage, this.#realmStorage, this.contextConfigStorage, frame.url, undefined, this.#logger);
}
frameTree.childFrames?.map((frameTree) => this.#restoreFrameTreeState(frameTree));
}
async toggleFetchIfNeeded() {
const stages = this.#networkStorage.getInterceptionStages(this.topLevelId);
if (this.#fetchDomainStages.request === stages.request &&
this.#fetchDomainStages.response === stages.response &&
this.#fetchDomainStages.auth === stages.auth) {
return;
}
const patterns = [];
this.#fetchDomainStages = stages;
if (stages.request || stages.auth) {
// CDP quirk we need request interception when we intercept auth
patterns.push({
urlPattern: '*',
requestStage: 'Request',
});
}
if (stages.response) {
patterns.push({
urlPattern: '*',
requestStage: 'Response',
});
}
if (patterns.length) {
await this.#cdpClient.sendCommand('Fetch.enable', {
patterns,
handleAuthRequests: stages.auth,
});
}
else {
const blockedRequest = this.#networkStorage
.getRequestsByTarget(this)
.filter((request) => request.interceptPhase);
void Promise.allSettled(blockedRequest.map((request) => request.waitNextPhase))
.then(async () => {
const blockedRequest = this.#networkStorage
.getRequestsByTarget(this)
.filter((request) => request.interceptPhase);
if (blockedRequest.length) {
return await this.toggleFetchIfNeeded();
}
return await this.#cdpClient.sendCommand('Fetch.disable');
})
.catch((error) => {
this.#logger?.(log_js_1.LogType.bidi, 'Disable failed', error);
});
}
}
/**
* Toggles CDP "Fetch" domain and enable/disable network cache.
*/
async toggleNetworkIfNeeded() {
// Although the Network domain remains active, Fetch domain activation and caching
// settings should be managed dynamically.
try {
await Promise.all([
this.toggleSetCacheDisabled(),
this.toggleFetchIfNeeded(),
]);
}
catch (err) {
this.#logger?.(log_js_1.LogType.debugError, err);
if (!this.#isExpectedError(err)) {
throw err;
}
}
}
async toggleSetCacheDisabled(disable) {
const defaultCacheDisabled = this.#networkStorage.defaultCacheBehavior === 'bypass';
const cacheDisabled = disable ?? defaultCacheDisabled;
if (this.#cacheDisableState === cacheDisabled) {
return;
}
this.#cacheDisableState = cacheDisabled;
try {
await this.#cdpClient.sendCommand('Network.setCacheDisabled', {
cacheDisabled,
});
}
catch (err) {
this.#logger?.(log_js_1.LogType.debugError, err);
this.#cacheDisableState = !cacheDisabled;
if (!this.#isExpectedError(err)) {
throw err;
}
}
}
async toggleDeviceAccessIfNeeded() {
const enabled = this.isSubscribedTo(chromium_bidi_js_1.Bluetooth.EventNames.RequestDevicePromptUpdated);
if (this.#deviceAccessEnabled === enabled) {
return;
}
this.#deviceAccessEnabled = enabled;
try {
await this.#cdpClient.sendCommand(enabled ? 'DeviceAccess.enable' : 'DeviceAccess.disable');
}
catch (err) {
this.#logger?.(log_js_1.LogType.debugError, err);
this.#deviceAccessEnabled = !enabled;
if (!this.#isExpectedError(err)) {
throw err;
}
}
}
async togglePreloadIfNeeded() {
const enabled = this.isSubscribedTo(chromium_bidi_js_1.Speculation.EventNames.PrefetchStatusUpdated);
if (this.#preloadEnabled === enabled) {
return;
}
this.#preloadEnabled = enabled;
try {
await this.#cdpClient.sendCommand(enabled ? 'Preload.enable' : 'Preload.disable');
}
catch (err) {
this.#logger?.(log_js_1.LogType.debugError, err);
this.#preloadEnabled = !enabled;
if (!this.#isExpectedError(err)) {
throw err;
}
}
}
/**
* Heuristic checking if the error is due to the session being closed. If so, ignore the
* error.
*/
#isExpectedError(err) {
const error = err;
return ((error.code === -32001 &&
error.message === 'Session with given id not found.') ||
this.#cdpClient.isCloseError(err));
}
#setEventListeners() {
this.#cdpClient.on('*', (event, params) => {
// We may encounter uses for EventEmitter other than CDP events,
// which we want to skip.
if (typeof event !== 'string') {
return;
}
this.#eventManager.registerEvent({
type: 'event',
method: `goog:cdp.${event}`,
params: {
event,
params,
session: this.cdpSessionId,
},
}, this.id);
});
}
async #enableFetch(stages) {
const patterns = [];
if (stages.request || stages.auth) {
// CDP quirk we need request interception when we intercept auth
patterns.push({
urlPattern: '*',
requestStage: 'Request',
});
}
if (stages.response) {
patterns.push({
urlPattern: '*',
requestStage: 'Response',
});
}
if (patterns.length) {
const oldStages = this.#fetchDomainStages;
this.#fetchDomainStages = stages;
try {
await this.#cdpClient.sendCommand('Fetch.enable', {
patterns,
handleAuthRequests: stages.auth,
});
}
catch {
this.#fetchDomainStages = oldStages;
}
}
}
async #disableFetch() {
const blockedRequest = this.#networkStorage
.getRequestsByTarget(this)
.filter((request) => request.interceptPhase);
if (blockedRequest.length === 0) {
this.#fetchDomainStages = {
request: false,
response: false,
auth: false,
};
await this.#cdpClient.sendCommand('Fetch.disable');
}
}
async toggleNetwork() {
// TODO: respect the data collectors once CDP Network domain is enabled on-demand:
// const networkEnable = this.#networkStorage.getCollectorsForBrowsingContext(this.topLevelId).length > 0;
const stages = this.#networkStorage.getInterceptionStages(this.topLevelId);
const fetchEnable = Object.values(stages).some((value) => value);
const fetchChanged = this.#fetchDomainStages.request !== stages.request ||
this.#fetchDomainStages.response !== stages.response ||
this.#fetchDomainStages.auth !== stages.auth;
this.#logger?.(log_js_1.LogType.debugInfo, 'Toggle Network', `Fetch (${fetchEnable}) ${fetchChanged}`);
if (fetchEnable && fetchChanged) {
await this.#enableFetch(stages);
}
if (!fetchEnable && fetchChanged) {
await this.#disableFetch();
}
}
/**
* All the ProxyChannels from all the preload scripts of the given
* BrowsingContext.
*/
getChannels() {
return this.#preloadScriptStorage
.find()
.flatMap((script) => script.channels);
}
async #updateWindowId() {
const { windowId } = await this.#browserCdpClient.sendCommand('Browser.getWindowForTarget', { targetId: this.id });
this.#windowId = windowId;
}
/** Loads all top-level preload scripts. */
async #initAndEvaluatePreloadScripts() {
await Promise.all(this.#preloadScriptStorage
.find({
// Needed for OOPIF
targetId: this.topLevelId,
})
.map((script) => {
return script.initInTarget(this, true);
}));
}
async setDeviceMetricsOverride(viewport, devicePixelRatio, screenOrientation, screenArea) {
if (viewport === null &&
devicePixelRatio === null &&
screenOrientation === null &&
screenArea === null) {
await this.cdpClient.sendCommand('Emulation.clearDeviceMetricsOverride');
return;
}
const metricsOverride = {
width: viewport?.width ?? 0,
height: viewport?.height ?? 0,
deviceScaleFactor: devicePixelRatio ?? 0,
screenOrientation: this.#toCdpScreenOrientationAngle(screenOrientation) ?? undefined,
mobile: false,
screenWidth: screenArea?.width,
screenHeight: screenArea?.height,
};
await this.cdpClient.sendCommand('Emulation.setDeviceMetricsOverride', metricsOverride);
}
/**
* Immediately schedules all the required commands to configure user context
* configuration and waits for them to finish. It's important to schedule them
* in parallel, so that they are enqueued before any page's scripts.
*/
async #setUserContextConfig(config) {
const promises = [];
promises.push(this.#cdpClient
.sendCommand('Page.setPrerenderingAllowed', {
isAllowed: !config.prerenderingDisabled,
})
.catch(() => {
// Ignore CDP errors, as the command is not supported by iframe targets or
// prerendered pages. Generic catch, as the error can vary between CdpClient
// implementations: Tab vs Puppeteer.
}));
if (config.viewport !== undefined ||
config.devicePixelRatio !== undefined ||
config.screenOrientation !== undefined ||
config.screenArea !== undefined) {
promises.push(this.setDeviceMetricsOverride(config.viewport ?? null, config.devicePixelRatio ?? null, config.screenOrientation ?? null, config.screenArea ?? null).catch(() => {
// Ignore CDP errors, as the command is not supported by iframe targets. Generic
// catch, as the error can vary between CdpClient implementations: Tab vs
// Puppeteer.
}));
}
if (config.geolocation !== undefined && config.geolocation !== null) {
promises.push(this.setGeolocationOverride(config.geolocation));
}
if (config.locale !== undefined) {
promises.push(this.setLocaleOverride(config.locale));
}
if (config.timezone !== undefined) {
promises.push(this.setTimezoneOverride(config.timezone));
}
if (config.extraHeaders !== undefined) {
promises.push(this.setExtraHeaders(config.extraHeaders));
}
if (config.userAgent !== undefined ||
config.locale !== undefined ||
config.clientHints !== undefined) {
promises.push(this.setUserAgentAndAcceptLanguage(config.userAgent, config.locale, config.clientHints));
}
if (config.scriptingEnabled !== undefined) {
promises.push(this.setScriptingEnabled(config.scriptingEnabled));
}
if (config.acceptInsecureCerts !== undefined) {
promises.push(this.cdpClient.sendCommand('Security.setIgnoreCertificateErrors', {
ignore: config.acceptInsecureCerts,
}));
}
if (config.emulatedNetworkConditions !== undefined) {
promises.push(this.setEmulatedNetworkConditions(config.emulatedNetworkConditions));
}
if (config.maxTouchPoints !== undefined) {
promises.push(this.setTouchOverride(config.maxTouchPoints));
}
await Promise.all(promises);
}
get topLevelId() {
return (this.#browsingContextStorage.findTopLevelContextId(this.id) ?? this.id);
}
isSubscribedTo(moduleOrEvent) {
return this.#eventManager.subscriptionManager.isSubscribedTo(moduleOrEvent, this.topLevelId);
}
#ignoreFileDialog() {
const config = this.contextConfigStorage.getActiveConfig(this.topLevelId, this.userContext);
return ((config.userPromptHandler?.file ??
config.userPromptHandler?.default ??
"ignore" /* Session.UserPromptHandlerType.Ignore */) ===
"ignore" /* Session.UserPromptHandlerType.Ignore */);
}
async setGeolocationOverride(geolocation) {
if (geolocation === null) {
await this.cdpClient.sendCommand('Emulation.clearGeolocationOverride');
}
else if ('type' in geolocation) {
if (geolocation.type !== 'positionUnavailable') {
// Unreachable. Handled by params parser.
throw new protocol_js_1.UnknownErrorException(`Unknown geolocation error ${geolocation.type}`);
}
// Omitting latitude, longitude or accuracy emulates position unavailable.
await this.cdpClient.sendCommand('Emulation.setGeolocationOverride', {});
}
else if ('latitude' in geolocation) {
await this.cdpClient.sendCommand('Emulation.setGeolocationOverride', {
latitude: geolocation.latitude,
longitude: geolocation.longitude,
accuracy: geolocation.accuracy ?? 1,
// `null` value is treated as "missing".
altitude: geolocation.altitude ?? undefined,
altitudeAccuracy: geolocation.altitudeAccuracy ?? undefined,
heading: geolocation.heading ?? undefined,
speed: geolocation.speed ?? undefined,
});
}
else {
// Unreachable. Handled by params parser.
throw new protocol_js_1.UnknownErrorException('Unexpected geolocation coordinates value');
}
}
async setTouchOverride(maxTouchPoints) {
const touchEmulationParams = {
enabled: maxTouchPoints !== null,
};
if (maxTouchPoints !== null) {
touchEmulationParams.maxTouchPoints = maxTouchPoints;
}
await this.cdpClient.sendCommand('Emulation.setTouchEmulationEnabled', touchEmulationParams);
}
#toCdpScreenOrientationAngle(orientation) {
if (orientation === null) {
return null;
}
// https://w3c.github.io/screen-orientation/#the-current-screen-orientation-type-and-angle
if (orientation.natural === "portrait" /* Emulation.ScreenOrientationNatural.Portrait */) {
switch (orientation.type) {
case 'portrait-primary':
return {
angle: 0,
type: 'portraitPrimary',
};
case 'landscape-primary':
return {
angle: 90,
type: 'landscapePrimary',
};
case 'portrait-secondary':
return {
angle: 180,
type: 'portraitSecondary',
};
case 'landscape-secondary':
return {
angle: 270,
type: 'landscapeSecondary',
};
default:
// Unreachable.
throw new protocol_js_1.UnknownErrorException(`Unexpected screen orientation type ${orientation.type}`);
}
}
if (orientation.natural === "landscape" /* Emulation.ScreenOrientationNatural.Landscape */) {
switch (orientation.type) {
case 'landscape-primary':
return {
angle: 0,
type: 'landscapePrimary',
};
case 'portrait-primary':
return {
angle: 90,
type: 'portraitPrimary',
};
case 'landscape-secondary':
return {
angle: 180,
type: 'landscapeSecondary',
};
case 'portrait-secondary':
return {
angle: 270,
type: 'portraitSecondary',
};
default:
// Unreachable.
throw new protocol_js_1.UnknownErrorException(`Unexpected screen orientation type ${orientation.type}`);
}
}
// Unreachable.
throw new protocol_js_1.UnknownErrorException(`Unexpected orientation natural ${orientation.natural}`);
}
async setLocaleOverride(locale) {
if (locale === null) {
await this.cdpClient.sendCommand('Emulation.setLocaleOverride', {});
}
else {
await this.cdpClient.sendCommand('Emulation.setLocaleOverride', {
locale,
});
}
}
async setScriptingEnabled(scriptingEnabled) {
await this.cdpClient.sendCommand('Emulation.setScriptExecutionDisabled', {
value: scriptingEnabled === false,
});
}
async setTimezoneOverride(timezone) {
if (timezone === null) {
await this.cdpClient.sendCommand('Emulation.setTimezoneOverride', {
// If empty, disables the override and restores default host system timezone.
timezoneId: '',
});
}
else {
await this.cdpClient.sendCommand('Emulation.setTimezoneOverride', {
timezoneId: timezone,
});
}
}
async setExtraHeaders(headers) {
await this.cdpClient.sendCommand('Network.setExtraHTTPHeaders', {
headers,
});
}
async setUserAgentAndAcceptLanguage(userAgent, acceptLanguage, clientHints) {
const userAgentMetadata = clientHints
? {
brands: clientHints.brands?.map((b) => ({
brand: b.brand,
version: b.version,
})),
fullVersionList: clientHints.fullVersionList,
platform: clientHints.platform ?? '',
platformVersion: clientHints.platformVersion ?? '',
architecture: clientHints.architecture ?? '',
model: clientHints.model ?? '',
mobile: clientHints.mobile ?? false,
bitness: clientHints.bitness ?? undefined,
wow64: clientHints.wow64 ?? undefined,
formFactors: clientHints.formFactors ?? undefined,
}
: undefined;
await this.cdpClient.sendCommand('Emulation.setUserAgentOverride', {
// `userAgent` is required if `userAgentMetadata` is provided.
userAgent: userAgent || (userAgentMetadata ? this.#defaultUserAgent : ''),
acceptLanguage: acceptLanguage ?? undefined,
// We need to provide the platform to enable platform emulation.
// Note that the value might be different from the one expected by the
// legacy `navigator.platform` (e.g. `Win32` vs `Windows`).
// https://github.com/w3c/webdriver-bidi/issues/1065
platform: clientHints?.platform ?? undefined,
userAgentMetadata,
});
}
async setEmulatedNetworkConditions(networkConditions) {
if (networkConditions !== null && networkConditions.type !== 'offline') {
throw new protocol_js_1.UnsupportedOperationException(`Unsupported network conditions ${networkConditions.type}`);
}
await Promise.all([
this.cdpClient.sendCommand('Network.emulateNetworkConditionsByRule', {
offline: networkConditions?.type === 'offline',
matchedNetworkConditions: [
{
urlPattern: '',
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
},
],
}),
this.cdpClient.sendCommand('Network.overrideNetworkState', {
offline: networkConditions?.type === 'offline',
// TODO: restore the original `latency` value when emulation is removed.
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
}),
]);
}
}
exports.CdpTarget = CdpTarget;
//# sourceMappingURL=CdpTarget.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,16 @@
import type { CdpClient } from '../../../cdp/CdpClient.js';
import type { CdpConnection } from '../../../cdp/CdpConnection.js';
import type { Browser } from '../../../protocol/protocol.js';
import { type LoggerFn } from '../../../utils/log.js';
import type { BluetoothProcessor } from '../bluetooth/BluetoothProcessor.js';
import type { ContextConfigStorage } from '../browser/ContextConfigStorage.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
import type { NetworkStorage } from '../network/NetworkStorage.js';
import type { PreloadScriptStorage } from '../script/PreloadScriptStorage.js';
import type { RealmStorage } from '../script/RealmStorage.js';
import type { EventManager } from '../session/EventManager.js';
import type { SpeculationProcessor } from '../speculation/SpeculationProcessor.js';
export declare class CdpTargetManager {
#private;
constructor(cdpConnection: CdpConnection, browserCdpClient: CdpClient, selfTargetId: string, eventManager: EventManager, browsingContextStorage: BrowsingContextStorage, realmStorage: RealmStorage, networkStorage: NetworkStorage, configStorage: ContextConfigStorage, bluetoothProcessor: BluetoothProcessor, speculationProcessor: SpeculationProcessor, preloadScriptStorage: PreloadScriptStorage, defaultUserContextId: Browser.UserContext, defaultUserAgent: string, logger?: LoggerFn);
}

View File

@@ -0,0 +1,252 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CdpTargetManager = void 0;
const log_js_1 = require("../../../utils/log.js");
const BrowsingContextImpl_js_1 = require("../context/BrowsingContextImpl.js");
const WorkerRealm_js_1 = require("../script/WorkerRealm.js");
const CdpTarget_js_1 = require("./CdpTarget.js");
const cdpToBidiTargetTypes = {
service_worker: 'service-worker',
shared_worker: 'shared-worker',
worker: 'dedicated-worker',
};
class CdpTargetManager {
#browserCdpClient;
#cdpConnection;
#targetKeysToBeIgnoredByAutoAttach = new Set();
#selfTargetId;
#eventManager;
#browsingContextStorage;
#networkStorage;
#bluetoothProcessor;
#preloadScriptStorage;
#realmStorage;
#configStorage;
#speculationProcessor;
#defaultUserContextId;
#defaultUserAgent;
#logger;
constructor(cdpConnection, browserCdpClient, selfTargetId, eventManager, browsingContextStorage, realmStorage, networkStorage, configStorage, bluetoothProcessor, speculationProcessor, preloadScriptStorage, defaultUserContextId, defaultUserAgent, logger) {
this.#cdpConnection = cdpConnection;
this.#browserCdpClient = browserCdpClient;
this.#targetKeysToBeIgnoredByAutoAttach.add(selfTargetId);
this.#selfTargetId = selfTargetId;
this.#eventManager = eventManager;
this.#browsingContextStorage = browsingContextStorage;
this.#preloadScriptStorage = preloadScriptStorage;
this.#networkStorage = networkStorage;
this.#configStorage = configStorage;
this.#bluetoothProcessor = bluetoothProcessor;
this.#speculationProcessor = speculationProcessor;
this.#realmStorage = realmStorage;
this.#defaultUserContextId = defaultUserContextId;
this.#defaultUserAgent = defaultUserAgent;
this.#logger = logger;
this.#setEventListeners(browserCdpClient);
}
/**
* This method is called for each CDP session, since this class is responsible
* for creating and destroying all targets and browsing contexts.
*/
#setEventListeners(cdpClient) {
cdpClient.on('Target.attachedToTarget', (params) => {
this.#handleAttachedToTargetEvent(params, cdpClient);
});
cdpClient.on('Target.detachedFromTarget', this.#handleDetachedFromTargetEvent.bind(this));
cdpClient.on('Target.targetInfoChanged', this.#handleTargetInfoChangedEvent.bind(this));
cdpClient.on('Inspector.targetCrashed', () => {
this.#handleTargetCrashedEvent(cdpClient);
});
cdpClient.on('Page.frameAttached', this.#handleFrameAttachedEvent.bind(this));
cdpClient.on('Page.frameSubtreeWillBeDetached', this.#handleFrameSubtreeWillBeDetached.bind(this));
}
#handleFrameAttachedEvent(params) {
const parentBrowsingContext = this.#browsingContextStorage.findContext(params.parentFrameId);
if (parentBrowsingContext !== undefined) {
BrowsingContextImpl_js_1.BrowsingContextImpl.create(params.frameId, params.parentFrameId, parentBrowsingContext.userContext, parentBrowsingContext.cdpTarget, this.#eventManager, this.#browsingContextStorage, this.#realmStorage, this.#configStorage,
// At this point, we don't know the URL of the frame yet, so it will be updated
// later.
'about:blank', undefined, this.#logger);
}
}
#handleFrameSubtreeWillBeDetached(params) {
this.#browsingContextStorage.findContext(params.frameId)?.dispose(true);
}
#handleAttachedToTargetEvent(params, parentSessionCdpClient) {
const { sessionId, targetInfo } = params;
const targetCdpClient = this.#cdpConnection.getCdpClient(sessionId);
const detach = async () => {
// Detaches and resumes the target suppressing errors.
await targetCdpClient
.sendCommand('Runtime.runIfWaitingForDebugger')
.then(() => parentSessionCdpClient.sendCommand('Target.detachFromTarget', params))
.catch((error) => this.#logger?.(log_js_1.LogType.debugError, error));
};
// Do not attach to the Mapper target.
if (this.#selfTargetId === targetInfo.targetId) {
void detach();
return;
}
// Service workers are special case because they attach to the
// browser target and the page target (so twice per worker) during
// the regular auto-attach and might hang if the CDP session on
// the browser level is not detached. The logic to detach the
// right session is handled in the switch below.
const targetKey = targetInfo.type === 'service_worker'
? `${parentSessionCdpClient.sessionId}_${targetInfo.targetId}`
: targetInfo.targetId;
// Mapper generally only needs one session per target. If we
// receive additional auto-attached sessions, that is very likely
// coming from custom CDP sessions.
if (this.#targetKeysToBeIgnoredByAutoAttach.has(targetKey)) {
// Return to leave the session untouched.
return;
}
this.#targetKeysToBeIgnoredByAutoAttach.add(targetKey);
const userContext = targetInfo.browserContextId &&
targetInfo.browserContextId !== this.#defaultUserContextId
? targetInfo.browserContextId
: 'default';
switch (targetInfo.type) {
case 'tab': {
// Tab targets are required only to handle page targets beneath them.
this.#setEventListeners(targetCdpClient);
// Auto-attach to the page target. No need in resuming tab target debugger, as it
// should preserve the page target debugger state, and will be resumed by the page
// target.
void (async () => {
await targetCdpClient.sendCommand('Target.setAutoAttach', {
autoAttach: true,
waitForDebuggerOnStart: true,
flatten: true,
});
})();
return;
}
case 'page':
case 'iframe': {
const cdpTarget = this.#createCdpTarget(targetCdpClient, parentSessionCdpClient, targetInfo, userContext);
const maybeContext = this.#browsingContextStorage.findContext(targetInfo.targetId);
if (maybeContext && targetInfo.type === 'iframe') {
// OOPiF.
maybeContext.updateCdpTarget(cdpTarget);
}
else {
// If attaching to existing browser instance, there could be OOPiF targets. This
// case is handled by the `findFrameParentId` method.
const parentId = this.#findFrameParentId(targetInfo, parentSessionCdpClient.sessionId);
// New context.
BrowsingContextImpl_js_1.BrowsingContextImpl.create(targetInfo.targetId, parentId, userContext, cdpTarget, this.#eventManager, this.#browsingContextStorage, this.#realmStorage, this.#configStorage,
// Hack: when a new target created, CDP emits targetInfoChanged with an empty
// url, and navigates it to about:blank later. When the event is emitted for
// an existing target (reconnect), the url is already known, and navigation
// events will not be emitted anymore. Replacing empty url with `about:blank`
// allows to handle both cases in the same way.
// "7.3.2.1 Creating browsing contexts".
// https://html.spec.whatwg.org/multipage/document-sequences.html#creating-browsing-contexts
// TODO: check who to deal with non-null creator and its `creatorOrigin`.
targetInfo.url === '' ? 'about:blank' : targetInfo.url, targetInfo.openerFrameId ?? targetInfo.openerId, this.#logger);
}
return;
}
case 'service_worker':
case 'worker': {
const realm = this.#realmStorage.findRealm({
cdpSessionId: parentSessionCdpClient.sessionId,
sandbox: null, // Non-sandboxed realms.
});
// If there is no browsing context, this worker is already terminated.
if (!realm) {
void detach();
return;
}
const cdpTarget = this.#createCdpTarget(targetCdpClient, parentSessionCdpClient, targetInfo, userContext);
this.#handleWorkerTarget(cdpToBidiTargetTypes[targetInfo.type], cdpTarget, realm);
return;
}
// In CDP, we only emit shared workers on the browser and not the set of
// frames that use the shared worker. If we change this in the future to
// behave like service workers (emits on both browser and frame targets),
// we can remove this block and merge service workers with the above one.
case 'shared_worker': {
const cdpTarget = this.#createCdpTarget(targetCdpClient, parentSessionCdpClient, targetInfo, userContext);
this.#handleWorkerTarget(cdpToBidiTargetTypes[targetInfo.type], cdpTarget);
return;
}
}
// DevTools or some other not supported by BiDi target. Just release
// debugger and ignore them.
void detach();
}
/** Try to find the parent browsing context ID for the given attached target. */
#findFrameParentId(targetInfo, parentSessionId) {
if (targetInfo.type !== 'iframe') {
return null;
}
const parentId = targetInfo.openerFrameId ?? targetInfo.openerId;
if (parentId !== undefined) {
return parentId;
}
if (parentSessionId !== undefined) {
return (this.#browsingContextStorage.findContextBySession(parentSessionId)
?.id ?? null);
}
return null;
}
#createCdpTarget(targetCdpClient, parentCdpClient, targetInfo, userContext) {
this.#setEventListeners(targetCdpClient);
this.#preloadScriptStorage.onCdpTargetCreated(targetInfo.targetId, userContext);
const target = CdpTarget_js_1.CdpTarget.create(targetInfo.targetId, targetCdpClient, this.#browserCdpClient, parentCdpClient, this.#realmStorage, this.#eventManager, this.#preloadScriptStorage, this.#browsingContextStorage, this.#networkStorage, this.#configStorage, userContext,
// Pass the cached default User Agent to the new target.
this.#defaultUserAgent, this.#logger);
this.#networkStorage.onCdpTargetCreated(target);
this.#bluetoothProcessor.onCdpTargetCreated(target);
this.#speculationProcessor.onCdpTargetCreated(target);
return target;
}
#workers = new Map();
#handleWorkerTarget(realmType, cdpTarget, ownerRealm) {
cdpTarget.cdpClient.on('Runtime.executionContextCreated', (params) => {
const { uniqueId, id, origin } = params.context;
const workerRealm = new WorkerRealm_js_1.WorkerRealm(cdpTarget.cdpClient, this.#eventManager, id, this.#logger, (0, BrowsingContextImpl_js_1.serializeOrigin)(origin), ownerRealm ? [ownerRealm] : [], uniqueId, this.#realmStorage, realmType);
this.#workers.set(cdpTarget.cdpSessionId, workerRealm);
});
}
#handleDetachedFromTargetEvent({ sessionId, targetId, }) {
if (targetId) {
this.#preloadScriptStorage.find({ targetId }).map((preloadScript) => {
preloadScript.dispose(targetId);
});
}
const context = this.#browsingContextStorage.findContextBySession(sessionId);
if (context) {
context.dispose(true);
return;
}
const worker = this.#workers.get(sessionId);
if (worker) {
this.#realmStorage.deleteRealms({
cdpSessionId: worker.cdpClient.sessionId,
});
}
}
#handleTargetInfoChangedEvent(params) {
const context = this.#browsingContextStorage.findContext(params.targetInfo.targetId);
if (context) {
context.onTargetInfoChanged(params);
}
}
#handleTargetCrashedEvent(cdpClient) {
// This is primarily used for service and shared workers. CDP tends to not
// signal they closed gracefully and instead says they crashed to signal
// they are closed.
const realms = this.#realmStorage.findRealms({
cdpSessionId: cdpClient.sessionId,
});
for (const realm of realms) {
realm.dispose();
}
}
}
exports.CdpTargetManager = CdpTargetManager;
//# sourceMappingURL=CdpTargetManager.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,91 @@
/**
* 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.
*/
import type { Protocol } from 'devtools-protocol';
import { BrowsingContext, type Emulation, type UAClientHints } from '../../../protocol/protocol.js';
import { type LoggerFn } from '../../../utils/log.js';
import type { ContextConfigStorage } from '../browser/ContextConfigStorage.js';
import type { CdpTarget } from '../cdp/CdpTarget.js';
import type { Realm } from '../script/Realm.js';
import type { RealmStorage } from '../script/RealmStorage.js';
import type { EventManager } from '../session/EventManager.js';
import type { BrowsingContextStorage } from './BrowsingContextStorage.js';
export declare class BrowsingContextImpl {
#private;
static readonly LOGGER_PREFIX: "debug:browsingContext";
readonly userContext: string;
private constructor();
static create(id: BrowsingContext.BrowsingContext, parentId: BrowsingContext.BrowsingContext | null, userContext: string, cdpTarget: CdpTarget, eventManager: EventManager, browsingContextStorage: BrowsingContextStorage, realmStorage: RealmStorage, configStorage: ContextConfigStorage, url: string, originalOpener?: string, logger?: LoggerFn): BrowsingContextImpl;
/**
* @see https://html.spec.whatwg.org/multipage/document-sequences.html#navigable
*/
get navigableId(): string | undefined;
get navigationId(): string;
dispose(emitContextDestroyed: boolean): void;
/** Returns the ID of this context. */
get id(): BrowsingContext.BrowsingContext;
/** Returns the parent context ID. */
get parentId(): BrowsingContext.BrowsingContext | null;
/** Sets the parent context ID and updates parent's children. */
set parentId(parentId: BrowsingContext.BrowsingContext | null);
/** Returns the parent context. */
get parent(): BrowsingContextImpl | null;
/** Returns all direct children contexts. */
get directChildren(): BrowsingContextImpl[];
/** Returns all children contexts, flattened. */
get allChildren(): BrowsingContextImpl[];
/**
* Returns true if this is a top-level context.
* This is the case whenever the parent context ID is null.
*/
isTopLevelContext(): boolean;
get top(): BrowsingContextImpl;
addChild(childId: BrowsingContext.BrowsingContext): void;
get cdpTarget(): CdpTarget;
updateCdpTarget(cdpTarget: CdpTarget): void;
get url(): string;
lifecycleLoaded(): Promise<void>;
targetUnblockedOrThrow(): Promise<void>;
/** Returns a sandbox for internal helper scripts which is not exposed to the user.*/
getOrCreateHiddenSandbox(): Promise<Realm>;
/** Returns a sandbox which is exposed to user. */
getOrCreateUserSandbox(sandbox: string | undefined): Promise<Realm>;
/**
* Implements https://w3c.github.io/webdriver-bidi/#get-the-navigable-info.
*/
serializeToBidiValue(maxDepth?: number | null, addParentField?: boolean): BrowsingContext.Info;
onTargetInfoChanged(params: Protocol.Target.TargetInfoChangedEvent): void;
navigate(url: string, wait: BrowsingContext.ReadinessState): Promise<BrowsingContext.NavigateResult>;
reload(ignoreCache: boolean, wait: BrowsingContext.ReadinessState): Promise<BrowsingContext.NavigateResult>;
setViewport(viewport: BrowsingContext.Viewport | null, devicePixelRatio: number | null, screenOrientation: Emulation.ScreenOrientation | null): Promise<void>;
handleUserPrompt(accept?: boolean, userText?: string): Promise<void>;
activate(): Promise<void>;
captureScreenshot(params: BrowsingContext.CaptureScreenshotParameters): Promise<BrowsingContext.CaptureScreenshotResult>;
print(params: BrowsingContext.PrintParameters): Promise<BrowsingContext.PrintResult>;
close(): Promise<void>;
traverseHistory(delta: number): Promise<void>;
toggleModulesIfNeeded(): Promise<void>;
locateNodes(params: BrowsingContext.LocateNodesParameters): Promise<BrowsingContext.LocateNodesResult>;
setTimezoneOverride(timezone: string | null): Promise<void>;
setLocaleOverride(locale: string | null): Promise<void>;
setGeolocationOverride(geolocation: Emulation.GeolocationCoordinates | Emulation.GeolocationPositionError | null): Promise<void>;
setScriptingEnabled(scriptingEnabled: false | null): Promise<void>;
setUserAgentAndAcceptLanguage(userAgent: string | null | undefined, acceptLanguage: string | null | undefined, clientHints: UAClientHints.Emulation.ClientHintsMetadata | null | undefined): Promise<void>;
setEmulatedNetworkConditions(networkConditions: Emulation.NetworkConditions | null): Promise<void>;
setTouchOverride(maxTouchPoints: number | null): Promise<void>;
setExtraHeaders(cdpExtraHeaders: Protocol.Network.Headers): Promise<Promise<any>>;
}
export declare function serializeOrigin(origin: string): string;

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,22 @@
import type { CdpClient } from '../../../cdp/CdpClient.js';
import { BrowsingContext, type EmptyResult } from '../../../protocol/protocol.js';
import type { ContextConfigStorage } from '../browser/ContextConfigStorage.js';
import type { UserContextStorage } from '../browser/UserContextStorage.js';
import type { EventManager } from '../session/EventManager.js';
import type { BrowsingContextStorage } from './BrowsingContextStorage.js';
export declare class BrowsingContextProcessor {
#private;
constructor(browserCdpClient: CdpClient, browsingContextStorage: BrowsingContextStorage, userContextStorage: UserContextStorage, contextConfigStorage: ContextConfigStorage, eventManager: EventManager);
getTree(params: BrowsingContext.GetTreeParameters): BrowsingContext.GetTreeResult;
create(params: BrowsingContext.CreateParameters): Promise<BrowsingContext.CreateResult>;
navigate(params: BrowsingContext.NavigateParameters): Promise<BrowsingContext.NavigateResult>;
reload(params: BrowsingContext.ReloadParameters): Promise<EmptyResult>;
activate(params: BrowsingContext.ActivateParameters): Promise<EmptyResult>;
captureScreenshot(params: BrowsingContext.CaptureScreenshotParameters): Promise<BrowsingContext.CaptureScreenshotResult>;
print(params: BrowsingContext.PrintParameters): Promise<BrowsingContext.PrintResult>;
setViewport(params: BrowsingContext.SetViewportParameters): Promise<EmptyResult>;
traverseHistory(params: BrowsingContext.TraverseHistoryParameters): Promise<BrowsingContext.TraverseHistoryResult>;
handleUserPrompt(params: BrowsingContext.HandleUserPromptParameters): Promise<EmptyResult>;
close(params: BrowsingContext.CloseParameters): Promise<EmptyResult>;
locateNodes(params: BrowsingContext.LocateNodesParameters): Promise<BrowsingContext.LocateNodesResult>;
}

View File

@@ -0,0 +1,267 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BrowsingContextProcessor = void 0;
const protocol_js_1 = require("../../../protocol/protocol.js");
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(protocol_js_1.ChromiumBidi.BrowsingContext.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 protocol_js_1.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" /* BrowsingContext.CreateType.Tab */:
newWindow = false;
break;
case "window" /* BrowsingContext.CreateType.Window */:
newWindow = true;
break;
}
if (!existingContexts.length) {
// If there are no contexts in the given user context, we need to set
// newWindow to true as newWindow=false will be rejected.
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 (
// See https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/devtools/protocol/target_handler.cc;l=90;drc=e80392ac11e48a691f4309964cab83a3a59e01c8
err.message.startsWith('Failed to find browser context with id') ||
// See https://source.chromium.org/chromium/chromium/src/+/main:headless/lib/browser/protocol/target_handler.cc;l=49;drc=e80392ac11e48a691f4309964cab83a3a59e01c8
err.message === 'browserContextId') {
throw new protocol_js_1.NoSuchUserContextException(`The context ${userContext} was not found`);
}
throw err;
}
// Wait for the new target to be attached and to be added to the browsing context
// storage.
const context = await this.#browsingContextStorage.waitForContext(result.targetId);
// Wait for the new tab to be loaded to avoid race conditions in the
// `browsingContext` events, when the `browsingContext.domContentLoaded` and
// `browsingContext.load` events from the initial `about:blank` navigation
// are emitted after the next navigation is started.
// Details: https://github.com/web-platform-tests/wpt/issues/35846
await context.lifecycleLoaded();
return { context: context.id };
}
navigate(params) {
const context = this.#browsingContextStorage.getContext(params.context);
return context.navigate(params.url, params.wait ?? "none" /* BrowsingContext.ReadinessState.None */);
}
reload(params) {
const context = this.#browsingContextStorage.getContext(params.context);
return context.reload(params.ignoreCache ?? false, params.wait ?? "none" /* BrowsingContext.ReadinessState.None */);
}
async activate(params) {
const context = this.#browsingContextStorage.getContext(params.context);
if (!context.isTopLevelContext()) {
throw new protocol_js_1.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) {
// Check the The viewport size limits is not checked by protocol parser, so we need to validate
// it manually:
// https://crsrc.org/c/content/browser/devtools/protocol/emulation_handler.cc;drc=f49e23d8e2bd190b42ec62284b8be10dcccd0446;l=660
const maxDimensionSize = 10_000_000;
if ((params.viewport?.height ?? 0) > maxDimensionSize ||
(params.viewport?.width ?? 0) > maxDimensionSize) {
throw new protocol_js_1.UnsupportedOperationException(`Viewport dimension over ${maxDimensionSize} are not supported`);
}
const config = {};
// `undefined` means no changes should be done to the 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 {};
}
/**
* Returns a list of top-level browsing context ids.
*/
async #getRelatedTopLevelBrowsingContexts(browsingContextId, userContextIds) {
if (browsingContextId === undefined && userContextIds === undefined) {
throw new protocol_js_1.InvalidArgumentException('Either userContexts or context must be provided');
}
if (browsingContextId !== undefined && userContextIds !== undefined) {
throw new protocol_js_1.InvalidArgumentException('userContexts and context are mutually exclusive');
}
if (browsingContextId !== undefined) {
const context = this.#browsingContextStorage.getContext(browsingContextId);
if (!context.isTopLevelContext()) {
throw new protocol_js_1.InvalidArgumentException('Emulating viewport is only supported on the top-level context');
}
return [context];
}
// Verify that all user contexts exist.
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);
}
// Remove duplicates. Compare `BrowsingContextImpl` by reference is correct here, as
// `browsingContextStorage` returns the same instance for the same id.
return [...new Set(result).values()];
}
async traverseHistory(params) {
const context = this.#browsingContextStorage.getContext(params.context);
if (!context) {
throw new protocol_js_1.InvalidArgumentException(`No browsing context with id ${params.context}`);
}
if (!context.isTopLevelContext()) {
throw new protocol_js_1.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) {
// Heuristically determine the error
// https://source.chromium.org/chromium/chromium/src/+/main:content/browser/devtools/protocol/page_handler.cc;l=1085?q=%22No%20dialog%20is%20showing%22&ss=chromium
if (error.message?.includes('No dialog is showing')) {
throw new protocol_js_1.NoSuchAlertException('No dialog is showing');
}
throw error;
}
return {};
}
async close(params) {
const context = this.#browsingContextStorage.getContext(params.context);
if (!context.isTopLevelContext()) {
throw new protocol_js_1.InvalidArgumentException(`Non top-level browsing context ${context.id} cannot be closed.`);
}
// Parent session of a page target session can be a `browser` or a `tab` session.
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) {
// Swallow error that arise from the session being destroyed. Rely on the
// `detachedFromTargetPromise` event to be resolved.
if (!parentCdpClient.isCloseError(error)) {
throw error;
}
}
// Sometimes CDP command finishes before `detachedFromTarget` event,
// sometimes after. Wait for the CDP command to be finished, and then wait
// for `detachedFromTarget` if it hasn't emitted.
await detachedFromTargetPromise;
}
catch (error) {
// Swallow error that arise from the page being destroyed
// Example is navigating to faulty SSL certificate
if (!(error.code === -32000 /* CdpErrorConstants.GENERIC_ERROR */ &&
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: protocol_js_1.ChromiumBidi.BrowsingContext.EventNames.ContextCreated,
params: context.serializeToBidiValue(),
}, context.id);
});
return Promise.resolve();
}
}
exports.BrowsingContextProcessor = BrowsingContextProcessor;
//# sourceMappingURL=BrowsingContextProcessor.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,47 @@
/**
* 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.
*/
import { type BrowsingContext } from '../../../protocol/protocol.js';
import type { BrowsingContextImpl } from './BrowsingContextImpl.js';
/** Container class for browsing contexts. */
export declare class BrowsingContextStorage {
#private;
/** Gets all top-level contexts, i.e. those with no parent. */
getTopLevelContexts(): BrowsingContextImpl[];
/** Gets all contexts. */
getAllContexts(): BrowsingContextImpl[];
/** Deletes the context with the given ID. */
deleteContextById(id: BrowsingContext.BrowsingContext): void;
/** Deletes the given context. */
deleteContext(context: BrowsingContextImpl): void;
/** Tracks the given context. */
addContext(context: BrowsingContextImpl): void;
/**
* Waits for a context with the given ID to be added and returns it.
*/
waitForContext(browsingContextId: BrowsingContext.BrowsingContext): Promise<BrowsingContextImpl>;
/** Returns true whether there is an existing context with the given ID. */
hasContext(id: BrowsingContext.BrowsingContext): boolean;
/** Gets the context with the given ID, if any. */
findContext(id: BrowsingContext.BrowsingContext): BrowsingContextImpl | undefined;
/** Returns the top-level context ID of the given context, if any. */
findTopLevelContextId(id: BrowsingContext.BrowsingContext | null): BrowsingContext.BrowsingContext | null;
findContextBySession(sessionId: string): BrowsingContextImpl | undefined;
/** Gets the context with the given ID, if any, otherwise throws. */
getContext(id: BrowsingContext.BrowsingContext): BrowsingContextImpl;
verifyTopLevelContextsList(contexts: BrowsingContext.BrowsingContext[] | undefined): Set<BrowsingContextImpl>;
verifyContextsList(contexts: BrowsingContext.BrowsingContext[]): void;
}

View File

@@ -0,0 +1,134 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.BrowsingContextStorage = void 0;
const protocol_js_1 = require("../../../protocol/protocol.js");
const EventEmitter_js_1 = require("../../../utils/EventEmitter.js");
/** Container class for browsing contexts. */
class BrowsingContextStorage {
/** Map from context ID to context implementation. */
#contexts = new Map();
/** Event emitter for browsing context storage eventsis not expected to be exposed to
* the outside world. */
#eventEmitter = new EventEmitter_js_1.EventEmitter();
/** Gets all top-level contexts, i.e. those with no parent. */
getTopLevelContexts() {
return this.getAllContexts().filter((context) => context.isTopLevelContext());
}
/** Gets all contexts. */
getAllContexts() {
return Array.from(this.#contexts.values());
}
/** Deletes the context with the given ID. */
deleteContextById(id) {
this.#contexts.delete(id);
}
/** Deletes the given context. */
deleteContext(context) {
this.#contexts.delete(context.id);
}
/** Tracks the given context. */
addContext(context) {
this.#contexts.set(context.id, context);
this.#eventEmitter.emit("added" /* BrowsingContextStorageEvents.Added */, {
browsingContext: context,
});
}
/**
* Waits for a context with the given ID to be added and returns it.
*/
waitForContext(browsingContextId) {
if (this.#contexts.has(browsingContextId)) {
return Promise.resolve(this.getContext(browsingContextId));
}
return new Promise((resolve) => {
const listener = (event) => {
if (event.browsingContext.id === browsingContextId) {
this.#eventEmitter.off("added" /* BrowsingContextStorageEvents.Added */, listener);
resolve(event.browsingContext);
}
};
this.#eventEmitter.on("added" /* BrowsingContextStorageEvents.Added */, listener);
});
}
/** Returns true whether there is an existing context with the given ID. */
hasContext(id) {
return this.#contexts.has(id);
}
/** Gets the context with the given ID, if any. */
findContext(id) {
return this.#contexts.get(id);
}
/** Returns the top-level context ID of the given context, if any. */
findTopLevelContextId(id) {
if (id === null) {
return null;
}
const maybeContext = this.findContext(id);
if (!maybeContext) {
return null;
}
const parentId = maybeContext.parentId ?? null;
if (parentId === null) {
return id;
}
return this.findTopLevelContextId(parentId);
}
findContextBySession(sessionId) {
for (const context of this.#contexts.values()) {
if (context.cdpTarget.cdpSessionId === sessionId) {
return context;
}
}
return;
}
/** Gets the context with the given ID, if any, otherwise throws. */
getContext(id) {
const result = this.findContext(id);
if (result === undefined) {
throw new protocol_js_1.NoSuchFrameException(`Context ${id} not found`);
}
return result;
}
verifyTopLevelContextsList(contexts) {
const foundContexts = new Set();
if (!contexts) {
return foundContexts;
}
for (const contextId of contexts) {
const context = this.getContext(contextId);
if (context.isTopLevelContext()) {
foundContexts.add(context);
}
else {
throw new protocol_js_1.InvalidArgumentException(`Non top-level context '${contextId}' given.`);
}
}
return foundContexts;
}
verifyContextsList(contexts) {
if (!contexts.length) {
return;
}
for (const contextId of contexts) {
this.getContext(contextId);
}
}
}
exports.BrowsingContextStorage = BrowsingContextStorage;
//# sourceMappingURL=BrowsingContextStorage.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"BrowsingContextStorage.js","sourceRoot":"","sources":["../../../../../src/bidiMapper/modules/context/BrowsingContextStorage.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,87 @@
import type { Protocol } from 'devtools-protocol';
import { type BrowsingContext } from '../../../protocol/protocol.js';
import { Deferred } from '../../../utils/Deferred.js';
import { type LoggerFn } from '../../../utils/log.js';
import type { EventManager } from '../session/EventManager.js';
export declare const enum NavigationEventName {
FragmentNavigated = "browsingContext.fragmentNavigated",
NavigationAborted = "browsingContext.navigationAborted",
NavigationFailed = "browsingContext.navigationFailed",
Load = "browsingContext.load"
}
export declare class NavigationResult {
readonly eventName: NavigationEventName;
readonly message?: string;
constructor(eventName: NavigationEventName, message?: string);
}
export declare class NavigationState {
#private;
readonly navigationId: `${string}-${string}-${string}-${string}-${string}`;
url: string;
loaderId?: string;
committed: Deferred<void>;
isFragmentNavigation?: boolean;
get finished(): Promise<NavigationResult>;
constructor(url: string, browsingContextId: string, isInitial: boolean, eventManager: EventManager);
navigationInfo(): BrowsingContext.NavigationInfo;
start(): void;
frameNavigated(): void;
fragmentNavigated(): void;
load(): void;
fail(message: string): void;
}
/**
* Keeps track of navigations. Details: http://go/webdriver:bidi-navigation
*/
export declare class NavigationTracker {
#private;
constructor(url: string, browsingContextId: string, eventManager: EventManager, logger?: LoggerFn);
/**
* Returns current started ongoing navigation. It can be either a started pending
* navigation, or one is already navigated.
*/
get currentNavigationId(): `${string}-${string}-${string}-${string}-${string}`;
/**
* Flags if the current navigation relates to the initial to `about:blank` navigation.
*/
get isInitialNavigation(): boolean;
/**
* Url of the last navigated navigation.
*/
get url(): string;
/**
* Creates a pending navigation e.g. when navigation command is called. Required to
* provide navigation id before the actual navigation is started. It will be used when
* navigation started. Can be aborted, failed, fragment navigated, or became a current
* navigation.
*/
createPendingNavigation(url: string, canBeInitialNavigation?: boolean): NavigationState;
dispose(): void;
onTargetInfoChanged(url: string): void;
/**
* @param {string} unreachableUrl indicated the navigation is actually failed.
*/
frameNavigated(url: string, loaderId: string, unreachableUrl?: string): void;
navigatedWithinDocument(url: string, navigationType: Protocol.Page.NavigatedWithinDocumentEvent['navigationType']): void;
/**
* Required to mark navigation as fully complete.
* TODO: navigation should be complete when it became the current one on
* `Page.frameNavigated` or on navigating command finished with a new loader Id.
*/
loadPageEvent(loaderId: string): void;
/**
* Fail navigation due to navigation command failed.
*/
failNavigation(navigation: NavigationState, errorText: string): void;
/**
* Updates the navigation's `loaderId` and sets it as current one, if it is a
* cross-document navigation.
*/
navigationCommandFinished(navigation: NavigationState, loaderId?: string): void;
frameStartedNavigating(url: string, loaderId: string, navigationType: string): void;
/**
* If there is a navigation with the loaderId equals to the network request id, it means
* that the navigation failed.
*/
networkLoadingFailed(loaderId: string, errorText: string): void;
}

View File

@@ -0,0 +1,331 @@
"use strict";
/*
* Copyright 2024 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.
*
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.NavigationTracker = exports.NavigationState = exports.NavigationResult = void 0;
const protocol_js_1 = require("../../../protocol/protocol.js");
const Deferred_js_1 = require("../../../utils/Deferred.js");
const log_js_1 = require("../../../utils/log.js");
const time_js_1 = require("../../../utils/time.js");
const urlHelpers_js_1 = require("../../../utils/urlHelpers.js");
const uuid_js_1 = require("../../../utils/uuid.js");
class NavigationResult {
eventName;
message;
constructor(eventName, message) {
this.eventName = eventName;
this.message = message;
}
}
exports.NavigationResult = NavigationResult;
class NavigationState {
navigationId = (0, uuid_js_1.uuidv4)();
#browsingContextId;
#started = false;
#finished = new Deferred_js_1.Deferred();
url;
loaderId;
#isInitial;
#eventManager;
committed = new Deferred_js_1.Deferred();
isFragmentNavigation;
get finished() {
return this.#finished;
}
constructor(url, browsingContextId, isInitial, eventManager) {
this.#browsingContextId = browsingContextId;
this.url = url;
this.#isInitial = isInitial;
this.#eventManager = eventManager;
}
navigationInfo() {
return {
context: this.#browsingContextId,
navigation: this.navigationId,
timestamp: (0, time_js_1.getTimestamp)(),
url: this.url,
};
}
start() {
if (
// Initial navigation should not be reported.
!this.#isInitial &&
// No need in reporting started navigation twice.
!this.#started &&
// No need for reporting fragment navigations. Step 13 vs step 16 of the spec:
// https://html.spec.whatwg.org/#beginning-navigation:webdriver-bidi-navigation-started
!this.isFragmentNavigation) {
this.#eventManager.registerEvent({
type: 'event',
method: protocol_js_1.ChromiumBidi.BrowsingContext.EventNames.NavigationStarted,
params: this.navigationInfo(),
}, this.#browsingContextId);
}
this.#started = true;
}
#finish(navigationResult) {
this.#started = true;
if (!this.#isInitial &&
!this.#finished.isFinished &&
navigationResult.eventName !== "browsingContext.load" /* NavigationEventName.Load */) {
this.#eventManager.registerEvent({
type: 'event',
method: navigationResult.eventName,
params: this.navigationInfo(),
}, this.#browsingContextId);
}
this.#finished.resolve(navigationResult);
}
frameNavigated() {
this.committed.resolve();
if (!this.#isInitial) {
this.#eventManager.registerEvent({
type: 'event',
method: protocol_js_1.ChromiumBidi.BrowsingContext.EventNames.NavigationCommitted,
params: this.navigationInfo(),
}, this.#browsingContextId);
}
}
fragmentNavigated() {
this.committed.resolve();
this.#finish(new NavigationResult("browsingContext.fragmentNavigated" /* NavigationEventName.FragmentNavigated */));
}
load() {
this.#finish(new NavigationResult("browsingContext.load" /* NavigationEventName.Load */));
}
fail(message) {
this.#finish(new NavigationResult(this.committed.isFinished
? "browsingContext.navigationAborted" /* NavigationEventName.NavigationAborted */
: "browsingContext.navigationFailed" /* NavigationEventName.NavigationFailed */, message));
}
}
exports.NavigationState = NavigationState;
/**
* Keeps track of navigations. Details: http://go/webdriver:bidi-navigation
*/
class NavigationTracker {
#eventManager;
#logger;
#loaderIdToNavigationsMap = new Map();
#browsingContextId;
/**
* Last committed navigation is committed, but is not guaranteed to be finished, as it
* can still wait for `load` or `DOMContentLoaded` events.
*/
#lastCommittedNavigation;
/**
* Pending navigation is a navigation that is started but not yet committed.
*/
#pendingNavigation;
// Flags if the initial navigation to `about:blank` is in progress.
#isInitialNavigation = true;
constructor(url, browsingContextId, eventManager, logger) {
this.#browsingContextId = browsingContextId;
this.#eventManager = eventManager;
this.#logger = logger;
this.#isInitialNavigation = true;
// The initial navigation is always committed.
this.#lastCommittedNavigation = new NavigationState(url, browsingContextId, (0, urlHelpers_js_1.urlMatchesAboutBlank)(url), this.#eventManager);
}
/**
* Returns current started ongoing navigation. It can be either a started pending
* navigation, or one is already navigated.
*/
get currentNavigationId() {
if (this.#pendingNavigation?.isFragmentNavigation === false) {
// Use pending navigation if it is started and it is not a fragment navigation.
return this.#pendingNavigation.navigationId;
}
// If the pending navigation is a fragment one, or if it is not exists, the last
// committed navigation should be used.
return this.#lastCommittedNavigation.navigationId;
}
/**
* Flags if the current navigation relates to the initial to `about:blank` navigation.
*/
get isInitialNavigation() {
return this.#isInitialNavigation;
}
/**
* Url of the last navigated navigation.
*/
get url() {
return this.#lastCommittedNavigation.url;
}
/**
* Creates a pending navigation e.g. when navigation command is called. Required to
* provide navigation id before the actual navigation is started. It will be used when
* navigation started. Can be aborted, failed, fragment navigated, or became a current
* navigation.
*/
createPendingNavigation(url, canBeInitialNavigation = false) {
this.#logger?.(log_js_1.LogType.debug, 'createCommandNavigation');
this.#isInitialNavigation =
canBeInitialNavigation &&
this.#isInitialNavigation &&
(0, urlHelpers_js_1.urlMatchesAboutBlank)(url);
this.#pendingNavigation?.fail('navigation canceled by concurrent navigation');
const navigation = new NavigationState(url, this.#browsingContextId, this.#isInitialNavigation, this.#eventManager);
this.#pendingNavigation = navigation;
return navigation;
}
dispose() {
this.#pendingNavigation?.fail('navigation canceled by context disposal');
this.#lastCommittedNavigation.fail('navigation canceled by context disposal');
}
// Update the current url.
onTargetInfoChanged(url) {
this.#logger?.(log_js_1.LogType.debug, `onTargetInfoChanged ${url}`);
this.#lastCommittedNavigation.url = url;
}
#getNavigationForFrameNavigated(url, loaderId) {
if (this.#loaderIdToNavigationsMap.has(loaderId)) {
return this.#loaderIdToNavigationsMap.get(loaderId);
}
if (this.#pendingNavigation !== undefined &&
this.#pendingNavigation.loaderId === undefined) {
// This can be a pending navigation to `about:blank` created by a command. Use the
// pending navigation in this case.
return this.#pendingNavigation;
}
// Create a new pending navigation.
return this.createPendingNavigation(url, true);
}
/**
* @param {string} unreachableUrl indicated the navigation is actually failed.
*/
frameNavigated(url, loaderId, unreachableUrl) {
this.#logger?.(log_js_1.LogType.debug, `frameNavigated ${url}`);
if (unreachableUrl !== undefined) {
// The navigation failed.
const navigation = this.#loaderIdToNavigationsMap.get(loaderId) ??
this.#pendingNavigation ??
this.createPendingNavigation(unreachableUrl, true);
navigation.url = unreachableUrl;
navigation.start();
navigation.fail('the requested url is unreachable');
return;
}
const navigation = this.#getNavigationForFrameNavigated(url, loaderId);
if (navigation !== this.#lastCommittedNavigation) {
// Even though the `lastCommittedNavigation` is navigated, it still can be waiting
// for `load` or `DOMContentLoaded` events.
this.#lastCommittedNavigation.fail('navigation canceled by concurrent navigation');
}
navigation.url = url;
navigation.loaderId = loaderId;
this.#loaderIdToNavigationsMap.set(loaderId, navigation);
navigation.start();
navigation.frameNavigated();
this.#lastCommittedNavigation = navigation;
if (this.#pendingNavigation === navigation) {
this.#pendingNavigation = undefined;
}
}
navigatedWithinDocument(url, navigationType) {
this.#logger?.(log_js_1.LogType.debug, `navigatedWithinDocument ${url}, ${navigationType}`);
// Current navigation URL should be updated.
this.#lastCommittedNavigation.url = url;
if (navigationType !== 'fragment') {
// TODO: check for other navigation types, like `javascript`.
return;
}
// There is no way to map `navigatedWithinDocument` to a specific navigation. Consider
// it is the pending navigation, if it is a fragment one.
const fragmentNavigation = this.#pendingNavigation?.isFragmentNavigation === true
? this.#pendingNavigation
: new NavigationState(url, this.#browsingContextId, false, this.#eventManager);
// Finish ongoing navigation.
fragmentNavigation.fragmentNavigated();
if (fragmentNavigation === this.#pendingNavigation) {
this.#pendingNavigation = undefined;
}
}
/**
* Required to mark navigation as fully complete.
* TODO: navigation should be complete when it became the current one on
* `Page.frameNavigated` or on navigating command finished with a new loader Id.
*/
loadPageEvent(loaderId) {
this.#logger?.(log_js_1.LogType.debug, 'loadPageEvent');
// Even if it was an initial navigation, it is finished.
this.#isInitialNavigation = false;
this.#loaderIdToNavigationsMap.get(loaderId)?.load();
}
/**
* Fail navigation due to navigation command failed.
*/
failNavigation(navigation, errorText) {
this.#logger?.(log_js_1.LogType.debug, 'failCommandNavigation');
navigation.fail(errorText);
}
/**
* Updates the navigation's `loaderId` and sets it as current one, if it is a
* cross-document navigation.
*/
navigationCommandFinished(navigation, loaderId) {
this.#logger?.(log_js_1.LogType.debug, `finishCommandNavigation ${navigation.navigationId}, ${loaderId}`);
if (loaderId !== undefined) {
navigation.loaderId = loaderId;
this.#loaderIdToNavigationsMap.set(loaderId, navigation);
}
navigation.isFragmentNavigation = loaderId === undefined;
}
frameStartedNavigating(url, loaderId, navigationType) {
this.#logger?.(log_js_1.LogType.debug, `frameStartedNavigating ${url}, ${loaderId}`);
if (this.#pendingNavigation &&
this.#pendingNavigation?.loaderId !== undefined &&
this.#pendingNavigation?.loaderId !== loaderId) {
// If there is a pending navigation with loader id set, but not equal to the new
// loader id, cancel pending navigation.
this.#pendingNavigation?.fail('navigation canceled by concurrent navigation');
this.#pendingNavigation = undefined;
}
if (this.#loaderIdToNavigationsMap.has(loaderId)) {
const existingNavigation = this.#loaderIdToNavigationsMap.get(loaderId);
// Navigation can be changed from `sameDocument` to `differentDocument`.
existingNavigation.isFragmentNavigation =
NavigationTracker.#isFragmentNavigation(navigationType);
this.#pendingNavigation = existingNavigation;
return;
}
const pendingNavigation = this.#pendingNavigation ?? this.createPendingNavigation(url, true);
this.#loaderIdToNavigationsMap.set(loaderId, pendingNavigation);
pendingNavigation.isFragmentNavigation =
NavigationTracker.#isFragmentNavigation(navigationType);
pendingNavigation.url = url;
pendingNavigation.loaderId = loaderId;
pendingNavigation.start();
}
static #isFragmentNavigation(navigationType) {
// Page.frameStartedNavigating.navigationType can be one of the following values:
// reload, reloadBypassingCache, restore, restoreWithPost, historySameDocument,
// historyDifferentDocument, sameDocument, differentDocument.
// https://chromedevtools.github.io/devtools-protocol/tot/Page/#event-frameStartedNavigating
return ['historySameDocument', 'sameDocument'].includes(navigationType);
}
/**
* If there is a navigation with the loaderId equals to the network request id, it means
* that the navigation failed.
*/
networkLoadingFailed(loaderId, errorText) {
this.#loaderIdToNavigationsMap.get(loaderId)?.fail(errorText);
}
}
exports.NavigationTracker = NavigationTracker;
//# sourceMappingURL=NavigationTracker.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,37 @@
/**
* 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.
*/
import type { EmptyResult, Emulation, UAClientHints } from '../../../protocol/protocol.js';
import type { ContextConfigStorage } from '../browser/ContextConfigStorage.js';
import type { UserContextStorage } from '../browser/UserContextStorage.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
export declare class EmulationProcessor {
#private;
constructor(browsingContextStorage: BrowsingContextStorage, userContextStorage: UserContextStorage, contextConfigStorage: ContextConfigStorage);
setGeolocationOverride(params: Emulation.SetGeolocationOverrideParameters): Promise<EmptyResult>;
setLocaleOverride(params: Emulation.SetLocaleOverrideParameters): Promise<EmptyResult>;
setScriptingEnabled(params: Emulation.SetScriptingEnabledParameters): Promise<EmptyResult>;
setScreenOrientationOverride(params: Emulation.SetScreenOrientationOverrideParameters): Promise<EmptyResult>;
setScreenSettingsOverride(params: Emulation.SetScreenSettingsOverrideParameters): Promise<EmptyResult>;
setTimezoneOverride(params: Emulation.SetTimezoneOverrideParameters): Promise<EmptyResult>;
setTouchOverride(params: Emulation.SetTouchOverrideParameters): Promise<EmptyResult>;
setUserAgentOverrideParams(params: Emulation.SetUserAgentOverrideParameters): Promise<EmptyResult>;
setClientHintsOverride(params: UAClientHints.Emulation.SetClientHintsOverrideParameters): Promise<EmptyResult>;
setNetworkConditions(params: Emulation.SetNetworkConditionsParameters): Promise<EmptyResult>;
}
export declare function isValidLocale(locale: string): boolean;
export declare function isValidTimezone(timezone: string): boolean;
export declare function isTimeZoneOffsetString(timezone: string): boolean;

View File

@@ -0,0 +1,384 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.EmulationProcessor = void 0;
exports.isValidLocale = isValidLocale;
exports.isValidTimezone = isValidTimezone;
exports.isTimeZoneOffsetString = isTimeZoneOffsetString;
const protocol_js_1 = require("../../../protocol/protocol.js");
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) {
// Unreachable. Handled by params parser.
throw new protocol_js_1.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 protocol_js_1.InvalidArgumentException('Geolocation altitudeAccuracy can be set only with altitude');
}
geolocation = params.coordinates;
}
else if ('error' in params) {
if (params.error.type !== 'positionUnavailable') {
// Unreachable. Handled by params parser.
throw new protocol_js_1.InvalidArgumentException(`Unknown geolocation error ${params.error.type}`);
}
geolocation = params.error;
}
else {
// Unreachable. Handled by params parser.
throw new protocol_js_1.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) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
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 protocol_js_1.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) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await Promise.all([
context.setLocaleOverride(config.locale ?? null),
// Set `AcceptLanguage` to locale.
context.setUserAgentAndAcceptLanguage(config.userAgent, config.locale, config.clientHints),
]);
}));
return {};
}
async setScriptingEnabled(params) {
const scriptingEnabled = params.enabled;
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
scriptingEnabled,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
scriptingEnabled,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setScriptingEnabled(config.scriptingEnabled ?? null);
}));
return {};
}
async setScreenOrientationOverride(params) {
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
screenOrientation: params.screenOrientation,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
screenOrientation: params.screenOrientation,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setViewport(config.viewport ?? null, config.devicePixelRatio ?? null, config.screenOrientation ?? null);
}));
return {};
}
async setScreenSettingsOverride(params) {
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
screenArea: params.screenArea,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
screenArea: params.screenArea,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setViewport(config.viewport ?? null, config.devicePixelRatio ?? null, config.screenOrientation ?? null);
}));
return {};
}
/**
* Returns a list of top-level browsing contexts.
*/
async #getRelatedTopLevelBrowsingContexts(browsingContextIds, userContextIds, allowGlobal = false) {
if (browsingContextIds === undefined && userContextIds === undefined) {
if (allowGlobal) {
return this.#browsingContextStorage.getTopLevelContexts();
}
throw new protocol_js_1.InvalidArgumentException('Either user contexts or browsing contexts must be provided');
}
if (browsingContextIds !== undefined && userContextIds !== undefined) {
throw new protocol_js_1.InvalidArgumentException('User contexts and browsing contexts are mutually exclusive');
}
const result = [];
if (browsingContextIds === undefined) {
// userContextIds !== undefined
if (userContextIds.length === 0) {
throw new protocol_js_1.InvalidArgumentException('user context should be provided');
}
// Verify that all user contexts exist.
await this.#userContextStorage.verifyUserContextIdList(userContextIds);
for (const userContextId of userContextIds) {
const topLevelBrowsingContexts = this.#browsingContextStorage
.getTopLevelContexts()
.filter((browsingContext) => browsingContext.userContext === userContextId);
result.push(...topLevelBrowsingContexts);
}
}
else {
if (browsingContextIds.length === 0) {
throw new protocol_js_1.InvalidArgumentException('browsing context should be provided');
}
for (const browsingContextId of browsingContextIds) {
const browsingContext = this.#browsingContextStorage.getContext(browsingContextId);
if (!browsingContext.isTopLevelContext()) {
throw new protocol_js_1.InvalidArgumentException('The command is only supported on the top-level context');
}
result.push(browsingContext);
}
}
// Remove duplicates. Compare `BrowsingContextImpl` by reference is correct here, as
// `browsingContextStorage` returns the same instance for the same id.
return [...new Set(result).values()];
}
async setTimezoneOverride(params) {
let timezone = params.timezone ?? null;
if (timezone !== null && !isValidTimezone(timezone)) {
throw new protocol_js_1.InvalidArgumentException(`Invalid timezone "${timezone}"`);
}
if (timezone !== null && isTimeZoneOffsetString(timezone)) {
// CDP supports offset timezone with `GMT` prefix.
timezone = `GMT${timezone}`;
}
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
timezone,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
timezone,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setTimezoneOverride(config.timezone ?? null);
}));
return {};
}
async setTouchOverride(params) {
const maxTouchPoints = params.maxTouchPoints;
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts, true);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
maxTouchPoints,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
maxTouchPoints,
});
}
if (params.contexts === undefined && params.userContexts === undefined) {
this.#contextConfigStorage.updateGlobalConfig({
maxTouchPoints,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setTouchOverride(config.maxTouchPoints ?? null);
}));
return {};
}
async setUserAgentOverrideParams(params) {
if (params.userAgent === '') {
throw new protocol_js_1.UnsupportedOperationException('empty user agent string is not supported');
}
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts, true);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
userAgent: params.userAgent,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
userAgent: params.userAgent,
});
}
if (params.contexts === undefined && params.userContexts === undefined) {
this.#contextConfigStorage.updateGlobalConfig({
userAgent: params.userAgent,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setUserAgentAndAcceptLanguage(config.userAgent, config.locale, config.clientHints);
}));
return {};
}
async setClientHintsOverride(params) {
const clientHints = params.clientHints ?? null;
// Get all relevant contexts to update:
// 1. Specific browsing contexts (if provided).
// 2. All contexts for specific user contexts (if provided).
// 3. All top-level contexts (if global).
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts, true);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
clientHints,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
clientHints,
});
}
if (params.contexts === undefined && params.userContexts === undefined) {
this.#contextConfigStorage.updateGlobalConfig({
clientHints,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setUserAgentAndAcceptLanguage(config.userAgent, config.locale, config.clientHints);
}));
return {};
}
async setNetworkConditions(params) {
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts, true);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
emulatedNetworkConditions: params.networkConditions,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
emulatedNetworkConditions: params.networkConditions,
});
}
if (params.contexts === undefined && params.userContexts === undefined) {
this.#contextConfigStorage.updateGlobalConfig({
emulatedNetworkConditions: params.networkConditions,
});
}
if (params.networkConditions !== null &&
params.networkConditions.type !== 'offline') {
throw new protocol_js_1.UnsupportedOperationException(`Unsupported network conditions ${params.networkConditions.type}`);
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setEmulatedNetworkConditions(config.emulatedNetworkConditions ?? null);
}));
return {};
}
}
exports.EmulationProcessor = EmulationProcessor;
// Export for testing.
function isValidLocale(locale) {
try {
new Intl.Locale(locale);
return true;
}
catch (e) {
if (e instanceof RangeError) {
return false;
}
// Re-throw other errors
throw e;
}
}
// Export for testing.
function isValidTimezone(timezone) {
try {
Intl.DateTimeFormat(undefined, { timeZone: timezone });
return true;
}
catch (e) {
if (e instanceof RangeError) {
return false;
}
// Re-throw other errors
throw e;
}
}
// Export for testing.
function isTimeZoneOffsetString(timezone) {
return /^[+-](?:2[0-3]|[01]\d)(?::[0-5]\d)?$/.test(timezone);
}
//# sourceMappingURL=EmulationProcessor.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,27 @@
/**
* 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.
*/
import type { BrowsingContextImpl } from '../context/BrowsingContextImpl.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
import type { ActionOption } from './ActionOption.js';
import type { InputState } from './InputState.js';
export declare class ActionDispatcher {
#private;
static isMacOS: (context: BrowsingContextImpl) => Promise<boolean>;
constructor(inputState: InputState, browsingContextStorage: BrowsingContextStorage, contextId: string, isMacOS: boolean);
dispatchActions(optionsByTick: readonly (readonly Readonly<ActionOption>[])[]): Promise<void>;
dispatchTickActions(options: readonly Readonly<ActionOption>[]): Promise<void>;
}

View File

@@ -0,0 +1,744 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ActionDispatcher = void 0;
const protocol_js_1 = require("../../../protocol/protocol.js");
const assert_js_1 = require("../../../utils/assert.js");
const graphemeTools_js_1 = require("../../../utils/graphemeTools.js");
const InputSource_js_1 = require("./InputSource.js");
const keyUtils_js_1 = require("./keyUtils.js");
const USKeyboardLayout_js_1 = require("./USKeyboardLayout.js");
/** https://w3c.github.io/webdriver/#dfn-center-point */
const CALCULATE_IN_VIEW_CENTER_PT_DECL = ((i) => {
const t = i.getClientRects()[0], e = Math.max(0, Math.min(t.x, t.x + t.width)), n = Math.min(window.innerWidth, Math.max(t.x, t.x + t.width)), h = Math.max(0, Math.min(t.y, t.y + t.height)), m = Math.min(window.innerHeight, Math.max(t.y, t.y + t.height));
return [e + ((n - e) >> 1), h + ((m - h) >> 1)];
}).toString();
const IS_MAC_DECL = (() => {
return navigator.platform.toLowerCase().includes('mac');
}).toString();
async function getElementCenter(context, element) {
const hiddenSandboxRealm = await context.getOrCreateHiddenSandbox();
const result = await hiddenSandboxRealm.callFunction(CALCULATE_IN_VIEW_CENTER_PT_DECL, false, { type: 'undefined' }, [element]);
if (result.type === 'exception') {
throw new protocol_js_1.NoSuchElementException(`Origin element ${element.sharedId} was not found`);
}
(0, assert_js_1.assert)(result.result.type === 'array');
(0, assert_js_1.assert)(result.result.value?.[0]?.type === 'number');
(0, assert_js_1.assert)(result.result.value?.[1]?.type === 'number');
const { result: { value: [{ value: x }, { value: y }], }, } = result;
return { x: x, y: y };
}
class ActionDispatcher {
static isMacOS = async (context) => {
const hiddenSandboxRealm = await context.getOrCreateHiddenSandbox();
const result = await hiddenSandboxRealm.callFunction(IS_MAC_DECL, false);
(0, assert_js_1.assert)(result.type !== 'exception');
(0, assert_js_1.assert)(result.result.type === 'boolean');
return result.result.value;
};
#browsingContextStorage;
#tickStart = 0;
#tickDuration = 0;
#inputState;
#contextId;
#isMacOS;
constructor(inputState, browsingContextStorage, contextId, isMacOS) {
this.#browsingContextStorage = browsingContextStorage;
this.#inputState = inputState;
this.#contextId = contextId;
this.#isMacOS = isMacOS;
}
/**
* The context can be disposed between action ticks, so need to get it each time.
*/
get #context() {
return this.#browsingContextStorage.getContext(this.#contextId);
}
async dispatchActions(optionsByTick) {
await this.#inputState.queue.run(async () => {
for (const options of optionsByTick) {
await this.dispatchTickActions(options);
}
});
}
async dispatchTickActions(options) {
this.#tickStart = performance.now();
this.#tickDuration = 0;
for (const { action } of options) {
if ('duration' in action && action.duration !== undefined) {
this.#tickDuration = Math.max(this.#tickDuration, action.duration);
}
}
const promises = [
new Promise((resolve) => setTimeout(resolve, this.#tickDuration)),
];
for (const option of options) {
// In theory we have to wait for each action to happen, but CDP is serial,
// so as an optimization, we queue all CDP commands at once and await all
// of them.
promises.push(this.#dispatchAction(option));
}
await Promise.all(promises);
}
async #dispatchAction({ id, action }) {
const source = this.#inputState.get(id);
const keyState = this.#inputState.getGlobalKeyState();
switch (action.type) {
case 'keyDown': {
// SAFETY: The source is validated before.
await this.#dispatchKeyDownAction(source, action);
this.#inputState.cancelList.push({
id,
action: {
...action,
type: 'keyUp',
},
});
break;
}
case 'keyUp': {
// SAFETY: The source is validated before.
await this.#dispatchKeyUpAction(source, action);
break;
}
case 'pause': {
// TODO: Implement waiting on the input source.
break;
}
case 'pointerDown': {
// SAFETY: The source is validated before.
await this.#dispatchPointerDownAction(source, keyState, action);
this.#inputState.cancelList.push({
id,
action: {
...action,
type: 'pointerUp',
},
});
break;
}
case 'pointerMove': {
// SAFETY: The source is validated before.
await this.#dispatchPointerMoveAction(source, keyState, action);
break;
}
case 'pointerUp': {
// SAFETY: The source is validated before.
await this.#dispatchPointerUpAction(source, keyState, action);
break;
}
case 'scroll': {
// SAFETY: The source is validated before.
await this.#dispatchScrollAction(source, keyState, action);
break;
}
}
}
async #dispatchPointerDownAction(source, keyState, action) {
const { button } = action;
if (source.pressed.has(button)) {
return;
}
source.pressed.add(button);
const { x, y, subtype: pointerType } = source;
const { width, height, pressure, twist, tangentialPressure } = action;
const { tiltX, tiltY } = getTilt(action);
// --- Platform-specific code begins here ---
const { modifiers } = keyState;
const { radiusX, radiusY } = getRadii(width ?? 1, height ?? 1);
switch (pointerType) {
case "mouse" /* Input.PointerType.Mouse */:
case "pen" /* Input.PointerType.Pen */:
// TODO: Implement width and height when available.
await this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchMouseEvent', {
type: 'mousePressed',
x,
y,
modifiers,
button: getCdpButton(button),
buttons: source.buttons,
clickCount: source.setClickCount(button, new InputSource_js_1.PointerSource.ClickContext(x, y, performance.now())),
pointerType,
tangentialPressure,
tiltX,
tiltY,
twist,
force: pressure,
});
break;
case "touch" /* Input.PointerType.Touch */:
await this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchTouchEvent', {
type: 'touchStart',
touchPoints: [
{
x,
y,
radiusX,
radiusY,
tangentialPressure,
tiltX,
tiltY,
twist,
force: pressure,
id: source.pointerId,
},
],
modifiers,
});
break;
}
source.radiusX = radiusX;
source.radiusY = radiusY;
source.force = pressure;
// --- Platform-specific code ends here ---
}
#dispatchPointerUpAction(source, keyState, action) {
const { button } = action;
if (!source.pressed.has(button)) {
return;
}
source.pressed.delete(button);
const { x, y, force, radiusX, radiusY, subtype: pointerType } = source;
// --- Platform-specific code begins here ---
const { modifiers } = keyState;
switch (pointerType) {
case "mouse" /* Input.PointerType.Mouse */:
case "pen" /* Input.PointerType.Pen */:
// TODO: Implement width and height when available.
return this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchMouseEvent', {
type: 'mouseReleased',
x,
y,
modifiers,
button: getCdpButton(button),
buttons: source.buttons,
clickCount: source.getClickCount(button),
pointerType,
});
case "touch" /* Input.PointerType.Touch */:
return this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchTouchEvent', {
type: 'touchEnd',
touchPoints: [
{
x,
y,
id: source.pointerId,
force,
radiusX,
radiusY,
},
],
modifiers,
});
}
// --- Platform-specific code ends here ---
}
async #dispatchPointerMoveAction(source, keyState, action) {
const { x: startX, y: startY, subtype: pointerType } = source;
const { width, height, pressure, twist, tangentialPressure, x: offsetX, y: offsetY, origin = 'viewport', duration = this.#tickDuration, } = action;
const { tiltX, tiltY } = getTilt(action);
const { radiusX, radiusY } = getRadii(width ?? 1, height ?? 1);
const { targetX, targetY } = await this.#getCoordinateFromOrigin(origin, offsetX, offsetY, startX, startY);
if (targetX < 0 || targetY < 0) {
throw new protocol_js_1.MoveTargetOutOfBoundsException(`Cannot move beyond viewport (x: ${targetX}, y: ${targetY})`);
}
let last;
do {
const ratio = duration > 0 ? (performance.now() - this.#tickStart) / duration : 1;
last = ratio >= 1;
let x;
let y;
if (last) {
x = targetX;
y = targetY;
}
else {
x = Math.round(ratio * (targetX - startX) + startX);
y = Math.round(ratio * (targetY - startY) + startY);
}
if (source.x !== x || source.y !== y) {
// --- Platform-specific code begins here ---
const { modifiers } = keyState;
switch (pointerType) {
case "mouse" /* Input.PointerType.Mouse */:
// TODO: Implement width and height when available.
await this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchMouseEvent', {
type: 'mouseMoved',
x,
y,
modifiers,
clickCount: 0,
button: getCdpButton(source.pressed.values().next().value ?? 5),
buttons: source.buttons,
pointerType,
tangentialPressure,
tiltX,
tiltY,
twist,
force: pressure,
});
break;
case "pen" /* Input.PointerType.Pen */:
if (source.pressed.size !== 0) {
// Empty `source.pressed.size` means the pen is not detected by digitizer.
// Dispatch a mouse event for the pen only if either:
// 1. the pen is hovering over the digitizer (0);
// 2. the pen is in contact with the digitizer (1);
// 3. the pen has at least one button pressed (2, 4, etc).
// https://www.w3.org/TR/pointerevents/#the-buttons-property
// TODO: Implement width and height when available.
await this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchMouseEvent', {
type: 'mouseMoved',
x,
y,
modifiers,
clickCount: 0,
button: getCdpButton(source.pressed.values().next().value ?? 5),
buttons: source.buttons,
pointerType,
tangentialPressure,
tiltX,
tiltY,
twist,
force: pressure ?? 0.5,
});
}
break;
case "touch" /* Input.PointerType.Touch */:
if (source.pressed.size !== 0) {
await this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchTouchEvent', {
type: 'touchMove',
touchPoints: [
{
x,
y,
radiusX,
radiusY,
tangentialPressure,
tiltX,
tiltY,
twist,
force: pressure,
id: source.pointerId,
},
],
modifiers,
});
}
break;
}
// --- Platform-specific code ends here ---
source.x = x;
source.y = y;
source.radiusX = radiusX;
source.radiusY = radiusY;
source.force = pressure;
}
} while (!last);
}
async #getFrameOffset() {
if (this.#context.id === this.#context.cdpTarget.id) {
return { x: 0, y: 0 };
}
// https://github.com/w3c/webdriver/pull/1847 proposes dispatching events from
// the top-level browsing context. This implementation dispatches it on the top-most
// same-target frame, which is not top-level one in case of OOPiF.
// TODO: switch to the top-level browsing context.
const { backendNodeId } = await this.#context.cdpTarget.cdpClient.sendCommand('DOM.getFrameOwner', { frameId: this.#context.id });
const { model: frameBoxModel } = await this.#context.cdpTarget.cdpClient.sendCommand('DOM.getBoxModel', {
backendNodeId,
});
return { x: frameBoxModel.content[0], y: frameBoxModel.content[1] };
}
async #getCoordinateFromOrigin(origin, offsetX, offsetY, startX, startY) {
let targetX;
let targetY;
const frameOffset = await this.#getFrameOffset();
switch (origin) {
case 'viewport':
targetX = offsetX + frameOffset.x;
targetY = offsetY + frameOffset.y;
break;
case 'pointer':
targetX = startX + offsetX + frameOffset.x;
targetY = startY + offsetY + frameOffset.y;
break;
default: {
const { x: posX, y: posY } = await getElementCenter(this.#context, origin.element);
// SAFETY: These can never be special numbers.
targetX = posX + offsetX + frameOffset.x;
targetY = posY + offsetY + frameOffset.y;
break;
}
}
return { targetX, targetY };
}
async #dispatchScrollAction(_source, keyState, action) {
const { deltaX: targetDeltaX, deltaY: targetDeltaY, x: offsetX, y: offsetY, origin = 'viewport', duration = this.#tickDuration, } = action;
if (origin === 'pointer') {
throw new protocol_js_1.InvalidArgumentException('"pointer" origin is invalid for scrolling.');
}
const { targetX, targetY } = await this.#getCoordinateFromOrigin(origin, offsetX, offsetY, 0, 0);
if (targetX < 0 || targetY < 0) {
throw new protocol_js_1.MoveTargetOutOfBoundsException(`Cannot move beyond viewport (x: ${targetX}, y: ${targetY})`);
}
let currentDeltaX = 0;
let currentDeltaY = 0;
let last;
do {
const ratio = duration > 0 ? (performance.now() - this.#tickStart) / duration : 1;
last = ratio >= 1;
let deltaX;
let deltaY;
if (last) {
deltaX = targetDeltaX - currentDeltaX;
deltaY = targetDeltaY - currentDeltaY;
}
else {
deltaX = Math.round(ratio * targetDeltaX - currentDeltaX);
deltaY = Math.round(ratio * targetDeltaY - currentDeltaY);
}
if (deltaX !== 0 || deltaY !== 0) {
// --- Platform-specific code begins here ---
const { modifiers } = keyState;
await this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchMouseEvent', {
type: 'mouseWheel',
deltaX,
deltaY,
x: targetX,
y: targetY,
modifiers,
});
// --- Platform-specific code ends here ---
currentDeltaX += deltaX;
currentDeltaY += deltaY;
}
} while (!last);
}
async #dispatchKeyDownAction(source, action) {
const rawKey = action.value;
if (!(0, graphemeTools_js_1.isSingleGrapheme)(rawKey)) {
// https://w3c.github.io/webdriver/#dfn-process-a-key-action
// WebDriver spec allows a grapheme to be used.
throw new protocol_js_1.InvalidArgumentException(`Invalid key value: ${rawKey}`);
}
const isGrapheme = (0, graphemeTools_js_1.isSingleComplexGrapheme)(rawKey);
const key = (0, keyUtils_js_1.getNormalizedKey)(rawKey);
const repeat = source.pressed.has(key);
const code = (0, keyUtils_js_1.getKeyCode)(rawKey);
const location = (0, keyUtils_js_1.getKeyLocation)(rawKey);
switch (key) {
case 'Alt':
source.alt = true;
break;
case 'Shift':
source.shift = true;
break;
case 'Control':
source.ctrl = true;
break;
case 'Meta':
source.meta = true;
break;
}
source.pressed.add(key);
const { modifiers } = source;
// --- Platform-specific code begins here ---
// The spread is a little hack so JS gives us an array of unicode characters
// to measure.
const unmodifiedText = getKeyEventUnmodifiedText(key, source, isGrapheme);
const text = getKeyEventText(code ?? '', source) ?? unmodifiedText;
let command;
// The following commands need to be declared because Chromium doesn't
// handle them. See
// https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:third_party/blink/renderer/core/editing/editing_behavior.cc;l=169;drc=b8143cf1dfd24842890fcd831c4f5d909bef4fc4;bpv=0;bpt=1.
if (this.#isMacOS && source.meta) {
switch (code) {
case 'KeyA':
command = 'SelectAll';
break;
case 'KeyC':
command = 'Copy';
break;
case 'KeyV':
command = source.shift ? 'PasteAndMatchStyle' : 'Paste';
break;
case 'KeyX':
command = 'Cut';
break;
case 'KeyZ':
command = source.shift ? 'Redo' : 'Undo';
break;
default:
// Intentionally empty.
}
}
const promises = [
this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchKeyEvent', {
type: text ? 'keyDown' : 'rawKeyDown',
windowsVirtualKeyCode: USKeyboardLayout_js_1.KeyToKeyCode[key],
key,
code,
text,
unmodifiedText,
autoRepeat: repeat,
isSystemKey: source.alt || undefined,
location: location < 3 ? location : undefined,
isKeypad: location === 3,
modifiers,
commands: command ? [command] : undefined,
}),
];
// Drag cancelling happens on escape.
if (key === 'Escape') {
if (!source.alt &&
((this.#isMacOS && !source.ctrl && !source.meta) || !this.#isMacOS)) {
promises.push(this.#context.cdpTarget.cdpClient.sendCommand('Input.cancelDragging'));
}
}
await Promise.all(promises);
// --- Platform-specific code ends here ---
}
#dispatchKeyUpAction(source, action) {
const rawKey = action.value;
if (!(0, graphemeTools_js_1.isSingleGrapheme)(rawKey)) {
// https://w3c.github.io/webdriver/#dfn-process-a-key-action
// WebDriver spec allows a grapheme to be used.
throw new protocol_js_1.InvalidArgumentException(`Invalid key value: ${rawKey}`);
}
const isGrapheme = (0, graphemeTools_js_1.isSingleComplexGrapheme)(rawKey);
const key = (0, keyUtils_js_1.getNormalizedKey)(rawKey);
if (!source.pressed.has(key)) {
return;
}
const code = (0, keyUtils_js_1.getKeyCode)(rawKey);
const location = (0, keyUtils_js_1.getKeyLocation)(rawKey);
switch (key) {
case 'Alt':
source.alt = false;
break;
case 'Shift':
source.shift = false;
break;
case 'Control':
source.ctrl = false;
break;
case 'Meta':
source.meta = false;
break;
}
source.pressed.delete(key);
const { modifiers } = source;
// --- Platform-specific code begins here ---
// The spread is a little hack so JS gives us an array of unicode characters
// to measure.
const unmodifiedText = getKeyEventUnmodifiedText(key, source, isGrapheme);
const text = getKeyEventText(code ?? '', source) ?? unmodifiedText;
return this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchKeyEvent', {
type: 'keyUp',
windowsVirtualKeyCode: USKeyboardLayout_js_1.KeyToKeyCode[key],
key,
code,
text,
unmodifiedText,
location: location < 3 ? location : undefined,
isSystemKey: source.alt || undefined,
isKeypad: location === 3,
modifiers,
});
// --- Platform-specific code ends here ---
}
}
exports.ActionDispatcher = ActionDispatcher;
/**
* Translates a non-grapheme key to either an `undefined` for a special keys, or a single
* character modified by shift if needed.
*/
const getKeyEventUnmodifiedText = (key, source, isGrapheme) => {
if (isGrapheme) {
// Graphemes should be presented as text in the CDP command.
return key;
}
if (key === 'Enter') {
return '\r';
}
// If key is not a single character, it is a normalized key value, and should be
// presented as key, not text in the CDP command.
return [...key].length === 1
? source.shift
? key.toLocaleUpperCase('en-US')
: key
: undefined;
};
const getKeyEventText = (code, source) => {
if (source.ctrl) {
switch (code) {
case 'Digit2':
if (source.shift) {
return '\x00';
}
break;
case 'KeyA':
return '\x01';
case 'KeyB':
return '\x02';
case 'KeyC':
return '\x03';
case 'KeyD':
return '\x04';
case 'KeyE':
return '\x05';
case 'KeyF':
return '\x06';
case 'KeyG':
return '\x07';
case 'KeyH':
return '\x08';
case 'KeyI':
return '\x09';
case 'KeyJ':
return '\x0A';
case 'KeyK':
return '\x0B';
case 'KeyL':
return '\x0C';
case 'KeyM':
return '\x0D';
case 'KeyN':
return '\x0E';
case 'KeyO':
return '\x0F';
case 'KeyP':
return '\x10';
case 'KeyQ':
return '\x11';
case 'KeyR':
return '\x12';
case 'KeyS':
return '\x13';
case 'KeyT':
return '\x14';
case 'KeyU':
return '\x15';
case 'KeyV':
return '\x16';
case 'KeyW':
return '\x17';
case 'KeyX':
return '\x18';
case 'KeyY':
return '\x19';
case 'KeyZ':
return '\x1A';
case 'BracketLeft':
return '\x1B';
case 'Backslash':
return '\x1C';
case 'BracketRight':
return '\x1D';
case 'Digit6':
if (source.shift) {
return '\x1E';
}
break;
case 'Minus':
return '\x1F';
}
return '';
}
if (source.alt) {
return '';
}
return;
};
function getCdpButton(button) {
// https://www.w3.org/TR/pointerevents/#the-button-property
switch (button) {
case 0:
return 'left';
case 1:
return 'middle';
case 2:
return 'right';
case 3:
return 'back';
case 4:
return 'forward';
default:
return 'none';
}
}
function getTilt(action) {
// https://w3c.github.io/pointerevents/#converting-between-tiltx-tilty-and-altitudeangle-azimuthangle
const altitudeAngle = action.altitudeAngle ?? Math.PI / 2;
const azimuthAngle = action.azimuthAngle ?? 0;
let tiltXRadians = 0;
let tiltYRadians = 0;
if (altitudeAngle === 0) {
// the pen is in the X-Y plane
if (azimuthAngle === 0 || azimuthAngle === 2 * Math.PI) {
// pen is on positive X axis
tiltXRadians = Math.PI / 2;
}
if (azimuthAngle === Math.PI / 2) {
// pen is on positive Y axis
tiltYRadians = Math.PI / 2;
}
if (azimuthAngle === Math.PI) {
// pen is on negative X axis
tiltXRadians = -Math.PI / 2;
}
if (azimuthAngle === (3 * Math.PI) / 2) {
// pen is on negative Y axis
tiltYRadians = -Math.PI / 2;
}
if (azimuthAngle > 0 && azimuthAngle < Math.PI / 2) {
tiltXRadians = Math.PI / 2;
tiltYRadians = Math.PI / 2;
}
if (azimuthAngle > Math.PI / 2 && azimuthAngle < Math.PI) {
tiltXRadians = -Math.PI / 2;
tiltYRadians = Math.PI / 2;
}
if (azimuthAngle > Math.PI && azimuthAngle < (3 * Math.PI) / 2) {
tiltXRadians = -Math.PI / 2;
tiltYRadians = -Math.PI / 2;
}
if (azimuthAngle > (3 * Math.PI) / 2 && azimuthAngle < 2 * Math.PI) {
tiltXRadians = Math.PI / 2;
tiltYRadians = -Math.PI / 2;
}
}
if (altitudeAngle !== 0) {
const tanAlt = Math.tan(altitudeAngle);
tiltXRadians = Math.atan(Math.cos(azimuthAngle) / tanAlt);
tiltYRadians = Math.atan(Math.sin(azimuthAngle) / tanAlt);
}
const factor = 180 / Math.PI;
return {
tiltX: Math.round(tiltXRadians * factor),
tiltY: Math.round(tiltYRadians * factor),
};
}
function getRadii(width, height) {
return {
radiusX: width ? width / 2 : 0.5,
radiusY: height ? height / 2 : 0.5,
};
}
//# sourceMappingURL=ActionDispatcher.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,22 @@
/**
* 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.
*/
import type { Input } from '../../../protocol/protocol.js';
export type ActionOption = ActionOptionFor<Input.NoneSourceAction | Input.KeySourceAction | Input.PointerSourceAction | Input.WheelSourceAction>;
export interface ActionOptionFor<A> {
id: string;
action: A;
}

View File

@@ -0,0 +1,19 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=ActionOption.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ActionOption.js","sourceRoot":"","sources":["../../../../../src/bidiMapper/modules/input/ActionOption.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG"}

View File

@@ -0,0 +1,9 @@
import { Input, type EmptyResult } from '../../../protocol/protocol.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
export declare class InputProcessor {
#private;
constructor(browsingContextStorage: BrowsingContextStorage);
performActions(params: Input.PerformActionsParameters): Promise<EmptyResult>;
releaseActions(params: Input.ReleaseActionsParameters): Promise<EmptyResult>;
setFiles(params: Input.SetFilesParameters): Promise<EmptyResult>;
}

View File

@@ -0,0 +1,194 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.InputProcessor = void 0;
/*
* 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.
*/
const protocol_js_1 = require("../../../protocol/protocol.js");
const assert_js_1 = require("../../../utils/assert.js");
const ActionDispatcher_js_1 = require("../input/ActionDispatcher.js");
const InputStateManager_js_1 = require("../input/InputStateManager.js");
class InputProcessor {
#browsingContextStorage;
#inputStateManager = new InputStateManager_js_1.InputStateManager();
constructor(browsingContextStorage) {
this.#browsingContextStorage = browsingContextStorage;
}
async performActions(params) {
const context = this.#browsingContextStorage.getContext(params.context);
const inputState = this.#inputStateManager.get(context.top);
const actionsByTick = this.#getActionsByTick(params, inputState);
const dispatcher = new ActionDispatcher_js_1.ActionDispatcher(inputState, this.#browsingContextStorage, params.context, await ActionDispatcher_js_1.ActionDispatcher.isMacOS(context).catch(() => false));
await dispatcher.dispatchActions(actionsByTick);
return {};
}
async releaseActions(params) {
const context = this.#browsingContextStorage.getContext(params.context);
const topContext = context.top;
const inputState = this.#inputStateManager.get(topContext);
const dispatcher = new ActionDispatcher_js_1.ActionDispatcher(inputState, this.#browsingContextStorage, params.context, await ActionDispatcher_js_1.ActionDispatcher.isMacOS(context).catch(() => false));
await dispatcher.dispatchTickActions(inputState.cancelList.reverse());
this.#inputStateManager.delete(topContext);
return {};
}
async setFiles(params) {
const context = this.#browsingContextStorage.getContext(params.context);
const hiddenSandboxRealm = await context.getOrCreateHiddenSandbox();
let result;
try {
result = await hiddenSandboxRealm.callFunction(String(function getFiles(fileListLength) {
if (!(this instanceof HTMLInputElement)) {
if (this instanceof Element) {
return 1 /* ErrorCode.Element */;
}
return 0 /* ErrorCode.Node */;
}
if (this.type !== 'file') {
return 2 /* ErrorCode.Type */;
}
if (this.disabled) {
return 3 /* ErrorCode.Disabled */;
}
if (fileListLength > 1 && !this.multiple) {
return 4 /* ErrorCode.Multiple */;
}
return;
}), false, params.element, [{ type: 'number', value: params.files.length }]);
}
catch {
throw new protocol_js_1.NoSuchNodeException(`Could not find element ${params.element.sharedId}`);
}
(0, assert_js_1.assert)(result.type === 'success');
if (result.result.type === 'number') {
switch (result.result.value) {
case 0 /* ErrorCode.Node */: {
throw new protocol_js_1.NoSuchElementException(`Could not find element ${params.element.sharedId}`);
}
case 1 /* ErrorCode.Element */: {
throw new protocol_js_1.UnableToSetFileInputException(`Element ${params.element.sharedId} is not a input`);
}
case 2 /* ErrorCode.Type */: {
throw new protocol_js_1.UnableToSetFileInputException(`Input element ${params.element.sharedId} is not a file type`);
}
case 3 /* ErrorCode.Disabled */: {
throw new protocol_js_1.UnableToSetFileInputException(`Input element ${params.element.sharedId} is disabled`);
}
case 4 /* ErrorCode.Multiple */: {
throw new protocol_js_1.UnableToSetFileInputException(`Cannot set multiple files on a non-multiple input element`);
}
}
}
/**
* The zero-length array is a special case, it seems that
* DOM.setFileInputFiles does not actually update the files in that case, so
* the solution is to eval the element value to a new FileList directly.
*/
if (params.files.length === 0) {
// XXX: These events should converted to trusted events. Perhaps do this
// in `DOM.setFileInputFiles`?
await hiddenSandboxRealm.callFunction(String(function dispatchEvent() {
if (this.files?.length === 0) {
this.dispatchEvent(new Event('cancel', {
bubbles: true,
}));
return;
}
this.files = new DataTransfer().files;
// Dispatch events for this case because it should behave akin to a user action.
this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
this.dispatchEvent(new Event('change', { bubbles: true }));
}), false, params.element);
return {};
}
// Our goal here is to iterate over the input element files and get their
// file paths.
const paths = [];
for (let i = 0; i < params.files.length; ++i) {
const result = await hiddenSandboxRealm.callFunction(String(function getFiles(index) {
return this.files?.item(index);
}), false, params.element, [{ type: 'number', value: 0 }], "root" /* Script.ResultOwnership.Root */);
(0, assert_js_1.assert)(result.type === 'success');
if (result.result.type !== 'object') {
break;
}
const { handle } = result.result;
(0, assert_js_1.assert)(handle !== undefined);
const { path } = await hiddenSandboxRealm.cdpClient.sendCommand('DOM.getFileInfo', {
objectId: handle,
});
paths.push(path);
// Cleanup the handle.
void hiddenSandboxRealm.disown(handle).catch(undefined);
}
paths.sort();
// We create a new array so we preserve the order of the original files.
const sortedFiles = [...params.files].sort();
if (paths.length !== params.files.length ||
sortedFiles.some((path, index) => {
return paths[index] !== path;
})) {
const { objectId } = await hiddenSandboxRealm.deserializeForCdp(params.element);
// This cannot throw since this was just used in `callFunction` above.
(0, assert_js_1.assert)(objectId !== undefined);
await hiddenSandboxRealm.cdpClient.sendCommand('DOM.setFileInputFiles', {
files: params.files,
objectId,
});
}
else {
// XXX: We should dispatch a trusted event.
await hiddenSandboxRealm.callFunction(String(function dispatchEvent() {
this.dispatchEvent(new Event('cancel', {
bubbles: true,
}));
}), false, params.element);
}
return {};
}
#getActionsByTick(params, inputState) {
const actionsByTick = [];
for (const action of params.actions) {
switch (action.type) {
case "pointer" /* SourceType.Pointer */: {
action.parameters ??= { pointerType: "mouse" /* Input.PointerType.Mouse */ };
action.parameters.pointerType ??= "mouse" /* Input.PointerType.Mouse */;
const source = inputState.getOrCreate(action.id, "pointer" /* SourceType.Pointer */, action.parameters.pointerType);
if (source.subtype !== action.parameters.pointerType) {
throw new protocol_js_1.InvalidArgumentException(`Expected input source ${action.id} to be ${source.subtype}; got ${action.parameters.pointerType}.`);
}
// https://github.com/GoogleChromeLabs/chromium-bidi/issues/3043
source.resetClickCount();
break;
}
default:
inputState.getOrCreate(action.id, action.type);
}
const actions = action.actions.map((item) => ({
id: action.id,
action: item,
}));
for (let i = 0; i < actions.length; i++) {
if (actionsByTick.length === i) {
actionsByTick.push([]);
}
actionsByTick[i].push(actions[i]);
}
}
return actionsByTick;
}
}
exports.InputProcessor = InputProcessor;
//# sourceMappingURL=InputProcessor.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,78 @@
/**
* 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.
*/
import type { Input } from '../../../protocol/protocol.js';
export declare const enum SourceType {
Key = "key",
Pointer = "pointer",
Wheel = "wheel",
None = "none"
}
export declare class NoneSource {
type: SourceType.None;
}
export declare class KeySource {
#private;
type: SourceType.Key;
pressed: Set<string>;
get modifiers(): number;
get alt(): boolean;
set alt(value: boolean);
get ctrl(): boolean;
set ctrl(value: boolean);
get meta(): boolean;
set meta(value: boolean);
get shift(): boolean;
set shift(value: boolean);
}
export declare class PointerSource {
#private;
type: SourceType.Pointer;
subtype: Input.PointerType;
pointerId: number;
pressed: Set<number>;
x: number;
y: number;
radiusX?: number;
radiusY?: number;
force?: number;
constructor(id: number, subtype: Input.PointerType);
get buttons(): number;
static ClickContext: {
new (x: number, y: number, time: number): {
count: number;
"__#private@#x": number;
"__#private@#y": number;
"__#private@#time": number;
compare(context: /*elided*/ any): boolean;
};
"__#private@#DOUBLE_CLICK_TIME_MS": number;
"__#private@#MAX_DOUBLE_CLICK_RADIUS": number;
};
setClickCount(button: number, context: InstanceType<typeof PointerSource.ClickContext>): number;
getClickCount(button: number): number;
/**
* Resets click count. Resets consequent click counter. Prevents grouping clicks in
* different `performActions` calls, so that they are not grouped as double, triple etc
* clicks. Required for https://github.com/GoogleChromeLabs/chromium-bidi/issues/3043.
*/
resetClickCount(): void;
}
export declare class WheelSource {
type: SourceType.Wheel;
}
export type InputSource = NoneSource | KeySource | PointerSource | WheelSource;
export type InputSourceFor<Type extends SourceType> = Type extends SourceType.Key ? KeySource : Type extends SourceType.Pointer ? PointerSource : Type extends SourceType.Wheel ? WheelSource : NoneSource;

View File

@@ -0,0 +1,161 @@
"use strict";
/**
* 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 _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.WheelSource = exports.PointerSource = exports.KeySource = exports.NoneSource = void 0;
class NoneSource {
type = "none" /* SourceType.None */;
}
exports.NoneSource = NoneSource;
class KeySource {
type = "key" /* SourceType.Key */;
pressed = new Set();
// This is a bitfield that matches the modifiers parameter of
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchKeyEvent
#modifiers = 0;
get modifiers() {
return this.#modifiers;
}
get alt() {
return (this.#modifiers & 1) === 1;
}
set alt(value) {
this.#setModifier(value, 1);
}
get ctrl() {
return (this.#modifiers & 2) === 2;
}
set ctrl(value) {
this.#setModifier(value, 2);
}
get meta() {
return (this.#modifiers & 4) === 4;
}
set meta(value) {
this.#setModifier(value, 4);
}
get shift() {
return (this.#modifiers & 8) === 8;
}
set shift(value) {
this.#setModifier(value, 8);
}
#setModifier(value, bit) {
if (value) {
this.#modifiers |= bit;
}
else {
this.#modifiers &= ~bit;
}
}
}
exports.KeySource = KeySource;
class PointerSource {
type = "pointer" /* SourceType.Pointer */;
subtype;
pointerId;
pressed = new Set();
x = 0;
y = 0;
radiusX;
radiusY;
force;
constructor(id, subtype) {
this.pointerId = id;
this.subtype = subtype;
}
// This is a bitfield that matches the buttons parameter of
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent
get buttons() {
let buttons = 0;
for (const button of this.pressed) {
switch (button) {
case 0:
buttons |= 1;
break;
case 1:
buttons |= 4;
break;
case 2:
buttons |= 2;
break;
case 3:
buttons |= 8;
break;
case 4:
buttons |= 16;
break;
}
}
return buttons;
}
// --- Platform-specific code starts here ---
// Input.dispatchMouseEvent doesn't know the concept of double click, so we
// need to create the logic, similar to how it's done for OSes:
// https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:ui/events/event.cc;l=479
static ClickContext = class ClickContext {
static #DOUBLE_CLICK_TIME_MS = 500;
static #MAX_DOUBLE_CLICK_RADIUS = 2;
count = 0;
#x;
#y;
#time;
constructor(x, y, time) {
this.#x = x;
this.#y = y;
this.#time = time;
}
compare(context) {
return (
// The click needs to be within a certain amount of ms.
context.#time - this.#time > ClickContext.#DOUBLE_CLICK_TIME_MS ||
// The click needs to be within a certain square radius.
Math.abs(context.#x - this.#x) >
ClickContext.#MAX_DOUBLE_CLICK_RADIUS ||
Math.abs(context.#y - this.#y) > ClickContext.#MAX_DOUBLE_CLICK_RADIUS);
}
};
#clickContexts = new Map();
setClickCount(button, context) {
let storedContext = this.#clickContexts.get(button);
if (!storedContext || storedContext.compare(context)) {
storedContext = context;
}
++storedContext.count;
this.#clickContexts.set(button, storedContext);
return storedContext.count;
}
getClickCount(button) {
return this.#clickContexts.get(button)?.count ?? 0;
}
/**
* Resets click count. Resets consequent click counter. Prevents grouping clicks in
* different `performActions` calls, so that they are not grouped as double, triple etc
* clicks. Required for https://github.com/GoogleChromeLabs/chromium-bidi/issues/3043.
*/
resetClickCount() {
this.#clickContexts = new Map();
}
}
exports.PointerSource = PointerSource;
_a = PointerSource;
class WheelSource {
type = "wheel" /* SourceType.Wheel */;
}
exports.WheelSource = WheelSource;
//# sourceMappingURL=InputSource.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"InputSource.js","sourceRoot":"","sources":["../../../../../src/bidiMapper/modules/input/InputSource.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,29 @@
/**
* 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.
*/
import { Input } from '../../../protocol/protocol.js';
import { Mutex } from '../../../utils/Mutex.js';
import type { ActionOption } from './ActionOption.js';
import { KeySource, PointerSource, SourceType, type InputSource, type InputSourceFor } from './InputSource.js';
export declare class InputState {
#private;
cancelList: ActionOption[];
getOrCreate(id: string, type: SourceType.Pointer, subtype: Input.PointerType): PointerSource;
getOrCreate<Type extends SourceType>(id: string, type: Type): InputSourceFor<Type>;
get(id: string): InputSource;
getGlobalKeyState(): KeySource;
get queue(): Mutex;
}

View File

@@ -0,0 +1,93 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.InputState = void 0;
const protocol_js_1 = require("../../../protocol/protocol.js");
const Mutex_js_1 = require("../../../utils/Mutex.js");
const InputSource_js_1 = require("./InputSource.js");
class InputState {
cancelList = [];
#sources = new Map();
#mutex = new Mutex_js_1.Mutex();
getOrCreate(id, type, subtype) {
let source = this.#sources.get(id);
if (!source) {
switch (type) {
case "none" /* SourceType.None */:
source = new InputSource_js_1.NoneSource();
break;
case "key" /* SourceType.Key */:
source = new InputSource_js_1.KeySource();
break;
case "pointer" /* SourceType.Pointer */: {
let pointerId = subtype === "mouse" /* Input.PointerType.Mouse */ ? 0 : 2;
const pointerIds = new Set();
for (const [, source] of this.#sources) {
if (source.type === "pointer" /* SourceType.Pointer */) {
pointerIds.add(source.pointerId);
}
}
while (pointerIds.has(pointerId)) {
++pointerId;
}
source = new InputSource_js_1.PointerSource(pointerId, subtype);
break;
}
case "wheel" /* SourceType.Wheel */:
source = new InputSource_js_1.WheelSource();
break;
default:
throw new protocol_js_1.InvalidArgumentException(`Expected "${"none" /* SourceType.None */}", "${"key" /* SourceType.Key */}", "${"pointer" /* SourceType.Pointer */}", or "${"wheel" /* SourceType.Wheel */}". Found unknown source type ${type}.`);
}
this.#sources.set(id, source);
return source;
}
if (source.type !== type) {
throw new protocol_js_1.InvalidArgumentException(`Input source type of ${id} is ${source.type}, but received ${type}.`);
}
return source;
}
get(id) {
const source = this.#sources.get(id);
if (!source) {
throw new protocol_js_1.UnknownErrorException(`Internal error.`);
}
return source;
}
getGlobalKeyState() {
const state = new InputSource_js_1.KeySource();
for (const [, source] of this.#sources) {
if (source.type !== "key" /* SourceType.Key */) {
continue;
}
for (const pressed of source.pressed) {
state.pressed.add(pressed);
}
state.alt ||= source.alt;
state.ctrl ||= source.ctrl;
state.meta ||= source.meta;
state.shift ||= source.shift;
}
return state;
}
get queue() {
return this.#mutex;
}
}
exports.InputState = InputState;
//# sourceMappingURL=InputState.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"InputState.js","sourceRoot":"","sources":["../../../../../src/bidiMapper/modules/input/InputState.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,21 @@
/**
* 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.
*/
import type { BrowsingContextImpl } from '../context/BrowsingContextImpl.js';
import { InputState } from './InputState.js';
export declare class InputStateManager extends WeakMap<BrowsingContextImpl, InputState> {
get(context: BrowsingContextImpl): InputState;
}

View File

@@ -0,0 +1,34 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.InputStateManager = void 0;
const assert_js_1 = require("../../../utils/assert.js");
const InputState_js_1 = require("./InputState.js");
// We use a weak map here as specified here:
// https://www.w3.org/TR/webdriver/#dfn-browsing-context-input-state-map
class InputStateManager extends WeakMap {
get(context) {
(0, assert_js_1.assert)(context.isTopLevelContext());
if (!this.has(context)) {
this.set(context, new InputState_js_1.InputState());
}
return super.get(context);
}
}
exports.InputStateManager = InputStateManager;
//# sourceMappingURL=InputStateManager.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"InputStateManager.js","sourceRoot":"","sources":["../../../../../src/bidiMapper/modules/input/InputStateManager.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;;AAEH,wDAAgD;AAGhD,mDAA2C;AAE3C,4CAA4C;AAC5C,wEAAwE;AACxE,MAAa,iBAAkB,SAAQ,OAGtC;IACU,GAAG,CAAC,OAA4B;QACvC,IAAA,kBAAM,EAAC,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAC;QAEpC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YACvB,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,0BAAU,EAAE,CAAC,CAAC;QACtC,CAAC;QAED,OAAO,KAAK,CAAC,GAAG,CAAC,OAAO,CAAE,CAAC;IAC7B,CAAC;CACF;AAbD,8CAaC"}

View File

@@ -0,0 +1,17 @@
/**
* 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.
*/
export declare const KeyToKeyCode: Record<string, number | undefined>;

View File

@@ -0,0 +1,274 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.KeyToKeyCode = void 0;
// TODO: Remove this once https://crrev.com/c/4548290 is stably in Chromium.
// `Input.dispatchKeyboardEvent` will automatically handle these conversions.
exports.KeyToKeyCode = {
'0': 48,
'1': 49,
'2': 50,
'3': 51,
'4': 52,
'5': 53,
'6': 54,
'7': 55,
'8': 56,
'9': 57,
Abort: 3,
Help: 6,
Backspace: 8,
Tab: 9,
Numpad5: 12,
NumpadEnter: 13,
Enter: 13,
'\\r': 13,
'\\n': 13,
ShiftLeft: 16,
ShiftRight: 16,
ControlLeft: 17,
ControlRight: 17,
AltLeft: 18,
AltRight: 18,
Pause: 19,
CapsLock: 20,
Escape: 27,
Convert: 28,
NonConvert: 29,
Space: 32,
Numpad9: 33,
PageUp: 33,
Numpad3: 34,
PageDown: 34,
End: 35,
Numpad1: 35,
Home: 36,
Numpad7: 36,
ArrowLeft: 37,
Numpad4: 37,
Numpad8: 38,
ArrowUp: 38,
ArrowRight: 39,
Numpad6: 39,
Numpad2: 40,
ArrowDown: 40,
Select: 41,
Open: 43,
PrintScreen: 44,
Insert: 45,
Numpad0: 45,
Delete: 46,
NumpadDecimal: 46,
Digit0: 48,
Digit1: 49,
Digit2: 50,
Digit3: 51,
Digit4: 52,
Digit5: 53,
Digit6: 54,
Digit7: 55,
Digit8: 56,
Digit9: 57,
KeyA: 65,
KeyB: 66,
KeyC: 67,
KeyD: 68,
KeyE: 69,
KeyF: 70,
KeyG: 71,
KeyH: 72,
KeyI: 73,
KeyJ: 74,
KeyK: 75,
KeyL: 76,
KeyM: 77,
KeyN: 78,
KeyO: 79,
KeyP: 80,
KeyQ: 81,
KeyR: 82,
KeyS: 83,
KeyT: 84,
KeyU: 85,
KeyV: 86,
KeyW: 87,
KeyX: 88,
KeyY: 89,
KeyZ: 90,
MetaLeft: 91,
MetaRight: 92,
ContextMenu: 93,
NumpadMultiply: 106,
NumpadAdd: 107,
NumpadSubtract: 109,
NumpadDivide: 111,
F1: 112,
F2: 113,
F3: 114,
F4: 115,
F5: 116,
F6: 117,
F7: 118,
F8: 119,
F9: 120,
F10: 121,
F11: 122,
F12: 123,
F13: 124,
F14: 125,
F15: 126,
F16: 127,
F17: 128,
F18: 129,
F19: 130,
F20: 131,
F21: 132,
F22: 133,
F23: 134,
F24: 135,
NumLock: 144,
ScrollLock: 145,
AudioVolumeMute: 173,
AudioVolumeDown: 174,
AudioVolumeUp: 175,
MediaTrackNext: 176,
MediaTrackPrevious: 177,
MediaStop: 178,
MediaPlayPause: 179,
Semicolon: 186,
Equal: 187,
NumpadEqual: 187,
Comma: 188,
Minus: 189,
Period: 190,
Slash: 191,
Backquote: 192,
BracketLeft: 219,
Backslash: 220,
BracketRight: 221,
Quote: 222,
AltGraph: 225,
Props: 247,
Cancel: 3,
Clear: 12,
Shift: 16,
Control: 17,
Alt: 18,
Accept: 30,
ModeChange: 31,
' ': 32,
Print: 42,
Execute: 43,
'\\u0000': 46,
a: 65,
b: 66,
c: 67,
d: 68,
e: 69,
f: 70,
g: 71,
h: 72,
i: 73,
j: 74,
k: 75,
l: 76,
m: 77,
n: 78,
o: 79,
p: 80,
q: 81,
r: 82,
s: 83,
t: 84,
u: 85,
v: 86,
w: 87,
x: 88,
y: 89,
z: 90,
Meta: 91,
'*': 106,
'+': 107,
'-': 109,
'/': 111,
';': 186,
'=': 187,
',': 188,
'.': 190,
'`': 192,
'[': 219,
'\\\\': 220,
']': 221,
"'": 222,
Attn: 246,
CrSel: 247,
ExSel: 248,
EraseEof: 249,
Play: 250,
ZoomOut: 251,
')': 48,
'!': 49,
'@': 50,
'#': 51,
$: 52,
'%': 53,
'^': 54,
'&': 55,
'(': 57,
A: 65,
B: 66,
C: 67,
D: 68,
E: 69,
F: 70,
G: 71,
H: 72,
I: 73,
J: 74,
K: 75,
L: 76,
M: 77,
N: 78,
O: 79,
P: 80,
Q: 81,
R: 82,
S: 83,
T: 84,
U: 85,
V: 86,
W: 87,
X: 88,
Y: 89,
Z: 90,
':': 186,
'<': 188,
_: 189,
'>': 190,
'?': 191,
'~': 192,
'{': 219,
'|': 220,
'}': 221,
'"': 222,
Camera: 44,
EndCall: 95,
VolumeDown: 182,
VolumeUp: 183,
};
//# sourceMappingURL=USKeyboardLayout.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,31 @@
/**
* 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.
*/
/**
* Returns the normalized key value for a given key according to the table:
* https://w3c.github.io/webdriver/#dfn-normalized-key-value
*/
export declare function getNormalizedKey(value: string): string;
/**
* Returns the key code for a given key according to the table:
* https://w3c.github.io/webdriver/#dfn-shifted-character
*/
export declare function getKeyCode(key: string): string | undefined;
/**
* Returns the location of the key according to the table:
* https://w3c.github.io/webdriver/#dfn-key-location
*/
export declare function getKeyLocation(key: string): 0 | 1 | 2 | 3;

View File

@@ -0,0 +1,497 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.getNormalizedKey = getNormalizedKey;
exports.getKeyCode = getKeyCode;
exports.getKeyLocation = getKeyLocation;
/**
* Returns the normalized key value for a given key according to the table:
* https://w3c.github.io/webdriver/#dfn-normalized-key-value
*/
function getNormalizedKey(value) {
switch (value) {
case '\uE000':
return 'Unidentified';
case '\uE001':
return 'Cancel';
case '\uE002':
return 'Help';
case '\uE003':
return 'Backspace';
case '\uE004':
return 'Tab';
case '\uE005':
return 'Clear';
// Specification declares the '\uE006' to be `Return`, but it is not supported by
// Chrome, so fall back to `Enter`, which aligns with WPT.
case '\uE006':
case '\uE007':
return 'Enter';
case '\uE008':
return 'Shift';
case '\uE009':
return 'Control';
case '\uE00A':
return 'Alt';
case '\uE00B':
return 'Pause';
case '\uE00C':
return 'Escape';
case '\uE00D':
return ' ';
case '\uE00E':
return 'PageUp';
case '\uE00F':
return 'PageDown';
case '\uE010':
return 'End';
case '\uE011':
return 'Home';
case '\uE012':
return 'ArrowLeft';
case '\uE013':
return 'ArrowUp';
case '\uE014':
return 'ArrowRight';
case '\uE015':
return 'ArrowDown';
case '\uE016':
return 'Insert';
case '\uE017':
return 'Delete';
case '\uE018':
return ';';
case '\uE019':
return '=';
case '\uE01A':
return '0';
case '\uE01B':
return '1';
case '\uE01C':
return '2';
case '\uE01D':
return '3';
case '\uE01E':
return '4';
case '\uE01F':
return '5';
case '\uE020':
return '6';
case '\uE021':
return '7';
case '\uE022':
return '8';
case '\uE023':
return '9';
case '\uE024':
return '*';
case '\uE025':
return '+';
case '\uE026':
return ',';
case '\uE027':
return '-';
case '\uE028':
return '.';
case '\uE029':
return '/';
case '\uE031':
return 'F1';
case '\uE032':
return 'F2';
case '\uE033':
return 'F3';
case '\uE034':
return 'F4';
case '\uE035':
return 'F5';
case '\uE036':
return 'F6';
case '\uE037':
return 'F7';
case '\uE038':
return 'F8';
case '\uE039':
return 'F9';
case '\uE03A':
return 'F10';
case '\uE03B':
return 'F11';
case '\uE03C':
return 'F12';
case '\uE03D':
return 'Meta';
case '\uE040':
return 'ZenkakuHankaku';
case '\uE050':
return 'Shift';
case '\uE051':
return 'Control';
case '\uE052':
return 'Alt';
case '\uE053':
return 'Meta';
case '\uE054':
return 'PageUp';
case '\uE055':
return 'PageDown';
case '\uE056':
return 'End';
case '\uE057':
return 'Home';
case '\uE058':
return 'ArrowLeft';
case '\uE059':
return 'ArrowUp';
case '\uE05A':
return 'ArrowRight';
case '\uE05B':
return 'ArrowDown';
case '\uE05C':
return 'Insert';
case '\uE05D':
return 'Delete';
default:
return value;
}
}
/**
* Returns the key code for a given key according to the table:
* https://w3c.github.io/webdriver/#dfn-shifted-character
*/
function getKeyCode(key) {
switch (key) {
case '`':
case '~':
return 'Backquote';
case '\\':
case '|':
return 'Backslash';
case '\uE003':
return 'Backspace';
case '[':
case '{':
return 'BracketLeft';
case ']':
case '}':
return 'BracketRight';
case ',':
case '<':
return 'Comma';
case '0':
case ')':
return 'Digit0';
case '1':
case '!':
return 'Digit1';
case '2':
case '@':
return 'Digit2';
case '3':
case '#':
return 'Digit3';
case '4':
case '$':
return 'Digit4';
case '5':
case '%':
return 'Digit5';
case '6':
case '^':
return 'Digit6';
case '7':
case '&':
return 'Digit7';
case '8':
case '*':
return 'Digit8';
case '9':
case '(':
return 'Digit9';
case '=':
case '+':
return 'Equal';
// The spec declares the '<' to be `IntlBackslash` as well, but it is already covered
// in the `Comma` above.
case '>':
return 'IntlBackslash';
case 'a':
case 'A':
return 'KeyA';
case 'b':
case 'B':
return 'KeyB';
case 'c':
case 'C':
return 'KeyC';
case 'd':
case 'D':
return 'KeyD';
case 'e':
case 'E':
return 'KeyE';
case 'f':
case 'F':
return 'KeyF';
case 'g':
case 'G':
return 'KeyG';
case 'h':
case 'H':
return 'KeyH';
case 'i':
case 'I':
return 'KeyI';
case 'j':
case 'J':
return 'KeyJ';
case 'k':
case 'K':
return 'KeyK';
case 'l':
case 'L':
return 'KeyL';
case 'm':
case 'M':
return 'KeyM';
case 'n':
case 'N':
return 'KeyN';
case 'o':
case 'O':
return 'KeyO';
case 'p':
case 'P':
return 'KeyP';
case 'q':
case 'Q':
return 'KeyQ';
case 'r':
case 'R':
return 'KeyR';
case 's':
case 'S':
return 'KeyS';
case 't':
case 'T':
return 'KeyT';
case 'u':
case 'U':
return 'KeyU';
case 'v':
case 'V':
return 'KeyV';
case 'w':
case 'W':
return 'KeyW';
case 'x':
case 'X':
return 'KeyX';
case 'y':
case 'Y':
return 'KeyY';
case 'z':
case 'Z':
return 'KeyZ';
case '-':
case '_':
return 'Minus';
case '.':
return 'Period';
case "'":
case '"':
return 'Quote';
case ';':
case ':':
return 'Semicolon';
case '/':
case '?':
return 'Slash';
case '\uE00A':
return 'AltLeft';
case '\uE052':
return 'AltRight';
case '\uE009':
return 'ControlLeft';
case '\uE051':
return 'ControlRight';
case '\uE006':
return 'Enter';
case '\uE00B':
return 'Pause';
case '\uE03D':
return 'MetaLeft';
case '\uE053':
return 'MetaRight';
case '\uE008':
return 'ShiftLeft';
case '\uE050':
return 'ShiftRight';
case ' ':
case '\uE00D':
return 'Space';
case '\uE004':
return 'Tab';
case '\uE017':
return 'Delete';
case '\uE010':
return 'End';
case '\uE002':
return 'Help';
case '\uE011':
return 'Home';
case '\uE016':
return 'Insert';
case '\uE00F':
return 'PageDown';
case '\uE00E':
return 'PageUp';
case '\uE015':
return 'ArrowDown';
case '\uE012':
return 'ArrowLeft';
case '\uE014':
return 'ArrowRight';
case '\uE013':
return 'ArrowUp';
case '\uE00C':
return 'Escape';
case '\uE031':
return 'F1';
case '\uE032':
return 'F2';
case '\uE033':
return 'F3';
case '\uE034':
return 'F4';
case '\uE035':
return 'F5';
case '\uE036':
return 'F6';
case '\uE037':
return 'F7';
case '\uE038':
return 'F8';
case '\uE039':
return 'F9';
case '\uE03A':
return 'F10';
case '\uE03B':
return 'F11';
case '\uE03C':
return 'F12';
case '\uE019':
return 'NumpadEqual';
case '\uE01A':
case '\uE05C':
return 'Numpad0';
case '\uE01B':
case '\uE056':
return 'Numpad1';
case '\uE01C':
case '\uE05B':
return 'Numpad2';
case '\uE01D':
case '\uE055':
return 'Numpad3';
case '\uE01E':
case '\uE058':
return 'Numpad4';
case '\uE01F':
return 'Numpad5';
case '\uE020':
case '\uE05A':
return 'Numpad6';
case '\uE021':
case '\uE057':
return 'Numpad7';
case '\uE022':
case '\uE059':
return 'Numpad8';
case '\uE023':
case '\uE054':
return 'Numpad9';
case '\uE025':
return 'NumpadAdd';
case '\uE026':
return 'NumpadComma';
case '\uE028':
case '\uE05D':
return 'NumpadDecimal';
case '\uE029':
return 'NumpadDivide';
case '\uE007':
return 'NumpadEnter';
case '\uE024':
return 'NumpadMultiply';
case '\uE027':
return 'NumpadSubtract';
default:
return;
}
}
/**
* Returns the location of the key according to the table:
* https://w3c.github.io/webdriver/#dfn-key-location
*/
function getKeyLocation(key) {
switch (key) {
case '\uE007':
case '\uE008':
case '\uE009':
case '\uE00A':
case '\uE03D':
return 1;
case '\uE019':
case '\uE01A':
case '\uE01B':
case '\uE01C':
case '\uE01D':
case '\uE01E':
case '\uE01F':
case '\uE020':
case '\uE021':
case '\uE022':
case '\uE023':
case '\uE024':
case '\uE025':
case '\uE026':
case '\uE027':
case '\uE028':
case '\uE029':
case '\uE054':
case '\uE055':
case '\uE056':
case '\uE057':
case '\uE058':
case '\uE059':
case '\uE05A':
case '\uE05B':
case '\uE05C':
case '\uE05D':
return 3;
case '\uE050':
case '\uE051':
case '\uE052':
case '\uE053':
return 2;
default:
return 0;
}
}
//# sourceMappingURL=keyUtils.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,9 @@
import { type LoggerFn } from '../../../utils/log.js';
import type { CdpTarget } from '../cdp/CdpTarget.js';
import type { RealmStorage } from '../script/RealmStorage.js';
import type { EventManager } from '../session/EventManager.js';
export declare class LogManager {
#private;
private constructor();
static create(cdpTarget: CdpTarget, realmStorage: RealmStorage, eventManager: EventManager, logger?: LoggerFn): LogManager;
}

View File

@@ -0,0 +1,187 @@
"use strict";
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.LogManager = void 0;
const protocol_js_1 = require("../../../protocol/protocol.js");
const log_js_1 = require("../../../utils/log.js");
const logHelper_js_1 = require("./logHelper.js");
/** Converts CDP StackTrace object to BiDi StackTrace object. */
function getBidiStackTrace(cdpStackTrace) {
const stackFrames = cdpStackTrace?.callFrames.map((callFrame) => {
return {
columnNumber: callFrame.columnNumber,
functionName: callFrame.functionName,
lineNumber: callFrame.lineNumber,
url: callFrame.url,
};
});
return stackFrames ? { callFrames: stackFrames } : undefined;
}
function getLogLevel(consoleApiType) {
if (["error" /* Log.Level.Error */, 'assert'].includes(consoleApiType)) {
return "error" /* Log.Level.Error */;
}
if (["debug" /* Log.Level.Debug */, 'trace'].includes(consoleApiType)) {
return "debug" /* Log.Level.Debug */;
}
if (["warn" /* Log.Level.Warn */, 'warning'].includes(consoleApiType)) {
return "warn" /* Log.Level.Warn */;
}
return "info" /* Log.Level.Info */;
}
function getLogMethod(consoleApiType) {
switch (consoleApiType) {
case 'warning':
return 'warn';
case 'startGroup':
return 'group';
case 'startGroupCollapsed':
return 'groupCollapsed';
case 'endGroup':
return 'groupEnd';
}
return consoleApiType;
}
class LogManager {
#eventManager;
#realmStorage;
#cdpTarget;
#logger;
constructor(cdpTarget, realmStorage, eventManager, logger) {
this.#cdpTarget = cdpTarget;
this.#realmStorage = realmStorage;
this.#eventManager = eventManager;
this.#logger = logger;
}
static create(cdpTarget, realmStorage, eventManager, logger) {
const logManager = new _a(cdpTarget, realmStorage, eventManager, logger);
logManager.#initializeEntryAddedEventListener();
return logManager;
}
/**
* Heuristic serialization of CDP remote object. If possible, return the BiDi value
* without deep serialization.
*/
async #heuristicSerializeArg(arg, realm) {
switch (arg.type) {
// TODO: Implement regexp, array, object, map and set heuristics base on
// preview.
case 'undefined':
return { type: 'undefined' };
case 'boolean':
return { type: 'boolean', value: arg.value };
case 'string':
return { type: 'string', value: arg.value };
case 'number':
// The value can be either a number or a string like `Infinity` or `-0`.
return { type: 'number', value: arg.unserializableValue ?? arg.value };
case 'bigint':
if (arg.unserializableValue !== undefined &&
arg.unserializableValue[arg.unserializableValue.length - 1] === 'n') {
return {
type: arg.type,
value: arg.unserializableValue.slice(0, -1),
};
}
// Unexpected bigint value, fall back to CDP deep serialization.
break;
case 'object':
if (arg.subtype === 'null') {
return { type: 'null' };
}
// Fall back to CDP deep serialization.
break;
default:
// Fall back to CDP deep serialization.
break;
}
// Fall back to CDP deep serialization.
return await realm.serializeCdpObject(arg, "none" /* Script.ResultOwnership.None */);
}
#initializeEntryAddedEventListener() {
this.#cdpTarget.cdpClient.on('Runtime.consoleAPICalled', (params) => {
// Try to find realm by `cdpSessionId` and `executionContextId`,
// if provided.
const realm = this.#realmStorage.findRealm({
cdpSessionId: this.#cdpTarget.cdpSessionId,
executionContextId: params.executionContextId,
});
if (realm === undefined) {
// Ignore exceptions not attached to any realm.
this.#logger?.(log_js_1.LogType.cdp, params);
return;
}
const argsPromise = Promise.all(params.args.map((arg) => this.#heuristicSerializeArg(arg, realm)));
for (const browsingContext of realm.associatedBrowsingContexts) {
this.#eventManager.registerPromiseEvent(argsPromise.then((args) => ({
kind: 'success',
value: {
type: 'event',
method: protocol_js_1.ChromiumBidi.Log.EventNames.LogEntryAdded,
params: {
level: getLogLevel(params.type),
source: realm.source,
text: (0, logHelper_js_1.getRemoteValuesText)(args, true),
timestamp: Math.round(params.timestamp),
stackTrace: getBidiStackTrace(params.stackTrace),
type: 'console',
method: getLogMethod(params.type),
args,
},
},
}), (error) => ({
kind: 'error',
error,
})), browsingContext.id, protocol_js_1.ChromiumBidi.Log.EventNames.LogEntryAdded);
}
});
this.#cdpTarget.cdpClient.on('Runtime.exceptionThrown', (params) => {
// Try to find realm by `cdpSessionId` and `executionContextId`,
// if provided.
const realm = this.#realmStorage.findRealm({
cdpSessionId: this.#cdpTarget.cdpSessionId,
executionContextId: params.exceptionDetails.executionContextId,
});
if (realm === undefined) {
// Ignore exceptions not attached to any realm.
this.#logger?.(log_js_1.LogType.cdp, params);
return;
}
for (const browsingContext of realm.associatedBrowsingContexts) {
this.#eventManager.registerPromiseEvent(_a.#getExceptionText(params, realm).then((text) => ({
kind: 'success',
value: {
type: 'event',
method: protocol_js_1.ChromiumBidi.Log.EventNames.LogEntryAdded,
params: {
level: "error" /* Log.Level.Error */,
source: realm.source,
text,
timestamp: Math.round(params.timestamp),
stackTrace: getBidiStackTrace(params.exceptionDetails.stackTrace),
type: 'javascript',
},
},
}), (error) => ({
kind: 'error',
error,
})), browsingContext.id, protocol_js_1.ChromiumBidi.Log.EventNames.LogEntryAdded);
}
});
}
/**
* Try the best to get the exception text.
*/
static async #getExceptionText(params, realm) {
if (!params.exceptionDetails.exception) {
return params.exceptionDetails.text;
}
if (realm === undefined) {
return JSON.stringify(params.exceptionDetails.exception);
}
return await realm.stringifyObject(params.exceptionDetails.exception);
}
}
exports.LogManager = LogManager;
_a = LogManager;
//# sourceMappingURL=LogManager.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,23 @@
/**
* 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.
*/
import type { Script } from '../../../protocol/protocol.js';
/**
* @param args input remote values to be format printed
* @return parsed text of the remote values in specific format
*/
export declare function logMessageFormatter(args: Script.RemoteValue[]): string;
export declare function getRemoteValuesText(args: Script.RemoteValue[], formatText: boolean): string;

View File

@@ -0,0 +1,172 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.logMessageFormatter = logMessageFormatter;
exports.getRemoteValuesText = getRemoteValuesText;
const assert_js_1 = require("../../../utils/assert.js");
const specifiers = ['%s', '%d', '%i', '%f', '%o', '%O', '%c'];
function isFormatSpecifier(str) {
return specifiers.some((spec) => str.includes(spec));
}
/**
* @param args input remote values to be format printed
* @return parsed text of the remote values in specific format
*/
function logMessageFormatter(args) {
let output = '';
const argFormat = args[0].value.toString();
const argValues = args.slice(1, undefined);
const tokens = argFormat.split(new RegExp(specifiers.map((spec) => `(${spec})`).join('|'), 'g'));
for (const token of tokens) {
if (token === undefined || token === '') {
continue;
}
if (isFormatSpecifier(token)) {
const arg = argValues.shift();
// raise an exception when less value is provided
(0, assert_js_1.assert)(arg, `Less value is provided: "${getRemoteValuesText(args, false)}"`);
if (token === '%s') {
output += stringFromArg(arg);
}
else if (token === '%d' || token === '%i') {
if (arg.type === 'bigint' ||
arg.type === 'number' ||
arg.type === 'string') {
output += parseInt(arg.value.toString(), 10);
}
else {
output += 'NaN';
}
}
else if (token === '%f') {
if (arg.type === 'bigint' ||
arg.type === 'number' ||
arg.type === 'string') {
output += parseFloat(arg.value.toString());
}
else {
output += 'NaN';
}
}
else {
// %o, %O, %c
output += toJson(arg);
}
}
else {
output += token;
}
}
// raise an exception when more value is provided
if (argValues.length > 0) {
throw new Error(`More value is provided: "${getRemoteValuesText(args, false)}"`);
}
return output;
}
/**
* @param arg input remote value to be parsed
* @return parsed text of the remote value
*
* input: {"type": "number", "value": 1}
* output: 1
*
* input: {"type": "string", "value": "abc"}
* output: "abc"
*
* input: {"type": "object", "value": [["id", {"type": "number", "value": 1}]]}
* output: '{"id": 1}'
*
* input: {"type": "object", "value": [["font-size", {"type": "string", "value": "20px"}]]}
* output: '{"font-size": "20px"}'
*/
function toJson(arg) {
// arg type validation
if (arg.type !== 'array' &&
arg.type !== 'bigint' &&
arg.type !== 'date' &&
arg.type !== 'number' &&
arg.type !== 'object' &&
arg.type !== 'string') {
return stringFromArg(arg);
}
if (arg.type === 'bigint') {
return `${arg.value.toString()}n`;
}
if (arg.type === 'number') {
return arg.value.toString();
}
if (['date', 'string'].includes(arg.type)) {
return JSON.stringify(arg.value);
}
if (arg.type === 'object') {
return `{${arg.value
.map((pair) => {
return `${JSON.stringify(pair[0])}:${toJson(pair[1])}`;
})
.join(',')}}`;
}
if (arg.type === 'array') {
return `[${arg.value?.map((val) => toJson(val)).join(',') ?? ''}]`;
}
throw Error(`Invalid value type: ${arg}`);
}
function stringFromArg(arg) {
if (!Object.hasOwn(arg, 'value')) {
return arg.type;
}
switch (arg.type) {
case 'string':
case 'number':
case 'boolean':
case 'bigint':
return String(arg.value);
case 'regexp':
return `/${arg.value.pattern}/${arg.value.flags ?? ''}`;
case 'date':
return new Date(arg.value).toString();
case 'object':
return `Object(${arg.value?.length ?? ''})`;
case 'array':
return `Array(${arg.value?.length ?? ''})`;
case 'map':
return `Map(${arg.value?.length})`;
case 'set':
return `Set(${arg.value?.length})`;
default:
return arg.type;
}
}
function getRemoteValuesText(args, formatText) {
const arg = args[0];
if (!arg) {
return '';
}
// if args[0] is a format specifier, format the args as output
if (arg.type === 'string' &&
isFormatSpecifier(arg.value.toString()) &&
formatText) {
return logMessageFormatter(args);
}
// if args[0] is not a format specifier, just join the args with \u0020 (unicode 'SPACE')
return args
.map((arg) => {
return stringFromArg(arg);
})
.join('\u0020');
}
//# sourceMappingURL=logHelper.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"logHelper.js","sourceRoot":"","sources":["../../../../../src/bidiMapper/modules/log/logHelper.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,13 @@
import type { Browser, BrowsingContext } from '../../../protocol/generated/webdriver-bidi.js';
import { Network } from '../../../protocol/generated/webdriver-bidi.js';
import { type LoggerFn } from '../../../utils/log.js';
import type { NetworkRequest } from './NetworkRequest.js';
export declare class CollectorsStorage {
#private;
constructor(maxEncodedDataSize: number, logger?: LoggerFn);
addDataCollector(params: Network.AddDataCollectorParameters): `${string}-${string}-${string}-${string}-${string}`;
isCollected(requestId: Network.Request, dataType?: Network.DataType, collectorId?: string): boolean;
disownData(requestId: Network.Request, dataType: Network.DataType, collectorId?: string): void;
collectIfNeeded(request: NetworkRequest, dataType: Network.DataType, topLevelBrowsingContext: BrowsingContext.BrowsingContext, userContext: Browser.UserContext): void;
removeDataCollector(collectorId: Network.Collector): Network.Request[];
}

View File

@@ -0,0 +1,153 @@
"use strict";
/*
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.CollectorsStorage = void 0;
const ErrorResponse_js_1 = require("../../../protocol/ErrorResponse.js");
const log_js_1 = require("../../../utils/log.js");
const uuid_js_1 = require("../../../utils/uuid.js");
class CollectorsStorage {
#collectors = new Map();
#responseCollectors = new Map();
#requestBodyCollectors = new Map();
#maxEncodedDataSize;
#logger;
constructor(maxEncodedDataSize, logger) {
this.#maxEncodedDataSize = maxEncodedDataSize;
this.#logger = logger;
}
addDataCollector(params) {
if (params.maxEncodedDataSize < 1 ||
params.maxEncodedDataSize > this.#maxEncodedDataSize) {
// 200 MB is the default limit in CDP:
// https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/inspector/inspector_network_agent.cc;drc=da1f749634c9a401cc756f36c2e6ce233e1c9b4d;l=133
throw new ErrorResponse_js_1.InvalidArgumentException(`Max encoded data size should be between 1 and ${this.#maxEncodedDataSize}`);
}
const collectorId = (0, uuid_js_1.uuidv4)();
this.#collectors.set(collectorId, params);
return collectorId;
}
isCollected(requestId, dataType, collectorId) {
if (collectorId !== undefined && !this.#collectors.has(collectorId)) {
throw new ErrorResponse_js_1.NoSuchNetworkCollectorException(`Unknown collector ${collectorId}`);
}
if (dataType === undefined) {
return (this.isCollected(requestId, "response" /* Network.DataType.Response */, collectorId) ||
this.isCollected(requestId, "request" /* Network.DataType.Request */, collectorId));
}
const requestToCollectorsMap = this.#getRequestToCollectorMap(dataType).get(requestId);
if (requestToCollectorsMap === undefined ||
requestToCollectorsMap.size === 0) {
return false;
}
if (collectorId === undefined) {
// There is at least 1 collector for the data.
return true;
}
if (!requestToCollectorsMap.has(collectorId)) {
return false;
}
return true;
}
#getRequestToCollectorMap(dataType) {
switch (dataType) {
case "response" /* Network.DataType.Response */:
return this.#responseCollectors;
case "request" /* Network.DataType.Request */:
return this.#requestBodyCollectors;
default:
throw new ErrorResponse_js_1.UnsupportedOperationException(`Unsupported data type ${dataType}`);
}
}
disownData(requestId, dataType, collectorId) {
const requestToCollectorsMap = this.#getRequestToCollectorMap(dataType);
if (collectorId !== undefined) {
requestToCollectorsMap.get(requestId)?.delete(collectorId);
}
if (collectorId === undefined ||
requestToCollectorsMap.get(requestId)?.size === 0) {
requestToCollectorsMap.delete(requestId);
}
}
#shouldCollectRequest(collectorId, request, dataType, topLevelBrowsingContext, userContext) {
const collector = this.#collectors.get(collectorId);
if (collector === undefined) {
throw new ErrorResponse_js_1.NoSuchNetworkCollectorException(`Unknown collector ${collectorId}`);
}
if (collector.userContexts &&
!collector.userContexts.includes(userContext)) {
// Collector is aimed for a different user context.
return false;
}
if (collector.contexts &&
!collector.contexts.includes(topLevelBrowsingContext)) {
// Collector is aimed for a different top-level browsing context.
return false;
}
if (!collector.dataTypes.includes(dataType)) {
// Collector is aimed for a different data type.
return false;
}
if (dataType === "request" /* Network.DataType.Request */ &&
request.bodySize > collector.maxEncodedDataSize) {
this.#logger?.(log_js_1.LogType.debug, `Request's ${request.id} body size is too big for the collector ${collectorId}`);
return false;
}
if (dataType === "response" /* Network.DataType.Response */ &&
request.encodedResponseBodySize > collector.maxEncodedDataSize) {
this.#logger?.(log_js_1.LogType.debug, `Request's ${request.id} response is too big for the collector ${collectorId}`);
return false;
}
this.#logger?.(log_js_1.LogType.debug, `Collector ${collectorId} collected ${dataType} of ${request.id}`);
return true;
}
collectIfNeeded(request, dataType, topLevelBrowsingContext, userContext) {
const collectorIds = [...this.#collectors.keys()].filter((collectorId) => this.#shouldCollectRequest(collectorId, request, dataType, topLevelBrowsingContext, userContext));
if (collectorIds.length > 0) {
this.#getRequestToCollectorMap(dataType).set(request.id, new Set(collectorIds));
}
}
removeDataCollector(collectorId) {
if (!this.#collectors.has(collectorId)) {
throw new ErrorResponse_js_1.NoSuchNetworkCollectorException(`Collector ${collectorId} does not exist`);
}
this.#collectors.delete(collectorId);
const affectedRequests = [];
// Clean up collected responses.
for (const [requestId, collectorIds] of this.#responseCollectors) {
if (collectorIds.has(collectorId)) {
collectorIds.delete(collectorId);
if (collectorIds.size === 0) {
this.#responseCollectors.delete(requestId);
affectedRequests.push(requestId);
}
}
}
for (const [requestId, collectorIds] of this.#requestBodyCollectors) {
if (collectorIds.has(collectorId)) {
collectorIds.delete(collectorId);
if (collectorIds.size === 0) {
this.#requestBodyCollectors.delete(requestId);
affectedRequests.push(requestId);
}
}
}
return affectedRequests;
}
}
exports.CollectorsStorage = CollectorsStorage;
//# sourceMappingURL=CollectorsStorage.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"CollectorsStorage.js","sourceRoot":"","sources":["../../../../../src/bidiMapper/modules/network/CollectorsStorage.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,54 @@
/**
* 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.
*/
import type { Protocol } from 'devtools-protocol';
import { Network, type EmptyResult } from '../../../protocol/protocol.js';
import type { ContextConfigStorage } from '../browser/ContextConfigStorage.js';
import type { UserContextStorage } from '../browser/UserContextStorage.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
import type { NetworkStorage } from './NetworkStorage.js';
import { type ParsedUrlPattern } from './NetworkUtils.js';
/** Dispatches Network module commands. */
export declare class NetworkProcessor {
#private;
constructor(browsingContextStorage: BrowsingContextStorage, networkStorage: NetworkStorage, userContextStorage: UserContextStorage, contextConfigStorage: ContextConfigStorage);
addIntercept(params: Network.AddInterceptParameters): Promise<Network.AddInterceptResult>;
continueRequest(params: Network.ContinueRequestParameters): Promise<EmptyResult>;
continueResponse(params: Network.ContinueResponseParameters): Promise<EmptyResult>;
continueWithAuth(params: Network.ContinueWithAuthParameters): Promise<EmptyResult>;
failRequest({ request: networkId, }: Network.FailRequestParameters): Promise<EmptyResult>;
provideResponse(params: Network.ProvideResponseParameters): Promise<EmptyResult>;
removeIntercept(params: Network.RemoveInterceptParameters): Promise<EmptyResult>;
setCacheBehavior(params: Network.SetCacheBehaviorParameters): Promise<EmptyResult>;
/**
* Validate https://fetch.spec.whatwg.org/#header-value
*/
static validateHeaders(headers: Network.Header[]): void;
static isMethodValid(method: string): boolean;
/**
* Attempts to parse the given url.
* Throws an InvalidArgumentException if the url is invalid.
*/
static parseUrlString(url: string): URL;
static parseUrlPatterns(urlPatterns: Network.UrlPattern[]): ParsedUrlPattern[];
static wrapInterceptionError(error: any): any;
addDataCollector(params: Network.AddDataCollectorParameters): Promise<Network.AddDataCollectorResult>;
getData(params: Network.GetDataParameters): Promise<Network.GetDataResult>;
removeDataCollector(params: Network.RemoveDataCollectorParameters): Promise<EmptyResult>;
disownData(params: Network.DisownDataParameters): EmptyResult;
setExtraHeaders(params: Network.SetExtraHeadersParameters): Promise<EmptyResult>;
}
export declare function parseBiDiHeaders(headers: Network.Header[]): Protocol.Network.Headers;

View File

@@ -0,0 +1,546 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.NetworkProcessor = void 0;
exports.parseBiDiHeaders = parseBiDiHeaders;
const protocol_js_1 = require("../../../protocol/protocol.js");
const NetworkUtils_js_1 = require("./NetworkUtils.js");
/** Dispatches Network module commands. */
class NetworkProcessor {
#browsingContextStorage;
#networkStorage;
#userContextStorage;
#contextConfigStorage;
constructor(browsingContextStorage, networkStorage, userContextStorage, contextConfigStorage) {
this.#userContextStorage = userContextStorage;
this.#browsingContextStorage = browsingContextStorage;
this.#networkStorage = networkStorage;
this.#contextConfigStorage = contextConfigStorage;
}
async addIntercept(params) {
this.#browsingContextStorage.verifyTopLevelContextsList(params.contexts);
const urlPatterns = params.urlPatterns ?? [];
const parsedUrlPatterns = NetworkProcessor.parseUrlPatterns(urlPatterns);
const intercept = this.#networkStorage.addIntercept({
urlPatterns: parsedUrlPatterns,
phases: params.phases,
contexts: params.contexts,
});
// Adding interception may require enabling CDP Network domains.
await this.#toggleNetwork();
return {
intercept,
};
}
async continueRequest(params) {
if (params.url !== undefined) {
NetworkProcessor.parseUrlString(params.url);
}
if (params.method !== undefined) {
if (!NetworkProcessor.isMethodValid(params.method)) {
throw new protocol_js_1.InvalidArgumentException(`Method '${params.method}' is invalid.`);
}
}
if (params.headers) {
NetworkProcessor.validateHeaders(params.headers);
}
const request = this.#getBlockedRequestOrFail(params.request, [
"beforeRequestSent" /* Network.InterceptPhase.BeforeRequestSent */,
]);
try {
await request.continueRequest(params);
}
catch (error) {
throw NetworkProcessor.wrapInterceptionError(error);
}
return {};
}
async continueResponse(params) {
if (params.headers) {
NetworkProcessor.validateHeaders(params.headers);
}
const request = this.#getBlockedRequestOrFail(params.request, [
"authRequired" /* Network.InterceptPhase.AuthRequired */,
"responseStarted" /* Network.InterceptPhase.ResponseStarted */,
]);
try {
await request.continueResponse(params);
}
catch (error) {
throw NetworkProcessor.wrapInterceptionError(error);
}
return {};
}
async continueWithAuth(params) {
const networkId = params.request;
const request = this.#getBlockedRequestOrFail(networkId, [
"authRequired" /* Network.InterceptPhase.AuthRequired */,
]);
await request.continueWithAuth(params);
return {};
}
async failRequest({ request: networkId, }) {
const request = this.#getRequestOrFail(networkId);
if (request.interceptPhase === "authRequired" /* Network.InterceptPhase.AuthRequired */) {
throw new protocol_js_1.InvalidArgumentException(`Request '${networkId}' in 'authRequired' phase cannot be failed`);
}
if (!request.interceptPhase) {
throw new protocol_js_1.NoSuchRequestException(`No blocked request found for network id '${networkId}'`);
}
await request.failRequest('Failed');
return {};
}
async provideResponse(params) {
if (params.headers) {
NetworkProcessor.validateHeaders(params.headers);
}
const request = this.#getBlockedRequestOrFail(params.request, [
"beforeRequestSent" /* Network.InterceptPhase.BeforeRequestSent */,
"responseStarted" /* Network.InterceptPhase.ResponseStarted */,
"authRequired" /* Network.InterceptPhase.AuthRequired */,
]);
try {
await request.provideResponse(params);
}
catch (error) {
throw NetworkProcessor.wrapInterceptionError(error);
}
return {};
}
/**
* In some states CDP Network and Fetch domains are not required, but in some they have
* to be updated. Whenever potential change in these kinds of states is introduced,
* update the states of all the CDP targets.
*/
async #toggleNetwork() {
await Promise.all(this.#browsingContextStorage.getAllContexts().map((context) => {
return context.cdpTarget.toggleNetwork();
}));
}
async removeIntercept(params) {
this.#networkStorage.removeIntercept(params.intercept);
// Removing interception may allow for disabling CDP Network domains.
await this.#toggleNetwork();
return {};
}
async setCacheBehavior(params) {
const contexts = this.#browsingContextStorage.verifyTopLevelContextsList(params.contexts);
// Change all targets
if (contexts.size === 0) {
this.#networkStorage.defaultCacheBehavior = params.cacheBehavior;
await Promise.all(this.#browsingContextStorage.getAllContexts().map((context) => {
return context.cdpTarget.toggleSetCacheDisabled();
}));
return {};
}
const cacheDisabled = params.cacheBehavior === 'bypass';
await Promise.all([...contexts.values()].map((context) => {
return context.cdpTarget.toggleSetCacheDisabled(cacheDisabled);
}));
return {};
}
#getRequestOrFail(id) {
const request = this.#networkStorage.getRequestById(id);
if (!request) {
throw new protocol_js_1.NoSuchRequestException(`Network request with ID '${id}' doesn't exist`);
}
return request;
}
#getBlockedRequestOrFail(id, phases) {
const request = this.#getRequestOrFail(id);
if (!request.interceptPhase) {
throw new protocol_js_1.NoSuchRequestException(`No blocked request found for network id '${id}'`);
}
if (request.interceptPhase && !phases.includes(request.interceptPhase)) {
throw new protocol_js_1.InvalidArgumentException(`Blocked request for network id '${id}' is in '${request.interceptPhase}' phase`);
}
return request;
}
/**
* Validate https://fetch.spec.whatwg.org/#header-value
*/
static validateHeaders(headers) {
for (const header of headers) {
let headerValue;
if (header.value.type === 'string') {
headerValue = header.value.value;
}
else {
headerValue = atob(header.value.value);
}
if (headerValue !== headerValue.trim() ||
headerValue.includes('\n') ||
headerValue.includes('\0')) {
throw new protocol_js_1.InvalidArgumentException(`Header value '${headerValue}' is not acceptable value`);
}
}
}
static isMethodValid(method) {
// https://httpwg.org/specs/rfc9110.html#method.overview
return /^[!#$%&'*+\-.^_`|~a-zA-Z\d]+$/.test(method);
}
/**
* Attempts to parse the given url.
* Throws an InvalidArgumentException if the url is invalid.
*/
static parseUrlString(url) {
try {
return new URL(url);
}
catch (error) {
throw new protocol_js_1.InvalidArgumentException(`Invalid URL '${url}': ${error}`);
}
}
static parseUrlPatterns(urlPatterns) {
return urlPatterns.map((urlPattern) => {
let patternUrl = '';
let hasProtocol = true;
let hasHostname = true;
let hasPort = true;
let hasPathname = true;
let hasSearch = true;
switch (urlPattern.type) {
case 'string': {
patternUrl = unescapeURLPattern(urlPattern.pattern);
break;
}
case 'pattern': {
if (urlPattern.protocol === undefined) {
hasProtocol = false;
patternUrl += 'http';
}
else {
if (urlPattern.protocol === '') {
throw new protocol_js_1.InvalidArgumentException('URL pattern must specify a protocol');
}
urlPattern.protocol = unescapeURLPattern(urlPattern.protocol);
if (!urlPattern.protocol.match(/^[a-zA-Z+-.]+$/)) {
throw new protocol_js_1.InvalidArgumentException('Forbidden characters');
}
patternUrl += urlPattern.protocol;
}
const scheme = patternUrl.toLocaleLowerCase();
patternUrl += ':';
if ((0, NetworkUtils_js_1.isSpecialScheme)(scheme)) {
patternUrl += '//';
}
if (urlPattern.hostname === undefined) {
if (scheme !== 'file') {
patternUrl += 'placeholder';
}
hasHostname = false;
}
else {
if (urlPattern.hostname === '') {
throw new protocol_js_1.InvalidArgumentException('URL pattern must specify a hostname');
}
if (urlPattern.protocol === 'file') {
throw new protocol_js_1.InvalidArgumentException(`URL pattern protocol cannot be 'file'`);
}
urlPattern.hostname = unescapeURLPattern(urlPattern.hostname);
let insideBrackets = false;
for (const c of urlPattern.hostname) {
if (c === '/' || c === '?' || c === '#') {
throw new protocol_js_1.InvalidArgumentException(`'/', '?', '#' are forbidden in hostname`);
}
if (!insideBrackets && c === ':') {
throw new protocol_js_1.InvalidArgumentException(`':' is only allowed inside brackets in hostname`);
}
if (c === '[') {
insideBrackets = true;
}
if (c === ']') {
insideBrackets = false;
}
}
patternUrl += urlPattern.hostname;
}
if (urlPattern.port === undefined) {
hasPort = false;
}
else {
if (urlPattern.port === '') {
throw new protocol_js_1.InvalidArgumentException(`URL pattern must specify a port`);
}
urlPattern.port = unescapeURLPattern(urlPattern.port);
patternUrl += ':';
if (!urlPattern.port.match(/^\d+$/)) {
throw new protocol_js_1.InvalidArgumentException('Forbidden characters');
}
patternUrl += urlPattern.port;
}
if (urlPattern.pathname === undefined) {
hasPathname = false;
}
else {
urlPattern.pathname = unescapeURLPattern(urlPattern.pathname);
if (urlPattern.pathname[0] !== '/') {
patternUrl += '/';
}
if (urlPattern.pathname.includes('#') ||
urlPattern.pathname.includes('?')) {
throw new protocol_js_1.InvalidArgumentException('Forbidden characters');
}
patternUrl += urlPattern.pathname;
}
if (urlPattern.search === undefined) {
hasSearch = false;
}
else {
urlPattern.search = unescapeURLPattern(urlPattern.search);
if (urlPattern.search[0] !== '?') {
patternUrl += '?';
}
if (urlPattern.search.includes('#')) {
throw new protocol_js_1.InvalidArgumentException('Forbidden characters');
}
patternUrl += urlPattern.search;
}
break;
}
}
const serializePort = (url) => {
const defaultPorts = {
'ftp:': 21,
'file:': null,
'http:': 80,
'https:': 443,
'ws:': 80,
'wss:': 443,
};
if ((0, NetworkUtils_js_1.isSpecialScheme)(url.protocol) &&
defaultPorts[url.protocol] !== null &&
(!url.port || String(defaultPorts[url.protocol]) === url.port)) {
return '';
}
else if (url.port) {
return url.port;
}
return undefined;
};
try {
const url = new URL(patternUrl);
return {
protocol: hasProtocol ? url.protocol.replace(/:$/, '') : undefined,
hostname: hasHostname ? url.hostname : undefined,
port: hasPort ? serializePort(url) : undefined,
pathname: hasPathname && url.pathname ? url.pathname : undefined,
search: hasSearch ? url.search : undefined,
};
}
catch (err) {
throw new protocol_js_1.InvalidArgumentException(`${err.message} '${patternUrl}'`);
}
});
}
static wrapInterceptionError(error) {
// https://source.chromium.org/chromium/chromium/src/+/main:content/browser/devtools/protocol/fetch_handler.cc;l=169
if (error?.message.includes('Invalid header') ||
error?.message.includes('Unsafe header')) {
return new protocol_js_1.InvalidArgumentException(error.message);
}
return error;
}
async addDataCollector(params) {
if (params.userContexts !== undefined && params.contexts !== undefined) {
throw new protocol_js_1.InvalidArgumentException("'contexts' and 'userContexts' are mutually exclusive");
}
if (params.userContexts !== undefined) {
// Assert the user contexts exist.
await this.#userContextStorage.verifyUserContextIdList(params.userContexts);
}
if (params.contexts !== undefined) {
for (const browsingContextId of params.contexts) {
// Assert the browsing context exists and are top-level.
const browsingContext = this.#browsingContextStorage.getContext(browsingContextId);
if (!browsingContext.isTopLevelContext()) {
throw new protocol_js_1.InvalidArgumentException(`Data collectors are available only on top-level browsing contexts`);
}
}
}
const collectorId = this.#networkStorage.addDataCollector(params);
// Adding data collectors may require enabling CDP Network domains.
await this.#toggleNetwork();
return { collector: collectorId };
}
async getData(params) {
return await this.#networkStorage.getCollectedData(params);
}
async removeDataCollector(params) {
this.#networkStorage.removeDataCollector(params);
// Removing data collectors may allow disabling CDP Network domains.
await this.#toggleNetwork();
return {};
}
disownData(params) {
this.#networkStorage.disownData(params);
return {};
}
async #getRelatedTopLevelBrowsingContexts(browsingContextIds, userContextIds) {
// Duplicated with EmulationProcessor logic. Consider moving to ConfigStorage.
if (browsingContextIds === undefined && userContextIds === undefined) {
return this.#browsingContextStorage.getTopLevelContexts();
}
if (browsingContextIds !== undefined && userContextIds !== undefined) {
throw new protocol_js_1.InvalidArgumentException('User contexts and browsing contexts are mutually exclusive');
}
const result = [];
if (userContextIds !== undefined) {
if (userContextIds.length === 0) {
throw new protocol_js_1.InvalidArgumentException('user context should be provided');
}
// Verify that all user contexts exist.
await this.#userContextStorage.verifyUserContextIdList(userContextIds);
for (const userContextId of userContextIds) {
const topLevelBrowsingContexts = this.#browsingContextStorage
.getTopLevelContexts()
.filter((browsingContext) => browsingContext.userContext === userContextId);
result.push(...topLevelBrowsingContexts);
}
}
if (browsingContextIds !== undefined) {
if (browsingContextIds.length === 0) {
throw new protocol_js_1.InvalidArgumentException('browsing context should be provided');
}
for (const browsingContextId of browsingContextIds) {
const browsingContext = this.#browsingContextStorage.getContext(browsingContextId);
if (!browsingContext.isTopLevelContext()) {
throw new protocol_js_1.InvalidArgumentException('The command is only supported on the top-level context');
}
result.push(browsingContext);
}
}
// Remove duplicates. Compare `BrowsingContextImpl` by reference is correct here, as
// `browsingContextStorage` returns the same instance for the same id.
return [...new Set(result).values()];
}
async setExtraHeaders(params) {
const affectedBrowsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
const cdpExtraHeaders = parseBiDiHeaders(params.headers);
if (params.userContexts === undefined && params.contexts === undefined) {
this.#contextConfigStorage.updateGlobalConfig({
extraHeaders: cdpExtraHeaders,
});
}
if (params.userContexts !== undefined) {
params.userContexts.forEach((userContext) => {
this.#contextConfigStorage.updateUserContextConfig(userContext, {
extraHeaders: cdpExtraHeaders,
});
});
}
if (params.contexts !== undefined) {
params.contexts.forEach((browsingContextId) => {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, { extraHeaders: cdpExtraHeaders });
});
}
await Promise.all(affectedBrowsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing setting.
const extraHeaders = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext).extraHeaders ?? {};
await context.setExtraHeaders(extraHeaders);
}));
return {};
}
}
exports.NetworkProcessor = NetworkProcessor;
/**
* See https://w3c.github.io/webdriver-bidi/#unescape-url-pattern
*/
function unescapeURLPattern(pattern) {
const forbidden = new Set(['(', ')', '*', '{', '}']);
let result = '';
let isEscaped = false;
for (const c of pattern) {
if (!isEscaped) {
if (forbidden.has(c)) {
throw new protocol_js_1.InvalidArgumentException('Forbidden characters');
}
if (c === '\\') {
isEscaped = true;
continue;
}
}
result += c;
isEscaped = false;
}
return result;
}
// https://fetch.spec.whatwg.org/#header-name
const FORBIDDEN_HEADER_NAME_SYMBOLS = new Set([
' ',
'\t',
'\n',
'"',
'(',
')',
',',
'/',
':',
';',
'<',
'=',
'>',
'?',
'@',
'[',
'\\',
']',
'{',
'}',
]);
// https://fetch.spec.whatwg.org/#header-value
const FORBIDDEN_HEADER_VALUE_SYMBOLS = new Set(['\0', '\n', '\r']);
function includesChar(str, chars) {
for (const char of str) {
if (chars.has(char)) {
return true;
}
}
return false;
}
// Export for testing.
function parseBiDiHeaders(headers) {
const parsedHeaders = {};
for (const bidiHeader of headers) {
if (bidiHeader.value.type === 'string') {
const name = bidiHeader.name;
const value = bidiHeader.value.value;
if (name.length === 0) {
throw new protocol_js_1.InvalidArgumentException(`Empty header name is not allowed`);
}
if (includesChar(name, FORBIDDEN_HEADER_NAME_SYMBOLS)) {
throw new protocol_js_1.InvalidArgumentException(`Header name '${name}' contains forbidden symbols`);
}
if (includesChar(value, FORBIDDEN_HEADER_VALUE_SYMBOLS)) {
throw new protocol_js_1.InvalidArgumentException(`Header value '${value}' contains forbidden symbols`);
}
if (value.trim() !== value) {
throw new protocol_js_1.InvalidArgumentException(`Header value should not contain trailing or ending whitespaces`);
}
// BiDi spec does not combine but overrides the headers with the same names.
// https://www.w3.org/TR/webdriver-bidi/#update-headers
parsedHeaders[bidiHeader.name] = bidiHeader.value.value;
}
else {
throw new protocol_js_1.UnsupportedOperationException('Only string headers values are supported');
}
}
return parsedHeaders;
}
//# sourceMappingURL=NetworkProcessor.js.map

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More