Bug 1623581 - [remote] Refactor Runtime.evaluate and Runtime.callFunctionOn browser chrome tests. r=remote-protocol-reviewers,maja_zf
authorHenrik Skupin <mail@hskupin.info>
Tue, 24 Mar 2020 20:32:15 +0000
changeset 520286 9f32172ae499e754c49403a797fed6fa4aa9fd78
parent 520285 1f395d5f063e56749061c2ee773e9ede9104b80e
child 520287 36920c4ef85039eda7cdd7e5c03f7757f047dc9b
push id37246
push useropoprus@mozilla.com
push dateWed, 25 Mar 2020 03:40:33 +0000
treeherdermozilla-central@14b59d4adc95 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersremote-protocol-reviewers, maja_zf
bugs1623581
milestone76.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1623581 - [remote] Refactor Runtime.evaluate and Runtime.callFunctionOn browser chrome tests. r=remote-protocol-reviewers,maja_zf Tests are mixing APIs between each other, which this patch removes. Also error messages have been adjusted for both methods to be on par with Chrome. Differential Revision: https://phabricator.services.mozilla.com/D67840
remote/domains/content/Runtime.jsm
remote/test/browser/runtime/browser_callFunctionOn.js
remote/test/browser/runtime/browser_evaluate.js
--- a/remote/domains/content/Runtime.jsm
+++ b/remote/domains/content/Runtime.jsm
@@ -91,98 +91,160 @@ class Runtime extends ContentProcessDoma
   }
 
   disable() {
     if (this.enabled) {
       this.enabled = false;
     }
   }
 
-  evaluate({ expression, contextId = null } = {}) {
-    let context;
-    if (contextId) {
-      context = this.contexts.get(contextId);
-      if (!context) {
-        throw new Error(
-          `Unable to find execution context with id: ${contextId}`
-        );
-      }
-    } else {
-      context = this._getDefaultContextForWindow();
-    }
-
-    if (typeof expression != "string") {
-      throw new Error(
-        `Expecting 'expression' attribute to be a string. ` +
-          `But was: ${typeof expression}`
-      );
-    }
-
-    return context.evaluate(expression);
-  }
-
   releaseObject({ objectId }) {
     let context = null;
     for (const ctx of this.contexts.values()) {
       if (ctx.hasRemoteObject(objectId)) {
         context = ctx;
         break;
       }
     }
     if (!context) {
       throw new Error(`Unable to get execution context by ID: ${objectId}`);
     }
     context.releaseObject(objectId);
   }
 
