Bug 1692468 - [marionette] Move Element Reference store as node cache into content process. r=jgraham,webdriver-reviewers,jdescottes

Differential Revision: https://phabricator.services.mozilla.com/D151258
This commit is contained in:
Henrik Skupin 2023-01-01 20:24:02 +00:00
parent ffe403d95c
commit 88445f5567
23 changed files with 675 additions and 700 deletions

View File

@ -57,7 +57,10 @@ remote.jar:
content/shared/webdriver/Capabilities.sys.mjs (shared/webdriver/Capabilities.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)
content/shared/webdriver/Session.sys.mjs (shared/webdriver/Session.sys.mjs)
content/shared/webdriver/process-actors/WebDriverProcessDataChild.sys.mjs (shared/webdriver/process-actors/WebDriverProcessDataChild.sys.mjs)
content/shared/webdriver/process-actors/WebDriverProcessDataParent.sys.mjs (shared/webdriver/process-actors/WebDriverProcessDataParent.sys.mjs)
# imports from external folders
content/external/EventUtils.js (../testing/mochitest/tests/SimpleTest/EventUtils.js)

View File

@ -27,11 +27,14 @@ XPCOMUtils.defineLazyGetter(lazy, "logger", () =>
);
export class MarionetteCommandsChild extends JSWindowActorChild {
#processActor;
constructor() {
super();
// The following state is session-specific. It's assumed that we only have
// a single session at a time, and the actor is destroyed at the end of a session.
this.#processActor = ChromeUtils.domProcessChild.getActor(
"WebDriverProcessData"
);
// sandbox storage and name of the current sandbox
this.sandboxes = new lazy.Sandboxes(() => this.document.defaultView);
@ -61,6 +64,13 @@ export class MarionetteCommandsChild extends JSWindowActorChild {
);
}
didDestroy() {
lazy.logger.trace(
`[${this.browsingContext.id}] MarionetteCommands actor destroyed ` +
`for window id ${this.innerWindowId}`
);
}
async receiveMessage(msg) {
if (!this.contentWindow) {
throw new DOMException("Actor is no longer active", "InactiveActor");
@ -72,8 +82,8 @@ export class MarionetteCommandsChild extends JSWindowActorChild {
const { name, data: serializedData } = msg;
const data = lazy.evaluate.fromJSON(serializedData, {
seenEls: null,
win: this.document.defaultView,
seenEls: this.#processActor.getNodeCache(),
win: this.contentWindow,
});
switch (name) {
@ -165,10 +175,11 @@ export class MarionetteCommandsChild extends JSWindowActorChild {
await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
}
// The element reference store lives in the parent process. Calling
// toJSON() without a second argument here passes element reference ids
// of DOM nodes to the parent frame.
return { data: lazy.evaluate.toJSON(result) };
return {
data: lazy.evaluate.toJSON(result, {
seenEls: this.#processActor.getNodeCache(),
}),
};
} catch (e) {
// Always wrap errors as WebDriverError
return { error: lazy.error.wrap(e).toJSON() };

View File

@ -8,7 +8,6 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
capture: "chrome://remote/content/shared/Capture.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",
Log: "chrome://remote/content/shared/Log.sys.mjs",
@ -17,16 +16,10 @@ ChromeUtils.defineESModuleGetters(lazy, {
XPCOMUtils.defineLazyGetter(lazy, "logger", () =>
lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
);
XPCOMUtils.defineLazyGetter(lazy, "elementIdCache", () => {
return new lazy.element.ReferenceStore();
});
export class MarionetteCommandsParent extends JSWindowActorParent {
actorCreated() {
this._resolveDialogOpened = null;
this.topWindow = this.browsingContext.top.embedderElement?.ownerGlobal;
this.topWindow?.addEventListener("TabClose", _onTabClose);
}
dialogOpenedPromise() {
@ -36,9 +29,7 @@ export class MarionetteCommandsParent extends JSWindowActorParent {
}
async sendQuery(name, data) {
const serializedData = lazy.evaluate.toJSON(data, {
seenEls: lazy.elementIdCache,
});
const serializedData = lazy.evaluate.toJSON(data);
// return early if a dialog is opened
const result = await Promise.race([
@ -51,16 +42,10 @@ export class MarionetteCommandsParent extends JSWindowActorParent {
if ("error" in result) {
throw lazy.error.WebDriverError.fromJSON(result.error);
} else {
return lazy.evaluate.fromJSON(result.data, {
seenEls: lazy.elementIdCache,
});
return lazy.evaluate.fromJSON(result.data);
}
}
didDestroy() {
this.topWindow?.removeEventListener("TabClose", _onTabClose);
}
notifyDialogOpened() {
if (this._resolveDialogOpened) {
this._resolveDialogOpened({ data: null });
@ -269,17 +254,6 @@ export class MarionetteCommandsParent extends JSWindowActorParent {
}
}
/**
* Clear all the entries from the element id cache.
*/
export function clearElementIdCache() {
lazy.elementIdCache.clear();
}
function _onTabClose(event) {
lazy.elementIdCache.clear(event.target.linkedBrowser.browsingContext);
}
/**
* Proxy that will dynamically create MarionetteCommands actors for a dynamically
* provided browsing context until the method can be fully executed by the

View File

@ -18,8 +18,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
atom: "chrome://remote/content/marionette/atom.sys.mjs",
browser: "chrome://remote/content/marionette/browser.sys.mjs",
capture: "chrome://remote/content/shared/Capture.sys.mjs",
clearElementIdCache:
"chrome://remote/content/marionette/actors/MarionetteCommandsParent.sys.mjs",
Context: "chrome://remote/content/marionette/browser.sys.mjs",
cookie: "chrome://remote/content/marionette/cookie.sys.mjs",
DebounceCallback: "chrome://remote/content/marionette/sync.sys.mjs",
@ -2272,8 +2270,6 @@ GeckoDriver.prototype.deleteSession = function() {
lazy.logger.debug(`Failed to remove observer "${TOPIC_BROWSER_READY}"`);
}
lazy.clearElementIdCache();
// Always unregister actors after all other observers
// and listeners have been removed.
lazy.unregisterCommandsActor();

View File

@ -5,8 +5,6 @@
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ContentDOMReference: "resource://gre/modules/ContentDOMReference.sys.mjs",
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",
@ -46,11 +44,6 @@ const XUL_SELECTED_ELS = new Set([
* web element reference for every element representing the same element
* is the same.
*
* The {@link element.ReferenceStore} provides a mapping between web element
* references and the ContentDOMReference of DOM elements for each browsing
* context. It also provides functionality for looking up and retrieving
* elements.
*
* @namespace
*/
export const element = {};
@ -66,141 +59,6 @@ element.Strategy = {
XPath: "xpath",
};
/**
* Stores known/seen web element references and their associated
* ContentDOMReference ElementIdentifiers.
*
* The ContentDOMReference ElementIdentifier is augmented with a WebReference
* reference, so in Marionette's IPC it looks like the following example:
*
* { browsingContextId: 9,
* id: 0.123,
* webElRef: {element-6066-11e4-a52e-4f735466cecf: <uuid>} }
*
* For use in parent process in conjunction with ContentDOMReference in content.
*
* @class
* @memberof element
*/
element.ReferenceStore = class {
constructor() {
// uuid -> { id, browsingContextId, webElRef }
this.refs = new Map();
// id -> webElRef
this.domRefs = new Map();
}
clear(browsingContext) {
if (!browsingContext) {
this.refs.clear();
this.domRefs.clear();
return;
}
for (const context of browsingContext.getAllBrowsingContextsInSubtree()) {
for (const [uuid, elId] of this.refs) {
if (elId.browsingContextId == context.id) {
this.refs.delete(uuid);
this.domRefs.delete(elId.id);
}
}
}
}
/**
* Make a collection of elements seen.
*
* The order of the returned web element references is guaranteed to
* match that of the collection passed in.
*
* @param {Array.<ElementIdentifer>} elIds
* Sequence of ids to add to set of seen elements.
*
* @return {Array.<WebReference>}
* List of the web element references associated with each element
* from <var>els</var>.
*/
addAll(elIds) {
return [...elIds].map(elId => this.add(elId));
}
/**
* Make an element seen.
*
* @param {ElementIdentifier} elId
* {id, browsingContextId} to add to set of seen elements.
*
* @return {WebReference}
* Web element reference associated with element.
*
*/
add(elId) {
if (!elId.id || !elId.browsingContextId) {
throw new TypeError(
lazy.pprint`Expected ElementIdentifier, got: ${elId}`
);
}
if (this.domRefs.has(elId.id)) {
return WebReference.fromJSON(this.domRefs.get(elId.id));
}
const webEl = WebReference.fromJSON(elId.webElRef);
this.refs.set(webEl.uuid, elId);
this.domRefs.set(elId.id, elId.webElRef);
return webEl;
}
/**
* Determine if the provided web element reference is in the store.
*
* Unlike when getting the element, a staleness check is not
* performed.
*
* @param {WebReference} webEl
* Element's associated web element reference.
*
* @return {boolean}
* True if element is in the store, false otherwise.
*
* @throws {TypeError}
* If <var>webEl</var> is not a {@link WebReference}.
*/
has(webEl) {
if (!(webEl instanceof WebReference)) {
throw new TypeError(lazy.pprint`Expected web element, got: ${webEl}`);
}
return this.refs.has(webEl.uuid);
}
/**
* Retrieve a DOM {@link Element} or a {@link XULElement} by its
* unique {@link WebReference} reference.
*
* @param {WebReference} webEl
* Web element reference to find the associated {@link Element}
* of.
* @returns {ElementIdentifier}
* ContentDOMReference identifier
*
* @throws {TypeError}
* If <var>webEl</var> is not a {@link WebReference}.
* @throws {NoSuchElementError}
* If the web element reference <var>uuid</var> has not been
* seen before.
*/
get(webEl) {
if (!(webEl instanceof WebReference)) {
throw new TypeError(lazy.pprint`Expected web element, got: ${webEl}`);
}
const elId = this.refs.get(webEl.uuid);
if (!elId) {
throw new lazy.error.NoSuchElementError(
"Web element reference not seen before: " + webEl.uuid
);
}
return elId;
}
};
/**
* Find a single element or a collection of elements starting at the
* document root or a given node.
@ -589,96 +447,73 @@ element.findClosest = function(startNode, selector) {
};
/**
* Wrapper around ContentDOMReference.get with additional steps specific to
* Marionette.
*
* @param {Element} el
* The DOM element to generate the identifier for.
*
* @return {object}
* The ContentDOMReference ElementIdentifier for the DOM element augmented
* with a Marionette WebReference reference, and some additional properties.
*
* @throws {StaleElementReferenceError}
* If the element has gone stale, indicating it is no longer
* attached to the DOM, or its node document is no longer the
* active document.
*/
element.getElementId = function(el) {
if (element.isStale(el)) {
throw new lazy.error.StaleElementReferenceError(
lazy.pprint`The element reference of ${el} ` +
"is stale; either the element is no longer attached to the DOM, " +
"it is not in the current frame context, " +
"or the document has been refreshed"
);
}
const webEl = WebReference.from(el);
const id = lazy.ContentDOMReference.get(el);
const browsingContext = BrowsingContext.get(id.browsingContextId);
id.webElRef = webEl.toJSON();
id.browserId = browsingContext.browserId;
id.isTopLevel = !browsingContext.parent;
return id;
};
/**
* Wrapper around ContentDOMReference.resolve with additional error handling
* specific to Marionette.
* Resolve element from specified web element reference.
*
* @param {ElementIdentifier} id
* The identifier generated via ContentDOMReference.get for a DOM element.
*
* The WebElement reference identifier for a DOM element.
* @param {WindowProxy} win
* Current window, which may differ from the associated
* window of <var>el</var>.
* @param {NodeCache} seenEls
* Known element store to look up Element instances from.
*
* @return {Element} The DOM element that the identifier was generated for, or
* null if the element does not still exist.
* @return {Element|null} The DOM element that the identifier was generated
* for, or null if the element does not still exist.
*
* @throws {NoSuchElementError}
* If element represented by reference <var>id</var> doesn't exist
* in the current browsing context.
* @throws {StaleElementReferenceError}
* If the element has gone stale, indicating it is no longer
* attached to the DOM, or its node document is no longer the
* active document.
* 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.resolveElement = function(id, win) {
let sameBrowsingContext;
if (id.isTopLevel) {
// Cross-group navigations cause a swap of the current top-level browsing
// context. The only unique identifier is the browser id the browsing
// context actually lives in. If it's equal also treat the browsing context
// as the same (bug 1690308).
// If the element's browsing context is a top-level browsing context,
sameBrowsingContext = id.browserId == win?.browsingContext.browserId;
} else {
// For non top-level browsing contexts check for equality directly.
sameBrowsingContext = id.browsingContextId == win?.browsingContext.id;
}
element.resolveElement = function(id, win, seenEls) {
const el = seenEls.resolve(id);
if (!sameBrowsingContext) {
throw new lazy.error.NoSuchElementError(
`Web element reference not seen before: ${JSON.stringify(id.webElRef)}`
);
}
// For WebDriver classic only elements from the same browsing context
// are allowed to be accessed.
if (el?.ownerGlobal) {
if (win === undefined) {
throw new TypeError(
"Expected a valid window to resolve the element reference of " +
lazy.pprint`${el || JSON.stringify(id.webElRef)}`
);
}
const el = lazy.ContentDOMReference.resolve(id);
const elementBrowsingContext = el.ownerGlobal.browsingContext;
let sameBrowsingContext = true;
if (elementBrowsingContext.top === elementBrowsingContext) {
// Cross-group navigations cause a swap of the current top-level browsing
// context. The only unique identifier is the browser id the browsing
// context actually lives in. If it's equal also treat the browsing context
// as the same (bug 1690308).
// If the element's browsing context is a top-level browsing context,
sameBrowsingContext =
elementBrowsingContext.browserId == win.browsingContext.browserId;
} else {
// For non top-level browsing contexts check for equality directly.
sameBrowsingContext = elementBrowsingContext.id == win.browsingContext.id;
}
if (!sameBrowsingContext) {
throw new lazy.error.NoSuchElementError(
lazy.pprint`The element reference of ${el ||
JSON.stringify(id.webElRef)} ` +
"is not known in the current browsing context"
);
}
}
if (element.isStale(el)) {
throw new lazy.error.StaleElementReferenceError(
lazy.pprint`The element reference of ${el ||
JSON.stringify(id.webElRef)} ` +
"is stale; either the element is no longer attached to the DOM, " +
"it is not in the current frame context, " +
"or the document has been refreshed"
"is stale; either its node document is not the active document, " +
"or it is no longer connected to the DOM"
);
}
return el;
};
@ -1071,6 +906,21 @@ element.isInView = function(el) {
}
};
/**
* Generates a unique identifier.
*
* The generated uuid will not contain the curly braces.
*
* @return {string}
* UUID.
*/
element.generateUUID = function() {
return Services.uuid
.generateUUID()
.toString()
.slice(1, -1);
};
/**
* This function throws the visibility of the element error if the element is
* not displayed or the given coordinates are not within the viewport.
@ -1451,6 +1301,9 @@ export class WebReference {
*
* @param {(Element|ShadowRoot|WindowProxy|XULElement)} 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.
*
* @return {WebReference)}
* Web reference for <var>node</var>.
@ -1459,8 +1312,10 @@ export class WebReference {
* If <var>node</var> is neither a <code>WindowProxy</code>,
* DOM or XUL element, or <code>ShadowRoot</code>.
*/
static from(node) {
const uuid = WebReference.generateUUID();
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
@ -1570,17 +1425,6 @@ export class WebReference {
}
return false;
}
/**
* Generates a unique identifier.
*
* @return {string}
* Generated UUID.
*/
static generateUUID() {
let uuid = Services.uuid.generateUUID().toString();
return uuid.substring(1, uuid.length - 1);
}
}
/**

View File

@ -12,6 +12,9 @@ ChromeUtils.defineESModuleGetters(lazy, {
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/element.sys.mjs",
WebElement: "chrome://remote/content/marionette/element.sys.mjs",
WebReference: "chrome://remote/content/marionette/element.sys.mjs",
});
@ -199,35 +202,30 @@ evaluate.sandbox = function(
};
/**
* Convert any web elements in arbitrary objects to a ContentDOMReference by
* looking them up in the seen element reference store. For ElementIdentifiers a
* new entry in the seen element reference store gets added when running in the
* parent process, otherwise ContentDOMReference is used to retrieve the DOM
* node.
* Convert any web elements in arbitrary objects to a DOM element by
* looking them up in the seen element reference store.
*
* @param {Object} obj
* Arbitrary object containing web elements or ElementIdentifiers.
* @param {Object=} options
* @param {element.ReferenceStore=} options.seenEls
* Known element store to look up web elements from. If `seenEls` is an
* instance of `element.ReferenceStore`, return WebReference. If `seenEls` is
* `undefined` the Element from the ContentDOMReference cache is returned
* when executed in the child process, in the parent process the WebReference
* is passed-through.
* @param {NodeCache=} options.seenEls
* Known node cache to look up WebElement instances from. If `seenEls` is
* an instance of `NodeCache`, return WebElement. In the parent
* process where `seenEls` is `undefined` the WebElement reference is
* passed through.
* @param {WindowProxy=} options.win
* Current browsing context, if `seenEls` is provided.
* Current window, if `seenEls` is provided.
*
* @return {Object}
* Same object as provided by `obj` with the web elements
* replaced by DOM elements.
* replaced by DOM elements when run in the target window.
*
* @throws {NoSuchElementError}
* If `seenEls` is an `element.ReferenceStore` and the web element reference
* If `seenEls` is a `NodeCache` and the WebElement reference
* has not been seen before.
* @throws {StaleElementReferenceError}
* If `seenEls` is an `element.ReferenceStore` and the element has gone
* stale, indicating it is no longer attached to the DOM, or its node
* document is no longer the active document.
* If `seenEls` is a `NodeCache` and the element has gone
* stale, indicating it is no longer attached to the DOM.
*/
evaluate.fromJSON = function(obj, options = {}) {
const { seenEls, win } = options;
@ -242,21 +240,29 @@ evaluate.fromJSON = function(obj, options = {}) {
case "object":
if (obj === null) {
return obj;
// arrays
} else if (Array.isArray(obj)) {
return obj.map(e => evaluate.fromJSON(e, { seenEls, win }));
} else if (lazy.WebReference.isReference(obj)) {
if (seenEls) {
// With the element reference store available the code runs from
// within the JSWindowActorChild scope. As such create a WebReference
// based on the WebElement identifier and resolve it to a DOM element
// or ShadowRoot.
const webRef = lazy.WebReference.fromJSON(obj);
// ElementIdentifier and ReferenceStore (used by JSWindowActor)
} else if (lazy.WebReference.isReference(obj.webElRef)) {
if (seenEls instanceof lazy.element.ReferenceStore) {
// Parent: Store web element reference in the cache
return seenEls.add(obj);
} else if (!seenEls) {
// Child: Resolve ElementIdentifier by using ContentDOMReference
return lazy.element.resolveElement(obj, win);
if (
webRef instanceof lazy.WebElement ||
webRef instanceof lazy.ShadowRoot
) {
return lazy.element.resolveElement(webRef.uuid, win, seenEls);
}
// WebFrame and WebWindow not supported yet
throw new lazy.error.UnsupportedOperationError();
}
throw new TypeError("seenEls is not an instance of ReferenceStore");
// Within the JSWindowActorParent scope just pass-through the WebReference.
return obj;
}
// arbitrary objects
@ -279,12 +285,12 @@ evaluate.fromJSON = function(obj, options = {}) {
* - Collections, such as `Array<`, `NodeList`, `HTMLCollection`
* et al. are expanded to arrays and then recursed.
*
* - Elements that are not known web elements are added to the
* ContentDOMReference registry. Once known, the elements'
* - Elements that are not known WebElement's are added to the
* `NodeCache`. Once known, the elements'
* associated web element representation is returned.
*
* - WebReferences are transformed to the corresponding ElementIdentifier
* for use in the content process, if an `element.ReferenceStore` is provided.
* - In the parent process where a `NodeCache` is not provided
* WebElement references are passed through.
*
* - Objects with custom JSON representations, i.e. if they have
* a callable `toJSON` function, are returned verbatim. This means
@ -296,68 +302,58 @@ evaluate.fromJSON = function(obj, options = {}) {
* @param {Object} obj
* Object to be marshaled.
* @param {Object=} options
* @param {element.ReferenceStore=} seenEls
* Element store to use for lookup of web element references.
* @param {NodeCache=} seenEls
* Known element store to look up Element instances from. If `seenEls` is
* an instance of `NodeCache`, return a WebElement reference.
* If the element isn't known yet a new reference will be created. In the
* parent process where `seenEls` is `undefined` the WebElement reference
* is passed through as arbitrary object.
*
* @return {Object}
* Same object as provided by `obj` with the elements
* replaced by web elements.
* Same object as provided by `obj` with the DOM elements
* replaced by WebElement references.
*
* @throws {JavaScriptError}
* If an object contains cyclic references.
* @throws {StaleElementReferenceError}
* If the element has gone stale, indicating it is no longer
* attached to the DOM, or its node document is no longer the
* active document.
* attached to the DOM.
*/
evaluate.toJSON = function(obj, options = {}) {
const { seenEls } = options;
const t = Object.prototype.toString.call(obj);
// null
if (t == "[object Undefined]" || t == "[object Null]") {
return null;
// primitives
} else if (
// Primitive values
t == "[object Boolean]" ||
t == "[object Number]" ||
t == "[object String]"
) {
return obj;
// Array, NodeList, HTMLCollection, et al.
} else if (lazy.element.isCollection(obj)) {
// Array, NodeList, HTMLCollection, et al.
evaluate.assertAcyclic(obj);
return [...obj].map(el => evaluate.toJSON(el, { seenEls }));
// WebReference
} else if (lazy.WebReference.isReference(obj)) {
// Parent: Convert to ElementIdentifier for use in child actor
return seenEls.get(lazy.WebReference.fromJSON(obj));
// ElementIdentifier
} else if (lazy.WebReference.isReference(obj.webElRef)) {
// Parent: Pass-through ElementIdentifiers to the child
return obj;
// Element (HTMLElement, SVGElement, XULElement, et al.)
} else if (lazy.element.isElement(obj) || lazy.element.isShadowRoot(obj)) {
// Parent
if (seenEls instanceof lazy.element.ReferenceStore) {
throw new TypeError(`ReferenceStore can't be used with Element`);
// JSWindowActorChild scope: Convert DOM elements (eg. HTMLElement,
// XULElement, et al) and ShadowRoot instances to WebReference references.
const el = Cu.unwaiveXrays(obj);
// Don't create a reference for stale elements.
if (lazy.element.isStale(el)) {
throw new lazy.error.StaleElementReferenceError(
lazy.pprint`The element ${el} is no longer attached to the DOM`
);
}
// If no storage has been specified assume we are in a child process.
// Evaluation of code will take place in mutable sandboxes, which are
// created to waive xrays by default. As such DOM nodes have to be unwaived
// before accessing the ownerGlobal is possible, which is needed by
// ContentDOMReference.
return lazy.element.getElementId(Cu.unwaiveXrays(obj));
// custom JSON representation
const sharedId = seenEls.add(el);
return lazy.WebReference.from(el, sharedId).toJSON();
} else if (typeof obj.toJSON == "function") {
// custom JSON representation
let unsafeJSON = obj.toJSON();
return evaluate.toJSON(unsafeJSON, { seenEls });
}

View File

@ -303,7 +303,7 @@ export class TCPConnection {
let rv = await fn.bind(this.driver)(cmd);
if (rv != null) {
if (rv instanceof lazy.WebReference || typeof rv != "object") {
if (lazy.WebReference.isReference(rv) || typeof rv != "object") {
resp.body = { value: rv };
} else {
resp.body = rv;

View File

@ -0,0 +1,7 @@
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const SVG_NS = "http://www.w3.org/2000/svg";
const XHTML_NS = "http://www.w3.org/1999/xhtml";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const browser = Services.appShell.createWindowlessBrowser(false);

View File

@ -12,10 +12,6 @@ const {
"chrome://remote/content/marionette/element.sys.mjs"
);
const SVG_NS = "http://www.w3.org/2000/svg";
const XHTML_NS = "http://www.w3.org/1999/xhtml";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
class Element {
constructor(tagName, attrs = {}) {
this.tagName = tagName;
@ -509,8 +505,8 @@ add_test(function test_WebReference_isReference() {
run_next_test();
});
add_test(function test_WebReference_generateUUID() {
equal(typeof WebReference.generateUUID(), "string");
add_test(function test_generateUUID() {
equal(typeof element.generateUUID(), "string");
run_next_test();
});

View File

@ -1,83 +1,36 @@
const { element, WebReference } = ChromeUtils.importESModule(
const { WebElement, WebReference } = ChromeUtils.importESModule(
"chrome://remote/content/marionette/element.sys.mjs"
);
const { evaluate } = ChromeUtils.importESModule(
"chrome://remote/content/marionette/evaluate.sys.mjs"
);
const { NodeCache } = ChromeUtils.importESModule(
"chrome://remote/content/shared/webdriver/NodeCache.sys.mjs"
);
const SVG_NS = "http://www.w3.org/2000/svg";
const XHTML_NS = "http://www.w3.org/1999/xhtml";
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 Element {
constructor(tagName, attrs = {}) {
this.tagName = tagName;
this.localName = tagName;
const nodeCache = new NodeCache();
// Set default properties
this.isConnected = true;
this.ownerDocument = { documentElement: {} };
this.ownerGlobal = { document: this.ownerDocument };
const domEl = browser.document.createElement("img");
const svgEl = browser.document.createElementNS(SVG_NS, "rect");
for (let attr in attrs) {
this[attr] = attrs[attr];
}
}
get nodeType() {
return 1;
}
get ELEMENT_NODE() {
return 1;
}
}
class DOMElement extends Element {
constructor(tagName, attrs = {}) {
super(tagName, attrs);
this.namespaceURI = XHTML_NS;
}
}
class SVGElement extends Element {
constructor(tagName, attrs = {}) {
super(tagName, attrs);
this.namespaceURI = SVG_NS;
}
}
class XULElement extends Element {
constructor(tagName, attrs = {}) {
super(tagName, attrs);
this.namespaceURI = XUL_NS;
}
}
const domEl = new DOMElement("p");
const svgEl = new SVGElement("rect");
const xulEl = new XULElement("browser");
const domWebEl = WebReference.from(domEl);
const svgWebEl = WebReference.from(svgEl);
const xulWebEl = WebReference.from(xulEl);
const domElId = { id: 1, browsingContextId: 4, webElRef: domWebEl.toJSON() };
const svgElId = { id: 2, browsingContextId: 5, webElRef: svgWebEl.toJSON() };
const xulElId = { id: 3, browsingContextId: 6, webElRef: xulWebEl.toJSON() };
const elementIdCache = new element.ReferenceStore();
browser.document.body.appendChild(domEl);
browser.document.body.appendChild(svgEl);
add_test(function test_acyclic() {
evaluate.assertAcyclic({});
Assert.throws(() => {
let obj = {};
const obj = {};
obj.reference = obj;
evaluate.assertAcyclic(obj);
}, /JavaScriptError/);
// custom message
let cyclic = {};
const cyclic = {};
cyclic.reference = cyclic;
Assert.throws(
() => evaluate.assertAcyclic(cyclic, "", RangeError),
@ -124,34 +77,32 @@ add_test(function test_toJSON_types() {
run_next_test();
});
add_test(function test_toJSON_types_ReferenceStore() {
// Temporarily add custom elements until xpcshell tests
// have access to real DOM nodes (including the Window Proxy)
elementIdCache.add(domElId);
elementIdCache.add(svgElId);
elementIdCache.add(xulElId);
deepEqual(evaluate.toJSON(domWebEl, { seenEls: elementIdCache }), domElId);
deepEqual(evaluate.toJSON(svgWebEl, { seenEls: elementIdCache }), svgElId);
deepEqual(evaluate.toJSON(xulWebEl, { seenEls: elementIdCache }), xulElId);
Assert.throws(
() => evaluate.toJSON(domEl, { seenEls: elementIdCache }),
/TypeError/,
"Reference store not usable for elements"
add_test(function test_toJSON_types_NodeCache() {
const domElSharedId = nodeCache.add(domEl);
deepEqual(
evaluate.toJSON(domEl, { seenEls: nodeCache }),
WebReference.from(domEl, domElSharedId).toJSON()
);
elementIdCache.clear();
const svgElSharedId = nodeCache.add(svgEl);
deepEqual(
evaluate.toJSON(svgEl, { seenEls: nodeCache }),
WebReference.from(svgEl, svgElSharedId).toJSON()
);
nodeCache.clear({ all: true });
run_next_test();
});
add_test(function test_toJSON_sequences() {
const domElSharedId = nodeCache.add(domEl);
const input = [
null,
true,
[],
domWebEl,
domEl,
{
toJSON() {
return "foo";
@ -160,35 +111,28 @@ add_test(function test_toJSON_sequences() {
{ bar: "baz" },
];
Assert.throws(
() => evaluate.toJSON(input, { seenEls: elementIdCache }),
/NoSuchElementError/,
"Expected no element"
);
elementIdCache.add(domElId);
const actual = evaluate.toJSON(input, { seenEls: elementIdCache });
const actual = evaluate.toJSON(input, { seenEls: nodeCache });
equal(null, actual[0]);
equal(true, actual[1]);
deepEqual([], actual[2]);
deepEqual(actual[3], domElId);
deepEqual(actual[3], { [WebElement.Identifier]: domElSharedId });
equal("foo", actual[4]);
deepEqual({ bar: "baz" }, actual[5]);
elementIdCache.clear();
nodeCache.clear({ all: true });
run_next_test();
});
add_test(function test_toJSON_objects() {
const domElSharedId = nodeCache.add(domEl);
const input = {
null: null,
boolean: true,
array: [],
webElement: domWebEl,
elementId: domElId,
element: domEl,
toJSON: {
toJSON() {
return "foo";
@ -197,52 +141,73 @@ add_test(function test_toJSON_objects() {
object: { bar: "baz" },
};
Assert.throws(
() => evaluate.toJSON(input, { seenEls: elementIdCache }),
/NoSuchElementError/,
"Expected no element"
);
elementIdCache.add(domElId);
const actual = evaluate.toJSON(input, { seenEls: elementIdCache });
const actual = evaluate.toJSON(input, { seenEls: nodeCache });
equal(null, actual.null);
equal(true, actual.boolean);
deepEqual([], actual.array);
deepEqual(actual.webElement, domElId);
deepEqual(actual.elementId, domElId);
deepEqual(actual.element, { [WebElement.Identifier]: domElSharedId });
equal("foo", actual.toJSON);
deepEqual({ bar: "baz" }, actual.object);
elementIdCache.clear();
nodeCache.clear({ all: true });
run_next_test();
});
add_test(function test_fromJSON_ReferenceStore() {
// Add unknown element to reference store
let webEl = evaluate.fromJSON(domElId, { seenEls: elementIdCache });
deepEqual(webEl, domWebEl);
deepEqual(elementIdCache.get(webEl), domElId);
add_test(function test_fromJSON_NodeCache() {
// Fails to resolve for unknown elements
const unknownWebElId = { [WebElement.Identifier]: "foo" };
Assert.throws(() => {
evaluate.fromJSON(unknownWebElId, {
seenEls: nodeCache,
win: domEl.ownerGlobal,
});
}, /NoSuchElementError/);
const domElSharedId = nodeCache.add(domEl);
const domWebEl = { [WebElement.Identifier]: domElSharedId };
// Fails to resolve for missing window reference
Assert.throws(() => {
evaluate.fromJSON(domWebEl, {
seenEls: nodeCache,
});
}, /TypeError/);
// Previously seen element is associated with original web element reference
const domElId2 = {
id: 1,
browsingContextId: 4,
webElRef: WebReference.from(domEl).toJSON(),
};
webEl = evaluate.fromJSON(domElId2, { seenEls: elementIdCache });
deepEqual(webEl, domWebEl);
deepEqual(elementIdCache.get(webEl), domElId);
const el = evaluate.fromJSON(domWebEl, {
seenEls: nodeCache,
win: domEl.ownerGlobal,
});
deepEqual(el, domEl);
deepEqual(el, nodeCache.resolve(domElSharedId));
elementIdCache.clear();
// Fails with stale element reference for removed element
let imgEl = browser.document.createElement("img");
const win = imgEl.ownerGlobal;
const imgElSharedId = nodeCache.add(imgEl);
const imgWebEl = { [WebElement.Identifier]: imgElSharedId };
run_next_test();
// Delete element and force a garbage collection
imgEl = null;
MemoryReporter.minimizeMemoryUsage(() => {
Assert.throws(() => {
evaluate.fromJSON(imgWebEl, {
seenEls: nodeCache,
win,
});
}, /StaleElementReferenceError:/);
nodeCache.clear({ all: true });
run_next_test();
});
});
add_test(function test_isCyclic_noncyclic() {
for (let type of [true, 42, "foo", [], {}, null, undefined]) {
for (const type of [true, 42, "foo", [], {}, null, undefined]) {
ok(!evaluate.isCyclic(type));
}
@ -250,7 +215,7 @@ add_test(function test_isCyclic_noncyclic() {
});
add_test(function test_isCyclic_object() {
let obj = {};
const obj = {};
obj.reference = obj;
ok(evaluate.isCyclic(obj));
@ -258,7 +223,7 @@ add_test(function test_isCyclic_object() {
});
add_test(function test_isCyclic_array() {
let arr = [];
const arr = [];
arr.push(arr);
ok(evaluate.isCyclic(arr));
@ -266,7 +231,7 @@ add_test(function test_isCyclic_array() {
});
add_test(function test_isCyclic_arrayInObject() {
let arr = [];
const arr = [];
arr.push(arr);
ok(evaluate.isCyclic({ arr }));
@ -274,7 +239,7 @@ add_test(function test_isCyclic_arrayInObject() {
});
add_test(function test_isCyclic_objectInArray() {
let obj = {};
const obj = {};
obj.reference = obj;
ok(evaluate.isCyclic([obj]));

View File

@ -1,220 +0,0 @@
const { element, WebReference } = ChromeUtils.importESModule(
"chrome://remote/content/marionette/element.sys.mjs"
);
const SVG_NS = "http://www.w3.org/2000/svg";
const XHTML_NS = "http://www.w3.org/1999/xhtml";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
class Element {
constructor(tagName, attrs = {}) {
this.tagName = tagName;
this.localName = tagName;
// Set default properties
this.isConnected = true;
this.ownerDocument = {};
this.ownerGlobal = { document: this.ownerDocument };
for (let attr in attrs) {
this[attr] = attrs[attr];
}
}
get nodeType() {
return 1;
}
get ELEMENT_NODE() {
return 1;
}
}
class DOMElement extends Element {
constructor(tagName, attrs = {}) {
super(tagName, attrs);
this.namespaceURI = XHTML_NS;
this.ownerDocument = { documentElement: { namespaceURI: XHTML_NS } };
}
}
class SVGElement extends Element {
constructor(tagName, attrs = {}) {
super(tagName, attrs);
this.namespaceURI = SVG_NS;
this.ownerDocument = { documentElement: { namespaceURI: SVG_NS } };
}
}
class XULElement extends Element {
constructor(tagName, attrs = {}) {
super(tagName, attrs);
this.namespaceURI = XUL_NS;
this.ownerDocument = { documentElement: { namespaceURI: XUL_NS } };
}
}
function makeIterator(items) {
return function*() {
for (const i of items) {
yield i;
}
};
}
const nestedBrowsingContext = {
id: 7,
getAllBrowsingContextsInSubtree: makeIterator([
{ id: 7 },
{ id: 71 },
{ id: 72 },
]),
};
const domEl = new DOMElement("p");
const svgEl = new SVGElement("rect");
const xulEl = new XULElement("browser");
const frameEl = new DOMElement("iframe");
const innerEl = new DOMElement("p", { id: "inner" });
const domWebEl = WebReference.from(domEl);
const svgWebEl = WebReference.from(svgEl);
const xulWebEl = WebReference.from(xulEl);
const frameWebEl = WebReference.from(frameEl);
const innerWebEl = WebReference.from(innerEl);
const domElId = { id: 1, browsingContextId: 4, webElRef: domWebEl.toJSON() };
const svgElId = { id: 2, browsingContextId: 15, webElRef: svgWebEl.toJSON() };
const xulElId = { id: 3, browsingContextId: 15, webElRef: xulWebEl.toJSON() };
const frameElId = {
id: 10,
browsingContextId: 7,
webElRef: frameWebEl.toJSON(),
};
const innerElId = {
id: 11,
browsingContextId: 72,
webElRef: innerWebEl.toJSON(),
};
const elementIdCache = new element.ReferenceStore();
registerCleanupFunction(() => {
elementIdCache.clear();
});
add_test(function test_add_element() {
elementIdCache.add(domElId);
equal(elementIdCache.refs.size, 1);
equal(elementIdCache.domRefs.size, 1);
deepEqual(elementIdCache.refs.get(domWebEl.uuid), domElId);
deepEqual(elementIdCache.domRefs.get(domElId.id), domWebEl.toJSON());
elementIdCache.add(domElId);
equal(elementIdCache.refs.size, 1);
equal(elementIdCache.domRefs.size, 1);
elementIdCache.add(xulElId);
equal(elementIdCache.refs.size, 2);
equal(elementIdCache.domRefs.size, 2);
elementIdCache.clear();
equal(elementIdCache.refs.size, 0);
equal(elementIdCache.domRefs.size, 0);
run_next_test();
});
add_test(function test_get_element() {
elementIdCache.add(domElId);
deepEqual(elementIdCache.get(domWebEl), domElId);
run_next_test();
});
add_test(function test_get_no_such_element() {
throws(() => elementIdCache.get(frameWebEl), /NoSuchElementError/);
elementIdCache.add(domElId);
throws(() => elementIdCache.get(frameWebEl), /NoSuchElementError/);
run_next_test();
});
add_test(function test_clear_by_unknown_browsing_context() {
const unknownContext = {
id: 1000,
getAllBrowsingContextsInSubtree: makeIterator([{ id: 1000 }]),
};
elementIdCache.add(domElId);
elementIdCache.add(svgElId);
elementIdCache.add(xulElId);
elementIdCache.add(frameElId);
elementIdCache.add(innerElId);
equal(elementIdCache.refs.size, 5);
equal(elementIdCache.domRefs.size, 5);
elementIdCache.clear(unknownContext);
equal(elementIdCache.refs.size, 5);
equal(elementIdCache.domRefs.size, 5);
run_next_test();
});
add_test(function test_clear_by_known_browsing_context() {
const context = {
id: 15,
getAllBrowsingContextsInSubtree: makeIterator([{ id: 15 }]),
};
const anotherContext = {
id: 4,
getAllBrowsingContextsInSubtree: makeIterator([{ id: 4 }]),
};
elementIdCache.add(domElId);
elementIdCache.add(svgElId);
elementIdCache.add(xulElId);
elementIdCache.add(frameElId);
elementIdCache.add(innerElId);
equal(elementIdCache.refs.size, 5);
equal(elementIdCache.domRefs.size, 5);
elementIdCache.clear(context);
equal(elementIdCache.refs.size, 3);
equal(elementIdCache.domRefs.size, 3);
ok(elementIdCache.has(domWebEl));
ok(!elementIdCache.has(svgWebEl));
ok(!elementIdCache.has(xulWebEl));
elementIdCache.clear(anotherContext);
equal(elementIdCache.refs.size, 2);
equal(elementIdCache.domRefs.size, 2);
ok(!elementIdCache.has(domWebEl));
run_next_test();
});
add_test(function test_clear_by_nested_browsing_context() {
elementIdCache.add(domElId);
elementIdCache.add(svgElId);
elementIdCache.add(xulElId);
elementIdCache.add(frameElId);
elementIdCache.add(innerElId);
equal(elementIdCache.refs.size, 5);
equal(elementIdCache.domRefs.size, 5);
elementIdCache.clear(nestedBrowsingContext);
equal(elementIdCache.refs.size, 3);
equal(elementIdCache.domRefs.size, 3);
ok(elementIdCache.has(domWebEl));
ok(!elementIdCache.has(frameWebEl));
ok(!elementIdCache.has(innerWebEl));
run_next_test();
});

View File

@ -3,6 +3,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
[DEFAULT]
head = head.js
skip-if = appname == "thunderbird"
[test_action.js]
@ -16,5 +17,4 @@ skip-if = appname == "thunderbird"
[test_modal.js]
[test_navigate.js]
[test_prefs.js]
[test_store.js]
[test_sync.js]

View File

@ -0,0 +1,134 @@
/* 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, {
ContentDOMReference: "resource://gre/modules/ContentDOMReference.sys.mjs",
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
pprint: "chrome://remote/content/shared/Format.sys.mjs",
});
/**
* The class provides a mapping between DOM nodes and unique element
* references by using `ContentDOMReference` identifiers.
*/
export class NodeCache {
#domRefs;
#sharedIds;
constructor() {
// ContentDOMReference id => shared unique id
this.#sharedIds = new Map();
// shared unique id => ContentDOMReference
this.#domRefs = new Map();
}
/**
* Get the number of elements in the cache.
*/
get size() {
return this.#sharedIds.size;
}
/**
* Add a DOM element to the cache if not known yet.
*
* @param {Element} el
* The DOM Element to be added.
*
* @return {string}
* The shared id to uniquely identify the DOM element.
*/
add(el) {
let domRef, sharedId;
try {
// Evaluation of code will take place in mutable sandboxes, which are
// created to waive xrays by default. As such DOM elements have to be
// unwaived before accessing the ownerGlobal if possible, which is
// needed by ContentDOMReference.
domRef = lazy.ContentDOMReference.get(Cu.unwaiveXrays(el));
} catch (e) {
throw new lazy.error.UnknownError(
lazy.pprint`Failed to create element reference for ${el}: ${e.message}`
);
}
if (this.#sharedIds.has(domRef.id)) {
// For already known elements retrieve the cached shared id.
sharedId = this.#sharedIds.get(domRef.id);
} else {
// For new elements generate a unique id without curly braces.
sharedId = Services.uuid
.generateUUID()
.toString()
.slice(1, -1);
this.#sharedIds.set(domRef.id, sharedId);
this.#domRefs.set(sharedId, domRef);
}
return sharedId;
}
/**
* Clears all known DOM elements.
*
* @param {Object=} options
* @param {boolean=} options.all
* Clear all references from any browsing context. Defaults to false.
* @param {BrowsingContext=} browsingContext
* Clear all references living in that browsing context.
*/
clear(options = {}) {
const { all = false, browsingContext } = options;
if (all) {
this.#sharedIds.clear();
this.#domRefs.clear();
return;
}
if (browsingContext) {
for (const [sharedId, domRef] of this.#domRefs.entries()) {
if (domRef.browsingContextId === browsingContext.id) {
this.#sharedIds.delete(domRef.id);
this.#domRefs.delete(sharedId);
}
}
return;
}
throw new Error(`Requires "browsingContext" or "all" to be set.`);
}
/**
* Wrapper around ContentDOMReference.resolve with additional error handling
* specific to WebDriver.
*
* @param {string} sharedId
* The unique identifier for the DOM element.
*
* @return {Element|null}
* The DOM element that the unique identifier was generated for or
* `null` if the element does not exist anymore.
*
* @throws {NoSuchElementError}
* If the DOM element as represented by the unique WebElement reference
* <var>sharedId</var> isn't known.
*/
resolve(sharedId) {
const domRef = this.#domRefs.get(sharedId);
if (domRef == undefined) {
throw new lazy.error.NoSuchElementError(
`Unknown element with id ${sharedId}`
);
}
return lazy.ContentDOMReference.resolve(domRef);
}
}

View File

@ -12,10 +12,14 @@ ChromeUtils.defineESModuleGetters(lazy, {
Capabilities: "chrome://remote/content/shared/webdriver/Capabilities.sys.mjs",
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
Log: "chrome://remote/content/shared/Log.sys.mjs",
registerProcessDataActor:
"chrome://remote/content/shared/webdriver/process-actors/WebDriverProcessDataParent.sys.mjs",
RootMessageHandler:
"chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs",
RootMessageHandlerRegistry:
"chrome://remote/content/shared/messagehandler/RootMessageHandlerRegistry.sys.mjs",
unregisterProcessDataActor:
"chrome://remote/content/shared/webdriver/process-actors/WebDriverProcessDataParent.sys.mjs",
WebDriverBiDiConnection:
"chrome://remote/content/webdriver-bidi/WebDriverBiDiConnection.sys.mjs",
WebSocketHandshake:
@ -195,6 +199,8 @@ export class WebDriverSession {
connection.registerSession(this);
this._connections.add(connection);
}
lazy.registerProcessDataActor();
}
destroy() {
@ -216,6 +222,8 @@ export class WebDriverSession {
);
this._messageHandler.destroy();
}
lazy.unregisterProcessDataActor();
}
async execute(module, command, params) {

View File

@ -0,0 +1,95 @@
/* 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
Log: "chrome://remote/content/shared/Log.sys.mjs",
NodeCache: "chrome://remote/content/shared/webdriver/NodeCache.sys.mjs",
});
XPCOMUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
// Observer to clean-up element references for closed browsing contexts.
class BrowsingContextObserver {
constructor(actor) {
this.actor = actor;
}
async observe(subject, topic, data) {
if (topic === "browsing-context-discarded") {
this.actor.cleanUp({ browsingContext: subject });
}
}
}
export class WebDriverProcessDataChild extends JSProcessActorChild {
#browsingContextObserver;
#nodeCache;
constructor() {
super();
// For now have a single reference store only. Once multiple WebDriver
// sessions are supported, it needs to be hashed by the session id.
this.#nodeCache = new lazy.NodeCache();
// Register observer to cleanup element references when a browsing context
// gets destroyed.
this.#browsingContextObserver = new BrowsingContextObserver(this);
Services.obs.addObserver(
this.#browsingContextObserver,
"browsing-context-discarded"
);
}
actorCreated() {
lazy.logger.trace(
`WebDriverProcessData actor created for PID ${Services.appinfo.processID}`
);
}
didDestroy() {
Services.obs.removeObserver(
this.#browsingContextObserver,
"browsing-context-discarded"
);
}
/**
* Clean up all the process specific data.
*
* @param {Object=} options
* @param {BrowsingContext=} browsingContext
* If specified only clear data living in that browsing context.
*/
cleanUp(options = {}) {
const { browsingContext = null } = options;
this.#nodeCache.clear({ browsingContext });
}
/**
* Get the node cache.
*
* @returns {NodeCache}
* The cache containing DOM node references.
*/
getNodeCache() {
return this.#nodeCache;
}
async receiveMessage(msg) {
switch (msg.name) {
case "WebDriverProcessDataParent:CleanUp":
return this.cleanUp(msg.data);
default:
return Promise.reject(
new Error(`Unexpected message received: ${msg.name}`)
);
}
}
}

View File

@ -0,0 +1,39 @@
/* 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
Log: "chrome://remote/content/shared/Log.sys.mjs",
});
XPCOMUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
/**
* Register the WebDriverProcessData actor that holds session data.
*/
export function registerProcessDataActor() {
try {
ChromeUtils.registerProcessActor("WebDriverProcessData", {
kind: "JSProcessActor",
child: {
esModuleURI:
"chrome://remote/content/shared/webdriver/process-actors/WebDriverProcessDataChild.sys.mjs",
},
includeParent: true,
});
} catch (e) {
if (e.name === "NotSupportedError") {
lazy.logger.warn(`WebDriverProcessData actor is already registered!`);
} else {
throw e;
}
}
}
export function unregisterProcessDataActor() {
ChromeUtils.unregisterProcessActor("WebDriverProcessData");
}

View File

@ -0,0 +1,14 @@
async function doGC() {
// Run GC and CC a few times to make sure that as much as possible is freed.
const numCycles = 3;
for (let i = 0; i < numCycles; i++) {
Cu.forceGC();
Cu.forceCC();
await new Promise(resolve => Cu.schedulePreciseShrinkingGC(resolve));
}
const MemoryReporter = Cc[
"@mozilla.org/memory-reporter-manager;1"
].getService(Ci.nsIMemoryReporterManager);
await new Promise(resolve => MemoryReporter.minimizeMemoryUsage(resolve));
}

View File

@ -0,0 +1,123 @@
const { NodeCache } = ChromeUtils.importESModule(
"chrome://remote/content/shared/webdriver/NodeCache.sys.mjs"
);
const nodeCache = new NodeCache();
const SVG_NS = "http://www.w3.org/2000/svg";
const browser = Services.appShell.createWindowlessBrowser(false);
const domEl = browser.document.createElement("div");
browser.document.body.appendChild(domEl);
const svgEl = browser.document.createElementNS(SVG_NS, "rect");
browser.document.body.appendChild(svgEl);
registerCleanupFunction(() => {
nodeCache.clear({ all: true });
});
add_test(function addElement() {
const domElRef = nodeCache.add(domEl);
equal(nodeCache.size, 1);
const domElRefOther = nodeCache.add(domEl);
equal(nodeCache.size, 1);
equal(domElRefOther, domElRef);
nodeCache.add(svgEl);
equal(nodeCache.size, 2);
run_next_test();
});
add_test(function addInvalidElement() {
Assert.throws(() => nodeCache.add("foo"), /UnknownError/);
run_next_test();
});
add_test(function clear() {
nodeCache.add(domEl);
nodeCache.add(svgEl);
equal(nodeCache.size, 2);
// Clear requires explicit arguments.
Assert.throws(() => nodeCache.clear(), /Error/);
// Clear references for a different browsing context
const browser2 = Services.appShell.createWindowlessBrowser(false);
let imgEl = browser2.document.createElement("img");
browser2.document.body.appendChild(imgEl);
nodeCache.add(imgEl);
nodeCache.clear({ browsingContext: browser.browsingContext });
equal(nodeCache.size, 1);
// Clear all references
nodeCache.add(domEl);
equal(nodeCache.size, 2);
nodeCache.clear({ all: true });
equal(nodeCache.size, 0);
run_next_test();
});
add_test(function resolveElement() {
const domElSharedId = nodeCache.add(domEl);
deepEqual(nodeCache.resolve(domElSharedId), domEl);
const svgElSharedId = nodeCache.add(svgEl);
deepEqual(nodeCache.resolve(svgElSharedId), svgEl);
deepEqual(nodeCache.resolve(domElSharedId), domEl);
run_next_test();
});
add_test(function resolveUnknownElement() {
Assert.throws(() => nodeCache.resolve("foo"), /NoSuchElementError/);
run_next_test();
});
add_test(function resolveElementNotAttachedToDOM() {
const imgEl = browser.document.createElement("img");
const imgElSharedId = nodeCache.add(imgEl);
deepEqual(nodeCache.resolve(imgElSharedId), imgEl);
run_next_test();
});
add_test(async function resolveElementRemoved() {
let imgEl = browser.document.createElement("img");
const imgElSharedId = nodeCache.add(imgEl);
// Delete element and force a garbage collection
imgEl = null;
await doGC();
const el = nodeCache.resolve(imgElSharedId);
deepEqual(el, null);
run_next_test();
});
add_test(function elementReferencesDifferentPerNodeCache() {
const sharedId = nodeCache.add(domEl);
const nodeCache2 = new NodeCache();
const sharedId2 = nodeCache2.add(domEl);
notEqual(sharedId, sharedId2);
equal(nodeCache.resolve(sharedId), nodeCache2.resolve(sharedId2));
Assert.throws(() => nodeCache.resolve(sharedId2), /NoSuchElementError/);
nodeCache2.clear({ all: true });
run_next_test();
});

View File

@ -2,7 +2,11 @@
# 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/.
[DEFAULT]
head = head.js
[test_Assert.js]
[test_Capabilities.js]
[test_Errors.js]
[test_NodeCache.js]
[test_Session.js]

View File

@ -289,7 +289,7 @@ class TestNavigate(BaseNavigationTestCase):
self.marionette.navigate("about:robots")
self.assertFalse(self.is_remote_tab)
def test_stale_element_after_remoteness_change(self):
def test_no_such_element_after_remoteness_change(self):
self.marionette.navigate(self.test_page_file_url)
self.assertTrue(self.is_remote_tab)
elem = self.marionette.find_element(By.ID, "file-url")
@ -297,21 +297,7 @@ class TestNavigate(BaseNavigationTestCase):
self.marionette.navigate("about:robots")
self.assertFalse(self.is_remote_tab)
# Force a GC to get rid of the replaced browsing context.
with self.marionette.using_context("chrome"):
self.marionette.execute_async_script(
"""
const resolve = arguments[0];
var memSrv = Cc["@mozilla.org/memory-reporter-manager;1"]
.getService(Ci.nsIMemoryReporterManager);
Services.obs.notifyObservers(null, "child-mmu-request", null);
memSrv.minimizeMemoryUsage(resolve);
"""
)
with self.assertRaises(errors.StaleElementException):
with self.assertRaises(errors.NoSuchElementException):
elem.click()
def test_about_blank_for_new_docshell(self):

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

@ -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)