summaryrefslogtreecommitdiffstats
path: root/devtools/shared/commands/script
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /devtools/shared/commands/script
parentInitial commit. (diff)
downloadfirefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz
firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--devtools/shared/commands/script/moz.build10
-rw-r--r--devtools/shared/commands/script/script-command.js149
-rw-r--r--devtools/shared/commands/script/tests/browser.ini11
-rw-r--r--devtools/shared/commands/script/tests/browser_script_command_execute_basic.js413
-rw-r--r--devtools/shared/commands/script/tests/browser_script_command_execute_document__proto__.js41
-rw-r--r--devtools/shared/commands/script/tests/browser_script_command_execute_last_result.js85
-rw-r--r--devtools/shared/commands/script/tests/browser_script_command_execute_throw.js75
-rw-r--r--devtools/shared/commands/script/tests/head.js46
8 files changed, 830 insertions, 0 deletions
diff --git a/devtools/shared/commands/script/moz.build b/devtools/shared/commands/script/moz.build
new file mode 100644
index 0000000000..2387c7e63b
--- /dev/null
+++ b/devtools/shared/commands/script/moz.build
@@ -0,0 +1,10 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "script-command.js",
+)
+
+if CONFIG["MOZ_BUILD_APP"] != "mobile/android":
+ BROWSER_CHROME_MANIFESTS += ["tests/browser.ini"]
diff --git a/devtools/shared/commands/script/script-command.js b/devtools/shared/commands/script/script-command.js
new file mode 100644
index 0000000000..93917944d5
--- /dev/null
+++ b/devtools/shared/commands/script/script-command.js
@@ -0,0 +1,149 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ getAdHocFrontOrPrimitiveGrip,
+ // eslint-disable-next-line mozilla/reject-some-requires
+} = require("resource://devtools/client/fronts/object.js");
+
+class ScriptCommand {
+ constructor({ commands }) {
+ this._commands = commands;
+ }
+
+ /**
+ * Execute a JavaScript expression.
+ *
+ * @param {String} expression: The code you want to evaluate.
+ * @param {Object} options: Options for evaluation:
+ * @param {Object} options.frameActor: a FrameActor ID. The actor holds a reference to
+ * a Debugger.Frame. This option allows you to evaluate the string in the frame
+ * of the given FrameActor.
+ * @param {String} options.url: the url to evaluate the script as. Defaults to "debugger eval code".
+ * @param {TargetFront} options.selectedTargetFront: When passed, the expression will be
+ * evaluated in the context of the target (as opposed to the default, top-level one).
+ * @param {String} options.selectedNodeActor: A NodeActor ID that may be used by helper
+ * functions that can reference the currently selected node in the Inspector, like $0.
+ * @param {String} options.selectedObjectActor: the actorID of a given objectActor.
+ * This is used by context menu entries to get a reference to an object, in order
+ * to perform some operation on it (copy it, store it as a global variable, …).
+ * @param {Number} options.innerWindowID: An optional window id to be used for the evaluation,
+ * instead of the regular webConsoleActor.evalWindow.
+ * This is used by functions that may want to evaluate in a different window (for
+ * example a non-remote iframe), like getting the elements of a given document.
+ * @param {object} options.mapped: An optional object indicating if the original expression
+ * entered by the users have been modified
+ * @param {boolean} options.mapped.await: true if the expression was a top-level await
+ * expression that was wrapped in an async-iife
+ *
+ * @return {Promise}: A promise that resolves with the response.
+ */
+ async execute(expression, options = {}) {
+ const {
+ selectedObjectActor,
+ selectedNodeActor,
+ frameActor,
+ selectedTargetFront,
+ } = options;
+
+ // Retrieve the right WebConsole front that relates either to (by order of priority):
+ // - the currently selected target in the context selector
+ // (selectedTargetFront argument),
+ // - the object picked in the console (when using store as global) (selectedObjectActor),
+ // - the currently selected Node in the inspector (selectedNodeActor),
+ // - the currently selected frame in the debugger (when paused) (frameActor),
+ // - the currently selected target in the iframe dropdown
+ // (selectedTargetFront from the TargetCommand)
+ let targetFront = this._commands.targetCommand.selectedTargetFront;
+
+ const selectedActor =
+ selectedObjectActor || selectedNodeActor || frameActor;
+
+ if (selectedTargetFront) {
+ targetFront = selectedTargetFront;
+ } else if (selectedActor) {
+ const selectedFront = this._commands.client.getFrontByID(selectedActor);
+ if (selectedFront) {
+ targetFront = selectedFront.targetFront;
+ }
+ }
+
+ const consoleFront = await targetFront.getFront("console");
+
+ // We call `evaluateJSAsync` RDP request, which immediately returns a simple `resultID`,
+ // for which we later receive a related `evaluationResult` RDP event, with the same resultID.
+ // The evaluation result will be contained in this RDP event.
+ let resultID;
+ const response = await new Promise(resolve => {
+ const offEvaluationResult = consoleFront.on(
+ "evaluationResult",
+ async packet => {
+ // In some cases, the evaluationResult event can be received before the call to
+ // evaluationJSAsync completes. So make sure to wait for the corresponding promise
+ // before handling the evaluationResult event.
+ await onEvaluateJSAsync;
+
+ if (packet.resultID === resultID) {
+ resolve(packet);
+ offEvaluationResult();
+ }
+ }
+ );
+
+ const onEvaluateJSAsync = consoleFront
+ .evaluateJSAsync({
+ text: expression,
+ eager: options.eager,
+ frameActor,
+ innerWindowID: options.innerWindowID,
+ mapped: options.mapped,
+ selectedNodeActor,
+ selectedObjectActor,
+ url: options.url,
+ })
+ .then(packet => {
+ resultID = packet.resultID;
+ });
+ });
+
+ // `response` is the packet sent via `evaluationResult` RDP event.
+ if (response.error) {
+ throw response;
+ }
+
+ if (response.result) {
+ response.result = getAdHocFrontOrPrimitiveGrip(
+ response.result,
+ consoleFront
+ );
+ }
+
+ if (response.helperResult?.object) {
+ response.helperResult.object = getAdHocFrontOrPrimitiveGrip(
+ response.helperResult.object,
+ consoleFront
+ );
+ }
+
+ if (response.exception) {
+ response.exception = getAdHocFrontOrPrimitiveGrip(
+ response.exception,
+ consoleFront
+ );
+ }
+
+ if (response.exceptionMessage) {
+ response.exceptionMessage = getAdHocFrontOrPrimitiveGrip(
+ response.exceptionMessage,
+ consoleFront
+ );
+ }
+
+ return response;
+ }
+}
+
+module.exports = ScriptCommand;
diff --git a/devtools/shared/commands/script/tests/browser.ini b/devtools/shared/commands/script/tests/browser.ini
new file mode 100644
index 0000000000..949c54f760
--- /dev/null
+++ b/devtools/shared/commands/script/tests/browser.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ !/devtools/client/shared/test/shared-head.js
+ head.js
+
+[browser_script_command_execute_basic.js]
+[browser_script_command_execute_document__proto__.js]
+[browser_script_command_execute_last_result.js]
+[browser_script_command_execute_throw.js]
diff --git a/devtools/shared/commands/script/tests/browser_script_command_execute_basic.js b/devtools/shared/commands/script/tests/browser_script_command_execute_basic.js
new file mode 100644
index 0000000000..5eff8c4520
--- /dev/null
+++ b/devtools/shared/commands/script/tests/browser_script_command_execute_basic.js
@@ -0,0 +1,413 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Testing basic expression evaluation
+const {
+ MAX_AUTOCOMPLETE_ATTEMPTS,
+ MAX_AUTOCOMPLETIONS,
+} = require("resource://devtools/shared/webconsole/js-property-provider.js");
+const {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+
+add_task(async () => {
+ const tab = await addTab(`data:text/html;charset=utf-8,
+ <script>
+ window.foobarObject = Object.create(
+ null,
+ Object.getOwnPropertyDescriptors({
+ foo: 1,
+ foobar: 2,
+ foobaz: 3,
+ omg: 4,
+ omgfoo: 5,
+ strfoo: "foobarz",
+ omgstr: "foobarz" + "abb".repeat(${DevToolsServer.LONG_STRING_LENGTH} * 2),
+ })
+ );
+
+ window.largeObject1 = Object.create(null);
+ for (let i = 0; i < ${MAX_AUTOCOMPLETE_ATTEMPTS} + 1; i++) {
+ window.largeObject1["a" + i] = i;
+ }
+
+ window.largeObject2 = Object.create(null);
+ for (let i = 0; i < ${MAX_AUTOCOMPLETIONS} * 2; i++) {
+ window.largeObject2["a" + i] = i;
+ }
+
+ var originalExec = RegExp.prototype.exec;
+
+ var promptIterable = { [Symbol.iterator]() { return { next: prompt } } };
+
+ function aliasedTest() {
+ const aliased = "ALIASED";
+ return [0].map(() => aliased)[0];
+ }
+ </script>`);
+
+ const commands = await CommandsFactory.forTab(tab);
+ await commands.targetCommand.startListening();
+
+ await doSimpleEval(commands);
+ await doWindowEval(commands);
+ await doEvalWithException(commands);
+ await doEvalWithHelper(commands);
+ await doEvalString(commands);
+ await doEvalLongString(commands);
+ await doEvalWithBinding(commands);
+ await forceLexicalInit(commands);
+ await doSimpleEagerEval(commands);
+ await doEagerEvalWithSideEffect(commands);
+ await doEagerEvalWithSideEffectIterator(commands);
+ await doEagerEvalWithSideEffectMonkeyPatched(commands);
+
+ await commands.destroy();
+});
+
+async function doSimpleEval(commands) {
+ info("test eval '2+2'");
+ const response = await commands.scriptCommand.execute("2+2");
+ checkObject(response, {
+ input: "2+2",
+ result: 4,
+ });
+
+ ok(!response.exception, "no eval exception");
+ ok(!response.helperResult, "no helper result");
+}
+
+async function doWindowEval(commands) {
+ info("test eval 'document'");
+ const response = await commands.scriptCommand.execute("document");
+ checkObject(response, {
+ input: "document",
+ result: {
+ type: "object",
+ class: "HTMLDocument",
+ actor: /[a-z]/,
+ },
+ });
+
+ ok(!response.exception, "no eval exception");
+ ok(!response.helperResult, "no helper result");
+}
+
+async function doEvalWithException(commands) {
+ info("test eval with exception");
+ const response = await commands.scriptCommand.execute(
+ "window.doTheImpossible()"
+ );
+ checkObject(response, {
+ input: "window.doTheImpossible()",
+ result: {
+ type: "undefined",
+ },
+ exceptionMessage: /doTheImpossible/,
+ });
+
+ ok(response.exception, "js eval exception");
+ ok(!response.helperResult, "no helper result");
+}
+
+async function doEvalWithHelper(commands) {
+ info("test eval with helper");
+ const response = await commands.scriptCommand.execute("clear()");
+ checkObject(response, {
+ input: "clear()",
+ result: {
+ type: "undefined",
+ },
+ helperResult: { type: "clearOutput" },
+ });
+
+ ok(!response.exception, "no eval exception");
+}
+
+async function doEvalString(commands) {
+ const response = await commands.scriptCommand.execute(
+ "window.foobarObject.strfoo"
+ );
+
+ checkObject(response, {
+ input: "window.foobarObject.strfoo",
+ result: "foobarz",
+ });
+}
+
+async function doEvalLongString(commands) {
+ const response = await commands.scriptCommand.execute(
+ "window.foobarObject.omgstr"
+ );
+
+ const str = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ function() {
+ return content.wrappedJSObject.foobarObject.omgstr;
+ }
+ );
+
+ const initial = str.substring(0, DevToolsServer.LONG_STRING_INITIAL_LENGTH);
+
+ checkObject(response, {
+ input: "window.foobarObject.omgstr",
+ result: {
+ type: "longString",
+ initial,
+ length: str.length,
+ },
+ });
+}
+
+async function doEvalWithBinding(commands) {
+ const response = await commands.scriptCommand.execute("document;");
+ const documentActor = response.result.actorID;
+
+ info("running a command with _self as document using selectedObjectActor");
+ const selectedObjectSame = await commands.scriptCommand.execute(
+ "_self === document",
+ {
+ selectedObjectActor: documentActor,
+ }
+ );
+ checkObject(selectedObjectSame, {
+ result: true,
+ });
+}
+
+async function forceLexicalInit(commands) {
+ info("test that failed let/const bindings are initialized to undefined");
+
+ const testData = [
+ {
+ stmt: "let foopie = wubbalubadubdub",
+ vars: ["foopie"],
+ },
+ {
+ stmt: "let {z, w={n}=null} = {}",
+ vars: ["z", "w"],
+ },
+ {
+ stmt: "let [a, b, c] = null",
+ vars: ["a", "b", "c"],
+ },
+ {
+ stmt: "const nein1 = rofl, nein2 = copter",
+ vars: ["nein1", "nein2"],
+ },
+ {
+ stmt: "const {ha} = null",
+ vars: ["ha"],
+ },
+ {
+ stmt: "const [haw=[lame]=null] = []",
+ vars: ["haw"],
+ },
+ {
+ stmt: "const [rawr, wat=[lame]=null] = []",
+ vars: ["rawr", "haw"],
+ },
+ {
+ stmt: "let {zzz: xyz=99, zwz: wb} = nexistepas()",
+ vars: ["xyz", "wb"],
+ },
+ {
+ stmt: "let {c3pdoh=101} = null",
+ vars: ["c3pdoh"],
+ },
+ {
+ stmt: "const {...x} = x",
+ vars: ["x"],
+ },
+ {
+ stmt: "const {xx,yy,...rest} = null",
+ vars: ["xx", "yy", "rest"],
+ },
+ ];
+
+ for (const data of testData) {
+ const response = await commands.scriptCommand.execute(data.stmt);
+ checkObject(response, {
+ input: data.stmt,
+ result: { type: "undefined" },
+ });
+ ok(response.exception, "expected exception");
+ for (const varName of data.vars) {
+ const response2 = await commands.scriptCommand.execute(varName);
+ checkObject(response2, {
+ input: varName,
+ result: { type: "undefined" },
+ });
+ ok(!response2.exception, "unexpected exception");
+ }
+ }
+}
+
+async function doSimpleEagerEval(commands) {
+ const testData = [
+ {
+ code: "2+2",
+ result: 4,
+ },
+ {
+ code: "(x => x * 2)(3)",
+ result: 6,
+ },
+ {
+ code: "[1, 2, 3].map(x => x * 2).join()",
+ result: "2,4,6",
+ },
+ {
+ code: `"abc".match(/a./)[0]`,
+ result: "ab",
+ },
+ {
+ code: "aliasedTest()",
+ result: "ALIASED",
+ },
+ ];
+
+ for (const { code, result } of testData) {
+ const response = await commands.scriptCommand.execute(code, {
+ eager: true,
+ });
+ checkObject(response, {
+ input: code,
+ result,
+ });
+
+ ok(!response.exception, "no eval exception");
+ ok(!response.helperResult, "no helper result");
+ }
+}
+
+async function doEagerEvalWithSideEffect(commands) {
+ const testData = [
+ // Modify environment.
+ "var a = 10; a;",
+
+ // Directly call a funtion with side effect.
+ "prompt();",
+
+ // Call a funtion with side effect inside a scripted function.
+ "(() => { prompt(); })()",
+
+ // Call a funtion with side effect from self-hosted JS function.
+ "[1, 2, 3].map(prompt)",
+
+ // Call a function with Function.prototype.call.
+ "Function.prototype.call.bind(Function.prototype.call)(prompt);",
+
+ // Call a function with Function.prototype.apply.
+ "Function.prototype.apply.bind(Function.prototype.apply)(prompt);",
+
+ // Indirectly call a function with Function.prototype.apply.
+ "Reflect.apply(prompt, null, []);",
+ "'aaaaaaaa'.replace(/(a)(a)(a)(a)(a)(a)(a)(a)/, prompt)",
+
+ // Indirect call on obj[Symbol.iterator]().next.
+ "Array.from(promptIterable)",
+ ];
+
+ for (const code of testData) {
+ const response = await commands.scriptCommand.execute(code, {
+ eager: true,
+ });
+ checkObject(response, {
+ input: code,
+ result: { type: "undefined" },
+ });
+
+ ok(!response.exception, "no eval exception");
+ ok(!response.helperResult, "no helper result");
+ }
+}
+
+async function doEagerEvalWithSideEffectIterator(commands) {
+ // Indirect call on %ArrayIterator%.prototype.next,
+
+ // Create an iterable object that reuses iterator across multiple call.
+ let response = await commands.scriptCommand.execute(`
+var arr = [1, 2, 3];
+var iterator = arr[Symbol.iterator]();
+var iterable = { [Symbol.iterator]() { return iterator; } };
+"ok";
+`);
+ checkObject(response, {
+ result: "ok",
+ });
+ ok(!response.exception, "no eval exception");
+ ok(!response.helperResult, "no helper result");
+
+ const testData = [
+ "Array.from(iterable)",
+ "new Map(iterable)",
+ "new Set(iterable)",
+ ];
+
+ for (const code of testData) {
+ response = await commands.scriptCommand.execute(code, {
+ eager: true,
+ });
+ checkObject(response, {
+ input: code,
+ result: { type: "undefined" },
+ });
+
+ ok(!response.exception, "no eval exception");
+ ok(!response.helperResult, "no helper result");
+ }
+
+ // Verify the iterator's internal state isn't modified.
+ response = await commands.scriptCommand.execute(`[...iterator].join(",")`);
+ checkObject(response, {
+ result: "1,2,3",
+ });
+ ok(!response.exception, "no eval exception");
+ ok(!response.helperResult, "no helper result");
+}
+
+async function doEagerEvalWithSideEffectMonkeyPatched(commands) {
+ // Patch the built-in function without eager evaluation.
+ let response = await commands.scriptCommand.execute(
+ `RegExp.prototype.exec = prompt; "patched"`
+ );
+ checkObject(response, {
+ result: "patched",
+ });
+ ok(!response.exception, "no eval exception");
+ ok(!response.helperResult, "no helper result");
+
+ // Test eager evaluation, where the patched built-in is called internally.
+ // This should be aborted.
+ const code = `"abc".match(/a./)[0]`;
+ response = await commands.scriptCommand.execute(code, { eager: true });
+ checkObject(response, {
+ input: code,
+ result: { type: "undefined" },
+ });
+
+ ok(!response.exception, "no eval exception");
+ ok(!response.helperResult, "no helper result");
+
+ // Undo the patch without eager evaluation.
+ response = await commands.scriptCommand.execute(
+ `RegExp.prototype.exec = originalExec; "unpatched"`
+ );
+ checkObject(response, {
+ result: "unpatched",
+ });
+ ok(!response.exception, "no eval exception");
+ ok(!response.helperResult, "no helper result");
+
+ // Test eager evaluation again, without the patch.
+ // This should be evaluated.
+ response = await commands.scriptCommand.execute(code, { eager: true });
+ checkObject(response, {
+ input: code,
+ result: "ab",
+ });
+}
diff --git a/devtools/shared/commands/script/tests/browser_script_command_execute_document__proto__.js b/devtools/shared/commands/script/tests/browser_script_command_execute_document__proto__.js
new file mode 100644
index 0000000000..28f56ebac3
--- /dev/null
+++ b/devtools/shared/commands/script/tests/browser_script_command_execute_document__proto__.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Testing evaluating document.__proto__
+
+add_task(async () => {
+ const tab = await addTab(
+ `data:text/html;charset=utf-8,Test evaluating document.__proto__`
+ );
+ const commands = await CommandsFactory.forTab(tab);
+ await commands.targetCommand.startListening();
+
+ const evaluationResponse = await commands.scriptCommand.execute(
+ "document.__proto__"
+ );
+ checkObject(evaluationResponse, {
+ input: "document.__proto__",
+ result: {
+ type: "object",
+ actor: /[a-z]/,
+ },
+ });
+
+ ok(!evaluationResponse.exception, "no eval exception");
+ ok(!evaluationResponse.helperResult, "no helper result");
+
+ const response = await evaluationResponse.result.getPrototypeAndProperties();
+ ok(!response.error, "no response error");
+
+ const props = response.ownProperties;
+ ok(props, "response properties available");
+
+ const expectedProps = Object.getOwnPropertyNames(
+ Object.getPrototypeOf(document)
+ );
+ checkObject(Object.keys(props), expectedProps, "Same own properties.");
+
+ await commands.destroy();
+});
diff --git a/devtools/shared/commands/script/tests/browser_script_command_execute_last_result.js b/devtools/shared/commands/script/tests/browser_script_command_execute_last_result.js
new file mode 100644
index 0000000000..aebdaeb168
--- /dev/null
+++ b/devtools/shared/commands/script/tests/browser_script_command_execute_last_result.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Testing that last evaluation result can be accessed with `$_`
+
+add_task(async () => {
+ const tab = await addTab(`data:text/html;charset=utf-8,`);
+
+ const commands = await CommandsFactory.forTab(tab);
+ await commands.targetCommand.startListening();
+
+ info("$_ returns undefined if nothing has evaluated yet");
+ let response = await commands.scriptCommand.execute("$_");
+ basicResultCheck(response, "$_", { type: "undefined" });
+
+ info("$_ returns last value and performs basic arithmetic");
+ response = await commands.scriptCommand.execute("2+2");
+ basicResultCheck(response, "2+2", 4);
+
+ response = await commands.scriptCommand.execute("$_");
+ basicResultCheck(response, "$_", 4);
+
+ response = await commands.scriptCommand.execute("$_ + 2");
+ basicResultCheck(response, "$_ + 2", 6);
+
+ response = await commands.scriptCommand.execute("$_ + 4");
+ basicResultCheck(response, "$_ + 4", 10);
+
+ info("$_ has correct references to objects");
+ response = await commands.scriptCommand.execute("var foo = {bar:1}; foo;");
+ basicResultCheck(response, "var foo = {bar:1}; foo;", {
+ type: "object",
+ class: "Object",
+ actor: /[a-z]/,
+ });
+ checkObject(response.result.getGrip().preview.ownProperties, {
+ bar: {
+ value: 1,
+ },
+ });
+
+ response = await commands.scriptCommand.execute("$_");
+ basicResultCheck(response, "$_", {
+ type: "object",
+ class: "Object",
+ actor: /[a-z]/,
+ });
+ checkObject(response.result.getGrip().preview.ownProperties, {
+ bar: {
+ value: 1,
+ },
+ });
+
+ info(
+ "Update a property value and check that evaluating $_ returns the expected object instance"
+ );
+ await ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
+ content.wrappedJSObject.foo.bar = "updated_value";
+ });
+
+ response = await commands.scriptCommand.execute("$_");
+ basicResultCheck(response, "$_", {
+ type: "object",
+ class: "Object",
+ actor: /[a-z]/,
+ });
+ checkObject(response.result.getGrip().preview.ownProperties, {
+ bar: {
+ value: "updated_value",
+ },
+ });
+
+ await commands.destroy();
+});
+
+function basicResultCheck(response, input, output) {
+ checkObject(response, {
+ input,
+ result: output,
+ });
+ ok(!response.exception, "no eval exception");
+ ok(!response.helperResult, "no helper result");
+}
diff --git a/devtools/shared/commands/script/tests/browser_script_command_execute_throw.js b/devtools/shared/commands/script/tests/browser_script_command_execute_throw.js
new file mode 100644
index 0000000000..8680193ecb
--- /dev/null
+++ b/devtools/shared/commands/script/tests/browser_script_command_execute_throw.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Testing evaluating thowing expressions
+const {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+
+add_task(async () => {
+ const tab = await addTab(`data:text/html;charset=utf-8,Test throw`);
+ const commands = await CommandsFactory.forTab(tab);
+ await commands.targetCommand.startListening();
+
+ const falsyValues = [
+ "-0",
+ "null",
+ "undefined",
+ "Infinity",
+ "-Infinity",
+ "NaN",
+ ];
+ for (const value of falsyValues) {
+ const response = await commands.scriptCommand.execute(`throw ${value};`);
+ is(
+ response.exception.type,
+ value,
+ `Got the expected value for response.exception.type when throwing "${value}"`
+ );
+ }
+
+ const identityTestValues = [false, 0];
+ for (const value of identityTestValues) {
+ const response = await commands.scriptCommand.execute(`throw ${value};`);
+ is(
+ response.exception,
+ value,
+ `Got the expected value for response.exception when throwing "${value}"`
+ );
+ }
+
+ const symbolTestValues = [
+ ["Symbol.iterator", "Symbol(Symbol.iterator)"],
+ ["Symbol('foo')", "Symbol(foo)"],
+ ["Symbol()", "Symbol()"],
+ ];
+ for (const [expr, message] of symbolTestValues) {
+ const response = await commands.scriptCommand.execute(`throw ${expr};`);
+ is(
+ response.exceptionMessage,
+ message,
+ `Got the expected value for response.exceptionMessage when throwing "${expr}"`
+ );
+ }
+
+ const longString = Array(DevToolsServer.LONG_STRING_LENGTH + 1).join("a"),
+ shortedString = longString.substring(
+ 0,
+ DevToolsServer.LONG_STRING_INITIAL_LENGTH
+ );
+ const response = await commands.scriptCommand.execute(
+ "throw '" + longString + "';"
+ );
+ is(
+ response.exception.initial,
+ shortedString,
+ "Got the expected value for exception.initial when throwing a longString"
+ );
+ is(
+ response.exceptionMessage.initial,
+ shortedString,
+ "Got the expected value for exceptionMessage.initial when throwing a longString"
+ );
+});
diff --git a/devtools/shared/commands/script/tests/head.js b/devtools/shared/commands/script/tests/head.js
new file mode 100644
index 0000000000..9b7591a9cf
--- /dev/null
+++ b/devtools/shared/commands/script/tests/head.js
@@ -0,0 +1,46 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../../../client/shared/test/shared-head.js */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
+
+function checkObject(object, expected) {
+ if (object && object.getGrip) {
+ object = object.getGrip();
+ }
+
+ for (const name of Object.keys(expected)) {
+ const expectedValue = expected[name];
+ const value = object[name];
+ checkValue(name, value, expectedValue);
+ }
+}
+
+function checkValue(name, value, expected) {
+ if (expected === null) {
+ is(value, null, `'${name}' is null`);
+ } else if (expected === undefined) {
+ is(value, expected, `'${name}' is undefined`);
+ } else if (
+ typeof expected == "string" ||
+ typeof expected == "number" ||
+ typeof expected == "boolean"
+ ) {
+ is(value, expected, "property '" + name + "'");
+ } else if (expected instanceof RegExp) {
+ ok(expected.test(value), name + ": " + expected + " matched " + value);
+ } else if (Array.isArray(expected)) {
+ info("checking array for property '" + name + "'");
+ checkObject(value, expected);
+ } else if (typeof expected == "object") {
+ info("checking object for property '" + name + "'");
+ checkObject(value, expected);
+ }
+}