-  callFunctionOn(request) {
+  /**
+   * Calls function with given declaration on the given object.
+   *
+   * Object group of the result is inherited from the target object.
+   *
+   * @param {Object} options
+   * @param {string} options.functionDeclaration
+   *     Declaration of the function to call.
+   * @param {Array.<Object>=} options.arguments
+   *     Call arguments. All call arguments must belong to the same
+   *     JavaScript world as the target object.
+   * @param {boolean=} options.awaitPromise
+   *     Whether execution should `await` for resulting value
+   *     and return once awaited promise is resolved.
+   * @param {number=} options.executionContextId
+   *     Specifies execution context which global object will be used
+   *     to call function on. Either executionContextId or objectId
+   *     should be specified.
+   * @param {string=} options.objectId
+   *     Identifier of the object to call function on.
+   *     Either objectId or executionContextId should be specified.
+   * @param {boolean=} options.returnByValue
+   *     Whether the result is expected to be a JSON object
+   *     which should be sent by value.
+   *
+   * @return {Object.<RemoteObject, ExceptionDetails>}
+   */
+  callFunctionOn(options = {}) {
+    if (typeof options.functionDeclaration != "string") {
+      throw new TypeError("functionDeclaration: string value expected");
+    }
+    if (
+      typeof options.arguments != "undefined" &&
+      !Array.isArray(options.arguments)
+    ) {
+      throw new TypeError("arguments: array value expected");
+    }
+    if (!["undefined", "boolean"].includes(typeof options.awaitPromise)) {
+      throw new TypeError("awaitPromise: boolean value expected");
+    }
+    if (!["undefined", "number"].includes(typeof options.executionContextId)) {
+      throw new TypeError("executionContextId: number value expected");
+    }
+    if (!["undefined", "string"].includes(typeof options.objectId)) {
+      throw new TypeError("objectId: string value expected");
+    }
+    if (!["undefined", "boolean"].includes(typeof options.returnByValue)) {
+      throw new TypeError("returnByValue: boolean value expected");
+    }
+
+    if (
+      typeof options.executionContextId == "undefined" &&
+      typeof options.objectId == "undefined"
+    ) {
+      throw new Error(
+        "Either objectId or executionContextId must be specified"
+      );
+    }
+
     let context = null;
     // When an `objectId` is passed, we want to execute the function of a given object
     // So we first have to find its ExecutionContext
-    if (request.objectId) {
+    if (options.objectId) {
       for (const ctx of this.contexts.values()) {
-        if (ctx.hasRemoteObject(request.objectId)) {
+        if (ctx.hasRemoteObject(options.objectId)) {
           context = ctx;
           break;
         }
       }
       if (!context) {
         throw new Error(
-          `Unable to get the context for object with id: ${request.objectId}`
+          `Unable to get the context for object with id: ${options.objectId}`
         );
       }
     } else {
-      context = this.contexts.get(request.executionContextId);
+      context = this.contexts.get(options.executionContextId);
       if (!context) {
-        throw new Error(
-          `Unable to find execution context with id: ${request.executionContextId}`
-        );
+        throw new Error("Cannot find context with specified id");
       }
     }
-    if (typeof request.functionDeclaration != "string") {
-      throw new Error(
-        "Expect 'functionDeclaration' attribute to be passed and be a string"
-      );
-    }
-    if (request.arguments && !Array.isArray(request.arguments)) {
-      throw new Error("Expect 'arguments' to be an array");
-    }
-    if (request.returnByValue && typeof request.returnByValue != "boolean") {
-      throw new Error("Expect 'returnByValue' to be a boolean");
+
+    return context.callFunctionOn(
+      options.functionDeclaration,
+      options.arguments,
+      options.returnByValue,
+      options.awaitPromise,
+      options.objectId
+    );
+  }
+
+  /**
+   * Evaluate expression on global object.
+   *
+   * @param {Object} options
+   * @param {string} options.expression
+   *     Expression to evaluate.
+   * @param {boolean=} options.awaitPromise [unsupported]
+   *     Whether execution should `await` for resulting value
+   *     and return once awaited promise is resolved.
+   * @param {number=} options.contextId
+   *     Specifies in which execution context to perform evaluation.
+   *     If the parameter is omitted the evaluation will be performed
+   *     in the context of the inspected page.
+   * @param {boolean=} options.returnByValue
+   *     Whether the result is expected to be a JSON object
+   *     that should be sent by value. Defaults to false.
+   * @param {boolean=} options.userGesture [unsupported]
+   *     Whether execution should be treated as initiated by user in the UI.
+   *
+   * @return {Object<RemoteObject, exceptionDetails>}
+   *     The evaluation result, and optionally exception details.
+   */
+  evaluate(options = {}) {
+    const { expression, contextId } = options;
+
+    if (typeof expression != "string") {
+      throw new Error("expression: string value expected");
     }
-    if (request.awaitPromise && typeof request.awaitPromise != "boolean") {
-      throw new Error("Expect 'awaitPromise' to be a boolean");
+
+    let context;
+    if (typeof contextId != "undefined") {
+      context = this.contexts.get(contextId);
+      if (!context) {
+        throw new Error("Cannot find context with specified id");
+      }
+    } else {
+      context = this._getDefaultContextForWindow();
     }
-    return context.callFunctionOn(
-      request.functionDeclaration,
-      request.arguments,
-      request.returnByValue,
-      request.awaitPromise,
-      request.objectId
-    );
+
+    return context.evaluate(expression);
   }
 
   getProperties({ objectId, ownProperties }) {
     for (const ctx of this.contexts.values()) {
       const obj = ctx.getRemoteObject(objectId);
       if (typeof obj != "undefined") {
         return ctx.getProperties({ objectId, ownProperties });
       }
--- a/remote/test/browser/runtime/browser_callFunctionOn.js
+++ b/remote/test/browser/runtime/browser_callFunctionOn.js
@@ -1,270 +1,802 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-// Test the Runtime.callFunctionOn
-// See also browser_runtime_evaluate, which covers basic usages of this method.
+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 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 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 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 executionContextId = await enableRuntime(client);
 
-add_task(async function({ client }) {
-  const firstContext = await testRuntimeEnable(client);
-  const contextId = firstContext.id;
-  await testObjectReferences(client, contextId);
-  await testExceptions(client, contextId);
-  await testReturnByValue(client, contextId);
-  await testAwaitPromise(client, contextId);
-  await testObjectId(client, contextId);
+  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 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 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, null, "The subtype is null for numbers");
+  is(result.value, 42, "The result is the promise's resolution");
+});
+
+add_task(async function awaitPromiseDelayedResolve({ client }) {
+  const { Runtime } = client;
+
+  const 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, null, "The subtype is null for numbers");
+  is(result.value, 42, "The result is the promise's resolution");
+});
+
+add_task(async function awaitPromiseReject({ client }) {
+  const { Runtime } = client;
+
+  const 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"
+  );
 });
 
-async function testRuntimeEnable({ Runtime }) {
-  // Enable watching for new execution context
-  await Runtime.enable();
-  info("Runtime domain has been enabled");
+add_task(async function awaitPromiseDelayedReject({ client }) {
+  const { Runtime } = client;
+
+  const 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 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 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 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 executionContextId = await enableRuntime(client);
+
+  const { result } = await Runtime.callFunctionOn({
+    functionDeclaration:
+      "() => new Promise((_,r) => setTimeout(() => r(42), 0))",
+    awaitPromise: false,
+    executionContextId,
+  });
 
-  // 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");
+  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"));
+  }
+});
 
-  return context;
-}
+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;
 
-async function testObjectReferences({ Runtime }, contextId) {
-  // First create a JS object remotely via Runtime.evaluate
-  const { result } = await Runtime.evaluate({
-    contextId,
-    expression: "({ foo: 1 })",
+  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 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, null, "The subtype is null for objects");
   ok(!!result.objectId, "Got an object id");
 
-  // Then increment the `foo` attribute of this JS object, while returning this
-  // attribute value
+  // Then apply a method on this object
   const { result: result2 } = await Runtime.callFunctionOn({
-    executionContextId: contextId,
-    functionDeclaration: "arg => ++arg.foo",
-    arguments: [{ objectId: result.objectId }],
+    functionDeclaration: "function () { return this.foo; }",
+    executionContextId,
+    objectId: result.objectId,
   });
+
   is(result2.type, "number", "The type is correct");
   is(result2.subtype, null, "The subtype is null for numbers");
-  is(
-    result2.value,
-    2,
-    "Updated the existing object and returned the incremented value"
+  is(result2.value, 42, "Expected value returned");
+});
+
+add_task(async function objectIdArgumentReference({ client }) {
+  const { Runtime } = client;
+
+  const 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, null, "The subtype is null 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.
+  // 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({
-    executionContextId: contextId,
     functionDeclaration: "arg => { arg.foo++; return arg; }",
     arguments: [{ objectId: result.objectId }],
+    executionContextId,
   });
+
   is(result3.type, "object", "The type is correct");
   is(result3.subtype, null, "The subtype is null for objects");
-  // Remote object are not having unique id. So you may have multiple object ids
+  // 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 stable");
+  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({
-    executionContextId: contextId,
     functionDeclaration: "arg => arg.foo",
     arguments: [{ objectId: result3.objectId }],
-  });
-  is(result4.type, "number", "The type is correct");
-  is(result4.subtype, null, "The subtype is null for numbers");
-  is(
-    result4.value,
-    3,
-    "Updated the existing object and returned the incremented value"
-  );
-}
-
-async function testExceptions({ Runtime }, executionContextId) {
-  // Test error when evaluating the function
-  let { exceptionDetails } = await Runtime.callFunctionOn({
     executionContextId,
-    functionDeclaration: "doesNotExists()",
   });
-  is(
-    exceptionDetails.text,
-    "doesNotExists is not defined",
-    "Exception message is passed to the client"
+
+  Assert.deepEqual(
+    result4,
+    {
+      type: "number",
+      value: 3,
+    },
+    "The result has the expected type and value"
   );
+});
 
-  // Test error when calling the function
-  ({ exceptionDetails } = await Runtime.callFunctionOn({
+add_task(async function returnAsObjectTypes({ client }) {
+  const { Runtime } = client;
+
+  const executionContextId = await enableRuntime(client);
+
+  const expressions = [
+    { expression: "({foo:true})", type: "object", subtype: null },
+    { expression: "Symbol('foo')", type: "symbol", subtype: null },
+    { 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" },
+    { expression: "document.documentElement", type: "object", subtype: "node" },
+    {
+      expression: "document.createElement('div')",
+      type: "object",
+      subtype: "node",
+    },
+  ];
+
+  for (const { expression, type, subtype } of expressions) {
+    const { result } = await Runtime.callFunctionOn({
+      functionDeclaration: `() => ${expression}`,
+      executionContextId,
+    });
+    is(
+      result.subtype,
+      subtype,
+      `Evaluating '${expression}' has the expected subtype`
+    );
+    is(result.type, type, "The type is correct");
+    ok(!!result.objectId, "Got an object id");
+  }
+});
+
+add_task(async function returnAsObjectPrimitiveTypes({ client }) {
+  const { Runtime } = client;
+
+  const 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 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 executionContextId = await enableRuntime(client);
+
+  const { result } = await Runtime.callFunctionOn({
+    functionDeclaration: "() => null",
     executionContextId,
-    functionDeclaration: "() => doesNotExists()",
-  }));
-  is(
-    exceptionDetails.text,
-    "doesNotExists is not defined",
-    "Exception message is passed to the client"
+  });
+  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 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 executionContextId = await enableRuntime(client);
 
-async function testReturnByValue({ Runtime }, executionContextId) {
+  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 executionContextId = await enableRuntime(client);
+
   const values = [
+    null,
     42,
+    42.0,
     "42",
-    42.0,
     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: `() => (${JSON.stringify(value)})`,
       executionContextId,
-      functionDeclaration: "() => (" + JSON.stringify(value) + ")",
       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"
     );
   }
+});
 
-  // Test non-serializable values
-  const nonSerializableNumbers = {
+add_task(async function returnByValueNotSerializable({ client }) {
+  const { Runtime } = client;
+
+  const executionContextId = await enableRuntime(client);
+
+  const notSerializableNumbers = {
     number: ["-0", "NaN", "Infinity", "-Infinity"],
     bigint: ["42n"],
   };
 
-  for (const type in nonSerializableNumbers) {
-    for (const unserializableValue of nonSerializableNumbers[type]) {
+  for (const type in notSerializableNumbers) {
+    for (const unserializableValue of notSerializableNumbers[type]) {
       const { result } = await Runtime.callFunctionOn({
+        functionDeclaration: `() => (${unserializableValue})`,
         executionContextId,
-        functionDeclaration: "a => a",
-        arguments: [{ unserializableValue }],
         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
+// Test undefined individually as JSON.stringify doesn't return a string
+add_task(async function returnByValueUndefined({ client }) {
+  const { Runtime } = client;
+
+  const executionContextId = await enableRuntime(client);
+
   const { result } = await Runtime.callFunctionOn({
+    functionDeclaration: "() => {}",
     executionContextId,
-    functionDeclaration: "() => {}",
     returnByValue: true,
   });
-  is(result.type, "undefined", "The returned value is undefined");
-}
 
-async function testAwaitPromise({ Runtime }, executionContextId) {
-  // First assert promise resolution with awaitPromise
-  let { result } = await Runtime.callFunctionOn({
-    executionContextId,
-    functionDeclaration: "() => Promise.resolve(42)",
-    awaitPromise: true,
-  });
-  is(result.type, "number", "The type is correct");
-  is(result.subtype, null, "The subtype is null for numbers");
-  is(result.value, 42, "The result is the promise's resolution");
-
-  // Also test promise rejection with awaitPromise
-  let { exceptionDetails } = await Runtime.callFunctionOn({
-    executionContextId,
-    functionDeclaration: "() => Promise.reject(42)",
-    awaitPromise: true,
-  });
-  is(
-    exceptionDetails.exception.value,
-    42,
-    "The result is the promise's rejection"
-  );
-
-  // Then check delayed promise resolution
-  ({ result } = await Runtime.callFunctionOn({
-    executionContextId,
-    functionDeclaration: "() => new Promise(r => setTimeout(() => r(42), 0))",
-    awaitPromise: true,
-  }));
-  is(result.type, "number", "The type is correct");
-  is(result.subtype, null, "The subtype is null for numbers");
-  is(result.value, 42, "The result is the promise's resolution");
-
-  // And delayed promise rejection
-  ({ exceptionDetails } = await Runtime.callFunctionOn({
-    executionContextId,
-    functionDeclaration:
-      "() => new Promise((_,r) => setTimeout(() => r(42), 0))",
-    awaitPromise: true,
-  }));
-  is(
-    exceptionDetails.exception.value,
-    42,
-    "The result is the promise's rejection"
+  Assert.deepEqual(
+    result,
+    {
+      type: "undefined",
+    },
+    "Undefined type is correct"
   );
+});
 
-  // Finally assert promise resolution without awaitPromise
-  ({ result } = await Runtime.callFunctionOn({
-    executionContextId,
-    functionDeclaration: "() => 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 returnByValueArguments({ client }) {
+  const { Runtime } = client;
+
+  const 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,
+    });
 
-  // As well as promise rejection without awaitPromise
-  ({ result } = await Runtime.callFunctionOn({
-    executionContextId,
-    functionDeclaration: "() => 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");
-}
+    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 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 executionContextId = await enableRuntime(client);
 
-async function testObjectId({ Runtime }, contextId) {
-  // First create an object via Runtime.evaluate
-  const { result } = await Runtime.evaluate({
-    contextId,
-    expression: "({ foo: 42 })",
+  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");
+});
+
+add_task(async function exceptionDetailsJavascriptError({ client }) {
+  const { Runtime } = client;
+
+  const executionContextId = await enableRuntime(client);
+
+  const { exceptionDetails } = await Runtime.callFunctionOn({
+    functionDeclaration: "doesNotExists()",
+    executionContextId,
   });
-  is(result.type, "object", "The type is correct");
-  is(result.subtype, null, "The subtype is null for objects");
-  ok(!!result.objectId, "Got an object id");
+
+  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 executionContextId = await enableRuntime(client);
+
+  const { exceptionDetails } = await Runtime.callFunctionOn({
+    functionDeclaration: "() => { throw new Error('foo') }",
+    executionContextId,
+  });
 
-  // Then apply a method on this object
-  const { result: result2 } = await Runtime.callFunctionOn({
-    executionContextId: contextId,
-    functionDeclaration: "function () { return this.foo; }",
-    objectId: result.objectId,
+  Assert.deepEqual(
+    exceptionDetails,
+    {
+      text: "foo",
+    },
+    "Exception details are passed to the client"
+  );
+});
+
+add_task(async function exceptionDetailsThrowValue({ client }) {
+  const { Runtime } = client;
+
+  const executionContextId = await enableRuntime(client);
+
+  const { exceptionDetails } = await Runtime.callFunctionOn({
+    functionDeclaration: "() => { throw 'foo' }",
+    executionContextId,
   });
-  is(result2.type, "number", "The type is correct");
-  is(result2.subtype, null, "The subtype is null for numbers");
-  is(
-    result2.value,
-    42,
-    "We have a good proof that the function was ran against the target object"
+
+  Assert.deepEqual(
+    exceptionDetails,
+    {
+      exception: {
+        type: "string",
+        value: "foo",
+      },
+    },
+    "Exception details are passed as a RemoteObject"
   );
+});
+
+async function enableRuntime(client) {
+  const { Runtime } = client;
+
+  // 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.id;
 }
--- a/remote/test/browser/runtime/browser_evaluate.js
+++ b/remote/test/browser/runtime/browser_evaluate.js
@@ -1,203 +1,55 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-// Test the Runtime execution context events
-
 const TEST_DOC = toDataURL("default-test-page");
 
-add_task(async function({ client }) {
-  await loadURL(TEST_DOC);
-
-  const firstContext = await testRuntimeEnable(client);
-  const contextId = firstContext.id;
-
-  await testEvaluate(client);
-  await testEvaluateWithContextId(client, contextId);
-  await testEvaluateInvalidContextId(client, contextId);
-
-  await testCallFunctionOn(client, contextId);
-  await testCallFunctionOnInvalidContextId(client, contextId);
-
+add_task(async function contextIdInvalidValue({ client }) {
   const { Runtime } = client;
 
-  // First test Runtime.evaluate, which accepts an JS expression string.
-  // This string may have instructions separated with `;` before ending
-  // with a JS value that is returned as a CDP `RemoteObject`.
-  function runtimeEvaluate(expression) {
-    return Runtime.evaluate({ contextId, expression });
-  }
-
-  // Then test Runtime.callFunctionOn, which accepts a JS string, but this
-  // time, it has to be a function. In this first test against callFunctionOn,
-  // we only assert the returned type and ignore the arguments.
-  function callFunctionOn(expression, instruction = false) {
-    if (instruction) {
-      return Runtime.callFunctionOn({
-        executionContextId: contextId,
-        functionDeclaration: `() => { ${expression} }`,
-      });
-    }
-    return Runtime.callFunctionOn({
-      executionContextId: contextId,
-      functionDeclaration: `() => ${expression}`,
-    });
+  let errorThrown = "";
+  try {
+    await Runtime.evaluate({ expression: "", contextId: -1 });
+  } catch (e) {
+    errorThrown = e.message;
   }
-
-  // Finally, run another test against Runtime.callFunctionOn in order to assert
-  // the arguments being passed to the executed function.
-  async function callFunctionOnArguments(expression, instruction = false) {
-    // First evaluate the expression via Runtime.evaluate in order to generate the
-    // CDP's `RemoteObject` for the given expression. A previous test already
-    // asserted the returned value of Runtime.evaluate, so we can trust this.
-    const { result } = await Runtime.evaluate({ contextId, expression });
-
-    // We then pass this RemoteObject as an argument to Runtime.callFunctionOn.
-    return Runtime.callFunctionOn({
-      executionContextId: contextId,
-      functionDeclaration: `arg => arg`,
-      arguments: [result],
-    });
-  }
-
-  for (const fun of [
-    runtimeEvaluate,
-    callFunctionOn,
-    callFunctionOnArguments,
-  ]) {
-    info("Test " + fun.name);
-    await testPrimitiveTypes(fun);
-    await testUnserializable(fun);
-    await testObjectTypes(fun);
-
-    // Tests involving an instruction (exception throwing, or errors) are not
-    // using any argument. So ignore these particular tests.
-    if (fun != callFunctionOnArguments) {
-      await testThrowError(fun);
-      await testThrowValue(fun);
-      await testJSError(fun);
-    }
-  }
+  ok(errorThrown.includes("Cannot find context with specified id"));
 });
 
-async function testRuntimeEnable({ Runtime }) {
-  // Enable watching for new execution context
-  await Runtime.enable();
-  info("Runtime domain has been enabled");
+add_task(async function contextIdNotSpecified({ client }) {
+  const { Runtime } = client;
 
-  // 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");
+  await loadURL(TEST_DOC);
+  await enableRuntime(client);
 
-  return context;
-}
+  const { result } = await Runtime.evaluate({ expression: "location.href" });
+  is(result.value, TEST_DOC, "Works against the current document");
+});
 
-async function testEvaluate({ Runtime }) {
-  const { result } = await Runtime.evaluate({ expression: "location.href" });
-  is(
-    result.value,
-    TEST_DOC,
-    "Runtime.evaluate works against the current document"
-  );
-}
+add_task(async function contextIdSpecified({ client }) {
+  const { Runtime } = client;
 
-async function testEvaluateWithContextId({ Runtime }, contextId) {
+  await loadURL(TEST_DOC);
+  const contextId = await enableRuntime(client);
+
   const { result } = await Runtime.evaluate({
+    expression: "location.href",
     contextId,
-    expression: "location.href",
-  });
-  is(
-    result.value,
-    TEST_DOC,
-    "Runtime.evaluate works against the targetted document"
-  );
-}
-
-async function testEvaluateInvalidContextId({ Runtime }, contextId) {
-  try {
-    await Runtime.evaluate({ contextId: -1, expression: "" });
-    ok(false, "Evaluate shouldn't pass");
-  } catch (e) {
-    ok(
-      e.message.includes("Unable to find execution context with id: -1"),
-      "Throws with the expected error message"
-    );
-  }
-}
-
-async function testCallFunctionOn({ Runtime }, executionContextId) {
-  const { result } = await Runtime.callFunctionOn({
-    executionContextId,
-    functionDeclaration: "() => location.href",
   });
-  is(
-    result.value,
-    TEST_DOC,
-    "Runtime.callFunctionOn works and is against the test page"
-  );
-}
-
-async function testCallFunctionOnInvalidContextId(
-  { Runtime },
-  executionContextId
-) {
-  try {
-    await Runtime.callFunctionOn({
-      executionContextId: -1,
-      functionDeclaration: "",
-    });
-    ok(false, "callFunctionOn shouldn't pass");
-  } catch (e) {
-    ok(
-      e.message.includes("Unable to find execution context with id: -1"),
-      "Throws with the expected error message"
-    );
-  }
-}
+  is(result.value, TEST_DOC, "Works against the targetted document");
+});
 
-async function testPrimitiveTypes(testFunction) {
-  const expressions = [42, "42", true, 4.2];
-  for (const expression of expressions) {
-    const { result } = await testFunction(JSON.stringify(expression));
-    is(result.value, expression, `Evaluating primitive '${expression}' works`);
-    is(result.type, typeof expression, `${expression} type is correct`);
-  }
-
-  // undefined doesn't work with JSON.stringify, so test it independently
-  let { result } = await testFunction("undefined");
-  is(result.value, undefined, "undefined works");
-  is(result.type, "undefined", "undefined type is correct");
+add_task(async function returnAsObjectTypes({ client }) {
+  const { Runtime } = client;
 
-  // `null` is special as it has its own subtype, is of type 'object' but is returned as
-  // a value, without an `objectId` attribute
-  ({ result } = await testFunction("null"));
-  is(result.value, null, "Evaluating 'null' works");
-  is(result.type, "object", "'null' type is correct");
-  is(result.subtype, "null", "'null' subtype is correct");
-  ok(!result.objectId, "'null' has no objectId");
-}
+  await enableRuntime(client);
 
-async function testUnserializable(testFunction) {
-  const expressions = ["-0", "NaN", "Infinity", "-Infinity"];
-  for (const expression of expressions) {
-    const { result } = await testFunction(expression);
-    is(
-      result.unserializableValue,
-      expression,
-      `Evaluating unserializable '${expression}' works`
-    );
-  }
-}
-
-async function testObjectTypes(testFunction) {
   const expressions = [
     { expression: "({foo:true})", type: "object", subtype: null },
     { expression: "Symbol('foo')", type: "symbol", subtype: null },
     { expression: "BigInt(42)", type: "bigint", subtype: null },
     { 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" },
@@ -212,45 +64,163 @@ async function testObjectTypes(testFunct
     {
       expression: "document.createElement('div')",
       type: "object",
       subtype: "node",
     },
   ];
 
   for (const { expression, type, subtype } of expressions) {
-    const { result } = await testFunction(expression);
+    const { result } = await Runtime.evaluate({ expression });
     is(
       result.subtype,
       subtype,
       `Evaluating '${expression}' has the expected subtype`
     );
     is(result.type, type, "The type is correct");
     ok(!!result.objectId, "Got an object id");
   }
-}
+});
+
+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);
 
-async function testThrowError(testFunction) {
-  const { exceptionDetails } = await testFunction(
-    "throw new Error('foo')",
-    true
+  const expressions = ["-0", "NaN", "Infinity", "-Infinity"];
+  for (const expression of expressions) {
+    const { result } = await Runtime.evaluate({ expression });
+    Assert.deepEqual(
+      result,
+      {
+        unserializableValue: 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"
   );
-  is(exceptionDetails.text, "foo", "Exception message is passed to the client");
-}
+});
+
+// undefined doesn't work with JSON.stringify, so test it independently
+add_task(async function returnAsObjectUndefined({ client }) {
+  const { Runtime } = client;
+
+  await enableRuntime(client);
 
-async function testThrowValue(testFunction) {
-  const { exceptionDetails } = await testFunction("throw 'foo'", true);
-  is(exceptionDetails.exception.type, "string", "Exception type is correct");
-  is(
-    exceptionDetails.exception.value,
-    "foo",
-    "Exception value is passed as a RemoteObject"
+  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"
+  );
+});
+
+async function enableRuntime(client) {
+  const { Runtime } = client;
+
+  // 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.id;
 }
-
-async function testJSError(testFunction) {
-  const { exceptionDetails } = await testFunction("doesNotExists()", true);
-  is(
-    exceptionDetails.text,
-    "doesNotExists is not defined",
-    "Exception message is passed to the client"
-  );
-}