summaryrefslogtreecommitdiffstats
path: root/remote/cdp/test/browser/runtime
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /remote/cdp/test/browser/runtime
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'remote/cdp/test/browser/runtime')
-rw-r--r--remote/cdp/test/browser/runtime/browser.ini32
-rw-r--r--remote/cdp/test/browser/runtime/browser_callFunctionOn.js304
-rw-r--r--remote/cdp/test/browser/runtime/browser_callFunctionOn_awaitPromise.js181
-rw-r--r--remote/cdp/test/browser/runtime/browser_callFunctionOn_returnByValue.js401
-rw-r--r--remote/cdp/test/browser/runtime/browser_consoleAPICalled.js380
-rw-r--r--remote/cdp/test/browser/runtime/browser_evaluate.js256
-rw-r--r--remote/cdp/test/browser/runtime/browser_evaluate_awaitPromise.js169
-rw-r--r--remote/cdp/test/browser/runtime/browser_evaluate_returnByValue.js151
-rw-r--r--remote/cdp/test/browser/runtime/browser_exceptionThrown.js121
-rw-r--r--remote/cdp/test/browser/runtime/browser_executionContextEvents.js332
-rw-r--r--remote/cdp/test/browser/runtime/browser_getProperties.js184
-rw-r--r--remote/cdp/test/browser/runtime/browser_remoteObjects.js83
-rw-r--r--remote/cdp/test/browser/runtime/browser_withDefaultPrefs.js9
-rw-r--r--remote/cdp/test/browser/runtime/browser_with_default_prefs.ini13
-rw-r--r--remote/cdp/test/browser/runtime/doc_console_events.html31
-rw-r--r--remote/cdp/test/browser/runtime/doc_console_events_onload.html12
-rw-r--r--remote/cdp/test/browser/runtime/doc_empty.html9
-rw-r--r--remote/cdp/test/browser/runtime/doc_frame.html9
-rw-r--r--remote/cdp/test/browser/runtime/doc_frameset_single.html10
-rw-r--r--remote/cdp/test/browser/runtime/head.js15
20 files changed, 2702 insertions, 0 deletions
diff --git a/remote/cdp/test/browser/runtime/browser.ini b/remote/cdp/test/browser/runtime/browser.ini
new file mode 100644
index 0000000000..f039968784
--- /dev/null
+++ b/remote/cdp/test/browser/runtime/browser.ini
@@ -0,0 +1,32 @@
+[DEFAULT]
+tags = cdp
+subsuite = remote
+args =
+ --remote-debugging-port
+ --remote-allow-origins=null
+prefs = # Bug 1600054: Make CDP Fission compatible
+ fission.bfcacheInParent=false
+ fission.webContentIsolationStrategy=0
+support-files =
+ !/remote/cdp/test/browser/chrome-remote-interface.js
+ !/remote/cdp/test/browser/head.js
+ doc_console_events.html
+ doc_console_events_onload.html
+ doc_empty.html
+ doc_frame.html
+ doc_frameset_single.html
+ head.js
+
+[browser_callFunctionOn.js]
+[browser_callFunctionOn_awaitPromise.js]
+[browser_callFunctionOn_returnByValue.js]
+[browser_consoleAPICalled.js]
+https_first_disabled = true
+[browser_evaluate.js]
+[browser_evaluate_awaitPromise.js]
+[browser_evaluate_returnByValue.js]
+[browser_exceptionThrown.js]
+https_first_disabled = true
+[browser_executionContextEvents.js]
+[browser_getProperties.js]
+[browser_remoteObjects.js]
diff --git a/remote/cdp/test/browser/runtime/browser_callFunctionOn.js b/remote/cdp/test/browser/runtime/browser_callFunctionOn.js
new file mode 100644
index 0000000000..ce0c5e7e7a
--- /dev/null
+++ b/remote/cdp/test/browser/runtime/browser_callFunctionOn.js
@@ -0,0 +1,304 @@
+/* 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 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/cdp/test/browser/runtime/browser_callFunctionOn_awaitPromise.js b/remote/cdp/test/browser/runtime/browser_callFunctionOn_awaitPromise.js
new file mode 100644
index 0000000000..d15cd8d221
--- /dev/null
+++ b/remote/cdp/test/browser/runtime/browser_callFunctionOn_awaitPromise.js
@@ -0,0 +1,181 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+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 awaitPromiseDelayedRejectError({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ const { exceptionDetails } = await Runtime.callFunctionOn({
+ functionDeclaration:
+ "() => new Promise((_,r) => setTimeout(() => r(new Error('foo')), 0))",
+ awaitPromise: true,
+ executionContextId,
+ });
+
+ Assert.deepEqual(
+ exceptionDetails,
+ {
+ text: "foo",
+ },
+ "Exception details are passed to the client"
+ );
+});
+
+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");
+});
diff --git a/remote/cdp/test/browser/runtime/browser_callFunctionOn_returnByValue.js b/remote/cdp/test/browser/runtime/browser_callFunctionOn_returnByValue.js
new file mode 100644
index 0000000000..8ba8e896d9
--- /dev/null
+++ b/remote/cdp/test/browser/runtime/browser_callFunctionOn_returnByValue.js
@@ -0,0 +1,401 @@
+/* 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 returnByValueCyclicValue({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ const functionDeclarations = [
+ "() => { const b = { a: 1}; b.b = b; return b; }",
+ "() => window",
+ ];
+
+ for (const functionDeclaration of functionDeclarations) {
+ let errorThrown;
+ try {
+ await Runtime.callFunctionOn({
+ functionDeclaration,
+ executionContextId,
+ returnByValue: true,
+ });
+ } catch (e) {
+ errorThrown = e.message;
+ }
+ ok(errorThrown.includes("Object reference chain is too long"));
+ }
+});
+
+add_task(async function returnByValueNotPossible({ client }) {
+ const { Runtime } = client;
+
+ const { id: executionContextId } = await enableRuntime(client);
+
+ const functionDeclarations = [
+ "() => Symbol('foo')",
+ "() => [Symbol('foo')]",
+ "() => { return {a: Symbol('foo')}; }",
+ ];
+
+ for (const functionDeclaration of functionDeclarations) {
+ let errorThrown;
+ try {
+ await Runtime.callFunctionOn({
+ functionDeclaration,
+ executionContextId,
+ returnByValue: true,
+ });
+ } catch (e) {
+ errorThrown = e.message;
+ }
+ ok(errorThrown.includes("Object couldn't be returned by value"));
+ }
+});
+
+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"
+ );
+ }
+ }
+});
diff --git a/remote/cdp/test/browser/runtime/browser_consoleAPICalled.js b/remote/cdp/test/browser/runtime/browser_consoleAPICalled.js
new file mode 100644
index 0000000000..eac774bd28
--- /dev/null
+++ b/remote/cdp/test/browser/runtime/browser_consoleAPICalled.js
@@ -0,0 +1,380 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Request a longer timeout as we have many tests which are longer
+requestLongerTimeout(2);
+
+const PAGE_CONSOLE_EVENTS =
+ "https://example.com/browser/remote/cdp/test/browser/runtime/doc_console_events.html";
+const PAGE_CONSOLE_EVENTS_ONLOAD =
+ "https://example.com/browser/remote/cdp/test/browser/runtime/doc_console_events_onload.html";
+
+add_task(async function noEventsWhenRuntimeDomainDisabled({ client }) {
+ await runConsoleTest(client, 0, async () => {
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], () =>
+ content.console.log("foo")
+ );
+ });
+});
+
+add_task(async function noEventsAfterRuntimeDomainDisabled({ client }) {
+ const { Runtime } = client;
+
+ await Runtime.enable();
+ await Runtime.disable();
+
+ await runConsoleTest(client, 0, async () => {
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], () =>
+ content.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 consoleAPIBeforeEnable({ client }) {
+ const { Runtime } = client;
+ const timeBefore = Date.now();
+
+ const check = async () => {
+ const events = await runConsoleTest(
+ client,
+ 1,
+ async () => {
+ await Runtime.enable();
+ },
+ // Set custom before timestamp as the event is before our callback
+ { timeBefore }
+ );
+
+ is(events[0].type, "log", "Got expected type");
+ is(events[0].args[0].value, "foo", "Got expected argument value");
+ };
+
+ // Load the page which runs a log on load
+ await loadURL(PAGE_CONSOLE_EVENTS_ONLOAD);
+ await check();
+
+ // Disable and re-enable Runtime domain, should send event again
+ await Runtime.disable();
+ await check();
+});
+
+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"
+ );
+
+ const { callFrames } = events[0].stackTrace;
+ is(callFrames.length, 1, "Got expected amount of call frames");
+
+ is(callFrames[0].functionName, "", "Got expected call frame function name");
+ is(callFrames[0].lineNumber, 0, "Got expected call frame line number");
+ is(callFrames[0].columnNumber, 8, "Got expected call frame column number");
+ is(
+ callFrames[0].url,
+ "javascript:console.error('foo')",
+ "Got expected call frame URL"
+ );
+});
+
+add_task(async function consoleAPIByScript({ client }) {
+ const context = await enableRuntime(client);
+
+ const events = await runConsoleTest(client, 1, async () => {
+ await evaluate(client, context.id, function runLog() {
+ console.trace("foo");
+ });
+ });
+
+ const { callFrames } = events[0].stackTrace;
+ is(callFrames.length, 1, "Got expected amount of call frames");
+
+ is(
+ callFrames[0].functionName,
+ "runLog",
+ "Got expected call frame function name"
+ );
+ is(callFrames[0].lineNumber, 1, "Got expected call frame line number");
+ is(callFrames[0].columnNumber, 14, "Got expected call frame column number");
+ is(callFrames[0].url, "debugger eval code", "Got expected call frame URL");
+});
+
+add_task(async function consoleAPIByScriptSubstack({ client }) {
+ await loadURL(PAGE_CONSOLE_EVENTS);
+ const context = await enableRuntime(client);
+
+ const events = await runConsoleTest(client, 1, async () => {
+ await evaluate(client, context.id, () => {
+ document.getElementById("log-wrapper").click();
+ });
+ });
+
+ const { callFrames } = events[0].stackTrace;
+ is(callFrames.length, 5, "Got expected amount of call frames");
+
+ is(
+ callFrames[0].functionName,
+ "runLogCaller",
+ "Got expected call frame function name (frame 1)"
+ );
+ is(
+ callFrames[0].lineNumber,
+ 13,
+ "Got expected call frame line number (frame 1)"
+ );
+ is(
+ callFrames[0].columnNumber,
+ 16,
+ "Got expected call frame column number (frame 1)"
+ );
+ is(
+ callFrames[0].url,
+ PAGE_CONSOLE_EVENTS,
+ "Got expected call frame UR (frame 1)"
+ );
+
+ is(
+ callFrames[1].functionName,
+ "runLogChild",
+ "Got expected call frame function name (frame 2)"
+ );
+ is(
+ callFrames[1].lineNumber,
+ 17,
+ "Got expected call frame line number (frame 2)"
+ );
+ is(
+ callFrames[1].columnNumber,
+ 8,
+ "Got expected call frame column number (frame 2)"
+ );
+ is(
+ callFrames[1].url,
+ PAGE_CONSOLE_EVENTS,
+ "Got expected call frame URL (frame 2)"
+ );
+
+ is(
+ callFrames[2].functionName,
+ "runLogParent",
+ "Got expected call frame function name (frame 3)"
+ );
+ is(
+ callFrames[2].lineNumber,
+ 20,
+ "Got expected call frame line number (frame 3)"
+ );
+ is(
+ callFrames[2].columnNumber,
+ 6,
+ "Got expected call frame column number (frame 3)"
+ );
+ is(
+ callFrames[2].url,
+ PAGE_CONSOLE_EVENTS,
+ "Got expected call frame URL (frame 3)"
+ );
+
+ is(
+ callFrames[3].functionName,
+ "onclick",
+ "Got expected call frame function name (frame 4)"
+ );
+ is(
+ callFrames[3].lineNumber,
+ 0,
+ "Got expected call frame line number (frame 4)"
+ );
+ is(
+ callFrames[3].columnNumber,
+ 0,
+ "Got expected call frame column number (frame 4)"
+ );
+ is(
+ callFrames[3].url,
+ PAGE_CONSOLE_EVENTS,
+ "Got expected call frame URL (frame 4)"
+ );
+
+ is(
+ callFrames[4].functionName,
+ "",
+ "Got expected call frame function name (frame 5)"
+ );
+ is(
+ callFrames[4].lineNumber,
+ 1,
+ "Got expected call frame line number (frame 5)"
+ );
+ is(
+ callFrames[4].columnNumber,
+ 45,
+ "Got expected call frame column number (frame 5)"
+ );
+ is(
+ callFrames[4].url,
+ "debugger eval code",
+ "Got expected call frame URL (frame 5)"
+ );
+});
+
+async function runConsoleTest(client, eventCount, callback, options = {}) {
+ let { timeBefore } = 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}`,
+ });
+
+ 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 }) => {
+ const timestamp = payload.timestamp;
+
+ ok(
+ timestamp >= timeBefore && timestamp <= timeAfter,
+ `Timestamp ${timestamp} in expected range [${timeBefore} - ${timeAfter}]`
+ );
+ });
+
+ return consoleAPIentries.map(event => event.payload);
+}
diff --git a/remote/cdp/test/browser/runtime/browser_evaluate.js b/remote/cdp/test/browser/runtime/browser_evaluate.js
new file mode 100644
index 0000000000..c5e1965883
--- /dev/null
+++ b/remote/cdp/test/browser/runtime/browser_evaluate.js
@@ -0,0 +1,256 @@
+/* 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 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 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/cdp/test/browser/runtime/browser_evaluate_awaitPromise.js b/remote/cdp/test/browser/runtime/browser_evaluate_awaitPromise.js
new file mode 100644
index 0000000000..b99dfb5ba9
--- /dev/null
+++ b/remote/cdp/test/browser/runtime/browser_evaluate_awaitPromise.js
@@ -0,0 +1,169 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+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 awaitPromiseDelayedRejectError({ client }) {
+ const { Runtime } = client;
+
+ await enableRuntime(client);
+
+ const { exceptionDetails } = await Runtime.evaluate({
+ expression:
+ "new Promise((_,r) => setTimeout(() => r(new Error('foo')), 0))",
+ awaitPromise: true,
+ });
+
+ Assert.deepEqual(
+ exceptionDetails,
+ {
+ text: "foo",
+ },
+ "Exception details are passed to the client"
+ );
+});
+
+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");
+});
diff --git a/remote/cdp/test/browser/runtime/browser_evaluate_returnByValue.js b/remote/cdp/test/browser/runtime/browser_evaluate_returnByValue.js
new file mode 100644
index 0000000000..dfd816b85a
--- /dev/null
+++ b/remote/cdp/test/browser/runtime/browser_evaluate_returnByValue.js
@@ -0,0 +1,151 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+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 returnByValueCyclicValue({ client }) {
+ const { Runtime } = client;
+
+ await enableRuntime(client);
+
+ const expressions = ["const b = { a: 1}; b.b = b; b", "window"];
+
+ for (const expression of expressions) {
+ let errorThrown;
+ try {
+ await Runtime.evaluate({
+ expression,
+ returnByValue: true,
+ });
+ } catch (e) {
+ errorThrown = e.message;
+ }
+ ok(errorThrown.includes("Object reference chain is too long"));
+ }
+});
+
+add_task(async function returnByValueNotPossible({ client }) {
+ const { Runtime } = client;
+
+ await enableRuntime(client);
+
+ const expressions = ["Symbol(42)", "[Symbol(42)]", "{a: Symbol(42)}"];
+
+ for (const expression of expressions) {
+ let errorThrown;
+ try {
+ await Runtime.evaluate({
+ expression,
+ returnByValue: true,
+ });
+ } catch (e) {
+ errorThrown = e.message;
+ }
+ ok(errorThrown.includes("Object couldn't be returned by value"));
+ }
+});
+
+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"
+ );
+});
diff --git a/remote/cdp/test/browser/runtime/browser_exceptionThrown.js b/remote/cdp/test/browser/runtime/browser_exceptionThrown.js
new file mode 100644
index 0000000000..23e6dc2de9
--- /dev/null
+++ b/remote/cdp/test/browser/runtime/browser_exceptionThrown.js
@@ -0,0 +1,121 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const PAGE_CONSOLE_EVENTS =
+ "https://example.com/browser/remote/cdp/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, 8, "Got expected line number");
+ is(events[0].columnNumber, 10, "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(typeof callFrames[0].scriptId, "string", "Got scriptId as string");
+ is(callFrames[0].url, PAGE_CONSOLE_EVENTS, "Got expected url");
+ is(callFrames[0].lineNumber, 8, "Got expected line number");
+ is(callFrames[0].columnNumber, 10, "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,
+ `Timestamp ${timestamp} in expected range [${timeBefore} - ${timeAfter}]`
+ );
+ });
+
+ return exceptionThrownEvents.map(event => event.payload.exceptionDetails);
+}
diff --git a/remote/cdp/test/browser/runtime/browser_executionContextEvents.js b/remote/cdp/test/browser/runtime/browser_executionContextEvents.js
new file mode 100644
index 0000000000..9e6230901a
--- /dev/null
+++ b/remote/cdp/test/browser/runtime/browser_executionContextEvents.js
@@ -0,0 +1,332 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the Runtime execution context events
+
+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(PAGE_FRAME_URL);
+ 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(PAGE_FRAME_URL);
+ 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: PAGE_FRAME_URL });
+ 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(FRAMESET_SINGLE_URL);
+ 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,
+ BASE_ORIGIN,
+ "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,
+ BASE_ORIGIN,
+ "The frame's execution context origin is the frame"
+ );
+
+ // Check navigation from a frameset
+ const historyFrom = recordContextEvents(Runtime, 4);
+ await loadURL(PAGE_FRAME_URL);
+ 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, CREATED, DESTROYED],
+ });
+
+ 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,
+ BASE_ORIGIN,
+ "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(PAGE_FRAME_URL);
+ const previousContext = await enableRuntime(client);
+
+ const executionContextCreated = Runtime.executionContextCreated();
+ await loadURL(PAGE_URL);
+ 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,
+ PAGE_FRAME_URL,
+ "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(PAGE_FRAME_URL);
+ 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 = '${PAGE_FRAME_URL}';`,
+ });
+ 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/cdp/test/browser/runtime/browser_getProperties.js b/remote/cdp/test/browser/runtime/browser_getProperties.js
new file mode 100644
index 0000000000..f897c3dfcd
--- /dev/null
+++ b/remote/cdp/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/cdp/test/browser/runtime/browser_remoteObjects.js b/remote/cdp/test/browser/runtime/browser_remoteObjects.js
new file mode 100644
index 0000000000..9176028966
--- /dev/null
+++ b/remote/cdp/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("Cannot find context with specified id"),
+ "callFunctionOn throws on released target"
+ );
+ }
+}
diff --git a/remote/cdp/test/browser/runtime/browser_withDefaultPrefs.js b/remote/cdp/test/browser/runtime/browser_withDefaultPrefs.js
new file mode 100644
index 0000000000..20b8264f92
--- /dev/null
+++ b/remote/cdp/test/browser/runtime/browser_withDefaultPrefs.js
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function enableRuntime_noHangAfterNavigation({ client }) {
+ await loadURL(PAGE_URL);
+ await enableRuntime(client);
+});
diff --git a/remote/cdp/test/browser/runtime/browser_with_default_prefs.ini b/remote/cdp/test/browser/runtime/browser_with_default_prefs.ini
new file mode 100644
index 0000000000..a3f78c9961
--- /dev/null
+++ b/remote/cdp/test/browser/runtime/browser_with_default_prefs.ini
@@ -0,0 +1,13 @@
+[DEFAULT]
+tags = cdp
+subsuite = remote
+args =
+ --remote-debugging-port
+ --remote-allow-origins=null
+support-files =
+ !/remote/cdp/test/browser/chrome-remote-interface.js
+ !/remote/cdp/test/browser/head.js
+ doc_empty.html
+ head.js
+
+[browser_withDefaultPrefs.js]
diff --git a/remote/cdp/test/browser/runtime/doc_console_events.html b/remote/cdp/test/browser/runtime/doc_console_events.html
new file mode 100644
index 0000000000..f52fcfa555
--- /dev/null
+++ b/remote/cdp/test/browser/runtime/doc_console_events.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Empty page</title>
+ <script>
+ function throwError() {
+ let foo = {};
+ foo.click();
+ }
+
+ function runLogParent() {
+ function runLogCaller() {
+ console.trace("foo");
+ }
+
+ function runLogChild() {
+ runLogCaller();
+ }
+
+ runLogChild();
+ }
+ </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/>
+ <a id="log-wrapper" onclick="runLogParent()">console.log() in function wrappers</a><br/>
+</body>
+</html>
diff --git a/remote/cdp/test/browser/runtime/doc_console_events_onload.html b/remote/cdp/test/browser/runtime/doc_console_events_onload.html
new file mode 100644
index 0000000000..608a22991a
--- /dev/null
+++ b/remote/cdp/test/browser/runtime/doc_console_events_onload.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Console events onload</title>
+ <script>
+ console.log("foo");
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/remote/cdp/test/browser/runtime/doc_empty.html b/remote/cdp/test/browser/runtime/doc_empty.html
new file mode 100644
index 0000000000..e59d2d8901
--- /dev/null
+++ b/remote/cdp/test/browser/runtime/doc_empty.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Empty page</title>
+</head>
+<body>
+</body>
+</html>
diff --git a/remote/cdp/test/browser/runtime/doc_frame.html b/remote/cdp/test/browser/runtime/doc_frame.html
new file mode 100644
index 0000000000..e2efd61554
--- /dev/null
+++ b/remote/cdp/test/browser/runtime/doc_frame.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Frame page</title>
+</head>
+<body>
+</body>
+</html>
diff --git a/remote/cdp/test/browser/runtime/doc_frameset_single.html b/remote/cdp/test/browser/runtime/doc_frameset_single.html
new file mode 100644
index 0000000000..2ad56a140e
--- /dev/null
+++ b/remote/cdp/test/browser/runtime/doc_frameset_single.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Frameset with a single frame</title>
+</head>
+<body>
+ <iframe src="doc_frame.html"></iframe>
+</body>
+</html>
diff --git a/remote/cdp/test/browser/runtime/head.js b/remote/cdp/test/browser/runtime/head.js
new file mode 100644
index 0000000000..8f05f225ec
--- /dev/null
+++ b/remote/cdp/test/browser/runtime/head.js
@@ -0,0 +1,15 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/remote/cdp/test/browser/head.js",
+ this
+);
+
+const BASE_ORIGIN = "https://example.com";
+const BASE_PATH = `${BASE_ORIGIN}/browser/remote/cdp/test/browser/runtime`;
+const FRAMESET_SINGLE_URL = `${BASE_PATH}/doc_frameset_single.html`;
+const PAGE_FRAME_URL = `${BASE_PATH}/doc_frame.html`;
+const PAGE_URL = `${BASE_PATH}/doc_empty.html`;