Merge mozilla-central to autoland. a=merge CLOSED TREE

This commit is contained in:
Natalia Csoregi 2023-05-16 15:37:05 +03:00
commit 4ab7fd28a6
45 changed files with 1412 additions and 2119 deletions

View file

@ -57,7 +57,6 @@ remote.jar:
content/shared/webdriver/Actions.sys.mjs (shared/webdriver/Actions.sys.mjs)
content/shared/webdriver/Assert.sys.mjs (shared/webdriver/Assert.sys.mjs)
content/shared/webdriver/Capabilities.sys.mjs (shared/webdriver/Capabilities.sys.mjs)
content/shared/webdriver/Element.sys.mjs (shared/webdriver/Element.sys.mjs)
content/shared/webdriver/Errors.sys.mjs (shared/webdriver/Errors.sys.mjs)
content/shared/webdriver/KeyData.sys.mjs (shared/webdriver/KeyData.sys.mjs)
content/shared/webdriver/NodeCache.sys.mjs (shared/webdriver/NodeCache.sys.mjs)

View file

@ -12,7 +12,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
accessibility: "chrome://remote/content/marionette/accessibility.sys.mjs",
action: "chrome://remote/content/shared/webdriver/Actions.sys.mjs",
atom: "chrome://remote/content/marionette/atom.sys.mjs",
element: "chrome://remote/content/shared/webdriver/Element.sys.mjs",
element: "chrome://remote/content/marionette/element.sys.mjs",
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
evaluate: "chrome://remote/content/marionette/evaluate.sys.mjs",
interaction: "chrome://remote/content/marionette/interaction.sys.mjs",
@ -21,7 +21,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
Log: "chrome://remote/content/shared/Log.sys.mjs",
sandbox: "chrome://remote/content/marionette/evaluate.sys.mjs",
Sandboxes: "chrome://remote/content/marionette/evaluate.sys.mjs",
WebReference: "chrome://remote/content/marionette/web-reference.sys.mjs",
});
XPCOMUtils.defineLazyGetter(lazy, "logger", () =>
@ -59,10 +58,6 @@ export class MarionetteCommandsChild extends JSWindowActorChild {
return this._legacyactions;
}
get #nodeCache() {
return this.#processActor.getNodeCache();
}
actorCreated() {
lazy.logger.trace(
`[${this.browsingContext.id}] MarionetteCommands actor created ` +
@ -87,12 +82,11 @@ export class MarionetteCommandsChild extends JSWindowActorChild {
let waitForNextTick = false;
const { name, data: serializedData } = msg;
const data = await lazy.json.deserialize({
value: serializedData,
browsingContext: this.contentWindow.browsingContext,
getKnownElement: this.#getKnownElement.bind(this),
getKnownShadowRoot: this.#getKnownShadowRoot.bind(this),
});
const data = lazy.json.deserialize(
serializedData,
this.#processActor.getNodeCache(),
this.contentWindow
);
switch (name) {
case "MarionetteCommandsParent:clearElement":
@ -190,11 +184,7 @@ export class MarionetteCommandsChild extends JSWindowActorChild {
}
return {
data: await lazy.json.clone({
value: result,
getOrCreateNodeReference: this.#getOrCreateNodeReference.bind(this),
browsingContext: this.contentWindow.browsingContext,
}),
data: lazy.json.clone(result, this.#processActor.getNodeCache()),
};
} catch (e) {
// Always wrap errors as WebDriverError
@ -616,148 +606,4 @@ export class MarionetteCommandsChild extends JSWindowActorChild {
return { browsingContextId: browsingContext.id };
}
// Private methods
/**
* Resolve element from specified web reference identifier.
*
* @param {BrowsingContext} browsingContext
* The browsing context to retrieve the element from.
* @param {string} nodeId
* The WebReference uuid for a DOM element.
*
* @returns {Element}
* The DOM element that the identifier was generated for.
*
* @throws {NoSuchElementError}
* If the element doesn't exist in the current browsing context.
* @throws {StaleElementReferenceError}
* If the element has gone stale, indicating its node document is no
* longer the active document or it is no longer attached to the DOM.
*/
async #getKnownElement(browsingContext, nodeId) {
const isKnown = await this.#isNodeReferenceKnown(browsingContext, nodeId);
if (!isKnown) {
throw new lazy.error.NoSuchElementError(
`The element with the reference ${nodeId} is not known in the current browsing context`
);
}
const node = this.#nodeCache.getNode(browsingContext, nodeId);
// Ensure the node is of the correct Node type.
if (node !== null && !lazy.element.isElement(node)) {
throw new lazy.error.NoSuchElementError(
`The element with the reference ${nodeId} is not of type HTMLElement`
);
}
// If null, which may be the case if the element has been unwrapped from a
// weak reference, it is always considered stale.
if (node === null || lazy.element.isStale(node)) {
throw new lazy.error.StaleElementReferenceError(
`The element with the reference ${nodeId} ` +
"is stale; either its node document is not the active document, " +
"or it is no longer connected to the DOM"
);
}
return node;
}
/**
* Resolve ShadowRoot from specified web reference identifier.
*
* @param {BrowsingContext} browsingContext
* The browsing context to retrieve the shadow root from.
* @param {string} nodeId
* The WebReference uuid for a ShadowRoot.
*
* @returns {ShadowRoot}
* The ShadowRoot that the identifier was generated for.
*
* @throws {NoSuchShadowRootError}
* If the ShadowRoot doesn't exist in the current browsing context.
* @throws {DetachedShadowRootError}
* If the ShadowRoot is detached, indicating its node document is no
* longer the active document or it is no longer attached to the DOM.
*/
async #getKnownShadowRoot(browsingContext, nodeId) {
const isKnown = await this.#isNodeReferenceKnown(browsingContext, nodeId);
if (!isKnown) {
throw new lazy.error.NoSuchShadowRootError(
`The shadow root with the reference ${nodeId} is not known in the current browsing context`
);
}
const node = this.#nodeCache.getNode(browsingContext, nodeId);
// Ensure the node is of the correct Node type.
if (node !== null && !lazy.element.isShadowRoot(node)) {
throw new lazy.error.NoSuchShadowRootError(
`The shadow root with the reference ${nodeId} is not of type ShadowRoot`
);
}
// If null, which may be the case if the element has been unwrapped from a
// weak reference, it is always considered stale.
if (node === null || lazy.element.isDetached(node)) {
throw new lazy.error.DetachedShadowRootError(
`The shadow root with the reference ${nodeId} ` +
"is detached; either its node document is not the active document, " +
"or it is no longer connected to the DOM"
);
}
return node;
}
/**
* Returns the WebReference for the given node.
*
* Hereby it tries to find a known node reference for that node in the
* node cache, and returns it. Otherwise it creates a new reference and
* adds it to the cache.
*
* @param {BrowsingContext} browsingContext
* The browsing context the element is part of.
* @param {Node} node
* The node to create or get a WebReference for.
*
* @returns {WebReference} The web reference for the node.
*/
async #getOrCreateNodeReference(browsingContext, node) {
const nodeRef = this.#nodeCache.getOrCreateNodeReference(node);
const webRef = lazy.WebReference.from(node, nodeRef);
await this.sendQuery("MarionetteCommandsChild:addNodeToSeenNodes", {
browsingContext,
nodeId: webRef.uuid,
});
return webRef;
}
/**
* Determines if the node reference is known for the given browsing context.
*
* For WebDriver classic only nodes from the same browsing context are
* allowed to be accessed.
*
* @param {BrowsingContext} browsingContext
* The browsing context the element has to be part of.
* @param {ElementIdentifier} nodeId
* The WebElement reference identifier for a DOM element.
*
* @returns {Promise<boolean>}
* A promise that resolved to True if the element is known in the given
* browsing context.
*/
#isNodeReferenceKnown(browsingContext, nodeId) {
return this.sendQuery("MarionetteCommandsChild:isNodeReferenceKnown", {
browsingContext,
nodeId,
});
}
}

View file

