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

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Henrik Skupin 2020-03-24 20:32:15 +00:00
parent 9f05df43d6
commit bc1a51934e
3 changed files with 977 additions and 413 deletions

View File

@ -96,29 +96,6 @@ class Runtime extends ContentProcessDomain {
}
}
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()) {
@ -133,53 +110,138 @@ class Runtime extends ContentProcessDomain {
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");
}
if (request.awaitPromise && typeof request.awaitPromise != "boolean") {
throw new Error("Expect 'awaitPromise' to be a boolean");
}
return context.callFunctionOn(
request.functionDeclaration,
request.arguments,
request.returnByValue,
request.awaitPromise,
request.objectId
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");
}
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.evaluate(expression);
}
getProperties({ objectId, ownProperties }) {
for (const ctx of this.contexts.values()) {
const obj = ctx.getRemoteObject(objectId);

View File

@ -3,132 +3,575 @@
"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({ 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);
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"));
});
async function testRuntimeEnable({ Runtime }) {
// Enable watching for new execution context
await Runtime.enable();
info("Runtime domain has been enabled");
add_task(async function functionDeclarationInvalidTypes({ 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");
const executionContextId = await enableRuntime(client);
return context;
}
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"));
}
});
async function testObjectReferences({ Runtime }, contextId) {
// First create a JS object remotely via Runtime.evaluate
const { result } = await Runtime.evaluate({
contextId,
expression: "({ foo: 1 })",
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);
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"
);
});
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,
});
is(result.type, "object", "The type is correct");
is(result.subtype, "promise", "The subtype is promise");
ok(!!result.objectId, "We got the object id for the promise");
ok(!result.exceptionDetails, "We do not receive any exception");
});
add_task(async function executionContextIdNorObjectIdSpecified({ client }) {
const { Runtime } = client;
let errorThrown = "";
try {
await Runtime.callFunctionOn({
functionDeclaration: "",
});
} catch (e) {
errorThrown = e.message;
}
ok(
errorThrown.includes(
"Either objectId or executionContextId must be specified"
)
);
});
add_task(async function executionContextIdInvalidTypes({ client }) {
const { Runtime } = client;
for (const executionContextId of [null, true, "foo", [], {}]) {
let errorThrown = "";
try {
await Runtime.callFunctionOn({
functionDeclaration: "",
executionContextId,
});
} catch (e) {
errorThrown = e.message;
}
ok(errorThrown.includes("executionContextId: number value expected"));
}
});
add_task(async function executionContextIdInvalidValue({ client }) {
const { Runtime } = client;
let errorThrown = "";
try {
await Runtime.callFunctionOn({
functionDeclaration: "",
executionContextId: -1,
});
} catch (e) {
errorThrown = e.message;
}
ok(errorThrown.includes("Cannot find context with specified id"));
});
add_task(async function objectIdInvalidTypes({ client }) {
const { Runtime } = client;
for (const objectId of [null, true, 1, [], {}]) {
let errorThrown = "";
try {
await Runtime.callFunctionOn({ functionDeclaration: "", objectId });
} catch (e) {
errorThrown = e.message;
}
ok(errorThrown.includes("objectId: string value expected"));
}
});
add_task(async function objectId({ client }) {
const { Runtime } = client;
const 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"
);
// Test error when calling the function
({ exceptionDetails } = await Runtime.callFunctionOn({
Assert.deepEqual(
result4,
{
type: "number",
value: 3,
},
"The result has the expected type and value"
);
});
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);
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);
async function testReturnByValue({ Runtime }, executionContextId) {
const values = [
null,
42,
"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: `() => (${JSON.stringify(value)})`,
executionContextId,
functionDeclaration: "() => (" + JSON.stringify(value) + ")",
returnByValue: true,
});
Assert.deepEqual(
result,
{
@ -139,21 +582,26 @@ async function testReturnByValue({ Runtime }, executionContextId) {
"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,
{
@ -165,106 +613,190 @@ async function testReturnByValue({ Runtime }, executionContextId) {
);
}
}
});
// 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);
// Test undefined individually as JSON.stringify doesn't return a string
const { result } = await Runtime.callFunctionOn({
executionContextId,
functionDeclaration: "() => {}",
executionContextId,
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"
Assert.deepEqual(
result,
{
type: "undefined",
},
"Undefined type is correct"
);
});
// 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");
add_task(async function returnByValueArguments({ client }) {
const { Runtime } = client;
// And delayed promise rejection
({ exceptionDetails } = await Runtime.callFunctionOn({
executionContextId,
functionDeclaration:
"() => new Promise((_,r) => setTimeout(() => r(42), 0))",
awaitPromise: true,
}));
is(
exceptionDetails.exception.value,
const executionContextId = await enableRuntime(client);
const values = [
42,
"The result is the promise's rejection"
);
42.0,
"42",
true,
false,
null,
{ foo: true },
{ foo: { bar: 42, str: "str", array: [1, 2, 3] } },
[42, "42", true],
[{ foo: true }],
];
// Finally assert promise resolution without awaitPromise
({ result } = await Runtime.callFunctionOn({
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 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);
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,
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");
// 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");
}
async function testObjectId({ Runtime }, contextId) {
// First create an object via Runtime.evaluate
const { result } = await Runtime.evaluate({
contextId,
expression: "({ foo: 42 })",
});
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 apply a method on this object
const { result: result2 } = await Runtime.callFunctionOn({
executionContextId: contextId,
functionDeclaration: "function () { return this.foo; }",
objectId: result.objectId,
});
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,
{
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,
});
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,
});
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;
}

