Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

/* Any copyright is dedicated to the Public Domain.
"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, {});
}
);
}