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