summaryrefslogtreecommitdiffstats
path: root/remote/webdriver-bidi/modules
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:13:27 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:13:27 +0000
commit40a355a42d4a9444dc753c04c6608dade2f06a23 (patch)
tree871fc667d2de662f171103ce5ec067014ef85e61 /remote/webdriver-bidi/modules
parentAdding upstream version 124.0.1. (diff)
downloadfirefox-40a355a42d4a9444dc753c04c6608dade2f06a23.tar.xz
firefox-40a355a42d4a9444dc753c04c6608dade2f06a23.zip
Adding upstream version 125.0.1.upstream/125.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'remote/webdriver-bidi/modules')
-rw-r--r--remote/webdriver-bidi/modules/root/browsingContext.sys.mjs80
-rw-r--r--remote/webdriver-bidi/modules/root/input.sys.mjs57
-rw-r--r--remote/webdriver-bidi/modules/root/storage.sys.mjs189
-rw-r--r--remote/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs24
-rw-r--r--remote/webdriver-bidi/modules/windowglobal/input.sys.mjs71
-rw-r--r--remote/webdriver-bidi/modules/windowglobal/script.sys.mjs2
6 files changed, 356 insertions, 67 deletions
diff --git a/remote/webdriver-bidi/modules/root/browsingContext.sys.mjs b/remote/webdriver-bidi/modules/root/browsingContext.sys.mjs
index bc600e89cd..f2a5d5e645 100644
--- a/remote/webdriver-bidi/modules/root/browsingContext.sys.mjs
+++ b/remote/webdriver-bidi/modules/root/browsingContext.sys.mjs
@@ -232,11 +232,27 @@ class BrowsingContextModule extends Module {
);
}
- const tab = lazy.TabManager.getTabForBrowsingContext(context);
- const window = lazy.TabManager.getWindowForTab(tab);
+ const targetTab = lazy.TabManager.getTabForBrowsingContext(context);
+ const targetWindow = lazy.TabManager.getWindowForTab(targetTab);
+ const selectedTab = lazy.TabManager.getTabBrowser(targetWindow).selectedTab;
+
+ const activated = [
+ lazy.windowManager.focusWindow(targetWindow),
+ lazy.TabManager.selectTab(targetTab),
+ ];
- await lazy.windowManager.focusWindow(window);
- await lazy.TabManager.selectTab(tab);
+ if (targetTab !== selectedTab && !lazy.AppInfo.isAndroid) {
+ // We need to wait until the "document.visibilityState" of the currently
+ // selected tab in the target window is marked as "hidden".
+ //
+ // Bug 1884142: It's not supported on Android for the TestRunner package.
+ const selectedBrowser = lazy.TabManager.getBrowserForTab(selectedTab);
+ activated.push(
+ this.#waitForVisibilityChange(selectedBrowser.browsingContext)
+ );
+ }
+
+ await Promise.all(activated);
}
/**
@@ -532,33 +548,58 @@ class BrowsingContextModule extends Module {
const type = lazy.AppInfo.isAndroid ? "tab" : typeHint;
switch (type) {
- case "window":
+ case "window": {
const newWindow = await lazy.windowManager.openBrowserWindow({
focus: !background,
userContextId: userContext,
});
browser = lazy.TabManager.getTabBrowser(newWindow).selectedBrowser;
break;
-
- case "tab":
+ }
+ case "tab": {
if (!lazy.TabManager.supportsTabs()) {
throw new lazy.error.UnsupportedOperationError(
`browsingContext.create with type "tab" not supported in ${lazy.AppInfo.name}`
);
}
+ // The window to open the new tab in.
+ let window = Services.wm.getMostRecentWindow(null);
+
let referenceTab;
if (referenceContext !== null) {
referenceTab =
lazy.TabManager.getTabForBrowsingContext(referenceContext);
+ window = lazy.TabManager.getWindowForTab(referenceTab);
}
- const tab = await lazy.TabManager.addTab({
- focus: !background,
- referenceTab,
- userContextId: userContext,
- });
+ const promises = [];
+
+ if (!background && !lazy.AppInfo.isAndroid) {
+ // When opening a new foreground tab we need to wait until the
+ // "document.visibilityState" of the currently selected tab in this
+ // window is marked as "hidden".
+ //
+ // Bug 1884142: It's not supported on Android for the TestRunner package.
+ const selectedTab = lazy.TabManager.getTabBrowser(window).selectedTab;
+ promises.push(
+ this.#waitForVisibilityChange(
+ lazy.TabManager.getBrowserForTab(selectedTab).browsingContext
+ )
+ );
+ }
+
+ promises.unshift(
+ lazy.TabManager.addTab({
+ focus: !background,
+ referenceTab,
+ userContextId: userContext,
+ })
+ );
+
+ const [tab] = await Promise.all(promises);
browser = lazy.TabManager.getBrowserForTab(tab);
+ }
}
await lazy.waitForInitialNavigationCompleted(
@@ -1918,6 +1959,21 @@ class BrowsingContextModule extends Module {
}
}
+ #waitForVisibilityChange(browsingContext) {
+ return this.messageHandler.forwardCommand({
+ moduleName: "browsingContext",
+ commandName: "_awaitVisibilityState",
+ destination: {
+ type: lazy.WindowGlobalMessageHandler.type,
+ id: browsingContext.id,
+ },
+ params: {
+ value: "hidden",
+ },
+ retryOnAbort: true,
+ });
+ }
+
/**
* Internal commands
*/
diff --git a/remote/webdriver-bidi/modules/root/input.sys.mjs b/remote/webdriver-bidi/modules/root/input.sys.mjs
index 8edd8299b7..0764ac3f92 100644
--- a/remote/webdriver-bidi/modules/root/input.sys.mjs
+++ b/remote/webdriver-bidi/modules/root/input.sys.mjs
@@ -91,6 +91,63 @@ class InputModule extends Module {
return {};
}
+ /**
+ * Sets the file property of a given input element with type file to a set of file paths.
+ *
+ * @param {object=} options
+ * @param {string} options.context
+ * Id of the browsing context to set the file property
+ * of a given input element.
+ * @param {SharedReference} options.element
+ * A reference to a node, which is used as
+ * a target for setting files.
+ * @param {Array<string>} options.files
+ * A list of file paths which should be set.
+ *
+ * @throws {InvalidArgumentError}
+ * Raised if an argument is of an invalid type or value.
+ * @throws {NoSuchElementError}
+ * If the input element cannot be found.
+ * @throws {NoSuchFrameError}
+ * If the browsing context cannot be found.
+ * @throws {UnableToSetFileInputError}
+ * If the set of file paths was not set to the input element.
+ */
+ async setFiles(options = {}) {
+ const { context: contextId, element, files } = options;
+
+ lazy.assert.string(
+ contextId,
+ `Expected "context" to be a string, got ${contextId}`
+ );
+
+ const context = lazy.TabManager.getBrowsingContextById(contextId);
+ if (!context) {
+ throw new lazy.error.NoSuchFrameError(
+ `Browsing context with id ${contextId} not found`
+ );
+ }
+
+ lazy.assert.array(files, `Expected "files" to be an array, got ${files}`);
+
+ for (const file of files) {
+ lazy.assert.string(
+ file,
+ `Expected an element of "files" to be a string, got ${file}`
+ );
+ }
+
+ await this.messageHandler.forwardCommand({
+ moduleName: "input",
+ commandName: "setFiles",
+ destination: {
+ type: lazy.WindowGlobalMessageHandler.type,
+ id: context.id,
+ },
+ params: { element, files },
+ });
+ }
+
static get supportedEvents() {
return [];
}
diff --git a/remote/webdriver-bidi/modules/root/storage.sys.mjs b/remote/webdriver-bidi/modules/root/storage.sys.mjs
index 50fbd8ecd6..3eced2da4c 100644
--- a/remote/webdriver-bidi/modules/root/storage.sys.mjs
+++ b/remote/webdriver-bidi/modules/root/storage.sys.mjs
@@ -12,6 +12,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
"chrome://remote/content/webdriver-bidi/modules/root/network.sys.mjs",
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
+ UserContextManager:
+ "chrome://remote/content/shared/UserContextManager.sys.mjs",
});
const CookieFieldsMapping = {
@@ -96,14 +98,14 @@ class StorageModule extends Module {
*
* @property {PartitionType} [type=PartitionType.storageKey]
* @property {string=} sourceOrigin
- * @property {string=} userContext (not supported)
+ * @property {string=} userContext
*/
/**
* @typedef PartitionKey
*
* @property {string=} sourceOrigin
- * @property {string=} userContext (not supported)
+ * @property {string=} userContext
*/
/**
@@ -119,6 +121,48 @@ class StorageModule extends Module {
*/
/**
+ * Remove zero or more cookies which match a set of provided parameters.
+ *
+ * @param {object=} options
+ * @param {CookieFilter=} options.filter
+ * An object which holds field names and values, which
+ * should be used to filter the output of the command.
+ * @param {PartitionDescriptor=} options.partition
+ * An object which holds the information which
+ * should be used to build a partition key.
+ *
+ * @returns {PartitionKey}
+ * An object with the partition key which was used to
+ * retrieve cookies which had to be removed.
+ * @throws {InvalidArgumentError}
+ * If the provided arguments are not valid.
+ * @throws {NoSuchFrameError}
+ * If the provided browsing context cannot be found.
+ */
+ async deleteCookies(options = {}) {
+ let { filter = {} } = options;
+ const { partition: partitionSpec = null } = options;
+
+ this.#assertPartition(partitionSpec);
+ filter = this.#assertCookieFilter(filter);
+
+ const partitionKey = this.#expandStoragePartitionSpec(partitionSpec);
+ const store = this.#getTheCookieStore(partitionKey);
+ const cookies = this.#getMatchingCookies(store, filter);
+
+ for (const cookie of cookies) {
+ Services.cookies.remove(
+ cookie.host,
+ cookie.name,
+ cookie.path,
+ cookie.originAttributes
+ );
+ }
+
+ return { partitionKey: this.#formatPartitionKey(partitionKey) };
+ }
+
+ /**
* Retrieve zero or more cookies which match a set of provided parameters.
*
* @param {object=} options
@@ -136,27 +180,27 @@ class StorageModule extends Module {
* If the provided arguments are not valid.
* @throws {NoSuchFrameError}
* If the provided browsing context cannot be found.
- * @throws {UnsupportedOperationError}
- * Raised when the command is called with `userContext` as
- * in `partition` argument.
*/
async getCookies(options = {}) {
let { filter = {} } = options;
const { partition: partitionSpec = null } = options;
this.#assertPartition(partitionSpec);
- filter = this.#assertGetCookieFilter(filter);
+ filter = this.#assertCookieFilter(filter);
const partitionKey = this.#expandStoragePartitionSpec(partitionSpec);
const store = this.#getTheCookieStore(partitionKey);
const cookies = this.#getMatchingCookies(store, filter);
+ const serializedCookies = [];
- // Bug 1875255. Exchange platform id for Webdriver BiDi id for the user context to return it to the client.
- // For now we use platform user context id for returning cookies for a specific browsing context in the platform API,
- // but we can not return it directly to the client, so for now we just remove it from the response.
- delete partitionKey.userContext;
+ for (const cookie of cookies) {
+ serializedCookies.push(this.#serializeCookie(cookie));
+ }
- return { cookies, partitionKey };
+ return {
+ cookies: serializedCookies,
+ partitionKey: this.#formatPartitionKey(partitionKey),
+ };
}
/**
@@ -195,9 +239,6 @@ class StorageModule extends Module {
* If the provided browsing context cannot be found.
* @throws {UnableToSetCookieError}
* If the cookie was not added.
- * @throws {UnsupportedOperationError}
- * Raised when the command is called with `userContext` as
- * in `partition` argument.
*/
async setCookie(options = {}) {
const { cookie: cookieSpec, partition: partitionSpec = null } = options;
@@ -264,12 +305,7 @@ class StorageModule extends Module {
throw new lazy.error.UnableToSetCookieError(e);
}
- // Bug 1875255. Exchange platform id for Webdriver BiDi id for the user context to return it to the client.
- // For now we use platform user context id for returning cookies for a specific browsing context in the platform API,
- // but we can not return it directly to the client, so for now we just remove it from the response.
- delete partitionKey.userContext;
-
- return { partitionKey };
+ return { partitionKey: this.#formatPartitionKey(partitionKey) };
}
#assertCookie(cookie) {
@@ -318,7 +354,7 @@ class StorageModule extends Module {
}
}
- #assertGetCookieFilter(filter) {
+ #assertCookieFilter(filter) {
lazy.assert.object(
filter,
`Expected "filter" to be an object, got ${filter}`
@@ -454,10 +490,11 @@ class StorageModule extends Module {
`Expected "partition.userContext" to be a string, got ${userContext}`
);
- // TODO: Bug 1875255. Implement support for "userContext" field.
- throw new lazy.error.UnsupportedOperationError(
- `"userContext" as a field on "partition" argument is not supported yet for "storage.getCookies" command`
- );
+ if (!lazy.UserContextManager.hasUserContextId(userContext)) {
+ throw new lazy.error.NoSuchUserContextError(
+ `User Context with id ${userContext} was not found`
+ );
+ }
}
break;
}
@@ -505,6 +542,40 @@ class StorageModule extends Module {
}
/**
+ * Deserialize filter.
+ *
+ * @see https://w3c.github.io/webdriver-bidi/#deserialize-filter
+ */
+ #deserializeFilter(filter) {
+ const deserializedFilter = {};
+ for (const [fieldName, value] of Object.entries(filter)) {
+ if (value === null) {
+ continue;
+ }
+
+ const deserializedName = CookieFieldsMapping[fieldName];
+ let deserializedValue;
+
+ switch (deserializedName) {
+ case "sameSite":
+ deserializedValue = this.#getSameSitePlatformProperty(value);
+ break;
+
+ case "value":
+ deserializedValue = this.#deserializeProtocolBytes(value);
+ break;
+
+ default:
+ deserializedValue = value;
+ }
+
+ deserializedFilter[deserializedName] = deserializedValue;
+ }
+
+ return deserializedFilter;
+ }
+
+ /**
* Deserialize the value to string, since platform API
* returns cookie's value as a string.
*/
@@ -544,7 +615,14 @@ class StorageModule extends Module {
const partitionKey = {};
for (const keyName of PartitionKeyAttributes) {
if (keyName in partitionSpec) {
- partitionKey[keyName] = partitionSpec[keyName];
+ // Retrieve a platform user context id.
+ if (keyName === "userContext") {
+ partitionKey[keyName] = lazy.UserContextManager.getInternalIdById(
+ partitionSpec.userContext
+ );
+ } else {
+ partitionKey[keyName] = partitionSpec[keyName];
+ }
}
}
@@ -552,6 +630,20 @@ class StorageModule extends Module {
}
/**
+ * Prepare the partition key in the right format for returning to a client.
+ */
+ #formatPartitionKey(partitionKey) {
+ if ("userContext" in partitionKey) {
+ // Exchange platform id for Webdriver BiDi id for the user context to return it to the client.
+ partitionKey.userContext = lazy.UserContextManager.getIdByInternalId(
+ partitionKey.userContext
+ );
+ }
+
+ return partitionKey;
+ }
+
+ /**
* Retrieves a browsing context based on its id.
*
* @param {number} contextId
@@ -595,11 +687,11 @@ class StorageModule extends Module {
*/
#getMatchingCookies(cookieStore, filter) {
const cookies = [];
+ const deserializedFilter = this.#deserializeFilter(filter);
for (const storedCookie of cookieStore) {
- const serializedCookie = this.#serializeCookie(storedCookie);
- if (this.#matchCookie(serializedCookie, filter)) {
- cookies.push(serializedCookie);
+ if (this.#matchCookie(storedCookie, deserializedFilter)) {
+ cookies.push(storedCookie);
}
}
return cookies;
@@ -649,18 +741,14 @@ class StorageModule extends Module {
// Prepare the data in the format required for the platform API.
const originAttributes = this.#getOriginAttributes(storagePartitionKey);
- // In case we want to get the cookies for a certain `sourceOrigin`,
- // we have to additionally specify `hostname`. When `sourceOrigin` is not present
- // `hostname` will stay equal undefined.
- let hostname;
// In case we want to get the cookies for a certain `sourceOrigin`,
// we have to separately retrieve cookies for a hostname built from `sourceOrigin`,
// and with `partitionKey` equal an empty string to retrieve the cookies that which were set
- // by this hostname but without `partitionKey`, e.g. with `document.cookie`
+ // by this hostname but without `partitionKey`, e.g. with `document.cookie`.
if (storagePartitionKey.sourceOrigin) {
const url = new URL(storagePartitionKey.sourceOrigin);
- hostname = url.hostname;
+ const hostname = url.hostname;
const principal = Services.scriptSecurityManager.createContentPrincipal(
Services.io.newURI(url),
@@ -688,8 +776,7 @@ class StorageModule extends Module {
// Add the cookies which exactly match a built partition attributes.
store = store.concat(
Services.cookies.getCookiesWithOriginAttributes(
- JSON.stringify(originAttributes),
- hostname
+ JSON.stringify(originAttributes)
)
);
@@ -702,19 +789,23 @@ class StorageModule extends Module {
* @see https://w3c.github.io/webdriver-bidi/#match-cookie
*/
#matchCookie(storedCookie, filter) {
- for (const [fieldName] of Object.entries(CookieFieldsMapping)) {
- let value = filter[fieldName];
- if (value !== null) {
- let storedCookieValue = storedCookie[fieldName];
-
- if (fieldName === "value") {
- value = this.#deserializeProtocolBytes(value);
- storedCookieValue = this.#deserializeProtocolBytes(storedCookieValue);
- }
+ for (const [fieldName, value] of Object.entries(filter)) {
+ // Since we set `null` to not specified values, we have to check for `null` here
+ // and not match on these values.
+ if (value === null) {
+ continue;
+ }
- if (storedCookieValue !== value) {
- return false;
- }
+ let storedCookieValue = storedCookie[fieldName];
+
+ // The platform represantation of cookie doesn't contain a size field,
+ // so we have to calculate it to match.
+ if (fieldName === "size") {
+ storedCookieValue = this.#getCookieSize(storedCookie);
+ }
+
+ if (storedCookieValue !== value) {
+ return false;
}
}
diff --git a/remote/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs b/remote/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs
index 8421445d2c..adf821601d 100644
--- a/remote/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs
+++ b/remote/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs
@@ -17,6 +17,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
"chrome://remote/content/webdriver-bidi/modules/root/browsingContext.sys.mjs",
OriginType:
"chrome://remote/content/webdriver-bidi/modules/root/browsingContext.sys.mjs",
+ PollPromise: "chrome://remote/content/shared/Sync.sys.mjs",
});
const DOCUMENT_FRAGMENT_NODE = 11;
@@ -356,6 +357,29 @@ class BrowsingContextModule extends WindowGlobalBiDiModule {
});
}
+ /**
+ * Waits until the visibility state of the document has the expected value.
+ *
+ * @param {object} options
+ * @param {number} options.value
+ * Expected value of the visibility state.
+ *
+ * @returns {Promise}
+ * Promise that resolves when the visibility state has the expected value.
+ */
+ async _awaitVisibilityState(options) {
+ const { value } = options;
+ const win = this.messageHandler.window;
+
+ await lazy.PollPromise((resolve, reject) => {
+ if (win.document.visibilityState === value) {
+ resolve();
+ } else {
+ reject();
+ }
+ });
+ }
+
_getBaseURL() {
return this.messageHandler.window.document.baseURI;
}
diff --git a/remote/webdriver-bidi/modules/windowglobal/input.sys.mjs b/remote/webdriver-bidi/modules/windowglobal/input.sys.mjs
index 099cf53d46..b91cce2310 100644
--- a/remote/webdriver-bidi/modules/windowglobal/input.sys.mjs
+++ b/remote/webdriver-bidi/modules/windowglobal/input.sys.mjs
@@ -10,6 +10,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
action: "chrome://remote/content/shared/webdriver/Actions.sys.mjs",
dom: "chrome://remote/content/shared/DOM.sys.mjs",
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
+ event: "chrome://remote/content/shared/webdriver/Event.sys.mjs",
});
class InputModule extends WindowGlobalBiDiModule {
@@ -43,6 +44,67 @@ class InputModule extends WindowGlobalBiDiModule {
this.#actionState = null;
}
+ async setFiles(options) {
+ const { element: sharedReference, files } = options;
+
+ const element = await this.#deserializeElementSharedReference(
+ sharedReference
+ );
+
+ if (
+ !HTMLInputElement.isInstance(element) ||
+ element.type !== "file" ||
+ element.disabled
+ ) {
+ throw new lazy.error.UnableToSetFileInputError(
+ `Element needs to be an <input> element with type "file" and not disabled`
+ );
+ }
+
+ if (files.length > 1 && !element.hasAttribute("multiple")) {
+ throw new lazy.error.UnableToSetFileInputError(
+ `Element should have an attribute "multiple" set when trying to set more than 1 file`
+ );
+ }
+
+ const fileObjects = [];
+ for (const file of files) {
+ try {
+ fileObjects.push(await File.createFromFileName(file));
+ } catch (e) {
+ throw new lazy.error.InvalidArgumentError(
+ `Failed to add file ${file} (${e})`
+ );
+ }
+ }
+
+ const selectedFiles = Array.from(element.files);
+
+ const intersection = fileObjects.filter(fileObject =>
+ selectedFiles.some(
+ selectedFile =>
+ // Compare file fields to identify if the files are equal.
+ // TODO: Bug 1883856. Add check for full path or use a different way
+ // to compare files when it's available.
+ selectedFile.name === fileObject.name &&
+ selectedFile.size === fileObject.size &&
+ selectedFile.type === fileObject.type
+ )
+ );
+
+ if (
+ intersection.length === selectedFiles.length &&
+ selectedFiles.length === fileObjects.length
+ ) {
+ lazy.event.cancel(element);
+ } else {
+ element.mozSetFileArray(fileObjects);
+
+ lazy.event.input(element);
+ lazy.event.change(element);
+ }
+ }
+
/**
* In the provided array of input.SourceActions, replace all origins matching
* the input.ElementOrigin production with the Element corresponding to this
@@ -75,8 +137,8 @@ class InputModule extends WindowGlobalBiDiModule {
if (action?.origin?.type === "element") {
promises.push(
(async () => {
- action.origin = await this.#getElementFromElementOrigin(
- action.origin
+ action.origin = await this.#deserializeElementSharedReference(
+ action.origin.element
);
})()
);
@@ -87,11 +149,10 @@ class InputModule extends WindowGlobalBiDiModule {
return Promise.all(promises);
}
- async #getElementFromElementOrigin(origin) {
- const sharedReference = origin.element;
+ async #deserializeElementSharedReference(sharedReference) {
if (typeof sharedReference?.sharedId !== "string") {
throw new lazy.error.InvalidArgumentError(
- `Expected "origin.element" to be a SharedReference, got: ${sharedReference}`
+ `Expected "element" to be a SharedReference, got: ${sharedReference}`
);
}
diff --git a/remote/webdriver-bidi/modules/windowglobal/script.sys.mjs b/remote/webdriver-bidi/modules/windowglobal/script.sys.mjs
index e0f9542bdd..88d58f8064 100644
--- a/remote/webdriver-bidi/modules/windowglobal/script.sys.mjs
+++ b/remote/webdriver-bidi/modules/windowglobal/script.sys.mjs
@@ -290,7 +290,7 @@ class ScriptModule extends WindowGlobalBiDiModule {
const rawObject = maybeDebuggerObject.unsafeDereference();
// TODO: Getters for Maps and Sets iterators return "Opaque" objects and
- // are not iterable. RemoteValue.jsm' serializer should handle calling
+ // are not iterable. RemoteValue.sys.mjs' serializer should handle calling
// waiveXrays on Maps/Sets/... and then unwaiveXrays on entries but since
// we serialize with maxDepth=1, calling waiveXrays once on the root
// object allows to return correctly serialized values.