Source code

Revision control

Copy as Markdown

Other Tools

/* 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/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
getSeenNodesForBrowsingContext:
});
ChromeUtils.defineLazyGetter(lazy, "logger", () =>
lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
);
// Because Marionette supports a single session only we store its id
// globally so that the parent actor can access it.
let webDriverSessionId = null;
export class MarionetteCommandsParent extends JSWindowActorParent {
#deferredDialogOpened;
actorCreated() {
this.#deferredDialogOpened = null;
}
async sendQuery(name, serializedValue) {
const seenNodes = lazy.getSeenNodesForBrowsingContext(
webDriverSessionId,
this.manager.browsingContext
);
// return early if a dialog is opened
this.#deferredDialogOpened = Promise.withResolvers();
let {
error,
seenNodeIds,
serializedValue: serializedResult,
hasSerializedWindows,
} = await Promise.race([
super.sendQuery(name, serializedValue),
this.#deferredDialogOpened.promise,
]).finally(() => {
this.#deferredDialogOpened = null;
});
if (error) {
const err = lazy.error.WebDriverError.fromJSON(error);
this.#handleError(err, seenNodes);
}
// Update seen nodes for serialized element and shadow root nodes.
seenNodeIds?.forEach(nodeId => seenNodes.add(nodeId));
if (hasSerializedWindows) {
// The serialized data contains WebWindow references that need to be
// converted to unique identifiers.
serializedResult = lazy.json.mapToNavigableIds(serializedResult);
}
return serializedResult;
}
/**
* Handle WebDriver error and replace error type if necessary.
*
* @param {WebDriverError} error
* The WebDriver error to handle.
* @param {Set<string>} seenNodes
* List of node ids already seen in this navigable.
*
* @throws {WebDriverError}
* The original or replaced WebDriver error.
*/
#handleError(error, seenNodes) {
// If an element hasn't been found during deserialization check if it
// may be a stale reference.
if (
error instanceof lazy.error.NoSuchElementError &&
error.data.elementId !== undefined &&
seenNodes.has(error.data.elementId)
) {
throw new lazy.error.StaleElementReferenceError(error);
}
// If a shadow root hasn't been found during deserialization check if it
// may be a detached reference.
if (
error instanceof lazy.error.NoSuchShadowRootError &&
error.data.shadowId !== undefined &&
seenNodes.has(error.data.shadowId)
) {
throw new lazy.error.DetachedShadowRootError(error);
}
throw error;
}
notifyDialogOpened() {
if (this.#deferredDialogOpened) {
this.#deferredDialogOpened.resolve({ data: null });
}
}
// Proxying methods for WebDriver commands
clearElement(webEl) {
return this.sendQuery("MarionetteCommandsParent:clearElement", {
elem: webEl,
});
}
clickElement(webEl, capabilities) {
return this.sendQuery("MarionetteCommandsParent:clickElement", {
elem: webEl,
capabilities: capabilities.toJSON(),
});
}
async executeScript(script, args, opts) {
return this.sendQuery("MarionetteCommandsParent:executeScript", {
script,
args: lazy.json.mapFromNavigableIds(args),
opts,
});
}
findElement(strategy, selector, opts) {
return this.sendQuery("MarionetteCommandsParent:findElement", {
strategy,
selector,
opts,
});
}
findElements(strategy, selector, opts) {
return this.sendQuery("MarionetteCommandsParent:findElements", {
strategy,
selector,
opts,
});
}
async getShadowRoot(webEl) {
return this.sendQuery("MarionetteCommandsParent:getShadowRoot", {
elem: webEl,
});
}
async getActiveElement() {
return this.sendQuery("MarionetteCommandsParent:getActiveElement");
}
async getComputedLabel(webEl) {
return this.sendQuery("MarionetteCommandsParent:getComputedLabel", {
elem: webEl,
});
}
async getComputedRole(webEl) {
return this.sendQuery("MarionetteCommandsParent:getComputedRole", {
elem: webEl,
});
}
async getElementAttribute(webEl, name) {
return this.sendQuery("MarionetteCommandsParent:getElementAttribute", {
elem: webEl,
name,
});
}
async getElementProperty(webEl, name) {
return this.sendQuery("MarionetteCommandsParent:getElementProperty", {
elem: webEl,
name,
});
}
async getElementRect(webEl) {
return this.sendQuery("MarionetteCommandsParent:getElementRect", {
elem: webEl,
});
}
async getElementTagName(webEl) {
return this.sendQuery("MarionetteCommandsParent:getElementTagName", {
elem: webEl,
});
}
async getElementText(webEl) {
return this.sendQuery("MarionetteCommandsParent:getElementText", {
elem: webEl,
});
}
async getElementValueOfCssProperty(webEl, name) {
return this.sendQuery(
"MarionetteCommandsParent:getElementValueOfCssProperty",
{
elem: webEl,
name,
}
);
}
async getPageSource() {
return this.sendQuery("MarionetteCommandsParent:getPageSource");
}
async isElementDisplayed(webEl, capabilities) {
return this.sendQuery("MarionetteCommandsParent:isElementDisplayed", {
capabilities: capabilities.toJSON(),
elem: webEl,
});
}
async isElementEnabled(webEl, capabilities) {
return this.sendQuery("MarionetteCommandsParent:isElementEnabled", {
capabilities: capabilities.toJSON(),
elem: webEl,
});
}
async isElementSelected(webEl, capabilities) {
return this.sendQuery("MarionetteCommandsParent:isElementSelected", {
capabilities: capabilities.toJSON(),
elem: webEl,
});
}
async sendKeysToElement(webEl, text, capabilities) {
return this.sendQuery("MarionetteCommandsParent:sendKeysToElement", {
capabilities: capabilities.toJSON(),
elem: webEl,
text,
});
}
async performActions(actions) {
return this.sendQuery("MarionetteCommandsParent:performActions", {
actions,
});
}
async releaseActions() {
return this.sendQuery("MarionetteCommandsParent:releaseActions");
}
async switchToFrame(id) {
const { browsingContextId } = await this.sendQuery(
"MarionetteCommandsParent:switchToFrame",
{ id }
);
return {
browsingContext: BrowsingContext.get(browsingContextId),
};
}
async switchToParentFrame() {
const { browsingContextId } = await this.sendQuery(
"MarionetteCommandsParent:switchToParentFrame"
);
return {
browsingContext: BrowsingContext.get(browsingContextId),
};
}
async takeScreenshot(webEl, format, full, scroll) {
const rect = await this.sendQuery(
"MarionetteCommandsParent:getScreenshotRect",
{
elem: webEl,
full,
scroll,
}
);
// If no element has been specified use the top-level browsing context.
// Otherwise use the browsing context from the currently selected frame.
const browsingContext = webEl
? this.browsingContext
: this.browsingContext.top;
let canvas = await lazy.capture.canvas(
browsingContext.topChromeWindow,
browsingContext,
rect.x,
rect.y,
rect.width,
rect.height
);
switch (format) {
case lazy.capture.Format.Hash:
return lazy.capture.toHash(canvas);
case lazy.capture.Format.Base64:
return lazy.capture.toBase64(canvas);
default:
throw new TypeError(`Invalid capture format: ${format}`);
}
}
}
/**
* Proxy that will dynamically create MarionetteCommands actors for a dynamically
* provided browsing context until the method can be fully executed by the
* JSWindowActor pair.
*
* @param {function(): BrowsingContext} browsingContextFn
* A function that returns the reference to the browsing context for which
* the query should run.
*/
export function getMarionetteCommandsActorProxy(browsingContextFn) {
const MAX_ATTEMPTS = 10;
/**
* Methods which modify the content page cannot be retried safely.
* See Bug 1673345.
*/
const NO_RETRY_METHODS = [
"clickElement",
"executeScript",
"performActions",
"releaseActions",
"sendKeysToElement",
];
return new Proxy(
{},
{
get(target, methodName) {
return async (...args) => {
let attempts = 0;
while (true) {
try {
const browsingContext = browsingContextFn();
if (!browsingContext) {
throw new DOMException(
"No BrowsingContext found",
"NoBrowsingContext"
);
}
// TODO: Scenarios where the window/tab got closed and
// currentWindowGlobal is null will be handled in Bug 1662808.
const actor =
browsingContext.currentWindowGlobal.getActor(
"MarionetteCommands"
);
const result = await actor[methodName](...args);
return result;
} catch (e) {
if (!["AbortError", "InactiveActor"].includes(e.name)) {
// Only retry when the JSWindowActor pair gets destroyed, or
// gets inactive eg. when the page is moved into bfcache.
throw e;
}
if (NO_RETRY_METHODS.includes(methodName)) {
const browsingContextId = browsingContextFn()?.id;
lazy.logger.trace(
`[${browsingContextId}] Querying "${methodName}" failed with` +
` ${e.name}, returning "null" as fallback`
);
return null;
}
if (++attempts > MAX_ATTEMPTS) {
const browsingContextId = browsingContextFn()?.id;
lazy.logger.trace(
`[${browsingContextId}] Querying "${methodName} "` +
`reached the limit of retry attempts (${MAX_ATTEMPTS})`
);
throw e;
}
lazy.logger.trace(
`Retrying "${methodName}", attempt: ${attempts}`
);
}
}
};
},
}
);
}
/**
* Register the MarionetteCommands actor that holds all the commands.
*
* @param {string} sessionId
* The id of the current WebDriver session.
*/
export function registerCommandsActor(sessionId) {
try {
ChromeUtils.registerWindowActor("MarionetteCommands", {
kind: "JSWindowActor",
},
},
allFrames: true,
includeChrome: true,
});
} catch (e) {
if (e.name === "NotSupportedError") {
lazy.logger.warn(`MarionetteCommands actor is already registered!`);
} else {
throw e;
}
}
webDriverSessionId = sessionId;
}
export function unregisterCommandsActor() {
webDriverSessionId = null;
ChromeUtils.unregisterWindowActor("MarionetteCommands");
}