Bug 1830884 - [webdriver-bidi] Update Navigable's seen nodes map for known nodes. r=webdriver-reviewers,jdescottes

Differential Revision: https://phabricator.services.mozilla.com/D177496
This commit is contained in:
Henrik Skupin
2023-06-12 08:53:05 +00:00
parent 177e02374d
commit 8c55c44d11
14 changed files with 800 additions and 580 deletions

View File

@@ -67,6 +67,9 @@ export const OwnershipModel = {
*
* @property {NodeCache=} nodeCache
* The cache containing DOM node references.
* @property {Map<BrowsingContext, Array<string>>} seenNodeIds
* Map of browsing contexts to their seen node ids during the current
* serialization.
*/
/**
@@ -469,28 +472,27 @@ function getHandleForObject(realm, ownershipType, object) {
*
* @param {Node} node
* Node to create the unique reference for.
* @param {Realm} realm
* The Realm in which the value is serialized.
* @param {ExtraSerializationOptions} extraOptions
* Extra Remote Value serialization options.
*
* @returns {string}
* Shared unique reference for the Node.
*/
function getSharedIdForNode(node, realm, extraOptions) {
const { nodeCache } = extraOptions;
function getSharedIdForNode(node, extraOptions) {
const { nodeCache, seenNodeIds } = extraOptions;
node = Cu.unwaiveXrays(node);
if (!Node.isInstance(node)) {
return null;
}
const browsingContext = realm.browsingContext;
const browsingContext = node.ownerGlobal.browsingContext;
if (!browsingContext) {
return null;
}
const unwrapped = Cu.unwaiveXrays(node);
return nodeCache.getOrCreateNodeReference(unwrapped);
return nodeCache.getOrCreateNodeReference(node, seenNodeIds);
}
/**
@@ -514,7 +516,7 @@ function getSharedIdForNode(node, realm, extraOptions) {
* Map of internal ids.
* @param {Realm} realm
* The Realm from which comes the value being serialized.
* @param {ExtraSerializationOptions} options
* @param {ExtraSerializationOptions} extraOptions
* Extra Remote Value serialization options.
*
* @returns {object} Object for serialized values.
@@ -528,7 +530,7 @@ function serializeArrayLike(
ownershipType,
serializationInternalMap,
realm,
options
extraOptions
) {
const serialized = buildSerialized(production, handleId);
setInternalIdsIfNeeded(serializationInternalMap, serialized, value);
@@ -540,7 +542,7 @@ function serializeArrayLike(
ownershipType,
serializationInternalMap,
realm,
options
extraOptions
);
}
@@ -899,7 +901,7 @@ export function serialize(
const serialized = buildSerialized("node", handleId);
// Get or create the shared id for WebDriver classic compat from the node.
const sharedId = getSharedIdForNode(value, realm, extraOptions);
const sharedId = getSharedIdForNode(value, extraOptions);
if (sharedId !== null) {
serialized.sharedId = sharedId;
}

View File

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

View File

@@ -0,0 +1,49 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
getSeenNodesForBrowsingContext:
"chrome://remote/content/shared/webdriver/Session.sys.mjs",
});
/**
* The serialization of JavaScript objects in the content process might produce
* extra data that needs to be transfered and then processed by the parent
* process. This extra data is part of the payload as returned by commands
* and events and can contain the following:
*
* - {Map<BrowsingContext, Array<string>>} seenNodeIds
* DOM nodes that need to be added to the navigable seen nodes map.
*
* @param {string} sessionId
* Id of the WebDriver session
* @param {object} payload
* Payload of the response for the command and event that might contain
* a `_extraData` field.
*
* @returns {object}
* The payload with the extra data removed if it was present.
*/
export function processExtraData(sessionId, payload) {
// Process extra data if present and delete it from the payload
if ("_extraData" in payload) {
const { seenNodeIds } = payload._extraData;
// Updates the seen nodes for the current session and browsing context.
seenNodeIds?.forEach((nodeIds, browsingContext) => {
const seenNodes = lazy.getSeenNodesForBrowsingContext(
sessionId,
browsingContext
);
nodeIds.forEach(nodeId => seenNodes.add(nodeId));
});
delete payload._extraData;
}
return payload;
}

View File

@@ -57,7 +57,7 @@ export class WindowGlobalBiDiModule extends Module {
* Extra Remote Value serialization options.
*
* @returns {object}
* Serialized representation of the value.
* Promise that resolves to the serialized representation of the value.
*/
serialize(
value,
@@ -66,15 +66,18 @@ export class WindowGlobalBiDiModule extends Module {
realm,
extraOptions = {}
) {
extraOptions.nodeCache = this.#nodeCache;
const { nodeCache = this.#nodeCache, seenNodeIds = new Map() } =
extraOptions;
return lazy.serialize(
const serializedValue = lazy.serialize(
value,
serializationOptions,
ownershipType,
new Map(),
realm,
extraOptions
{ nodeCache, seenNodeIds }
);
return serializedValue;
}
}

View File

@@ -13,6 +13,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
generateUUID: "chrome://remote/content/shared/UUID.sys.mjs",
OwnershipModel: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
processExtraData:
"chrome://remote/content/webdriver-bidi/modules/Intercept.sys.mjs",
RealmType: "chrome://remote/content/shared/Realm.sys.mjs",
setDefaultAndAssertSerializationOptions:
"chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
@@ -657,6 +659,11 @@ class ScriptModule extends Module {
}
#buildReturnValue(evaluationResult) {
evaluationResult = lazy.processExtraData(
this.messageHandler.sessionId,
evaluationResult
);
const rv = { realm: evaluationResult.realmId };
switch (evaluationResult.evaluationStatus) {
// TODO: Compare with EvaluationStatus.Normal after Bug 1774444 is fixed.

View File

@@ -18,7 +18,7 @@ class BrowsingContextModule extends Module {
name == "browsingContext.domContentLoaded" ||
name == "browsingContext.load"
) {
// Resolve browsing context to a TabManager id.
// Resolve browsing context to a Navigable id.
payload.context = lazy.TabManager.getIdForBrowsingContext(
payload.context
);

View File

@@ -7,6 +7,8 @@ import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
processExtraData:
"chrome://remote/content/webdriver-bidi/modules/Intercept.sys.mjs",
TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
});
@@ -15,10 +17,12 @@ class LogModule extends Module {
interceptEvent(name, payload) {
if (name == "log.entryAdded") {
// Resolve browsing context to a TabManager id.
// Resolve browsing context to a Navigable id.
payload.source.context = lazy.TabManager.getIdForBrowsingContext(
payload.source.context
);
payload = lazy.processExtraData(this.messageHandler.sessionId, payload);
}
return payload;

View File

@@ -7,6 +7,8 @@ import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
processExtraData:
"chrome://remote/content/webdriver-bidi/modules/Intercept.sys.mjs",
TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
});
@@ -15,10 +17,12 @@ class ScriptModule extends Module {
interceptEvent(name, payload) {
if (name == "script.message") {
// Resolve browsing context to a TabManager id.
// Resolve browsing context to a Navigable id.
payload.source.context = lazy.TabManager.getIdForBrowsingContext(
payload.source.context
);
payload = lazy.processExtraData(this.messageHandler.sessionId, payload);
}
return payload;

View File

@@ -135,9 +135,11 @@ class LogModule extends WindowGlobalBiDiModule {
const args = messageArguments || [];
text += args.map(String).join(" ");
// Serialize each arg as remote value.
const defaultRealm = this.messageHandler.getRealm();
const serializedArgs = [];
const seenNodeIds = new Map();
// Serialize each arg as remote value.
for (const arg of args) {
// Note that we can pass a default realm for now since realms are only
// involved when creating object references, which will not happen with
@@ -147,7 +149,8 @@ class LogModule extends WindowGlobalBiDiModule {
Cu.waiveXrays(arg),
lazy.setDefaultSerializationOptions(),
lazy.OwnershipModel.None,
defaultRealm
defaultRealm,
{ seenNodeIds }
)
);
}
@@ -172,6 +175,7 @@ class LogModule extends WindowGlobalBiDiModule {
text,
timestamp,
stackTrace,
_extraData: { seenNodeIds },
};
// TODO: Those steps relate to:

View File

@@ -62,7 +62,13 @@ class ScriptModule extends WindowGlobalBiDiModule {
}
}
#buildExceptionDetails(exception, stack, realm, resultOwnership) {
#buildExceptionDetails(
exception,
stack,
realm,
resultOwnership,
seenNodeIds
) {
exception = this.#toRawObject(exception);
// A stacktrace is mandatory to build exception details and a missing stack
@@ -96,7 +102,8 @@ class ScriptModule extends WindowGlobalBiDiModule {
exception,
lazy.setDefaultSerializationOptions(),
resultOwnership,
realm
realm,
{ seenNodeIds }
),
lineNumber: stack.line - 1,
stackTrace: { callFrames },
@@ -150,28 +157,37 @@ class ScriptModule extends WindowGlobalBiDiModule {
stack = rv.stack;
}
const seenNodeIds = new Map();
switch (evaluationStatus) {
case EvaluationStatus.Normal:
const dataSuccess = this.serialize(
this.#toRawObject(result),
serializationOptions,
resultOwnership,
realm,
{ seenNodeIds }
);
return {
evaluationStatus,
result: this.serialize(
this.#toRawObject(result),
serializationOptions,
resultOwnership,
realm
),
realmId: realm.id,
result: dataSuccess,
_extraData: { seenNodeIds },
};
case EvaluationStatus.Throw:
const dataThrow = this.#buildExceptionDetails(
exception,
stack,
realm,
resultOwnership,
seenNodeIds
);
return {
evaluationStatus,
exceptionDetails: this.#buildExceptionDetails(
exception,
stack,
realm,
resultOwnership
),
exceptionDetails: dataThrow,
realmId: realm.id,
_extraData: { seenNodeIds },
};
default:
throw new lazy.error.UnsupportedOperationError(
@@ -194,17 +210,20 @@ class ScriptModule extends WindowGlobalBiDiModule {
serializationOptions,
} = channelProperties;
const seenNodeIds = new Map();
const data = this.serialize(
this.#toRawObject(message),
lazy.setDefaultSerializationOptions(serializationOptions),
ownershipType,
realm
realm,
{ seenNodeIds }
);
this.emitEvent("script.message", {
channel,
data,
source: this.#getSource(realm),
_extraData: { seenNodeIds },
});
};

View File

@@ -1,5 +1,8 @@
[DEFAULT]
tags = wd
subsuite = remote
support-files =
head.js
[browser_RemoteValue.js]
[browser_RemoteValueDOM.js]

View File

@@ -3,10 +3,7 @@
"use strict";
const { NodeCache } = ChromeUtils.importESModule(
"chrome://remote/content/shared/webdriver/NodeCache.sys.mjs"
);
const { Realm, WindowRealm } = ChromeUtils.importESModule(
const { Realm } = ChromeUtils.importESModule(
"chrome://remote/content/shared/Realm.sys.mjs"
);
const { deserialize, serialize, setDefaultSerializationOptions, stringify } =
@@ -14,13 +11,6 @@ const { deserialize, serialize, setDefaultSerializationOptions, stringify } =
"chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs"
);
const browser = Services.appShell.createWindowlessBrowser(false);
const bodyEl = browser.document.body;
const domEl = browser.document.createElement("div");
bodyEl.appendChild(domEl);
const iframeEl = browser.document.createElement("iframe");
bodyEl.appendChild(iframeEl);
const PRIMITIVE_TYPES = [
{ value: undefined, serialized: { type: "undefined" } },
{ value: null, serialized: { type: "null" } },
@@ -295,59 +285,6 @@ const REMOTE_COMPLEX_VALUES = [
{ value: new Promise(() => true), serialized: { type: "promise" } },
{ value: new Int8Array(), serialized: { type: "typedarray" } },
{ value: new ArrayBuffer(), serialized: { type: "arraybuffer" } },
{
value: browser.document.querySelectorAll("div"),
serialized: {
type: "nodelist",
value: [
{
type: "node",
value: {
nodeType: 1,
localName: "div",
namespaceURI: "http://www.w3.org/1999/xhtml",
childNodeCount: 0,
attributes: {},
shadowRoot: null,
},
},
],
},
},
{
value: browser.document.getElementsByTagName("div"),
serialized: {
type: "htmlcollection",
value: [
{
type: "node",
value: {
nodeType: 1,
localName: "div",
namespaceURI: "http://www.w3.org/1999/xhtml",
childNodeCount: 0,
attributes: {},
shadowRoot: null,
},
},
],
},
},
{
value: domEl,
serialized: {
type: "node",
value: {
attributes: {},
childNodeCount: 0,
localName: "div",
namespaceURI: "http://www.w3.org/1999/xhtml",
nodeType: 1,
shadowRoot: null,
},
},
},
{ value: browser.document.defaultView, serialized: { type: "window" } },
{ value: new URL("https://example.com"), serialized: { type: "object" } },
{ value: () => true, serialized: { type: "function" } },
{ value() {}, serialized: { type: "function" } },
@@ -601,106 +538,6 @@ add_task(function test_deserializeHandleInvalidTypes() {
}
});
add_task(function test_deserializeSharedIdInvalidTypes() {
const nodeCache = new NodeCache();
const realm = new WindowRealm(browser.document.defaultView);
for (const invalidType of [false, 42, {}, []]) {
info(`Checking type: '${invalidType}'`);
const serializedValue = {
sharedId: invalidType,
};
Assert.throws(
() => deserialize(realm, serializedValue, { nodeCache }),
/InvalidArgumentError:/,
`Got expected error for type ${invalidType}`
);
}
});
add_task(function test_deserializeSharedIdInvalidValue() {
const nodeCache = new NodeCache();
const serializedValue = {
sharedId: "foo",
};
const realm = new WindowRealm(browser.document.defaultView);
Assert.throws(
() => deserialize(realm, serializedValue, { nodeCache }),
/NoSuchNodeError:/,
"Got expected error for unknown 'sharedId'"
);
});
add_task(function test_deserializeSharedId() {
const nodeCache = new NodeCache();
const domElRef = nodeCache.getOrCreateNodeReference(domEl);
const serializedValue = {
sharedId: domElRef,
};
const realm = new WindowRealm(browser.document.defaultView);
const node = deserialize(realm, serializedValue, { nodeCache });
Assert.equal(node, domEl);
});
add_task(function test_deserializeSharedIdPrecedenceOverHandle() {
const nodeCache = new NodeCache();
const domElRef = nodeCache.getOrCreateNodeReference(domEl);
const serializedValue = {
handle: "foo",
sharedId: domElRef,
};
const realm = new WindowRealm(browser.document.defaultView);
const node = deserialize(realm, serializedValue, { nodeCache });
Assert.equal(node, domEl);
});
add_task(function test_deserializeSharedIdNoWindowRealm() {
const nodeCache = new NodeCache();
const domElRef = nodeCache.getOrCreateNodeReference(domEl);
const serializedValue = {
sharedId: domElRef,
};
const realm = new Realm();
Assert.throws(
() => deserialize(realm, serializedValue, { nodeCache }),
/NoSuchNodeError/,
`Got expected error for a non-window realm`
);
});
// Bug 1819902: Instead of a browsing context check compare the origin
add_task(function test_deserializeSharedIdOtherBrowsingContext() {
const nodeCache = new NodeCache();
const domElRef = nodeCache.getOrCreateNodeReference(domEl);
const serializedValue = {
sharedId: domElRef,
};
const realm = new WindowRealm(iframeEl.contentWindow);
const node = deserialize(realm, serializedValue, { nodeCache });
Assert.equal(node, null);
});
add_task(function test_deserializePrimitiveTypesInvalidValues() {
const realm = new Realm();
@@ -1003,15 +840,15 @@ add_task(function test_serializeRemoteSimpleValues() {
});
add_task(function test_serializeRemoteComplexValues() {
const realm = new Realm();
for (const type of REMOTE_COMPLEX_VALUES) {
const { value, serialized, serializationOptions } = type;
const serializationOptionsWithDefaults =
setDefaultSerializationOptions(serializationOptions);
info(`Checking '${serialized.type}' with none ownershipType`);
const realm = new Realm();
const serializationInternalMapWithNone = new Map();
const serializedValue = serialize(
value,
serializationOptionsWithDefaults,
@@ -1049,343 +886,6 @@ add_task(function test_serializeRemoteComplexValues() {
}
});
add_task(function test_serializeNodeChildren() {
const nodeCache = new NodeCache();
// Add the used elements to the cache so that we know the unique reference.
const bodyElRef = nodeCache.getOrCreateNodeReference(bodyEl);
const domElRef = nodeCache.getOrCreateNodeReference(domEl);
const iframeElRef = nodeCache.getOrCreateNodeReference(iframeEl);
const realm = new WindowRealm(browser.document.defaultView);
const dataSet = [
{
node: bodyEl,
serializationOptions: {
maxDomDepth: null,
},
serialized: {
type: "node",
sharedId: bodyElRef,
value: {
nodeType: 1,
localName: "body",
namespaceURI: "http://www.w3.org/1999/xhtml",
childNodeCount: 2,
children: [
{
type: "node",
sharedId: domElRef,
value: {
nodeType: 1,
localName: "div",
namespaceURI: "http://www.w3.org/1999/xhtml",
childNodeCount: 0,
children: [],
attributes: {},
shadowRoot: null,
},
},
{
type: "node",
sharedId: iframeElRef,
value: {
nodeType: 1,
localName: "iframe",
namespaceURI: "http://www.w3.org/1999/xhtml",
childNodeCount: 0,
children: [],
attributes: {},
shadowRoot: null,
},
},
],
attributes: {},
shadowRoot: null,
},
},
},
{
node: bodyEl,
serializationOptions: {
maxDomDepth: 0,
},
serialized: {
type: "node",
sharedId: bodyElRef,
value: {
attributes: {},
childNodeCount: 2,
localName: "body",
namespaceURI: "http://www.w3.org/1999/xhtml",
nodeType: 1,
shadowRoot: null,
},
},
},
{
node: bodyEl,
serializationOptions: {
maxDomDepth: 1,
},
serialized: {
type: "node",
sharedId: bodyElRef,
value: {
attributes: {},
childNodeCount: 2,
children: [
{
type: "node",
sharedId: domElRef,
value: {
attributes: {},
childNodeCount: 0,
localName: "div",
namespaceURI: "http://www.w3.org/1999/xhtml",
nodeType: 1,
shadowRoot: null,
},
},
{
type: "node",
sharedId: iframeElRef,
value: {
attributes: {},
childNodeCount: 0,
localName: "iframe",
namespaceURI: "http://www.w3.org/1999/xhtml",
nodeType: 1,
shadowRoot: null,
},
},
],
localName: "body",
namespaceURI: "http://www.w3.org/1999/xhtml",
nodeType: 1,
shadowRoot: null,
},
},
},
{
node: domEl,
serializationOptions: {
maxDomDepth: 0,
},
serialized: {
type: "node",
sharedId: domElRef,
value: {
attributes: {},
childNodeCount: 0,
localName: "div",
namespaceURI: "http://www.w3.org/1999/xhtml",
nodeType: 1,
shadowRoot: null,
},
},
},
{
node: domEl,
serializationOptions: {
maxDomDepth: 1,
},
serialized: {
type: "node",
sharedId: domElRef,
value: {
attributes: {},
childNodeCount: 0,
children: [],
localName: "div",
namespaceURI: "http://www.w3.org/1999/xhtml",
nodeType: 1,
shadowRoot: null,
},
},
},
];
for (const { node, serializationOptions, serialized } of dataSet) {
const { maxDomDepth } = serializationOptions;
info(`Checking '${node.localName}' with maxDomDepth ${maxDomDepth}`);
const serializationInternalMap = new Map();
const serializedValue = serialize(
node,
serializationOptions,
"none",
serializationInternalMap,
realm,
{ nodeCache }
);
Assert.deepEqual(serializedValue, serialized, "Got expected structure");
}
});
add_task(function test_serializeShadowRoot() {
const nodeCache = new NodeCache();
const realm = new WindowRealm(browser.document.defaultView);
for (const mode of ["open", "closed"]) {
info(`Checking shadow root with mode '${mode}'`);
const customElement = browser.document.createElement(
`${mode}-custom-element`
);
const insideShadowRootElement = browser.document.createElement("input");
bodyEl.appendChild(customElement);
const shadowRoot = customElement.attachShadow({ mode });
shadowRoot.appendChild(insideShadowRootElement);
// Add the used elements to the cache so that we know the unique reference.
const customElementRef = nodeCache.getOrCreateNodeReference(customElement);
const shadowRootRef = nodeCache.getOrCreateNodeReference(shadowRoot);
const insideShadowRootElementRef = nodeCache.getOrCreateNodeReference(
insideShadowRootElement
);
const dataSet = [
{
node: customElement,
serializationOptions: {
maxDomDepth: 1,
},
serialized: {
type: "node",
sharedId: customElementRef,
value: {
attributes: {},
childNodeCount: 0,
children: [],
localName: `${mode}-custom-element`,
namespaceURI: "http://www.w3.org/1999/xhtml",
nodeType: 1,
shadowRoot: {
sharedId: shadowRootRef,
type: "node",
value: {
childNodeCount: 1,
mode,
nodeType: 11,
},
},
},
},
},
{
node: customElement,
serializationOptions: {
includeShadowTree: "open",
maxDomDepth: 1,
},
serialized: {
type: "node",
sharedId: customElementRef,
value: {
attributes: {},
childNodeCount: 0,
children: [],
localName: `${mode}-custom-element`,
namespaceURI: "http://www.w3.org/1999/xhtml",
nodeType: 1,
shadowRoot: {
sharedId: shadowRootRef,
type: "node",
value: {
childNodeCount: 1,
mode,
nodeType: 11,
...(mode === "open"
? {
children: [
{
type: "node",
sharedId: insideShadowRootElementRef,
value: {
nodeType: 1,
localName: "input",
namespaceURI: "http://www.w3.org/1999/xhtml",
childNodeCount: 0,
attributes: {},
shadowRoot: null,
},
},
],
}
: {}),
},
},
},
},
},
{
node: customElement,
serializationOptions: {
includeShadowTree: "all",
maxDomDepth: 1,
},
serialized: {
type: "node",
sharedId: customElementRef,
value: {
attributes: {},
childNodeCount: 0,
children: [],
localName: `${mode}-custom-element`,
namespaceURI: "http://www.w3.org/1999/xhtml",
nodeType: 1,
shadowRoot: {
sharedId: shadowRootRef,
type: "node",
value: {
childNodeCount: 1,
mode,
nodeType: 11,
children: [
{
type: "node",
sharedId: insideShadowRootElementRef,
value: {
nodeType: 1,
localName: "input",
namespaceURI: "http://www.w3.org/1999/xhtml",
childNodeCount: 0,
attributes: {},
shadowRoot: null,
},
},
],
},
},
},
},
},
];
for (const { node, serializationOptions, serialized } of dataSet) {
const { maxDomDepth, includeShadowTree } = serializationOptions;
info(
`Checking shadow root with maxDomDepth ${maxDomDepth} and includeShadowTree ${includeShadowTree}`
);
const serializationInternalMap = new Map();
const serializedValue = serialize(
node,
serializationOptions,
"none",
serializationInternalMap,
realm,
{ nodeCache }
);
Assert.deepEqual(serializedValue, serialized, "Got expected structure");
}
}
});
add_task(function test_serializeWithSerializationInternalMap() {
const dataSet = [
{
@@ -1505,48 +1005,6 @@ add_task(function test_serializeMultipleValuesWithSerializationInternalMap() {
);
});
add_task(function test_serializeNodeSharedId() {
const nodeCache = new NodeCache();
// Already add the domEl to the cache so that we know the unique reference.
const domElRef = nodeCache.getOrCreateNodeReference(domEl);
const realm = new WindowRealm(browser.document.defaultView);
const serializationInternalMap = new Map();
const serializedValue = serialize(
domEl,
{ maxDomDepth: 0 },
"root",
serializationInternalMap,
realm,
{ nodeCache }
);
Assert.equal(nodeCache.size, 1, "No additional reference added");
Assert.equal(serializedValue.sharedId, domElRef);
Assert.notEqual(serializedValue.handle, domElRef);
});
add_task(function test_serializeNodeSharedId_noWindowRealm() {
const nodeCache = new NodeCache();
nodeCache.getOrCreateNodeReference(domEl);
const realm = new Realm();
const serializationInternalMap = new Map();
const serializedValue = serialize(
domEl,
{ maxDomDepth: 0 },
"none",
serializationInternalMap,
realm,
{ nodeCache }
);
Assert.equal(nodeCache.size, 1, "No additional reference added");
Assert.equal(serializedValue.sharedId, undefined);
});
add_task(function test_stringify() {
const STRINGIFY_TEST_CASES = [
[undefined, "undefined"],

View File

@@ -0,0 +1,638 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/* eslint no-undef: 0 no-unused-vars: 0 */
add_task(async function test_deserializeSharedIdInvalidTypes() {
await runTestInContent(() => {
for (const invalidType of [false, 42, {}, []]) {
info(`Checking type: '${invalidType}'`);
const serializedValue = {
sharedId: invalidType,
};
Assert.throws(
() => deserialize(realm, serializedValue, { nodeCache }),
/InvalidArgumentError:/,
`Got expected error for type ${invalidType}`
);
}
});
});
add_task(async function test_deserializeSharedIdInvalidValue() {
await runTestInContent(() => {
const serializedValue = {
sharedId: "foo",
};
Assert.throws(
() => deserialize(realm, serializedValue, { nodeCache }),
/NoSuchNodeError:/,
"Got expected error for unknown 'sharedId'"
);
});
});
add_task(async function test_deserializeSharedId() {
await loadURL(inline("<div>"));
await runTestInContent(() => {
const domEl = content.document.querySelector("div");
const domElRef = nodeCache.getOrCreateNodeReference(domEl, seenNodeIds);
const serializedValue = {
sharedId: domElRef,
};
const node = deserialize(realm, serializedValue, { nodeCache });
Assert.equal(node, domEl);
});
});
add_task(async function test_deserializeSharedIdPrecedenceOverHandle() {
await loadURL(inline("<div>"));
await runTestInContent(() => {
const domEl = content.document.querySelector("div");
const domElRef = nodeCache.getOrCreateNodeReference(domEl, seenNodeIds);
const serializedValue = {
handle: "foo",
sharedId: domElRef,
};
const node = deserialize(realm, serializedValue, { nodeCache });
Assert.equal(node, domEl);
});
});
add_task(async function test_deserializeSharedIdNoWindowRealm() {
await loadURL(inline("<div>"));
await runTestInContent(() => {
const domEl = content.document.querySelector("div");
const domElRef = nodeCache.getOrCreateNodeReference(domEl, seenNodeIds);
const serializedValue = {
sharedId: domElRef,
};
Assert.throws(
() => deserialize(new Realm(), serializedValue, { nodeCache }),
/NoSuchNodeError/,
`Got expected error for a non-window realm`
);
});
});
// Bug 1819902: Instead of a browsing context check compare the origin
add_task(async function test_deserializeSharedIdOtherBrowsingContext() {
await loadURL(inline("<iframe>"));
await runTestInContent(() => {
const iframeEl = content.document.querySelector("iframe");
const domEl = iframeEl.contentWindow.document.createElement("div");
iframeEl.contentWindow.document.body.appendChild(domEl);
const domElRef = nodeCache.getOrCreateNodeReference(domEl, seenNodeIds);
const serializedValue = {
sharedId: domElRef,
};
const node = deserialize(realm, serializedValue, { nodeCache });
Assert.equal(node, null);
});
});
add_task(async function test_serializeRemoteComplexValues() {
await loadURL(inline("<div>"));
await runTestInContent(() => {
const domEl = content.document.querySelector("div");
const domElRef = nodeCache.getOrCreateNodeReference(domEl, seenNodeIds);
const REMOTE_COMPLEX_VALUES = [
{ value: content, serialized: { type: "window" } },
{
value: content.document.querySelector("div"),
serialized: {
type: "node",
sharedId: domElRef,
value: {
attributes: {},
childNodeCount: 0,
localName: "div",
namespaceURI: "http://www.w3.org/1999/xhtml",
nodeType: 1,
shadowRoot: null,
},
},
},
{
value: content.document.querySelectorAll("div"),
serialized: {
type: "nodelist",
value: [
{
type: "node",
sharedId: domElRef,
value: {
nodeType: 1,
localName: "div",
namespaceURI: "http://www.w3.org/1999/xhtml",
childNodeCount: 0,
attributes: {},
shadowRoot: null,
},
},
],
},
},
{
value: content.document.getElementsByTagName("div"),
serialized: {
type: "htmlcollection",
value: [
{
type: "node",
sharedId: domElRef,
value: {
nodeType: 1,
localName: "div",
namespaceURI: "http://www.w3.org/1999/xhtml",
childNodeCount: 0,
attributes: {},
shadowRoot: null,
},
},
],
},
},
];
for (const type of REMOTE_COMPLEX_VALUES) {
const { value, serialized } = type;
const serializationOptionsWithDefaults = setDefaultSerializationOptions();
const serializationInternalMapWithNone = new Map();
info(`Checking '${serialized.type}' with none ownershipType`);
const serializedValue = serialize(
value,
serializationOptionsWithDefaults,
"none",
serializationInternalMapWithNone,
realm,
{ nodeCache, seenNodeIds }
);
assertInternalIds(serializationInternalMapWithNone, 0);
Assert.deepEqual(serialized, serializedValue, "Got expected structure");
info(`Checking '${serialized.type}' with root ownershipType`);
const serializationInternalMapWithRoot = new Map();
const serializedWithRoot = serialize(
value,
serializationOptionsWithDefaults,
"root",
serializationInternalMapWithRoot,
realm,
{ nodeCache, seenNodeIds }
);
assertInternalIds(serializationInternalMapWithRoot, 0);
Assert.equal(
typeof serializedWithRoot.handle,
"string",
"Got a handle property"
);
Assert.deepEqual(
Object.assign({}, serialized, { handle: serializedWithRoot.handle }),
serializedWithRoot,
"Got expected structure, plus a generated handle id"
);
}
});
});
add_task(async function test_serializeNodeChildren() {
await loadURL(inline("<div></div><iframe/>"));
await runTestInContent(() => {
// Add the used elements to the cache so that we know the unique reference.
const bodyEl = content.document.body;
const domEl = bodyEl.querySelector("div");
const iframeEl = bodyEl.querySelector("iframe");
const bodyElRef = nodeCache.getOrCreateNodeReference(bodyEl, seenNodeIds);
const domElRef = nodeCache.getOrCreateNodeReference(domEl, seenNodeIds);
const iframeElRef = nodeCache.getOrCreateNodeReference(
iframeEl,
seenNodeIds
);
const dataSet = [
{
node: bodyEl,
serializationOptions: {
maxDomDepth: null,
},
serialized: {
type: "node",
sharedId: bodyElRef,
value: {
nodeType: 1,
localName: "body",
namespaceURI: "http://www.w3.org/1999/xhtml",
childNodeCount: 2,
children: [
{
type: "node",
sharedId: domElRef,
value: {
nodeType: 1,
localName: "div",
namespaceURI: "http://www.w3.org/1999/xhtml",
childNodeCount: 0,
children: [],
attributes: {},
shadowRoot: null,
},
},
{
type: "node",
sharedId: iframeElRef,
value: {
nodeType: 1,
localName: "iframe",
namespaceURI: "http://www.w3.org/1999/xhtml",
childNodeCount: 0,
children: [],
attributes: {},
shadowRoot: null,
},
},
],
attributes: {},
shadowRoot: null,
},
},
},
{
node: bodyEl,
serializationOptions: {
maxDomDepth: 0,
},
serialized: {
type: "node",
sharedId: bodyElRef,
value: {
attributes: {},
childNodeCount: 2,
localName: "body",
namespaceURI: "http://www.w3.org/1999/xhtml",
nodeType: 1,
shadowRoot: null,
},
},
},
{
node: bodyEl,
serializationOptions: {
maxDomDepth: 1,
},
serialized: {
type: "node",
sharedId: bodyElRef,
value: {
attributes: {},
childNodeCount: 2,
children: [
{
type: "node",
sharedId: domElRef,
value: {
attributes: {},
childNodeCount: 0,
localName: "div",
namespaceURI: "http://www.w3.org/1999/xhtml",
nodeType: 1,
shadowRoot: null,
},
},
{
type: "node",
sharedId: iframeElRef,
value: {
attributes: {},
childNodeCount: 0,
localName: "iframe",
namespaceURI: "http://www.w3.org/1999/xhtml",
nodeType: 1,
shadowRoot: null,
},
},
],
localName: "body",
namespaceURI: "http://www.w3.org/1999/xhtml",
nodeType: 1,
shadowRoot: null,
},
},
},
{
node: domEl,
serializationOptions: {
maxDomDepth: 0,
},
serialized: {
type: "node",
sharedId: domElRef,
value: {
attributes: {},
childNodeCount: 0,
localName: "div",
namespaceURI: "http://www.w3.org/1999/xhtml",
nodeType: 1,
shadowRoot: null,
},
},
},
{
node: domEl,
serializationOptions: {
maxDomDepth: 1,
},
serialized: {
type: "node",
sharedId: domElRef,
value: {
attributes: {},
childNodeCount: 0,
children: [],
localName: "div",
namespaceURI: "http://www.w3.org/1999/xhtml",
nodeType: 1,
shadowRoot: null,
},
},
},
];
for (const { node, serializationOptions, serialized } of dataSet) {
const { maxDomDepth } = serializationOptions;
info(`Checking '${node.localName}' with maxDomDepth ${maxDomDepth}`);
const serializationInternalMap = new Map();
const serializedValue = serialize(
node,
serializationOptions,
"none",
serializationInternalMap,
realm,
{ nodeCache, seenNodeIds }
);
Assert.deepEqual(serializedValue, serialized, "Got expected structure");
}
});
});
add_task(async function test_serializeShadowRoot() {
await runTestInContent(() => {
for (const mode of ["open", "closed"]) {
info(`Checking shadow root with mode '${mode}'`);
const customElement = content.document.createElement(
`${mode}-custom-element`
);
const insideShadowRootElement = content.document.createElement("input");
content.document.body.appendChild(customElement);
const shadowRoot = customElement.attachShadow({ mode });
shadowRoot.appendChild(insideShadowRootElement);
// Add the used elements to the cache so that we know the unique reference.
const customElementRef = nodeCache.getOrCreateNodeReference(
customElement,
seenNodeIds
);
const shadowRootRef = nodeCache.getOrCreateNodeReference(
shadowRoot,
seenNodeIds
);
const insideShadowRootElementRef = nodeCache.getOrCreateNodeReference(
insideShadowRootElement,
seenNodeIds
);
const dataSet = [
{
node: customElement,
serializationOptions: {
maxDomDepth: 1,
},
serialized: {
type: "node",
sharedId: customElementRef,
value: {
attributes: {},
childNodeCount: 0,
children: [],
localName: `${mode}-custom-element`,
namespaceURI: "http://www.w3.org/1999/xhtml",
nodeType: 1,
shadowRoot: {
sharedId: shadowRootRef,
type: "node",
value: {
childNodeCount: 1,
mode,
nodeType: 11,
},
},
},
},
},
{
node: customElement,
serializationOptions: {
includeShadowTree: "open",
maxDomDepth: 1,
},
serialized: {
type: "node",
sharedId: customElementRef,
value: {
attributes: {},
childNodeCount: 0,
children: [],
localName: `${mode}-custom-element`,
namespaceURI: "http://www.w3.org/1999/xhtml",
nodeType: 1,
shadowRoot: {
sharedId: shadowRootRef,
type: "node",
value: {
childNodeCount: 1,
mode,
nodeType: 11,
...(mode === "open"
? {
children: [
{
type: "node",
sharedId: insideShadowRootElementRef,
value: {
nodeType: 1,
localName: "input",
namespaceURI: "http://www.w3.org/1999/xhtml",
childNodeCount: 0,
attributes: {},
shadowRoot: null,
},
},
],
}
: {}),
},
},
},
},
},
{
node: customElement,
serializationOptions: {
includeShadowTree: "all",
maxDomDepth: 1,
},
serialized: {
type: "node",
sharedId: customElementRef,
value: {
attributes: {},
childNodeCount: 0,
children: [],
localName: `${mode}-custom-element`,
namespaceURI: "http://www.w3.org/1999/xhtml",
nodeType: 1,
shadowRoot: {
sharedId: shadowRootRef,
type: "node",
value: {
childNodeCount: 1,
mode,
nodeType: 11,
children: [
{
type: "node",
sharedId: insideShadowRootElementRef,
value: {
nodeType: 1,
localName: "input",
namespaceURI: "http://www.w3.org/1999/xhtml",
childNodeCount: 0,
attributes: {},
shadowRoot: null,
},
},
],
},
},
},
},
},
];
for (const { node, serializationOptions, serialized } of dataSet) {
const { maxDomDepth, includeShadowTree } = serializationOptions;
info(
`Checking shadow root with maxDomDepth ${maxDomDepth} and includeShadowTree ${includeShadowTree}`
);
const serializationInternalMap = new Map();
const serializedValue = serialize(
node,
serializationOptions,
"none",
serializationInternalMap,
realm,
{ nodeCache }
);
Assert.deepEqual(serializedValue, serialized, "Got expected structure");
}
}
});
});
add_task(async function test_serializeNodeSharedId() {
await loadURL(inline("<div>"));
await runTestInContent(() => {
const domEl = content.document.querySelector("div");
// Already add the domEl to the cache so that we know the unique reference.
const domElRef = nodeCache.getOrCreateNodeReference(domEl, seenNodeIds);
const serializedValue = serialize(
domEl,
{ maxDomDepth: 0 },
"root",
serializationInternalMap,
realm,
{ nodeCache, seenNodeIds }
);
Assert.equal(nodeCache.size, 1, "No additional reference added");
Assert.equal(serializedValue.sharedId, domElRef);
Assert.notEqual(serializedValue.handle, domElRef);
});
});
function runTestInContent(callback) {
return SpecialPowers.spawn(
gBrowser.selectedBrowser,
[callback.toString()],
async callback => {
const { NodeCache } = ChromeUtils.importESModule(
"chrome://remote/content/shared/webdriver/NodeCache.sys.mjs"
);
const { Realm, WindowRealm } = ChromeUtils.importESModule(
"chrome://remote/content/shared/Realm.sys.mjs"
);
const { deserialize, serialize, setDefaultSerializationOptions } =
ChromeUtils.importESModule(
"chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs"
);
function assertInternalIds(serializationInternalMap, amount) {
const remoteValuesWithInternalIds = Array.from(
serializationInternalMap.values()
).filter(remoteValue => !!remoteValue.internalId);
Assert.equal(
remoteValuesWithInternalIds.length,
amount,
"Got expected amount of internalIds in serializationInternalMap"
);
}
const nodeCache = new NodeCache();
const seenNodeIds = new Map();
const realm = new WindowRealm(content);
const serializationInternalMap = new Map();
// eslint-disable-next-line no-eval
eval(`(${callback})()`);
}
);
}

View File

@@ -0,0 +1,28 @@
/**
* Load a given URL in the currently selected tab
*/
async function loadURL(url, expectedURL = undefined) {
expectedURL = expectedURL || url;
const browser = gBrowser.selectedTab.linkedBrowser;
const loaded = BrowserTestUtils.browserLoaded(browser, true, expectedURL);
BrowserTestUtils.loadURIString(browser, url);
await loaded;
}
/** Creates an inline URL for the given source document. */
function inline(src, doctype = "html") {
let doc;
switch (doctype) {
case "html":
doc = `<!doctype html>\n<meta charset=utf-8>\n${src}`;
break;
default:
throw new Error("Unexpected doctype: " + doctype);
}
return `https://example.com/document-builder.sjs?html=${encodeURIComponent(
doc
)}`;
}