Bug 1807227 - [marionette] Refactor DOM Node (de-)serialization code. r=webdriver-reviewers,jdescottes

Based on WebDriver classic specification changes:
https://github.com/w3c/webdriver/pull/1705

Differential Revision: https://phabricator.services.mozilla.com/D166773
This commit is contained in:
Henrik Skupin 2023-01-16 18:04:41 +00:00
parent fcebb7d02d
commit 1dd9c81aee
8 changed files with 482 additions and 240 deletions

View File

@ -449,16 +449,15 @@ element.findClosest = function(startNode, selector) {
/**
* Resolve element from specified web element reference.
*
* @param {ElementIdentifier} id
* @param {BrowsingContext} browsingContext
* The browsing context to retrieve the element from.
* @param {ElementIdentifier} nodeId
* The WebElement reference identifier for a DOM element.
* @param {NodeCache} nodeCache
* Node cache that holds already seen WebElement and ShadowRoot references.
* @param {WindowProxy} win
* Current window, which may differ from the associated
* window of <var>el</var>.
*
* @return {Element|null} The DOM element that the identifier was generated
* for, or null if the element does not still exist.
* @returns {Element}
* The DOM element that the identifier was generated for.
*
* @throws {NoSuchElementError}
* If element represented by reference <var>id</var> doesn't exist
@ -467,48 +466,22 @@ element.findClosest = function(startNode, selector) {
* 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, nodeCache, win) {
const el = nodeCache.resolve(id);
// 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 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"
);
}
element.getKnownElement = function(browsingContext, nodeId, nodeCache) {
if (!element.isNodeReferenceKnown(browsingContext, nodeId, nodeCache)) {
throw new lazy.error.NoSuchElementError(
lazy.pprint`The element reference of ${JSON.stringify(
nodeId.webElRef
)} is not known in the current browsing context`
);
}
if (element.isStale(el)) {
// If null, which may be the case if the element has been unwrapped from a
// weak reference, it is always considered stale.
const el = nodeCache.getNode(browsingContext, nodeId);
if (el === null || element.isStale(el)) {
throw new lazy.error.StaleElementReferenceError(
lazy.pprint`The element reference of ${el ||
JSON.stringify(id.webElRef)} ` +
JSON.stringify(nodeId.webElRef)} ` +
"is stale; either its node document is not the active document, " +
"or it is no longer connected to the DOM"
);
@ -543,22 +516,53 @@ element.isCollection = function(seq) {
}
};
/**
* 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.
*
* An element is stale if its node document is not the active document
* or if it is not connected.
*
* @param {Element=} el
* Element to check for staleness. If null, which may be
* the case if the element has been unwrapped from a weak
* reference, it is always considered stale.
* @param {Element} el
* Element to check for staleness.
*
* @return {boolean}
* True if <var>el</var> is stale, false otherwise.
*/
element.isStale = function(el) {
if (el == null || !el.ownerGlobal) {
if (!el.ownerGlobal) {
// Without a valid inner window the document is basically closed.
return true;
}

View File

@ -126,9 +126,9 @@ json.clone = function(value, nodeCache) {
// ShadowRoot instances to WebReference references.
// 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.
// 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.
const el = Cu.unwaiveXrays(value);
// Don't create a reference for stale elements.
@ -138,8 +138,8 @@ json.clone = function(value, nodeCache) {
);
}
const sharedId = nodeCache.add(value);
return lazy.WebReference.from(el, sharedId).toJSON();
const nodeRef = nodeCache.getOrCreateNodeReference(el);
return lazy.WebReference.from(el, nodeRef).toJSON();
} else if (typeof value.toJSON == "function") {
// custom JSON representation
let unsafeJSON;
@ -203,7 +203,11 @@ json.deserialize = function(value, nodeCache, win) {
webRef instanceof lazy.WebElement ||
webRef instanceof lazy.ShadowRoot
) {
return lazy.element.resolveElement(webRef.uuid, nodeCache, win);
return lazy.element.getKnownElement(
win.browsingContext,
webRef.uuid,
nodeCache
);
}
// WebFrame and WebWindow not supported yet

View File

@ -1,7 +1,3 @@
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

@ -11,12 +11,28 @@ const {
} = ChromeUtils.importESModule(
"chrome://remote/content/marionette/element.sys.mjs"
);
const { NodeCache } = ChromeUtils.importESModule(
"chrome://remote/content/shared/webdriver/NodeCache.sys.mjs"
);
const MemoryReporter = Cc["@mozilla.org/memory-reporter-manager;1"].getService(
Ci.nsIMemoryReporterManager
);
class Element {
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];
}
@ -25,6 +41,7 @@ class Element {
get nodeType() {
return 1;
}
get ELEMENT_NODE() {
return 1;
}
@ -41,12 +58,16 @@ class DOMElement extends Element {
constructor(tagName, attrs = {}) {
super(tagName, attrs);
this.isConnected = true;
if (typeof this.namespaceURI == "undefined") {
this.namespaceURI = XHTML_NS;
}
if (typeof this.ownerDocument == "undefined") {
this.ownerDocument = { designMode: "off" };
}
if (typeof this.ownerDocument.documentElement == "undefined") {
this.ownerDocument.documentElement = { namespaceURI: XHTML_NS };
}
@ -405,6 +426,97 @@ add_test(function test_coordinates() {
run_next_test();
});
add_test(function test_isNodeReferenceKnown() {
const browser = Services.appShell.createWindowlessBrowser(false);
const nodeCache = new NodeCache();
// Unknown node reference
ok(!element.isNodeReferenceKnown(browser.browsingContext, "foo", nodeCache));
// Known node reference
const el = browser.document.createElement("video");
const elRef = nodeCache.getOrCreateNodeReference(el);
ok(element.isNodeReferenceKnown(browser.browsingContext, elRef, nodeCache));
// Different top-level browsing context
const browser2 = Services.appShell.createWindowlessBrowser(false);
ok(!element.isNodeReferenceKnown(browser2.browsingContext, elRef, nodeCache));
// Different child browsing context
const iframeEl = browser.document.createElement("iframe");
browser.document.body.appendChild(iframeEl);
const childEl = iframeEl.contentDocument.createElement("div");
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)
);
run_next_test();
});
add_test(function test_getKnownElement() {
const browser = Services.appShell.createWindowlessBrowser(false);
const nodeCache = new NodeCache();
// Unknown element reference
Assert.throws(() => {
element.getKnownElement(browser.browsingContext, "foo", nodeCache);
}, /NoSuchElementError/);
// Deleted element (eg. garbage collected)
let divEl = browser.document.createElement("div");
const divElRef = nodeCache.getOrCreateNodeReference(divEl);
divEl = null;
MemoryReporter.minimizeMemoryUsage(() => {
Assert.throws(() => {
element.getKnownElement(browser.browsingContext, divElRef, nodeCache);
}, /StaleElementReferenceError/);
run_next_test();
});
// Known element reference
let imgEl = browser.document.createElement("img");
browser.document.body.appendChild(imgEl);
const imgElRef = nodeCache.getOrCreateNodeReference(imgEl);
equal(
element.getKnownElement(browser.browsingContext, imgElRef, nodeCache),
imgEl
);
});
add_test(function test_isStale() {
// Not connected to the DOM
ok(element.isStale(new Element("div")));
// Connected to the DOM
const domDivEl = new DOMElement("div");
ok(!element.isStale(domDivEl));
// Not part of the active document
domDivEl.ownerGlobal = {
document: {
isActive() {
return false;
},
},
};
ok(element.isStale(domDivEl));
// Without ownerGlobal
delete domDivEl.ownerGlobal;
ok(element.isStale(domDivEl));
run_next_test();
});
add_test(function test_WebReference_ctor() {
let el = new WebReference("foo");
equal(el.uuid, "foo");

View File

@ -1,28 +1,38 @@
const { WebElement, WebReference } = ChromeUtils.importESModule(
"chrome://remote/content/marionette/element.sys.mjs"
);
const { json } = ChromeUtils.importESModule(
"chrome://remote/content/marionette/json.sys.mjs"
);
const { NodeCache } = ChromeUtils.importESModule(
"chrome://remote/content/shared/webdriver/NodeCache.sys.mjs"
);
const MemoryReporter = Cc["@mozilla.org/memory-reporter-manager;1"].getService(
Ci.nsIMemoryReporterManager
const { WebElement, WebReference } = ChromeUtils.importESModule(
"chrome://remote/content/marionette/element.sys.mjs"
);
const nodeCache = new NodeCache();
function setupTest() {
const browser = Services.appShell.createWindowlessBrowser(false);
const nodeCache = new NodeCache();
const domEl = browser.document.createElement("div");
const svgEl = browser.document.createElementNS(SVG_NS, "rect");
const htmlEl = browser.document.createElement("video");
browser.document.body.appendChild(htmlEl);
browser.document.body.appendChild(domEl);
browser.document.body.appendChild(svgEl);
const svgEl = browser.document.createElementNS(
"http://www.w3.org/2000/svg",
"rect"
);
browser.document.body.appendChild(svgEl);
const win = domEl.ownerGlobal;
const shadowRoot = htmlEl.openOrClosedShadowRoot;
const iframeEl = browser.document.createElement("iframe");
browser.document.body.appendChild(iframeEl);
const childEl = iframeEl.contentDocument.createElement("div");
return { browser, nodeCache, childEl, iframeEl, htmlEl, shadowRoot, svgEl };
}
add_test(function test_clone_generalTypes() {
const { nodeCache } = setupTest();
// null
equal(json.clone(undefined, nodeCache), null);
equal(json.clone(null, nodeCache), null);
@ -42,35 +52,38 @@ add_test(function test_clone_generalTypes() {
"foo"
);
nodeCache.clear({ all: true });
run_next_test();
});
add_test(function test_clone_WebElements() {
const domElSharedId = nodeCache.add(domEl);
const { htmlEl, nodeCache, svgEl } = setupTest();
const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl);
deepEqual(
json.clone(domEl, nodeCache),
WebReference.from(domEl, domElSharedId).toJSON()
json.clone(htmlEl, nodeCache),
WebReference.from(htmlEl, htmlElRef).toJSON()
);
const svgElSharedId = nodeCache.add(svgEl);
// Check an element with a different namespace
const svgElRef = nodeCache.getOrCreateNodeReference(svgEl);
deepEqual(
json.clone(svgEl, nodeCache),
WebReference.from(svgEl, svgElSharedId).toJSON()
WebReference.from(svgEl, svgElRef).toJSON()
);
nodeCache.clear({ all: true });
run_next_test();
});
add_test(function test_clone_Sequences() {
const domElSharedId = nodeCache.add(domEl);
const { htmlEl, nodeCache } = setupTest();
const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl);
const input = [
null,
true,
[],
domEl,
htmlEl,
{
toJSON() {
return "foo";
@ -84,22 +97,23 @@ add_test(function test_clone_Sequences() {
equal(actual[0], null);
equal(actual[1], true);
deepEqual(actual[2], []);
deepEqual(actual[3], { [WebElement.Identifier]: domElSharedId });
deepEqual(actual[3], { [WebElement.Identifier]: htmlElRef });
equal(actual[4], "foo");
deepEqual(actual[5], { bar: "baz" });
nodeCache.clear({ all: true });
run_next_test();
});
add_test(function test_clone_objects() {
const domElSharedId = nodeCache.add(domEl);
const { htmlEl, nodeCache } = setupTest();
const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl);
const input = {
null: null,
boolean: true,
array: [42],
element: domEl,
element: htmlEl,
toJSON: {
toJSON() {
return "foo";
@ -113,15 +127,16 @@ add_test(function test_clone_objects() {
equal(actual.null, null);
equal(actual.boolean, true);
deepEqual(actual.array, [42]);
deepEqual(actual.element, { [WebElement.Identifier]: domElSharedId });
deepEqual(actual.element, { [WebElement.Identifier]: htmlElRef });
equal(actual.toJSON, "foo");
deepEqual(actual.object, { bar: "baz" });
nodeCache.clear({ all: true });
run_next_test();
});
add_test(function test_clone_сyclicReference() {
const { nodeCache } = setupTest();
// object
Assert.throws(() => {
const obj = {};
@ -154,6 +169,9 @@ add_test(function test_clone_сyclicReference() {
});
add_test(function test_deserialize_generalTypes() {
const { browser, nodeCache } = setupTest();
const win = browser.document.ownerGlobal;
// null
equal(json.deserialize(undefined, nodeCache, win), undefined);
equal(json.deserialize(null, nodeCache, win), null);
@ -163,55 +181,43 @@ add_test(function test_deserialize_generalTypes() {
equal(json.deserialize(42, nodeCache, win), 42);
equal(json.deserialize("foo", nodeCache, win), "foo");
nodeCache.clear({ all: true });
run_next_test();
});
add_test(function test_deserialize_WebElements() {
const { browser, htmlEl, nodeCache } = setupTest();
const win = browser.document.ownerGlobal;
// Fails to resolve for unknown elements
const unknownWebElId = { [WebElement.Identifier]: "foo" };
Assert.throws(() => {
json.deserialize(unknownWebElId, nodeCache, win);
}, /NoSuchElementError/);
const domElSharedId = nodeCache.add(domEl);
const domWebEl = { [WebElement.Identifier]: domElSharedId };
const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl);
const htmlWebEl = { [WebElement.Identifier]: htmlElRef };
// Fails to resolve for missing window reference
Assert.throws(() => json.deserialize(domWebEl, nodeCache), /TypeError/);
Assert.throws(() => json.deserialize(htmlWebEl, nodeCache), /TypeError/);
// Previously seen element is associated with original web element reference
const el = json.deserialize(domWebEl, nodeCache, win);
deepEqual(el, domEl);
deepEqual(el, nodeCache.resolve(domElSharedId));
const el = json.deserialize(htmlWebEl, nodeCache, win);
deepEqual(el, htmlEl);
deepEqual(el, nodeCache.getNode(browser.browsingContext, htmlElRef));
// Fails with stale element reference for removed element
let imgEl = browser.document.createElement("img");
const imgElSharedId = nodeCache.add(imgEl);
const imgWebEl = { [WebElement.Identifier]: imgElSharedId };
// Delete element and force a garbage collection
imgEl = null;
MemoryReporter.minimizeMemoryUsage(() => {
Assert.throws(
() => json.deserialize(imgWebEl, nodeCache, win),
/StaleElementReferenceError:/
);
nodeCache.clear({ all: true });
run_next_test();
});
run_next_test();
});
add_test(function test_deserialize_Sequences() {
const domElSharedId = nodeCache.add(domEl);
const { browser, htmlEl, nodeCache } = setupTest();
const win = browser.document.ownerGlobal;
const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl);
const input = [
null,
true,
[42],
{ [WebElement.Identifier]: domElSharedId },
{ [WebElement.Identifier]: htmlElRef },
{ bar: "baz" },
];
@ -220,21 +226,23 @@ add_test(function test_deserialize_Sequences() {
equal(actual[0], null);
equal(actual[1], true);
deepEqual(actual[2], [42]);
deepEqual(actual[3], domEl);
deepEqual(actual[3], htmlEl);
deepEqual(actual[4], { bar: "baz" });
nodeCache.clear({ all: true });
run_next_test();
});
add_test(function test_deserialize_objects() {
const domElSharedId = nodeCache.add(domEl);
const { browser, htmlEl, nodeCache } = setupTest();
const win = browser.document.ownerGlobal;
const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl);
const input = {
null: null,
boolean: true,
array: [42],
element: { [WebElement.Identifier]: domElSharedId },
element: { [WebElement.Identifier]: htmlElRef },
object: { bar: "baz" },
};
@ -243,7 +251,7 @@ add_test(function test_deserialize_objects() {
equal(actual.null, null);
equal(actual.boolean, true);
deepEqual(actual.array, [42]);
deepEqual(actual.element, domEl);
deepEqual(actual.element, htmlEl);
deepEqual(actual.object, { bar: "baz" });
nodeCache.clear({ all: true });

View File

@ -2,81 +2,88 @@
* 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",
});
const DOCUMENT_FRAGMENT_NODE = 11;
const ELEMENT_NODE = 1;
/**
* The class provides a mapping between DOM nodes and unique element
* references by using `ContentDOMReference` identifiers.
* @typedef {Object} NodeReferenceDetails
* @property {number} browserId
* @property {number} browsingContextGroupId
* @property {number} browsingContextId
* @property {boolean} isTopBrowsingContext
* @property {WeakRef} nodeWeakRef
*/
/**
* The class provides a mapping between DOM nodes and a unique node references.
*/
export class NodeCache {
#domRefs;
#sharedIds;
#nodeIdMap;
#seenNodesMap;
constructor() {
// ContentDOMReference id => shared unique id
this.#sharedIds = new Map();
// node => node id
this.#nodeIdMap = new WeakMap();
// shared unique id => ContentDOMReference
this.#domRefs = new Map();
// Reverse map for faster lookup requests of node references. Values do
// not only contain the resolved DOM node but also further details like
// browsing context information.
//
// node id => node details
this.#seenNodesMap = new Map();
}
/**
* Get the number of elements in the cache.
* Get the number of nodes in the cache.
*/
get size() {
return this.#sharedIds.size;
return this.#seenNodesMap.size;
}
/**
* Add a DOM element to the cache if not known yet.
* Get or if not yet existent create a unique reference for a DOM node.
*
* @param {Element} el
* The DOM Element to be added.
* @param {Node} node
* The DOM node to be added.
*
* @return {string}
* The shared id to uniquely identify the DOM element.
* The unique node reference for the DOM node.
*/
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}`
);
getOrCreateNodeReference(node) {
if (![DOCUMENT_FRAGMENT_NODE, ELEMENT_NODE].includes(node?.nodeType)) {
throw new TypeError(`Failed to create node reference for ${node}`);
}
if (this.#sharedIds.has(domRef.id)) {
// For already known elements retrieve the cached shared id.
sharedId = this.#sharedIds.get(domRef.id);
let nodeId;
if (this.#nodeIdMap.has(node)) {
// For already known nodes return the cached node id.
nodeId = this.#nodeIdMap.get(node);
} else {
// For new elements generate a unique id without curly braces.
sharedId = Services.uuid
const browsingContext = node.ownerGlobal.browsingContext;
// For not yet cached nodes generate a unique id without curly braces.
nodeId = Services.uuid
.generateUUID()
.toString()
.slice(1, -1);
this.#sharedIds.set(domRef.id, sharedId);
this.#domRefs.set(sharedId, domRef);
const details = {
browserId: browsingContext.browserId,
browsingContextGroupId: browsingContext.group.id,
browsingContextId: browsingContext.id,
isTopBrowsingContext: browsingContext.parent === null,
nodeWeakRef: Cu.getWeakReference(node),
};
this.#nodeIdMap.set(node, nodeId);
this.#seenNodesMap.set(nodeId, details);
}
return sharedId;
return nodeId;
}
/**
* Clears all known DOM elements.
* Clear known DOM nodes.
*
* @param {Object=} options
* @param {boolean=} options.all
@ -88,18 +95,22 @@ export class NodeCache {
const { all = false, browsingContext } = options;
if (all) {
this.#sharedIds.clear();
this.#domRefs.clear();
this.#nodeIdMap = new WeakMap();
this.#seenNodesMap.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);
for (const [nodeId, identifier] of this.#seenNodesMap.entries()) {
const { browsingContextId, nodeWeakRef } = identifier;
const node = nodeWeakRef.get();
if (browsingContextId === browsingContext.id) {
this.#nodeIdMap.delete(node);
this.#seenNodesMap.delete(nodeId);
}
}
return;
}
@ -107,28 +118,47 @@ export class NodeCache {
}
/**
* Wrapper around ContentDOMReference.resolve with additional error handling
* specific to WebDriver.
* Get a DOM node by its unique reference.
*
* @param {string} sharedId
* The unique identifier for the DOM element.
* @param {BrowsingContext} browsingContext
* The browsing context the node should be part of.
* @param {string} nodeId
* The unique node reference of the DOM node.
*
* @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.
* @return {Node|null}
* The DOM node that the unique identifier was generated for or
* `null` if the node does not exist anymore.
*/
resolve(sharedId) {
const domRef = this.#domRefs.get(sharedId);
if (domRef == undefined) {
throw new lazy.error.NoSuchElementError(
`Unknown element with id ${sharedId}`
);
getNode(browsingContext, nodeId) {
const nodeDetails = this.getReferenceDetails(nodeId);
// Check that the node reference is known, and is accociated with a
// browsing context that shares the same browsing context group.
if (
nodeDetails === null ||
nodeDetails.browsingContextGroupId !== browsingContext.group.id
) {
return null;
}
return lazy.ContentDOMReference.resolve(domRef);
if (nodeDetails.nodeWeakRef) {
return nodeDetails.nodeWeakRef.get();
}
return null;
}
/**
* Get detailed information for the node reference.
*
* @param {string} nodeId
*
* @returns {NodeReferenceDetails}
* Node details like: browsingContextId
*/
getReferenceDetails(nodeId) {
const details = this.#seenNodesMap.get(nodeId);
return details !== undefined ? details : null;
}
}

View File

@ -10,5 +10,6 @@ async function doGC() {
const MemoryReporter = Cc[
"@mozilla.org/memory-reporter-manager;1"
].getService(Ci.nsIMemoryReporterManager);
await new Promise(resolve => MemoryReporter.minimizeMemoryUsage(resolve));
}

View File

@ -2,45 +2,99 @@ const { NodeCache } = ChromeUtils.importESModule(
"chrome://remote/content/shared/webdriver/NodeCache.sys.mjs"
);
const nodeCache = new NodeCache();
function setupTest() {
const browser = Services.appShell.createWindowlessBrowser(false);
const nodeCache = new NodeCache();
const SVG_NS = "http://www.w3.org/2000/svg";
const htmlEl = browser.document.createElement("video");
htmlEl.setAttribute("id", "foo");
browser.document.body.appendChild(htmlEl);
const browser = Services.appShell.createWindowlessBrowser(false);
const svgEl = browser.document.createElementNS(
"http://www.w3.org/2000/svg",
"rect"
);
browser.document.body.appendChild(svgEl);
const domEl = browser.document.createElement("div");
browser.document.body.appendChild(domEl);
const shadowRoot = htmlEl.openOrClosedShadowRoot;
const svgEl = browser.document.createElementNS(SVG_NS, "rect");
browser.document.body.appendChild(svgEl);
const iframeEl = browser.document.createElement("iframe");
browser.document.body.appendChild(iframeEl);
const childEl = iframeEl.contentDocument.createElement("div");
registerCleanupFunction(() => {
nodeCache.clear({ all: true });
});
return { browser, nodeCache, childEl, iframeEl, htmlEl, shadowRoot, svgEl };
}
add_test(function addElement() {
const domElRef = nodeCache.add(domEl);
equal(nodeCache.size, 1);
add_test(function getOrCreateNodeReference_invalid() {
const { htmlEl, nodeCache } = setupTest();
const domElRefOther = nodeCache.add(domEl);
equal(nodeCache.size, 1);
equal(domElRefOther, domElRef);
const invalidValues = [
null,
undefined,
"foo",
42,
true,
[],
{},
htmlEl.attributes[0],
];
nodeCache.add(svgEl);
equal(nodeCache.size, 2);
for (const value of invalidValues) {
info(`Testing value: ${value}`);
Assert.throws(() => nodeCache.getOrCreateNodeReference(value), /TypeError/);
}
run_next_test();
});
add_test(function addInvalidElement() {
Assert.throws(() => nodeCache.add("foo"), /UnknownError/);
add_test(function getOrCreateNodeReference_supportedNodeTypes() {
const { htmlEl, nodeCache, shadowRoot } = setupTest();
const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl);
equal(nodeCache.size, 1);
const shadowRootRef = nodeCache.getOrCreateNodeReference(shadowRoot);
equal(nodeCache.size, 2);
notEqual(htmlElRef, shadowRootRef);
run_next_test();
});
add_test(function getOrCreateNodeReference_referenceAlreadyCreated() {
const { htmlEl, nodeCache } = setupTest();
const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl);
const htmlElRefOther = nodeCache.getOrCreateNodeReference(htmlEl);
equal(nodeCache.size, 1);
equal(htmlElRefOther, htmlElRef);
run_next_test();
});
add_test(function getOrCreateNodeReference_differentReferencePerNodeCache() {
const { browser, htmlEl, nodeCache } = setupTest();
const nodeCache2 = new NodeCache();
const htmlElRef1 = nodeCache.getOrCreateNodeReference(htmlEl);
const htmlElRef2 = nodeCache2.getOrCreateNodeReference(htmlEl);
notEqual(htmlElRef1, htmlElRef2);
equal(
nodeCache.getNode(browser.browsingContext, htmlElRef1),
nodeCache2.getNode(browser.browsingContext, htmlElRef2)
);
equal(nodeCache.getNode(browser.browsingContext, htmlElRef2), null);
run_next_test();
});
add_test(function clear() {
nodeCache.add(domEl);
nodeCache.add(svgEl);
const { browser, htmlEl, nodeCache, svgEl } = setupTest();
nodeCache.getOrCreateNodeReference(htmlEl);
nodeCache.getOrCreateNodeReference(svgEl);
equal(nodeCache.size, 2);
// Clear requires explicit arguments.
@ -48,15 +102,16 @@ add_test(function clear() {
// Clear references for a different browsing context
const browser2 = Services.appShell.createWindowlessBrowser(false);
let imgEl = browser2.document.createElement("img");
browser2.document.body.appendChild(imgEl);
const imgEl = browser2.document.createElement("img");
const imgElRef = nodeCache.getOrCreateNodeReference(imgEl);
equal(nodeCache.size, 3);
nodeCache.add(imgEl);
nodeCache.clear({ browsingContext: browser.browsingContext });
equal(nodeCache.size, 1);
equal(nodeCache.getNode(browser2.browsingContext, imgElRef), imgEl);
// Clear all references
nodeCache.add(domEl);
nodeCache.getOrCreateNodeReference(htmlEl);
equal(nodeCache.size, 2);
nodeCache.clear({ all: true });
@ -65,59 +120,91 @@ add_test(function clear() {
run_next_test();
});
add_test(function resolveElement() {
const domElSharedId = nodeCache.add(domEl);
deepEqual(nodeCache.resolve(domElSharedId), domEl);
add_test(function getNode_multiple_nodes() {
const { browser, htmlEl, nodeCache, svgEl } = setupTest();
const svgElSharedId = nodeCache.add(svgEl);
deepEqual(nodeCache.resolve(svgElSharedId), svgEl);
deepEqual(nodeCache.resolve(domElSharedId), domEl);
const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl);
const svgElRef = nodeCache.getOrCreateNodeReference(svgEl);
equal(nodeCache.getNode(browser.browsingContext, svgElRef), svgEl);
equal(nodeCache.getNode(browser.browsingContext, htmlElRef), htmlEl);
run_next_test();
});
add_test(function resolveUnknownElement() {
Assert.throws(() => nodeCache.resolve("foo"), /NoSuchElementError/);
add_test(function getNode_differentBrowsingContextInSameGroup() {
const { iframeEl, htmlEl, nodeCache } = setupTest();
const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl);
equal(nodeCache.size, 1);
equal(
nodeCache.getNode(iframeEl.contentWindow.browsingContext, htmlElRef),
htmlEl
);
run_next_test();
});
add_test(function resolveElementNotAttachedToDOM() {
const imgEl = browser.document.createElement("img");
add_test(function getNode_differentBrowsingContextInOtherGroup() {
const { htmlEl, nodeCache } = setupTest();
const imgElSharedId = nodeCache.add(imgEl);
deepEqual(nodeCache.resolve(imgElSharedId), imgEl);
const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl);
equal(nodeCache.size, 1);
const browser2 = Services.appShell.createWindowlessBrowser(false);
equal(nodeCache.getNode(browser2.browsingContext, htmlElRef), null);
run_next_test();
});
add_test(async function resolveElementRemoved() {
let imgEl = browser.document.createElement("img");
const imgElSharedId = nodeCache.add(imgEl);
add_test(async function getNode_nodeDeleted() {
const { browser, nodeCache } = setupTest();
let el = browser.document.createElement("div");
const elRef = nodeCache.getOrCreateNodeReference(el);
// Delete element and force a garbage collection
imgEl = null;
el = null;
await doGC();
const el = nodeCache.resolve(imgElSharedId);
deepEqual(el, null);
equal(nodeCache.getNode(browser.browsingContext, elRef), null);
run_next_test();
});
add_test(function elementReferencesDifferentPerNodeCache() {
const sharedId = nodeCache.add(domEl);
add_test(function getNodeDetails_forTopBrowsingContext() {
const { browser, htmlEl, nodeCache } = setupTest();
const nodeCache2 = new NodeCache();
const sharedId2 = nodeCache2.add(domEl);
const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl);
notEqual(sharedId, sharedId2);
equal(nodeCache.resolve(sharedId), nodeCache2.resolve(sharedId2));
Assert.throws(() => nodeCache.resolve(sharedId2), /NoSuchElementError/);
nodeCache2.clear({ all: true });
const nodeDetails = nodeCache.getReferenceDetails(htmlElRef);
equal(nodeDetails.browserId, browser.browsingContext.browserId);
equal(nodeDetails.browsingContextGroupId, browser.browsingContext.group.id);
equal(nodeDetails.browsingContextId, browser.browsingContext.id);
ok(nodeDetails.isTopBrowsingContext);
ok(nodeDetails.nodeWeakRef);
equal(nodeDetails.nodeWeakRef.get(), htmlEl);
run_next_test();
});
add_test(async function getNodeDetails_forChildBrowsingContext() {
const { browser, iframeEl, childEl, nodeCache } = setupTest();
const childElRef = nodeCache.getOrCreateNodeReference(childEl);
const nodeDetails = nodeCache.getReferenceDetails(childElRef);
equal(nodeDetails.browserId, browser.browsingContext.browserId);
equal(nodeDetails.browsingContextGroupId, browser.browsingContext.group.id);
equal(
nodeDetails.browsingContextId,
iframeEl.contentWindow.browsingContext.id
);
ok(!nodeDetails.isTopBrowsingContext);
ok(nodeDetails.nodeWeakRef);
equal(nodeDetails.nodeWeakRef.get(), childEl);
run_next_test();
});