summaryrefslogtreecommitdiffstats
path: root/remote/test/browser/runtime
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /remote/test/browser/runtime
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'remote/test/browser/runtime')
-rw-r--r--remote/test/browser/runtime/browser.ini20
-rw-r--r--remote/test/browser/runtime/browser_callFunctionOn.js460
-rw-r--r--remote/test/browser/runtime/browser_callFunctionOn_returnByValue.js369
-rw-r--r--remote/test/browser/runtime/browser_consoleAPICalled.js186
-rw-r--r--remote/test/browser/runtime/browser_evaluate.js506
-rw-r--r--remote/test/browser/runtime/browser_exceptionThrown.js120
-rw-r--r--remote/test/browser/runtime/browser_executionContextEvents.js339
-rw-r--r--remote/test/browser/runtime/browser_getProperties.js184
-rw-r--r--remote/test/browser/runtime/browser_remoteObjects.js83
-rw-r--r--remote/test/browser/runtime/doc_console_events.html18
-rw-r--r--remote/test/browser/runtime/head.js11
11 files changed, 2296 insertions, 0 deletions
diff --git a/remote/test/browser/runtime/browser.ini b/remote/test/browser/runtime/browser.ini
new file mode 100644
index 0000000000..4e6f884ec4
--- /dev/null
+++ b/remote/test/browser/runtime/browser.ini
@@ -0,0 +1,20 @@
+[DEFAULT]
+tags = remote
+subsuite = remote
+prefs =
+ remote.enabled=true
+support-files =
+ !/remote/test/browser/chrome-remote-interface.js
+ !/remote/test/browser/head.js
+ doc_console_events.html
+ head.js
+
+[browser_callFunctionOn.js]
+[browser_callFunctionOn_returnByValue.js]
+[browser_consoleAPICalled.js]
+[browser_evaluate.js]
+[browser_exceptionThrown.js]
+[browser_executionContextEvents.js]
+skip-if = os == "mac" || os == "win" # bug 1586503,1590930
+[browser_getProperties.js]
+[browser_remoteObjects.js]
diff --git a/remote/test/browser/runtime/browser_callFunctionOn.js b/remote/test/browser/runtime/browser_callFunctionOn.js
new file mode 100644
index 0000000000..7977631961
--- /dev/null
+++ b/remote/test/browser/runtime/browser_callFunctionOn.js
@@ -0,0 +1,460 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_DOC = toDataURL("default-test-page");
+
+add_task(async function FunctionDeclarationMissing({ client }) {
+ const { Runtime } = client;
+
+ let errorThrown = "";
+ try {
+ await Runtime.callFunctionOn();
+ } catch (e) {
+ errorThrown = e.message;
+ }
+ ok(errorThrown.includes("functionDeclaration: string value expected"));
+});
+
+add_task(async function functionDeclarationInvalidTypes({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ for (const functionDeclaration of [null, true, 1, [], {}]) {
+ let errorThrown = "";
+ try {
+ await Runtime.callFunctionOn({ functionDeclaration, executionContextId });
+ } catch (e) {
+ errorThrown = e.message;
+ }
+ ok(errorThrown.includes("functionDeclaration: string value expected"));
+ }
+});
+
+add_task(async function functionDeclarationGetCurrentLocation({ client }) {
+ const { Runtime } = client;
+
+ await loadURL(TEST_DOC);
+ const { id: executionContextId } = await enableRuntime(client);
+
+ const { result } = await Runtime.callFunctionOn({
+ functionDeclaration: "() => location.href",
+ executionContextId,
+ });
+ is(result.value, TEST_DOC, "Works against the test page");
+});
+
+add_task(async function argumentsInvalidTypes({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ for (const args of [null, true, 1, "foo", {}]) {
+ let errorThrown = "";
+ try {
+ await Runtime.callFunctionOn({
+ functionDeclaration: "",
+ arguments: args,
+ executionContextId,
+ });
+ } catch (e) {
+ errorThrown = e.message;
+ }
+ ok(errorThrown.includes("arguments: array value expected"));
+ }
+});
+
+add_task(async function argumentsPrimitiveTypes({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ for (const args of [null, true, 1, "foo", {}]) {
+ let errorThrown = "";
+ try {
+ await Runtime.callFunctionOn({
+ functionDeclaration: "",
+ arguments: args,
+ executionContextId,
+ });
+ } catch (e) {
+ errorThrown = e.message;
+ }
+ ok(errorThrown.includes("arguments: array value expected"));
+ }
+});
+
+add_task(async function awaitPromiseInvalidTypes({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ for (const awaitPromise of [null, 1, "foo", [], {}]) {
+ let errorThrown = "";
+ try {
+ await Runtime.callFunctionOn({
+ functionDeclaration: "",
+ awaitPromise,
+ executionContextId,
+ });
+ } catch (e) {
+ errorThrown = e.message;
+ }
+ ok(errorThrown.includes("awaitPromise: boolean value expected"));
+ }
+});
+
+add_task(async function awaitPromiseResolve({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ const { result } = await Runtime.callFunctionOn({
+ functionDeclaration: "() => Promise.resolve(42)",
+ awaitPromise: true,
+ executionContextId,
+ });
+
+ is(result.type, "number", "The type is correct");
+ is(result.subtype, undefined, "The subtype is undefined for numbers");
+ is(result.value, 42, "The result is the promise's resolution");
+});
+
+add_task(async function awaitPromiseDelayedResolve({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ const { result } = await Runtime.callFunctionOn({
+ functionDeclaration: "() => new Promise(r => setTimeout(() => r(42), 0))",
+ awaitPromise: true,
+ executionContextId,
+ });
+ is(result.type, "number", "The type is correct");
+ is(result.subtype, undefined, "The subtype is undefined for numbers");
+ is(result.value, 42, "The result is the promise's resolution");
+});
+
+add_task(async function awaitPromiseReject({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ const { exceptionDetails } = await Runtime.callFunctionOn({
+ functionDeclaration: "() => Promise.reject(42)",
+ awaitPromise: true,
+ executionContextId,
+ });
+ // TODO: Implement all values for exceptionDetails (bug 1548480)
+ is(
+ exceptionDetails.exception.value,
+ 42,
+ "The result is the promise's rejection"
+ );
+});
+
+add_task(async function awaitPromiseDelayedReject({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ const { exceptionDetails } = await Runtime.callFunctionOn({
+ functionDeclaration:
+ "() => new Promise((_,r) => setTimeout(() => r(42), 0))",
+ awaitPromise: true,
+ executionContextId,
+ });
+ is(
+ exceptionDetails.exception.value,
+ 42,
+ "The result is the promise's rejection"
+ );
+});
+
+add_task(async function awaitPromiseResolveWithoutWait({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ const { result } = await Runtime.callFunctionOn({
+ functionDeclaration: "() => Promise.resolve(42)",
+ awaitPromise: false,
+ executionContextId,
+ });
+
+ is(result.type, "object", "The type is correct");
+ is(result.subtype, "promise", "The subtype is promise");
+ ok(!!result.objectId, "We got the object id for the promise");
+ ok(!result.value, "We do not receive any value");
+});
+
+add_task(async function awaitPromiseDelayedResolveWithoutWait({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ const { result } = await Runtime.callFunctionOn({
+ functionDeclaration: "() => new Promise(r => setTimeout(() => r(42), 0))",
+ awaitPromise: false,
+ executionContextId,
+ });
+
+ is(result.type, "object", "The type is correct");
+ is(result.subtype, "promise", "The subtype is promise");
+ ok(!!result.objectId, "We got the object id for the promise");
+ ok(!result.value, "We do not receive any value");
+});
+
+add_task(async function awaitPromiseRejectWithoutWait({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ const { result } = await Runtime.callFunctionOn({
+ functionDeclaration: "() => Promise.reject(42)",
+ awaitPromise: false,
+ executionContextId,
+ });
+
+ is(result.type, "object", "The type is correct");
+ is(result.subtype, "promise", "The subtype is promise");
+ ok(!!result.objectId, "We got the object id for the promise");
+ ok(!result.exceptionDetails, "We do not receive any exception");
+});
+
+add_task(async function awaitPromiseDelayedRejectWithoutWait({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ const { result } = await Runtime.callFunctionOn({
+ functionDeclaration:
+ "() => new Promise((_,r) => setTimeout(() => r(42), 0))",
+ awaitPromise: false,
+ executionContextId,
+ });
+
+ is(result.type, "object", "The type is correct");
+ is(result.subtype, "promise", "The subtype is promise");
+ ok(!!result.objectId, "We got the object id for the promise");
+ ok(!result.exceptionDetails, "We do not receive any exception");
+});
+
+add_task(async function executionContextIdNorObjectIdSpecified({ client }) {
+ const { Runtime } = client;
+
+ let errorThrown = "";
+ try {
+ await Runtime.callFunctionOn({
+ functionDeclaration: "",
+ });
+ } catch (e) {
+ errorThrown = e.message;
+ }
+ ok(
+ errorThrown.includes(
+ "Either objectId or executionContextId must be specified"
+ )
+ );
+});
+
+add_task(async function executionContextIdInvalidTypes({ client }) {
+ const { Runtime } = client;
+
+ for (const executionContextId of [null, true, "foo", [], {}]) {
+ let errorThrown = "";
+ try {
+ await Runtime.callFunctionOn({
+ functionDeclaration: "",
+ executionContextId,
+ });
+ } catch (e) {
+ errorThrown = e.message;
+ }
+ ok(errorThrown.includes("executionContextId: number value expected"));
+ }
+});
+
+add_task(async function executionContextIdInvalidValue({ client }) {
+ const { Runtime } = client;
+
+ let errorThrown = "";
+ try {
+ await Runtime.callFunctionOn({
+ functionDeclaration: "",
+ executionContextId: -1,
+ });
+ } catch (e) {
+ errorThrown = e.message;
+ }
+ ok(errorThrown.includes("Cannot find context with specified id"));
+});
+
+add_task(async function objectIdInvalidTypes({ client }) {
+ const { Runtime } = client;
+
+ for (const objectId of [null, true, 1, [], {}]) {
+ let errorThrown = "";
+ try {
+ await Runtime.callFunctionOn({ functionDeclaration: "", objectId });
+ } catch (e) {
+ errorThrown = e.message;
+ }
+ ok(errorThrown.includes("objectId: string value expected"));
+ }
+});
+
+add_task(async function objectId({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ // First create an object
+ const { result } = await Runtime.callFunctionOn({
+ functionDeclaration: "() => ({ foo: 42 })",
+ executionContextId,
+ });
+
+ is(result.type, "object", "The type is correct");
+ is(result.subtype, undefined, "The subtype is undefined for objects");
+ ok(!!result.objectId, "Got an object id");
+
+ // Then apply a method on this object
+ const { result: result2 } = await Runtime.callFunctionOn({
+ functionDeclaration: "function () { return this.foo; }",
+ executionContextId,
+ objectId: result.objectId,
+ });
+
+ is(result2.type, "number", "The type is correct");
+ is(result2.subtype, undefined, "The subtype is undefined for numbers");
+ is(result2.value, 42, "Expected value returned");
+});
+
+add_task(async function objectIdArgumentReference({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ // First create a remote JS object
+ const { result } = await Runtime.callFunctionOn({
+ functionDeclaration: "() => ({ foo: 1 })",
+ executionContextId,
+ });
+
+ is(result.type, "object", "The type is correct");
+ is(result.subtype, undefined, "The subtype is undefined for objects");
+ ok(!!result.objectId, "Got an object id");
+
+ // Then increment the `foo` attribute of this JS object,
+ // while returning this attribute value
+ const { result: result2 } = await Runtime.callFunctionOn({
+ functionDeclaration: "arg => ++arg.foo",
+ arguments: [{ objectId: result.objectId }],
+ executionContextId,
+ });
+
+ Assert.deepEqual(
+ result2,
+ {
+ type: "number",
+ value: 2,
+ },
+ "The result has the expected type and value"
+ );
+
+ // Finally, try to pass this JS object and get it back. Ensure that it
+ // returns the same object id. Also increment the attribute again.
+ const { result: result3 } = await Runtime.callFunctionOn({
+ functionDeclaration: "arg => { arg.foo++; return arg; }",
+ arguments: [{ objectId: result.objectId }],
+ executionContextId,
+ });
+
+ is(result3.type, "object", "The type is correct");
+ is(result3.subtype, undefined, "The subtype is undefined for objects");
+ // Remote objects don't have unique ids. So you may have multiple object ids
+ // that reference the same remote object
+ ok(!!result3.objectId, "Got an object id");
+ isnot(result3.objectId, result.objectId, "The object id is different");
+
+ // Assert that we can still access this object and that its foo attribute
+ // has been incremented. Use the second object id we got from previous call
+ // to callFunctionOn.
+ const { result: result4 } = await Runtime.callFunctionOn({
+ functionDeclaration: "arg => arg.foo",
+ arguments: [{ objectId: result3.objectId }],
+ executionContextId,
+ });
+
+ Assert.deepEqual(
+ result4,
+ {
+ type: "number",
+ value: 3,
+ },
+ "The result has the expected type and value"
+ );
+});
+
+add_task(async function exceptionDetailsJavascriptError({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ const { exceptionDetails } = await Runtime.callFunctionOn({
+ functionDeclaration: "doesNotExists()",
+ executionContextId,
+ });
+
+ Assert.deepEqual(
+ exceptionDetails,
+ {
+ text: "doesNotExists is not defined",
+ },
+ "Javascript error is passed to the client"
+ );
+});
+
+add_task(async function exceptionDetailsThrowError({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ const { exceptionDetails } = await Runtime.callFunctionOn({
+ functionDeclaration: "() => { throw new Error('foo') }",
+ executionContextId,
+ });
+
+ Assert.deepEqual(
+ exceptionDetails,
+ {
+ text: "foo",
+ },
+ "Exception details are passed to the client"
+ );
+});
+
+add_task(async function exceptionDetailsThrowValue({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ const { exceptionDetails } = await Runtime.callFunctionOn({
+ functionDeclaration: "() => { throw 'foo' }",
+ executionContextId,
+ });
+
+ Assert.deepEqual(
+ exceptionDetails,
+ {
+ exception: {
+ type: "string",
+ value: "foo",
+ },
+ },
+ "Exception details are passed as a RemoteObject"
+ );
+});
diff --git a/remote/test/browser/runtime/browser_callFunctionOn_returnByValue.js b/remote/test/browser/runtime/browser_callFunctionOn_returnByValue.js
new file mode 100644
index 0000000000..3647599116
--- /dev/null
+++ b/remote/test/browser/runtime/browser_callFunctionOn_returnByValue.js
@@ -0,0 +1,369 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function returnAsObjectTypes({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ const expressions = [
+ { expression: "({foo:true})", type: "object", subtype: undefined },
+ { expression: "Symbol('foo')", type: "symbol", subtype: undefined },
+ { expression: "new Promise(()=>{})", type: "object", subtype: "promise" },
+ { expression: "new Int8Array(8)", type: "object", subtype: "typedarray" },
+ { expression: "new WeakMap()", type: "object", subtype: "weakmap" },
+ { expression: "new WeakSet()", type: "object", subtype: "weakset" },
+ { expression: "new Map()", type: "object", subtype: "map" },
+ { expression: "new Set()", type: "object", subtype: "set" },
+ { expression: "/foo/", type: "object", subtype: "regexp" },
+ { expression: "[1, 2]", type: "object", subtype: "array" },
+ { expression: "new Proxy({}, {})", type: "object", subtype: "proxy" },
+ { expression: "new Date()", type: "object", subtype: "date" },
+ {
+ expression: "document",
+ type: "object",
+ subtype: "node",
+ className: "HTMLDocument",
+ description: "#document",
+ },
+ {
+ expression: `{{
+ const div = document.createElement('div');
+ div.id = "foo";
+ return div;
+ }}`,
+ type: "object",
+ subtype: "node",
+ className: "HTMLDivElement",
+ description: "div#foo",
+ },
+ ];
+
+ for (const entry of expressions) {
+ const { expression, type, subtype, className, description } = entry;
+
+ const { result } = await Runtime.callFunctionOn({
+ functionDeclaration: `() => ${expression}`,
+ executionContextId,
+ });
+
+ is(result.type, type, "The type is correct");
+ is(result.subtype, subtype, "The subtype is correct");
+ ok(!!result.objectId, "Got an object id");
+ if (className) {
+ is(result.className, className, "The className is correct");
+ }
+ if (description) {
+ is(result.description, description, "The description is correct");
+ }
+ }
+});
+
+add_task(async function returnAsObjectDifferentObjectIds({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ const expressions = [{}, "document"];
+ for (const expression of expressions) {
+ const { result: result1 } = await Runtime.callFunctionOn({
+ functionDeclaration: `() => ${JSON.stringify(expression)}`,
+ executionContextId,
+ });
+ const { result: result2 } = await Runtime.callFunctionOn({
+ functionDeclaration: `() => ${JSON.stringify(expression)}`,
+ executionContextId,
+ });
+ is(
+ result1.objectId,
+ result2.objectId,
+ `Different object ids returned for ${expression}`
+ );
+ }
+});
+
+add_task(async function returnAsObjectPrimitiveTypes({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ const expressions = [42, "42", true, 4.2];
+ for (const expression of expressions) {
+ const { result } = await Runtime.callFunctionOn({
+ functionDeclaration: `() => ${JSON.stringify(expression)}`,
+ executionContextId,
+ });
+ is(result.value, expression, `Evaluating primitive '${expression}' works`);
+ is(result.type, typeof expression, `${expression} type is correct`);
+ }
+});
+
+add_task(async function returnAsObjectNotSerializable({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ const notSerializableNumbers = {
+ number: ["-0", "NaN", "Infinity", "-Infinity"],
+ bigint: ["42n"],
+ };
+
+ for (const type in notSerializableNumbers) {
+ for (const expression of notSerializableNumbers[type]) {
+ const { result } = await Runtime.callFunctionOn({
+ functionDeclaration: `() => ${expression}`,
+ executionContextId,
+ });
+ Assert.deepEqual(
+ result,
+ {
+ type,
+ unserializableValue: expression,
+ description: expression,
+ },
+ `Evaluating unserializable '${expression}' works`
+ );
+ }
+ }
+});
+
+// `null` is special as it has its own subtype, is of type 'object'
+// but is returned as a value, without an `objectId` attribute
+add_task(async function returnAsObjectNull({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ const { result } = await Runtime.callFunctionOn({
+ functionDeclaration: "() => null",
+ executionContextId,
+ });
+ Assert.deepEqual(
+ result,
+ {
+ type: "object",
+ subtype: "null",
+ value: null,
+ },
+ "Null type is correct"
+ );
+});
+
+// undefined doesn't work with JSON.stringify, so test it independently
+add_task(async function returnAsObjectUndefined({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ const { result } = await Runtime.callFunctionOn({
+ functionDeclaration: "() => undefined",
+ executionContextId,
+ });
+ Assert.deepEqual(
+ result,
+ {
+ type: "undefined",
+ },
+ "Undefined type is correct"
+ );
+});
+
+add_task(async function returnByValueInvalidTypes({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ for (const returnByValue of [null, 1, "foo", [], {}]) {
+ let errorThrown = "";
+ try {
+ await Runtime.callFunctionOn({
+ functionDeclaration: "",
+ executionContextId,
+ returnByValue,
+ });
+ } catch (e) {
+ errorThrown = e.message;
+ }
+ ok(errorThrown.includes("returnByValue: boolean value expected"));
+ }
+});
+
+add_task(async function returnByValue({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ const values = [
+ null,
+ 42,
+ 42.0,
+ "42",
+ true,
+ false,
+ { foo: true },
+ { foo: { bar: 42, str: "str", array: [1, 2, 3] } },
+ [42, "42", true],
+ [{ foo: true }],
+ ];
+
+ for (const value of values) {
+ const { result } = await Runtime.callFunctionOn({
+ functionDeclaration: `() => (${JSON.stringify(value)})`,
+ executionContextId,
+ returnByValue: true,
+ });
+
+ Assert.deepEqual(
+ result,
+ {
+ type: typeof value,
+ value,
+ description: value != null ? value.toString() : value,
+ },
+ "The returned value is the same than the input value"
+ );
+ }
+});
+
+add_task(async function returnByValueNotSerializable({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ const notSerializableNumbers = {
+ number: ["-0", "NaN", "Infinity", "-Infinity"],
+ bigint: ["42n"],
+ };
+
+ for (const type in notSerializableNumbers) {
+ for (const unserializableValue of notSerializableNumbers[type]) {
+ const { result } = await Runtime.callFunctionOn({
+ functionDeclaration: `() => (${unserializableValue})`,
+ executionContextId,
+ returnByValue: true,
+ });
+
+ Assert.deepEqual(
+ result,
+ {
+ type,
+ unserializableValue,
+ description: unserializableValue,
+ },
+ "The returned value is the same than the input value"
+ );
+ }
+ }
+});
+
+// Test undefined individually as JSON.stringify doesn't return a string
+add_task(async function returnByValueUndefined({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ const { result } = await Runtime.callFunctionOn({
+ functionDeclaration: "() => {}",
+ executionContextId,
+ returnByValue: true,
+ });
+
+ Assert.deepEqual(
+ result,
+ {
+ type: "undefined",
+ },
+ "Undefined type is correct"
+ );
+});
+
+add_task(async function returnByValueArguments({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ const values = [
+ 42,
+ 42.0,
+ "42",
+ true,
+ false,
+ null,
+ { foo: true },
+ { foo: { bar: 42, str: "str", array: [1, 2, 3] } },
+ [42, "42", true],
+ [{ foo: true }],
+ ];
+
+ for (const value of values) {
+ const { result } = await Runtime.callFunctionOn({
+ functionDeclaration: "a => a",
+ arguments: [{ value }],
+ executionContextId,
+ returnByValue: true,
+ });
+
+ Assert.deepEqual(
+ result,
+ {
+ type: typeof value,
+ value,
+ description: value != null ? value.toString() : value,
+ },
+ "The returned value is the same than the input value"
+ );
+ }
+});
+
+add_task(async function returnByValueArgumentsNotSerializable({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ const notSerializableNumbers = {
+ number: ["-0", "NaN", "Infinity", "-Infinity"],
+ bigint: ["42n"],
+ };
+
+ for (const type in notSerializableNumbers) {
+ for (const unserializableValue of notSerializableNumbers[type]) {
+ const { result } = await Runtime.callFunctionOn({
+ functionDeclaration: "a => a",
+ arguments: [{ unserializableValue }],
+ executionContextId,
+ returnByValue: true,
+ });
+
+ Assert.deepEqual(
+ result,
+ {
+ type,
+ unserializableValue,
+ description: unserializableValue,
+ },
+ "The returned value is the same than the input value"
+ );
+ }
+ }
+});
+
+add_task(async function returnByValueArgumentsSymbol({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ let errorThrown = "";
+ try {
+ await Runtime.callFunctionOn({
+ functionDeclaration: "a => a",
+ arguments: [{ unserializableValue: "Symbol('42')" }],
+ executionContextId,
+ returnByValue: true,
+ });
+ } catch (e) {
+ errorThrown = e.message;
+ }
+ ok(errorThrown, "Symbol cannot be returned as value");
+});
diff --git a/remote/test/browser/runtime/browser_consoleAPICalled.js b/remote/test/browser/runtime/browser_consoleAPICalled.js
new file mode 100644
index 0000000000..f203399ed0
--- /dev/null
+++ b/remote/test/browser/runtime/browser_consoleAPICalled.js
@@ -0,0 +1,186 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const PAGE_CONSOLE_EVENTS =
+ "http://example.com/browser/remote/test/browser/runtime/doc_console_events.html";
+
+add_task(async function noEventsWhenRuntimeDomainDisabled({ client }) {
+ await runConsoleTest(client, 0, async () => {
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, () => console.log("foo"));
+ });
+});
+
+add_task(async function noEventsAfterRuntimeDomainDisabled({ client }) {
+ const { Runtime } = client;
+
+ await Runtime.enable();
+ await Runtime.disable();
+
+ await runConsoleTest(client, 0, async () => {
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, () => console.log("foo"));
+ });
+});
+
+add_task(async function noEventsForJavascriptErrors({ client }) {
+ await loadURL(PAGE_CONSOLE_EVENTS);
+ const context = await enableRuntime(client);
+
+ await runConsoleTest(client, 0, async () => {
+ evaluate(client, context.id, () => {
+ document.getElementById("js-error").click();
+ });
+ });
+});
+
+add_task(async function consoleAPI({ client }) {
+ const context = await enableRuntime(client);
+
+ const events = await runConsoleTest(client, 1, async () => {
+ await evaluate(client, context.id, () => {
+ console.log("foo");
+ });
+ });
+
+ is(events[0].type, "log", "Got expected type");
+ is(events[0].args[0].value, "foo", "Got expected argument value");
+ is(
+ events[0].executionContextId,
+ context.id,
+ "Got event from current execution context"
+ );
+});
+
+add_task(async function consoleAPITypes({ client }) {
+ const expectedEvents = ["dir", "error", "log", "timeEnd", "trace", "warning"];
+ const levels = ["dir", "error", "log", "time", "timeEnd", "trace", "warn"];
+
+ const context = await enableRuntime(client);
+
+ const events = await runConsoleTest(
+ client,
+ expectedEvents.length,
+ async () => {
+ for (const level of levels) {
+ await evaluate(
+ client,
+ context.id,
+ level => {
+ console[level]("foo");
+ },
+ level
+ );
+ }
+ }
+ );
+
+ events.forEach((event, index) => {
+ console.log(`Check for event type "${expectedEvents[index]}"`);
+ is(event.type, expectedEvents[index], "Got expected type");
+ });
+});
+
+add_task(async function consoleAPIArgumentsCount({ client }) {
+ const argumentList = [[], ["foo"], ["foo", "bar", "cheese"]];
+
+ const context = await enableRuntime(client);
+
+ const events = await runConsoleTest(client, argumentList.length, async () => {
+ for (const args of argumentList) {
+ await evaluate(
+ client,
+ context.id,
+ args => {
+ console.log(...args);
+ },
+ args
+ );
+ }
+ });
+
+ events.forEach((event, index) => {
+ console.log(`Check for event args "${argumentList[index]}"`);
+
+ const argValues = event.args.map(arg => arg.value);
+ Assert.deepEqual(argValues, argumentList[index], "Got expected args");
+ });
+});
+
+add_task(async function consoleAPIArgumentsAsRemoteObject({ client }) {
+ const context = await enableRuntime(client);
+
+ const events = await runConsoleTest(client, 1, async () => {
+ await evaluate(client, context.id, () => {
+ console.log("foo", Symbol("foo"));
+ });
+ });
+
+ Assert.equal(events[0].args.length, 2, "Got expected amount of arguments");
+
+ is(events[0].args[0].type, "string", "Got expected argument type");
+ is(events[0].args[0].value, "foo", "Got expected argument value");
+
+ is(events[0].args[1].type, "symbol", "Got expected argument type");
+ is(
+ events[0].args[1].description,
+ "Symbol(foo)",
+ "Got expected argument description"
+ );
+ ok(!!events[0].args[1].objectId, "Got objectId for argument");
+});
+
+add_task(async function consoleAPIByContentInteraction({ client }) {
+ await loadURL(PAGE_CONSOLE_EVENTS);
+ const context = await enableRuntime(client);
+
+ const events = await runConsoleTest(client, 1, async () => {
+ evaluate(client, context.id, () => {
+ document.getElementById("console-error").click();
+ });
+ });
+
+ is(events[0].type, "error", "Got expected type");
+ is(events[0].args[0].value, "foo", "Got expected argument value");
+ is(
+ events[0].executionContextId,
+ context.id,
+ "Got event from current execution context"
+ );
+});
+
+async function runConsoleTest(client, eventCount, callback, options = {}) {
+ const { Runtime } = client;
+
+ const EVENT_CONSOLE_API_CALLED = "Runtime.consoleAPICalled";
+
+ const history = new RecordEvents(eventCount);
+ history.addRecorder({
+ event: Runtime.consoleAPICalled,
+ eventName: EVENT_CONSOLE_API_CALLED,
+ messageFn: payload =>
+ `Received ${EVENT_CONSOLE_API_CALLED} for ${payload.type}`,
+ });
+
+ const timeBefore = Date.now();
+ await callback();
+
+ const consoleAPIentries = await history.record();
+ is(consoleAPIentries.length, eventCount, "Got expected amount of events");
+
+ if (eventCount == 0) {
+ return [];
+ }
+
+ const timeAfter = Date.now();
+
+ // Check basic details for consoleAPICalled events
+ consoleAPIentries.forEach(({ payload }) => {
+ ok(
+ payload.timestamp >= timeBefore && payload.timestamp <= timeAfter,
+ "Got valid timestamp"
+ );
+ });
+
+ return consoleAPIentries.map(event => event.payload);
+}
diff --git a/remote/test/browser/runtime/browser_evaluate.js b/remote/test/browser/runtime/browser_evaluate.js
new file mode 100644
index 0000000000..61ab7b9dbe
--- /dev/null
+++ b/remote/test/browser/runtime/browser_evaluate.js
@@ -0,0 +1,506 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_DOC = toDataURL("default-test-page");
+
+add_task(async function awaitPromiseInvalidTypes({ client }) {
+ const { Runtime } = client;
+
+ await enableRuntime(client);
+
+ for (const awaitPromise of [null, 1, "foo", [], {}]) {
+ let errorThrown = "";
+ try {
+ await Runtime.evaluate({
+ expression: "",
+ awaitPromise,
+ });
+ } catch (e) {
+ errorThrown = e.message;
+ }
+ ok(errorThrown.includes("awaitPromise: boolean value expected"));
+ }
+});
+
+add_task(async function awaitPromiseResolve({ client }) {
+ const { Runtime } = client;
+
+ await enableRuntime(client);
+
+ const { result } = await Runtime.evaluate({
+ expression: "Promise.resolve(42)",
+ awaitPromise: true,
+ });
+
+ is(result.type, "number", "The type is correct");
+ is(result.subtype, undefined, "The subtype is undefined for numbers");
+ is(result.value, 42, "The result is the promise's resolution");
+});
+
+add_task(async function awaitPromiseReject({ client }) {
+ const { Runtime } = client;
+
+ await enableRuntime(client);
+
+ const { exceptionDetails } = await Runtime.evaluate({
+ expression: "Promise.reject(42)",
+ awaitPromise: true,
+ });
+ // TODO: Implement all values for exceptionDetails (bug 1548480)
+ is(
+ exceptionDetails.exception.value,
+ 42,
+ "The result is the promise's rejection"
+ );
+});
+
+add_task(async function awaitPromiseDelayedResolve({ client }) {
+ const { Runtime } = client;
+
+ await enableRuntime(client);
+
+ const { result } = await Runtime.evaluate({
+ expression: "new Promise(r => setTimeout(() => r(42), 0))",
+ awaitPromise: true,
+ });
+ is(result.type, "number", "The type is correct");
+ is(result.subtype, undefined, "The subtype is undefined for numbers");
+ is(result.value, 42, "The result is the promise's resolution");
+});
+
+add_task(async function awaitPromiseDelayedReject({ client }) {
+ const { Runtime } = client;
+
+ await enableRuntime(client);
+
+ const { exceptionDetails } = await Runtime.evaluate({
+ expression: "new Promise((_,r) => setTimeout(() => r(42), 0))",
+ awaitPromise: true,
+ });
+ is(
+ exceptionDetails.exception.value,
+ 42,
+ "The result is the promise's rejection"
+ );
+});
+
+add_task(async function awaitPromiseResolveWithoutWait({ client }) {
+ const { Runtime } = client;
+
+ await enableRuntime(client);
+
+ const { result } = await Runtime.evaluate({
+ expression: "Promise.resolve(42)",
+ awaitPromise: false,
+ });
+
+ is(result.type, "object", "The type is correct");
+ is(result.subtype, "promise", "The subtype is promise");
+ ok(!!result.objectId, "We got the object id for the promise");
+ ok(!result.value, "We do not receive any value");
+});
+
+add_task(async function awaitPromiseDelayedResolveWithoutWait({ client }) {
+ const { Runtime } = client;
+
+ await enableRuntime(client);
+
+ const { result } = await Runtime.evaluate({
+ expression: "new Promise(r => setTimeout(() => r(42), 0))",
+ awaitPromise: false,
+ });
+
+ is(result.type, "object", "The type is correct");
+ is(result.subtype, "promise", "The subtype is promise");
+ ok(!!result.objectId, "We got the object id for the promise");
+ ok(!result.value, "We do not receive any value");
+});
+
+add_task(async function awaitPromiseRejectWithoutWait({ client }) {
+ const { Runtime } = client;
+
+ await enableRuntime(client);
+
+ const { result } = await Runtime.evaluate({
+ expression: "Promise.reject(42)",
+ awaitPromise: false,
+ });
+
+ is(result.type, "object", "The type is correct");
+ is(result.subtype, "promise", "The subtype is promise");
+ ok(!!result.objectId, "We got the object id for the promise");
+ ok(!result.exceptionDetails, "We do not receive any exception");
+});
+
+add_task(async function awaitPromiseDelayedRejectWithoutWait({ client }) {
+ const { Runtime } = client;
+
+ await enableRuntime(client);
+
+ const { result } = await Runtime.evaluate({
+ expression: "new Promise((_,r) => setTimeout(() => r(42), 0))",
+ awaitPromise: false,
+ });
+
+ is(result.type, "object", "The type is correct");
+ is(result.subtype, "promise", "The subtype is promise");
+ ok(!!result.objectId, "We got the object id for the promise");
+ ok(!result.exceptionDetails, "We do not receive any exception");
+});
+
+add_task(async function contextIdInvalidValue({ client }) {
+ const { Runtime } = client;
+
+ await enableRuntime(client);
+
+ let errorThrown = "";
+ try {
+ await Runtime.evaluate({ expression: "", contextId: -1 });
+ } catch (e) {
+ errorThrown = e.message;
+ }
+ ok(errorThrown.includes("Cannot find context with specified id"));
+});
+
+add_task(async function contextIdNotSpecified({ client }) {
+ const { Runtime } = client;
+
+ await loadURL(TEST_DOC);
+ await enableRuntime(client);
+
+ const { result } = await Runtime.evaluate({ expression: "location.href" });
+ is(result.value, TEST_DOC, "Works against the current document");
+});
+
+add_task(async function contextIdSpecified({ client }) {
+ const { Runtime } = client;
+
+ await loadURL(TEST_DOC);
+ const { id: contextId } = await enableRuntime(client);
+
+ const { result } = await Runtime.evaluate({
+ expression: "location.href",
+ contextId,
+ });
+ is(result.value, TEST_DOC, "Works against the targetted document");
+});
+
+add_task(async function returnAsObjectTypes({ client }) {
+ const { Runtime } = client;
+
+ await enableRuntime(client);
+
+ const expressions = [
+ { expression: "({foo:true})", type: "object", subtype: undefined },
+ { expression: "Symbol('foo')", type: "symbol", subtype: undefined },
+ { expression: "new Promise(()=>{})", type: "object", subtype: "promise" },
+ { expression: "new Int8Array(8)", type: "object", subtype: "typedarray" },
+ { expression: "new WeakMap()", type: "object", subtype: "weakmap" },
+ { expression: "new WeakSet()", type: "object", subtype: "weakset" },
+ { expression: "new Map()", type: "object", subtype: "map" },
+ { expression: "new Set()", type: "object", subtype: "set" },
+ { expression: "/foo/", type: "object", subtype: "regexp" },
+ { expression: "[1, 2]", type: "object", subtype: "array" },
+ { expression: "new Proxy({}, {})", type: "object", subtype: "proxy" },
+ { expression: "new Date()", type: "object", subtype: "date" },
+ {
+ expression: "document",
+ type: "object",
+ subtype: "node",
+ className: "HTMLDocument",
+ description: "#document",
+ },
+ {
+ expression: `(() => {{
+ const div = document.createElement('div');
+ div.id = "foo";
+ return div;
+ }})()`,
+ type: "object",
+ subtype: "node",
+ className: "HTMLDivElement",
+ description: "div#foo",
+ },
+ ];
+
+ for (const entry of expressions) {
+ const { expression, type, subtype, className, description } = entry;
+
+ const { result } = await Runtime.evaluate({ expression });
+
+ is(result.type, type, "The type is correct");
+ is(result.subtype, subtype, "The subtype is correct");
+ ok(!!result.objectId, "Got an object id");
+ if (className) {
+ is(result.className, className, "The className is correct");
+ }
+ if (description) {
+ is(result.description, description, "The description is correct");
+ }
+ }
+});
+
+add_task(async function returnAsObjectDifferentObjectIds({ client }) {
+ const { Runtime } = client;
+
+ await enableRuntime(client);
+
+ const expressions = [{}, "document"];
+ for (const expression of expressions) {
+ const { result: result1 } = await Runtime.evaluate({
+ expression: JSON.stringify(expression),
+ });
+ const { result: result2 } = await Runtime.evaluate({
+ expression: JSON.stringify(expression),
+ });
+ is(
+ result1.objectId,
+ result2.objectId,
+ `Different object ids returned for ${expression}`
+ );
+ }
+});
+
+add_task(async function returnAsObjectPrimitiveTypes({ client }) {
+ const { Runtime } = client;
+
+ await enableRuntime(client);
+
+ const expressions = [42, "42", true, 4.2];
+ for (const expression of expressions) {
+ const { result } = await Runtime.evaluate({
+ expression: JSON.stringify(expression),
+ });
+ is(result.value, expression, `Evaluating primitive '${expression}' works`);
+ is(result.type, typeof expression, `${expression} type is correct`);
+ }
+});
+
+add_task(async function returnAsObjectNotSerializable({ client }) {
+ const { Runtime } = client;
+
+ await enableRuntime(client);
+
+ const notSerializableNumbers = {
+ number: ["-0", "NaN", "Infinity", "-Infinity"],
+ bigint: ["42n"],
+ };
+
+ for (const type in notSerializableNumbers) {
+ for (const expression of notSerializableNumbers[type]) {
+ const { result } = await Runtime.evaluate({ expression });
+ Assert.deepEqual(
+ result,
+ {
+ type,
+ unserializableValue: expression,
+ description: expression,
+ },
+ `Evaluating unserializable '${expression}' works`
+ );
+ }
+ }
+});
+
+// `null` is special as it has its own subtype, is of type 'object'
+// but is returned as a value, without an `objectId` attribute
+add_task(async function returnAsObjectNull({ client }) {
+ const { Runtime } = client;
+
+ await enableRuntime(client);
+
+ const { result } = await Runtime.evaluate({
+ expression: "null",
+ });
+ Assert.deepEqual(
+ result,
+ {
+ type: "object",
+ subtype: "null",
+ value: null,
+ },
+ "Null type is correct"
+ );
+});
+
+// undefined doesn't work with JSON.stringify, so test it independently
+add_task(async function returnAsObjectUndefined({ client }) {
+ const { Runtime } = client;
+
+ await enableRuntime(client);
+
+ const { result } = await Runtime.evaluate({
+ expression: "undefined",
+ });
+ Assert.deepEqual(
+ result,
+ {
+ type: "undefined",
+ },
+ "Undefined type is correct"
+ );
+});
+
+add_task(async function returnByValueInvalidTypes({ client }) {
+ const { Runtime } = client;
+
+ await enableRuntime(client);
+
+ for (const returnByValue of [null, 1, "foo", [], {}]) {
+ let errorThrown = "";
+ try {
+ await Runtime.evaluate({
+ expression: "",
+ returnByValue,
+ });
+ } catch (e) {
+ errorThrown = e.message;
+ }
+ ok(errorThrown.includes("returnByValue: boolean value expected"));
+ }
+});
+
+add_task(async function returnByValue({ client }) {
+ const { Runtime } = client;
+
+ await enableRuntime(client);
+
+ const values = [
+ null,
+ 42,
+ 42.0,
+ "42",
+ true,
+ false,
+ { foo: true },
+ { foo: { bar: 42, str: "str", array: [1, 2, 3] } },
+ [42, "42", true],
+ [{ foo: true }],
+ ];
+
+ for (const value of values) {
+ const { result } = await Runtime.evaluate({
+ expression: `(${JSON.stringify(value)})`,
+ returnByValue: true,
+ });
+
+ Assert.deepEqual(
+ result,
+ {
+ type: typeof value,
+ value,
+ description: value != null ? value.toString() : value,
+ },
+ `Returned expected value for ${JSON.stringify(value)}`
+ );
+ }
+});
+
+add_task(async function returnByValueNotSerializable({ client }) {
+ const { Runtime } = client;
+
+ await enableRuntime(client);
+
+ const notSerializableNumbers = {
+ number: ["-0", "NaN", "Infinity", "-Infinity"],
+ bigint: ["42n"],
+ };
+
+ for (const type in notSerializableNumbers) {
+ for (const unserializableValue of notSerializableNumbers[type]) {
+ const { result } = await Runtime.evaluate({
+ expression: `(${unserializableValue})`,
+ returnByValue: true,
+ });
+
+ Assert.deepEqual(
+ result,
+ {
+ type,
+ unserializableValue,
+ description: unserializableValue,
+ },
+ `Returned expected value for ${JSON.stringify(unserializableValue)}`
+ );
+ }
+ }
+});
+
+// Test undefined individually as JSON.stringify doesn't return a string
+add_task(async function returnByValueUndefined({ client }) {
+ const { Runtime } = client;
+
+ await enableRuntime(client);
+
+ const { result } = await Runtime.evaluate({
+ expression: "undefined",
+ returnByValue: true,
+ });
+
+ Assert.deepEqual(
+ result,
+ {
+ type: "undefined",
+ },
+ "Undefined type is correct"
+ );
+});
+
+add_task(async function exceptionDetailsJavascriptError({ client }) {
+ const { Runtime } = client;
+
+ await enableRuntime(client);
+
+ const { exceptionDetails } = await Runtime.evaluate({
+ expression: "doesNotExists()",
+ });
+
+ Assert.deepEqual(
+ exceptionDetails,
+ {
+ text: "doesNotExists is not defined",
+ },
+ "Javascript error is passed to the client"
+ );
+});
+
+add_task(async function exceptionDetailsThrowError({ client }) {
+ const { Runtime } = client;
+
+ await enableRuntime(client);
+
+ const { exceptionDetails } = await Runtime.evaluate({
+ expression: "throw new Error('foo')",
+ });
+
+ Assert.deepEqual(
+ exceptionDetails,
+ {
+ text: "foo",
+ },
+ "Exception details are passed to the client"
+ );
+});
+
+add_task(async function exceptionDetailsThrowValue({ client }) {
+ const { Runtime } = client;
+
+ await enableRuntime(client);
+
+ const { exceptionDetails } = await Runtime.evaluate({
+ expression: "throw 'foo'",
+ });
+
+ Assert.deepEqual(
+ exceptionDetails,
+ {
+ exception: {
+ type: "string",
+ value: "foo",
+ },
+ },
+ "Exception details are passed as a RemoteObject"
+ );
+});
diff --git a/remote/test/browser/runtime/browser_exceptionThrown.js b/remote/test/browser/runtime/browser_exceptionThrown.js
new file mode 100644
index 0000000000..b573f0cffd
--- /dev/null
+++ b/remote/test/browser/runtime/browser_exceptionThrown.js
@@ -0,0 +1,120 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const PAGE_CONSOLE_EVENTS =
+ "http://example.com/browser/remote/test/browser/runtime/doc_console_events.html";
+
+add_task(async function noEventsWhenRuntimeDomainDisabled({ client }) {
+ await runExceptionThrownTest(client, 0, async () => {
+ await throwScriptError({ text: "foo" });
+ });
+});
+
+add_task(async function noEventsAfterRuntimeDomainDisabled({ client }) {
+ const { Runtime } = client;
+
+ await Runtime.enable();
+ await Runtime.disable();
+
+ await runExceptionThrownTest(client, 0, async () => {
+ await throwScriptError({ text: "foo" });
+ });
+});
+
+add_task(async function noEventsForScriptErrorWithoutException({ client }) {
+ const { Runtime } = client;
+
+ await Runtime.enable();
+
+ await runExceptionThrownTest(client, 0, async () => {
+ await throwScriptError({ text: "foo" });
+ });
+});
+
+add_task(async function eventsForScriptErrorWithException({ client }) {
+ await loadURL(PAGE_CONSOLE_EVENTS);
+
+ const context = await enableRuntime(client);
+
+ const events = await runExceptionThrownTest(client, 1, async () => {
+ evaluate(client, context.id, () => {
+ document.getElementById("js-error").click();
+ });
+ });
+
+ is(
+ typeof events[0].exceptionId,
+ "number",
+ "Got expected type for exception id"
+ );
+ is(
+ events[0].text,
+ "TypeError: foo.click is not a function",
+ "Got expected text"
+ );
+ is(events[0].lineNumber, 9, "Got expected line number");
+ is(events[0].columnNumber, 11, "Got expected column number");
+ is(events[0].url, PAGE_CONSOLE_EVENTS, "Got expected url");
+ is(
+ events[0].executionContextId,
+ context.id,
+ "Got event from current execution context"
+ );
+
+ const callFrames = events[0].stackTrace.callFrames;
+ is(callFrames.length, 2, "Got expected amount of call frames");
+
+ is(callFrames[0].functionName, "throwError", "Got expected function name");
+ is(callFrames[0].url, PAGE_CONSOLE_EVENTS, "Got expected url");
+ is(callFrames[0].lineNumber, 9, "Got expected line number");
+ is(callFrames[0].columnNumber, 11, "Got expected column number");
+
+ is(callFrames[1].functionName, "onclick", "Got expected function name");
+ is(callFrames[1].url, PAGE_CONSOLE_EVENTS, "Got expected url");
+});
+
+async function runExceptionThrownTest(
+ client,
+ eventCount,
+ callback,
+ options = {}
+) {
+ const { Runtime } = client;
+
+ const EVENT_EXCEPTION_THROWN = "Runtime.exceptionThrown";
+
+ const history = new RecordEvents(eventCount);
+ history.addRecorder({
+ event: Runtime.exceptionThrown,
+ eventName: EVENT_EXCEPTION_THROWN,
+ messageFn: payload => `Received "${payload.name}"`,
+ });
+
+ const timeBefore = Date.now();
+ await callback();
+
+ const exceptionThrownEvents = await history.record();
+ is(exceptionThrownEvents.length, eventCount, "Got expected amount of events");
+
+ if (eventCount == 0) {
+ return [];
+ }
+
+ const timeAfter = Date.now();
+
+ // Check basic details for entryAdded events
+ exceptionThrownEvents.forEach(event => {
+ const details = event.payload.exceptionDetails;
+ const timestamp = event.payload.timestamp;
+
+ is(typeof details, "object", "Got expected 'exceptionDetails' property");
+ ok(
+ timestamp >= timeBefore && timestamp <= timeAfter,
+ "Got valid timestamp"
+ );
+ });
+
+ return exceptionThrownEvents.map(event => event.payload.exceptionDetails);
+}
diff --git a/remote/test/browser/runtime/browser_executionContextEvents.js b/remote/test/browser/runtime/browser_executionContextEvents.js
new file mode 100644
index 0000000000..f0f15c5516
--- /dev/null
+++ b/remote/test/browser/runtime/browser_executionContextEvents.js
@@ -0,0 +1,339 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the Runtime execution context events
+
+const DOC = toDataURL("default-test-page");
+const DOC_IFRAME = toDataURL(`<iframe src='${DOC}'></iframe>`);
+
+const DESTROYED = "Runtime.executionContextDestroyed";
+const CREATED = "Runtime.executionContextCreated";
+const CLEARED = "Runtime.executionContextsCleared";
+
+add_task(async function noEventsWhenRuntimeDomainDisabled({ client }) {
+ const { Runtime } = client;
+
+ const history = recordContextEvents(Runtime, 0);
+ await loadURL(DOC);
+ await assertEventOrder({ history, expectedEvents: [] });
+});
+
+add_task(async function noEventsAfterRuntimeDomainDisabled({ client }) {
+ const { Runtime } = client;
+
+ await Runtime.enable();
+ await Runtime.disable();
+
+ const history = recordContextEvents(Runtime, 0);
+ await loadURL(DOC);
+ await assertEventOrder({ history, expectedEvents: [] });
+});
+
+add_task(async function eventsWhenNavigatingWithNoFrames({ client }) {
+ const { Page, Runtime } = client;
+
+ const previousContext = await enableRuntime(client);
+ const history = recordContextEvents(Runtime, 3);
+
+ const { frameId } = await Page.navigate({ url: DOC });
+ await assertEventOrder({ history });
+
+ const { executionContextId: destroyedId } = history.findEvent(
+ DESTROYED
+ ).payload;
+ is(
+ destroyedId,
+ previousContext.id,
+ "The destroyed event reports the previous context id"
+ );
+
+ const { context: contextCreated } = history.findEvent(CREATED).payload;
+ checkDefaultContext(contextCreated);
+ isnot(
+ contextCreated.id,
+ previousContext.id,
+ "The new execution context has a different id"
+ );
+ is(
+ contextCreated.auxData.frameId,
+ frameId,
+ "The execution context frame id is the same " +
+ "than the one returned by Page.navigate"
+ );
+});
+
+add_task(async function eventsWhenNavigatingFrameSet({ client }) {
+ const { Runtime } = client;
+
+ const previousContext = await enableRuntime(client);
+
+ // Check navigation to a frameset
+ const historyTo = recordContextEvents(Runtime, 4);
+ await loadURL(DOC_IFRAME);
+ await assertEventOrder({
+ history: historyTo,
+ expectedEvents: [DESTROYED, CLEARED, CREATED, CREATED],
+ });
+
+ const { executionContextId: destroyedId } = historyTo.findEvent(
+ DESTROYED
+ ).payload;
+ is(
+ destroyedId,
+ previousContext.id,
+ "The destroyed event reports the previous context id"
+ );
+
+ const contexts = historyTo.findEvents(CREATED);
+ const createdTopContext = contexts[0].payload.context;
+ const createdFrameContext = contexts[1].payload.context;
+
+ checkDefaultContext(createdTopContext);
+ isnot(
+ createdTopContext.id,
+ previousContext.id,
+ "The new execution context has a different id"
+ );
+ is(
+ createdTopContext.origin,
+ DOC_IFRAME,
+ "The execution context origin is the frameset"
+ );
+
+ checkDefaultContext(createdFrameContext);
+ isnot(
+ createdFrameContext.id,
+ createdTopContext.id,
+ "The new frame's execution context has a different id"
+ );
+ is(
+ createdFrameContext.origin,
+ DOC,
+ "The frame's execution context origin is the frame"
+ );
+
+ // Check navigation from a frameset
+ const historyFrom = recordContextEvents(Runtime, 4);
+ await loadURL(DOC);
+ await assertEventOrder({
+ history: historyFrom,
+ // Bug 1644657: The cleared event should come last but we emit destroy events
+ // for the top-level context and for frames afterward. Chrome only sends out
+ // the cleared event on navigation.
+ expectedEvents: [DESTROYED, CLEARED, DESTROYED, CREATED],
+ });
+
+ const destroyedContextIds = historyFrom.findEvents(DESTROYED);
+ is(
+ destroyedContextIds[0].payload.executionContextId,
+ createdTopContext.id,
+ "The destroyed event reports the previous context id"
+ );
+ is(
+ destroyedContextIds[1].payload.executionContextId,
+ createdFrameContext.id,
+ "The destroyed event reports the previous frame's context id"
+ );
+
+ const { context: contextCreated } = historyFrom.findEvent(CREATED).payload;
+ checkDefaultContext(contextCreated);
+ isnot(
+ contextCreated.id,
+ createdTopContext.id,
+ "The new execution context has a different id"
+ );
+ is(
+ contextCreated.origin,
+ DOC,
+ "The execution context origin is not the frameset"
+ );
+});
+
+add_task(async function eventsWhenNavigatingBackWithNoFrames({ client }) {
+ const { Runtime } = client;
+
+ // Load an initial URL so that navigating back will work
+ await loadURL(DOC);
+ const previousContext = await enableRuntime(client);
+
+ const executionContextCreated = Runtime.executionContextCreated();
+ await loadURL(toDataURL("other-test-page"));
+ const { context: createdContext } = await executionContextCreated;
+
+ const history = recordContextEvents(Runtime, 3);
+ gBrowser.selectedBrowser.goBack();
+ await assertEventOrder({ history });
+
+ const { executionContextId: destroyedId } = history.findEvent(
+ DESTROYED
+ ).payload;
+ is(
+ destroyedId,
+ createdContext.id,
+ "The destroyed event reports the current context id"
+ );
+
+ const { context } = history.findEvent(CREATED).payload;
+ checkDefaultContext(context);
+ is(
+ context.origin,
+ previousContext.origin,
+ "The new execution context has the same origin as the previous one."
+ );
+ isnot(
+ context.id,
+ previousContext.id,
+ "The new execution context has a different id"
+ );
+ ok(context.auxData.isDefault, "The execution context is the default one");
+ is(
+ context.auxData.frameId,
+ previousContext.auxData.frameId,
+ "The execution context frame id is always the same"
+ );
+ is(context.auxData.type, "default", "Execution context has 'default' type");
+ is(context.name, "", "The default execution context is named ''");
+
+ const { result } = await Runtime.evaluate({
+ contextId: context.id,
+ expression: "location.href",
+ });
+ is(
+ result.value,
+ DOC,
+ "Runtime.evaluate works and is against the page we just navigated to"
+ );
+});
+
+add_task(async function eventsWhenReloadingPageWithNoFrames({ client }) {
+ const { Page, Runtime } = client;
+
+ // Load an initial URL so that reload will work
+ await loadURL(DOC);
+ const previousContext = await enableRuntime(client);
+
+ await Page.enable();
+
+ const history = recordContextEvents(Runtime, 3);
+ const frameNavigated = Page.frameNavigated();
+ gBrowser.selectedBrowser.reload();
+ await frameNavigated;
+
+ await assertEventOrder({ history });
+
+ const { executionContextId } = history.findEvent(DESTROYED).payload;
+ is(
+ executionContextId,
+ previousContext.id,
+ "The destroyed event reports the previous context id"
+ );
+
+ const { context } = history.findEvent(CREATED).payload;
+ checkDefaultContext(context);
+ is(
+ context.auxData.frameId,
+ previousContext.auxData.frameId,
+ "The execution context frame id is the same as before reloading"
+ );
+
+ isnot(
+ executionContextId,
+ context.id,
+ "The destroyed id is different from the created one"
+ );
+});
+
+add_task(async function eventsWhenNavigatingByLocationWithNoFrames({ client }) {
+ const { Runtime } = client;
+
+ const previousContext = await enableRuntime(client);
+ const history = recordContextEvents(Runtime, 3);
+
+ await Runtime.evaluate({
+ contextId: previousContext.id,
+ expression: `window.location = '${DOC}';`,
+ });
+ await assertEventOrder({ history });
+
+ const { executionContextId: destroyedId } = history.findEvent(
+ DESTROYED
+ ).payload;
+ is(
+ destroyedId,
+ previousContext.id,
+ "The destroyed event reports the previous context id"
+ );
+
+ const { context: createdContext } = history.findEvent(CREATED).payload;
+ checkDefaultContext(createdContext);
+ is(
+ createdContext.auxData.frameId,
+ previousContext.auxData.frameId,
+ "The execution context frame id is identical " +
+ "to the one from before before setting the window's location"
+ );
+ isnot(
+ destroyedId,
+ createdContext.id,
+ "The destroyed id is different from the created one"
+ );
+});
+
+function recordContextEvents(Runtime, total) {
+ const history = new RecordEvents(total);
+
+ history.addRecorder({
+ event: Runtime.executionContextDestroyed,
+ eventName: DESTROYED,
+ messageFn: payload => {
+ return `Received ${DESTROYED} for id ${payload.executionContextId}`;
+ },
+ });
+ history.addRecorder({
+ event: Runtime.executionContextCreated,
+ eventName: CREATED,
+ messageFn: ({ context }) => {
+ return (
+ `Received ${CREATED} for id ${context.id}` +
+ ` type: ${context.auxData.type}` +
+ ` name: ${context.name}` +
+ ` origin: ${context.origin}`
+ );
+ },
+ });
+ history.addRecorder({
+ event: Runtime.executionContextsCleared,
+ eventName: CLEARED,
+ });
+
+ return history;
+}
+
+async function assertEventOrder(options = {}) {
+ const { history, expectedEvents = [DESTROYED, CLEARED, CREATED] } = options;
+ const events = await history.record();
+ const eventNames = events.map(item => item.eventName);
+ info(`Expected events: ${expectedEvents}`);
+ info(`Received events: ${eventNames}`);
+
+ is(
+ events.length,
+ expectedEvents.length,
+ "Received expected number of Runtime context events"
+ );
+ Assert.deepEqual(
+ events.map(item => item.eventName),
+ expectedEvents,
+ "Received Runtime context events in expected order"
+ );
+}
+
+function checkDefaultContext(context) {
+ ok(!!context.id, "The execution context has an id");
+ ok(context.auxData.isDefault, "The execution context is the default one");
+ is(context.auxData.type, "default", "Execution context has 'default' type");
+ ok(!!context.origin, "The execution context has an origin");
+ is(context.name, "", "The default execution context is named ''");
+}
diff --git a/remote/test/browser/runtime/browser_getProperties.js b/remote/test/browser/runtime/browser_getProperties.js
new file mode 100644
index 0000000000..84229db10e
--- /dev/null
+++ b/remote/test/browser/runtime/browser_getProperties.js
@@ -0,0 +1,184 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the Runtime remote object
+
+add_task(async function({ client }) {
+ const firstContext = await testRuntimeEnable(client);
+ const contextId = firstContext.id;
+
+ await testGetOwnSimpleProperties(client, contextId);
+ await testGetCustomProperty(client, contextId);
+ await testGetPrototypeProperties(client, contextId);
+ await testGetGetterSetterProperties(client, contextId);
+});
+
+async function testRuntimeEnable({ Runtime }) {
+ // Enable watching for new execution context
+ await Runtime.enable();
+ info("Runtime domain has been enabled");
+
+ // Calling Runtime.enable will emit executionContextCreated for the existing contexts
+ const { context } = await Runtime.executionContextCreated();
+ ok(!!context.id, "The execution context has an id");
+ ok(context.auxData.isDefault, "The execution context is the default one");
+ ok(!!context.auxData.frameId, "The execution context has a frame id set");
+
+ return context;
+}
+
+async function testGetOwnSimpleProperties({ Runtime }, contextId) {
+ const { result } = await Runtime.evaluate({
+ contextId,
+ expression: "({ bool: true, fun() {}, int: 1, object: {}, string: 'foo' })",
+ });
+ is(result.subtype, undefined, "JS Object has no subtype");
+ is(result.type, "object", "The type is correct");
+ ok(!!result.objectId, "Got an object id");
+
+ const { result: result2 } = await Runtime.getProperties({
+ objectId: result.objectId,
+ ownProperties: true,
+ });
+ is(
+ result2.length,
+ 5,
+ "ownProperties=true allows to iterate only over direct object properties (i.e. ignore prototype)"
+ );
+ result2.sort((a, b) => a.name > b.name);
+ is(result2[0].name, "bool");
+ is(result2[0].configurable, true);
+ is(result2[0].enumerable, true);
+ is(result2[0].writable, true);
+ is(result2[0].value.type, "boolean");
+ is(result2[0].value.value, true);
+ is(result2[0].isOwn, true);
+
+ is(result2[1].name, "fun");
+ is(result2[1].configurable, true);
+ is(result2[1].enumerable, true);
+ is(result2[1].writable, true);
+ is(result2[1].value.type, "function");
+ ok(!!result2[1].value.objectId);
+ is(result2[1].isOwn, true);
+
+ is(result2[2].name, "int");
+ is(result2[2].configurable, true);
+ is(result2[2].enumerable, true);
+ is(result2[2].writable, true);
+ is(result2[2].value.type, "number");
+ is(result2[2].value.value, 1);
+ is(result2[2].isOwn, true);
+
+ is(result2[3].name, "object");
+ is(result2[3].configurable, true);
+ is(result2[3].enumerable, true);
+ is(result2[3].writable, true);
+ is(result2[3].value.type, "object");
+ ok(!!result2[3].value.objectId);
+ is(result2[3].isOwn, true);
+
+ is(result2[4].name, "string");
+ is(result2[4].configurable, true);
+ is(result2[4].enumerable, true);
+ is(result2[4].writable, true);
+ is(result2[4].value.type, "string");
+ is(result2[4].value.value, "foo");
+ is(result2[4].isOwn, true);
+}
+
+async function testGetPrototypeProperties({ Runtime }, contextId) {
+ const { result } = await Runtime.evaluate({
+ contextId,
+ expression: "({ foo: 42 })",
+ });
+ is(result.subtype, undefined, "JS Object has no subtype");
+ is(result.type, "object", "The type is correct");
+ ok(!!result.objectId, "Got an object id");
+
+ const { result: result2 } = await Runtime.getProperties({
+ objectId: result.objectId,
+ ownProperties: false,
+ });
+ ok(result2.length > 1, "We have more properties than just the object one");
+ const foo = result2.find(p => p.name == "foo");
+ ok(foo, "The object property is described");
+ ok(foo.isOwn, "and is reported as 'own' property");
+
+ const toString = result2.find(p => p.name == "toString");
+ ok(
+ toString,
+ "Function from Object's prototype are also described like toString"
+ );
+ ok(!toString.isOwn, "but are reported as not being an 'own' property");
+}
+
+async function testGetGetterSetterProperties({ Runtime }, contextId) {
+ const { result } = await Runtime.evaluate({
+ contextId,
+ expression:
+ "({ get prop() { return this.x; }, set prop(v) { this.x = v; } })",
+ });
+ is(result.subtype, undefined, "JS Object has no subtype");
+ is(result.type, "object", "The type is correct");
+ ok(!!result.objectId, "Got an object id");
+
+ const { result: result2 } = await Runtime.getProperties({
+ objectId: result.objectId,
+ ownProperties: true,
+ });
+ is(result2.length, 1);
+
+ is(result2[0].name, "prop");
+ is(result2[0].configurable, true);
+ is(result2[0].enumerable, true);
+ is(
+ result2[0].writable,
+ undefined,
+ "writable is only set for data properties"
+ );
+
+ is(result2[0].get.type, "function");
+ ok(!!result2[0].get.objectId);
+ is(result2[0].set.type, "function");
+ ok(!!result2[0].set.objectId);
+
+ is(result2[0].isOwn, true);
+
+ const { result: result3 } = await Runtime.callFunctionOn({
+ executionContextId: contextId,
+ functionDeclaration: "(set, get) => { set(42); return get(); }",
+ arguments: [
+ { objectId: result2[0].set.objectId },
+ { objectId: result2[0].get.objectId },
+ ],
+ });
+ is(result3.type, "number", "The type is correct");
+ is(result3.subtype, undefined, "The subtype is undefined for numbers");
+ is(result3.value, 42, "The getter returned the value set by the setter");
+}
+
+async function testGetCustomProperty({ Runtime }, contextId) {
+ const { result } = await Runtime.evaluate({
+ contextId,
+ expression: `const obj = {}; Object.defineProperty(obj, "prop", { value: 42 }); obj`,
+ });
+ is(result.subtype, undefined, "JS Object has no subtype");
+ is(result.type, "object", "The type is correct");
+ ok(!!result.objectId, "Got an object id");
+
+ const { result: result2 } = await Runtime.getProperties({
+ objectId: result.objectId,
+ ownProperties: true,
+ });
+ is(result2.length, 1, "We only get the one object's property");
+ is(result2[0].name, "prop");
+ is(result2[0].configurable, false);
+ is(result2[0].enumerable, false);
+ is(result2[0].writable, false);
+ is(result2[0].value.type, "number");
+ is(result2[0].value.value, 42);
+ is(result2[0].isOwn, true);
+}
diff --git a/remote/test/browser/runtime/browser_remoteObjects.js b/remote/test/browser/runtime/browser_remoteObjects.js
new file mode 100644
index 0000000000..07453ffa59
--- /dev/null
+++ b/remote/test/browser/runtime/browser_remoteObjects.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the Runtime remote object
+
+add_task(async function({ client }) {
+ const firstContext = await testRuntimeEnable(client);
+ const contextId = firstContext.id;
+
+ await testObjectRelease(client, contextId);
+});
+
+async function testRuntimeEnable({ Runtime }) {
+ // Enable watching for new execution context
+ await Runtime.enable();
+ info("Runtime domain has been enabled");
+
+ // Calling Runtime.enable will emit executionContextCreated for the existing contexts
+ const { context } = await Runtime.executionContextCreated();
+ ok(!!context.id, "The execution context has an id");
+ ok(context.auxData.isDefault, "The execution context is the default one");
+ ok(!!context.auxData.frameId, "The execution context has a frame id set");
+
+ return context;
+}
+
+async function testObjectRelease({ Runtime }, contextId) {
+ const { result } = await Runtime.evaluate({
+ contextId,
+ expression: "({ foo: 42 })",
+ });
+ is(result.subtype, undefined, "JS Object has no subtype");
+ is(result.type, "object", "The type is correct");
+ ok(!!result.objectId, "Got an object id");
+
+ const { result: result2 } = await Runtime.callFunctionOn({
+ executionContextId: contextId,
+ functionDeclaration: "obj => JSON.stringify(obj)",
+ arguments: [{ objectId: result.objectId }],
+ });
+ is(result2.type, "string", "The type is correct");
+ is(result2.value, JSON.stringify({ foo: 42 }), "Got the object's JSON");
+
+ const { result: result3 } = await Runtime.callFunctionOn({
+ objectId: result.objectId,
+ functionDeclaration: "function () { return this.foo; }",
+ });
+ is(result3.type, "number", "The type is correct");
+ is(result3.value, 42, "Got the object's foo attribute");
+
+ await Runtime.releaseObject({
+ objectId: result.objectId,
+ });
+ info("Object is released");
+
+ try {
+ await Runtime.callFunctionOn({
+ executionContextId: contextId,
+ functionDeclaration: "() => {}",
+ arguments: [{ objectId: result.objectId }],
+ });
+ ok(false, "callFunctionOn with a released object as argument should throw");
+ } catch (e) {
+ ok(
+ e.message.includes("Could not find object with given id"),
+ "callFunctionOn throws on released argument"
+ );
+ }
+ try {
+ await Runtime.callFunctionOn({
+ objectId: result.objectId,
+ functionDeclaration: "() => {}",
+ });
+ ok(false, "callFunctionOn with a released object as target should throw");
+ } catch (e) {
+ ok(
+ e.message.includes("Unable to get the context for object with id"),
+ "callFunctionOn throws on released target"
+ );
+ }
+}
diff --git a/remote/test/browser/runtime/doc_console_events.html b/remote/test/browser/runtime/doc_console_events.html
new file mode 100644
index 0000000000..63b37ce592
--- /dev/null
+++ b/remote/test/browser/runtime/doc_console_events.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Empty page</title>
+
+ <script>
+ function throwError() {
+ let foo = {};
+ foo.click();
+ }
+ </script>
+</head>
+<body>
+ <a id="console-log" href="javascript:console.log('foo')">console.log()</a><br/>
+ <a id="console-error" href="javascript:console.error('foo')">console.error()</a><br/>
+ <a id="js-error" onclick="throwError()">Javascript Error</a><br/>
+</body>
+</html>
diff --git a/remote/test/browser/runtime/head.js b/remote/test/browser/runtime/head.js
new file mode 100644
index 0000000000..7131e98b6f
--- /dev/null
+++ b/remote/test/browser/runtime/head.js
@@ -0,0 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from ../head.js */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/remote/test/browser/head.js",
+ this
+);