@ -10,17 +10,12 @@ ChromeUtils.defineESModuleGetters(lazy, {
capture: "chrome://remote/content/shared/Capture.sys.mjs",
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
Log: "chrome://remote/content/shared/Log.sys.mjs",
session: "chrome://remote/content/shared/webdriver/Session.sys.mjs",
});
XPCOMUtils.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 {
actorCreated() {
this._resolveDialogOpened = null;
@ -32,28 +27,6 @@ export class MarionetteCommandsParent extends JSWindowActorParent {
});
}
async receiveMessage(msg) {
const { name, data } = msg;
switch (name) {
case "MarionetteCommandsChild:addNodeToSeenNodes":
return lazy.session.addNodeToSeenNodes(
webDriverSessionId,
data.browsingContext,
data.nodeId
);
case "MarionetteCommandsChild:isNodeReferenceKnown":
return lazy.session.isNodeReferenceKnown(
webDriverSessionId,
data.browsingContext,
data.nodeId
);
}
return null;
}
async sendQuery(name, data) {
// return early if a dialog is opened
const result = await Promise.race([
@ -377,11 +350,8 @@ export function getMarionetteCommandsActorProxy(browsingContextFn) {
/**
* Register the MarionetteCommands actor that holds all the commands.
*
* @param {string} sessionId
* The id of the current WebDriver session.
*/
export function registerCommandsActor(sessionId) {
export function registerCommandsActor() {
try {
ChromeUtils.registerWindowActor("MarionetteCommands", {
kind: "JSWindowActor",
@ -404,12 +374,8 @@ export function registerCommandsActor(sessionId) {
throw e;
}
}
webDriverSessionId = sessionId;
}
export function unregisterCommandsActor() {
webDriverSessionId = null;
ChromeUtils.unregisterWindowActor("MarionetteCommands");
}

View file

@ -4,6 +4,12 @@
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import {
element,
ShadowRoot,
WebElement,
} from "chrome://remote/content/marionette/element.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
@ -18,7 +24,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
DebounceCallback: "chrome://remote/content/marionette/sync.sys.mjs",
disableEventsActor:
"chrome://remote/content/marionette/actors/MarionetteEventsParent.sys.mjs",
element: "chrome://remote/content/shared/webdriver/Element.sys.mjs",
enableEventsActor:
"chrome://remote/content/marionette/actors/MarionetteEventsParent.sys.mjs",
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
@ -39,7 +44,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
registerCommandsActor:
"chrome://remote/content/marionette/actors/MarionetteCommandsParent.sys.mjs",
RemoteAgent: "chrome://remote/content/components/RemoteAgent.sys.mjs",
ShadowRoot: "chrome://remote/content/marionette/web-reference.sys.mjs",
TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
TimedPromise: "chrome://remote/content/marionette/sync.sys.mjs",
Timeouts: "chrome://remote/content/shared/webdriver/Capabilities.sys.mjs",
@ -51,7 +55,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
"chrome://remote/content/shared/Navigate.sys.mjs",
waitForObserverTopic: "chrome://remote/content/marionette/sync.sys.mjs",
WebDriverSession: "chrome://remote/content/shared/webdriver/Session.sys.mjs",
WebElement: "chrome://remote/content/marionette/web-reference.sys.mjs",
windowManager: "chrome://remote/content/shared/WindowManager.sys.mjs",
WindowState: "chrome://remote/content/marionette/browser.sys.mjs",
});
@ -62,21 +65,16 @@ XPCOMUtils.defineLazyGetter(lazy, "logger", () =>
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
XPCOMUtils.defineLazyGetter(
lazy,
"supportedStrategies",
() =>
new Set([
lazy.element.Strategy.ClassName,
lazy.element.Strategy.Selector,
lazy.element.Strategy.ID,
lazy.element.Strategy.Name,
lazy.element.Strategy.LinkText,
lazy.element.Strategy.PartialLinkText,
lazy.element.Strategy.TagName,
lazy.element.Strategy.XPath,
])
);
const SUPPORTED_STRATEGIES = new Set([
element.Strategy.ClassName,
element.Strategy.Selector,
element.Strategy.ID,
element.Strategy.Name,
element.Strategy.LinkText,
element.Strategy.PartialLinkText,
element.Strategy.TagName,
element.Strategy.XPath,
]);
// Timeout used to abort fullscreen, maximize, and minimize
// commands if no window manager is present.
@ -475,7 +473,7 @@ GeckoDriver.prototype.newSession = async function(cmd) {
this.dialog = lazy.modal.findModalDialogs(this.curBrowser);
}
lazy.registerCommandsActor(this.currentSession.id);
lazy.registerCommandsActor();
lazy.enableEventsActor();
Services.obs.addObserver(this, TOPIC_BROWSER_READY);
@ -1361,7 +1359,7 @@ GeckoDriver.prototype.switchToFrame = async function(cmd) {
// Bug 1495063: Elements should be passed as WebReference reference
let byFrame;
if (typeof el == "string") {
byFrame = lazy.WebElement.fromUUID(el).toJSON();
byFrame = WebElement.fromUUID(el).toJSON();
} else if (el) {
byFrame = el;
}
@ -1404,7 +1402,7 @@ GeckoDriver.prototype.singleTap = async function(cmd) {
lazy.assert.open(this.getBrowsingContext());
let { id, x, y } = cmd.parameters;
let webEl = lazy.WebElement.fromUUID(id).toJSON();
let webEl = WebElement.fromUUID(id).toJSON();
await this.getActor().singleTap(
webEl,
@ -1487,7 +1485,7 @@ GeckoDriver.prototype.releaseActions = async function() {
GeckoDriver.prototype.findElement = async function(cmd) {
const { element: el, using, value } = cmd.parameters;
if (!lazy.supportedStrategies.has(using)) {
if (!SUPPORTED_STRATEGIES.has(using)) {
throw new lazy.error.InvalidSelectorError(
`Strategy not supported: ${using}`
);
@ -1499,7 +1497,7 @@ GeckoDriver.prototype.findElement = async function(cmd) {
let startNode;
if (typeof el != "undefined") {
startNode = lazy.WebElement.fromUUID(el).toJSON();
startNode = WebElement.fromUUID(el).toJSON();
}
let opts = {
@ -1541,7 +1539,7 @@ GeckoDriver.prototype.findElement = async function(cmd) {
GeckoDriver.prototype.findElementFromShadowRoot = async function(cmd) {
const { shadowRoot, using, value } = cmd.parameters;
if (!lazy.supportedStrategies.has(using)) {
if (!SUPPORTED_STRATEGIES.has(using)) {
throw new lazy.error.InvalidSelectorError(
`Strategy not supported: ${using}`
);
@ -1553,7 +1551,7 @@ GeckoDriver.prototype.findElementFromShadowRoot = async function(cmd) {
const opts = {
all: false,
startNode: lazy.ShadowRoot.fromUUID(shadowRoot).toJSON(),
startNode: ShadowRoot.fromUUID(shadowRoot).toJSON(),
timeout: this.currentSession.timeouts.implicit,
};
@ -1586,7 +1584,7 @@ GeckoDriver.prototype.findElementFromShadowRoot = async function(cmd) {
GeckoDriver.prototype.findElements = async function(cmd) {
const { element: el, using, value } = cmd.parameters;
if (!lazy.supportedStrategies.has(using)) {
if (!SUPPORTED_STRATEGIES.has(using)) {
throw new lazy.error.InvalidSelectorError(
`Strategy not supported: ${using}`
);
@ -1598,7 +1596,7 @@ GeckoDriver.prototype.findElements = async function(cmd) {
let startNode;
if (typeof el != "undefined") {
startNode = lazy.WebElement.fromUUID(el).toJSON();
startNode = WebElement.fromUUID(el).toJSON();
}
let opts = {
@ -1637,7 +1635,7 @@ GeckoDriver.prototype.findElements = async function(cmd) {
GeckoDriver.prototype.findElementsFromShadowRoot = async function(cmd) {
const { shadowRoot, using, value } = cmd.parameters;
if (!lazy.supportedStrategies.has(using)) {
if (!SUPPORTED_STRATEGIES.has(using)) {
throw new lazy.error.InvalidSelectorError(
`Strategy not supported: ${using}`
);
@ -1649,7 +1647,7 @@ GeckoDriver.prototype.findElementsFromShadowRoot = async function(cmd) {
const opts = {
all: true,
startNode: lazy.ShadowRoot.fromUUID(shadowRoot).toJSON(),
startNode: ShadowRoot.fromUUID(shadowRoot).toJSON(),
timeout: this.currentSession.timeouts.implicit,
};
@ -1690,7 +1688,7 @@ GeckoDriver.prototype.getShadowRoot = async function(cmd) {
cmd.parameters.id,
lazy.pprint`Expected "id" to be a string, got ${cmd.parameters.id}`
);
let webEl = lazy.WebElement.fromUUID(id).toJSON();
let webEl = WebElement.fromUUID(id).toJSON();
return this.getActor().getShadowRoot(webEl);
};
@ -1743,7 +1741,7 @@ GeckoDriver.prototype.clickElement = async function(cmd) {
await this._handleUserPrompts();
let id = lazy.assert.string(cmd.parameters.id);
let webEl = lazy.WebElement.fromUUID(id).toJSON();
let webEl = WebElement.fromUUID(id).toJSON();
const actor = this.getActor();
@ -1795,7 +1793,7 @@ GeckoDriver.prototype.getElementAttribute = async function(cmd) {
const id = lazy.assert.string(cmd.parameters.id);
const name = lazy.assert.string(cmd.parameters.name);
const webEl = lazy.WebElement.fromUUID(id).toJSON();
const webEl = WebElement.fromUUID(id).toJSON();
return this.getActor().getElementAttribute(webEl, name);
};
@ -1829,7 +1827,7 @@ GeckoDriver.prototype.getElementProperty = async function(cmd) {
const id = lazy.assert.string(cmd.parameters.id);
const name = lazy.assert.string(cmd.parameters.name);
const webEl = lazy.WebElement.fromUUID(id).toJSON();
const webEl = WebElement.fromUUID(id).toJSON();
return this.getActor().getElementProperty(webEl, name);
};
@ -1861,7 +1859,7 @@ GeckoDriver.prototype.getElementText = async function(cmd) {
await this._handleUserPrompts();
let id = lazy.assert.string(cmd.parameters.id);
let webEl = lazy.WebElement.fromUUID(id).toJSON();
let webEl = WebElement.fromUUID(id).toJSON();
return this.getActor().getElementText(webEl);
};
@ -1892,7 +1890,7 @@ GeckoDriver.prototype.getElementTagName = async function(cmd) {
await this._handleUserPrompts();
let id = lazy.assert.string(cmd.parameters.id);
let webEl = lazy.WebElement.fromUUID(id).toJSON();
let webEl = WebElement.fromUUID(id).toJSON();
return this.getActor().getElementTagName(webEl);
};
@ -1921,7 +1919,7 @@ GeckoDriver.prototype.isElementDisplayed = async function(cmd) {
await this._handleUserPrompts();
let id = lazy.assert.string(cmd.parameters.id);
let webEl = lazy.WebElement.fromUUID(id).toJSON();
let webEl = WebElement.fromUUID(id).toJSON();
return this.getActor().isElementDisplayed(
webEl,
@ -1958,7 +1956,7 @@ GeckoDriver.prototype.getElementValueOfCssProperty = async function(cmd) {
let id = lazy.assert.string(cmd.parameters.id);
let prop = lazy.assert.string(cmd.parameters.propertyName);
let webEl = lazy.WebElement.fromUUID(id).toJSON();
let webEl = WebElement.fromUUID(id).toJSON();
return this.getActor().getElementValueOfCssProperty(webEl, prop);
};
@ -1989,7 +1987,7 @@ GeckoDriver.prototype.isElementEnabled = async function(cmd) {
await this._handleUserPrompts();
let id = lazy.assert.string(cmd.parameters.id);
let webEl = lazy.WebElement.fromUUID(id).toJSON();
let webEl = WebElement.fromUUID(id).toJSON();
return this.getActor().isElementEnabled(
webEl,
@ -2021,7 +2019,7 @@ GeckoDriver.prototype.isElementSelected = async function(cmd) {
await this._handleUserPrompts();
let id = lazy.assert.string(cmd.parameters.id);
let webEl = lazy.WebElement.fromUUID(id).toJSON();
let webEl = WebElement.fromUUID(id).toJSON();
return this.getActor().isElementSelected(
webEl,
@ -2046,7 +2044,7 @@ GeckoDriver.prototype.getElementRect = async function(cmd) {
await this._handleUserPrompts();
let id = lazy.assert.string(cmd.parameters.id);
let webEl = lazy.WebElement.fromUUID(id).toJSON();
let webEl = WebElement.fromUUID(id).toJSON();
return this.getActor().getElementRect(webEl);
};
@ -2077,7 +2075,7 @@ GeckoDriver.prototype.sendKeysToElement = async function(cmd) {
let id = lazy.assert.string(cmd.parameters.id);
let text = lazy.assert.string(cmd.parameters.text);
let webEl = lazy.WebElement.fromUUID(id).toJSON();
let webEl = WebElement.fromUUID(id).toJSON();
return this.getActor().sendKeysToElement(
webEl,
@ -2109,7 +2107,7 @@ GeckoDriver.prototype.clearElement = async function(cmd) {
await this._handleUserPrompts();
let id = lazy.assert.string(cmd.parameters.id);
let webEl = lazy.WebElement.fromUUID(id).toJSON();
let webEl = WebElement.fromUUID(id).toJSON();
await this.getActor().clearElement(webEl);
};
@ -2465,7 +2463,7 @@ GeckoDriver.prototype.takeScreenshot = async function(cmd) {
full = typeof full == "undefined" ? true : full;
scroll = typeof scroll == "undefined" ? true : scroll;
let webEl = id ? lazy.WebElement.fromUUID(id).toJSON() : null;
let webEl = id ? WebElement.fromUUID(id).toJSON() : null;
// Only consider full screenshot if no element has been specified
full = webEl ? false : full;
@ -3262,7 +3260,7 @@ GeckoDriver.prototype.getComputedLabel = async function(cmd) {
await this._handleUserPrompts();
let id = lazy.assert.string(cmd.parameters.id);
let webEl = lazy.WebElement.fromUUID(id).toJSON();
let webEl = WebElement.fromUUID(id).toJSON();
return this.getActor().getComputedLabel(webEl);
};
@ -3283,7 +3281,7 @@ GeckoDriver.prototype.getComputedRole = async function(cmd) {
await this._handleUserPrompts();
let id = lazy.assert.string(cmd.parameters.id);
let webEl = lazy.WebElement.fromUUID(id).toJSON();
let webEl = WebElement.fromUUID(id).toJSON();
return this.getActor().getComputedRole(webEl);
};

View file

@ -5,9 +5,11 @@
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
atom: "chrome://remote/content/marionette/atom.sys.mjs",
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
PollPromise: "chrome://remote/content/marionette/sync.sys.mjs",
pprint: "chrome://remote/content/shared/Format.sys.mjs",
});
const ORDERED_NODE_ITERATOR_TYPE = 5;
@ -465,6 +467,102 @@ element.findClosest = function(startNode, selector) {
return null;
};
/**
* Resolve element from specified web reference identifier.
*
* @param {BrowsingContext} browsingContext
* The browsing context to retrieve the element from.
* @param {string} nodeId
* The WebReference uuid for a DOM element.
* @param {NodeCache} nodeCache
* Node cache that holds already seen WebElement and ShadowRoot references.
*
* @returns {Element}
* The DOM element that the identifier was generated for.
*
* @throws {NoSuchElementError}
* If the element doesn't exist in the current browsing context.
* @throws {StaleElementReferenceError}
* If the element has gone stale, indicating its node document is no
* longer the active document or it is no longer attached to the DOM.
*/
element.getKnownElement = function(browsingContext, nodeId, nodeCache) {
if (!element.isNodeReferenceKnown(browsingContext, nodeId, nodeCache)) {
throw new lazy.error.NoSuchElementError(
`The element with the reference ${nodeId} is not known in the current browsing context`
);
}
const node = nodeCache.getNode(browsingContext, nodeId);
// Ensure the node is of the correct Node type.
if (node !== null && !element.isElement(node)) {
throw new lazy.error.NoSuchElementError(
`The element with the reference ${nodeId} is not of type HTMLElement`
);
}
// If null, which may be the case if the element has been unwrapped from a
// weak reference, it is always considered stale.
if (node === null || element.isStale(node)) {
throw new lazy.error.StaleElementReferenceError(
`The element with the reference ${nodeId} ` +
"is stale; either its node document is not the active document, " +
"or it is no longer connected to the DOM"
);
}
return node;
};
/**
* Resolve ShadowRoot from specified web reference identifier.
*
* @param {BrowsingContext} browsingContext
* The browsing context to retrieve the shadow root from.
* @param {string} nodeId
* The WebReference uuid for a ShadowRoot.
* @param {NodeCache} nodeCache
* Node cache that holds already seen WebElement and ShadowRoot references.
*
* @returns {ShadowRoot}
* The ShadowRoot that the identifier was generated for.
*
* @throws {NoSuchShadowRootError}
* If the ShadowRoot doesn't exist in the current browsing context.
* @throws {DetachedShadowRootError}
* If the ShadowRoot is detached, indicating its node document is no
* longer the active document or it is no longer attached to the DOM.
*/
element.getKnownShadowRoot = function(browsingContext, nodeId, nodeCache) {
if (!element.isNodeReferenceKnown(browsingContext, nodeId, nodeCache)) {
throw new lazy.error.NoSuchShadowRootError(
`The shadow root with the reference ${nodeId} is not known in the current browsing context`
);
}
const node = nodeCache.getNode(browsingContext, nodeId);
// Ensure the node is of the correct Node type.
if (node !== null && !element.isShadowRoot(node)) {
throw new lazy.error.NoSuchShadowRootError(
`The shadow root with the reference ${nodeId} is not of type ShadowRoot`
);
}
// If null, which may be the case if the element has been unwrapped from a
// weak reference, it is always considered stale.
if (node === null || element.isDetached(node)) {
throw new lazy.error.DetachedShadowRootError(
`The shadow root with the reference ${nodeId} ` +
"is detached; either its node document is not the active document, " +
"or it is no longer connected to the DOM"
);
}
return node;
};
/**
* Determines if <var>obj<var> is an HTML or JS collection.
*
@ -510,6 +608,39 @@ element.isDetached = function(shadowRoot) {
);
};
/**
* Determines if the node reference is known for the given browsing context.
*
* For WebDriver classic only nodes from the same browsing context are
* allowed to be accessed.
*
* @param {BrowsingContext} browsingContext
* The browsing context the element has to be part of.
* @param {ElementIdentifier} nodeId
* The WebElement reference identifier for a DOM element.
* @param {NodeCache} nodeCache
* Node cache that holds already seen node references.
*
* @returns {boolean}
* True if the element is known in the given browsing context.
*/
element.isNodeReferenceKnown = function(browsingContext, nodeId, nodeCache) {
const nodeDetails = nodeCache.getReferenceDetails(nodeId);
if (nodeDetails === null) {
return false;
}
if (nodeDetails.isTopBrowsingContext) {
// As long as Navigables are not available any cross-group navigation will
// cause a swap of the current top-level browsing context. The only unique
// identifier in such a case is the browser id the top-level browsing
// context actually lives in.
return nodeDetails.browserId === browsingContext.browserId;
}
return nodeDetails.browsingContextId === browsingContext.id;
};
/**
* Determines if <var>el</var> is stale.
*
@ -1213,3 +1344,287 @@ element.isBooleanAttribute = function(el, attr) {
}
return boolEls[el.localName].includes(attr);
};
/**
* A web reference is an abstraction used to identify an element when
* it is transported via the protocol, between remote- and local ends.
*
* In Marionette this abstraction can represent DOM elements,
* WindowProxies, and XUL elements.
*/
export class WebReference {
/**
* @param {string} uuid
* Identifier that must be unique across all browsing contexts
* for the contract to be upheld.
*/
constructor(uuid) {
this.uuid = lazy.assert.string(uuid);
}
/**
* Performs an equality check between this web element and
* <var>other</var>.
*
* @param {WebReference} other
* Web element to compare with this.
*
* @returns {boolean}
* True if this and <var>other</var> are the same. False
* otherwise.
*/
is(other) {
return other instanceof WebReference && this.uuid === other.uuid;
}
toString() {
return `[object ${this.constructor.name} uuid=${this.uuid}]`;
}
/**
* Returns a new {@link WebReference} reference for a DOM or XUL element,
* <code>WindowProxy</code>, or <code>ShadowRoot</code>.
*
* @param {(Element|ShadowRoot|WindowProxy|MockXULElement)} node
* Node to construct a web element reference for.
* @param {string=} uuid
* Optional unique identifier of the WebReference if already known.
* If not defined a new unique identifier will be created.
*
* @returns {WebReference}
* Web reference for <var>node</var>.
*
* @throws {InvalidArgumentError}
* If <var>node</var> is neither a <code>WindowProxy</code>,
* DOM or XUL element, or <code>ShadowRoot</code>.
*/
static from(node, uuid) {
if (uuid === undefined) {
uuid = element.generateUUID();
}
if (element.isShadowRoot(node) && !element.isInPrivilegedDocument(node)) {
// When we support Chrome Shadowroots we will need to
// do a check here of shadowroot.host being in a privileged document
// See Bug 1743541
return new ShadowRoot(uuid);
} else if (element.isElement(node)) {
return new WebElement(uuid);
} else if (element.isDOMWindow(node)) {
if (node.parent === node) {
return new WebWindow(uuid);
}
return new WebFrame(uuid);
}
throw new lazy.error.InvalidArgumentError(
"Expected DOM window/element " + lazy.pprint`or XUL element, got: ${node}`
);
}
/**
* Unmarshals a JSON Object to one of {@link ShadowRoot}, {@link WebElement},
* {@link WebFrame}, or {@link WebWindow}.
*
* @param {Object<string, string>} json
* Web reference, which is supposed to be a JSON Object
* where the key is one of the {@link WebReference} concrete
* classes' UUID identifiers.
*
* @returns {WebReference}
* Web reference for the JSON object.
*
* @throws {InvalidArgumentError}
* If <var>json</var> is not a web reference.
*/
static fromJSON(json) {
lazy.assert.object(json);
if (json instanceof WebReference) {
return json;
}
let keys = Object.keys(json);
for (let key of keys) {
switch (key) {
case ShadowRoot.Identifier:
return ShadowRoot.fromJSON(json);
case WebElement.Identifier:
return WebElement.fromJSON(json);
case WebFrame.Identifier:
return WebFrame.fromJSON(json);
case WebWindow.Identifier:
return WebWindow.fromJSON(json);
}
}
throw new lazy.error.InvalidArgumentError(
lazy.pprint`Expected web reference, got: ${json}`
);
}
/**
* Checks if <var>obj<var> is a {@link WebReference} reference.
*
* @param {Object<string, string>} obj
* Object that represents a {@link WebReference}.
*
* @returns {boolean}
* True if <var>obj</var> is a {@link WebReference}, false otherwise.
*/
static isReference(obj) {
if (Object.prototype.toString.call(obj) != "[object Object]") {
return false;
}
if (
ShadowRoot.Identifier in obj ||
WebElement.Identifier in obj ||
WebFrame.Identifier in obj ||
WebWindow.Identifier in obj
) {
return true;
}
return false;
}
}
/**
* DOM elements are represented as web elements when they are
* transported over the wire protocol.
*/
export class WebElement extends WebReference {
toJSON() {
return { [WebElement.Identifier]: this.uuid };
}
static fromJSON(json) {
const { Identifier } = WebElement;
if (!(Identifier in json)) {
throw new lazy.error.InvalidArgumentError(
lazy.pprint`Expected web element reference, got: ${json}`
);
}
let uuid = json[Identifier];
return new WebElement(uuid);
}
/**
* Constructs a {@link WebElement} from a string <var>uuid</var>.
*
* This whole function is a workaround for the fact that clients
* to Marionette occasionally pass <code>{id: <uuid>}</code> JSON
* Objects instead of web element representations.
*
* @param {string} uuid
* UUID to be associated with the web reference.
*
* @returns {WebElement}
* The web element reference.
*
* @throws {InvalidArgumentError}
* If <var>uuid</var> is not a string.
*/
static fromUUID(uuid) {
return new WebElement(uuid);
}
}
WebElement.Identifier = "element-6066-11e4-a52e-4f735466cecf";
/**
* Shadow Root elements are represented as shadow root references when they are
* transported over the wire protocol
*/
export class ShadowRoot extends WebReference {
toJSON() {
return { [ShadowRoot.Identifier]: this.uuid };
}
static fromJSON(json) {
const { Identifier } = ShadowRoot;
if (!(Identifier in json)) {
throw new lazy.error.InvalidArgumentError(
lazy.pprint`Expected shadow root reference, got: ${json}`
);
}
let uuid = json[Identifier];
return new ShadowRoot(uuid);
}
/**
* Constructs a {@link ShadowRoot} from a string <var>uuid</var>.
*
* This whole function is a workaround for the fact that clients
* to Marionette occasionally pass <code>{id: <uuid>}</code> JSON
* Objects instead of shadow root representations.
*
* @param {string} uuid
* UUID to be associated with the web reference.
*
* @returns {ShadowRoot}
* The shadow root reference.
*
* @throws {InvalidArgumentError}
* If <var>uuid</var> is not a string.
*/
static fromUUID(uuid) {
lazy.assert.string(uuid);
return new ShadowRoot(uuid);
}
}
ShadowRoot.Identifier = "shadow-6066-11e4-a52e-4f735466cecf";
/**
* Top-level browsing contexts, such as <code>WindowProxy</code>
* whose <code>opener</code> is null, are represented as web windows
* over the wire protocol.
*/
export class WebWindow extends WebReference {
toJSON() {
return { [WebWindow.Identifier]: this.uuid };
}
static fromJSON(json) {
if (!(WebWindow.Identifier in json)) {
throw new lazy.error.InvalidArgumentError(
lazy.pprint`Expected web window reference, got: ${json}`
);
}
let uuid = json[WebWindow.Identifier];
return new WebWindow(uuid);
}
}
WebWindow.Identifier = "window-fcc6-11e5-b4f8-330a88ab9d7f";
/**
* Nested browsing contexts, such as the <code>WindowProxy</code>
* associated with <tt>&lt;frame&gt;</tt> and <tt>&lt;iframe&gt;</tt>,
* are represented as web frames over the wire protocol.
*/
export class WebFrame extends WebReference {
toJSON() {
return { [WebFrame.Identifier]: this.uuid };
}
static fromJSON(json) {
if (!(WebFrame.Identifier in json)) {
throw new lazy.error.InvalidArgumentError(
lazy.pprint`Expected web frame reference, got: ${json}`
);
}
let uuid = json[WebFrame.Identifier];
return new WebFrame(uuid);
}
}
WebFrame.Identifier = "frame-075b-4da1-b6ba-e579c2d3230a";

View file

@ -11,7 +11,7 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
accessibility: "chrome://remote/content/marionette/accessibility.sys.mjs",
atom: "chrome://remote/content/marionette/atom.sys.mjs",
element: "chrome://remote/content/shared/webdriver/Element.sys.mjs",
element: "chrome://remote/content/marionette/element.sys.mjs",
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
event: "chrome://remote/content/marionette/event.sys.mjs",
Log: "chrome://remote/content/shared/Log.sys.mjs",

View file

@ -17,6 +17,7 @@ remote.jar:
content/marionette/cert.sys.mjs (cert.sys.mjs)
content/marionette/cookie.sys.mjs (cookie.sys.mjs)
content/marionette/driver.sys.mjs (driver.sys.mjs)
content/marionette/element.sys.mjs (element.sys.mjs)
content/marionette/evaluate.sys.mjs (evaluate.sys.mjs)
content/marionette/event.sys.mjs (event.sys.mjs)
content/marionette/interaction.sys.mjs (interaction.sys.mjs)
@ -36,7 +37,6 @@ remote.jar:
content/marionette/stream-utils.sys.mjs (stream-utils.sys.mjs)
content/marionette/sync.sys.mjs (sync.sys.mjs)
content/marionette/transport.sys.mjs (transport.sys.mjs)
content/marionette/web-reference.sys.mjs (web-reference.sys.mjs)
#ifdef ENABLE_TESTS
content/marionette/test_dialog.dtd (chrome/test_dialog.dtd)
content/marionette/test_dialog.properties (chrome/test_dialog.properties)

View file

@ -7,13 +7,13 @@ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
element: "chrome://remote/content/shared/webdriver/Element.sys.mjs",
element: "chrome://remote/content/marionette/element.sys.mjs",
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
Log: "chrome://remote/content/shared/Log.sys.mjs",
pprint: "chrome://remote/content/shared/Format.sys.mjs",
ShadowRoot: "chrome://remote/content/marionette/web-reference.sys.mjs",
WebElement: "chrome://remote/content/marionette/web-reference.sys.mjs",
WebReference: "chrome://remote/content/marionette/web-reference.sys.mjs",
ShadowRoot: "chrome://remote/content/marionette/element.sys.mjs",
WebElement: "chrome://remote/content/marionette/element.sys.mjs",
WebReference: "chrome://remote/content/marionette/element.sys.mjs",
});
XPCOMUtils.defineLazyGetter(lazy, "logger", () =>
@ -34,10 +34,10 @@ export const json = {};
* The clone algorithm to invoke for individual list entries or object
* properties.
*
* @returns {Promise<object>}
* A promise that resolves to the cloned object.
* @returns {object}
* The cloned object.
*/
async function cloneObject(value, seen, cloneAlgorithm) {
function cloneObject(value, seen, cloneAlgorithm) {
// Only proceed with cloning an object if it hasn't been seen yet.
if (seen.has(value)) {
throw new lazy.error.JavaScriptError("Cyclic object value");
@ -47,15 +47,13 @@ async function cloneObject(value, seen, cloneAlgorithm) {
let result;
if (lazy.element.isCollection(value)) {
result = await Promise.all(
[...value].map(entry => cloneAlgorithm(entry, seen))
);
result = [...value].map(entry => cloneAlgorithm(entry, seen));
} else {
// arbitrary objects
result = {};
for (let prop in value) {
try {
result[prop] = await cloneAlgorithm(value[prop], seen);
result[prop] = cloneAlgorithm(value[prop], seen);
} catch (e) {
if (e.result == Cr.NS_ERROR_NOT_IMPLEMENTED) {
lazy.logger.debug(`Skipping ${prop}: ${e.message}`);
@ -92,18 +90,14 @@ async function cloneObject(value, seen, cloneAlgorithm) {
*
* - If a cyclic references is detected a JavaScriptError is thrown.
*
* @param {object} options
* @param {BrowsingContext} options.browsingContext
* Current browsing context.
* @param {Function} options.getOrCreateNodeReference
* Callback that tries to use a known node reference from the node cache,
* or creates a new one if not present yet.
* @param {object} options.value
* @param {object} value
* Object to be cloned.
* @param {NodeCache} nodeCache
* Node cache that holds already seen WebElement and ShadowRoot references.
*
* @returns {Promise<object>}
* Promise that resolves to the same object as provided by `value` with
* the WebDriver specific elements replaced by WebReference's.
* @returns {object}
* Same object as provided by `value` with the WebDriver specific
* elements replaced by WebReference's.
*
* @throws {JavaScriptError}
* If an object contains cyclic references.
@ -111,18 +105,8 @@ async function cloneObject(value, seen, cloneAlgorithm) {
* If the element has gone stale, indicating it is no longer
* attached to the DOM.
*/
json.clone = async function(options) {
const { browsingContext, getOrCreateNodeReference, value } = options;
if (typeof browsingContext === "undefined") {
throw new TypeError("Browsing context not specified");
}
if (typeof getOrCreateNodeReference !== "function") {
throw new TypeError("Invalid callback for 'getOrCreateNodeReference'");
}
async function cloneJSON(value, seen) {
json.clone = function(value, nodeCache) {
function cloneJSON(value, seen) {
if (seen === undefined) {
seen = new Set();
}
@ -159,8 +143,8 @@ json.clone = async function(options) {
);
}
const ref = await getOrCreateNodeReference(browsingContext, value);
return ref.toJSON();
const nodeRef = nodeCache.getOrCreateNodeReference(value);
return lazy.WebReference.from(value, nodeRef).toJSON();
}
if (isNode && lazy.element.isShadowRoot(value)) {
@ -173,8 +157,8 @@ json.clone = async function(options) {
);
}
const ref = await getOrCreateNodeReference(browsingContext, value);
return ref.toJSON();
const nodeRef = nodeCache.getOrCreateNodeReference(value);
return lazy.WebReference.from(value, nodeRef).toJSON();
}
if (typeof value.toJSON == "function") {
@ -199,50 +183,24 @@ json.clone = async function(options) {
/**
* Deserialize an arbitrary object.
*
* @param {object} options
* @param {BrowsingContext} options.browsingContext
* Current browsing context.
* @param {Function} options.getKnownElement
* Callback that will try to resolve a WebElement reference to an Element node.
* @param {Function} options.getKnownShadowRoot
* Callback that will try to resolve a ShadowRoot reference to a ShadowRoot node.
* @param {object} options.value
* @param {object} value
* Arbitrary object.
* @param {NodeCache} nodeCache
* Node cache that holds already seen WebElement and ShadowRoot references.
* @param {WindowProxy} win
* Current window.
*
* @returns {Promise<object>}
* Promise that resolves to the same object as provided by `value` with the
* WebDriver specific references replaced with real JavaScript objects.
* @returns {object}
* Same object as provided by `value` with the WebDriver specific
* references replaced with real JavaScript objects.
*
* @throws {DetachedShadowRootError}
* If the ShadowRoot is detached, indicating it is no longer attached to the DOM.
* @throws {NoSuchElementError}
* If the WebElement reference has not been seen before.
* @throws {NoSuchShadowRootError}
* If the ShadowRoot reference has not been seen before.
* @throws {StaleElementReferenceError}
* If the element is stale, indicating it is no longer attached to the DOM.
*/
json.deserialize = async function(options) {
const {
browsingContext,
getKnownElement,
getKnownShadowRoot,
value,
} = options;
if (typeof browsingContext === "undefined") {
throw new TypeError("Browsing context not specified");
}
if (typeof getKnownElement !== "function") {
throw new TypeError("Invalid callback for 'getKnownElement'");
}
if (typeof getKnownShadowRoot !== "function") {
throw new TypeError("Invalid callback for 'getKnownShadowRoot'");
}
async function deserializeJSON(value, seen) {
json.deserialize = function(value, nodeCache, win) {
function deserializeJSON(value, seen) {
if (seen === undefined) {
seen = new Set();
}
@ -264,11 +222,19 @@ json.deserialize = async function(options) {
const webRef = lazy.WebReference.fromJSON(value);
if (webRef instanceof lazy.ShadowRoot) {
return getKnownShadowRoot(browsingContext, webRef.uuid);
return lazy.element.getKnownShadowRoot(
win.browsingContext,
webRef.uuid,
nodeCache
);
}
if (webRef instanceof lazy.WebElement) {
return getKnownElement(browsingContext, webRef.uuid);
return lazy.element.getKnownElement(
win.browsingContext,
webRef.uuid,
nodeCache
);
}
// WebFrame and WebWindow not supported yet

View file

@ -12,12 +12,12 @@ ChromeUtils.defineESModuleGetters(lazy, {
Preferences: "resource://gre/modules/Preferences.sys.mjs",
accessibility: "chrome://remote/content/marionette/accessibility.sys.mjs",
element: "chrome://remote/content/shared/webdriver/Element.sys.mjs",
element: "chrome://remote/content/marionette/element.sys.mjs",
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
json: "chrome://remote/content/marionette/json.sys.mjs",
event: "chrome://remote/content/marionette/event.sys.mjs",
Log: "chrome://remote/content/shared/Log.sys.mjs",
WebElement: "chrome://remote/content/shared/webdriver/Element.sys.mjs",
WebElement: "chrome://remote/content/marionette/element.sys.mjs",
});
XPCOMUtils.defineLazyGetter(lazy, "logger", () =>

View file

@ -2,11 +2,23 @@
* 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 { element } = ChromeUtils.importESModule(
"chrome://remote/content/shared/webdriver/Element.sys.mjs"
const {
element,
ShadowRoot,
WebElement,
WebFrame,
WebReference,
WebWindow,
} = ChromeUtils.importESModule(
"chrome://remote/content/marionette/element.sys.mjs"
);
const { NodeCache } = ChromeUtils.importESModule(
"chrome://remote/content/shared/webdriver/NodeCache.sys.mjs"
);
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const MemoryReporter = Cc["@mozilla.org/memory-reporter-manager;1"].getService(
Ci.nsIMemoryReporterManager
);
class MockElement {
constructor(tagName, attrs = {}) {
@ -91,6 +103,7 @@ function setupTest() {
return {
browser,
nodeCache: new NodeCache(),
childEl,
divEl,
iframeEl,
@ -442,6 +455,142 @@ add_task(function test_coordinates() {
);
});
add_task(function test_isNodeReferenceKnown() {
const { browser, nodeCache, childEl, iframeEl, videoEl } = setupTest();
// Unknown node reference
ok(!element.isNodeReferenceKnown(browser.browsingContext, "foo", nodeCache));
// Known node reference
const videoElRef = nodeCache.getOrCreateNodeReference(videoEl);
ok(
element.isNodeReferenceKnown(browser.browsingContext, videoElRef, nodeCache)
);
// Different top-level browsing context
const browser2 = Services.appShell.createWindowlessBrowser(false);
ok(
!element.isNodeReferenceKnown(
browser2.browsingContext,
videoElRef,
nodeCache
)
);
// Different child browsing context
const childElRef = nodeCache.getOrCreateNodeReference(childEl);
const childBrowsingContext = iframeEl.contentWindow.browsingContext;
ok(element.isNodeReferenceKnown(childBrowsingContext, childElRef, nodeCache));
const iframeEl2 = browser2.document.createElement("iframe");
browser2.document.body.appendChild(iframeEl2);
const childBrowsingContext2 = iframeEl2.contentWindow.browsingContext;
ok(
!element.isNodeReferenceKnown(childBrowsingContext2, childElRef, nodeCache)
);
});
add_task(function test_getKnownElement() {
const { browser, nodeCache, shadowRoot, videoEl } = setupTest();
// Unknown element reference
Assert.throws(() => {
element.getKnownElement(browser.browsingContext, "foo", nodeCache);
}, /NoSuchElementError/);
// With a ShadowRoot reference
const shadowRootRef = nodeCache.getOrCreateNodeReference(shadowRoot);
Assert.throws(() => {
element.getKnownElement(browser.browsingContext, shadowRootRef, nodeCache);
}, /NoSuchElementError/);
// Deleted element (eg. garbage collected)
let detachedEl = browser.document.createElement("div");
const detachedElRef = nodeCache.getOrCreateNodeReference(detachedEl);
// ... not connected to the DOM
Assert.throws(() => {
element.getKnownElement(browser.browsingContext, detachedElRef, nodeCache);
}, /StaleElementReferenceError/);
// ... element garbage collected
detachedEl = null;
MemoryReporter.minimizeMemoryUsage(() => {
Assert.throws(() => {
element.getKnownElement(
browser.browsingContext,
detachedElRef,
nodeCache
);
}, /StaleElementReferenceError/);
});
// Known element reference
const videoElRef = nodeCache.getOrCreateNodeReference(videoEl);
equal(
element.getKnownElement(browser.browsingContext, videoElRef, nodeCache),
videoEl
);
});
add_task(function test_getKnownShadowRoot() {
const { browser, nodeCache, shadowRoot, videoEl } = setupTest();
const videoElRef = nodeCache.getOrCreateNodeReference(videoEl);
// Unknown ShadowRoot reference
Assert.throws(() => {
element.getKnownShadowRoot(browser.browsingContext, "foo", nodeCache);
}, /NoSuchShadowRootError/);
// With a HTMLElement reference
Assert.throws(() => {
element.getKnownShadowRoot(browser.browsingContext, videoElRef, nodeCache);
}, /NoSuchShadowRootError/);
// Known ShadowRoot reference
const shadowRootRef = nodeCache.getOrCreateNodeReference(shadowRoot);
equal(
element.getKnownShadowRoot(
browser.browsingContext,
shadowRootRef,
nodeCache
),
shadowRoot
);
// Detached ShadowRoot host
let el = browser.document.createElement("div");
let detachedShadowRoot = el.attachShadow({ mode: "open" });
detachedShadowRoot.innerHTML = "<input></input>";
const detachedShadowRootRef = nodeCache.getOrCreateNodeReference(
detachedShadowRoot
);
// ... not connected to the DOM
Assert.throws(() => {
element.getKnownShadowRoot(
browser.browsingContext,
detachedShadowRootRef,
nodeCache
);
}, /DetachedShadowRootError/);
// ... host and shadow root garbage collected
el = null;
detachedShadowRoot = null;
MemoryReporter.minimizeMemoryUsage(() => {
Assert.throws(() => {
element.getKnownShadowRoot(
browser.browsingContext,
detachedShadowRootRef,
nodeCache
);
}, /DetachedShadowRootError/);
});
});
add_task(function test_isDetached() {
const { childEl, iframeEl } = setupTest();
@ -475,3 +624,177 @@ add_task(function test_isStale() {
childEl.remove();
ok(element.isStale(childEl));
});
add_task(function test_WebReference_ctor() {
const el = new WebReference("foo");
equal(el.uuid, "foo");
for (let t of [42, true, [], {}, null, undefined]) {
Assert.throws(() => new WebReference(t), /to be a string/);
}
});
add_task(function test_WebElemenet_is() {
const a = new WebReference("a");
const b = new WebReference("b");
ok(a.is(a));
ok(b.is(b));
ok(!a.is(b));
ok(!b.is(a));
ok(!a.is({}));
});
add_task(function test_WebReference_from() {
const { divEl, iframeEl } = setupTest();
ok(WebReference.from(divEl) instanceof WebElement);
ok(WebReference.from(xulEl) instanceof WebElement);
ok(WebReference.from(divEl.ownerGlobal) instanceof WebWindow);
ok(WebReference.from(iframeEl.contentWindow) instanceof WebFrame);
ok(WebReference.from(domElInPrivilegedDocument) instanceof WebElement);
ok(WebReference.from(xulElInPrivilegedDocument) instanceof WebElement);
Assert.throws(() => WebReference.from({}), /InvalidArgumentError/);
});
add_task(function test_WebReference_fromJSON_WebElement() {
const { Identifier } = WebElement;
const ref = { [Identifier]: "foo" };
const webEl = WebReference.fromJSON(ref);
ok(webEl instanceof WebElement);
equal(webEl.uuid, "foo");
let identifierPrecedence = {
[Identifier]: "identifier-uuid",
};
const precedenceEl = WebReference.fromJSON(identifierPrecedence);
ok(precedenceEl instanceof WebElement);
equal(precedenceEl.uuid, "identifier-uuid");
});
add_task(function test_WebReference_fromJSON_WebWindow() {
const ref = { [WebWindow.Identifier]: "foo" };
const win = WebReference.fromJSON(ref);
ok(win instanceof WebWindow);
equal(win.uuid, "foo");
});
add_task(function test_WebReference_fromJSON_WebFrame() {
const ref = { [WebFrame.Identifier]: "foo" };
const frame = WebReference.fromJSON(ref);
ok(frame instanceof WebFrame);
equal(frame.uuid, "foo");
});
add_task(function test_WebReference_fromJSON_malformed() {
Assert.throws(() => WebReference.fromJSON({}), /InvalidArgumentError/);
Assert.throws(() => WebReference.fromJSON(null), /InvalidArgumentError/);
});
add_task(function test_WebReference_isReference() {
for (let t of [42, true, "foo", [], {}]) {
ok(!WebReference.isReference(t));
}
ok(WebReference.isReference({ [WebElement.Identifier]: "foo" }));
ok(WebReference.isReference({ [WebWindow.Identifier]: "foo" }));
ok(WebReference.isReference({ [WebFrame.Identifier]: "foo" }));
});
add_task(function test_generateUUID() {
equal(typeof element.generateUUID(), "string");
});
add_task(function test_WebElement_toJSON() {
const { Identifier } = WebElement;
const el = new WebElement("foo");
const json = el.toJSON();
ok(Identifier in json);
equal(json[Identifier], "foo");
});
add_task(function test_WebElement_fromJSON() {
const { Identifier } = WebElement;
const el = WebElement.fromJSON({ [Identifier]: "foo" });
ok(el instanceof WebElement);
equal(el.uuid, "foo");
Assert.throws(() => WebElement.fromJSON({}), /InvalidArgumentError/);
});
add_task(function test_WebElement_fromUUID() {
const domWebEl = WebElement.fromUUID("bar");
ok(domWebEl instanceof WebElement);
equal(domWebEl.uuid, "bar");
Assert.throws(() => WebElement.fromUUID(), /InvalidArgumentError/);
});
add_task(function test_ShadowRoot_toJSON() {
const { Identifier } = ShadowRoot;
const shadowRoot = new ShadowRoot("foo");
const json = shadowRoot.toJSON();
ok(Identifier in json);
equal(json[Identifier], "foo");
});
add_task(function test_ShadowRoot_fromJSON() {
const { Identifier } = ShadowRoot;
const shadowRoot = ShadowRoot.fromJSON({ [Identifier]: "foo" });
ok(shadowRoot instanceof ShadowRoot);
equal(shadowRoot.uuid, "foo");
Assert.throws(() => ShadowRoot.fromJSON({}), /InvalidArgumentError/);
});
add_task(function test_ShadowRoot_fromUUID() {
const shadowRoot = ShadowRoot.fromUUID("baz");
ok(shadowRoot instanceof ShadowRoot);
equal(shadowRoot.uuid, "baz");
Assert.throws(() => ShadowRoot.fromUUID(), /InvalidArgumentError/);
});
add_task(function test_WebWindow_toJSON() {
const win = new WebWindow("foo");
const json = win.toJSON();
ok(WebWindow.Identifier in json);
equal(json[WebWindow.Identifier], "foo");
});
add_task(function test_WebWindow_fromJSON() {
const ref = { [WebWindow.Identifier]: "foo" };
const win = WebWindow.fromJSON(ref);
ok(win instanceof WebWindow);
equal(win.uuid, "foo");
});
add_task(function test_WebFrame_toJSON() {
const frame = new WebFrame("foo");
const json = frame.toJSON();
ok(WebFrame.Identifier in json);
equal(json[WebFrame.Identifier], "foo");
});
add_task(function test_WebFrame_fromJSON() {
const ref = { [WebFrame.Identifier]: "foo" };
const win = WebFrame.fromJSON(ref);
ok(win instanceof WebFrame);
equal(win.uuid, "foo");
});

View file

@ -5,7 +5,7 @@ const { NodeCache } = ChromeUtils.importESModule(
"chrome://remote/content/shared/webdriver/NodeCache.sys.mjs"
);
const { ShadowRoot, WebElement, WebReference } = ChromeUtils.importESModule(
"chrome://remote/content/marionette/web-reference.sys.mjs"
"chrome://remote/content/marionette/element.sys.mjs"
);
function setupTest() {
@ -27,164 +27,61 @@ function setupTest() {
browser.document.body.appendChild(iframeEl);
const childEl = iframeEl.contentDocument.createElement("div");
return {
browser,
browsingContext: browser.browsingContext,
nodeCache,
childEl,
iframeEl,
htmlEl,
shadowRoot,
svgEl,
};
return { browser, nodeCache, childEl, iframeEl, htmlEl, shadowRoot, svgEl };
}
function clone(options = {}) {
const {
browsingContext,
getOrCreateNodeReference = async () =>
ok(false, "'getOrCreateNodeReference' called"),
value,
} = options;
return json.clone({ browsingContext, getOrCreateNodeReference, value });
}
function deserialize(options = {}) {
const {
browsingContext,
getKnownElement = async () => ok(false, "'getKnownElement' called"),
getKnownShadowRoot = async () => ok(false, "'getKnownShadowRoot' called"),
value,
} = options;
return json.deserialize({
browsingContext,
getKnownElement,
getKnownShadowRoot,
value,
});
}
add_task(async function test_clone_generalTypes() {
const { browsingContext } = setupTest();
add_task(function test_clone_generalTypes() {
const { nodeCache } = setupTest();
// null
equal(await clone({ browsingContext, value: undefined }), null);
equal(await clone({ browsingContext, value: null }), null);
equal(json.clone(undefined, nodeCache), null);
equal(json.clone(null, nodeCache), null);
// primitives
equal(await clone({ browsingContext, value: true }), true);
equal(await clone({ browsingContext, value: 42 }), 42);
equal(await clone({ browsingContext, value: "foo" }), "foo");
equal(json.clone(true, nodeCache), true);
equal(json.clone(42, nodeCache), 42);
equal(json.clone("foo", nodeCache), "foo");
// toJSON
equal(
await clone({
browsingContext,
value: {
toJSON() {
return "foo";
},
json.clone({
toJSON() {
return "foo";
},
}),
"foo"
);
});
add_task(async function test_clone_ShadowRoot() {
const { browsingContext, nodeCache, shadowRoot } = setupTest();
async function getOrCreateNodeReference(bc, node) {
equal(bc, browsingContext);
equal(node, shadowRoot);
const nodeRef = nodeCache.getOrCreateNodeReference(node);
return WebReference.from(node, nodeRef);
}
// Fails with missing browsing context
await Assert.rejects(
json.clone({ getOrCreateNodeReference, value: shadowRoot }),
/TypeError/,
"Missing getOrCreateNodeReference callback expected to throw"
);
// Fails with missing getOrCreateNodeReference callback
await Assert.rejects(
json.clone({ browsingContext, value: shadowRoot }),
/TypeError/,
"Missing getOrCreateNodeReference callback expected to throw"
);
add_task(function test_clone_ShadowRoot() {
const { nodeCache, shadowRoot } = setupTest();
const shadowRootRef = nodeCache.getOrCreateNodeReference(shadowRoot);
deepEqual(
await clone({
browsingContext,
getOrCreateNodeReference,
value: shadowRoot,
}),
json.clone(shadowRoot, nodeCache),
WebReference.from(shadowRoot, shadowRootRef).toJSON()
);
});
add_task(async function test_clone_WebElement() {
const { browsingContext, htmlEl, nodeCache, svgEl } = setupTest();
async function getOrCreateNodeReference(bc, node) {
equal(bc, browsingContext);
ok([htmlEl, svgEl].includes(node));
const nodeRef = nodeCache.getOrCreateNodeReference(node);
return WebReference.from(node, nodeRef);
}
// Fails with missing browsing context
await Assert.rejects(
json.clone({ getOrCreateNodeReference, value: htmlEl }),
/TypeError/,
"Missing getOrCreateNodeReference callback expected to throw"
);
// Fails with missing getOrCreateNodeReference callback
await Assert.rejects(
json.clone({ browsingContext, value: htmlEl }),
/TypeError/,
"Missing getOrCreateNodeReference callback expected to throw"
);
add_task(function test_clone_WebElement() {
const { htmlEl, nodeCache, svgEl } = setupTest();
const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl);
deepEqual(
await clone({
browsingContext,
getOrCreateNodeReference,
value: htmlEl,
}),
json.clone(htmlEl, nodeCache),
WebReference.from(htmlEl, htmlElRef).toJSON()
);
// Check an element with a different namespace
const svgElRef = nodeCache.getOrCreateNodeReference(svgEl);
deepEqual(
await clone({
browsingContext,
getOrCreateNodeReference,
value: svgEl,
}),
json.clone(svgEl, nodeCache),
WebReference.from(svgEl, svgElRef).toJSON()
);
});
add_task(async function test_clone_Sequences() {
const { browsingContext, htmlEl, nodeCache } = setupTest();
async function getOrCreateNodeReference(bc, node) {
equal(bc, browsingContext);
equal(node, htmlEl);
const nodeRef = nodeCache.getOrCreateNodeReference(node);
return WebReference.from(node, nodeRef);
}
add_task(function test_clone_Sequences() {
const { htmlEl, nodeCache } = setupTest();
const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl);
@ -201,11 +98,7 @@ add_task(async function test_clone_Sequences() {
{ bar: "baz" },
];
const actual = await clone({
browsingContext,
getOrCreateNodeReference,
value: input,
});
const actual = json.clone(input, nodeCache);
equal(actual[0], null);
equal(actual[1], true);
@ -215,16 +108,8 @@ add_task(async function test_clone_Sequences() {
deepEqual(actual[5], { bar: "baz" });
});
add_task(async function test_clone_objects() {
const { browsingContext, htmlEl, nodeCache } = setupTest();
async function getOrCreateNodeReference(bc, node) {
equal(bc, browsingContext);
equal(node, htmlEl);
const nodeRef = nodeCache.getOrCreateNodeReference(node);
return WebReference.from(node, nodeRef);
}
add_task(function test_clone_objects() {
const { htmlEl, nodeCache } = setupTest();
const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl);
@ -241,11 +126,7 @@ add_task(async function test_clone_objects() {
object: { bar: "baz" },
};
const actual = await clone({
browsingContext,
getOrCreateNodeReference,
value: input,
});
const actual = json.clone(input, nodeCache);
equal(actual.null, null);
equal(actual.boolean, true);
@ -255,164 +136,102 @@ add_task(async function test_clone_objects() {
deepEqual(actual.object, { bar: "baz" });
});
add_task(async function test_clone_сyclicReference() {
const { browsingContext } = setupTest();
const array = [];
array.push(array);
const obj = {};
obj.reference = obj;
add_task(function test_clone_сyclicReference() {
const { nodeCache } = setupTest();
// object
await Assert.rejects(
clone({ browsingContext, value: obj }),
/JavaScriptError/,
"Cyclic reference expected to throw"
);
Assert.throws(() => {
const obj = {};
obj.reference = obj;
json.clone(obj, nodeCache);
}, /JavaScriptError/);
// array
await Assert.rejects(
clone({ browsingContext, value: array }),
/JavaScriptError/,
"Cyclic reference expected to throw"
);
Assert.throws(() => {
const array = [];
array.push(array);
json.clone(array, nodeCache);
}, /JavaScriptError/);
// array in object
await Assert.rejects(
clone({ browsingContext, value: { array } }),
/JavaScriptError/,
"Cyclic reference expected to throw"
);
Assert.throws(() => {
const array = [];
array.push(array);
json.clone({ array }, nodeCache);
}, /JavaScriptError/);
// object in array
await Assert.rejects(
clone({ browsingContext, value: [obj] }),
/JavaScriptError/,
"Cyclic reference expected to throw"
);
Assert.throws(() => {
const obj = {};
obj.reference = obj;
json.clone([obj], nodeCache);
}, /JavaScriptError/);
});
add_task(async function test_deserialize_generalTypes() {
const { browsingContext } = setupTest();
add_task(function test_deserialize_generalTypes() {
const { browser, nodeCache } = setupTest();
const win = browser.document.ownerGlobal;
// null
equal(await deserialize({ browsingContext, value: undefined }), undefined);
equal(await deserialize({ browsingContext, value: null }), null);
equal(json.deserialize(undefined, nodeCache, win), undefined);
equal(json.deserialize(null, nodeCache, win), null);
// primitives
equal(await deserialize({ browsingContext, value: true }), true);
equal(await deserialize({ browsingContext, value: 42 }), 42);
equal(await deserialize({ browsingContext, value: "foo" }), "foo");
equal(json.deserialize(true, nodeCache, win), true);
equal(json.deserialize(42, nodeCache, win), 42);
equal(json.deserialize("foo", nodeCache, win), "foo");
});
add_task(async function test_deserialize_ShadowRoot() {
const { browsingContext, nodeCache, shadowRoot } = setupTest();
add_task(function test_deserialize_ShadowRoot() {
const { browser, nodeCache, shadowRoot } = setupTest();
const win = browser.document.ownerGlobal;
const getKnownElement = async () => ok(false, "'getKnownElement' called");
const getKnownShadowRoot = async (bc, nodeId) =>
nodeCache.getNode(bc, nodeId);
// Unknown shadow root
// Fails to resolve for unknown elements
const unknownShadowRootId = { [ShadowRoot.Identifier]: "foo" };
equal(
await deserialize({
browsingContext,
getKnownElement,
getKnownShadowRoot,
value: unknownShadowRootId,
}),
null
);
Assert.throws(() => {
json.deserialize(unknownShadowRootId, nodeCache, win);
}, /NoSuchShadowRootError/);
const shadowRootRef = nodeCache.getOrCreateNodeReference(shadowRoot);
const shadowRootEl = { [ShadowRoot.Identifier]: shadowRootRef };
// Fails with missing browsing context
await Assert.rejects(
json.deserialize({
getKnownElement,
getKnownShadowRoot,
value: shadowRootEl,
}),
/TypeError/,
"Missing browsing context expected to throw"
);
// Fails with missing getKnownShadowRoot callback
await Assert.rejects(
json.deserialize({ browsingContext, getKnownElement, value: shadowRootEl }),
/TypeError/,
"Missing getKnownShadowRoot callback expected to throw"
);
// Fails to resolve for missing window reference
Assert.throws(() => json.deserialize(shadowRootEl, nodeCache), /TypeError/);
// Previously seen element is associated with original web element reference
const root = await deserialize({
browsingContext,
getKnownShadowRoot,
value: shadowRootEl,
});
deepEqual(root, nodeCache.getNode(browsingContext, shadowRootRef));
const root = json.deserialize(shadowRootEl, nodeCache, win);
deepEqual(root, shadowRoot);
deepEqual(root, nodeCache.getNode(browser.browsingContext, shadowRootRef));
});
add_task(async function test_deserialize_WebElement() {
const { browsingContext, htmlEl, nodeCache } = setupTest();
add_task(function test_deserialize_WebElement() {
const { browser, htmlEl, nodeCache } = setupTest();
const win = browser.document.ownerGlobal;
const getKnownElement = async (bc, nodeId) => nodeCache.getNode(bc, nodeId);
const getKnownShadowRoot = async () =>
ok(false, "'getKnownShadowRoot' called");
// Unknown element
// Fails to resolve for unknown elements
const unknownWebElId = { [WebElement.Identifier]: "foo" };
equal(
await json.deserialize({
browsingContext,
getKnownElement,
getKnownShadowRoot,
value: unknownWebElId,
}),
null
);
Assert.throws(() => {
json.deserialize(unknownWebElId, nodeCache, win);
}, /NoSuchElementError/);
const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl);
const htmlWebEl = { [WebElement.Identifier]: htmlElRef };
// Fails with missing browsing context
await Assert.rejects(
json.deserialize({ getKnownElement, getKnownShadowRoot, value: htmlWebEl }),
/TypeError/,
"Missing browsing context expected to throw"
);
// Fails with missing getKnownElement callback
await Assert.rejects(
json.deserialize({ browsingContext, getKnownShadowRoot, value: htmlWebEl }),
/TypeError/,
"Missing getKnownElement callback expected to throw"
);
// Fails to resolve for missing window reference
Assert.throws(() => json.deserialize(htmlWebEl, nodeCache), /TypeError/);
// Previously seen element is associated with original web element reference
const el = await deserialize({
browsingContext,
getKnownElement,
value: htmlWebEl,
});
const el = json.deserialize(htmlWebEl, nodeCache, win);
deepEqual(el, htmlEl);
deepEqual(el, nodeCache.getNode(browsingContext, htmlElRef));
deepEqual(el, nodeCache.getNode(browser.browsingContext, htmlElRef));
});
add_task(async function test_deserialize_Sequences() {
const { browsingContext, htmlEl, nodeCache } = setupTest();
add_task(function test_deserialize_Sequences() {
const { browser, htmlEl, nodeCache } = setupTest();
const win = browser.document.ownerGlobal;
const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl);
async function getKnownElement(bc, nodeId) {
equal(bc, browsingContext);
equal(nodeId, htmlElRef);
return nodeCache.getNode(bc, nodeId);
}
const input = [
null,
true,
@ -421,11 +240,7 @@ add_task(async function test_deserialize_Sequences() {
{ bar: "baz" },
];
const actual = await deserialize({
browsingContext,
getKnownElement,
value: input,
});
const actual = json.deserialize(input, nodeCache, win);
equal(actual[0], null);
equal(actual[1], true);
@ -434,18 +249,12 @@ add_task(async function test_deserialize_Sequences() {
deepEqual(actual[4], { bar: "baz" });
});
add_task(async function test_deserialize_objects() {
const { browsingContext, htmlEl, nodeCache } = setupTest();
add_task(function test_deserialize_objects() {
const { browser, htmlEl, nodeCache } = setupTest();
const win = browser.document.ownerGlobal;
const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl);
async function getKnownElement(bc, nodeId) {
equal(bc, browsingContext);
equal(nodeId, htmlElRef);
return nodeCache.getNode(bc, nodeId);
}
const input = {
null: null,
boolean: true,
@ -454,11 +263,7 @@ add_task(async function test_deserialize_objects() {
object: { bar: "baz" },
};
const actual = await deserialize({
browsingContext,
getKnownElement,
value: input,
});
const actual = json.deserialize(input, nodeCache, win);
equal(actual.null, null);
equal(actual.boolean, true);

View file

@ -1,283 +0,0 @@
/* 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 {
ShadowRoot,
WebElement,
WebFrame,
WebReference,
WebWindow,
} = ChromeUtils.importESModule(
"chrome://remote/content/marionette/web-reference.sys.mjs"
);
const { element } = ChromeUtils.importESModule(
"chrome://remote/content/shared/webdriver/Element.sys.mjs"
);
class MockElement {
constructor(tagName, attrs = {}) {
this.tagName = tagName;
this.localName = tagName;
this.isConnected = false;
this.ownerGlobal = {
document: {
isActive() {
return true;
},
},
};
for (let attr in attrs) {
this[attr] = attrs[attr];
}
}
get nodeType() {
return 1;
}
get ELEMENT_NODE() {
return 1;
}
// this is a severely limited CSS selector
// that only supports lists of tag names
matches(selector) {
let tags = selector.split(",");
return tags.includes(this.localName);
}
}
class MockXULElement extends MockElement {
constructor(tagName, attrs = {}) {
super(tagName, attrs);
this.namespaceURI = XUL_NS;
if (typeof this.ownerDocument == "undefined") {
this.ownerDocument = {};
}
if (typeof this.ownerDocument.documentElement == "undefined") {
this.ownerDocument.documentElement = { namespaceURI: XUL_NS };
}
}
}
const xulEl = new MockXULElement("text");
const domElInPrivilegedDocument = new MockElement("input", {
nodePrincipal: { isSystemPrincipal: true },
});
const xulElInPrivilegedDocument = new MockXULElement("text", {
nodePrincipal: { isSystemPrincipal: true },
});
function setupTest() {
const browser = Services.appShell.createWindowlessBrowser(false);
browser.document.body.innerHTML = `
<div id="foo" style="margin: 50px">
<iframe></iframe>
<video></video>
<svg xmlns="http://www.w3.org/2000/svg"></svg>
<textarea></textarea>
</div>
`;
const divEl = browser.document.querySelector("div");
const svgEl = browser.document.querySelector("svg");
const textareaEl = browser.document.querySelector("textarea");
const videoEl = browser.document.querySelector("video");
const iframeEl = browser.document.querySelector("iframe");
const childEl = iframeEl.contentDocument.createElement("div");
iframeEl.contentDocument.body.appendChild(childEl);
const shadowRoot = videoEl.openOrClosedShadowRoot;
return {
browser,
childEl,
divEl,
iframeEl,
shadowRoot,
svgEl,
textareaEl,
videoEl,
};
}
add_task(function test_WebReference_ctor() {
const el = new WebReference("foo");
equal(el.uuid, "foo");
for (let t of [42, true, [], {}, null, undefined]) {
Assert.throws(() => new WebReference(t), /to be a string/);
}
});
add_task(function test_WebElemenet_is() {
const a = new WebReference("a");
const b = new WebReference("b");
ok(a.is(a));
ok(b.is(b));
ok(!a.is(b));
ok(!b.is(a));
ok(!a.is({}));
});
add_task(function test_WebReference_from() {
const { divEl, iframeEl } = setupTest();
ok(WebReference.from(divEl) instanceof WebElement);
ok(WebReference.from(xulEl) instanceof WebElement);
ok(WebReference.from(divEl.ownerGlobal) instanceof WebWindow);
ok(WebReference.from(iframeEl.contentWindow) instanceof WebFrame);
ok(WebReference.from(domElInPrivilegedDocument) instanceof WebElement);
ok(WebReference.from(xulElInPrivilegedDocument) instanceof WebElement);
Assert.throws(() => WebReference.from({}), /InvalidArgumentError/);
});
add_task(function test_WebReference_fromJSON_WebElement() {
const { Identifier } = WebElement;
const ref = { [Identifier]: "foo" };
const webEl = WebReference.fromJSON(ref);
ok(webEl instanceof WebElement);
equal(webEl.uuid, "foo");
let identifierPrecedence = {
[Identifier]: "identifier-uuid",
};
const precedenceEl = WebReference.fromJSON(identifierPrecedence);
ok(precedenceEl instanceof WebElement);
equal(precedenceEl.uuid, "identifier-uuid");
});
add_task(function test_WebReference_fromJSON_WebWindow() {
const ref = { [WebWindow.Identifier]: "foo" };
const win = WebReference.fromJSON(ref);
ok(win instanceof WebWindow);
equal(win.uuid, "foo");
});
add_task(function test_WebReference_fromJSON_WebFrame() {
const ref = { [WebFrame.Identifier]: "foo" };
const frame = WebReference.fromJSON(ref);
ok(frame instanceof WebFrame);
equal(frame.uuid, "foo");
});
add_task(function test_WebReference_fromJSON_malformed() {
Assert.throws(() => WebReference.fromJSON({}), /InvalidArgumentError/);
Assert.throws(() => WebReference.fromJSON(null), /InvalidArgumentError/);
});
add_task(function test_WebReference_isReference() {
for (let t of [42, true, "foo", [], {}]) {
ok(!WebReference.isReference(t));
}
ok(WebReference.isReference({ [WebElement.Identifier]: "foo" }));
ok(WebReference.isReference({ [WebWindow.Identifier]: "foo" }));
ok(WebReference.isReference({ [WebFrame.Identifier]: "foo" }));
});
add_task(function test_generateUUID() {
equal(typeof element.generateUUID(), "string");
});
add_task(function test_WebElement_toJSON() {
const { Identifier } = WebElement;
const el = new WebElement("foo");
const json = el.toJSON();
ok(Identifier in json);
equal(json[Identifier], "foo");
});
add_task(function test_WebElement_fromJSON() {
const { Identifier } = WebElement;
const el = WebElement.fromJSON({ [Identifier]: "foo" });
ok(el instanceof WebElement);
equal(el.uuid, "foo");
Assert.throws(() => WebElement.fromJSON({}), /InvalidArgumentError/);
});
add_task(function test_WebElement_fromUUID() {
const domWebEl = WebElement.fromUUID("bar");
ok(domWebEl instanceof WebElement);
equal(domWebEl.uuid, "bar");
Assert.throws(() => WebElement.fromUUID(), /InvalidArgumentError/);
});
add_task(function test_ShadowRoot_toJSON() {
const { Identifier } = ShadowRoot;
const shadowRoot = new ShadowRoot("foo");
const json = shadowRoot.toJSON();
ok(Identifier in json);
equal(json[Identifier], "foo");
});
add_task(function test_ShadowRoot_fromJSON() {
const { Identifier } = ShadowRoot;
const shadowRoot = ShadowRoot.fromJSON({ [Identifier]: "foo" });
ok(shadowRoot instanceof ShadowRoot);
equal(shadowRoot.uuid, "foo");
Assert.throws(() => ShadowRoot.fromJSON({}), /InvalidArgumentError/);
});
add_task(function test_ShadowRoot_fromUUID() {
const shadowRoot = ShadowRoot.fromUUID("baz");
ok(shadowRoot instanceof ShadowRoot);
equal(shadowRoot.uuid, "baz");
Assert.throws(() => ShadowRoot.fromUUID(), /InvalidArgumentError/);
});
add_task(function test_WebWindow_toJSON() {
const win = new WebWindow("foo");
const json = win.toJSON();
ok(WebWindow.Identifier in json);
equal(json[WebWindow.Identifier], "foo");
});
add_task(function test_WebWindow_fromJSON() {
const ref = { [WebWindow.Identifier]: "foo" };
const win = WebWindow.fromJSON(ref);
ok(win instanceof WebWindow);
equal(win.uuid, "foo");
});
add_task(function test_WebFrame_toJSON() {
const frame = new WebFrame("foo");
const json = frame.toJSON();
ok(WebFrame.Identifier in json);
equal(json[WebFrame.Identifier], "foo");
});
add_task(function test_WebFrame_fromJSON() {
const ref = { [WebFrame.Identifier]: "foo" };
const win = WebFrame.fromJSON(ref);
ok(win instanceof WebFrame);
equal(win.uuid, "foo");
});

View file

@ -9,10 +9,10 @@ skip-if = appname == "thunderbird"
[test_actors.js]
[test_browser.js]
[test_cookie.js]
[test_element.js]
[test_json.js]
[test_message.js]
[test_modal.js]
[test_navigate.js]
[test_prefs.js]
[test_sync.js]
[test_web_reference.js]

View file

@ -1,302 +0,0 @@
/* 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, {
assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
element: "chrome://remote/content/shared/webdriver/Element.sys.mjs",
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
pprint: "chrome://remote/content/shared/Format.sys.mjs",
});
/**
* A web reference is an abstraction used to identify an element when
* it is transported via the protocol, between remote- and local ends.
*
* In Marionette this abstraction can represent DOM elements,
* WindowProxies, and XUL elements.
*/
export class WebReference {
/**
* @param {string} uuid
* Identifier that must be unique across all browsing contexts
* for the contract to be upheld.
*/
constructor(uuid) {
this.uuid = lazy.assert.string(uuid);
}
/**
* Performs an equality check between this web element and
* <var>other</var>.
*
* @param {WebReference} other
* Web element to compare with this.
*
* @returns {boolean}
* True if this and <var>other</var> are the same. False
* otherwise.
*/
is(other) {
return other instanceof WebReference && this.uuid === other.uuid;
}
toString() {
return `[object ${this.constructor.name} uuid=${this.uuid}]`;
}
/**
* Returns a new {@link WebReference} reference for a DOM or XUL element,
* <code>WindowProxy</code>, or <code>ShadowRoot</code>.
*
* @param {(Element|ShadowRoot|WindowProxy|MockXULElement)} node
* Node to construct a web element reference for.
* @param {string=} uuid
* Optional unique identifier of the WebReference if already known.
* If not defined a new unique identifier will be created.
*
* @returns {WebReference}
* Web reference for <var>node</var>.
*
* @throws {InvalidArgumentError}
* If <var>node</var> is neither a <code>WindowProxy</code>,
* DOM or XUL element, or <code>ShadowRoot</code>.
*/
static from(node, uuid) {
if (uuid === undefined) {
uuid = Services.uuid
.generateUUID()
.toString()
.slice(1, -1);
}
if (
lazy.element.isShadowRoot(node) &&
!lazy.element.isInPrivilegedDocument(node)
) {
// When we support Chrome Shadowroots we will need to
// do a check here of shadowroot.host being in a privileged document
// See Bug 1743541
return new ShadowRoot(uuid);
} else if (lazy.element.isElement(node)) {
return new WebElement(uuid);
} else if (lazy.element.isDOMWindow(node)) {
if (node.parent === node) {
return new WebWindow(uuid);
}
return new WebFrame(uuid);
}
throw new lazy.error.InvalidArgumentError(
"Expected DOM window/element " + lazy.pprint`or XUL element, got: ${node}`
);
}
/**
* Unmarshals a JSON Object to one of {@link ShadowRoot}, {@link WebElement},
* {@link WebFrame}, or {@link WebWindow}.
*
* @param {Object<string, string>} json
* Web reference, which is supposed to be a JSON Object
* where the key is one of the {@link WebReference} concrete
* classes' UUID identifiers.
*
* @returns {WebReference}
* Web reference for the JSON object.
*
* @throws {InvalidArgumentError}
* If <var>json</var> is not a web reference.
*/
static fromJSON(json) {
lazy.assert.object(json);
if (json instanceof WebReference) {
return json;
}
let keys = Object.keys(json);
for (let key of keys) {
switch (key) {
case ShadowRoot.Identifier:
return ShadowRoot.fromJSON(json);
case WebElement.Identifier:
return WebElement.fromJSON(json);
case WebFrame.Identifier:
return WebFrame.fromJSON(json);
case WebWindow.Identifier:
return WebWindow.fromJSON(json);
}
}
throw new lazy.error.InvalidArgumentError(
lazy.pprint`Expected web reference, got: ${json}`
);
}
/**
* Checks if <var>obj<var> is a {@link WebReference} reference.
*
* @param {Object<string, string>} obj
* Object that represents a {@link WebReference}.
*
* @returns {boolean}
* True if <var>obj</var> is a {@link WebReference}, false otherwise.
*/
static isReference(obj) {
if (Object.prototype.toString.call(obj) != "[object Object]") {
return false;
}
if (
ShadowRoot.Identifier in obj ||
WebElement.Identifier in obj ||
WebFrame.Identifier in obj ||
WebWindow.Identifier in obj
) {
return true;
}
return false;
}
}
/**
* DOM elements are represented as web elements when they are
* transported over the wire protocol.
*/
export class WebElement extends WebReference {
toJSON() {
return { [WebElement.Identifier]: this.uuid };
}
static fromJSON(json) {
const { Identifier } = WebElement;
if (!(Identifier in json)) {
throw new lazy.error.InvalidArgumentError(
lazy.pprint`Expected web element reference, got: ${json}`
);
}
let uuid = json[Identifier];
return new WebElement(uuid);
}
/**
* Constructs a {@link WebElement} from a string <var>uuid</var>.
*
* This whole function is a workaround for the fact that clients
* to Marionette occasionally pass <code>{id: <uuid>}</code> JSON
* Objects instead of web element representations.
*
* @param {string} uuid
* UUID to be associated with the web reference.
*
* @returns {WebElement}
* The web element reference.
*
* @throws {InvalidArgumentError}
* If <var>uuid</var> is not a string.
*/
static fromUUID(uuid) {
return new WebElement(uuid);
}
}
WebElement.Identifier = "element-6066-11e4-a52e-4f735466cecf";
/**
* Shadow Root elements are represented as shadow root references when they are
* transported over the wire protocol
*/
export class ShadowRoot extends WebReference {
toJSON() {
return { [ShadowRoot.Identifier]: this.uuid };
}
static fromJSON(json) {
const { Identifier } = ShadowRoot;
if (!(Identifier in json)) {
throw new lazy.error.InvalidArgumentError(
lazy.pprint`Expected shadow root reference, got: ${json}`
);
}
let uuid = json[Identifier];
return new ShadowRoot(uuid);
}
/**
* Constructs a {@link ShadowRoot} from a string <var>uuid</var>.
*
* This whole function is a workaround for the fact that clients
* to Marionette occasionally pass <code>{id: <uuid>}</code> JSON
* Objects instead of shadow root representations.
*
* @param {string} uuid
* UUID to be associated with the web reference.
*
* @returns {ShadowRoot}
* The shadow root reference.
*
* @throws {InvalidArgumentError}
* If <var>uuid</var> is not a string.
*/
static fromUUID(uuid) {
lazy.assert.string(uuid);
return new ShadowRoot(uuid);
}
}
ShadowRoot.Identifier = "shadow-6066-11e4-a52e-4f735466cecf";
/**
* Top-level browsing contexts, such as <code>WindowProxy</code>
* whose <code>opener</code> is null, are represented as web windows
* over the wire protocol.
*/
export class WebWindow extends WebReference {
toJSON() {
return { [WebWindow.Identifier]: this.uuid };
}
static fromJSON(json) {
if (!(WebWindow.Identifier in json)) {
throw new lazy.error.InvalidArgumentError(
lazy.pprint`Expected web window reference, got: ${json}`
);
}
let uuid = json[WebWindow.Identifier];
return new WebWindow(uuid);
}
}
WebWindow.Identifier = "window-fcc6-11e5-b4f8-330a88ab9d7f";
/**
* Nested browsing contexts, such as the <code>WindowProxy</code>
* associated with <tt>&lt;frame&gt;</tt> and <tt>&lt;iframe&gt;</tt>,
* are represented as web frames over the wire protocol.
*/
export class WebFrame extends WebReference {
toJSON() {
return { [WebFrame.Identifier]: this.uuid };
}
static fromJSON(json) {
if (!(WebFrame.Identifier in json)) {
throw new lazy.error.InvalidArgumentError(
lazy.pprint`Expected web frame reference, got: ${json}`
);
}
let uuid = json[WebFrame.Identifier];
return new WebFrame(uuid);
}
}
WebFrame.Identifier = "frame-075b-4da1-b6ba-e579c2d3230a";

View file

@ -253,25 +253,6 @@ export var TabManager = {
return browsingContext.id.toString();
},
/**
* Get the navigable for the given browsing context.
*
* Because Gecko doesn't support the Navigable concept in content
* scope the content browser could be used to uniquely identify
* top-level browsing contexts.
*
* @param {BrowsingContext} browsingContext
*
* @returns {BrowsingContext|XULBrowser} The navigable
*/
getNavigableForBrowsingContext(browsingContext) {
if (browsingContext.isContent && browsingContext.parent === null) {
return browsingContext.embedderElement;
}
return browsingContext;
},
getTabCount() {
let count = 0;
for (const win of this.windows) {

View file

@ -128,34 +128,6 @@ add_task(async function test_addTab_window() {
}
});
add_task(async function test_getNavigableForBrowsingContext() {
const browser = gBrowser.selectedBrowser;
info(`Navigate to ${TEST_URL}`);
const loaded = BrowserTestUtils.browserLoaded(browser);
BrowserTestUtils.loadURIString(browser, TEST_URL);
await loaded;
const contexts = browser.browsingContext.getAllBrowsingContextsInSubtree();
is(contexts.length, 2, "Top context has 1 child");
// For a top-level browsing context the content browser is returned.
const topContext = contexts[0];
is(
TabManager.getNavigableForBrowsingContext(topContext),
browser,
"Top-Level browsing context has the content browser as navigable"
);
// For child browsing contexts the browsing context itself is returned.
const childContext = contexts[1];
is(
TabManager.getNavigableForBrowsingContext(childContext),
childContext,
"Child browsing context has itself as navigable"
);
});
add_task(async function test_getTabForBrowsingContext() {
const tab = await TabManager.addTab();
try {

View file

@ -12,7 +12,7 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
AppInfo: "chrome://remote/content/shared/AppInfo.sys.mjs",
assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
element: "chrome://remote/content/shared/webdriver/Element.sys.mjs",
element: "chrome://remote/content/marionette/element.sys.mjs",
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
event: "chrome://remote/content/marionette/event.sys.mjs",
keyData: "chrome://remote/content/shared/webdriver/KeyData.sys.mjs",

View file

@ -18,7 +18,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
"chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs",
RootMessageHandlerRegistry:
"chrome://remote/content/shared/messagehandler/RootMessageHandlerRegistry.sys.mjs",
TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
unregisterProcessDataActor:
"chrome://remote/content/shared/webdriver/process-actors/WebDriverProcessDataParent.sys.mjs",
WebDriverBiDiConnection:
@ -29,9 +28,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
XPCOMUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
/** @namespace */
export const session = {};
/**
* Representation of WebDriver session.
*/
@ -204,22 +200,10 @@ export class WebDriverSession {
this._connections.add(connection);
}
// Maps a Navigable (browsing context or content browser for top-level
// browsing contexts) to a Set of nodeId's.
this.navigableSeenNodes = new WeakMap();
lazy.registerProcessDataActor();
webDriverSessions.set(this.id, this);
}
destroy() {
webDriverSessions.delete(this.id);
lazy.unregisterProcessDataActor();
this.navigableSeenNodes = null;
lazy.allowAllCerts.disable();
// Close all open connections which unregister themselves.
@ -238,6 +222,8 @@ export class WebDriverSession {
);
this._messageHandler.destroy();
}
lazy.unregisterProcessDataActor();
}
async execute(module, command, params) {
@ -360,66 +346,3 @@ export class WebDriverSession {
return ChromeUtils.generateQI(["nsIHttpRequestHandler"]);
}
}
/**
*
* @param {string} sessionId
* The ID of the WebDriver session to retrieve.
*
* @returns {WebDriverSession}
* The WebDriver session.
*/
export function getWebDriverSessionById(sessionId) {
return webDriverSessions.get(sessionId);
}
// Global singleton that holds active WebDriver sessions
const webDriverSessions = new Map();
/**
* Adds the given node id to the list of seen nodes.
*
* @param {string} sessionId
* The id of the WebDriver session to use.
* @param {BrowsingContext} browsingContext
* Browsing context the node is part of.
* @param {string} nodeId
* Unique id of the node.
*/
session.addNodeToSeenNodes = function(sessionId, browsingContext, nodeId) {
const navigable = lazy.TabManager.getNavigableForBrowsingContext(
browsingContext
);
const session = getWebDriverSessionById(sessionId);
if (!session.navigableSeenNodes.has(navigable)) {
// The navigable hasn't been seen yet.
session.navigableSeenNodes.set(navigable, new Set());
}
// Add the nodeId to the list of already seen nodes.
session.navigableSeenNodes.get(navigable).add(nodeId);
};
/**
* Checks if the node id is known for the navigable and session.
*
* @param {string} sessionId
* The id of the WebDriver session to use.
* @param {BrowsingContext} browsingContext
* Browsing context the node is part of.
* @param {string} nodeId
* Unique id of the node.
*
* @returns {boolean}
* True if the node is known for the given navigable and session.
*/
session.isNodeReferenceKnown = function(sessionId, browsingContext, nodeId) {
const navigable = lazy.TabManager.getNavigableForBrowsingContext(
browsingContext
);
const session = getWebDriverSessionById(sessionId);
// Check if the nodeId has been seen before.
return !!session.navigableSeenNodes.get(navigable)?.has(nodeId);
};

View file

@ -7,10 +7,7 @@
const { Capabilities, Timeouts } = ChromeUtils.importESModule(
"chrome://remote/content/shared/webdriver/Capabilities.sys.mjs"
);
const {
WebDriverSession,
getWebDriverSessionById,
} = ChromeUtils.importESModule(
const { WebDriverSession } = ChromeUtils.importESModule(
"chrome://remote/content/shared/webdriver/Session.sys.mjs"
);
@ -21,12 +18,6 @@ add_task(function test_WebDriverSession_ctor() {
ok(session.capabilities instanceof Capabilities);
});
add_task(function test_WebDriverSession_destroy() {
const session = new WebDriverSession();
session.destroy();
});
add_task(function test_WebDriverSession_getters() {
const session = new WebDriverSession();
@ -56,19 +47,3 @@ add_task(function test_WebDriverSession_setters() {
session.timeouts = timeouts;
equal(session.timeouts, session.capabilities.get("timeouts"));
});
add_task(function test_getWebDriverSessionById() {
const session1 = new WebDriverSession();
const session2 = new WebDriverSession();
equal(getWebDriverSessionById(session1.id), session1);
equal(getWebDriverSessionById(session2.id), session2);
session1.destroy();
equal(getWebDriverSessionById(session1.id), undefined);
equal(getWebDriverSessionById(session2.id), session2);
session2.destroy();
equal(getWebDriverSessionById(session1.id), undefined);
equal(getWebDriverSessionById(session2.id), undefined);
});

View file

@ -8,7 +8,6 @@ head = head.js
[test_Actions.js]
[test_Assert.js]
[test_Capabilities.js]
[test_Element.js]
[test_Errors.js]
[test_NodeCache.js]
[test_Session.js]

View file

@ -8,7 +8,6 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
element: "chrome://remote/content/shared/webdriver/Element.sys.mjs",
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
Log: "chrome://remote/content/shared/Log.sys.mjs",
});
@ -48,20 +47,6 @@ export const OwnershipModel = {
Root: "root",
};
/**
* Extra options for serializing and deserializing remote values.
*
* @typedef {object} RemoteValueOptions
*
* @param {Function=} emitScriptMessage
* Callback to emit a "script.message" event.
* @param {Function=} getNode
* Callback to retrieve a DOM node via its nodeId and browsing context.
* @param {Function=} getOrCreateNodeReference
* Async callback to get or create a new node reference. Its return value
* is the unique id of the DOM node.
*/
/**
* An object which holds the information of how
* ECMAScript objects should be serialized.
@ -159,16 +144,16 @@ function checkDateTimeString(dateString) {
* The Realm in which the value is deserialized.
* @param {Array} serializedValueList
* List of serialized values.
* @param {RemoteValueOptions} options
* Extra Remote Value deserialization options.
* @param {object} options
* @param {NodeCache} options.nodeCache
* The cache containing DOM node references.
*
* @returns {Promise<Array>}
* Promise that resolves to the list of deserialized values.
* @returns {Array} List of deserialized values.
*
* @throws {InvalidArgumentError}
* If <var>serializedValueList</var> is not an array.
*/
async function deserializeValueList(realm, serializedValueList, options = {}) {
function deserializeValueList(realm, serializedValueList, options = {}) {
lazy.assert.array(
serializedValueList,
`Expected "serializedValueList" to be an array, got ${serializedValueList}`
@ -177,7 +162,7 @@ async function deserializeValueList(realm, serializedValueList, options = {}) {
const deserializedValues = [];
for (const item of serializedValueList) {
deserializedValues.push((await deserialize(realm, item, options)).data);
deserializedValues.push(deserialize(realm, item, options));
}
return deserializedValues;
@ -192,21 +177,17 @@ async function deserializeValueList(realm, serializedValueList, options = {}) {
* The Realm in which the value is deserialized.
* @param {Array} serializedKeyValueList
* List of serialized key-value.
* @param {RemoteValueOptions} options
* Extra Remote Value deserialization options.
* @param {object} options
* @param {NodeCache} options.nodeCache
* The cache containing DOM node references.
*
* @returns {Promise<Array>}
* Promise that resolves to the list of deserialized key-value pairs.
* @returns {Array} List of deserialized key-value.
*
* @throws {InvalidArgumentError}
* If <var>serializedKeyValueList</var> is not an array or
* not an array of key-value arrays.
*/
async function deserializeKeyValueList(
realm,
serializedKeyValueList,
options = {}
) {
function deserializeKeyValueList(realm, serializedKeyValueList, options = {}) {
lazy.assert.array(
serializedKeyValueList,
`Expected "serializedKeyValueList" to be an array, got ${serializedKeyValueList}`
@ -221,16 +202,11 @@ async function deserializeKeyValueList(
);
}
const [serializedKey, serializedValue] = serializedKeyValue;
let deserializedKey;
if (typeof serializedKey == "string") {
deserializedKey = serializedKey;
} else {
deserializedKey = (await deserialize(realm, serializedKey, options)).data;
}
const deserializedValue = (
await deserialize(realm, serializedValue, options)
).data;
const deserializedKey =
typeof serializedKey == "string"
? serializedKey
: deserialize(realm, serializedKey, options);
const deserializedValue = deserialize(realm, serializedValue, options);
deserializedKeyValueList.push([deserializedKey, deserializedValue]);
}
@ -249,21 +225,21 @@ async function deserializeKeyValueList(
* Shared unique reference of the Node.
* @param {Realm} realm
* The Realm in which the value is deserialized.
* @param {RemoteValueOptions} options
* Extra Remote Value deserialization options.
* @param {object} options
* @param {NodeCache} options.nodeCache
* The cache containing DOM node references.
*
* @returns {Node}
* The deserialized DOM node.
* @returns {Node} The deserialized DOM node.
*/
function deserializeSharedReference(sharedRef, realm, options = {}) {
const { getNode } = options;
const { nodeCache } = options;
const browsingContext = realm.browsingContext;
if (!browsingContext) {
throw new lazy.error.NoSuchNodeError("Realm isn't a Window global");
}
const node = getNode(browsingContext, sharedRef);
const node = nodeCache.getNode(browsingContext, sharedRef);
if (node === null) {
throw new lazy.error.NoSuchNodeError(
@ -271,6 +247,25 @@ function deserializeSharedReference(sharedRef, realm, options = {}) {
);
}
// Bug 1819902: Instead of a browsing context check compare the origin
const isSameBrowsingContext = sharedRef => {
const nodeDetails = nodeCache.getReferenceDetails(sharedRef);
if (nodeDetails.isTopBrowsingContext && browsingContext.parent === null) {
// As long as Navigables are not available any cross-group navigation will
// cause a swap of the current top-level browsing context. The only unique
// identifier in such a case is the browser id the top-level browsing
// context actually lives in.
return nodeDetails.browserId === browsingContext.browserId;
}
return nodeDetails.browsingContextId === browsingContext.id;
};
if (!isSameBrowsingContext(sharedRef)) {
return null;
}
return node;
}
@ -283,16 +278,16 @@ function deserializeSharedReference(sharedRef, realm, options = {}) {
* The Realm in which the value is deserialized.
* @param {object} serializedValue
* Value of any type to be deserialized.
* @param {RemoteValueOptions} options
* Extra Remote Value deserialization options.
* @param {object} options
* @param {NodeCache} options.nodeCache
* The cache containing DOM node references.
* @param {Function} options.emitScriptMessage
* The function to emit "script.message" event.
*
* @returns {Promise<object>}
* Promise that resolves to an object that contains the deserialized
* representation of the value as `data` property.
* @returns {object} Deserialized representation of the value.
*/
export async function deserialize(realm, serializedValue, options = {}) {
export function deserialize(realm, serializedValue, options = {}) {
const { handle, sharedId, type, value } = serializedValue;
let data = undefined;
// With a shared id present deserialize as node reference.
if (sharedId !== undefined) {
@ -301,157 +296,134 @@ export async function deserialize(realm, serializedValue, options = {}) {
`Expected "sharedId" to be a string, got ${sharedId}`
);
data = deserializeSharedReference(sharedId, realm, options);
return deserializeSharedReference(sharedId, realm, options);
}
// With a handle present deserialize as remote reference.
else if (handle !== undefined) {
if (handle !== undefined) {
lazy.assert.string(
handle,
`Expected "handle" to be a string, got ${handle}`
);
data = realm.getObjectForHandle(handle);
if (!data) {
const object = realm.getObjectForHandle(handle);
if (!object) {
throw new lazy.error.NoSuchHandleError(
`Unable to find an object reference for "handle" ${handle}`
);
}
} else {
lazy.assert.string(type, `Expected "type" to be a string, got ${type}`);
// Primitive protocol values
switch (type) {
case "undefined":
data = undefined;
break;
case "null":
data = null;
break;
case "string":
data = lazy.assert.string(
value,
`Expected "value" to be a string, got ${value}`
);
break;
case "number":
// If value is already a number return its value.
if (typeof value === "number") {
data = value;
} else {
// Otherwise it has to be one of the special strings
lazy.assert.in(
value,
["NaN", "-0", "Infinity", "-Infinity"],
`Expected "value" to be one of "NaN", "-0", "Infinity", "-Infinity", got ${value}`
);
data = Number(value);
}
break;
case "boolean":
lazy.assert.boolean(
value,
`Expected "value" to be a boolean, got ${value}`
);
data = value;
break;
case "bigint":
lazy.assert.string(
value,
`Expected "value" to be a string, got ${value}`
);
try {
data = BigInt(value);
} catch (e) {
throw new lazy.error.InvalidArgumentError(
`Failed to deserialize value as BigInt: ${value}`
);
}
break;
// Script channel
case "channel": {
const channel = message =>
options.emitScriptMessage(realm, value, message);
data = realm.cloneIntoRealm(channel);
break;
}
// Non-primitive protocol values
case "array":
const deserializedArray = await deserializeValueList(
realm,
value,
options
);
// TODO: clone deserializedList instead?
data = realm.cloneIntoRealm([]);
deserializedArray.forEach(v => data.push(v));
break;
case "date":
// We want to support only Date Time String format,
// check if the value follows it.
checkDateTimeString(value);
data = realm.cloneIntoRealm(new Date(value));
break;
case "map":
data = realm.cloneIntoRealm(new Map());
const deserializedMap = await deserializeKeyValueList(
realm,
value,
options
);
deserializedMap.forEach(([k, v]) => data.set(k, v));
break;
case "object":
data = realm.cloneIntoRealm({});
const deserializedObject = await deserializeKeyValueList(
realm,
value,
options
);
deserializedObject.forEach(([k, v]) => (data[k] = v));
break;
case "regexp":
lazy.assert.object(
value,
`Expected "value" for RegExp to be an object, got ${value}`
);
const { pattern, flags } = value;
lazy.assert.string(
pattern,
`Expected "pattern" for RegExp to be a string, got ${pattern}`
);
if (flags !== undefined) {
lazy.assert.string(
flags,
`Expected "flags" for RegExp to be a string, got ${flags}`
);
}
try {
data = realm.cloneIntoRealm(new RegExp(pattern, flags));
} catch (e) {
throw new lazy.error.InvalidArgumentError(
`Failed to deserialize value as RegExp: ${value}`
);
}
break;
case "set":
data = realm.cloneIntoRealm(new Set());
const deserializedSet = await deserializeValueList(
realm,
value,
options
);
deserializedSet.forEach(v => data.add(v));
break;
default:
lazy.logger.warn(`Unsupported type for local value ${type}`);
}
return object;
}
// Wrap into an object so that Promises can be returned as well.
return { data };
lazy.assert.string(type, `Expected "type" to be a string, got ${type}`);
// Primitive protocol values
switch (type) {
case "undefined":
return undefined;
case "null":
return null;
case "string":
lazy.assert.string(
value,
`Expected "value" to be a string, got ${value}`
);
return value;
case "number":
// If value is already a number return its value.
if (typeof value === "number") {
return value;
}
// Otherwise it has to be one of the special strings
lazy.assert.in(
value,
["NaN", "-0", "Infinity", "-Infinity"],
`Expected "value" to be one of "NaN", "-0", "Infinity", "-Infinity", got ${value}`
);
return Number(value);
case "boolean":
lazy.assert.boolean(
value,
`Expected "value" to be a boolean, got ${value}`
);
return value;
case "bigint":
lazy.assert.string(
value,
`Expected "value" to be a string, got ${value}`
);
try {
return BigInt(value);
} catch (e) {
throw new lazy.error.InvalidArgumentError(
`Failed to deserialize value as BigInt: ${value}`
);
}
// Script channel
case "channel": {
const channel = message =>
options.emitScriptMessage(realm, value, message);
return realm.cloneIntoRealm(channel);
}
// Non-primitive protocol values
case "array":
const array = realm.cloneIntoRealm([]);
deserializeValueList(realm, value, options).forEach(v => array.push(v));
return array;
case "date":
// We want to support only Date Time String format,
// check if the value follows it.
checkDateTimeString(value);
return realm.cloneIntoRealm(new Date(value));
case "map":
const map = realm.cloneIntoRealm(new Map());
deserializeKeyValueList(realm, value, options).forEach(([k, v]) =>
map.set(k, v)
);
return map;
case "object":
const object = realm.cloneIntoRealm({});
deserializeKeyValueList(realm, value, options).forEach(
([k, v]) => (object[k] = v)
);
return object;
case "regexp":
lazy.assert.object(
value,
`Expected "value" for RegExp to be an object, got ${value}`
);
const { pattern, flags } = value;
lazy.assert.string(
pattern,
`Expected "pattern" for RegExp to be a string, got ${pattern}`
);
if (flags !== undefined) {
lazy.assert.string(
flags,
`Expected "flags" for RegExp to be a string, got ${flags}`
);
}
try {
return realm.cloneIntoRealm(new RegExp(pattern, flags));
} catch (e) {
throw new lazy.error.InvalidArgumentError(
`Failed to deserialize value as RegExp: ${value}`
);
}
case "set":
const set = realm.cloneIntoRealm(new Set());
deserializeValueList(realm, value, options).forEach(v => set.add(v));
return set;
}
lazy.logger.warn(`Unsupported type for local value ${type}`);
return undefined;
}
/**
@ -488,14 +460,15 @@ function getHandleForObject(realm, ownershipType, object) {
* Node to create the unique reference for.
* @param {Realm} realm
* The Realm in which the value is serialized.
* @param {RemoteValueOptions} options
* Extra Remote Value serialization options.
* @param {object} options
* @param {NodeCache} options.nodeCache
* The cache containing DOM node references.
*
* @returns {Promise<string>}
* Promise that resolves to a shared unique reference for the Node.
* @returns {string}
* Shared unique reference for the Node.
*/
async function getSharedIdForNode(node, realm, options = {}) {
const { getOrCreateNodeReference } = options;
function getSharedIdForNode(node, realm, options = {}) {
const { nodeCache } = options;
if (!Node.isInstance(node)) {
return null;
@ -506,7 +479,26 @@ async function getSharedIdForNode(node, realm, options = {}) {
return null;
}
return getOrCreateNodeReference(browsingContext, Cu.unwaiveXrays(node));
const unwrapped = Cu.unwaiveXrays(node);
return nodeCache.getOrCreateNodeReference(unwrapped);
}
/**
* Determines if <var>node</var> is shadow root.
*
* @param {Node} node
* Node to check.
*
* @returns {boolean}
* True if <var>node</var> is shadow root, false otherwise.
*/
function isShadowRoot(node) {
const DOCUMENT_FRAGMENT_NODE = 11;
return (
node &&
node.nodeType === DOCUMENT_FRAGMENT_NODE &&
node.containingShadowRoot == node
);
}
/**
@ -530,13 +522,13 @@ async function getSharedIdForNode(node, realm, options = {}) {
* Map of internal ids.
* @param {Realm} realm
* The Realm from which comes the value being serialized.
* @param {RemoteValueOptions} options
* Extra Remote Value serialization options.
* @param {object} options
* @param {NodeCache} options.nodeCache
* The cache containing DOM node references.
*
* @returns {Promise<object>}
* Promise that resolves to the serialized values.
* @returns {object} Object for serialized values.
*/
async function serializeArrayLike(
function serializeArrayLike(
production,
handleId,
knownObject,
@ -551,7 +543,7 @@ async function serializeArrayLike(
setInternalIdsIfNeeded(serializationInternalMap, serialized, value);
if (!knownObject && serializationOptions.maxObjectDepth !== 0) {
serialized.value = await serializeList(
serialized.value = serializeList(
value,
serializationOptions,
ownershipType,
@ -579,13 +571,13 @@ async function serializeArrayLike(
* Map of internal ids.
* @param {Realm} realm
* The Realm from which comes the value being serialized.
* @param {RemoteValueOptions} options
* Extra Remote Value serialization options.
* @param {object} options
* @param {NodeCache} options.nodeCache
* The cache containing DOM node references.
*
* @returns {Promise<Array>}
* Promise that resolves to a list of serialized values.
* @returns {Array} List of serialized values.
*/
async function serializeList(
function serializeList(
iterable,
serializationOptions,
ownershipType,
@ -604,7 +596,7 @@ async function serializeList(
for (const item of iterable) {
serialized.push(
await serialize(
serialize(
item,
childSerializationOptions,
ownershipType,
@ -633,13 +625,13 @@ async function serializeList(
* Map of internal ids.
* @param {Realm} realm
* The Realm from which comes the value being serialized.
* @param {RemoteValueOptions} options
* Extra Remote Value serialization options.
* @param {object} options
* @param {NodeCache} options.nodeCache
* The cache containing DOM node references.
*
* @returns {Promise<Array>}
* Promise that resolves to a list of serialized values.
* @returns {Array} List of serialized values.
*/
async function serializeMapping(
function serializeMapping(
iterable,
serializationOptions,
ownershipType,
@ -660,7 +652,7 @@ async function serializeMapping(
const serializedKey =
typeof key == "string"
? key
: await serialize(
: serialize(
key,
childSerializationOptions,
ownershipType,
@ -668,7 +660,7 @@ async function serializeMapping(
realm,
options
);
const serializedValue = await serialize(
const serializedValue = serialize(
item,
childSerializationOptions,
ownershipType,
@ -696,13 +688,13 @@ async function serializeMapping(
* Map of internal ids.
* @param {Realm} realm
* The Realm from which comes the value being serialized.
* @param {RemoteValueOptions} options
* Extra Remote Value serialization options.
* @param {object} options
* @param {NodeCache} options.nodeCache
* The cache containing DOM node references.
*
* @returns {Promise<object>}
* Promise that resolves to the serialized value.
* @returns {object} Serialized value.
*/
async function serializeNode(
function serializeNode(
node,
serializationOptions,
ownershipType,
@ -730,7 +722,7 @@ async function serializeNode(
serialized.childNodeCount = node.childNodes.length;
if (
maxDomDepth !== 0 &&
(!lazy.element.isShadowRoot(node) ||
(!isShadowRoot(node) ||
(includeShadowTree === IncludeShadowTreeMode.Open &&
node.mode === "open") ||
includeShadowTree === IncludeShadowTreeMode.All)
@ -744,7 +736,7 @@ async function serializeNode(
}
for (const child of node.childNodes) {
children.push(
await serialize(
serialize(
child,
childSerializationOptions,
ownershipType,
@ -767,7 +759,7 @@ async function serializeNode(
const shadowRoot = Cu.unwaiveXrays(node).openOrClosedShadowRoot;
serialized.shadowRoot = null;
if (shadowRoot !== null) {
serialized.shadowRoot = await serialize(
serialized.shadowRoot = serialize(
shadowRoot,
serializationOptions,
ownershipType,
@ -778,7 +770,7 @@ async function serializeNode(
}
}
if (lazy.element.isShadowRoot(node)) {
if (isShadowRoot(node)) {
serialized.mode = node.mode;
}
@ -800,13 +792,13 @@ async function serializeNode(
* Map of internal ids.
* @param {Realm} realm
* The Realm from which comes the value being serialized.
* @param {RemoteValueOptions} options
* Extra Remote Value serialization options.
* @param {object} options
* @param {NodeCache} options.nodeCache
* The cache containing DOM node references.
*
* @returns {Promise<object>}
* Promise that resolves to the serialized representation of the value.
* @returns {object} Serialized representation of the value.
*/
export async function serialize(
export function serialize(
value,
serializationOptions,
ownershipType,
@ -878,7 +870,7 @@ export async function serialize(
setInternalIdsIfNeeded(serializationInternalMap, serialized, value);
if (!knownObject && maxObjectDepth !== 0) {
serialized.value = await serializeMapping(
serialized.value = serializeMapping(
value.entries(),
serializationOptions,
ownershipType,
@ -892,7 +884,7 @@ export async function serialize(
setInternalIdsIfNeeded(serializationInternalMap, serialized, value);
if (!knownObject && maxObjectDepth !== 0) {
serialized.value = await serializeList(
serialized.value = serializeList(
value.values(),
serializationOptions,
ownershipType,
@ -920,7 +912,7 @@ export async function serialize(
const serialized = buildSerialized("node", handleId);
// Get or create the shared id for WebDriver classic compat from the node.
const sharedId = await getSharedIdForNode(value, realm, options);
const sharedId = getSharedIdForNode(value, realm, options);
if (sharedId !== null) {
serialized.sharedId = sharedId;
}
@ -928,7 +920,7 @@ export async function serialize(
setInternalIdsIfNeeded(serializationInternalMap, serialized, value);
if (!knownObject) {
serialized.value = await serializeNode(
serialized.value = serializeNode(
value,
serializationOptions,
ownershipType,
@ -949,7 +941,7 @@ export async function serialize(
setInternalIdsIfNeeded(serializationInternalMap, serialized, value);
if (!knownObject && maxObjectDepth !== 0) {
serialized.value = await serializeMapping(
serialized.value = serializeMapping(
Object.entries(value),
serializationOptions,
ownershipType,

View file

@ -12,7 +12,6 @@ remote.jar:
# WebDriver BiDi modules
content/webdriver-bidi/modules/ModuleRegistry.sys.mjs (modules/ModuleRegistry.sys.mjs)
content/webdriver-bidi/modules/RootBiDiModule.sys.mjs (modules/RootBiDiModule.sys.mjs)
content/webdriver-bidi/modules/WindowGlobalBiDiModule.sys.mjs (modules/WindowGlobalBiDiModule.sys.mjs)
# WebDriver BiDi root modules

View file

@ -1,35 +0,0 @@
/* 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 { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
session: "chrome://remote/content/shared/webdriver/Session.sys.mjs",
});
/**
* Base class for all Root BiDi MessageHandler modules.
*/
export class RootBiDiModule extends Module {
/**
* Adds the given node id to the list of seen nodes.
*
* @param {object} options
* @param {BrowsingContext} options.browsingContext
* Browsing context the node is part of.
* @param {string} options.nodeId
* Unique id of the node.
*/
_addNodeToSeenNodes(options = {}) {
const { browsingContext, nodeId } = options;
lazy.session.addNodeToSeenNodes(
this.messageHandler.sessionId,
browsingContext,
nodeId
);
}
}

View file

@ -4,147 +4,15 @@
import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
deserialize: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
serialize: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
});
/**
* Base class for all WindowGlobal BiDi MessageHandler modules.
*/
export class WindowGlobalBiDiModule extends Module {
get #nodeCache() {
return this.#processActor.getNodeCache();
get nodeCache() {
return this.processActor.getNodeCache();
}
get #processActor() {
get processActor() {
return ChromeUtils.domProcessChild.getActor("WebDriverProcessData");
}
/**
* Wrapper to deserialize a local / remote value.
*
* @param {Realm} realm
* The Realm in which the value is deserialized.
* @param {object} serializedValue
* Value of any type to be deserialized.
* @param {RemoteValueOptions=} options
* Extra Remote Value deserialization options.
*
* @returns {Promise<object>}
* Promise that resolves to the deserialized representation of the value.
*/
deserialize(realm, serializedValue, options = {}) {
options.getNode = this.#getNode.bind(this);
return lazy.deserialize(realm, serializedValue, options);
}
/**
* Wrapper to serialize a value as a remote value.
*
* @param {object} value
* Value of any type to be serialized.
* @param {SerializationOptions} serializationOptions
* Options which define how ECMAScript objects should be serialized.
* @param {OwnershipModel} ownershipType
* The ownership model to use for this serialization.
* @param {Realm} realm
* The Realm from which comes the value being serialized.
* @param {RemoteValueOptions=} options
* Extra Remote Value serialization options.
*
* @returns {Promise<object>}
* Promise that resolves to the serialized representation of the value.
*/
serialize(value, serializationOptions, ownershipType, realm, options = {}) {
options.getOrCreateNodeReference = this.#getOrCreateNodeReference.bind(
this
);
return lazy.serialize(
value,
serializationOptions,
ownershipType,
new Map(),
realm,
options
);
}
// Private methods
/**
* Resolve node from specified web reference identifier.
*
* @param {BrowsingContext} browsingContext
* The browsing context to retrieve the node from.
* @param {string} nodeId
* The WebReference uuid for a DOM node.
*
* @returns {Node|null}
* The DOM node that the identifier was generated for, or null if the
* node has not been found.
*/
#getNode(browsingContext, nodeId) {
const node = this.#nodeCache.getNode(browsingContext, nodeId);
if (node === null) {
return null;
}
// Bug 1819902: Instead of a browsing context check compare the origin
const isSameBrowsingContext = nodeId => {
const nodeDetails = this.#nodeCache.getReferenceDetails(nodeId);
if (nodeDetails.isTopBrowsingContext && browsingContext.parent === null) {
// As long as Navigables are not available any cross-group navigation will
// cause a swap of the current top-level browsing context. The only unique
// identifier in such a case is the browser id the top-level browsing
// context actually lives in.
return nodeDetails.browserId === browsingContext.browserId;
}
return nodeDetails.browsingContextId === browsingContext.id;
};
if (!isSameBrowsingContext(nodeId)) {
return null;
}
return node;
}
/**
* Returns the WebReference for the given node.
*
* Hereby it tries to find a known node reference for that node in the
* node cache, and returns it. Otherwise it creates a new reference and
* adds it to the cache.
*
* @param {BrowsingContext} browsingContext
* The browsing context the node is part of.
* @param {Node} node
* The node to create or get a WebReference for.
*
* @returns {string}
* The unique shared id for the node.
*/
async #getOrCreateNodeReference(browsingContext, node) {
const nodeId = this.#nodeCache.getOrCreateNodeReference(node);
// Update the seen nodes map for WebDriver classic compatibility
await this.messageHandler.sendRootCommand({
moduleName: "script",
commandName: "_addNodeToSeenNodes",
params: {
browsingContext,
nodeId,
},
});
return nodeId;
}
}

View file

@ -4,7 +4,7 @@
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import { RootBiDiModule } from "chrome://remote/content/webdriver-bidi/modules/RootBiDiModule.sys.mjs";
import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
const lazy = {};
@ -63,7 +63,7 @@ const WaitCondition = {
Complete: "complete",
};
class BrowsingContextModule extends RootBiDiModule {
class BrowsingContextModule extends Module {
#contextListener;
#subscribedEvents;

View file

@ -2,7 +2,7 @@
* 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";
import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
const lazy = {};
@ -14,7 +14,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
"chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs",
});
class InputModule extends RootBiDiModule {
class InputModule extends Module {
destroy() {}
async performActions(options = {}) {

View file

@ -2,9 +2,9 @@
* 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";
import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
class LogModule extends RootBiDiModule {
class LogModule extends Module {
destroy() {}
static get supportedEvents() {

View file

@ -2,7 +2,7 @@
* 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";
import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
const lazy = {};
@ -161,7 +161,7 @@ const InitiatorType = {
*/
/* eslint-enable jsdoc/valid-types */
class NetworkModule extends RootBiDiModule {
class NetworkModule extends Module {
#networkListener;
#subscribedEvents;

View file

@ -2,7 +2,7 @@
* 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";
import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
const lazy = {};
@ -42,7 +42,7 @@ const ScriptEvaluateResultType = {
Success: "success",
};
class ScriptModule extends RootBiDiModule {
class ScriptModule extends Module {
#preloadScriptMap;
constructor(messageHandler) {

View file

@ -2,7 +2,7 @@
* 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";
import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
const lazy = {};
@ -16,7 +16,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
});
class SessionModule extends RootBiDiModule {
class SessionModule extends Module {
#browsingContextIdEventMap;
#globalEventSet;

View file

@ -8,6 +8,7 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
action: "chrome://remote/content/shared/webdriver/Actions.sys.mjs",
deserialize: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
});
@ -31,7 +32,6 @@ class InputModule extends WindowGlobalBiDiModule {
}
await this.#deserializeActionOrigins(actions);
const actionChain = lazy.action.Chain.fromJSON(this.#actionState, actions);
await actionChain.dispatch(this.#actionState, this.messageHandler.window);
}
@ -85,8 +85,9 @@ class InputModule extends WindowGlobalBiDiModule {
}
const realm = this.messageHandler.getRealm();
return (await this.deserialize(realm, sharedReference)).data;
return lazy.deserialize(realm, sharedReference, {
nodeCache: this.nodeCache,
});
}
}

View file

@ -13,6 +13,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
"chrome://remote/content/shared/listeners/ConsoleListener.sys.mjs",
isChromeFrame: "chrome://remote/content/shared/Stack.sys.mjs",
OwnershipModel: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
serialize: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
setDefaultSerializationOptions:
"chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
});
@ -103,7 +104,7 @@ class LogModule extends WindowGlobalBiDiModule {
}
}
#onConsoleAPIMessage = async (eventName, data = {}) => {
#onConsoleAPIMessage = (eventName, data = {}) => {
const {
// `arguments` cannot be used as variable name in functions
arguments: messageArguments,
@ -137,17 +138,22 @@ class LogModule extends WindowGlobalBiDiModule {
// Serialize each arg as remote value.
const defaultRealm = this.messageHandler.getRealm();
const nodeCache = this.nodeCache;
const serializedArgs = [];
for (const arg of args) {
// Note that we can pass a default realm for now since realms are only
// involved when creating object references, which will not happen with
// OwnershipModel.None. This will be revisited in Bug 1742589.
serializedArgs.push(
await this.serialize(
lazy.serialize(
Cu.waiveXrays(arg),
lazy.setDefaultSerializationOptions(),
lazy.OwnershipModel.None,
defaultRealm
new Map(),
defaultRealm,
{
nodeCache,
}
)
);
}

View file

@ -7,10 +7,12 @@ import { WindowGlobalBiDiModule } from "chrome://remote/content/webdriver-bidi/m
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
deserialize: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
getFramesFromStack: "chrome://remote/content/shared/Stack.sys.mjs",
isChromeFrame: "chrome://remote/content/shared/Stack.sys.mjs",
OwnershipModel: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
serialize: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
setDefaultSerializationOptions:
"chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
stringify: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
@ -49,7 +51,7 @@ class ScriptModule extends WindowGlobalBiDiModule {
this.#stopObserving();
}
async observe(subject, topic) {
observe(subject, topic) {
if (topic !== "document-element-inserted") {
return;
}
@ -58,11 +60,11 @@ class ScriptModule extends WindowGlobalBiDiModule {
// Ignore events without a window and those from other tabs.
if (window === this.messageHandler.window) {
await this.#evaluatePreloadScripts();
this.#evaluatePreloadScripts();
}
}
async #buildExceptionDetails(exception, stack, realm, resultOwnership) {
#buildExceptionDetails(exception, stack, realm, resultOwnership, options) {
exception = this.#toRawObject(exception);
// A stacktrace is mandatory to build exception details and a missing stack
@ -92,11 +94,13 @@ class ScriptModule extends WindowGlobalBiDiModule {
return {
columnNumber: stack.column - 1,
exception: await this.serialize(
exception: lazy.serialize(
exception,
lazy.setDefaultSerializationOptions(),
resultOwnership,
realm
new Map(),
realm,
options
),
lineNumber: stack.line - 1,
stackTrace: { callFrames },
@ -109,7 +113,8 @@ class ScriptModule extends WindowGlobalBiDiModule {
realm,
awaitPromise,
resultOwnership,
serializationOptions
serializationOptions,
options
) {
let evaluationStatus, exception, result, stack;
@ -155,22 +160,25 @@ class ScriptModule extends WindowGlobalBiDiModule {
case EvaluationStatus.Normal:
return {
evaluationStatus,
result: await this.serialize(
result: lazy.serialize(
this.#toRawObject(result),
serializationOptions,
resultOwnership,
realm
new Map(),
realm,
options
),
realmId: realm.id,
};
case EvaluationStatus.Throw:
return {
evaluationStatus,
exceptionDetails: await this.#buildExceptionDetails(
exceptionDetails: this.#buildExceptionDetails(
exception,
stack,
realm,
resultOwnership
resultOwnership,
options
),
realmId: realm.id,
};
@ -188,17 +196,18 @@ class ScriptModule extends WindowGlobalBiDiModule {
* @param {ChannelProperties} channelProperties
* @param {RemoteValue} message
*/
async #emitScriptMessage(realm, channelProperties, message) {
#emitScriptMessage = (realm, channelProperties, message) => {
const {
channel,
ownership: ownershipType = lazy.OwnershipModel.None,
serializationOptions,
} = channelProperties;
const data = await this.serialize(
const data = lazy.serialize(
this.#toRawObject(message),
lazy.setDefaultSerializationOptions(serializationOptions),
ownershipType,
new Map(),
realm
);
@ -207,9 +216,9 @@ class ScriptModule extends WindowGlobalBiDiModule {
data,
source: this.#getSource(realm),
});
}
};
async #evaluatePreloadScripts() {
#evaluatePreloadScripts() {
let resolveBlockerPromise;
const blockerPromise = new Promise(resolve => {
resolveBlockerPromise = resolve;
@ -223,17 +232,12 @@ class ScriptModule extends WindowGlobalBiDiModule {
functionDeclaration,
sandbox,
} = script;
const realm = this.messageHandler.getRealm({ sandboxName: sandbox });
const deserializedArguments = [];
for (const commandArgument of commandArguments) {
const argValue = await this.deserialize(realm, commandArgument, {
emitScriptMessage: this.#emitScriptMessage.bind(this),
});
deserializedArguments.push(argValue.data);
}
const deserializedArguments = commandArguments.map(arg =>
lazy.deserialize(realm, arg, {
emitScriptMessage: this.#emitScriptMessage,
})
);
const rv = realm.executeInGlobalWithBindings(
functionDeclaration,
deserializedArguments
@ -332,26 +336,25 @@ class ScriptModule extends WindowGlobalBiDiModule {
} = options;
const realm = this.messageHandler.getRealm({ realmId, sandboxName });
const nodeCache = this.nodeCache;
const deserializedArguments = [];
if (commandArguments !== null) {
const args = await Promise.all(
commandArguments.map(arg =>
this.deserialize(realm, arg, {
emitScriptMessage: this.#emitScriptMessage.bind(this),
const deserializedArguments =
commandArguments !== null
? commandArguments.map(arg =>
lazy.deserialize(realm, arg, {
emitScriptMessage: this.#emitScriptMessage,
nodeCache,
})
)
: [];
const deserializedThis =
thisParameter !== null
? lazy.deserialize(realm, thisParameter, {
emitScriptMessage: this.#emitScriptMessage,
nodeCache,
})
)
);
args.forEach(arg => deserializedArguments.push(arg.data));
}
let deserializedThis = null;
if (thisParameter !== null) {
const thisArg = await this.deserialize(realm, thisParameter, {
emitScriptMessage: this.#emitScriptMessage.bind(this),
});
deserializedThis = thisArg.data;
}
: null;
const rv = realm.executeInGlobalWithBindings(
functionDeclaration,
@ -364,7 +367,10 @@ class ScriptModule extends WindowGlobalBiDiModule {
realm,
awaitPromise,
resultOwnership,
serializationOptions
serializationOptions,
{
nodeCache,
}
);
}
@ -430,7 +436,10 @@ class ScriptModule extends WindowGlobalBiDiModule {
realm,
awaitPromise,
resultOwnership,
serializationOptions
serializationOptions,
{
nodeCache: this.nodeCache,
}
);
}

View file

@ -444,14 +444,14 @@ const REMOTE_COMPLEX_VALUES = [
},
];
add_task(async function test_deserializePrimitiveTypes() {
add_task(function test_deserializePrimitiveTypes() {
const realm = new Realm();
for (const type of PRIMITIVE_TYPES) {
const { value: expectedValue, serialized } = type;
info(`Checking '${serialized.type}'`);
const value = (await deserialize(realm, serialized)).data;
const value = deserialize(realm, serialized);
if (serialized.value == "NaN") {
ok(Number.isNaN(value), `Got expected value for ${serialized}`);
@ -465,7 +465,7 @@ add_task(async function test_deserializePrimitiveTypes() {
}
});
add_task(async function test_deserializeDateLocalValue() {
add_task(function test_deserializeDateLocalValue() {
const realm = new Realm();
const validaDateStrings = [
@ -485,9 +485,7 @@ add_task(async function test_deserializeDateLocalValue() {
];
for (const dateString of validaDateStrings) {
info(`Checking '${dateString}'`);
const value = (
await deserialize(realm, { type: "date", value: dateString })
).data;
const value = deserialize(realm, { type: "date", value: dateString });
Assert.equal(
value.getTime(),
@ -497,7 +495,7 @@ add_task(async function test_deserializeDateLocalValue() {
}
});
add_task(async function test_deserializeLocalValues() {
add_task(function test_deserializeLocalValues() {
const realm = new Realm();
for (const type of REMOTE_SIMPLE_VALUES.concat(REMOTE_COMPLEX_VALUES)) {
@ -509,7 +507,7 @@ add_task(async function test_deserializeLocalValues() {
}
info(`Checking '${serialized.type}'`);
const value = (await deserialize(realm, serialized)).data;
const value = deserialize(realm, serialized);
assertLocalValue(serialized.type, value, expectedValue);
}
});
@ -539,9 +537,7 @@ add_task(async function test_deserializeChannel() {
};
info(`Checking 'channel'`);
const deserializedValue = (
await deserialize(realm, channel, deserializationOptions)
).data;
const deserializedValue = deserialize(realm, channel, deserializationOptions);
Assert.equal(
Object.prototype.toString.call(deserializedValue),
"[object Function]",
@ -550,7 +546,7 @@ add_task(async function test_deserializeChannel() {
Assert.equal(deserializedValue("foo"), "foo", "Got expected result");
});
add_task(async function test_deserializeLocalValuesByHandle() {
add_task(function test_deserializeLocalValuesByHandle() {
// Create two realms, realm1 will be used to serialize values, while realm2
// will be used as a reference empty realm without any object reference.
const realm1 = new Realm();
@ -563,7 +559,7 @@ add_task(async function test_deserializeLocalValuesByHandle() {
info(`Checking '${serialized.type}'`);
// Serialize the value once to get a handle.
const serializedValue = await serialize(
const serializedValue = serialize(
expectedValue,
{ maxObjectDepth: 0 },
"root",
@ -576,62 +572,43 @@ add_task(async function test_deserializeLocalValuesByHandle() {
const remoteReference = { handle: serializedValue.handle };
// Check that the remote reference can be deserialized in realm1.
const deserializedValue = (await deserialize(realm1, remoteReference)).data;
const deserializedValue = deserialize(realm1, remoteReference);
assertLocalValue(serialized.type, deserializedValue, expectedValue);
try {
await deserialize(realm2, remoteReference);
ok(false, "Expected no such handle error to be raised");
} catch (e) {
Assert.equal(
e.name,
"NoSuchHandleError",
"Got expected error when using the wrong realm for deserialize"
);
}
Assert.throws(
() => deserialize(realm2, remoteReference),
/NoSuchHandleError:/,
`Got expected error when using the wrong realm for deserialize`
);
realm1.removeObjectHandle(serializedValue.handle);
try {
await deserialize(realm1, remoteReference);
ok(false, "Expected no such handle error to be raised");
} catch (e) {
Assert.equal(
e.name,
"NoSuchHandleError",
"Got expected error when after deleting the object handle"
);
}
Assert.throws(
() => deserialize(realm1, remoteReference),
/NoSuchHandleError:/,
`Got expected error when after deleting the object handle`
);
}
});
add_task(async function test_deserializeHandleInvalidTypes() {
add_task(function test_deserializeHandleInvalidTypes() {
const realm = new Realm();
for (const invalidType of [false, 42, {}, []]) {
info(`Checking type: '${invalidType}'`);
try {
await deserialize(realm, { type: "object", handle: invalidType });
ok(false, "Expected invalid argument error to be raised");
} catch (e) {
Assert.equal(
e.name,
"InvalidArgumentError",
`Got expected error for type ${invalidType}`
);
}
Assert.throws(
() => deserialize(realm, { type: "object", handle: invalidType }),
/InvalidArgumentError:/,
`Got expected error for type ${invalidType}`
);
}
});
add_task(async function test_deserializeSharedIdInvalidTypes() {
add_task(function test_deserializeSharedIdInvalidTypes() {
const nodeCache = new NodeCache();
const realm = new WindowRealm(browser.document.defaultView);
function getNode(bc, nodeId) {
return nodeCache.getNode(bc, nodeId);
}
for (const invalidType of [false, 42, {}, []]) {
info(`Checking type: '${invalidType}'`);
@ -639,20 +616,15 @@ add_task(async function test_deserializeSharedIdInvalidTypes() {
sharedId: invalidType,
};
try {
(await deserialize(realm, serializedValue, { getNode })).data;
ok(false, "Expected invalid argument error to be raised");
} catch (e) {
Assert.equal(
e.name,
"InvalidArgumentError",
`Got expected error for type ${invalidType}`
);
}
Assert.throws(
() => deserialize(realm, serializedValue, { nodeCache }),
/InvalidArgumentError:/,
`Got expected error for type ${invalidType}`
);
}
});
add_task(async function test_deserializeSharedIdInvalidValue() {
add_task(function test_deserializeSharedIdInvalidValue() {
const nodeCache = new NodeCache();
const serializedValue = {
@ -661,23 +633,14 @@ add_task(async function test_deserializeSharedIdInvalidValue() {
const realm = new WindowRealm(browser.document.defaultView);
function getNode(bc, nodeId) {
return nodeCache.getNode(bc, nodeId);
}
try {
(await deserialize(realm, serializedValue, { getNode })).data;
ok(false, "Expected no such node error to be raised");
} catch (e) {
Assert.equal(
e.name,
"NoSuchNodeError",
"Got expected error for unknown 'sharedId'"
);
}
Assert.throws(
() => deserialize(realm, serializedValue, { nodeCache }),
/NoSuchNodeError:/,
"Got expected error for unknown 'sharedId'"
);
});
add_task(async function test_deserializeSharedId() {
add_task(function test_deserializeSharedId() {
const nodeCache = new NodeCache();
const domElRef = nodeCache.getOrCreateNodeReference(domEl);
@ -687,16 +650,12 @@ add_task(async function test_deserializeSharedId() {
const realm = new WindowRealm(browser.document.defaultView);
function getNode(bc, nodeId) {
return nodeCache.getNode(bc, nodeId);
}
const node = (await deserialize(realm, serializedValue, { getNode })).data;
const node = deserialize(realm, serializedValue, { nodeCache });
Assert.equal(node, domEl);
});
add_task(async function test_deserializeSharedIdPrecedenceOverHandle() {
add_task(function test_deserializeSharedIdPrecedenceOverHandle() {
const nodeCache = new NodeCache();
const domElRef = nodeCache.getOrCreateNodeReference(domEl);
@ -707,16 +666,12 @@ add_task(async function test_deserializeSharedIdPrecedenceOverHandle() {
const realm = new WindowRealm(browser.document.defaultView);
function getNode(bc, nodeId) {
return nodeCache.getNode(bc, nodeId);
}
const node = (await deserialize(realm, serializedValue, { getNode })).data;
const node = deserialize(realm, serializedValue, { nodeCache });
Assert.equal(node, domEl);
});
add_task(async function test_deserializeSharedIdNoWindowRealm() {
add_task(function test_deserializeSharedIdNoWindowRealm() {
const nodeCache = new NodeCache();
const domElRef = nodeCache.getOrCreateNodeReference(domEl);
@ -726,23 +681,30 @@ add_task(async function test_deserializeSharedIdNoWindowRealm() {
const realm = new Realm();
function getNode(bc, nodeId) {
return nodeCache.getNode(bc, nodeId);
}
try {
(await deserialize(realm, serializedValue, { getNode })).data;
ok(false, "Expected no such node error to be raised");
} catch (e) {
Assert.equal(
e.name,
"NoSuchNodeError",
"Got expected error for a non-window realm"
);
}
Assert.throws(
() => deserialize(realm, serializedValue, { nodeCache }),
/NoSuchNodeError/,
`Got expected error for a non-window realm`
);
});
add_task(async function test_deserializePrimitiveTypesInvalidValues() {
// Bug 1819902: Instead of a browsing context check compare the origin
add_task(function test_deserializeSharedIdOtherBrowsingContext() {
const nodeCache = new NodeCache();
const domElRef = nodeCache.getOrCreateNodeReference(domEl);
const serializedValue = {
sharedId: domElRef,
};
const realm = new WindowRealm(iframeEl.contentWindow);
const node = deserialize(realm, serializedValue, { nodeCache });
Assert.equal(node, null);
});
add_task(function test_deserializePrimitiveTypesInvalidValues() {
const realm = new Realm();
const invalidValues = [
@ -761,21 +723,16 @@ add_task(async function test_deserializePrimitiveTypesInvalidValues() {
for (const value of values) {
info(`Checking '${type}' with value ${value}`);
try {
(await deserialize(realm, { type, value })).data;
ok(false, "Expected invalid argument error to be raised");
} catch (e) {
Assert.equal(
e.name,
"InvalidArgumentError",
`Got expected error for type ${type} and value ${value}`
);
}
Assert.throws(
() => deserialize(realm, { type, value }),
/InvalidArgument/,
`Got expected error for type ${type} and value ${value}`
);
}
}
});
add_task(async function test_deserializeDateLocalValueInvalidValues() {
add_task(function test_deserializeDateLocalValueInvalidValues() {
const realm = new Realm();
const invalidaDateStrings = [
@ -817,20 +774,15 @@ add_task(async function test_deserializeDateLocalValueInvalidValues() {
for (const dateString of invalidaDateStrings) {
info(`Checking '${dateString}'`);
try {
(await deserialize(realm, { type: "date", value: dateString })).data;
ok(false, "Expected invalid argument error to be raised");
} catch (e) {
Assert.equal(
e.name,
"InvalidArgumentError",
`Got expected error for date string: ${dateString}`
);
}
Assert.throws(
() => deserialize(realm, { type: "date", value: dateString }),
/InvalidArgumentError:/,
`Got expected error for date string: ${dateString}`
);
}
});
add_task(async function test_deserializeLocalValuesInvalidType() {
add_task(function test_deserializeLocalValuesInvalidType() {
const realm = new Realm();
const invalidTypes = [undefined, null, false, 42, {}];
@ -838,36 +790,25 @@ add_task(async function test_deserializeLocalValuesInvalidType() {
for (const invalidType of invalidTypes) {
info(`Checking type: '${invalidType}'`);
try {
(await deserialize(realm, { type: invalidType })).data;
ok(false, "Expected invalid argument error to be raised");
} catch (e) {
Assert.equal(
e.name,
"InvalidArgumentError",
`Got expected error for type ${invalidType}`
);
}
Assert.throws(
() => deserialize(realm, { type: invalidType }),
/InvalidArgumentError:/,
`Got expected error for type ${invalidType}`
);
try {
(
await deserialize(realm, {
Assert.throws(
() =>
deserialize(realm, {
type: "array",
value: [{ type: invalidType }],
})
).data;
ok(false, "Expected invalid argument error to be raised");
} catch (e) {
Assert.equal(
e.name,
"InvalidArgumentError",
`Got expected error for nested type ${invalidType}`
);
}
}),
/InvalidArgumentError:/,
`Got expected error for nested type ${invalidType}`
);
}
});
add_task(async function test_deserializeLocalValuesInvalidValues() {
add_task(function test_deserializeLocalValuesInvalidValues() {
const realm = new Realm();
const invalidValues = [
@ -970,21 +911,16 @@ add_task(async function test_deserializeLocalValuesInvalidValues() {
for (const value of values) {
info(`Checking '${type}' with value ${value}`);
try {
(await deserialize(realm, { type, value })).data;
ok(false, "Expected invalid argument error to be raised");
} catch (e) {
Assert.equal(
e.name,
"InvalidArgumentError",
`Got expected error for type ${type} and value ${value}`
);
}
Assert.throws(
() => deserialize(realm, { type, value }),
/InvalidArgumentError:/,
`Got expected error for type ${type} and value ${value}`
);
}
}
});
add_task(async function test_serializePrimitiveTypes() {
add_task(function test_serializePrimitiveTypes() {
const realm = new Realm();
for (const type of PRIMITIVE_TYPES) {
@ -992,7 +928,7 @@ add_task(async function test_serializePrimitiveTypes() {
const defaultSerializationOptions = setDefaultSerializationOptions();
const serializationInternalMap = new Map();
const serializedValue = await serialize(
const serializedValue = serialize(
value,
defaultSerializationOptions,
"none",
@ -1005,7 +941,7 @@ add_task(async function test_serializePrimitiveTypes() {
// For primitive values, the serialization with ownershipType=root should
// be exactly identical to the one with ownershipType=none.
const serializationInternalMapWithRoot = new Map();
const serializedWithRoot = await serialize(
const serializedWithRoot = serialize(
value,
defaultSerializationOptions,
"root",
@ -1017,7 +953,7 @@ add_task(async function test_serializePrimitiveTypes() {
}
});
add_task(async function test_serializeRemoteSimpleValues() {
add_task(function test_serializeRemoteSimpleValues() {
const realm = new Realm();
for (const type of REMOTE_SIMPLE_VALUES) {
@ -1026,7 +962,7 @@ add_task(async function test_serializeRemoteSimpleValues() {
info(`Checking '${serialized.type}' with none ownershipType`);
const serializationInternalMapWithNone = new Map();
const serializedValue = await serialize(
const serializedValue = serialize(
value,
defaultSerializationOptions,
"none",
@ -1039,7 +975,7 @@ add_task(async function test_serializeRemoteSimpleValues() {
info(`Checking '${serialized.type}' with root ownershipType`);
const serializationInternalMapWithRoot = new Map();
const serializedWithRoot = await serialize(
const serializedWithRoot = serialize(
value,
defaultSerializationOptions,
"root",
@ -1061,7 +997,7 @@ add_task(async function test_serializeRemoteSimpleValues() {
}
});
add_task(async function test_serializeRemoteComplexValues() {
add_task(function test_serializeRemoteComplexValues() {
const realm = new Realm();
for (const type of REMOTE_COMPLEX_VALUES) {
@ -1072,7 +1008,7 @@ add_task(async function test_serializeRemoteComplexValues() {
info(`Checking '${serialized.type}' with none ownershipType`);
const serializationInternalMapWithNone = new Map();
const serializedValue = await serialize(
const serializedValue = serialize(
value,
serializationOptionsWithDefaults,
"none",
@ -1085,7 +1021,7 @@ add_task(async function test_serializeRemoteComplexValues() {
info(`Checking '${serialized.type}' with root ownershipType`);
const serializationInternalMapWithRoot = new Map();
const serializedWithRoot = await serialize(
const serializedWithRoot = serialize(
value,
serializationOptionsWithDefaults,
"root",
@ -1107,7 +1043,7 @@ add_task(async function test_serializeRemoteComplexValues() {
}
});
add_task(async function test_serializeNodeChildren() {
add_task(function test_serializeNodeChildren() {
const nodeCache = new NodeCache();
// Add the used elements to the cache so that we know the unique reference.
const bodyElRef = nodeCache.getOrCreateNodeReference(bodyEl);
@ -1116,11 +1052,6 @@ add_task(async function test_serializeNodeChildren() {
const realm = new WindowRealm(browser.document.defaultView);
function getOrCreateNodeReference(bc, node) {
Assert.equal(bc, realm.browsingContext, "Got expected browsing context");
return nodeCache.getOrCreateNodeReference(node);
}
const dataSet = [
{
node: bodyEl,
@ -1275,20 +1206,20 @@ add_task(async function test_serializeNodeChildren() {
const serializationInternalMap = new Map();
const serializedValue = await serialize(
const serializedValue = serialize(
node,
serializationOptions,
"none",
serializationInternalMap,
realm,
{ getOrCreateNodeReference }
{ nodeCache }
);
Assert.deepEqual(serializedValue, serialized, "Got expected structure");
}
});
add_task(async function test_serializeShadowRoot() {
add_task(function test_serializeShadowRoot() {
const nodeCache = new NodeCache();
const realm = new WindowRealm(browser.document.defaultView);
@ -1309,11 +1240,6 @@ add_task(async function test_serializeShadowRoot() {
insideShadowRootElement
);
function getOrCreateNodeReference(bc, node) {
Assert.equal(bc, realm.browsingContext, "Got expected browsing context");
return nodeCache.getOrCreateNodeReference(node);
}
const dataSet = [
{
node: customElement,
@ -1440,13 +1366,13 @@ add_task(async function test_serializeShadowRoot() {
const serializationInternalMap = new Map();
const serializedValue = await serialize(
const serializedValue = serialize(
node,
serializationOptions,
"none",
serializationInternalMap,
realm,
{ getOrCreateNodeReference }
{ nodeCache }
);
Assert.deepEqual(serializedValue, serialized, "Got expected structure");
@ -1454,7 +1380,7 @@ add_task(async function test_serializeShadowRoot() {
}
});
add_task(async function test_serializeWithSerializationInternalMap() {
add_task(function test_serializeWithSerializationInternalMap() {
const dataSet = [
{
data: [1],
@ -1497,7 +1423,7 @@ add_task(async function test_serializeWithSerializationInternalMap() {
{ bar: data },
];
const serializedValue = await serialize(
const serializedValue = serialize(
value,
{ maxObjectDepth: 2 },
"none",
@ -1544,36 +1470,34 @@ add_task(async function test_serializeWithSerializationInternalMap() {
}
});
add_task(
async function test_serializeMultipleValuesWithSerializationInternalMap() {
const realm = new Realm();
const serializationInternalMap = new Map();
const obj1 = { foo: "bar" };
const obj2 = [1, 2];
const value = [obj1, obj2, obj1, obj2];
add_task(function test_serializeMultipleValuesWithSerializationInternalMap() {
const realm = new Realm();
const serializationInternalMap = new Map();
const obj1 = { foo: "bar" };
const obj2 = [1, 2];
const value = [obj1, obj2, obj1, obj2];
await serialize(
value,
{ maxObjectDepth: 2 },
"none",
serializationInternalMap,
realm
);
serialize(
value,
{ maxObjectDepth: 2 },
"none",
serializationInternalMap,
realm
);
assertInternalIds(serializationInternalMap, 2);
assertInternalIds(serializationInternalMap, 2);
const internalId1 = serializationInternalMap.get(obj1).internalId;
const internalId2 = serializationInternalMap.get(obj2).internalId;
const internalId1 = serializationInternalMap.get(obj1).internalId;
const internalId2 = serializationInternalMap.get(obj2).internalId;
Assert.notEqual(
internalId1,
internalId2,
"Internal ids for different object are also different"
);
}
);
Assert.notEqual(
internalId1,
internalId2,
"Internal ids for different object are also different"
);
});
add_task(async function test_serializeNodeSharedId() {
add_task(function test_serializeNodeSharedId() {
const nodeCache = new NodeCache();
// Already add the domEl to the cache so that we know the unique reference.
const domElRef = nodeCache.getOrCreateNodeReference(domEl);
@ -1581,18 +1505,13 @@ add_task(async function test_serializeNodeSharedId() {
const realm = new WindowRealm(browser.document.defaultView);
const serializationInternalMap = new Map();
function getOrCreateNodeReference(bc, node) {
Assert.equal(bc, realm.browsingContext, "Got expected browsing context");
return nodeCache.getOrCreateNodeReference(node);
}
const serializedValue = await serialize(
const serializedValue = serialize(
domEl,
{ maxDomDepth: 0 },
"root",
serializationInternalMap,
realm,
{ getOrCreateNodeReference }
{ nodeCache }
);
Assert.equal(nodeCache.size, 1, "No additional reference added");
@ -1600,32 +1519,27 @@ add_task(async function test_serializeNodeSharedId() {
Assert.notEqual(serializedValue.handle, domElRef);
});
add_task(async function test_serializeNodeSharedId_noWindowRealm() {
add_task(function test_serializeNodeSharedId_noWindowRealm() {
const nodeCache = new NodeCache();
nodeCache.getOrCreateNodeReference(domEl);
const realm = new Realm();
const serializationInternalMap = new Map();
function getOrCreateNodeReference(bc, node) {
Assert.equal(bc, realm.browsingContext, "Got expected browsing context");
return nodeCache.getOrCreateNodeReference(node);
}
const serializedValue = await serialize(
const serializedValue = serialize(
domEl,
{ maxDomDepth: 0 },
"none",
serializationInternalMap,
realm,
{ getOrCreateNodeReference }
{ nodeCache }
);
Assert.equal(nodeCache.size, 1, "No additional reference added");
Assert.equal(serializedValue.sharedId, undefined);
});
add_task(async function test_stringify() {
add_task(function test_stringify() {
const STRINGIFY_TEST_CASES = [
[undefined, "undefined"],
[null, "null"],
@ -1705,8 +1619,6 @@ function assertInternalIds(serializationInternalMap, amount) {
);
}
function deserializeValue(realm, value) {}
function deserializeInWindowRealm(serialized) {
return SpecialPowers.spawn(
gBrowser.selectedBrowser,
@ -1720,7 +1632,7 @@ function deserializeInWindowRealm(serialized) {
);
const realm = new WindowRealm(content);
info(`Checking '${_serialized.type}'`);
return (await deserialize(realm, _serialized)).data;
return deserialize(realm, _serialized);
}
);
}

View file

@ -297,7 +297,7 @@ class TestNavigate(BaseNavigationTestCase):
self.marionette.navigate("about:robots")
self.assertFalse(self.is_remote_tab)
with self.assertRaises(errors.StaleElementException):
with self.assertRaises(errors.NoSuchElementException):
elem.click()
def test_about_blank_for_new_docshell(self):

View file

@ -0,0 +1,8 @@
[back.py]
expected:
if (os == "win") and not debug and (processor == "x86_64"): [OK, TIMEOUT]
if (os == "linux") and fission and not debug: [OK, TIMEOUT]
if (os == "android") and not debug: [OK, TIMEOUT]
[test_cross_origin[capabilities0\]]
expected:
if not fission and (os == "linux"): FAIL

View file

@ -0,0 +1,7 @@
[forward.py]
[test_cross_origin[capabilities0\]]
expected:
if (os == "linux") and not fission: FAIL
if (os == "win") and not fission: FAIL
if (os == "mac") and not fission: FAIL
PASS

View file

@ -0,0 +1,7 @@
[navigate.py]
[test_cross_origin[capabilities0\]]
expected:
if (os == "linux") and not fission: FAIL
if (os == "win") and not fission: FAIL
if (os == "mac") and not fission: FAIL
PASS

View file

@ -1,33 +0,0 @@
import pytest
from webdriver.bidi import error
from webdriver.bidi.modules.script import ContextTarget
@pytest.mark.asyncio
async def test_remote_reference_node_argument_different_browsing_context(
bidi_session, get_test_page, top_context
):
await bidi_session.browsing_context.navigate(
context=top_context["context"], url=get_test_page(), wait="complete"
)
remote_reference = await bidi_session.script.evaluate(
expression="""document.querySelector("#button")""",
await_promise=False,
target=ContextTarget(top_context["context"]),
)
# Retrieve the first child browsing context
all_contexts = await bidi_session.browsing_context.get_tree(
root=top_context["context"]
)
assert len(all_contexts) == 1
child_context = all_contexts[0]["children"][0]["context"]
with pytest.raises(error.NoSuchNodeException):
await bidi_session.script.call_function(
function_declaration="(node) => node.nodeType",
arguments=[remote_reference],
await_promise=False,
target=ContextTarget(child_context),
)

View file

@ -164,6 +164,6 @@ def test_cross_origin(session, url):
assert session.url == first_page
with pytest.raises(error.StaleElementReferenceException):
with pytest.raises(error.NoSuchElementException):
elem.click()
elem = session.find.css("#delete", all=False)

View file

@ -136,124 +136,94 @@ async def test_local_value(bidi_session, top_context, argument, expected_type):
@pytest.mark.asyncio
@pytest.mark.parametrize(
"setup_expression, function_declaration, await_promise, expected",
"setup_expression, function_declaration, expected",
[
(
"Symbol('foo')",
"(symbol) => symbol.toString()",
False,
{"type": "string", "value": "Symbol(foo)"},
),
(
"[1,2]",
"(array) => array[0]",
False,
{"type": "number", "value": 1}),
("[1,2]", "(array) => array[0]", {"type": "number", "value": 1}),
(
"new RegExp('foo')",
"(regexp) => regexp.source",
False,
{"type": "string", "value": "foo"},
),
(
"new Date(1654004849000)",
"(date) => date.toISOString()",
False,
{"type": "string", "value": "2022-05-31T13:47:29.000Z"},
),
(
"new Map([['foo', 'bar']])",
"(map) => map.get('foo')",
False,
{"type": "string", "value": "bar"},
),
(
"new Set(['foo'])",
"(set) => set.has('foo')",
False,
{"type": "boolean", "value": True},
),
(
"{const weakMap = new WeakMap(); weakMap.set(weakMap, 'foo')}",
"(weakMap)=> weakMap.get(weakMap)",
False,
{"type": "string", "value": "foo"},
),
(
"{const weakSet = new WeakSet(); weakSet.add(weakSet)}",
"(weakSet)=> weakSet.has(weakSet)",
False,
{"type": "boolean", "value": True},
),
(
"new Error('error message')",
"(error) => error.message",
False,
{"type": "string", "value": "error message"},
),
(
"new SyntaxError('syntax error message')",
"(error) => error.message",
False,
{"type": "string", "value": "syntax error message"},
),
(
"new Promise((resolve) => resolve(3))",
"(promise) => promise",
True,
{"type": "number", "value": 3},
),
(
"new Promise(() => {})",
"(promise) => promise",
False,
{"type": "promise"},
),
(
"new Int8Array(2)",
"(int8Array) => int8Array.length",
False,
{"type": "number", "value": 2},
),
(
"new ArrayBuffer(8)",
"(arrayBuffer) => arrayBuffer.byteLength",
False,
{"type": "number", "value": 8},
),
(
"() => true",
"(func) => func()",
False,
{"type": "boolean", "value": True}),
("() => true", "(func) => func()", {"type": "boolean", "value": True}),
(
"(function() {return false;})",
"(func) => func()",
False,
{"type": "boolean", "value": False},
),
(
"window.foo = 3; window",
"(window) => window.foo",
False,
{"type": "number", "value": 3},
),
(
"window.url = new URL('https://example.com'); window.url",
"(url) => url.hostname",
False,
{"type": "string", "value": "example.com"},
),
(
"({SOME_PROPERTY:'SOME_VALUE'})",
"(obj) => obj.SOME_PROPERTY",
False,
{"type": "string", "value": "SOME_VALUE"},
),
],
)
async def test_remote_reference_argument(
bidi_session, top_context, setup_expression, function_declaration, await_promise, expected
bidi_session, top_context, setup_expression, function_declaration, expected
):
remote_value_result = await bidi_session.script.evaluate(
expression=setup_expression,
@ -268,7 +238,7 @@ async def test_remote_reference_argument(
result = await bidi_session.script.call_function(
function_declaration=function_declaration,
arguments=[{"handle": remote_value_handle}],
await_promise=await_promise,
await_promise=True if remote_value_result["type"] == "promise" else False,
target=ContextTarget(top_context["context"]),
)

View file

@ -1,5 +1,6 @@
import pytest
from webdriver import error
from webdriver.error import NoSuchElementException
from tests.support.asserts import assert_success
from tests.support.helpers import wait_for_new_handle
@ -120,7 +121,7 @@ def test_link_from_toplevel_context_with_target(session, inline, target):
wait = Poll(
session,
timeout=5,
ignored_exceptions=error.NoSuchElementException,
ignored_exceptions=NoSuchElementException,
message="Expected element has not been found")
wait.until(lambda s: s.find.css("#foo"))
@ -159,7 +160,7 @@ def test_link_from_nested_context_with_target(session, inline, iframe, target):
wait = Poll(
session,
timeout=5,
ignored_exceptions=error.NoSuchElementException,
ignored_exceptions=NoSuchElementException,
message="Expected element has not been found")
wait.until(lambda s: s.find.css("#foo"))
@ -179,8 +180,6 @@ def test_link_cross_origin(session, inline, url):
assert_success(response)
assert session.url == target_page
with pytest.raises(error.StaleElementReferenceException):
link.click()
session.find.css("#delete", all=False)

View file

@ -190,6 +190,6 @@ def test_cross_origin(session, url):
assert session.url == second_page
with pytest.raises(error.StaleElementReferenceException):
with pytest.raises(error.NoSuchElementException):
elem.click()
elem = session.find.css("#delete", all=False)

View file

@ -74,7 +74,7 @@ def test_cross_origin(session, inline, url):
assert_success(response)
assert session.url == second_page
with pytest.raises(error.StaleElementReferenceException):
with pytest.raises(error.NoSuchElementException):
elem.click()
session.find.css("#delete", all=False)