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>
695 lines
29 KiB
JavaScript
695 lines
29 KiB
JavaScript
"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
|