diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /remote/webdriver-bidi/test | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
6 files changed, 2026 insertions, 0 deletions
diff --git a/remote/webdriver-bidi/test/browser/browser.toml b/remote/webdriver-bidi/test/browser/browser.toml new file mode 100644 index 0000000000..21e54bf538 --- /dev/null +++ b/remote/webdriver-bidi/test/browser/browser.toml @@ -0,0 +1,8 @@ +[DEFAULT] +tags = "wd" +subsuite = "remote" +support-files = ["head.js"] + +["browser_RemoteValue.js"] + +["browser_RemoteValueDOM.js"] diff --git a/remote/webdriver-bidi/test/browser/browser_RemoteValue.js b/remote/webdriver-bidi/test/browser/browser_RemoteValue.js new file mode 100644 index 0000000000..b95636037b --- /dev/null +++ b/remote/webdriver-bidi/test/browser/browser_RemoteValue.js @@ -0,0 +1,1117 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { Realm } = ChromeUtils.importESModule( + "chrome://remote/content/shared/Realm.sys.mjs" +); +const { deserialize, serialize, setDefaultSerializationOptions, stringify } = + ChromeUtils.importESModule( + "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs" + ); + +const PRIMITIVE_TYPES = [ + { value: undefined, serialized: { type: "undefined" } }, + { value: null, serialized: { type: "null" } }, + { value: "foo", serialized: { type: "string", value: "foo" } }, + { value: Number.NaN, serialized: { type: "number", value: "NaN" } }, + { value: -0, serialized: { type: "number", value: "-0" } }, + { + value: Number.POSITIVE_INFINITY, + serialized: { type: "number", value: "Infinity" }, + }, + { + value: Number.NEGATIVE_INFINITY, + serialized: { type: "number", value: "-Infinity" }, + }, + { value: 42, serialized: { type: "number", value: 42 } }, + { value: false, serialized: { type: "boolean", value: false } }, + { value: 42n, serialized: { type: "bigint", value: "42" } }, +]; + +const REMOTE_SIMPLE_VALUES = [ + { + value: new RegExp(/foo/), + serialized: { + type: "regexp", + value: { + pattern: "foo", + flags: "", + }, + }, + deserializable: true, + }, + { + value: new RegExp(/foo/g), + serialized: { + type: "regexp", + value: { + pattern: "foo", + flags: "g", + }, + }, + deserializable: true, + }, + { + value: new Date(1654004849000), + serialized: { + type: "date", + value: "2022-05-31T13:47:29.000Z", + }, + deserializable: true, + }, +]; + +const REMOTE_COMPLEX_VALUES = [ + { value: Symbol("foo"), serialized: { type: "symbol" } }, + { + value: [1], + serialized: { + type: "array", + value: [{ type: "number", value: 1 }], + }, + }, + { + value: [1], + serializationOptions: { + maxObjectDepth: 0, + }, + serialized: { + type: "array", + }, + }, + { + value: [1, "2", true, new RegExp(/foo/g)], + serializationOptions: { + maxObjectDepth: 1, + }, + serialized: { + type: "array", + value: [ + { type: "number", value: 1 }, + { type: "string", value: "2" }, + { type: "boolean", value: true }, + { + type: "regexp", + value: { + pattern: "foo", + flags: "g", + }, + }, + ], + }, + deserializable: true, + }, + { + value: [1, [3, "4"]], + serializationOptions: { + maxObjectDepth: 1, + }, + serialized: { + type: "array", + value: [{ type: "number", value: 1 }, { type: "array" }], + }, + }, + { + value: [1, [3, "4"]], + serializationOptions: { + maxObjectDepth: 2, + }, + serialized: { + type: "array", + value: [ + { type: "number", value: 1 }, + { + type: "array", + value: [ + { type: "number", value: 3 }, + { type: "string", value: "4" }, + ], + }, + ], + }, + deserializable: true, + }, + { + value: new Map(), + serializationOptions: { + maxObjectDepth: 1, + }, + serialized: { + type: "map", + value: [], + }, + deserializable: true, + }, + { + value: new Map([]), + serializationOptions: { + maxObjectDepth: 1, + }, + serialized: { + type: "map", + value: [], + }, + deserializable: true, + }, + { + value: new Map([ + [1, 2], + ["2", "3"], + [true, false], + ]), + serialized: { + type: "map", + value: [ + [ + { type: "number", value: 1 }, + { type: "number", value: 2 }, + ], + ["2", { type: "string", value: "3" }], + [ + { type: "boolean", value: true }, + { type: "boolean", value: false }, + ], + ], + }, + }, + { + value: new Map([ + [1, 2], + ["2", "3"], + [true, false], + ]), + serializationOptions: { + maxObjectDepth: 0, + }, + serialized: { + type: "map", + }, + }, + { + value: new Map([ + [1, 2], + ["2", "3"], + [true, false], + ]), + serializationOptions: { + maxObjectDepth: 1, + }, + serialized: { + type: "map", + value: [ + [ + { type: "number", value: 1 }, + { type: "number", value: 2 }, + ], + ["2", { type: "string", value: "3" }], + [ + { type: "boolean", value: true }, + { type: "boolean", value: false }, + ], + ], + }, + deserializable: true, + }, + { + value: new Set(), + serializationOptions: { + maxObjectDepth: 1, + }, + serialized: { + type: "set", + value: [], + }, + deserializable: true, + }, + { + value: new Set([]), + serializationOptions: { + maxObjectDepth: 1, + }, + serialized: { + type: "set", + value: [], + }, + deserializable: true, + }, + { + value: new Set([1, "2", true]), + serialized: { + type: "set", + value: [ + { type: "number", value: 1 }, + { type: "string", value: "2" }, + { type: "boolean", value: true }, + ], + }, + }, + { + value: new Set([1, "2", true]), + serializationOptions: { + maxObjectDepth: 0, + }, + serialized: { + type: "set", + }, + }, + { + value: new Set([1, "2", true]), + serializationOptions: { + maxObjectDepth: 1, + }, + serialized: { + type: "set", + value: [ + { type: "number", value: 1 }, + { type: "string", value: "2" }, + { type: "boolean", value: true }, + ], + }, + deserializable: true, + }, + { value: new WeakMap([[{}, 1]]), serialized: { type: "weakmap" } }, + { value: new WeakSet([{}]), serialized: { type: "weakset" } }, + { + value: (function* () { + yield "a"; + })(), + serialized: { type: "generator" }, + }, + { + value: (async function* () { + yield await Promise.resolve(1); + })(), + serialized: { type: "generator" }, + }, + { value: new Error("error message"), serialized: { type: "error" } }, + { + value: new SyntaxError("syntax error message"), + serialized: { type: "error" }, + }, + { + value: new TypeError("type error message"), + serialized: { type: "error" }, + }, + { value: new Proxy({}, {}), serialized: { type: "proxy" } }, + { value: new Promise(() => true), serialized: { type: "promise" } }, + { value: new Int8Array(), serialized: { type: "typedarray" } }, + { value: new ArrayBuffer(), serialized: { type: "arraybuffer" } }, + { value: new URL("https://example.com"), serialized: { type: "object" } }, + { value: () => true, serialized: { type: "function" } }, + { value() {}, serialized: { type: "function" } }, + { + value: {}, + serializationOptions: { + maxObjectDepth: 1, + }, + serialized: { + type: "object", + value: [], + }, + deserializable: true, + }, + { + value: { + 1: 1, + 2: "2", + foo: true, + }, + serialized: { + type: "object", + value: [ + ["1", { type: "number", value: 1 }], + ["2", { type: "string", value: "2" }], + ["foo", { type: "boolean", value: true }], + ], + }, + }, + { + value: { + 1: 1, + 2: "2", + foo: true, + }, + serializationOptions: { + maxObjectDepth: 0, + }, + serialized: { + type: "object", + }, + }, + { + value: { + 1: 1, + 2: "2", + foo: true, + }, + serializationOptions: { + maxObjectDepth: 1, + }, + serialized: { + type: "object", + value: [ + ["1", { type: "number", value: 1 }], + ["2", { type: "string", value: "2" }], + ["foo", { type: "boolean", value: true }], + ], + }, + deserializable: true, + }, + { + value: { + 1: 1, + 2: "2", + 3: { + bar: "foo", + }, + foo: true, + }, + serializationOptions: { + maxObjectDepth: 2, + }, + serialized: { + type: "object", + value: [ + ["1", { type: "number", value: 1 }], + ["2", { type: "string", value: "2" }], + [ + "3", + { + type: "object", + value: [["bar", { type: "string", value: "foo" }]], + }, + ], + ["foo", { type: "boolean", value: true }], + ], + }, + deserializable: true, + }, +]; + +add_task(function test_deserializePrimitiveTypes() { + const realm = new Realm(); + + for (const type of PRIMITIVE_TYPES) { + const { value: expectedValue, serialized } = type; + + info(`Checking '${serialized.type}'`); + const value = deserialize(serialized, realm, {}); + + if (serialized.value == "NaN") { + ok(Number.isNaN(value), `Got expected value for ${serialized}`); + } else { + Assert.strictEqual( + value, + expectedValue, + `Got expected value for ${serialized}` + ); + } + } +}); + +add_task(function test_deserializeDateLocalValue() { + const realm = new Realm(); + + const validaDateStrings = [ + "2009", + "2009-05", + "2009-05-19", + "2022-02-29", + "2009T15:00", + "2009-05T15:00", + "2022-06-31T15:00", + "2009-05-19T15:00", + "2009-05-19T15:00:15", + "2009-05-19T15:00-00:00", + "2009-05-19T15:00:15.452", + "2009-05-19T15:00:15.452Z", + "2009-05-19T15:00:15.452+02:00", + "2009-05-19T15:00:15.452-02:00", + "-271821-04-20T00:00:00Z", + "+000000-01-01T00:00:00Z", + ]; + for (const dateString of validaDateStrings) { + info(`Checking '${dateString}'`); + const value = deserialize({ type: "date", value: dateString }, realm, {}); + + Assert.equal( + value.getTime(), + new Date(dateString).getTime(), + `Got expected value for ${dateString}` + ); + } +}); + +add_task(function test_deserializeLocalValues() { + const realm = new Realm(); + + for (const type of REMOTE_SIMPLE_VALUES.concat(REMOTE_COMPLEX_VALUES)) { + const { value: expectedValue, serialized, deserializable } = type; + + // Skip non deserializable cases + if (!deserializable) { + continue; + } + + info(`Checking '${serialized.type}'`); + const value = deserialize(serialized, realm, {}); + assertLocalValue(serialized.type, value, expectedValue); + } +}); + +add_task(async function test_deserializeLocalValuesInWindowRealm() { + for (const type of REMOTE_SIMPLE_VALUES.concat(REMOTE_COMPLEX_VALUES)) { + const { value: expectedValue, serialized, deserializable } = type; + + // Skip non deserializable cases + if (!deserializable) { + continue; + } + + const value = await deserializeInWindowRealm(serialized); + assertLocalValue(serialized.type, value, expectedValue); + } +}); + +add_task(async function test_deserializeChannel() { + const realm = new Realm(); + const channel = { + type: "channel", + value: { channel: "channel_name" }, + }; + const deserializationOptions = { + emitScriptMessage: (realm, channelProperties, message) => message, + }; + + info(`Checking 'channel'`); + const value = deserialize(channel, realm, deserializationOptions, {}); + Assert.equal( + Object.prototype.toString.call(value), + "[object Function]", + "Got expected type Function" + ); + Assert.equal(value("foo"), "foo", "Got expected result"); +}); + +add_task(function test_deserializeLocalValuesByHandle() { + // Create two realms, realm1 will be used to serialize values, while realm2 + // will be used as a reference empty realm without any object reference. + const realm1 = new Realm(); + const realm2 = new Realm(); + + for (const type of REMOTE_SIMPLE_VALUES.concat(REMOTE_COMPLEX_VALUES)) { + const { value: expectedValue, serialized } = type; + + // No need to skip non-deserializable cases here. + + info(`Checking '${serialized.type}'`); + // Serialize the value once to get a handle. + const serializedValue = serialize( + expectedValue, + { maxObjectDepth: 0 }, + "root", + new Map(), + realm1, + {} + ); + + // Create a remote reference containing only the handle. + // `deserialize` should not need any other property. + const remoteReference = { handle: serializedValue.handle }; + + // Check that the remote reference can be deserialized in realm1. + const value = deserialize(remoteReference, realm1, {}); + assertLocalValue(serialized.type, value, expectedValue); + + Assert.throws( + () => deserialize(remoteReference, realm2, {}), + /NoSuchHandleError:/, + `Got expected error when using the wrong realm for deserialize` + ); + + realm1.removeObjectHandle(serializedValue.handle); + Assert.throws( + () => deserialize(remoteReference, realm1, {}), + /NoSuchHandleError:/, + `Got expected error when after deleting the object handle` + ); + } +}); + +add_task(function test_deserializeHandleInvalidTypes() { + const realm = new Realm(); + + for (const invalidType of [false, 42, {}, []]) { + info(`Checking type: '${invalidType}'`); + + Assert.throws( + () => deserialize({ type: "object", handle: invalidType }, realm, {}), + /InvalidArgumentError:/, + `Got expected error for type ${invalidType}` + ); + } +}); + +add_task(function test_deserializePrimitiveTypesInvalidValues() { + const realm = new Realm(); + + const invalidValues = [ + { type: "bigint", values: [undefined, null, false, "foo", [], {}] }, + { type: "boolean", values: [undefined, null, 42, "foo", [], {}] }, + { + type: "number", + values: [undefined, null, false, "43", [], {}], + }, + { type: "string", values: [undefined, null, false, 42, [], {}] }, + ]; + + for (const invalidValue of invalidValues) { + const { type, values } = invalidValue; + + for (const value of values) { + info(`Checking '${type}' with value ${value}`); + + Assert.throws( + () => deserialize({ type, value }, realm, {}), + /InvalidArgument/, + `Got expected error for type ${type} and value ${value}` + ); + } + } +}); + +add_task(function test_deserializeDateLocalValueInvalidValues() { + const realm = new Realm(); + + const invalidaDateStrings = [ + "10", + "20009", + "+20009", + "2009-", + "2009-0", + "2009-15", + "2009-02-1", + "2009-02-50", + "15:00", + "T15:00", + "9-05-19T15:00", + "2009-5-19T15:00", + "2009-05-1T15:00", + "2009-02-10T15", + "2009-05-19T15:", + "2009-05-19T1:00", + "2009-05-19T10:1", + "2009-05-19T60:00", + "2009-05-19T15:70", + "2009-05-19T15:00.25", + "2009-05-19+10:00", + "2009-05-19Z", + "2009-05-19 15:00", + "2009-05-19t15:00Z", + "2009-05-19T15:00z", + "2009-05-19T15:00+01", + "2009-05-19T10:10+1:00", + "2009-05-19T10:10+01:1", + "2009-05-19T15:00+75:00", + "2009-05-19T15:00+02:80", + "02009-05-19T15:00", + ]; + for (const dateString of invalidaDateStrings) { + info(`Checking '${dateString}'`); + + Assert.throws( + () => deserialize({ type: "date", value: dateString }, realm, {}), + /InvalidArgumentError:/, + `Got expected error for date string: ${dateString}` + ); + } +}); + +add_task(function test_deserializeLocalValuesInvalidType() { + const realm = new Realm(); + + const invalidTypes = [undefined, null, false, 42, {}]; + + for (const invalidType of invalidTypes) { + info(`Checking type: '${invalidType}'`); + + Assert.throws( + () => deserialize({ type: invalidType }, realm, {}), + /InvalidArgumentError:/, + `Got expected error for type ${invalidType}` + ); + + Assert.throws( + () => + deserialize( + { + type: "array", + value: [{ type: invalidType }], + }, + realm, + {} + ), + /InvalidArgumentError:/, + `Got expected error for nested type ${invalidType}` + ); + } +}); + +add_task(function test_deserializeLocalValuesInvalidValues() { + const realm = new Realm(); + + const invalidValues = [ + { type: "array", values: [undefined, null, false, 42, "foo", {}] }, + { + type: "regexp", + values: [ + undefined, + null, + false, + "foo", + 42, + [], + {}, + { pattern: null }, + { pattern: 1 }, + { pattern: true }, + { pattern: "foo", flags: null }, + { pattern: "foo", flags: 1 }, + { pattern: "foo", flags: false }, + { pattern: "foo", flags: "foo" }, + ], + }, + { + type: "date", + values: [ + undefined, + null, + false, + "foo", + "05 October 2011 14:48 UTC", + "Tue Jun 14 2022 10:46:50 GMT+0200!", + 42, + [], + {}, + ], + }, + { + type: "map", + values: [ + undefined, + null, + false, + "foo", + 42, + ["1"], + [[]], + [["1"]], + [{ 1: "2" }], + {}, + ], + }, + { + type: "set", + values: [undefined, null, false, "foo", 42, {}], + }, + { + type: "object", + values: [ + undefined, + null, + false, + "foo", + 42, + {}, + ["1"], + [[]], + [["1"]], + [{ 1: "2" }], + [ + [ + { type: "number", value: "1" }, + { type: "number", value: "2" }, + ], + ], + [ + [ + { type: "object", value: [] }, + { type: "number", value: "1" }, + ], + ], + [ + [ + { + type: "regexp", + value: { + pattern: "foo", + }, + }, + { type: "number", value: "1" }, + ], + ], + ], + }, + ]; + + for (const invalidValue of invalidValues) { + const { type, values } = invalidValue; + + for (const value of values) { + info(`Checking '${type}' with value ${value}`); + + Assert.throws( + () => deserialize({ type, value }, realm, {}), + /InvalidArgumentError:/, + `Got expected error for type ${type} and value ${value}` + ); + } + } +}); + +add_task(function test_serializePrimitiveTypes() { + const realm = new Realm(); + + for (const type of PRIMITIVE_TYPES) { + const { value, serialized } = type; + const defaultSerializationOptions = setDefaultSerializationOptions(); + + const serializationInternalMap = new Map(); + const serializedValue = serialize( + value, + defaultSerializationOptions, + "none", + serializationInternalMap, + realm, + {} + ); + assertInternalIds(serializationInternalMap, 0); + Assert.deepEqual(serialized, serializedValue, "Got expected structure"); + + // For primitive values, the serialization with ownershipType=root should + // be exactly identical to the one with ownershipType=none. + const serializationInternalMapWithRoot = new Map(); + const serializedWithRoot = serialize( + value, + defaultSerializationOptions, + "root", + serializationInternalMapWithRoot, + realm, + {} + ); + assertInternalIds(serializationInternalMapWithRoot, 0); + Assert.deepEqual(serialized, serializedWithRoot, "Got expected structure"); + } +}); + +add_task(function test_serializeRemoteSimpleValues() { + const realm = new Realm(); + + for (const type of REMOTE_SIMPLE_VALUES) { + const { value, serialized } = type; + const defaultSerializationOptions = setDefaultSerializationOptions(); + + info(`Checking '${serialized.type}' with none ownershipType`); + const serializationInternalMapWithNone = new Map(); + const serializedValue = serialize( + value, + defaultSerializationOptions, + "none", + serializationInternalMapWithNone, + realm, + {} + ); + + 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, + defaultSerializationOptions, + "root", + serializationInternalMapWithRoot, + realm, + {} + ); + + 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(function test_serializeRemoteComplexValues() { + 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, + "none", + serializationInternalMapWithNone, + realm, + {} + ); + + 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, + {} + ); + + 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(function test_serializeWithSerializationInternalMap() { + const dataSet = [ + { + data: [1], + serializedData: [{ type: "number", value: 1 }], + type: "array", + }, + { + data: new Map([[true, false]]), + serializedData: [ + [ + { type: "boolean", value: true }, + { type: "boolean", value: false }, + ], + ], + type: "map", + }, + { + data: new Set(["foo"]), + serializedData: [{ type: "string", value: "foo" }], + type: "set", + }, + { + data: { foo: "bar" }, + serializedData: [["foo", { type: "string", value: "bar" }]], + type: "object", + }, + ]; + const realm = new Realm(); + + for (const { type, data, serializedData } of dataSet) { + info(`Checking '${type}' with serializationInternalMap`); + + const serializationInternalMap = new Map(); + const value = [ + data, + data, + [data], + new Set([data]), + new Map([["bar", data]]), + { bar: data }, + ]; + + const serializedValue = serialize( + value, + { maxObjectDepth: 2 }, + "none", + serializationInternalMap, + realm, + {} + ); + + assertInternalIds(serializationInternalMap, 1); + + const internalId = serializationInternalMap.get(data).internalId; + + const serialized = { + type: "array", + value: [ + { + type, + value: serializedData, + internalId, + }, + { + type, + internalId, + }, + { + type: "array", + value: [{ type, internalId }], + }, + { + type: "set", + value: [{ type, internalId }], + }, + { + type: "map", + value: [["bar", { type, internalId }]], + }, + { + type: "object", + value: [["bar", { type, internalId }]], + }, + ], + }; + + Assert.deepEqual(serialized, serializedValue, "Got expected structure"); + } +}); + +add_task(function test_serializeMultipleValuesWithSerializationInternalMap() { + const realm = new Realm(); + const serializationInternalMap = new Map(); + const obj1 = { foo: "bar" }; + const obj2 = [1, 2]; + const value = [obj1, obj2, obj1, obj2]; + + serialize( + value, + { maxObjectDepth: 2 }, + "none", + serializationInternalMap, + realm, + {} + ); + + assertInternalIds(serializationInternalMap, 2); + + const internalId1 = serializationInternalMap.get(obj1).internalId; + const internalId2 = serializationInternalMap.get(obj2).internalId; + + Assert.notEqual( + internalId1, + internalId2, + "Internal ids for different object are also different" + ); +}); + +add_task(function test_stringify() { + const STRINGIFY_TEST_CASES = [ + [undefined, "undefined"], + [null, "null"], + ["foobar", "foobar"], + ["2", "2"], + [-0, "0"], + [Infinity, "Infinity"], + [-Infinity, "-Infinity"], + [3, "3"], + [1.4, "1.4"], + [true, "true"], + [42n, "42"], + [{ toString: () => "bar" }, "bar", "toString: () => 'bar'"], + [{ toString: () => 4 }, "[object Object]", "toString: () => 4"], + [{ toString: undefined }, "[object Object]", "toString: undefined"], + [{ toString: null }, "[object Object]", "toString: null"], + [ + { + toString: () => { + throw new Error("toString error"); + }, + }, + "[object Object]", + "toString: () => { throw new Error('toString error'); }", + ], + ]; + + for (const [value, expectedString, description] of STRINGIFY_TEST_CASES) { + info(`Checking '${description || value}'`); + const stringifiedValue = stringify(value); + + Assert.strictEqual(expectedString, stringifiedValue, "Got expected string"); + } +}); + +function assertLocalValue(type, value, expectedValue) { + let formattedValue = value; + let formattedExpectedValue = expectedValue; + + // Format certain types for easier assertion + if (type == "map") { + Assert.equal( + Object.prototype.toString.call(expectedValue), + "[object Map]", + "Got expected type Map" + ); + + formattedValue = Array.from(value.values()); + formattedExpectedValue = Array.from(expectedValue.values()); + } else if (type == "set") { + Assert.equal( + Object.prototype.toString.call(expectedValue), + "[object Set]", + "Got expected type Set" + ); + + formattedValue = Array.from(value); + formattedExpectedValue = Array.from(expectedValue); + } + + Assert.deepEqual( + formattedValue, + formattedExpectedValue, + "Got expected structure" + ); +} + +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" + ); +} + +function deserializeInWindowRealm(serialized) { + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [serialized], + async _serialized => { + const { WindowRealm } = ChromeUtils.importESModule( + "chrome://remote/content/shared/Realm.sys.mjs" + ); + const { deserialize } = ChromeUtils.importESModule( + "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs" + ); + const realm = new WindowRealm(content); + info(`Checking '${_serialized.type}'`); + return deserialize(_serialized, realm, {}); + } + ); +} diff --git a/remote/webdriver-bidi/test/browser/browser_RemoteValueDOM.js b/remote/webdriver-bidi/test/browser/browser_RemoteValueDOM.js new file mode 100644 index 0000000000..3e72c9c659 --- /dev/null +++ b/remote/webdriver-bidi/test/browser/browser_RemoteValueDOM.js @@ -0,0 +1,845 @@ +/* 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(serializedValue, realm, { nodeCache }), + /InvalidArgumentError:/, + `Got expected error for type ${invalidType}` + ); + } + }); +}); + +add_task(async function test_deserializeSharedIdInvalidValue() { + await runTestInContent(() => { + const serializedValue = { + sharedId: "foo", + }; + + Assert.throws( + () => deserialize(serializedValue, realm, { 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(serializedValue, realm, { 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(serializedValue, realm, { 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(serializedValue, new Realm(), { 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(serializedValue, realm, { 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.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) { + serializeAndAssertRemoteValue(type); + } + }); +}); + +add_task(async function test_serializeWindow() { + await loadURL(inline("<iframe>")); + + await runTestInContent(() => { + const REMOTE_COMPLEX_VALUES = [ + { + value: content, + serialized: { + type: "window", + value: { + context: content.browsingContext.browserId.toString(), + isTopBrowsingContext: true, + }, + }, + }, + { + value: content.frames[0], + serialized: { + type: "window", + value: { + context: content.frames[0].browsingContext.id.toString(), + }, + }, + }, + { + value: content.document.querySelector("iframe").contentWindow, + serialized: { + type: "window", + value: { + context: content.document + .querySelector("iframe") + .contentWindow.browsingContext.id.toString(), + }, + }, + }, + ]; + + for (const type of REMOTE_COMPLEX_VALUES) { + serializeAndAssertRemoteValue(type); + } + }); +}); + +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_serializeNodeEmbeddedWithin() { + await loadURL(inline("<div>")); + + 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 domElRef = nodeCache.getOrCreateNodeReference(domEl, seenNodeIds); + + const dataSet = [ + { + embedder: "array", + wrapper: node => [node], + serialized: { + type: "array", + value: [ + { + type: "node", + sharedId: domElRef, + value: { + attributes: {}, + childNodeCount: 0, + localName: "div", + namespaceURI: "http://www.w3.org/1999/xhtml", + nodeType: 1, + shadowRoot: null, + }, + }, + ], + }, + }, + { + embedder: "map", + wrapper: node => { + const map = new Map(); + map.set(node, "elem"); + return map; + }, + serialized: { + type: "map", + value: [ + [ + { + type: "node", + sharedId: domElRef, + value: { + attributes: {}, + childNodeCount: 0, + localName: "div", + namespaceURI: "http://www.w3.org/1999/xhtml", + nodeType: 1, + shadowRoot: null, + }, + }, + { + type: "string", + value: "elem", + }, + ], + ], + }, + }, + { + embedder: "map", + wrapper: node => { + const map = new Map(); + map.set("elem", node); + return map; + }, + serialized: { + type: "map", + value: [ + [ + "elem", + { + type: "node", + sharedId: domElRef, + value: { + attributes: {}, + childNodeCount: 0, + localName: "div", + namespaceURI: "http://www.w3.org/1999/xhtml", + nodeType: 1, + shadowRoot: null, + }, + }, + ], + ], + }, + }, + { + embedder: "object", + wrapper: node => ({ elem: node }), + serialized: { + type: "object", + value: [ + [ + "elem", + { + type: "node", + sharedId: domElRef, + value: { + attributes: {}, + childNodeCount: 0, + localName: "div", + namespaceURI: "http://www.w3.org/1999/xhtml", + nodeType: 1, + shadowRoot: null, + }, + }, + ], + ], + }, + }, + { + embedder: "set", + wrapper: node => { + const set = new Set(); + set.add(node); + return set; + }, + serialized: { + type: "set", + value: [ + { + type: "node", + sharedId: domElRef, + value: { + attributes: {}, + childNodeCount: 0, + localName: "div", + namespaceURI: "http://www.w3.org/1999/xhtml", + nodeType: 1, + shadowRoot: null, + }, + }, + ], + }, + }, + ]; + + for (const { embedder, wrapper, serialized } of dataSet) { + info(`Checking embedding node within ${embedder}`); + + const serializationInternalMap = new Map(); + + const serializedValue = serialize( + wrapper(domEl), + { maxDomDepth: 0 }, + "none", + serializationInternalMap, + realm, + { nodeCache } + ); + + 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(); + + function serializeAndAssertRemoteValue(remoteValue) { + const { value, serialized } = remoteValue; + 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" + ); + } + + // eslint-disable-next-line no-eval + eval(`(${callback})()`); + } + ); +} diff --git a/remote/webdriver-bidi/test/browser/head.js b/remote/webdriver-bidi/test/browser/head.js new file mode 100644 index 0000000000..e9a125a193 --- /dev/null +++ b/remote/webdriver-bidi/test/browser/head.js @@ -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.startLoadingURIString(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 + )}`; +} diff --git a/remote/webdriver-bidi/test/xpcshell/test_WebDriverBiDiConnection.js b/remote/webdriver-bidi/test/xpcshell/test_WebDriverBiDiConnection.js new file mode 100644 index 0000000000..e10c77caf3 --- /dev/null +++ b/remote/webdriver-bidi/test/xpcshell/test_WebDriverBiDiConnection.js @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { splitMethod } = ChromeUtils.importESModule( + "chrome://remote/content/webdriver-bidi/WebDriverBiDiConnection.sys.mjs" +); + +add_task(function test_Connection_splitMethod() { + for (const t of [42, null, true, {}, [], undefined]) { + Assert.throws(() => splitMethod(t), /TypeError/, `${typeof t} throws`); + } + for (const s of ["", ".", "foo.", ".bar", "foo.bar.baz"]) { + Assert.throws( + () => splitMethod(s), + /Invalid method format: '.*'/, + `"${s}" throws` + ); + } + deepEqual(splitMethod("foo.bar"), { + module: "foo", + command: "bar", + }); +}); diff --git a/remote/webdriver-bidi/test/xpcshell/xpcshell.toml b/remote/webdriver-bidi/test/xpcshell/xpcshell.toml new file mode 100644 index 0000000000..31cd9e3f04 --- /dev/null +++ b/remote/webdriver-bidi/test/xpcshell/xpcshell.toml @@ -0,0 +1,3 @@ +[DEFAULT] + +["test_WebDriverBiDiConnection.js"] |