Bug 1954992 - [webdriver-bidi] Add "emulation.setGeolocationOverride" command. r=webdriver-reviewers,jdescottes

Differential Revision: https://phabricator.services.mozilla.com/D243435
This commit is contained in:
Alexandra Borovova
2025-04-08 12:58:33 +00:00
parent 781ac7a332
commit 46b2d5cf52
7 changed files with 378 additions and 15 deletions

View File

@@ -19,6 +19,7 @@ remote.jar:
# WebDriver BiDi root modules
content/webdriver-bidi/modules/root/browser.sys.mjs (modules/root/browser.sys.mjs)
content/webdriver-bidi/modules/root/browsingContext.sys.mjs (modules/root/browsingContext.sys.mjs)
content/webdriver-bidi/modules/root/emulation.sys.mjs (modules/root/emulation.sys.mjs)
content/webdriver-bidi/modules/root/input.sys.mjs (modules/root/input.sys.mjs)
content/webdriver-bidi/modules/root/log.sys.mjs (modules/root/log.sys.mjs)
content/webdriver-bidi/modules/root/network.sys.mjs (modules/root/network.sys.mjs)
@@ -31,6 +32,7 @@ remote.jar:
# WebDriver BiDi windowglobal modules
content/webdriver-bidi/modules/windowglobal/_configuration.sys.mjs (modules/windowglobal/_configuration.sys.mjs)
content/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs (modules/windowglobal/browsingContext.sys.mjs)
content/webdriver-bidi/modules/windowglobal/emulation.sys.mjs (modules/windowglobal/emulation.sys.mjs)
content/webdriver-bidi/modules/windowglobal/input.sys.mjs (modules/windowglobal/input.sys.mjs)
content/webdriver-bidi/modules/windowglobal/log.sys.mjs (modules/windowglobal/log.sys.mjs)
content/webdriver-bidi/modules/windowglobal/network.sys.mjs (modules/windowglobal/network.sys.mjs)

View File

