Bug 1822466 - [marionette] Check Navigable's seen nodes map for known nodes. r=webdriver-reviewers,jdescottes

Differential Revision: https://phabricator.services.mozilla.com/D177492
This commit is contained in:
Henrik Skupin
2023-06-12 08:53:04 +00:00
parent c6865260c0
commit c6d794e13e
17 changed files with 501 additions and 277 deletions

View File

@@ -82,10 +82,11 @@ export class MarionetteCommandsChild extends JSWindowActorChild {
let waitForNextTick = false;
const { name, data: serializedData } = msg;
const data = lazy.json.deserialize(
serializedData,
this.#processActor.getNodeCache(),
this.contentWindow
this.contentWindow.browsingContext
);
switch (name) {
@@ -183,9 +184,14 @@ export class MarionetteCommandsChild extends JSWindowActorChild {
await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
}
return {
data: lazy.json.clone(result, this.#processActor.getNodeCache()),
};
const { seenNodeIds, serializedValue } = lazy.json.clone(
result,
this.#processActor.getNodeCache()
);
// Because in WebDriver classic nodes can only be returned from the same
// browsing context, we only need the seen unique ids as flat array.
return { seenNodeIds: [...seenNodeIds.values()].flat(), serializedValue };
} catch (e) {
// Always wrap errors as WebDriverError
return { error: lazy.error.wrap(e).toJSON() };

View File

@@ -9,6 +9,8 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
capture: "chrome://remote/content/shared/Capture.sys.mjs",
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
getSeenNodesForBrowsingContext:
"chrome://remote/content/shared/webdriver/Session.sys.mjs",
Log: "chrome://remote/content/shared/Log.sys.mjs",
});
@@ -18,7 +20,6 @@ XPCOMUtils.defineLazyGetter(lazy, "logger", () =>
// Because Marionette supports a single session only we store its id
// globally so that the parent actor can access it.
// eslint-disable-next-line no-unused-vars
let webDriverSessionId = null;
export class MarionetteCommandsParent extends JSWindowActorParent {
@@ -32,20 +33,68 @@ export class MarionetteCommandsParent extends JSWindowActorParent {
});
}
async sendQuery(name, data) {
async sendQuery(name, serializedValue) {
const seenNodes = lazy.getSeenNodesForBrowsingContext(
webDriverSessionId,
this.manager.browsingContext
);
// return early if a dialog is opened
const result = await Promise.race([
super.sendQuery(name, data),
const {
error,
seenNodeIds,
serializedValue: serializedResult,
} = await Promise.race([
super.sendQuery(name, serializedValue),
this.dialogOpenedPromise(),
]).finally(() => {
this._resolveDialogOpened = null;
});
if ("error" in result) {
throw lazy.error.WebDriverError.fromJSON(result.error);
} else {
return result.data;
if (error) {
const err = lazy.error.WebDriverError.fromJSON(error);
this.#handleError(err, seenNodes);
}
// Update seen nodes for serialized element and shadow root nodes.
seenNodeIds?.forEach(nodeId => seenNodes.add(nodeId));
return serializedResult;
}
/**
* Handle WebDriver error and replace error type if necessary.
*
* @param {WebDriverError} error
* The WebDriver error to handle.
* @param {Set<string>} seenNodes
* List of node ids already seen in this navigable.
*
* @throws {WebDriverError}
* The original or replaced WebDriver error.
*/
#handleError(error, seenNodes) {
// If an element hasn't been found during deserialization check if it
// may be a stale reference.
if (
error instanceof lazy.error.NoSuchElementError &&
error.data.elementId !== undefined &&
seenNodes.has(error.data.elementId)
) {
throw new lazy.error.StaleElementReferenceError(error);
}
// If a shadow root hasn't been found during deserialization check if it
// may be a detached reference.
if (
error instanceof lazy.error.NoSuchShadowRootError &&
error.data.shadowId !== undefined &&
seenNodes.has(error.data.shadowId)
) {
throw new lazy.error.DetachedShadowRootError(error);
}
throw error;
}
notifyDialogOpened() {

View File

@@ -488,9 +488,10 @@ element.findClosest = function (startNode, selector) {
* longer the active document or it is no longer attached to the DOM.
*/
element.getKnownElement = function (browsingContext, nodeId, nodeCache) {
if (!element.isNodeReferenceKnown(browsingContext, nodeId, nodeCache)) {
if (!isNodeReferenceKnown(browsingContext, nodeId, nodeCache)) {
throw new lazy.error.NoSuchElementError(
`The element with the reference ${nodeId} is not known in the current browsing context`
`The element with the reference ${nodeId} is not known in the current browsing context`,
{ elementId: nodeId }
);
}
@@ -536,9 +537,10 @@ element.getKnownElement = function (browsingContext, nodeId, nodeCache) {
* longer the active document or it is no longer attached to the DOM.
*/
element.getKnownShadowRoot = function (browsingContext, nodeId, nodeCache) {
if (!element.isNodeReferenceKnown(browsingContext, nodeId, nodeCache)) {
if (!isNodeReferenceKnown(browsingContext, nodeId, nodeCache)) {
throw new lazy.error.NoSuchShadowRootError(
`The shadow root with the reference ${nodeId} is not known in the current browsing context`
`The shadow root with the reference ${nodeId} is not known in the current browsing context`,
{ shadowId: nodeId }
);
}
@@ -564,6 +566,39 @@ element.getKnownShadowRoot = function (browsingContext, nodeId, nodeCache) {
return node;
};
/**
* 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.
*/
function isNodeReferenceKnown(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>obj<var> is an HTML or JS collection.
*
@@ -609,39 +644,6 @@ element.isDetached = function (shadowRoot) {
);
};
/**
* Determines if the node reference is known for the given browsing context.
*
* For WebDriver classic only nodes from the same browsing context are
* allowed to be accessed.
*
* @param {BrowsingContext} browsingContext
* The browsing context the element has to be part of.
* @param {ElementIdentifier} nodeId
* The WebElement reference identifier for a DOM element.
* @param {NodeCache} nodeCache
* Node cache that holds already seen node references.
*
* @returns {boolean}
* True if the element is known in the given browsing context.
*/
element.isNodeReferenceKnown = function (browsingContext, nodeId, nodeCache) {
const nodeDetails = nodeCache.getReferenceDetails(nodeId);
if (nodeDetails === null) {
return false;
}
if (nodeDetails.isTopBrowsingContext) {
// As long as Navigables are not available any cross-group navigation will
// cause a swap of the current top-level browsing context. The only unique
// identifier in such a case is the browser id the top-level browsing
// context actually lives in.
return nodeDetails.browserId === browsingContext.browserId;
}
return nodeDetails.browsingContextId === browsingContext.id;
};
/**
* Determines if <var>el</var> is stale.
*

View File

@@ -95,9 +95,11 @@ function cloneObject(value, seen, cloneAlgorithm) {
* @param {NodeCache} nodeCache
* Node cache that holds already seen WebElement and ShadowRoot references.
*
* @returns {object}
* Same object as provided by `value` with the WebDriver specific
* elements replaced by WebReference's.
* @returns {Object<Map<BrowsingContext, Array<string>, object>>}
* Object that contains a list of browsing contexts each with a list of
* shared ids for collected elements and shadow root nodes, and second the
* same object as provided by `value` with the WebDriver classic supported
* DOM nodes replaced by WebReference's.
*
* @throws {JavaScriptError}
* If an object contains cyclic references.
@@ -106,6 +108,8 @@ function cloneObject(value, seen, cloneAlgorithm) {
* attached to the DOM.
*/
json.clone = function (value, nodeCache) {
const seenNodeIds = new Map();
function cloneJSON(value, seen) {
if (seen === undefined) {
seen = new Set();
@@ -143,7 +147,8 @@ json.clone = function (value, nodeCache) {
);
}
const nodeRef = nodeCache.getOrCreateNodeReference(value);
const nodeRef = nodeCache.getOrCreateNodeReference(value, seenNodeIds);
return lazy.WebReference.from(value, nodeRef).toJSON();
}
@@ -157,7 +162,8 @@ json.clone = function (value, nodeCache) {
);
}
const nodeRef = nodeCache.getOrCreateNodeReference(value);
const nodeRef = nodeCache.getOrCreateNodeReference(value, seenNodeIds);
return lazy.WebReference.from(value, nodeRef).toJSON();
}
@@ -177,7 +183,7 @@ json.clone = function (value, nodeCache) {
return cloneObject(value, seen, cloneJSON);
}
return cloneJSON(value, new Set());
return { seenNodeIds, serializedValue: cloneJSON(value, new Set()) };
};
/**
@@ -187,8 +193,8 @@ json.clone = function (value, nodeCache) {
* Arbitrary object.
* @param {NodeCache} nodeCache
* Node cache that holds already seen WebElement and ShadowRoot references.
* @param {WindowProxy} win
* Current window.
* @param {BrowsingContext} browsingContext
* The browsing context to check.
*
* @returns {object}
* Same object as provided by `value` with the WebDriver specific
@@ -199,7 +205,7 @@ json.clone = function (value, nodeCache) {
* @throws {StaleElementReferenceError}
* If the element is stale, indicating it is no longer attached to the DOM.
*/
json.deserialize = function (value, nodeCache, win) {
json.deserialize = function (value, nodeCache, browsingContext) {
function deserializeJSON(value, seen) {
if (seen === undefined) {
seen = new Set();
@@ -223,7 +229,7 @@ json.deserialize = function (value, nodeCache, win) {
if (webRef instanceof lazy.ShadowRoot) {
return lazy.element.getKnownShadowRoot(
win.browsingContext,
browsingContext,
webRef.uuid,
nodeCache
);
@@ -231,7 +237,7 @@ json.deserialize = function (value, nodeCache, win) {
if (webRef instanceof lazy.WebElement) {
return lazy.element.getKnownElement(
win.browsingContext,
browsingContext,
webRef.uuid,
nodeCache
);

View File

@@ -449,106 +449,123 @@ add_task(function test_coordinates() {
);
});
add_task(function test_isNodeReferenceKnown() {
const { browser, nodeCache, childEl, iframeEl, videoEl } = setupTest();
// Unknown node reference
ok(!element.isNodeReferenceKnown(browser.browsingContext, "foo", nodeCache));
// Known node reference
const videoElRef = nodeCache.getOrCreateNodeReference(videoEl);
ok(
element.isNodeReferenceKnown(browser.browsingContext, videoElRef, nodeCache)
);
// Different top-level browsing context
const browser2 = Services.appShell.createWindowlessBrowser(false);
ok(
!element.isNodeReferenceKnown(
browser2.browsingContext,
videoElRef,
nodeCache
)
);
// Different child browsing context
const childElRef = nodeCache.getOrCreateNodeReference(childEl);
const childBrowsingContext = iframeEl.contentWindow.browsingContext;
ok(element.isNodeReferenceKnown(childBrowsingContext, childElRef, nodeCache));
const iframeEl2 = browser2.document.createElement("iframe");
browser2.document.body.appendChild(iframeEl2);
const childBrowsingContext2 = iframeEl2.contentWindow.browsingContext;
ok(
!element.isNodeReferenceKnown(childBrowsingContext2, childElRef, nodeCache)
);
});
add_task(function test_getKnownElement() {
add_task(async function test_getKnownElement() {
const { browser, nodeCache, shadowRoot, videoEl } = setupTest();
const seenNodes = new Set();
// Unknown element reference
Assert.throws(() => {
element.getKnownElement(browser.browsingContext, "foo", nodeCache);
element.getKnownElement(
browser.browsingContext,
"foo",
nodeCache,
seenNodes
);
}, /NoSuchElementError/);
// With a ShadowRoot reference
const shadowRootRef = nodeCache.getOrCreateNodeReference(shadowRoot);
const seenNodeIds = new Map();
const shadowRootRef = nodeCache.getOrCreateNodeReference(
shadowRoot,
seenNodeIds
);
seenNodes.add(shadowRootRef);
Assert.throws(() => {
element.getKnownElement(browser.browsingContext, shadowRootRef, nodeCache);
element.getKnownElement(
browser.browsingContext,
shadowRootRef,
nodeCache,
seenNodes
);
}, /NoSuchElementError/);
// Deleted element (eg. garbage collected)
let detachedEl = browser.document.createElement("div");
const detachedElRef = nodeCache.getOrCreateNodeReference(detachedEl);
const detachedElRef = nodeCache.getOrCreateNodeReference(
detachedEl,
seenNodeIds
);
seenNodes.add(detachedElRef);
// ... not connected to the DOM
// Element not connected to the DOM
Assert.throws(() => {
element.getKnownElement(browser.browsingContext, detachedElRef, nodeCache);
element.getKnownElement(
browser.browsingContext,
detachedElRef,
nodeCache,
seenNodes
);
}, /StaleElementReferenceError/);
// ... element garbage collected
// Element garbage collected
detachedEl = null;
MemoryReporter.minimizeMemoryUsage(() => {
Assert.throws(() => {
element.getKnownElement(
browser.browsingContext,
detachedElRef,
nodeCache
);
}, /StaleElementReferenceError/);
});
await new Promise(resolve => MemoryReporter.minimizeMemoryUsage(resolve));
Assert.throws(() => {
element.getKnownElement(
browser.browsingContext,
detachedElRef,
nodeCache,
seenNodes
);
}, /StaleElementReferenceError/);
// Known element reference
const videoElRef = nodeCache.getOrCreateNodeReference(videoEl);
const videoElRef = nodeCache.getOrCreateNodeReference(videoEl, seenNodeIds);
seenNodes.add(videoElRef);
equal(
element.getKnownElement(browser.browsingContext, videoElRef, nodeCache),
element.getKnownElement(
browser.browsingContext,
videoElRef,
nodeCache,
seenNodes
),
videoEl
);
});
add_task(function test_getKnownShadowRoot() {
add_task(async function test_getKnownShadowRoot() {
const { browser, nodeCache, shadowRoot, videoEl } = setupTest();
const seenNodeIds = new Map();
const seenNodes = new Set();
const videoElRef = nodeCache.getOrCreateNodeReference(videoEl);
const videoElRef = nodeCache.getOrCreateNodeReference(videoEl, seenNodeIds);
seenNodes.add(videoElRef);
// Unknown ShadowRoot reference
Assert.throws(() => {
element.getKnownShadowRoot(browser.browsingContext, "foo", nodeCache);
element.getKnownShadowRoot(
browser.browsingContext,
"foo",
nodeCache,
seenNodes
);
}, /NoSuchShadowRootError/);
// With a HTMLElement reference
Assert.throws(() => {
element.getKnownShadowRoot(browser.browsingContext, videoElRef, nodeCache);
element.getKnownShadowRoot(
browser.browsingContext,
videoElRef,
nodeCache,
seenNodes
);
}, /NoSuchShadowRootError/);
// Known ShadowRoot reference
const shadowRootRef = nodeCache.getOrCreateNodeReference(shadowRoot);
const shadowRootRef = nodeCache.getOrCreateNodeReference(
shadowRoot,
seenNodeIds
);
seenNodes.add(shadowRootRef);
equal(
element.getKnownShadowRoot(
browser.browsingContext,
shadowRootRef,
nodeCache
nodeCache,
seenNodes
),
shadowRoot
);
@@ -558,30 +575,35 @@ add_task(function test_getKnownShadowRoot() {
let detachedShadowRoot = el.attachShadow({ mode: "open" });
detachedShadowRoot.innerHTML = "<input></input>";
const detachedShadowRootRef =
nodeCache.getOrCreateNodeReference(detachedShadowRoot);
const detachedShadowRootRef = nodeCache.getOrCreateNodeReference(
detachedShadowRoot,
seenNodeIds
);
seenNodes.add(detachedShadowRootRef);
// ... not connected to the DOM
Assert.throws(() => {
element.getKnownShadowRoot(
browser.browsingContext,
detachedShadowRootRef,
nodeCache
nodeCache,
seenNodes
);
}, /DetachedShadowRootError/);
// ... host and shadow root garbage collected
el = null;
detachedShadowRoot = null;
MemoryReporter.minimizeMemoryUsage(() => {
Assert.throws(() => {
element.getKnownShadowRoot(
browser.browsingContext,
detachedShadowRootRef,
nodeCache
);
}, /DetachedShadowRootError/);
});
await new Promise(resolve => MemoryReporter.minimizeMemoryUsage(resolve));
Assert.throws(() => {
element.getKnownShadowRoot(
browser.browsingContext,
detachedShadowRootRef,
nodeCache,
seenNodes
);
}, /DetachedShadowRootError/);
});
add_task(function test_isDetached() {

View File

@@ -27,68 +27,95 @@ function setupTest() {
browser.document.body.appendChild(iframeEl);
const childEl = iframeEl.contentDocument.createElement("div");
return { browser, nodeCache, childEl, iframeEl, htmlEl, shadowRoot, svgEl };
return {
browser,
browsingContext: browser.browsingContext,
nodeCache,
childEl,
iframeEl,
htmlEl,
seenNodeIds: new Map(),
shadowRoot,
svgEl,
};
}
function assert_cloned_value(value, clonedValue, nodeCache, seenNodes = []) {
const { seenNodeIds, serializedValue } = json.clone(value, nodeCache);
deepEqual(serializedValue, clonedValue);
deepEqual([...seenNodeIds.values()], seenNodes);
}
add_task(function test_clone_generalTypes() {
const { nodeCache } = setupTest();
// null
equal(json.clone(undefined, nodeCache), null);
equal(json.clone(null, nodeCache), null);
assert_cloned_value(undefined, null, nodeCache);
assert_cloned_value(null, null, nodeCache);
// primitives
equal(json.clone(true, nodeCache), true);
equal(json.clone(42, nodeCache), 42);
equal(json.clone("foo", nodeCache), "foo");
assert_cloned_value(true, true, nodeCache);
assert_cloned_value(42, 42, nodeCache);
assert_cloned_value("foo", "foo", nodeCache);
// toJSON
equal(
json.clone({
assert_cloned_value(
{
toJSON() {
return "foo";
},
}),
"foo"
},
"foo",
nodeCache
);
});
add_task(function test_clone_ShadowRoot() {
const { nodeCache, shadowRoot } = setupTest();
const { nodeCache, seenNodeIds, shadowRoot } = setupTest();
const shadowRootRef = nodeCache.getOrCreateNodeReference(shadowRoot);
deepEqual(
json.clone(shadowRoot, nodeCache),
WebReference.from(shadowRoot, shadowRootRef).toJSON()
const shadowRootRef = nodeCache.getOrCreateNodeReference(
shadowRoot,
seenNodeIds
);
assert_cloned_value(
shadowRoot,
WebReference.from(shadowRoot, shadowRootRef).toJSON(),
nodeCache,
seenNodeIds
);
});
add_task(function test_clone_WebElement() {
const { htmlEl, nodeCache, svgEl } = setupTest();
const { htmlEl, nodeCache, seenNodeIds, svgEl } = setupTest();
const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl);
deepEqual(
json.clone(htmlEl, nodeCache),
WebReference.from(htmlEl, htmlElRef).toJSON()
const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl, seenNodeIds);
assert_cloned_value(
htmlEl,
WebReference.from(htmlEl, htmlElRef).toJSON(),
nodeCache,
seenNodeIds
);
// Check an element with a different namespace
const svgElRef = nodeCache.getOrCreateNodeReference(svgEl);
deepEqual(
json.clone(svgEl, nodeCache),
WebReference.from(svgEl, svgElRef).toJSON()
const svgElRef = nodeCache.getOrCreateNodeReference(svgEl, seenNodeIds);
assert_cloned_value(
svgEl,
WebReference.from(svgEl, svgElRef).toJSON(),
nodeCache,
seenNodeIds
);
});
add_task(function test_clone_Sequences() {
const { htmlEl, nodeCache } = setupTest();
const { htmlEl, nodeCache, seenNodeIds } = setupTest();
const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl);
const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl, seenNodeIds);
const input = [
null,
true,
[],
[42],
htmlEl,
{
toJSON() {
@@ -98,20 +125,25 @@ add_task(function test_clone_Sequences() {
{ bar: "baz" },
];
const actual = json.clone(input, nodeCache);
equal(actual[0], null);
equal(actual[1], true);
deepEqual(actual[2], []);
deepEqual(actual[3], { [WebElement.Identifier]: htmlElRef });
equal(actual[4], "foo");
deepEqual(actual[5], { bar: "baz" });
assert_cloned_value(
input,
[
null,
true,
[42],
{ [WebElement.Identifier]: htmlElRef },
"foo",
{ bar: "baz" },
],
nodeCache,
seenNodeIds
);
});
add_task(function test_clone_objects() {
const { htmlEl, nodeCache } = setupTest();
const { htmlEl, nodeCache, seenNodeIds } = setupTest();
const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl);
const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl, seenNodeIds);
const input = {
null: null,
@@ -126,14 +158,19 @@ add_task(function test_clone_objects() {
object: { bar: "baz" },
};
const actual = json.clone(input, nodeCache);
equal(actual.null, null);
equal(actual.boolean, true);
deepEqual(actual.array, [42]);
deepEqual(actual.element, { [WebElement.Identifier]: htmlElRef });
equal(actual.toJSON, "foo");
deepEqual(actual.object, { bar: "baz" });
assert_cloned_value(
input,
{
null: null,
boolean: true,
array: [42],
element: { [WebElement.Identifier]: htmlElRef },
toJSON: "foo",
object: { bar: "baz" },
},
nodeCache,
seenNodeIds
);
});
add_task(function test_clone_сyclicReference() {
@@ -169,68 +206,84 @@ add_task(function test_clone_сyclicReference() {
});
add_task(function test_deserialize_generalTypes() {
const { browser, nodeCache } = setupTest();
const win = browser.document.ownerGlobal;
const { browsingContext, nodeCache } = setupTest();
// null
equal(json.deserialize(undefined, nodeCache, win), undefined);
equal(json.deserialize(null, nodeCache, win), null);
equal(json.deserialize(undefined, nodeCache, browsingContext), undefined);
equal(json.deserialize(null, nodeCache, browsingContext), null);
// primitives
equal(json.deserialize(true, nodeCache, win), true);
equal(json.deserialize(42, nodeCache, win), 42);
equal(json.deserialize("foo", nodeCache, win), "foo");
equal(json.deserialize(true, nodeCache, browsingContext), true);
equal(json.deserialize(42, nodeCache, browsingContext), 42);
equal(json.deserialize("foo", nodeCache, browsingContext), "foo");
});
add_task(function test_deserialize_ShadowRoot() {
const { browser, nodeCache, shadowRoot } = setupTest();
const win = browser.document.ownerGlobal;
const { browsingContext, nodeCache, seenNodeIds, shadowRoot } = setupTest();
const seenNodes = new Set();
// Fails to resolve for unknown elements
const unknownShadowRootId = { [ShadowRoot.Identifier]: "foo" };
Assert.throws(() => {
json.deserialize(unknownShadowRootId, nodeCache, win);
json.deserialize(
unknownShadowRootId,
nodeCache,
browsingContext,
seenNodes
);
}, /NoSuchShadowRootError/);
const shadowRootRef = nodeCache.getOrCreateNodeReference(shadowRoot);
const shadowRootRef = nodeCache.getOrCreateNodeReference(
shadowRoot,
seenNodeIds
);
const shadowRootEl = { [ShadowRoot.Identifier]: shadowRootRef };
// Fails to resolve for missing window reference
Assert.throws(() => json.deserialize(shadowRootEl, nodeCache), /TypeError/);
// Previously seen element is associated with original web element reference
const root = json.deserialize(shadowRootEl, nodeCache, win);
seenNodes.add(shadowRootRef);
const root = json.deserialize(
shadowRootEl,
nodeCache,
browsingContext,
seenNodes
);
deepEqual(root, shadowRoot);
deepEqual(root, nodeCache.getNode(browser.browsingContext, shadowRootRef));
deepEqual(root, nodeCache.getNode(browsingContext, shadowRootRef));
});
add_task(function test_deserialize_WebElement() {
const { browser, htmlEl, nodeCache } = setupTest();
const win = browser.document.ownerGlobal;
const { browser, browsingContext, htmlEl, nodeCache, seenNodeIds } =
setupTest();
const seenNodes = new Set();
// Fails to resolve for unknown elements
const unknownWebElId = { [WebElement.Identifier]: "foo" };
Assert.throws(() => {
json.deserialize(unknownWebElId, nodeCache, win);
json.deserialize(unknownWebElId, nodeCache, browsingContext, seenNodes);
}, /NoSuchElementError/);
const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl);
const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl, seenNodeIds);
const htmlWebEl = { [WebElement.Identifier]: htmlElRef };
// Fails to resolve for missing window reference
Assert.throws(() => json.deserialize(htmlWebEl, nodeCache), /TypeError/);
// Previously seen element is associated with original web element reference
const el = json.deserialize(htmlWebEl, nodeCache, win);
seenNodes.add(htmlElRef);
const el = json.deserialize(htmlWebEl, nodeCache, browsingContext, seenNodes);
deepEqual(el, htmlEl);
deepEqual(el, nodeCache.getNode(browser.browsingContext, htmlElRef));
});
add_task(function test_deserialize_Sequences() {
const { browser, htmlEl, nodeCache } = setupTest();
const win = browser.document.ownerGlobal;
const { browsingContext, htmlEl, nodeCache, seenNodeIds } = setupTest();
const seenNodes = new Set();
const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl);
const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl, seenNodeIds);
seenNodes.add(htmlElRef);
const input = [
null,
@@ -240,7 +293,7 @@ add_task(function test_deserialize_Sequences() {
{ bar: "baz" },
];
const actual = json.deserialize(input, nodeCache, win);
const actual = json.deserialize(input, nodeCache, browsingContext, seenNodes);
equal(actual[0], null);
equal(actual[1], true);
@@ -250,10 +303,11 @@ add_task(function test_deserialize_Sequences() {
});
add_task(function test_deserialize_objects() {
const { browser, htmlEl, nodeCache } = setupTest();
const win = browser.document.ownerGlobal;
const { browsingContext, htmlEl, nodeCache, seenNodeIds } = setupTest();
const seenNodes = new Set();
const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl);
const htmlElRef = nodeCache.getOrCreateNodeReference(htmlEl, seenNodeIds);
seenNodes.add(htmlElRef);
const input = {
null: null,
@@ -263,7 +317,7 @@ add_task(function test_deserialize_objects() {
object: { bar: "baz" },
};
const actual = json.deserialize(input, nodeCache, win);
const actual = json.deserialize(input, nodeCache, browsingContext, seenNodes);
equal(actual.null, null);
equal(actual.boolean, true);

View File

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

View File

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

View File

@@ -50,11 +50,14 @@ export class NodeCache {
*
* @param {Node} node
* The node to be added.
* @param {Map<BrowsingContext, Array<string>>} seenNodeIds
* Map of browsing contexts to their seen node ids during the current
* serialization.
*
* @returns {string}
* The unique node reference for the DOM node.
*/
getOrCreateNodeReference(node) {
getOrCreateNodeReference(node, seenNodeIds) {
if (!Node.isInstance(node)) {
throw new TypeError(`Failed to create node reference for ${node}`);
}
@@ -82,6 +85,13 @@ export class NodeCache {
this.#nodeIdMap.set(node, nodeId);
this.#seenNodesMap.set(nodeId, details);
// Also add the information for the node id and its correlated browsing
// context to allow the parent process to update the seen nodes.
if (!seenNodeIds.has(browsingContext)) {
seenNodeIds.set(browsingContext, []);
}
seenNodeIds.get(browsingContext).push(nodeId);
}
return nodeId;

View File

@@ -19,6 +19,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
"chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs",
RootMessageHandlerRegistry:
"chrome://remote/content/shared/messagehandler/RootMessageHandlerRegistry.sys.mjs",
TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
unregisterProcessDataActor:
"chrome://remote/content/shared/webdriver/process-actors/WebDriverProcessDataParent.sys.mjs",
WebDriverBiDiConnection:
@@ -201,6 +202,10 @@ export class WebDriverSession {
this._connections.add(connection);
}
// Maps a Navigable (browsing context or content browser for top-level
// browsing contexts) to a Set of nodeId's.
this.navigableSeenNodes = new WeakMap();
lazy.registerProcessDataActor();
webDriverSessions.set(this.id, this);
@@ -209,6 +214,10 @@ export class WebDriverSession {
destroy() {
webDriverSessions.delete(this.id);
lazy.unregisterProcessDataActor();
this.navigableSeenNodes = null;
lazy.allowAllCerts.disable();
// Close all open connections which unregister themselves.
@@ -227,8 +236,6 @@ export class WebDriverSession {
);
this._messageHandler.destroy();
}
lazy.unregisterProcessDataActor();
}
async execute(module, command, params) {
@@ -352,6 +359,30 @@ export class WebDriverSession {
/**
* Get a WebDriver session corresponding to the session id.
* Get the list of seen nodes for the given browsing context.
*
* @param {string} sessionId
* The id of the WebDriver session to use.
* @param {BrowsingContext} browsingContext
* Browsing context the node is part of.
*
* @returns {Set}
* The list of seen nodes.
*/
export function getSeenNodesForBrowsingContext(sessionId, browsingContext) {
const navigable =
lazy.TabManager.getNavigableForBrowsingContext(browsingContext);
const session = getWebDriverSessionById(sessionId);
if (!session.navigableSeenNodes.has(navigable)) {
// The navigable hasn't been seen yet.
session.navigableSeenNodes.set(navigable, new Set());
}
return session.navigableSeenNodes.get(navigable);
}
/**
*
* @param {string} sessionId
* The ID of the WebDriver session to retrieve.

View File

@@ -33,6 +33,7 @@ function setupTest() {
divEl,
iframeEl,
shadowRoot,
seenNodeIds: new Map(),
svgEl,
textareaEl,
videoEl,
@@ -40,32 +41,37 @@ function setupTest() {
}
add_task(function getOrCreateNodeReference_invalid() {
const { nodeCache } = setupTest();
const { nodeCache, seenNodeIds } = setupTest();
const invalidValues = [null, undefined, "foo", 42, true, [], {}];
for (const value of invalidValues) {
info(`Testing value: ${value}`);
Assert.throws(() => nodeCache.getOrCreateNodeReference(value), /TypeError/);
Assert.throws(
() => nodeCache.getOrCreateNodeReference(value, seenNodeIds),
/TypeError/
);
}
});
add_task(function getOrCreateNodeReference_supportedNodeTypes() {
const { browser, divEl, nodeCache } = setupTest();
const { browser, divEl, nodeCache, seenNodeIds } = setupTest();
const xmlDocument = new DOMParser().parseFromString(
"<xml></xml>",
"application/xml"
);
// Bug 1820734: No ownerGlobal is available in XPCShell tests
// const xmlDocument = new DOMParser().parseFromString(
// "<xml></xml>",
// "application/xml"
// );
const values = [
{ node: divEl, type: Node.ELEMENT_NODE },
{ node: divEl.attributes[0], type: Node.ATTRIBUTE_NODE },
{ node: browser.document.createTextNode("foo"), type: Node.TEXT_NODE },
{
node: xmlDocument.createCDATASection("foo"),
type: Node.CDATA_SECTION_NODE,
},
// Bug 1820734: No ownerGlobal is available in XPCShell tests
// {
// node: xmlDocument.createCDATASection("foo"),
// type: Node.CDATA_SECTION_NODE,
// },
{
node: browser.document.createProcessingInstruction(
"xml-stylesheet",
@@ -91,39 +97,51 @@ add_task(function getOrCreateNodeReference_supportedNodeTypes() {
values.forEach((value, index) => {
info(`Testing value: ${value.type}`);
const nodeRef = nodeCache.getOrCreateNodeReference(value.node);
const nodeRef = nodeCache.getOrCreateNodeReference(value.node, seenNodeIds);
equal(nodeCache.size, index + 1);
equal(typeof nodeRef, "string");
ok(seenNodeIds.get(browser.browsingContext).includes(nodeRef));
});
});
add_task(function getOrCreateNodeReference_referenceAlreadyCreated() {
const { divEl, nodeCache } = setupTest();
const { browser, divEl, nodeCache, seenNodeIds } = setupTest();
const divElRef = nodeCache.getOrCreateNodeReference(divEl, seenNodeIds);
const divElRefOther = nodeCache.getOrCreateNodeReference(divEl, seenNodeIds);
const divElRef = nodeCache.getOrCreateNodeReference(divEl);
const divElRefOther = nodeCache.getOrCreateNodeReference(divEl);
equal(nodeCache.size, 1);
equal(divElRefOther, divElRef);
equal(nodeCache.size, 1);
equal(seenNodeIds.size, 1);
ok(seenNodeIds.get(browser.browsingContext).includes(divElRef));
});
add_task(function getOrCreateNodeReference_differentReference() {
const { divEl, nodeCache, shadowRoot } = setupTest();
const { browser, divEl, nodeCache, seenNodeIds, shadowRoot } = setupTest();
const divElRef = nodeCache.getOrCreateNodeReference(divEl);
const divElRef = nodeCache.getOrCreateNodeReference(divEl, seenNodeIds);
equal(nodeCache.size, 1);
equal(seenNodeIds.size, 1);
ok(seenNodeIds.get(browser.browsingContext).includes(divElRef));
const shadowRootRef = nodeCache.getOrCreateNodeReference(shadowRoot);
const shadowRootRef = nodeCache.getOrCreateNodeReference(
shadowRoot,
seenNodeIds
);
equal(nodeCache.size, 2);
equal(seenNodeIds.size, 1);
ok(seenNodeIds.get(browser.browsingContext).includes(divElRef));
ok(seenNodeIds.get(browser.browsingContext).includes(shadowRootRef));
notEqual(divElRef, shadowRootRef);
});
add_task(function getOrCreateNodeReference_differentReferencePerNodeCache() {
const { browser, divEl, nodeCache } = setupTest();
const { browser, divEl, nodeCache, seenNodeIds } = setupTest();
const nodeCache2 = new NodeCache();
const divElRef1 = nodeCache.getOrCreateNodeReference(divEl);
const divElRef2 = nodeCache2.getOrCreateNodeReference(divEl);
const divElRef1 = nodeCache.getOrCreateNodeReference(divEl, seenNodeIds);
const divElRef2 = nodeCache2.getOrCreateNodeReference(divEl, seenNodeIds);
notEqual(divElRef1, divElRef2);
equal(
@@ -131,15 +149,20 @@ add_task(function getOrCreateNodeReference_differentReferencePerNodeCache() {
nodeCache2.getNode(browser.browsingContext, divElRef2)
);
equal(seenNodeIds.size, 1);
ok(seenNodeIds.get(browser.browsingContext).includes(divElRef1));
ok(seenNodeIds.get(browser.browsingContext).includes(divElRef2));
equal(nodeCache.getNode(browser.browsingContext, divElRef2), null);
});
add_task(function clear() {
const { browser, divEl, nodeCache, svgEl } = setupTest();
const { browser, divEl, nodeCache, seenNodeIds, svgEl } = setupTest();
nodeCache.getOrCreateNodeReference(divEl);
nodeCache.getOrCreateNodeReference(svgEl);
nodeCache.getOrCreateNodeReference(divEl, seenNodeIds);
nodeCache.getOrCreateNodeReference(svgEl, seenNodeIds);
equal(nodeCache.size, 2);
equal(seenNodeIds.size, 1);
// Clear requires explicit arguments.
Assert.throws(() => nodeCache.clear(), /Error/);
@@ -147,35 +170,37 @@ add_task(function clear() {
// Clear references for a different browsing context
const browser2 = Services.appShell.createWindowlessBrowser(false);
const imgEl = browser2.document.createElement("img");
const imgElRef = nodeCache.getOrCreateNodeReference(imgEl);
const imgElRef = nodeCache.getOrCreateNodeReference(imgEl, seenNodeIds);
equal(nodeCache.size, 3);
equal(seenNodeIds.size, 2);
nodeCache.clear({ browsingContext: browser.browsingContext });
equal(nodeCache.size, 1);
equal(nodeCache.getNode(browser2.browsingContext, imgElRef), imgEl);
// Clear all references
nodeCache.getOrCreateNodeReference(divEl);
nodeCache.getOrCreateNodeReference(divEl, seenNodeIds);
equal(nodeCache.size, 2);
equal(seenNodeIds.size, 2);
nodeCache.clear({ all: true });
equal(nodeCache.size, 0);
});
add_task(function getNode_multiple_nodes() {
const { browser, divEl, nodeCache, svgEl } = setupTest();
const { browser, divEl, nodeCache, seenNodeIds, svgEl } = setupTest();
const divElRef = nodeCache.getOrCreateNodeReference(divEl);
const svgElRef = nodeCache.getOrCreateNodeReference(svgEl);
const divElRef = nodeCache.getOrCreateNodeReference(divEl, seenNodeIds);
const svgElRef = nodeCache.getOrCreateNodeReference(svgEl, seenNodeIds);
equal(nodeCache.getNode(browser.browsingContext, svgElRef), svgEl);
equal(nodeCache.getNode(browser.browsingContext, divElRef), divEl);
});
add_task(function getNode_differentBrowsingContextInSameGroup() {
const { iframeEl, divEl, nodeCache } = setupTest();
const { iframeEl, divEl, nodeCache, seenNodeIds } = setupTest();
const divElRef = nodeCache.getOrCreateNodeReference(divEl);
const divElRef = nodeCache.getOrCreateNodeReference(divEl, seenNodeIds);
equal(nodeCache.size, 1);
equal(
@@ -185,9 +210,9 @@ add_task(function getNode_differentBrowsingContextInSameGroup() {
});
add_task(function getNode_differentBrowsingContextInOtherGroup() {
const { divEl, nodeCache } = setupTest();
const { divEl, nodeCache, seenNodeIds } = setupTest();
const divElRef = nodeCache.getOrCreateNodeReference(divEl);
const divElRef = nodeCache.getOrCreateNodeReference(divEl, seenNodeIds);
equal(nodeCache.size, 1);
const browser2 = Services.appShell.createWindowlessBrowser(false);
@@ -195,10 +220,10 @@ add_task(function getNode_differentBrowsingContextInOtherGroup() {
});
add_task(async function getNode_nodeDeleted() {
const { browser, nodeCache } = setupTest();
const { browser, nodeCache, seenNodeIds } = setupTest();
let el = browser.document.createElement("div");
const elRef = nodeCache.getOrCreateNodeReference(el);
const elRef = nodeCache.getOrCreateNodeReference(el, seenNodeIds);
// Delete element and force a garbage collection
el = null;
@@ -209,9 +234,9 @@ add_task(async function getNode_nodeDeleted() {
});
add_task(function getNodeDetails_forTopBrowsingContext() {
const { browser, divEl, nodeCache } = setupTest();
const { browser, divEl, nodeCache, seenNodeIds } = setupTest();
const divElRef = nodeCache.getOrCreateNodeReference(divEl);
const divElRef = nodeCache.getOrCreateNodeReference(divEl, seenNodeIds);
const nodeDetails = nodeCache.getReferenceDetails(divElRef);
equal(nodeDetails.browserId, browser.browsingContext.browserId);
@@ -223,9 +248,9 @@ add_task(function getNodeDetails_forTopBrowsingContext() {
});
add_task(async function getNodeDetails_forChildBrowsingContext() {
const { browser, iframeEl, childEl, nodeCache } = setupTest();
const { browser, iframeEl, childEl, nodeCache, seenNodeIds } = setupTest();
const childElRef = nodeCache.getOrCreateNodeReference(childEl);
const childElRef = nodeCache.getOrCreateNodeReference(childEl, seenNodeIds);
const nodeDetails = nodeCache.getReferenceDetails(childElRef);
equal(nodeDetails.browserId, browser.browsingContext.browserId);

View File

@@ -7,7 +7,7 @@
const { Capabilities, Timeouts } = ChromeUtils.importESModule(
"chrome://remote/content/shared/webdriver/Capabilities.sys.mjs"
);
const { WebDriverSession, getWebDriverSessionById } =
const { getWebDriverSessionById, WebDriverSession } =
ChromeUtils.importESModule(
"chrome://remote/content/shared/webdriver/Session.sys.mjs"
);

View File

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

View File

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

View File

@@ -3,8 +3,3 @@
if (os == "linux") and fission and not debug: [OK, TIMEOUT]
[test_link_unload_event]
bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1786639
[test_link_cross_origin[capabilities0\]]
expected:
if (processor == "x86") and debug: [PASS, FAIL]
FAIL

View File

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

View File

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