View File

@ -3,196 +3,48 @@
"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}`,
});
}
// 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);
}
let errorThrown = "";
try {
await Runtime.evaluate({ expression: "", contextId: -1 });
} catch (e) {
errorThrown = e.message;
}
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;
}
async function testEvaluate({ Runtime }) {
const { result } = await Runtime.evaluate({ expression: "location.href" });
is(
result.value,
TEST_DOC,
"Runtime.evaluate works against the current document"
);
}
is(result.value, TEST_DOC, "Works against the current document");
});
add_task(async function contextIdSpecified({ client }) {
const { Runtime } = client;
await loadURL(TEST_DOC);
const contextId = await enableRuntime(client);
async function testEvaluateWithContextId({ Runtime }, contextId) {
const { result } = await Runtime.evaluate({
contextId,
expression: "location.href",
contextId,
});
is(
result.value,
TEST_DOC,
"Runtime.evaluate works against the targetted document"
);
}
is(result.value, TEST_DOC, "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"
);
}
}
add_task(async function returnAsObjectTypes({ client }) {
const { Runtime } = client;
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"
);
}
await enableRuntime(client);
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"
);
}
}
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");
// `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");
}
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 },
@ -217,7 +69,7 @@ async function testObjectTypes(testFunction) {
];
for (const { expression, type, subtype } of expressions) {
const { result } = await testFunction(expression);
const { result } = await Runtime.evaluate({ expression });
is(
result.subtype,
subtype,
@ -226,31 +78,149 @@ async function testObjectTypes(testFunction) {
is(result.type, type, "The type is correct");
ok(!!result.objectId, "Got an object id");
}
}
});
async function testThrowError(testFunction) {
const { exceptionDetails } = await testFunction(
"throw new Error('foo')",
true
);
is(exceptionDetails.text, "foo", "Exception message is passed to the client");
}
add_task(async function returnAsObjectPrimitiveTypes({ client }) {
const { Runtime } = 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"
);
}
await enableRuntime(client);
async function testJSError(testFunction) {
const { exceptionDetails } = await testFunction("doesNotExists()", true);
is(
exceptionDetails.text,
"doesNotExists is not defined",
"Exception message is passed to the 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 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"
);
});
// 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"
);
});
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;
}