@@ -20,6 +20,8 @@ ChromeUtils.defineESModuleGetters(modules.root, {
"chrome://remote/content/webdriver-bidi/modules/root/browser.sys.mjs",
browsingContext:
"chrome://remote/content/webdriver-bidi/modules/root/browsingContext.sys.mjs",
emulation:
"chrome://remote/content/webdriver-bidi/modules/root/emulation.sys.mjs",
input: "chrome://remote/content/webdriver-bidi/modules/root/input.sys.mjs",
log: "chrome://remote/content/webdriver-bidi/modules/root/log.sys.mjs",
network:
@@ -52,6 +54,8 @@ ChromeUtils.defineESModuleGetters(modules.windowglobal, {
"chrome://remote/content/webdriver-bidi/modules/windowglobal/_configuration.sys.mjs",
browsingContext:
"chrome://remote/content/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs",
emulation:
"chrome://remote/content/webdriver-bidi/modules/windowglobal/emulation.sys.mjs",
input:
"chrome://remote/content/webdriver-bidi/modules/windowglobal/input.sys.mjs",
log: "chrome://remote/content/webdriver-bidi/modules/windowglobal/log.sys.mjs",

View File

@@ -1472,7 +1472,7 @@ class BrowsingContextModule extends RootBiDiModule {
if (contextId !== null && userContextIds !== null) {
throw new lazy.error.InvalidArgumentError(
`Providing both "contexts" and "userContexts" arguments is not supported`
`Providing both "context" and "userContexts" arguments is not supported`
);
}

View File

@@ -0,0 +1,291 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { RootBiDiModule } from "chrome://remote/content/webdriver-bidi/modules/RootBiDiModule.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
ContextDescriptorType:
"chrome://remote/content/shared/messagehandler/MessageHandler.sys.mjs",
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
pprint: "chrome://remote/content/shared/Format.sys.mjs",
SessionDataMethod:
"chrome://remote/content/shared/messagehandler/sessiondata/SessionData.sys.mjs",
TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
UserContextManager:
"chrome://remote/content/shared/UserContextManager.sys.mjs",
});
class EmulationModule extends RootBiDiModule {
/**
* Create a new module instance.
*
* @param {MessageHandler} messageHandler
* The MessageHandler instance which owns this Module instance.
*/
constructor(messageHandler) {
super(messageHandler);
}
destroy() {}
/**
* Used as an argument for emulation.setGeolocationOverride command
* to represent an object which holds geolocation coordinates which
* should override the return result of geolocation APIs.
*
* @typedef {object} GeolocationCoordinates
*
* @property {number} latitude
* @property {number} longitude
* @property {number=} accuracy
* Defaults to 1.
* @property {number=} altitude
* Defaults to null.
* @property {number=} altitudeAccuracy
* Defaults to null.
* @property {number=} heading
* Defaults to null.
* @property {number=} speed
* Defaults to null.
*/
/**
* Set the geolocation override to the list of top-level navigables
* or user contexts.
*
* @param {object=} options
* @param {Array<string>=} options.contexts
* Optional list of browsing context ids.
* @param {(GeolocationCoordinates|null)} options.coordinates
* Geolocation coordinates which have to override
* the return result of geolocation APIs.
* Null value resets the override.
* @param {Array<string>=} options.userContexts
* Optional list of user context ids.
*
* @throws {InvalidArgumentError}
* Raised if an argument is of an invalid type or value.
* @throws {NoSuchFrameError}
* If the browsing context cannot be found.
* @throws {NoSuchUserContextError}
* Raised if the user context id could not be found.
*/
async setGeolocationOverride(options = {}) {
let { coordinates } = options;
const { contexts: contextIds = null, userContexts: userContextIds = null } =
options;
if (coordinates !== null) {
lazy.assert.object(
coordinates,
lazy.pprint`Expected "coordinates" to be an object, got ${coordinates}`
);
const {
latitude,
longitude,
accuracy = 1,
// For platform API if we want to set values to null
// we have to set them to NaN.
altitude = NaN,
altitudeAccuracy = NaN,
heading = NaN,
speed = NaN,
} = coordinates;
lazy.assert.number(
latitude,
lazy.pprint`Expected "latitude" to be a number, got ${latitude}`
);
lazy.assert.number(
longitude,
lazy.pprint`Expected "longitude" to be a number, got ${longitude}`
);
lazy.assert.number(
accuracy,
lazy.pprint`Expected "accuracy" to be a number, got ${accuracy}`
);
if (!Number.isNaN(altitude)) {
lazy.assert.number(
altitude,
lazy.pprint`Expected "altitude" to be a number, got ${altitude}`
);
}
if (!Number.isNaN(altitudeAccuracy)) {
lazy.assert.number(
altitudeAccuracy,
lazy.pprint`Expected "altitudeAccuracy" to be a number, got ${altitudeAccuracy}`
);
if (Number.isNaN(altitude)) {
throw new lazy.error.InvalidArgumentError(
`When "altitudeAccuracy" is provided it's required to provide "altitude" as well`
);
}
}
if (!Number.isNaN(heading)) {
lazy.assert.number(
heading,
lazy.pprint`Expected "heading" to be a number, got ${heading}`
);
}
if (!Number.isNaN(speed)) {
lazy.assert.number(
speed,
lazy.pprint`Expected "speed" to be a number, got ${speed}`
);
}
coordinates = {
...coordinates,
accuracy,
altitude,
altitudeAccuracy,
heading,
speed,
};
}
const navigables = new Set();
const userContexts = new Set();
if (contextIds !== null) {
lazy.assert.isNonEmptyArray(
contextIds,
lazy.pprint`Expected "contexts" to be a non-empty array, got ${contextIds}`
);
for (const contextId of contextIds) {
lazy.assert.string(
contextId,
lazy.pprint`Expected elements of "contexts" to be a string, got ${contextId}`
);
const context = this.#getBrowsingContext(contextId);
lazy.assert.topLevel(
context,
`Browsing context with id ${contextId} is not top-level`
);
navigables.add(context);
}
} else if (userContextIds !== null) {
lazy.assert.isNonEmptyArray(
userContextIds,
lazy.pprint`Expected "userContexts" to be a non-empty array, got ${userContextIds}`
);
for (const userContextId of userContextIds) {
lazy.assert.string(
userContextId,
lazy.pprint`Expected elements of "userContexts" to be a string, got ${userContextId}`
);
const internalId =
lazy.UserContextManager.getInternalIdById(userContextId);
if (internalId === null) {
throw new lazy.error.NoSuchUserContextError(
`User context with id: ${userContextId} doesn't exist`
);
}
userContexts.add(internalId);
// Prepare the list of navigables to update.
lazy.UserContextManager.getTabsForUserContext(internalId).forEach(
tab => {
const contentBrowser = lazy.TabManager.getBrowserForTab(tab);
navigables.add(contentBrowser.browsingContext);
}
);
}
} else {
throw new lazy.error.InvalidArgumentError(
`At least one of "contexts" or "userContexts" arguments should be provided`
);
}
if (contextIds !== null && userContextIds !== null) {
throw new lazy.error.InvalidArgumentError(
`Providing both "contexts" and "userContexts" arguments is not supported`
);
}
const sessionDataItems = [];
if (userContextIds !== null) {
for (const userContext of userContexts) {
sessionDataItems.push({
category: "geolocation-override",
moduleName: "_configuration",
values: [coordinates],
contextDescriptor: {
type: lazy.ContextDescriptorType.UserContext,
id: userContext,
},
method: lazy.SessionDataMethod.Add,
});
}
} else {
for (const navigable of navigables) {
sessionDataItems.push({
category: "geolocation-override",
moduleName: "_configuration",
values: [coordinates],
contextDescriptor: {
type: lazy.ContextDescriptorType.TopBrowsingContext,
id: navigable.browserId,
},
method: lazy.SessionDataMethod.Add,
});
}
}
if (sessionDataItems.length) {
// TODO: Bug 1953079. Saving the geolocation override in the session data works fine
// with one session, but when we start supporting multiple BiDi session, we will
// have to rethink this approach.
await this.messageHandler.updateSessionData(sessionDataItems);
}
const commands = [];
for (const navigable of navigables) {
commands.push(
this._forwardToWindowGlobal(
"_setGeolocationOverride",
navigable.id,
{
coordinates,
},
{ retryOnAbort: true }
)
);
}
await Promise.all(commands);
}
#getBrowsingContext(contextId) {
const context = lazy.TabManager.getBrowsingContextById(contextId);
if (context === null) {
throw new lazy.error.NoSuchFrameError(
`Browsing Context with id ${contextId} not found`
);
}
return context;
}
}
export const emulation = EmulationModule;

View File

@@ -42,15 +42,14 @@ class PermissionsModule extends RootBiDiModule {
* The state which will be set to the permission.
* @param {string} options.origin
* The origin which is used as a target for permission update.
* @param {string=} options.userContext [unsupported]
* @param {string=} options.userContext
* The id of the user context which should be used as a target
* for permission update.
*
* @throws {InvalidArgumentError}
* Raised if an argument is of an invalid type or value.
* @throws {UnsupportedOperationError}
* Raised when unsupported permissions are set or <var>userContext</var>
* argument is used.
* Raised when unsupported permissions are set.
*/
async setPermission(options = {}) {
const {

View File

@@ -17,6 +17,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
* Internal module to set the configuration on the newly created navigables.
*/
class _ConfigurationModule extends WindowGlobalBiDiModule {
#geolocationConfiguration;
#preloadScripts;
#resolveBlockerPromise;
#viewportConfiguration;
@@ -24,6 +25,7 @@ class _ConfigurationModule extends WindowGlobalBiDiModule {
constructor(messageHandler) {
super(messageHandler);
this.#geolocationConfiguration = null;
this.#preloadScripts = new Set();
this.#viewportConfiguration = new Map();
@@ -53,7 +55,8 @@ class _ConfigurationModule extends WindowGlobalBiDiModule {
// Do nothing if there is no configuration to apply.
if (
this.#preloadScripts.size === 0 &&
this.#viewportConfiguration.size === 0
this.#viewportConfiguration.size === 0 &&
this.#geolocationConfiguration === null
) {
return;
}
@@ -64,6 +67,20 @@ class _ConfigurationModule extends WindowGlobalBiDiModule {
});
this.messageHandler.window.document.blockParsing(blockerPromise);
if (this.#geolocationConfiguration !== null) {
await this.messageHandler.handleCommand({
moduleName: "emulation",
commandName: "_setGeolocationOverride",
destination: {
type: lazy.WindowGlobalMessageHandler.type,
id: this.messageHandler.context.id,
},
params: {
coordinates: this.#geolocationConfiguration,
},
});
}
if (this.#viewportConfiguration.size !== 0) {
await this.messageHandler.forwardCommand({
moduleName: "browsingContext",
@@ -116,9 +133,10 @@ class _ConfigurationModule extends WindowGlobalBiDiModule {
}
}
// Viewport overrides apply only to top-level traversables.
// Geolocation and viewport overrides apply only to top-level traversables.
if (
category === "viewport-overrides" &&
(category === "geolocation-override" ||
category === "viewport-overrides") &&
!this.messageHandler.context.parent
) {
for (const { contextDescriptor, value } of sessionData) {
@@ -126,6 +144,9 @@ class _ConfigurationModule extends WindowGlobalBiDiModule {
continue;
}
if (category === "geolocation-override") {
this.#geolocationConfiguration = value;
} else {
if (value.viewport !== undefined) {
this.#viewportConfiguration.set("viewport", value.viewport);
}
@@ -139,6 +160,7 @@ class _ConfigurationModule extends WindowGlobalBiDiModule {
}
}
}
}
}
export const _configuration = _ConfigurationModule;

View File

@@ -0,0 +1,45 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { WindowGlobalBiDiModule } from "chrome://remote/content/webdriver-bidi/modules/WindowGlobalBiDiModule.sys.mjs";
class EmulationModule extends WindowGlobalBiDiModule {
constructor(messageHandler) {
super(messageHandler);
}
destroy() {}
/**
* Internal commands
*/
_applySessionData() {}
/**
* Set the geolocation override to the navigable.
*
* @param {object=} params
* @param {(GeolocationCoordinates|null)} params.coordinates
* Geolocation coordinates which have to override
* the return result of geolocation APIs.
* Null value resets the override.
*/
async _setGeolocationOverride(params = {}) {
const { coordinates } = params;
if (coordinates === null) {
this.messageHandler.context.setGeolocationServiceOverride();
} else {
this.messageHandler.context.setGeolocationServiceOverride({
coords: coordinates,
// The timestamp attribute represents the time
// when the geographic position of the device was acquired.
timestamp: Date.now(),
});
}
}
}
export const emulation = EmulationModule;