mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-23 21:01:08 +00:00
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:
parent
fcebb7d02d
commit
1dd9c81aee
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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");
|
||||
|
@ -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 });
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user