summaryrefslogtreecommitdiffstats
path: root/devtools/server/tests/xpcshell
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/tests/xpcshell')
-rw-r--r--devtools/server/tests/xpcshell/.eslintrc.js9
-rw-r--r--devtools/server/tests/xpcshell/addons/web-extension-upgrade/manifest.json10
-rw-r--r--devtools/server/tests/xpcshell/addons/web-extension/manifest.json10
-rw-r--r--devtools/server/tests/xpcshell/addons/web-extension2/manifest.json10
-rw-r--r--devtools/server/tests/xpcshell/completions.js23
-rw-r--r--devtools/server/tests/xpcshell/head_dbg.js984
-rw-r--r--devtools/server/tests/xpcshell/hello-actor.js23
-rw-r--r--devtools/server/tests/xpcshell/post_init_global_actors.js22
-rw-r--r--devtools/server/tests/xpcshell/post_init_target_scoped_actors.js22
-rw-r--r--devtools/server/tests/xpcshell/pre_init_global_actors.js22
-rw-r--r--devtools/server/tests/xpcshell/pre_init_target_scoped_actors.js22
-rw-r--r--devtools/server/tests/xpcshell/registertestactors-lazy.js43
-rw-r--r--devtools/server/tests/xpcshell/setBreakpoint-on-column-in-gcd-script.js7
-rw-r--r--devtools/server/tests/xpcshell/setBreakpoint-on-column-minified.js8
-rw-r--r--devtools/server/tests/xpcshell/setBreakpoint-on-column-with-no-offsets-in-gcd-script.js7
-rw-r--r--devtools/server/tests/xpcshell/setBreakpoint-on-column-with-no-offsets.js5
-rw-r--r--devtools/server/tests/xpcshell/setBreakpoint-on-column.js5
-rw-r--r--devtools/server/tests/xpcshell/setBreakpoint-on-line-in-gcd-script.js9
-rw-r--r--devtools/server/tests/xpcshell/setBreakpoint-on-line-with-multiple-offsets.js7
-rw-r--r--devtools/server/tests/xpcshell/setBreakpoint-on-line-with-multiple-statements.js5
-rw-r--r--devtools/server/tests/xpcshell/setBreakpoint-on-line-with-no-offsets-in-gcd-script.js9
-rw-r--r--devtools/server/tests/xpcshell/setBreakpoint-on-line-with-no-offsets.js7
-rw-r--r--devtools/server/tests/xpcshell/setBreakpoint-on-line.js7
-rw-r--r--devtools/server/tests/xpcshell/source-03.js7
-rw-r--r--devtools/server/tests/xpcshell/source-map-data/sourcemapped.coffee6
-rw-r--r--devtools/server/tests/xpcshell/source-map-data/sourcemapped.map10
-rw-r--r--devtools/server/tests/xpcshell/sourcemapped.js16
-rw-r--r--devtools/server/tests/xpcshell/stepping-async.js31
-rw-r--r--devtools/server/tests/xpcshell/stepping.js36
-rw-r--r--devtools/server/tests/xpcshell/test_MemoryActor_saveHeapSnapshot_01.js22
-rw-r--r--devtools/server/tests/xpcshell/test_MemoryActor_saveHeapSnapshot_02.js24
-rw-r--r--devtools/server/tests/xpcshell/test_MemoryActor_saveHeapSnapshot_03.js22
-rw-r--r--devtools/server/tests/xpcshell/test_add_actors.js107
-rw-r--r--devtools/server/tests/xpcshell/test_addon_debugging_connect.js158
-rw-r--r--devtools/server/tests/xpcshell/test_addon_events.js60
-rw-r--r--devtools/server/tests/xpcshell/test_addon_reload.js116
-rw-r--r--devtools/server/tests/xpcshell/test_addons_actor.js55
-rw-r--r--devtools/server/tests/xpcshell/test_animation_name.js93
-rw-r--r--devtools/server/tests/xpcshell/test_animation_type.js72
-rw-r--r--devtools/server/tests/xpcshell/test_attach.js28
-rw-r--r--devtools/server/tests/xpcshell/test_blackboxing-01.js155
-rw-r--r--devtools/server/tests/xpcshell/test_blackboxing-02.js95
-rw-r--r--devtools/server/tests/xpcshell/test_blackboxing-03.js115
-rw-r--r--devtools/server/tests/xpcshell/test_blackboxing-04.js70
-rw-r--r--devtools/server/tests/xpcshell/test_blackboxing-05.js97
-rw-r--r--devtools/server/tests/xpcshell/test_blackboxing-08.js52
-rw-r--r--devtools/server/tests/xpcshell/test_breakpoint-01.js53
-rw-r--r--devtools/server/tests/xpcshell/test_breakpoint-03.js74
-rw-r--r--devtools/server/tests/xpcshell/test_breakpoint-04.js56
-rw-r--r--devtools/server/tests/xpcshell/test_breakpoint-05.js62
-rw-r--r--devtools/server/tests/xpcshell/test_breakpoint-06.js68
-rw-r--r--devtools/server/tests/xpcshell/test_breakpoint-07.js65
-rw-r--r--devtools/server/tests/xpcshell/test_breakpoint-08.js75
-rw-r--r--devtools/server/tests/xpcshell/test_breakpoint-09.js72
-rw-r--r--devtools/server/tests/xpcshell/test_breakpoint-10.js81
-rw-r--r--devtools/server/tests/xpcshell/test_breakpoint-11.js77
-rw-r--r--devtools/server/tests/xpcshell/test_breakpoint-12.js93
-rw-r--r--devtools/server/tests/xpcshell/test_breakpoint-13.js78
-rw-r--r--devtools/server/tests/xpcshell/test_breakpoint-14.js90
-rw-r--r--devtools/server/tests/xpcshell/test_breakpoint-16.js70
-rw-r--r--devtools/server/tests/xpcshell/test_breakpoint-17.js130
-rw-r--r--devtools/server/tests/xpcshell/test_breakpoint-18.js60
-rw-r--r--devtools/server/tests/xpcshell/test_breakpoint-19.js45
-rw-r--r--devtools/server/tests/xpcshell/test_breakpoint-20.js109
-rw-r--r--devtools/server/tests/xpcshell/test_breakpoint-21.js62
-rw-r--r--devtools/server/tests/xpcshell/test_breakpoint-22.js60
-rw-r--r--devtools/server/tests/xpcshell/test_breakpoint-23.js35
-rw-r--r--devtools/server/tests/xpcshell/test_breakpoint-24.js239
-rw-r--r--devtools/server/tests/xpcshell/test_breakpoint-25.js79
-rw-r--r--devtools/server/tests/xpcshell/test_breakpoint-26.js63
-rw-r--r--devtools/server/tests/xpcshell/test_breakpoint-actor-map.js257
-rw-r--r--devtools/server/tests/xpcshell/test_client_request.js220
-rw-r--r--devtools/server/tests/xpcshell/test_conditional_breakpoint-01.js54
-rw-r--r--devtools/server/tests/xpcshell/test_conditional_breakpoint-02.js52
-rw-r--r--devtools/server/tests/xpcshell/test_conditional_breakpoint-03.js52
-rw-r--r--devtools/server/tests/xpcshell/test_conditional_breakpoint-04.js56
-rw-r--r--devtools/server/tests/xpcshell/test_connection_closes_all_pools.js100
-rw-r--r--devtools/server/tests/xpcshell/test_console_eval-01.js33
-rw-r--r--devtools/server/tests/xpcshell/test_console_eval-02.js22
-rw-r--r--devtools/server/tests/xpcshell/test_dbgactor.js46
-rw-r--r--devtools/server/tests/xpcshell/test_dbgclient_debuggerstatement.js39
-rw-r--r--devtools/server/tests/xpcshell/test_dbgglobal.js86
-rw-r--r--devtools/server/tests/xpcshell/test_extension_storage_actor.js1155
-rw-r--r--devtools/server/tests/xpcshell/test_extension_storage_actor_upgrade.js142
-rw-r--r--devtools/server/tests/xpcshell/test_forwardingprefix.js226
-rw-r--r--devtools/server/tests/xpcshell/test_frameactor-01.js35
-rw-r--r--devtools/server/tests/xpcshell/test_frameactor-02.js36
-rw-r--r--devtools/server/tests/xpcshell/test_frameactor-03.js54
-rw-r--r--devtools/server/tests/xpcshell/test_frameactor-04.js64
-rw-r--r--devtools/server/tests/xpcshell/test_frameactor-05.js39
-rw-r--r--devtools/server/tests/xpcshell/test_frameactor_wasm-01.js67
-rw-r--r--devtools/server/tests/xpcshell/test_framearguments-01.js43
-rw-r--r--devtools/server/tests/xpcshell/test_framebindings-01.js71
-rw-r--r--devtools/server/tests/xpcshell/test_framebindings-02.js60
-rw-r--r--devtools/server/tests/xpcshell/test_framebindings-03.js63
-rw-r--r--devtools/server/tests/xpcshell/test_framebindings-04.js77
-rw-r--r--devtools/server/tests/xpcshell/test_framebindings-05.js54
-rw-r--r--devtools/server/tests/xpcshell/test_framebindings-06.js45
-rw-r--r--devtools/server/tests/xpcshell/test_framebindings-07.js41
-rw-r--r--devtools/server/tests/xpcshell/test_front_destroy.js42
-rw-r--r--devtools/server/tests/xpcshell/test_functiongrips-01.js64
-rw-r--r--devtools/server/tests/xpcshell/test_getRuleText.js143
-rw-r--r--devtools/server/tests/xpcshell/test_getTextAtLineColumn.js35
-rw-r--r--devtools/server/tests/xpcshell/test_get_command_and_arg.js121
-rw-r--r--devtools/server/tests/xpcshell/test_getyoungestframe.js38
-rw-r--r--devtools/server/tests/xpcshell/test_ignore_caught_exceptions.js53
-rw-r--r--devtools/server/tests/xpcshell/test_ignore_no_interface_exceptions.js50
-rw-r--r--devtools/server/tests/xpcshell/test_interrupt.js15
-rw-r--r--devtools/server/tests/xpcshell/test_layout-reflows-observer.js311
-rw-r--r--devtools/server/tests/xpcshell/test_listsources-01.js56
-rw-r--r--devtools/server/tests/xpcshell/test_listsources-02.js36
-rw-r--r--devtools/server/tests/xpcshell/test_listsources-03.js45
-rw-r--r--devtools/server/tests/xpcshell/test_logpoint-01.js83
-rw-r--r--devtools/server/tests/xpcshell/test_logpoint-02.js85
-rw-r--r--devtools/server/tests/xpcshell/test_logpoint-03.js82
-rw-r--r--devtools/server/tests/xpcshell/test_longstringgrips-01.js75
-rw-r--r--devtools/server/tests/xpcshell/test_nativewrappers.js39
-rw-r--r--devtools/server/tests/xpcshell/test_nesting-03.js50
-rw-r--r--devtools/server/tests/xpcshell/test_nesting-04.js86
-rw-r--r--devtools/server/tests/xpcshell/test_new_source-01.js24
-rw-r--r--devtools/server/tests/xpcshell/test_new_source-02.js46
-rw-r--r--devtools/server/tests/xpcshell/test_nodelistactor.js30
-rw-r--r--devtools/server/tests/xpcshell/test_objectgrips-02.js44
-rw-r--r--devtools/server/tests/xpcshell/test_objectgrips-03.js52
-rw-r--r--devtools/server/tests/xpcshell/test_objectgrips-04.js54
-rw-r--r--devtools/server/tests/xpcshell/test_objectgrips-05.js56
-rw-r--r--devtools/server/tests/xpcshell/test_objectgrips-06.js56
-rw-r--r--devtools/server/tests/xpcshell/test_objectgrips-07.js65
-rw-r--r--devtools/server/tests/xpcshell/test_objectgrips-08.js61
-rw-r--r--devtools/server/tests/xpcshell/test_objectgrips-14.js55
-rw-r--r--devtools/server/tests/xpcshell/test_objectgrips-15.js53
-rw-r--r--devtools/server/tests/xpcshell/test_objectgrips-16.js139
-rw-r--r--devtools/server/tests/xpcshell/test_objectgrips-17.js320
-rw-r--r--devtools/server/tests/xpcshell/test_objectgrips-18.js173
-rw-r--r--devtools/server/tests/xpcshell/test_objectgrips-19.js75
-rw-r--r--devtools/server/tests/xpcshell/test_objectgrips-20.js387
-rw-r--r--devtools/server/tests/xpcshell/test_objectgrips-21.js396
-rw-r--r--devtools/server/tests/xpcshell/test_objectgrips-22.js50
-rw-r--r--devtools/server/tests/xpcshell/test_objectgrips-23.js45
-rw-r--r--devtools/server/tests/xpcshell/test_objectgrips-24.js57
-rw-r--r--devtools/server/tests/xpcshell/test_objectgrips-25.js131
-rw-r--r--devtools/server/tests/xpcshell/test_objectgrips-fn-apply-01.js117
-rw-r--r--devtools/server/tests/xpcshell/test_objectgrips-fn-apply-02.js56
-rw-r--r--devtools/server/tests/xpcshell/test_objectgrips-fn-apply-03.js51
-rw-r--r--devtools/server/tests/xpcshell/test_objectgrips-nested-promise.js56
-rw-r--r--devtools/server/tests/xpcshell/test_objectgrips-nested-proxy.js51
-rw-r--r--devtools/server/tests/xpcshell/test_objectgrips-property-value-01.js148
-rw-r--r--devtools/server/tests/xpcshell/test_objectgrips-property-value-02.js53
-rw-r--r--devtools/server/tests/xpcshell/test_objectgrips-property-value-03.js63
-rw-r--r--devtools/server/tests/xpcshell/test_objectgrips-sparse-array.js40
-rw-r--r--devtools/server/tests/xpcshell/test_pause_exceptions-01.js43
-rw-r--r--devtools/server/tests/xpcshell/test_pause_exceptions-02.js40
-rw-r--r--devtools/server/tests/xpcshell/test_pause_exceptions-03.js53
-rw-r--r--devtools/server/tests/xpcshell/test_pause_exceptions-04.js93
-rw-r--r--devtools/server/tests/xpcshell/test_pauselifetime-01.js54
-rw-r--r--devtools/server/tests/xpcshell/test_pauselifetime-02.js57
-rw-r--r--devtools/server/tests/xpcshell/test_pauselifetime-03.js64
-rw-r--r--devtools/server/tests/xpcshell/test_pauselifetime-04.js40
-rw-r--r--devtools/server/tests/xpcshell/test_promise_state-01.js44
-rw-r--r--devtools/server/tests/xpcshell/test_promise_state-02.js59
-rw-r--r--devtools/server/tests/xpcshell/test_promise_state-03.js58
-rw-r--r--devtools/server/tests/xpcshell/test_promises_run_to_completion.js132
-rw-r--r--devtools/server/tests/xpcshell/test_register_actor.js94
-rw-r--r--devtools/server/tests/xpcshell/test_requestTypes.js28
-rw-r--r--devtools/server/tests/xpcshell/test_restartFrame-01.js118
-rw-r--r--devtools/server/tests/xpcshell/test_safe-getter.js54
-rw-r--r--devtools/server/tests/xpcshell/test_sessionDataHelpers.js124
-rw-r--r--devtools/server/tests/xpcshell/test_setBreakpoint-at-the-beginning-of-a-minified-fn.js41
-rw-r--r--devtools/server/tests/xpcshell/test_setBreakpoint-at-the-end-of-a-minified-fn.js41
-rw-r--r--devtools/server/tests/xpcshell/test_setBreakpoint-on-column-in-gcd-script.js46
-rw-r--r--devtools/server/tests/xpcshell/test_setBreakpoint-on-column.js36
-rw-r--r--devtools/server/tests/xpcshell/test_setBreakpoint-on-line-in-gcd-script.js45
-rw-r--r--devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-multiple-offsets.js52
-rw-r--r--devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-multiple-statements.js38
-rw-r--r--devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-no-offsets-in-gcd-script.js56
-rw-r--r--devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-no-offsets.js44
-rw-r--r--devtools/server/tests/xpcshell/test_setBreakpoint-on-line.js36
-rw-r--r--devtools/server/tests/xpcshell/test_shapes_highlighter_helpers.js274
-rw-r--r--devtools/server/tests/xpcshell/test_source-01.js58
-rw-r--r--devtools/server/tests/xpcshell/test_source-02.js64
-rw-r--r--devtools/server/tests/xpcshell/test_source-03.js75
-rw-r--r--devtools/server/tests/xpcshell/test_source-04.js74
-rw-r--r--devtools/server/tests/xpcshell/test_stepping-01.js94
-rw-r--r--devtools/server/tests/xpcshell/test_stepping-02.js57
-rw-r--r--devtools/server/tests/xpcshell/test_stepping-03.js43
-rw-r--r--devtools/server/tests/xpcshell/test_stepping-04.js50
-rw-r--r--devtools/server/tests/xpcshell/test_stepping-05.js0
-rw-r--r--devtools/server/tests/xpcshell/test_stepping-06.js0
-rw-r--r--devtools/server/tests/xpcshell/test_stepping-07.js0
-rw-r--r--devtools/server/tests/xpcshell/test_stepping-08.js0
-rw-r--r--devtools/server/tests/xpcshell/test_stepping-09.js47
-rw-r--r--devtools/server/tests/xpcshell/test_stepping-10.js52
-rw-r--r--devtools/server/tests/xpcshell/test_stepping-11.js25
-rw-r--r--devtools/server/tests/xpcshell/test_stepping-12.js162
-rw-r--r--devtools/server/tests/xpcshell/test_stepping-13.js39
-rw-r--r--devtools/server/tests/xpcshell/test_stepping-14.js52
-rw-r--r--devtools/server/tests/xpcshell/test_stepping-15.js78
-rw-r--r--devtools/server/tests/xpcshell/test_stepping-16.js81
-rw-r--r--devtools/server/tests/xpcshell/test_stepping-17.js69
-rw-r--r--devtools/server/tests/xpcshell/test_stepping-18.js100
-rw-r--r--devtools/server/tests/xpcshell/test_stepping-19.js93
-rw-r--r--devtools/server/tests/xpcshell/test_stepping-with-skip-breakpoints.js84
-rw-r--r--devtools/server/tests/xpcshell/test_symbolactor.js53
-rw-r--r--devtools/server/tests/xpcshell/test_symbols-01.js50
-rw-r--r--devtools/server/tests/xpcshell/test_symbols-02.js44
-rw-r--r--devtools/server/tests/xpcshell/test_threadlifetime-01.js56
-rw-r--r--devtools/server/tests/xpcshell/test_threadlifetime-02.js73
-rw-r--r--devtools/server/tests/xpcshell/test_threadlifetime-04.js58
-rw-r--r--devtools/server/tests/xpcshell/test_unsafeDereference.js130
-rw-r--r--devtools/server/tests/xpcshell/test_wasm_source-01.js143
-rw-r--r--devtools/server/tests/xpcshell/test_watchpoint-01.js197
-rw-r--r--devtools/server/tests/xpcshell/test_watchpoint-02.js223
-rw-r--r--devtools/server/tests/xpcshell/test_watchpoint-03.js72
-rw-r--r--devtools/server/tests/xpcshell/test_watchpoint-04.js78
-rw-r--r--devtools/server/tests/xpcshell/test_watchpoint-05.js113
-rw-r--r--devtools/server/tests/xpcshell/test_webext_apis.js162
-rw-r--r--devtools/server/tests/xpcshell/test_webextension_descriptor.js141
-rw-r--r--devtools/server/tests/xpcshell/test_xpcshell_debugging.js90
-rw-r--r--devtools/server/tests/xpcshell/testactors.js242
-rw-r--r--devtools/server/tests/xpcshell/webextension-helpers.js197
-rw-r--r--devtools/server/tests/xpcshell/xpcshell.toml436
-rw-r--r--devtools/server/tests/xpcshell/xpcshell_debugging_script.js11
222 files changed, 18251 insertions, 0 deletions
diff --git a/devtools/server/tests/xpcshell/.eslintrc.js b/devtools/server/tests/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..b3d0382a56
--- /dev/null
+++ b/devtools/server/tests/xpcshell/.eslintrc.js
@@ -0,0 +1,9 @@
+"use strict";
+
+module.exports = {
+ // Extend from the common devtools xpcshell eslintrc config.
+ extends: "../../../.eslintrc.xpcshell.js",
+ rules: {
+ "no-debugger": 0,
+ },
+};
diff --git a/devtools/server/tests/xpcshell/addons/web-extension-upgrade/manifest.json b/devtools/server/tests/xpcshell/addons/web-extension-upgrade/manifest.json
new file mode 100644
index 0000000000..cad9442b80
--- /dev/null
+++ b/devtools/server/tests/xpcshell/addons/web-extension-upgrade/manifest.json
@@ -0,0 +1,10 @@
+{
+ "manifest_version": 2,
+ "name": "Test Addons Actor Upgrade",
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "test-addons-actor@mozilla.org"
+ }
+ }
+}
diff --git a/devtools/server/tests/xpcshell/addons/web-extension/manifest.json b/devtools/server/tests/xpcshell/addons/web-extension/manifest.json
new file mode 100644
index 0000000000..47f07671e5
--- /dev/null
+++ b/devtools/server/tests/xpcshell/addons/web-extension/manifest.json
@@ -0,0 +1,10 @@
+{
+ "manifest_version": 2,
+ "name": "Test Addons Actor",
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "test-addons-actor@mozilla.org"
+ }
+ }
+}
diff --git a/devtools/server/tests/xpcshell/addons/web-extension2/manifest.json b/devtools/server/tests/xpcshell/addons/web-extension2/manifest.json
new file mode 100644
index 0000000000..e1ba91f4fb
--- /dev/null
+++ b/devtools/server/tests/xpcshell/addons/web-extension2/manifest.json
@@ -0,0 +1,10 @@
+{
+ "manifest_version": 2,
+ "name": "Test Addons Actor 2",
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "test-addons-actor2@mozilla.org"
+ }
+ }
+}
diff --git a/devtools/server/tests/xpcshell/completions.js b/devtools/server/tests/xpcshell/completions.js
new file mode 100644
index 0000000000..5e77e4e886
--- /dev/null
+++ b/devtools/server/tests/xpcshell/completions.js
@@ -0,0 +1,23 @@
+"use strict";
+/* exported global doRet doThrow */
+
+function ret() {
+ return 2;
+}
+
+function throws() {
+ throw new Error("yo");
+}
+
+function doRet() {
+ debugger;
+ const r = ret();
+ return r;
+}
+
+function doThrow() {
+ debugger;
+ try {
+ throws();
+ } catch (e) {}
+}
diff --git a/devtools/server/tests/xpcshell/head_dbg.js b/devtools/server/tests/xpcshell/head_dbg.js
new file mode 100644
index 0000000000..7161d5eaea
--- /dev/null
+++ b/devtools/server/tests/xpcshell/head_dbg.js
@@ -0,0 +1,984 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint no-unused-vars: ["error", {"vars": "local"}] */
+/* eslint-disable no-shadow */
+
+"use strict";
+var CC = Components.Constructor;
+
+// Populate AppInfo before anything (like the shared loader) accesses
+// System.appinfo, which is a lazy getter.
+const appInfo = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+);
+appInfo.updateAppInfo({
+ ID: "devtools@tests.mozilla.org",
+ name: "devtools-tests",
+ version: "1",
+ platformVersion: "42",
+ crashReporter: true,
+});
+
+const { require, loader } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+const { worker } = ChromeUtils.import(
+ "resource://devtools/shared/loader/worker-loader.js"
+);
+
+const { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+
+// Always log packets when running tests. runxpcshelltests.py will throw
+// the output away anyway, unless you give it the --verbose flag.
+Services.prefs.setBoolPref("devtools.debugger.log", false);
+// Enable remote debugging for the relevant tests.
+Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true);
+
+const makeDebugger = require("resource://devtools/server/actors/utils/make-debugger.js");
+const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
+const {
+ ActorRegistry,
+} = require("resource://devtools/server/actors/utils/actor-registry.js");
+const {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+const { DevToolsServer: WorkerDevToolsServer } = worker.require(
+ "resource://devtools/server/devtools-server.js"
+);
+const {
+ DevToolsClient,
+} = require("resource://devtools/client/devtools-client.js");
+const { ObjectFront } = require("resource://devtools/client/fronts/object.js");
+const {
+ LongStringFront,
+} = require("resource://devtools/client/fronts/string.js");
+const {
+ createCommandsDictionary,
+} = require("resource://devtools/shared/commands/index.js");
+const {
+ CommandsFactory,
+} = require("resource://devtools/shared/commands/commands-factory.js");
+
+const { addDebuggerToGlobal } = ChromeUtils.importESModule(
+ "resource://gre/modules/jsdebugger.sys.mjs"
+);
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+const { getAppInfo } = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+);
+
+const systemPrincipal = Cc["@mozilla.org/systemprincipal;1"].createInstance(
+ Ci.nsIPrincipal
+);
+
+var { loadSubScript, loadSubScriptWithOptions } = Services.scriptloader;
+
+/**
+ * The logic here must resemble the logic of --start-debugger-server as closely
+ * as possible. DevToolsStartup.sys.mjs uses a distinct loader that results in
+ * the existence of two isolated module namespaces. In practice, this can cause
+ * bugs such as bug 1837185.
+ */
+function getDistinctDevToolsServer() {
+ const {
+ useDistinctSystemPrincipalLoader,
+ releaseDistinctSystemPrincipalLoader,
+ } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs"
+ );
+ const requester = {};
+ const distinctLoader = useDistinctSystemPrincipalLoader(requester);
+ registerCleanupFunction(() => {
+ releaseDistinctSystemPrincipalLoader(requester);
+ });
+
+ const { DevToolsServer: DistinctDevToolsServer } = distinctLoader.require(
+ "resource://devtools/server/devtools-server.js"
+ );
+ return DistinctDevToolsServer;
+}
+
+/**
+ * Initializes any test that needs to work with add-ons.
+ *
+ * Should be called once per test script that needs to use AddonTestUtils (and
+ * not once per test task!).
+ */
+async function startupAddonsManager() {
+ // Create a directory for extensions.
+ const profileDir = do_get_profile().clone();
+ profileDir.append("extensions");
+
+ AddonTestUtils.init(globalThis);
+ AddonTestUtils.overrideCertDB();
+ AddonTestUtils.appInfo = getAppInfo();
+
+ await AddonTestUtils.promiseStartupManager();
+}
+
+async function createTargetForFakeTab(title) {
+ const client = await startTestDevToolsServer(title);
+
+ const tabs = await listTabs(client);
+ const tabDescriptor = findTab(tabs, title);
+
+ // These xpcshell tests use mocked actors (xpcshell-test/testactors)
+ // which still don't support watcher actor.
+ // Because of that we still can't enable server side targets and target swiching.
+ tabDescriptor.disableTargetSwitching();
+
+ return tabDescriptor.getTarget();
+}
+
+async function createTargetForMainProcess() {
+ const commands = await CommandsFactory.forMainProcess();
+ return commands.descriptorFront.getTarget();
+}
+
+/**
+ * Create a MemoryFront for a fake test tab.
+ */
+async function createTabMemoryFront() {
+ const target = await createTargetForFakeTab("test_memory");
+
+ // MemoryFront requires the HeadSnapshotActor actor to be available
+ // as a global actor. This isn't registered by startTestDevToolsServer which
+ // only register the target actors and not the browser ones.
+ DevToolsServer.registerActors({ browser: true });
+
+ const memoryFront = await target.getFront("memory");
+ await memoryFront.attach();
+
+ registerCleanupFunction(async () => {
+ await memoryFront.detach();
+
+ // On XPCShell, the target isn't for a local tab and so target.destroy
+ // won't close the client. So do it so here. It will automatically destroy the target.
+ await target.client.close();
+ });
+
+ return { target, memoryFront };
+}
+
+/**
+ * Same as createTabMemoryFront but attaches the MemoryFront to the MemoryActor
+ * scoped to the full runtime rather than to a tab.
+ */
+async function createMainProcessMemoryFront() {
+ const target = await createTargetForMainProcess();
+
+ const memoryFront = await target.getFront("memory");
+ await memoryFront.attach();
+
+ registerCleanupFunction(async () => {
+ await memoryFront.detach();
+ // For XPCShell, the main process target actor is ContentProcessTargetActor
+ // which doesn't expose any `detach` method. So that the target actor isn't
+ // destroyed when calling target.destroy.
+ // Close the client to cleanup everything.
+ await target.client.close();
+ });
+
+ return { client: target.client, memoryFront };
+}
+
+function createLongStringFront(conn, form) {
+ // CAUTION -- do not replicate in the codebase. Instead, use marshalling
+ // This code is simulating how the LongStringFront would be created by protocol.js
+ // We should not use it like this in the codebase, this is done only for testing
+ // purposes until we can return a proper LongStringFront from the server.
+ const front = new LongStringFront(conn, form);
+ front.actorID = form.actor;
+ front.manage(front);
+ return front;
+}
+
+function createTestGlobal(name, options) {
+ const principal = Cc["@mozilla.org/systemprincipal;1"].createInstance(
+ Ci.nsIPrincipal
+ );
+ // NOTE: The Sandbox constructor behaves differently based on the argument
+ // length.
+ const sandbox = options
+ ? Cu.Sandbox(principal, options)
+ : Cu.Sandbox(principal);
+ sandbox.__name = name;
+ // Expose a few mocks to better represent a Window object.
+ // These attributes will be used by DOCUMENT_EVENT resource listener.
+ sandbox.performance = { timing: {} };
+ sandbox.document = {
+ readyState: "complete",
+ defaultView: sandbox,
+ };
+ return sandbox;
+}
+
+function connect(client) {
+ dump("Connecting client.\n");
+ return client.connect();
+}
+
+function close(client) {
+ dump("Closing client.\n");
+ return client.close();
+}
+
+function listTabs(client) {
+ dump("Listing tabs.\n");
+ return client.mainRoot.listTabs();
+}
+
+function findTab(tabs, title) {
+ dump("Finding tab with title '" + title + "'.\n");
+ for (const tab of tabs) {
+ if (tab.title === title) {
+ return tab;
+ }
+ }
+ return null;
+}
+
+function waitForNewSource(threadFront, url) {
+ dump("Waiting for new source with url '" + url + "'.\n");
+ return waitForEvent(threadFront, "newSource", function (packet) {
+ return packet.source.url === url;
+ });
+}
+
+function attachThread(targetFront, options = {}) {
+ dump("Attaching to thread.\n");
+ return targetFront.attachThread(options);
+}
+
+function resume(threadFront) {
+ dump("Resuming thread.\n");
+ return threadFront.resume();
+}
+
+async function addWatchpoint(threadFront, frame, variable, property, type) {
+ const path = `${variable}.${property}`;
+ info(`Add an ${path} ${type} watchpoint`);
+ const environment = await frame.getEnvironment();
+ const obj = environment.bindings.variables[variable];
+ const objFront = threadFront.pauseGrip(obj.value);
+ return objFront.addWatchpoint(property, path, type);
+}
+
+function getSources(threadFront) {
+ dump("Getting sources.\n");
+ return threadFront.getSources();
+}
+
+function findSource(sources, url) {
+ dump("Finding source with url '" + url + "'.\n");
+ for (const source of sources) {
+ if (source.url === url) {
+ return source;
+ }
+ }
+ return null;
+}
+
+function waitForPause(threadFront) {
+ dump("Waiting for pause.\n");
+ return waitForEvent(threadFront, "paused");
+}
+
+function waitForProperty(dbg, property) {
+ return new Promise(resolve => {
+ Object.defineProperty(dbg, property, {
+ set(newValue) {
+ resolve(newValue);
+ },
+ });
+ });
+}
+
+function setBreakpoint(threadFront, location) {
+ dump("Setting breakpoint.\n");
+ return threadFront.setBreakpoint(location, {});
+}
+
+function getPrototypeAndProperties(objClient) {
+ dump("getting prototype and properties.\n");
+
+ return objClient.getPrototypeAndProperties();
+}
+
+function dumpn(msg) {
+ dump("DBG-TEST: " + msg + "\n");
+}
+
+function testExceptionHook(ex) {
+ try {
+ do_report_unexpected_exception(ex);
+ } catch (e) {
+ return { throw: e };
+ }
+ return undefined;
+}
+
+// Convert an nsIScriptError 'logLevel' value into an appropriate string.
+function scriptErrorLogLevel(message) {
+ switch (message.logLevel) {
+ case Ci.nsIConsoleMessage.info:
+ return "info";
+ case Ci.nsIConsoleMessage.warn:
+ return "warning";
+ default:
+ Assert.equal(message.logLevel, Ci.nsIConsoleMessage.error);
+ return "error";
+ }
+}
+
+// Register a console listener, so console messages don't just disappear
+// into the ether.
+var errorCount = 0;
+var listener = {
+ observe(message) {
+ try {
+ let string;
+ errorCount++;
+ try {
+ // If we've been given an nsIScriptError, then we can print out
+ // something nicely formatted, for tools like Emacs to pick up.
+ message.QueryInterface(Ci.nsIScriptError);
+ dumpn(
+ message.sourceName +
+ ":" +
+ message.lineNumber +
+ ": " +
+ scriptErrorLogLevel(message) +
+ ": " +
+ message.errorMessage
+ );
+ string = message.errorMessage;
+ } catch (e1) {
+ // Be a little paranoid with message, as the whole goal here is to lose
+ // no information.
+ try {
+ string = "" + message.message;
+ } catch (e2) {
+ string = "<error converting error message to string>";
+ }
+ }
+
+ // Make sure we exit all nested event loops so that the test can finish.
+ while (
+ DevToolsServer &&
+ DevToolsServer.xpcInspector &&
+ DevToolsServer.xpcInspector.eventLoopNestLevel > 0
+ ) {
+ DevToolsServer.xpcInspector.exitNestedEventLoop();
+ }
+
+ // In the world before bug 997440, exceptions were getting lost because of
+ // the arbitrary JSContext being used in nsXPCWrappedJS::CallMethod.
+ // In the new world, the wanderers have returned. However, because of the,
+ // currently very-broken, exception reporting machinery in
+ // nsXPCWrappedJS these get reported as errors to the console, even if
+ // there's actually JS on the stack above that will catch them. If we
+ // throw an error here because of them our tests start failing. So, we'll
+ // just dump the message to the logs instead, to make sure the information
+ // isn't lost.
+ dumpn("head_dbg.js observed a console message: " + string);
+ } catch (_) {
+ // Swallow everything to avoid console reentrancy errors. We did our best
+ // to log above, but apparently that didn't cut it.
+ }
+ },
+};
+
+Services.console.registerListener(listener);
+
+function addTestGlobal(name, server = DevToolsServer) {
+ const global = createTestGlobal(name);
+ server.addTestGlobal(global);
+ return global;
+}
+
+// List the DevToolsClient |client|'s tabs, look for one whose title is
+// |title|.
+async function getTestTab(client, title) {
+ const tabs = await client.mainRoot.listTabs();
+ for (const tab of tabs) {
+ if (tab.title === title) {
+ return tab;
+ }
+ }
+ return null;
+}
+/**
+ * Attach to the client's tab whose title is specified
+ * @param {Object} client
+ * @param {Object} title
+ * @returns commands
+ */
+async function attachTestTab(client, title) {
+ const descriptorFront = await getTestTab(client, title);
+
+ // These xpcshell tests use mocked actors (xpcshell-test/testactors)
+ // which still don't support watcher actor.
+ // Because of that we still can't enable server side targets and target swiching.
+ descriptorFront.disableTargetSwitching();
+
+ const commands = await createCommandsDictionary(descriptorFront);
+ await commands.targetCommand.startListening();
+ return commands;
+}
+
+/**
+ * Attach to the client's tab whose title is specified, and then attach to
+ * that tab's thread.
+ * @param {Object} client
+ * @param {Object} title
+ * @returns {Object}
+ * targetFront
+ * threadFront
+ * commands
+ */
+async function attachTestThread(client, title) {
+ const commands = await attachTestTab(client, title);
+ const targetFront = commands.targetCommand.targetFront;
+ const threadFront = await targetFront.getFront("thread");
+ await targetFront.attachThread({
+ autoBlackBox: true,
+ });
+ Assert.equal(threadFront.state, "attached", "Thread front is attached");
+ return { targetFront, threadFront, commands };
+}
+
+/**
+ * Initialize the testing devtools server.
+ */
+function initTestDevToolsServer(server = DevToolsServer) {
+ if (server === WorkerDevToolsServer) {
+ const { createRootActor } = worker.require("xpcshell-test/testactors");
+ server.setRootActor(createRootActor);
+ } else {
+ const { createRootActor } = require("xpcshell-test/testactors");
+ server.setRootActor(createRootActor);
+ }
+
+ // Allow incoming connections.
+ server.init(function () {
+ return true;
+ });
+}
+
+/**
+ * Initialize the testing devtools server with a tab whose title is |title|.
+ */
+async function startTestDevToolsServer(title, server = DevToolsServer) {
+ initTestDevToolsServer(server);
+ addTestGlobal(title);
+ DevToolsServer.registerActors({ target: true });
+
+ const transport = DevToolsServer.connectPipe();
+ const client = new DevToolsClient(transport);
+
+ await connect(client);
+ return client;
+}
+
+async function finishClient(client) {
+ await client.close();
+ DevToolsServer.destroy();
+ do_test_finished();
+}
+
+/**
+ * Takes a relative file path and returns the absolute file url for it.
+ */
+function getFileUrl(name, allowMissing = false) {
+ const file = do_get_file(name, allowMissing);
+ return Services.io.newFileURI(file).spec;
+}
+
+/**
+ * Returns the full path of the file with the specified name in a
+ * platform-independent and URL-like form.
+ */
+function getFilePath(
+ name,
+ allowMissing = false,
+ usePlatformPathSeparator = false
+) {
+ const file = do_get_file(name, allowMissing);
+ let path = Services.io.newFileURI(file).spec;
+ let filePrePath = "file://";
+ if ("nsILocalFileWin" in Ci && file instanceof Ci.nsILocalFileWin) {
+ filePrePath += "/";
+ }
+
+ path = path.slice(filePrePath.length);
+
+ if (usePlatformPathSeparator && path.match(/^\w:/)) {
+ path = path.replace(/\//g, "\\");
+ }
+
+ return path;
+}
+
+/**
+ * Returns the full text contents of the given file.
+ */
+function readFile(fileName) {
+ const f = do_get_file(fileName);
+ const s = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ s.init(f, -1, -1, false);
+ try {
+ return NetUtil.readInputStreamToString(s, s.available());
+ } finally {
+ s.close();
+ }
+}
+
+function writeFile(fileName, content) {
+ const file = do_get_file(fileName, true);
+ const stream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(
+ Ci.nsIFileOutputStream
+ );
+ stream.init(file, -1, -1, 0);
+ try {
+ do {
+ const numWritten = stream.write(content, content.length);
+ content = content.slice(numWritten);
+ } while (content.length);
+ } finally {
+ stream.close();
+ }
+}
+
+function StubTransport() {}
+StubTransport.prototype.ready = function () {};
+StubTransport.prototype.send = function () {};
+StubTransport.prototype.close = function () {};
+
+// Create async version of the object where calling each method
+// is equivalent of calling it with asyncall. Mainly useful for
+// destructuring objects with methods that take callbacks.
+const Async = target => new Proxy(target, Async);
+Async.get = (target, name) =>
+ typeof target[name] === "function"
+ ? asyncall.bind(null, target[name], target)
+ : target[name];
+
+// Calls async function that takes callback and errorback and returns
+// returns promise representing result.
+const asyncall = (fn, self, ...args) =>
+ new Promise((...etc) => fn.call(self, ...args, ...etc));
+
+const Test = task => () => {
+ add_task(task);
+ run_next_test();
+};
+
+const assert = Assert.ok.bind(Assert);
+
+/**
+ * Create a promise that is resolved on the next occurence of the given event.
+ *
+ * @param ThreadFront threadFront
+ * @param String event
+ * @param Function predicate
+ * @returns Promise
+ */
+function waitForEvent(front, type, predicate) {
+ if (!predicate) {
+ return front.once(type);
+ }
+
+ return new Promise(function (resolve) {
+ function listener(packet) {
+ if (!predicate(packet)) {
+ return;
+ }
+ front.off(type, listener);
+ resolve(packet);
+ }
+ front.on(type, listener);
+ });
+}
+
+/**
+ * Execute the action on the next tick and return a promise that is resolved on
+ * the next pause.
+ *
+ * When using promises and Task.jsm, we often want to do an action that causes a
+ * pause and continue the task once the pause has ocurred. Unfortunately, if we
+ * do the action that causes the pause within the task's current tick we will
+ * pause before we have a chance to yield the promise that waits for the pause
+ * and we enter a dead lock. The solution is to create the promise that waits
+ * for the pause, schedule the action to run on the next tick of the event loop,
+ * and finally yield the promise.
+ *
+ * @param Function action
+ * @param ThreadFront threadFront
+ * @returns Promise
+ */
+function executeOnNextTickAndWaitForPause(action, threadFront) {
+ const paused = waitForPause(threadFront);
+ executeSoon(action);
+ return paused;
+}
+
+function evalCallback(debuggeeGlobal, func) {
+ Cu.evalInSandbox("(" + func + ")()", debuggeeGlobal, "1.8", "test.js", 1);
+}
+
+/**
+ * Interrupt JS execution for the specified thread.
+ *
+ * @param ThreadFront threadFront
+ * @returns Promise
+ */
+function interrupt(threadFront) {
+ dumpn("Interrupting.");
+ return threadFront.interrupt();
+}
+
+/**
+ * Resume JS execution for the specified thread and then wait for the next pause
+ * event.
+ *
+ * @param DevToolsClient client
+ * @param ThreadFront threadFront
+ * @returns Promise
+ */
+async function resumeAndWaitForPause(threadFront) {
+ const paused = waitForPause(threadFront);
+ await resume(threadFront);
+ return paused;
+}
+
+/**
+ * Resume JS execution for a single step and wait for the pause after the step
+ * has been taken.
+ *
+ * @param ThreadFront threadFront
+ * @returns Promise
+ */
+function stepIn(threadFront) {
+ dumpn("Stepping in.");
+ const paused = waitForPause(threadFront);
+ return threadFront.stepIn().then(() => paused);
+}
+
+/**
+ * Resume JS execution for a step over and wait for the pause after the step
+ * has been taken.
+ *
+ * @param ThreadFront threadFront
+ * @returns Promise
+ */
+async function stepOver(threadFront, frameActor) {
+ dumpn("Stepping over.");
+ await threadFront.stepOver(frameActor);
+ return waitForPause(threadFront);
+}
+
+/**
+ * Resume JS execution for a step out and wait for the pause after the step
+ * has been taken.
+ *
+ * @param DevToolsClient client
+ * @param ThreadFront threadFront
+ * @returns Promise
+ */
+async function stepOut(threadFront, frameActor) {
+ dumpn("Stepping out.");
+ await threadFront.stepOut(frameActor);
+ return waitForPause(threadFront);
+}
+
+/**
+ * Restart specific frame and wait for the pause after the restart
+ * has been taken.
+ *
+ * @param DevToolsClient client
+ * @param ThreadFront threadFront
+ * @returns Promise
+ */
+async function restartFrame(threadFront, frameActor) {
+ dumpn("Restarting frame.");
+ await threadFront.restart(frameActor);
+ return waitForPause(threadFront);
+}
+
+/**
+ * Get the list of `count` frames currently on stack, starting at the index
+ * `first` for the specified thread.
+ *
+ * @param ThreadFront threadFront
+ * @param Number first
+ * @param Number count
+ * @returns Promise
+ */
+function getFrames(threadFront, first, count) {
+ dumpn("Getting frames.");
+ return threadFront.getFrames(first, count);
+}
+
+/**
+ * Black box the specified source.
+ *
+ * @param SourceFront sourceFront
+ * @returns Promise
+ */
+async function blackBox(sourceFront, range = null) {
+ dumpn("Black boxing source: " + sourceFront.actor);
+ const pausedInSource = await sourceFront.blackBox(range);
+ ok(true, "blackBox didn't throw");
+ return pausedInSource;
+}
+
+/**
+ * Stop black boxing the specified source.
+ *
+ * @param SourceFront sourceFront
+ * @returns Promise
+ */
+async function unBlackBox(sourceFront, range = null) {
+ dumpn("Un-black boxing source: " + sourceFront.actor);
+ await sourceFront.unblackBox(range);
+ ok(true, "unblackBox didn't throw");
+}
+
+/**
+ * Get a source at the specified url.
+ *
+ * @param ThreadFront threadFront
+ * @param string url
+ * @returns Promise<SourceFront>
+ */
+async function getSource(threadFront, url) {
+ const source = await getSourceForm(threadFront, url);
+ if (source) {
+ return threadFront.source(source);
+ }
+
+ throw new Error("source not found");
+}
+
+async function getSourceById(threadFront, id) {
+ const form = await getSourceFormById(threadFront, id);
+ return threadFront.source(form);
+}
+
+async function getSourceForm(threadFront, url) {
+ const { sources } = await threadFront.getSources();
+ return sources.find(s => s.url === url);
+}
+
+async function getSourceFormById(threadFront, id) {
+ const { sources } = await threadFront.getSources();
+ return sources.find(source => source.actor == id);
+}
+
+async function checkFramesLength(threadFront, expectedFrames) {
+ const frameResponse = await threadFront.getFrames(0, null);
+ Assert.equal(
+ frameResponse.frames.length,
+ expectedFrames,
+ "Thread front has the expected number of frames"
+ );
+}
+
+/**
+ * Do a reload which clears the thread debugger
+ *
+ * @param TabFront tabFront
+ * @returns Promise<response>
+ */
+function reload(tabFront) {
+ return tabFront.reload({});
+}
+
+/**
+ * Returns an array of stack location strings given a thread and a sample.
+ *
+ * @param object thread
+ * @param object sample
+ * @returns object
+ */
+function getInflatedStackLocations(thread, sample) {
+ const stackTable = thread.stackTable;
+ const frameTable = thread.frameTable;
+ const stringTable = thread.stringTable;
+ const SAMPLE_STACK_SLOT = thread.samples.schema.stack;
+ const STACK_PREFIX_SLOT = stackTable.schema.prefix;
+ const STACK_FRAME_SLOT = stackTable.schema.frame;
+ const FRAME_LOCATION_SLOT = frameTable.schema.location;
+
+ // Build the stack from the raw data and accumulate the locations in
+ // an array.
+ let stackIndex = sample[SAMPLE_STACK_SLOT];
+ const locations = [];
+ while (stackIndex !== null) {
+ const stackEntry = stackTable.data[stackIndex];
+ const frame = frameTable.data[stackEntry[STACK_FRAME_SLOT]];
+ locations.push(stringTable[frame[FRAME_LOCATION_SLOT]]);
+ stackIndex = stackEntry[STACK_PREFIX_SLOT];
+ }
+
+ // The profiler tree is inverted, so reverse the array.
+ return locations.reverse();
+}
+
+async function setupTestFromUrl(url) {
+ do_test_pending();
+
+ const { createRootActor } = require("xpcshell-test/testactors");
+ DevToolsServer.setRootActor(createRootActor);
+ DevToolsServer.init(() => true);
+
+ const global = createTestGlobal("test");
+ DevToolsServer.addTestGlobal(global);
+
+ const devToolsClient = new DevToolsClient(DevToolsServer.connectPipe());
+ await connect(devToolsClient);
+
+ const tabs = await listTabs(devToolsClient);
+ const descriptorFront = findTab(tabs, "test");
+
+ // These xpcshell tests use mocked actors (xpcshell-test/testactors)
+ // which still don't support watcher actor.
+ // Because of that we still can't enable server side targets and target swiching.
+ descriptorFront.disableTargetSwitching();
+
+ const targetFront = await descriptorFront.getTarget();
+
+ const threadFront = await attachThread(targetFront);
+
+ const sourceUrl = getFileUrl(url);
+ const promise = waitForNewSource(threadFront, sourceUrl);
+ loadSubScript(sourceUrl, global);
+ const { source } = await promise;
+
+ const sourceFront = threadFront.source(source);
+ return { global, devToolsClient, threadFront, sourceFront };
+}
+
+/**
+ * Run the given test function twice, one with a regular DevToolsServer,
+ * testing against a fake tab. And another one against a WorkerDevToolsServer,
+ * testing the worker codepath.
+ *
+ * @param Function test
+ * Test function to run twice.
+ * This test function is called with a dictionary:
+ * - Sandbox debuggee
+ * The custom JS debuggee created for this test. This is a Sandbox using system
+ * principals by default.
+ * - ThreadFront threadFront
+ * A reference to a ThreadFront instance that is attached to the debuggee.
+ * - DevToolsClient client
+ * A reference to the DevToolsClient used to communicated with the RDP server.
+ * @param Object options
+ * Optional arguments to tweak test environment
+ * - JSPrincipal principal
+ * Principal to use for the debuggee. Defaults to systemPrincipal.
+ * - boolean doNotRunWorker
+ * If true, do not run this tests in worker debugger context. Defaults to false.
+ * - bool wantXrays
+ * Whether the debuggee wants Xray vision with respect to same-origin objects
+ * outside the sandbox. Defaults to true.
+ * - bool waitForFinish
+ * Whether to wait for a call to threadFrontTestFinished after the test
+ * function finishes.
+ */
+function threadFrontTest(test, options = {}) {
+ const {
+ principal = systemPrincipal,
+ doNotRunWorker = false,
+ wantXrays = true,
+ waitForFinish = false,
+ } = options;
+
+ async function runThreadFrontTestWithServer(server, test) {
+ // Setup a server and connect a client to it.
+ initTestDevToolsServer(server);
+
+ // Create a custom debuggee and register it to the server.
+ // We are using a custom Sandbox as debuggee. Create a new zone because
+ // debugger and debuggee must be in different compartments.
+ const debuggee = Cu.Sandbox(principal, { freshZone: true, wantXrays });
+ const scriptName = "debuggee.js";
+ debuggee.__name = scriptName;
+ server.addTestGlobal(debuggee);
+
+ const client = new DevToolsClient(server.connectPipe());
+ await client.connect();
+
+ // Attach to the fake tab target and retrieve the ThreadFront instance.
+ // Automatically resume as the thread is paused by default after attach.
+ const { targetFront, threadFront, commands } = await attachTestThread(
+ client,
+ scriptName
+ );
+
+ // Cross the client/server boundary to retrieve the target actor & thread
+ // actor instances, used by some tests.
+ const rootActor = client.transport._serverConnection.rootActor;
+ const targetActor =
+ rootActor._parameters.tabList.getTargetActorForTab("debuggee.js");
+ const { threadActor } = targetActor;
+
+ // Run the test function
+ const args = {
+ threadActor,
+ threadFront,
+ debuggee,
+ client,
+ server,
+ targetFront,
+ commands,
+ isWorkerServer: server === WorkerDevToolsServer,
+ };
+ if (waitForFinish) {
+ // Use dispatchToMainThread so that the test function does not have to
+ // finish executing before the test itself finishes.
+ const promise = new Promise(
+ resolve => (threadFrontTestFinished = resolve)
+ );
+ Services.tm.dispatchToMainThread(() => test(args));
+ await promise;
+ } else {
+ await test(args);
+ }
+
+ // Cleanup the client after the test ran
+ await client.close();
+
+ server.removeTestGlobal(debuggee);
+
+ // Also cleanup the created server
+ server.destroy();
+ }
+
+ return async () => {
+ dump(">>> Run thread front test against a regular DevToolsServer\n");
+ await runThreadFrontTestWithServer(DevToolsServer, test);
+
+ // Skip tests that fail in the worker context
+ if (!doNotRunWorker) {
+ dump(">>> Run thread front test against a worker DevToolsServer\n");
+ await runThreadFrontTestWithServer(WorkerDevToolsServer, test);
+ }
+ };
+}
+
+// This callback is used in tandem with the waitForFinish option of
+// threadFrontTest to support thread front tests that use promises to
+// asynchronously finish the tests, instead of using async/await.
+// Newly written tests should avoid using this. See bug 1596114 for migrating
+// existing tests to async/await and removing this functionality.
+let threadFrontTestFinished;
diff --git a/devtools/server/tests/xpcshell/hello-actor.js b/devtools/server/tests/xpcshell/hello-actor.js
new file mode 100644
index 0000000000..f4fc63cb86
--- /dev/null
+++ b/devtools/server/tests/xpcshell/hello-actor.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint no-unused-vars: ["error", {"vars": "local"}] */
+
+"use strict";
+
+const protocol = require("resource://devtools/shared/protocol.js");
+
+const helloSpec = protocol.generateActorSpec({
+ typeName: "helloActor",
+
+ methods: {
+ hello: {},
+ },
+});
+
+class HelloActor extends protocol.Actor {
+ constructor(conn) {
+ super(conn, helloSpec);
+ }
+
+ hello() {}
+}
diff --git a/devtools/server/tests/xpcshell/post_init_global_actors.js b/devtools/server/tests/xpcshell/post_init_global_actors.js
new file mode 100644
index 0000000000..4ec5fb8078
--- /dev/null
+++ b/devtools/server/tests/xpcshell/post_init_global_actors.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { Actor } = require("resource://devtools/shared/protocol/Actor.js");
+
+class PostInitGlobalActor extends Actor {
+ constructor(conn) {
+ super(conn, { typeName: "postInitGlobal", methods: [] });
+
+ this.requestTypes = {
+ ping: this.onPing,
+ };
+ }
+
+ onPing() {
+ return { message: "pong" };
+ }
+}
+
+exports.PostInitGlobalActor = PostInitGlobalActor;
diff --git a/devtools/server/tests/xpcshell/post_init_target_scoped_actors.js b/devtools/server/tests/xpcshell/post_init_target_scoped_actors.js
new file mode 100644
index 0000000000..9b0b4c053e
--- /dev/null
+++ b/devtools/server/tests/xpcshell/post_init_target_scoped_actors.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { Actor } = require("resource://devtools/shared/protocol/Actor.js");
+
+class PostInitTargetScopedActor extends Actor {
+ constructor(conn) {
+ super(conn, { typeName: "postInitTargetScoped", methods: [] });
+
+ this.requestTypes = {
+ ping: this.onPing,
+ };
+ }
+
+ onPing() {
+ return { message: "pong" };
+ }
+}
+
+exports.PostInitTargetScopedActor = PostInitTargetScopedActor;
diff --git a/devtools/server/tests/xpcshell/pre_init_global_actors.js b/devtools/server/tests/xpcshell/pre_init_global_actors.js
new file mode 100644
index 0000000000..f5e14aaaa9
--- /dev/null
+++ b/devtools/server/tests/xpcshell/pre_init_global_actors.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { Actor } = require("resource://devtools/shared/protocol/Actor.js");
+
+class PreInitGlobalActor extends Actor {
+ constructor(conn) {
+ super(conn, { typeName: "preInitGlobal", methods: [] });
+
+ this.requestTypes = {
+ ping: this.onPing,
+ };
+ }
+
+ onPing() {
+ return { message: "pong" };
+ }
+}
+
+exports.PreInitGlobalActor = PreInitGlobalActor;
diff --git a/devtools/server/tests/xpcshell/pre_init_target_scoped_actors.js b/devtools/server/tests/xpcshell/pre_init_target_scoped_actors.js
new file mode 100644
index 0000000000..360d4b52a0
--- /dev/null
+++ b/devtools/server/tests/xpcshell/pre_init_target_scoped_actors.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { Actor } = require("resource://devtools/shared/protocol/Actor.js");
+
+class PreInitTargetScopedActor extends Actor {
+ constructor(conn) {
+ super(conn, { typeName: "preInitTargetScoped", methods: [] });
+
+ this.requestTypes = {
+ ping: this.onPing,
+ };
+ }
+
+ onPing() {
+ return { message: "pong" };
+ }
+}
+
+exports.PreInitTargetScopedActor = PreInitTargetScopedActor;
diff --git a/devtools/server/tests/xpcshell/registertestactors-lazy.js b/devtools/server/tests/xpcshell/registertestactors-lazy.js
new file mode 100644
index 0000000000..ef04e7a8d2
--- /dev/null
+++ b/devtools/server/tests/xpcshell/registertestactors-lazy.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var {
+ RetVal,
+ Actor,
+ FrontClassWithSpec,
+ generateActorSpec,
+} = require("resource://devtools/shared/protocol.js");
+
+const lazySpec = generateActorSpec({
+ typeName: "lazy",
+
+ methods: {
+ hello: {
+ response: { str: RetVal("string") },
+ },
+ },
+});
+
+class LazyActor extends Actor {
+ constructor(conn, id) {
+ super(conn, lazySpec);
+
+ Services.obs.notifyObservers(null, "actor", "instantiated");
+ }
+
+ hello(str) {
+ return "world";
+ }
+}
+exports.LazyActor = LazyActor;
+
+Services.obs.notifyObservers(null, "actor", "loaded");
+
+class LazyFront extends FrontClassWithSpec(lazySpec) {
+ constructor(client) {
+ super(client);
+ }
+}
+exports.LazyFront = LazyFront;
diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-column-in-gcd-script.js b/devtools/server/tests/xpcshell/setBreakpoint-on-column-in-gcd-script.js
new file mode 100644
index 0000000000..575915c4fd
--- /dev/null
+++ b/devtools/server/tests/xpcshell/setBreakpoint-on-column-in-gcd-script.js
@@ -0,0 +1,7 @@
+"use strict";
+
+function f() {}
+
+(function () {
+ var a = 1; var b = 2; var c = 3;
+})();
diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-column-minified.js b/devtools/server/tests/xpcshell/setBreakpoint-on-column-minified.js
new file mode 100644
index 0000000000..1fbf8ef16e
--- /dev/null
+++ b/devtools/server/tests/xpcshell/setBreakpoint-on-column-minified.js
@@ -0,0 +1,8 @@
+"use strict";
+
+function other(){ var a = 1; } function test(){ var a = 1; var b = 2; var c = 3; }
+
+function f() {
+ other();
+ test();
+} \ No newline at end of file
diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-column-with-no-offsets-in-gcd-script.js b/devtools/server/tests/xpcshell/setBreakpoint-on-column-with-no-offsets-in-gcd-script.js
new file mode 100644
index 0000000000..adce39193d
--- /dev/null
+++ b/devtools/server/tests/xpcshell/setBreakpoint-on-column-with-no-offsets-in-gcd-script.js
@@ -0,0 +1,7 @@
+"use strict";
+
+function f() {}
+
+(function () {
+ var a = 1; var c = 3;
+})();
diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-column-with-no-offsets.js b/devtools/server/tests/xpcshell/setBreakpoint-on-column-with-no-offsets.js
new file mode 100644
index 0000000000..5faefc3c88
--- /dev/null
+++ b/devtools/server/tests/xpcshell/setBreakpoint-on-column-with-no-offsets.js
@@ -0,0 +1,5 @@
+"use strict";
+
+function f() {
+ var a = 1; var c = 3;
+}
diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-column.js b/devtools/server/tests/xpcshell/setBreakpoint-on-column.js
new file mode 100644
index 0000000000..d92231e651
--- /dev/null
+++ b/devtools/server/tests/xpcshell/setBreakpoint-on-column.js
@@ -0,0 +1,5 @@
+"use strict";
+
+function f() {
+ var a = 1; var b = 2; var c = 3;
+}
diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-line-in-gcd-script.js b/devtools/server/tests/xpcshell/setBreakpoint-on-line-in-gcd-script.js
new file mode 100644
index 0000000000..fb96be8aba
--- /dev/null
+++ b/devtools/server/tests/xpcshell/setBreakpoint-on-line-in-gcd-script.js
@@ -0,0 +1,9 @@
+"use strict";
+
+function f() {}
+
+(function () {
+ var a = 1;
+ var b = 2;
+ var c = 3;
+})();
diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-multiple-offsets.js b/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-multiple-offsets.js
new file mode 100644
index 0000000000..b30ebb5049
--- /dev/null
+++ b/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-multiple-offsets.js
@@ -0,0 +1,7 @@
+"use strict";
+
+function f() {
+ for (var i = 0; i < 1; ++i) {
+ ;
+ }
+}
diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-multiple-statements.js b/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-multiple-statements.js
new file mode 100644
index 0000000000..d92231e651
--- /dev/null
+++ b/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-multiple-statements.js
@@ -0,0 +1,5 @@
+"use strict";
+
+function f() {
+ var a = 1; var b = 2; var c = 3;
+}
diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-no-offsets-in-gcd-script.js b/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-no-offsets-in-gcd-script.js
new file mode 100644
index 0000000000..b03d400794
--- /dev/null
+++ b/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-no-offsets-in-gcd-script.js
@@ -0,0 +1,9 @@
+"use strict";
+
+function f() {}
+
+(function () {
+ var a = 1;
+
+ var c = 3;
+})();
diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-no-offsets.js b/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-no-offsets.js
new file mode 100644
index 0000000000..1268cf8db0
--- /dev/null
+++ b/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-no-offsets.js
@@ -0,0 +1,7 @@
+"use strict";
+
+function f() {
+ var a = 1;
+
+ var c = 3;
+}
diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-line.js b/devtools/server/tests/xpcshell/setBreakpoint-on-line.js
new file mode 100644
index 0000000000..1b15e2a5e7
--- /dev/null
+++ b/devtools/server/tests/xpcshell/setBreakpoint-on-line.js
@@ -0,0 +1,7 @@
+"use strict";
+
+function f() {
+ var a = 1;
+ var b = 2;
+ var c = 3;
+}
diff --git a/devtools/server/tests/xpcshell/source-03.js b/devtools/server/tests/xpcshell/source-03.js
new file mode 100644
index 0000000000..af623a2eb2
--- /dev/null
+++ b/devtools/server/tests/xpcshell/source-03.js
@@ -0,0 +1,7 @@
+/* eslint-disable */
+
+function init() {
+ var a = foo();
+}
+
+function foo() {}
diff --git a/devtools/server/tests/xpcshell/source-map-data/sourcemapped.coffee b/devtools/server/tests/xpcshell/source-map-data/sourcemapped.coffee
new file mode 100644
index 0000000000..73a400a219
--- /dev/null
+++ b/devtools/server/tests/xpcshell/source-map-data/sourcemapped.coffee
@@ -0,0 +1,6 @@
+foo = (n) ->
+ return "foo" + i for i in [0...n]
+
+[first, second, third] = foo(3)
+
+debugger \ No newline at end of file
diff --git a/devtools/server/tests/xpcshell/source-map-data/sourcemapped.map b/devtools/server/tests/xpcshell/source-map-data/sourcemapped.map
new file mode 100644
index 0000000000..dcee3c33c3
--- /dev/null
+++ b/devtools/server/tests/xpcshell/source-map-data/sourcemapped.map
@@ -0,0 +1,10 @@
+{
+ "version": 3,
+ "file": "sourcemapped.js",
+ "sourceRoot": "",
+ "sources": [
+ "sourcemapped.coffee"
+ ],
+ "names": [],
+ "mappings": ";AAAA;CAAA,KAAA,yBAAA;CAAA;CAAA,CAAA,CAAA,MAAO;CACL,IAAA,GAAA;AAAA,CAAA,EAAA,MAA0B,qDAA1B;CAAA,EAAe,EAAR,QAAA;CAAP,IADI;CAAN,EAAM;;CAAN,CAGA,CAAyB,IAAA;;CAEzB,UALA;CAAA"
+} \ No newline at end of file
diff --git a/devtools/server/tests/xpcshell/sourcemapped.js b/devtools/server/tests/xpcshell/sourcemapped.js
new file mode 100644
index 0000000000..94d130903b
--- /dev/null
+++ b/devtools/server/tests/xpcshell/sourcemapped.js
@@ -0,0 +1,16 @@
+// Generated by CoffeeScript 1.6.1
+(function () {
+ var first, foo, second, third, _ref;
+
+ foo = function (n) {
+ var i, _i;
+ for (i = _i = 0; 0 <= n ? _i < n : _i > n; i = 0 <= n ? ++_i : --_i) {
+ return "foo" + i;
+ }
+ };
+
+ _ref = foo(3), first = _ref[0], second = _ref[1], third = _ref[2];
+
+ debugger;
+
+}).call(this);
diff --git a/devtools/server/tests/xpcshell/stepping-async.js b/devtools/server/tests/xpcshell/stepping-async.js
new file mode 100644
index 0000000000..0ee37883bc
--- /dev/null
+++ b/devtools/server/tests/xpcshell/stepping-async.js
@@ -0,0 +1,31 @@
+"use strict";
+/* exported stuff */
+
+async function timer() {
+ return Promise.resolve();
+}
+
+function oops() {
+ return `oops`;
+}
+
+async function inner() {
+ oops();
+ await timer();
+ Promise.resolve().then(async () => {
+ Promise.resolve().then(() => {
+ oops();
+ });
+ oops();
+ });
+ oops();
+}
+
+async function stuff() {
+ debugger;
+ const task = inner();
+ oops();
+ await task;
+ oops();
+ debugger;
+}
diff --git a/devtools/server/tests/xpcshell/stepping.js b/devtools/server/tests/xpcshell/stepping.js
new file mode 100644
index 0000000000..2134bea38d
--- /dev/null
+++ b/devtools/server/tests/xpcshell/stepping.js
@@ -0,0 +1,36 @@
+"use strict";
+/* exported global arithmetic composition chaining nested */
+
+const obj = { b };
+
+function a() {
+ return obj;
+}
+
+function b() {
+ return 2;
+}
+
+function arithmetic() {
+ debugger;
+ a() + b();
+}
+
+function composition() {
+ debugger;
+ b(a());
+}
+
+function chaining() {
+ debugger;
+ a().b();
+}
+
+function c() {
+ return b();
+}
+
+function nested() {
+ debugger;
+ c();
+}
diff --git a/devtools/server/tests/xpcshell/test_MemoryActor_saveHeapSnapshot_01.js b/devtools/server/tests/xpcshell/test_MemoryActor_saveHeapSnapshot_01.js
new file mode 100644
index 0000000000..7df3cbd2ba
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_MemoryActor_saveHeapSnapshot_01.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that we can tell the memory actor to take a heap snapshot over the RDP
+// and then create a HeapSnapshot instance from the resulting file.
+
+add_task(async () => {
+ const { memoryFront } = await createTabMemoryFront();
+
+ const snapshotFilePath = await memoryFront.saveHeapSnapshot();
+ ok(
+ !!(await IOUtils.stat(snapshotFilePath)),
+ "Should have the heap snapshot file"
+ );
+ const snapshot = ChromeUtils.readHeapSnapshot(snapshotFilePath);
+ ok(
+ HeapSnapshot.isInstance(snapshot),
+ "And we should be able to read a HeapSnapshot instance from the file"
+ );
+});
diff --git a/devtools/server/tests/xpcshell/test_MemoryActor_saveHeapSnapshot_02.js b/devtools/server/tests/xpcshell/test_MemoryActor_saveHeapSnapshot_02.js
new file mode 100644
index 0000000000..91593d845f
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_MemoryActor_saveHeapSnapshot_02.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that we can properly stream heap snapshot files over the RDP as bulk
+// data.
+
+add_task(async () => {
+ const { memoryFront } = await createTabMemoryFront();
+
+ const snapshotFilePath = await memoryFront.saveHeapSnapshot({
+ forceCopy: true,
+ });
+ ok(
+ !!(await IOUtils.stat(snapshotFilePath)),
+ "Should have the heap snapshot file"
+ );
+ const snapshot = ChromeUtils.readHeapSnapshot(snapshotFilePath);
+ ok(
+ HeapSnapshot.isInstance(snapshot),
+ "And we should be able to read a HeapSnapshot instance from the file"
+ );
+});
diff --git a/devtools/server/tests/xpcshell/test_MemoryActor_saveHeapSnapshot_03.js b/devtools/server/tests/xpcshell/test_MemoryActor_saveHeapSnapshot_03.js
new file mode 100644
index 0000000000..b212abbced
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_MemoryActor_saveHeapSnapshot_03.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that we can save full runtime heap snapshots when attached to the
+// ParentProcessTargetActor or a ContentProcessTargetActor.
+
+add_task(async () => {
+ const { memoryFront } = await createMainProcessMemoryFront();
+
+ const snapshotFilePath = await memoryFront.saveHeapSnapshot();
+ ok(
+ !!(await IOUtils.stat(snapshotFilePath)),
+ "Should have the heap snapshot file"
+ );
+ const snapshot = ChromeUtils.readHeapSnapshot(snapshotFilePath);
+ ok(
+ HeapSnapshot.isInstance(snapshot),
+ "And we should be able to read a HeapSnapshot instance from the file"
+ );
+});
diff --git a/devtools/server/tests/xpcshell/test_add_actors.js b/devtools/server/tests/xpcshell/test_add_actors.js
new file mode 100644
index 0000000000..8077109d71
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_add_actors.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Get the object, from the server side, for a given actor ID
+function getActorInstance(connID, actorID) {
+ return DevToolsServer._connections[connID].getActor(actorID);
+}
+
+/**
+ * The purpose of these tests is to verify that it's possible to add actors
+ * both before and after the DevToolsServer has been initialized, so addons
+ * that add actors don't have to poll the object for its initialization state
+ * in order to add actors after initialization but rather can add actors anytime
+ * regardless of the object's state.
+ */
+add_task(async function () {
+ ActorRegistry.registerModule("resource://test/pre_init_global_actors.js", {
+ prefix: "preInitGlobal",
+ constructor: "PreInitGlobalActor",
+ type: { global: true },
+ });
+ ActorRegistry.registerModule(
+ "resource://test/pre_init_target_scoped_actors.js",
+ {
+ prefix: "preInitTargetScoped",
+ constructor: "PreInitTargetScopedActor",
+ type: { target: true },
+ }
+ );
+
+ const client = await startTestDevToolsServer("example tab");
+
+ ActorRegistry.registerModule("resource://test/post_init_global_actors.js", {
+ prefix: "postInitGlobal",
+ constructor: "PostInitGlobalActor",
+ type: { global: true },
+ });
+ ActorRegistry.registerModule(
+ "resource://test/post_init_target_scoped_actors.js",
+ {
+ prefix: "postInitTargetScoped",
+ constructor: "PostInitTargetScopedActor",
+ type: { target: true },
+ }
+ );
+
+ let actors = await client.mainRoot.rootForm;
+ const tabs = await client.mainRoot.listTabs();
+ const tabDescriptor = tabs[0];
+
+ // These xpcshell tests use mocked actors (xpcshell-test/testactors)
+ // which still don't support watcher actor.
+ // Because of that we still can't enable server side targets and target swiching.
+ tabDescriptor.disableTargetSwitching();
+
+ const tabTarget = await tabDescriptor.getTarget();
+
+ Assert.equal(tabs.length, 1);
+
+ let reply = await client.request({
+ to: actors.preInitGlobalActor,
+ type: "ping",
+ });
+ Assert.equal(reply.message, "pong");
+
+ reply = await client.request({
+ to: tabTarget.targetForm.preInitTargetScopedActor,
+ type: "ping",
+ });
+ Assert.equal(reply.message, "pong");
+
+ reply = await client.request({
+ to: actors.postInitGlobalActor,
+ type: "ping",
+ });
+ Assert.equal(reply.message, "pong");
+
+ reply = await client.request({
+ to: tabTarget.targetForm.postInitTargetScopedActor,
+ type: "ping",
+ });
+ Assert.equal(reply.message, "pong");
+
+ // Consider that there is only one connection, and the first one is ours
+ const connID = Object.keys(DevToolsServer._connections)[0];
+ const postInitGlobalActor = getActorInstance(
+ connID,
+ actors.postInitGlobalActor
+ );
+ const preInitGlobalActor = getActorInstance(
+ connID,
+ actors.preInitGlobalActor
+ );
+ actors = await client.mainRoot.getRoot();
+ Assert.equal(
+ postInitGlobalActor,
+ getActorInstance(connID, actors.postInitGlobalActor)
+ );
+ Assert.equal(
+ preInitGlobalActor,
+ getActorInstance(connID, actors.preInitGlobalActor)
+ );
+
+ await client.close();
+});
diff --git a/devtools/server/tests/xpcshell/test_addon_debugging_connect.js b/devtools/server/tests/xpcshell/test_addon_debugging_connect.js
new file mode 100644
index 0000000000..221e73d256
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_addon_debugging_connect.js
@@ -0,0 +1,158 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
+});
+
+const { createAppInfo, promiseStartupManager } = AddonTestUtils;
+
+AddonTestUtils.init(this);
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
+
+ExtensionTestUtils.init(this);
+
+function watchFrameUpdates(front) {
+ const collected = [];
+
+ const listener = data => {
+ collected.push(data);
+ };
+
+ front.on("frameUpdate", listener);
+ let unsubscribe = () => {
+ unsubscribe = null;
+ front.off("frameUpdate", listener);
+ return collected;
+ };
+
+ return unsubscribe;
+}
+
+function promiseFrameUpdate(front, matcher = () => true) {
+ return new Promise(resolve => {
+ const listener = data => {
+ if (matcher(data)) {
+ resolve();
+ front.off("frameUpdate", listener);
+ }
+ };
+
+ front.on("frameUpdate", listener);
+ });
+}
+
+// Bug 1302702 - Test connect to a webextension addon
+add_task(
+ {
+ // This test needs to run only when the extension are running in a separate
+ // child process, otherwise attachThread would pause the main process and this
+ // test would get stuck.
+ skip_if: () => !WebExtensionPolicy.useRemoteWebExtensions,
+ },
+ async function test_webextension_addon_debugging_connect() {
+ await promiseStartupManager();
+
+ // Install and start a test webextension.
+ const extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ background() {
+ const { browser } = this;
+ browser.test.log("background script executed");
+ // window is available in background scripts
+ // eslint-disable-next-line no-undef
+ browser.test.sendMessage("background page ready", window.location.href);
+ },
+ });
+ await extension.startup();
+ const bgPageURL = await extension.awaitMessage("background page ready");
+
+ const commands = await CommandsFactory.forAddon(extension.id);
+
+ // Connect to the target addon actor and wait for the updated list of frames.
+ const addonTarget = await commands.descriptorFront.getTarget();
+ ok(addonTarget, "Got an RDP target");
+
+ const { frames } = await addonTarget.listFrames();
+ const backgroundPageFrame = frames
+ .filter(frame => {
+ return (
+ frame.url && frame.url.endsWith("/_generated_background_page.html")
+ );
+ })
+ .pop();
+ ok(backgroundPageFrame, "Found the frame for the background page");
+
+ const threadFront = await addonTarget.attachThread();
+
+ ok(threadFront, "Got a threadFront for the target addon");
+ equal(threadFront.paused, false, "The addon threadActor isn't paused");
+
+ equal(
+ lazy.ExtensionParent.DebugUtils.debugBrowserPromises.size,
+ 1,
+ "The expected number of debug browser has been created by the addon actor"
+ );
+
+ const unwatchFrameUpdates = watchFrameUpdates(addonTarget);
+
+ const promiseBgPageFrameUpdate = promiseFrameUpdate(addonTarget, data => {
+ return data.frames?.some(frame => frame.url === bgPageURL);
+ });
+
+ // Reload the addon through the RDP protocol.
+ await addonTarget.reload();
+ info("Wait background page to be fully reloaded");
+ await extension.awaitMessage("background page ready");
+ info("Wait background page frameUpdate event");
+ await promiseBgPageFrameUpdate;
+
+ equal(
+ lazy.ExtensionParent.DebugUtils.debugBrowserPromises.size,
+ 1,
+ "The number of debug browser has not been changed after an addon reload"
+ );
+
+ const frameUpdates = unwatchFrameUpdates();
+ const [frameUpdate] = frameUpdates;
+
+ equal(
+ frameUpdates.length,
+ 1,
+ "Expect 1 frameUpdate events to have been received"
+ );
+ equal(
+ frameUpdate.frames?.length,
+ 1,
+ "Expect 1 frame in the frameUpdate event "
+ );
+ Assert.deepEqual(
+ {
+ url: frameUpdate.frames[0].url,
+ },
+ {
+ url: bgPageURL,
+ },
+ "Got the expected frame update when the addon background page was loaded back"
+ );
+
+ await commands.destroy();
+
+ // Check that if we close the debugging client without uninstalling the addon,
+ // the webextension debugging actor should release the debug browser.
+ equal(
+ lazy.ExtensionParent.DebugUtils.debugBrowserPromises.size,
+ 0,
+ "The debug browser has been released when the RDP connection has been closed"
+ );
+
+ await extension.unload();
+ }
+);
diff --git a/devtools/server/tests/xpcshell/test_addon_events.js b/devtools/server/tests/xpcshell/test_addon_events.js
new file mode 100644
index 0000000000..262a604953
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_addon_events.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AddonManager } = ChromeUtils.importESModule(
+ "resource://gre/modules/AddonManager.sys.mjs"
+);
+add_task(async function testReloadExitedAddon() {
+ await startupAddonsManager();
+
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+
+ const client = new DevToolsClient(DevToolsServer.connectPipe());
+ await client.connect();
+
+ // Retrieve the current list of addons to be notified of the next list update.
+ // We will also call listAddons every time we receive the event "addonListChanged" for
+ // the same reason.
+ await client.mainRoot.listAddons();
+
+ info("Install the addon");
+ const addonFile = do_get_file("addons/web-extension", false);
+
+ let installedAddon;
+ await expectAddonListChanged(client, async () => {
+ installedAddon = await AddonManager.installTemporaryAddon(addonFile);
+ });
+ ok(true, "Received onAddonListChanged when installing addon");
+
+ info("Disable the addon");
+ await expectAddonListChanged(client, () => installedAddon.disable());
+ ok(true, "Received onAddonListChanged when disabling addon");
+
+ info("Enable the addon");
+ await expectAddonListChanged(client, () => installedAddon.enable());
+ ok(true, "Received onAddonListChanged when enabling addon");
+
+ info("Put the addon in pending uninstall mode");
+ await expectAddonListChanged(client, () => installedAddon.uninstall(true));
+ ok(true, "Received onAddonListChanged when addon moves to pending uninstall");
+
+ info("Cancel uninstall for addon");
+ await expectAddonListChanged(client, () => installedAddon.cancelUninstall());
+ ok(true, "Received onAddonListChanged when addon uninstall is canceled");
+
+ info("Completely uninstall the addon");
+ await expectAddonListChanged(client, () => installedAddon.uninstall());
+ ok(true, "Received onAddonListChanged when addon is uninstalled");
+
+ await close(client);
+});
+
+async function expectAddonListChanged(client, predicate) {
+ const onAddonListChanged = client.mainRoot.once("addonListChanged");
+ await predicate();
+ await onAddonListChanged;
+ await client.mainRoot.listAddons();
+}
diff --git a/devtools/server/tests/xpcshell/test_addon_reload.js b/devtools/server/tests/xpcshell/test_addon_reload.js
new file mode 100644
index 0000000000..e0054f03cc
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_addon_reload.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AddonManager } = ChromeUtils.importESModule(
+ "resource://gre/modules/AddonManager.sys.mjs"
+);
+
+function promiseAddonEvent(event) {
+ return new Promise(resolve => {
+ const listener = {
+ [event](...args) {
+ AddonManager.removeAddonListener(listener);
+ resolve(args);
+ },
+ };
+
+ AddonManager.addAddonListener(listener);
+ });
+}
+
+function promiseWebExtensionStartup() {
+ const { Management } = ChromeUtils.importESModule(
+ "resource://gre/modules/Extension.sys.mjs"
+ );
+
+ return new Promise(resolve => {
+ const listener = (evt, extension) => {
+ Management.off("ready", listener);
+ resolve(extension);
+ };
+
+ Management.on("ready", listener);
+ });
+}
+
+async function reloadAddon(addonFront) {
+ // The add-on will be re-installed after a successful reload.
+ const onInstalled = promiseAddonEvent("onInstalled");
+ await addonFront.reload();
+ await onInstalled;
+}
+
+function getSupportFile(path) {
+ const allowMissing = false;
+ return do_get_file(path, allowMissing);
+}
+
+add_task(async function testReloadExitedAddon() {
+ await startupAddonsManager();
+
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+
+ const client = new DevToolsClient(DevToolsServer.connectPipe());
+ await client.connect();
+
+ // Install our main add-on to trigger reloads on.
+ const addonFile = getSupportFile("addons/web-extension");
+ const [installedAddon] = await Promise.all([
+ AddonManager.installTemporaryAddon(addonFile),
+ promiseWebExtensionStartup(),
+ ]);
+
+ // Install a decoy add-on.
+ const addonFile2 = getSupportFile("addons/web-extension2");
+ const [installedAddon2] = await Promise.all([
+ AddonManager.installTemporaryAddon(addonFile2),
+ promiseWebExtensionStartup(),
+ ]);
+
+ const addonFront = await client.mainRoot.getAddon({ id: installedAddon.id });
+
+ await Promise.all([reloadAddon(addonFront), promiseWebExtensionStartup()]);
+
+ // Uninstall the decoy add-on, which should cause its actor to exit.
+ const onUninstalled = promiseAddonEvent("onUninstalled");
+ installedAddon2.uninstall();
+ await onUninstalled;
+
+ // Try to re-list all add-ons after a reload.
+ // This was throwing an exception because of the exited actor.
+ const newAddonFront = await client.mainRoot.getAddon({
+ id: installedAddon.id,
+ });
+ equal(newAddonFront.id, addonFront.id);
+
+ // The fronts should be the same after the reload
+ equal(newAddonFront, addonFront);
+
+ const onAddonListChanged = client.mainRoot.once("addonListChanged");
+
+ // Install an upgrade version of the first add-on.
+ const addonUpgradeFile = getSupportFile("addons/web-extension-upgrade");
+ const [upgradedAddon] = await Promise.all([
+ AddonManager.installTemporaryAddon(addonUpgradeFile),
+ promiseWebExtensionStartup(),
+ ]);
+
+ // Waiting for addonListChanged unsolicited event
+ await onAddonListChanged;
+
+ // re-list all add-ons after an upgrade.
+ const upgradedAddonFront = await client.mainRoot.getAddon({
+ id: upgradedAddon.id,
+ });
+ equal(upgradedAddonFront.id, addonFront.id);
+ // The fronts should be the same after the upgrade.
+ equal(upgradedAddonFront, addonFront);
+
+ // The addon metadata has been updated.
+ equal(upgradedAddonFront.name, "Test Addons Actor Upgrade");
+
+ await close(client);
+});
diff --git a/devtools/server/tests/xpcshell/test_addons_actor.js b/devtools/server/tests/xpcshell/test_addons_actor.js
new file mode 100644
index 0000000000..ba9fda6c3d
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_addons_actor.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function connect() {
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+
+ const client = new DevToolsClient(DevToolsServer.connectPipe());
+ await client.connect();
+
+ const addons = await client.mainRoot.getFront("addons");
+ return [client, addons];
+}
+
+// The AddonsManager test helper can only be called once per test script.
+// This `setup` task will run first.
+add_task(async function setup() {
+ await startupAddonsManager();
+});
+
+add_task(async function testSuccessfulInstall() {
+ const [client, addons] = await connect();
+
+ const allowMissing = false;
+ const usePlatformSeparator = true;
+ const addonPath = getFilePath(
+ "addons/web-extension",
+ allowMissing,
+ usePlatformSeparator
+ );
+ const installedAddon = await addons.installTemporaryAddon(addonPath, false);
+ equal(installedAddon.id, "test-addons-actor@mozilla.org");
+ // The returned object is currently not a proper actor.
+ equal(installedAddon.actor, false);
+
+ const addonList = await client.mainRoot.listAddons();
+ ok(addonList && addonList.map(a => a.name), "Received list of add-ons");
+ const addon = addonList.find(a => a.id === installedAddon.id);
+ ok(addon, "Test add-on appeared in root install list");
+
+ await close(client);
+});
+
+add_task(async function testNonExistantPath() {
+ const [client, addons] = await connect();
+
+ await Assert.rejects(
+ addons.installTemporaryAddon("some-non-existant-path", false),
+ /Could not install add-on.*Component returned failure/
+ );
+
+ await close(client);
+});
diff --git a/devtools/server/tests/xpcshell/test_animation_name.js b/devtools/server/tests/xpcshell/test_animation_name.js
new file mode 100644
index 0000000000..e88911334c
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_animation_name.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that AnimationPlayerActor.getName returns the right name depending on
+// the type of an animation and the various properties available on it.
+
+const {
+ AnimationPlayerActor,
+} = require("resource://devtools/server/actors/animation.js");
+
+function run_test() {
+ // Mock a window with just the properties the AnimationPlayerActor uses.
+ const window = {};
+ window.MutationObserver = class {
+ constructor() {
+ this.observe = () => {};
+ }
+ };
+ window.Animation = class {
+ constructor() {
+ this.effect = { target: getMockNode() };
+ }
+ };
+
+ window.CSSAnimation = class extends window.Animation {};
+ window.CSSTransition = class extends window.Animation {};
+
+ // Helper to get a mock DOM node.
+ function getMockNode() {
+ return {
+ ownerDocument: {
+ defaultView: window,
+ },
+ };
+ }
+
+ // Objects in this array should contain the following properties:
+ // - desc {String} For logging
+ // - animation {Object} An animation object instantiated from one of the mock
+ // window animation constructors.
+ // - props {Objet} Properties of this object will be added to the animation
+ // object.
+ // - expectedName {String} The expected name returned by
+ // AnimationPlayerActor.getName.
+ const TEST_DATA = [
+ {
+ desc: "Animation with an id",
+ animation: new window.Animation(),
+ props: { id: "animation-id" },
+ expectedName: "animation-id",
+ },
+ {
+ desc: "Animation without an id",
+ animation: new window.Animation(),
+ props: {},
+ expectedName: "",
+ },
+ {
+ desc: "CSSTransition with an id",
+ animation: new window.CSSTransition(),
+ props: { id: "transition-with-id", transitionProperty: "width" },
+ expectedName: "transition-with-id",
+ },
+ {
+ desc: "CSSAnimation with an id",
+ animation: new window.CSSAnimation(),
+ props: { id: "animation-with-id", animationName: "move" },
+ expectedName: "animation-with-id",
+ },
+ {
+ desc: "CSSTransition without an id",
+ animation: new window.CSSTransition(),
+ props: { transitionProperty: "width" },
+ expectedName: "width",
+ },
+ {
+ desc: "CSSAnimation without an id",
+ animation: new window.CSSAnimation(),
+ props: { animationName: "move" },
+ expectedName: "move",
+ },
+ ];
+
+ for (const { desc, animation, props, expectedName } of TEST_DATA) {
+ info(desc);
+ for (const key in props) {
+ animation[key] = props[key];
+ }
+ const actor = new AnimationPlayerActor({}, animation);
+ Assert.equal(actor.getName(), expectedName);
+ }
+}
diff --git a/devtools/server/tests/xpcshell/test_animation_type.js b/devtools/server/tests/xpcshell/test_animation_type.js
new file mode 100644
index 0000000000..261b5ef2ac
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_animation_type.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test the output of AnimationPlayerActor.getType().
+
+const {
+ ANIMATION_TYPES,
+ AnimationPlayerActor,
+} = require("resource://devtools/server/actors/animation.js");
+
+function run_test() {
+ // Mock a window with just the properties the AnimationPlayerActor uses.
+ const window = {};
+ window.MutationObserver = class {
+ constructor() {
+ this.observe = () => {};
+ }
+ };
+ window.Animation = class {
+ constructor() {
+ this.effect = { target: getMockNode() };
+ }
+ };
+
+ window.CSSAnimation = class extends window.Animation {};
+ window.CSSTransition = class extends window.Animation {};
+
+ // Helper to get a mock DOM node.
+ function getMockNode() {
+ return {
+ ownerDocument: {
+ defaultView: window,
+ },
+ };
+ }
+
+ // Objects in this array should contain the following properties:
+ // - desc {String} For logging
+ // - animation {Object} An animation object instantiated from one of the mock
+ // window animation constructors.
+ // - expectedType {String} The expected type returned by
+ // AnimationPlayerActor.getType.
+ const TEST_DATA = [
+ {
+ desc: "Test CSSAnimation type",
+ animation: new window.CSSAnimation(),
+ expectedType: ANIMATION_TYPES.CSS_ANIMATION,
+ },
+ {
+ desc: "Test CSSTransition type",
+ animation: new window.CSSTransition(),
+ expectedType: ANIMATION_TYPES.CSS_TRANSITION,
+ },
+ {
+ desc: "Test ScriptAnimation type",
+ animation: new window.Animation(),
+ expectedType: ANIMATION_TYPES.SCRIPT_ANIMATION,
+ },
+ {
+ desc: "Test unknown type",
+ animation: { effect: { target: getMockNode() } },
+ expectedType: ANIMATION_TYPES.UNKNOWN,
+ },
+ ];
+
+ for (const { desc, animation, expectedType } of TEST_DATA) {
+ info(desc);
+ const actor = new AnimationPlayerActor({}, animation);
+ Assert.equal(actor.getType(), expectedType);
+ }
+}
diff --git a/devtools/server/tests/xpcshell/test_attach.js b/devtools/server/tests/xpcshell/test_attach.js
new file mode 100644
index 0000000000..fb7d232e76
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_attach.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { ThreadFront } = require("resource://devtools/client/fronts/thread.js");
+const {
+ WindowGlobalTargetFront,
+} = require("resource://devtools/client/fronts/targets/window-global.js");
+
+/**
+ * Very naive test that checks threadClearTest helper.
+ * It ensures that the thread front is correctly attached.
+ */
+add_task(
+ threadFrontTest(({ threadFront, debuggee, client, targetFront }) => {
+ ok(true, "Thread actor was able to attach");
+ ok(threadFront instanceof ThreadFront, "Thread Front is valid");
+ Assert.equal(threadFront.state, "attached", "Thread Front is resumed");
+ Assert.equal(
+ Cu.getSandboxMetadata(debuggee),
+ undefined,
+ "Debuggee client is valid (getSandboxMetadata did not fail)"
+ );
+ ok(client instanceof DevToolsClient, "Client is valid");
+ ok(targetFront instanceof WindowGlobalTargetFront, "TargetFront is valid");
+ })
+);
diff --git a/devtools/server/tests/xpcshell/test_blackboxing-01.js b/devtools/server/tests/xpcshell/test_blackboxing-01.js
new file mode 100644
index 0000000000..6c549b908e
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_blackboxing-01.js
@@ -0,0 +1,155 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test basic black boxing.
+ */
+
+var gDebuggee;
+var gThreadFront;
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ gThreadFront = threadFront;
+ gDebuggee = debuggee;
+ await testBlackBox();
+ })
+);
+
+const BLACK_BOXED_URL = "http://example.com/blackboxme.js";
+const SOURCE_URL = "http://example.com/source.js";
+
+const testBlackBox = async function () {
+ const packet = await executeOnNextTickAndWaitForPause(evalCode, gThreadFront);
+
+ const bpSource = await getSourceById(gThreadFront, packet.frame.where.actor);
+
+ await setBreakpoint(gThreadFront, { sourceUrl: bpSource.url, line: 2 });
+ await resume(gThreadFront);
+
+ let sourceForm = await getSourceForm(gThreadFront, BLACK_BOXED_URL);
+
+ Assert.ok(
+ !sourceForm.isBlackBoxed,
+ "By default the source is not black boxed."
+ );
+
+ // Test that we can step into `doStuff` when we are not black boxed.
+ await runTest(
+ async function onSteppedLocation(location) {
+ const source = await getSourceFormById(gThreadFront, location.actor);
+ Assert.equal(source.url, BLACK_BOXED_URL);
+ Assert.equal(location.line, 2);
+ },
+ async function onDebuggerStatementFrames(frames) {
+ for (const frame of frames) {
+ const source = await getSourceFormById(gThreadFront, frame.where.actor);
+ Assert.ok(!source.isBlackBoxed);
+ }
+ }
+ );
+
+ const blackboxedSource = await getSource(gThreadFront, BLACK_BOXED_URL);
+ await blackBox(blackboxedSource);
+ sourceForm = await getSourceForm(gThreadFront, BLACK_BOXED_URL);
+ Assert.ok(sourceForm.isBlackBoxed);
+
+ // Test that we step through `doStuff` when we are black boxed and its frame
+ // doesn't show up.
+ await runTest(
+ async function onSteppedLocation(location) {
+ const source = await getSourceFormById(gThreadFront, location.actor);
+ Assert.equal(source.url, SOURCE_URL);
+ Assert.equal(location.line, 4);
+ },
+ async function onDebuggerStatementFrames(frames) {
+ for (const frame of frames) {
+ const source = await getSourceFormById(gThreadFront, frame.where.actor);
+ if (source.url == BLACK_BOXED_URL) {
+ Assert.ok(source.isBlackBoxed);
+ } else {
+ Assert.ok(!source.isBlackBoxed);
+ }
+ }
+ }
+ );
+
+ await unBlackBox(blackboxedSource);
+ sourceForm = await getSourceForm(gThreadFront, BLACK_BOXED_URL);
+ Assert.ok(!sourceForm.isBlackBoxed);
+
+ // Test that we can step into `doStuff` again.
+ await runTest(
+ async function onSteppedLocation(location) {
+ const source = await getSourceFormById(gThreadFront, location.actor);
+ Assert.equal(source.url, BLACK_BOXED_URL);
+ Assert.equal(location.line, 2);
+ },
+ async function onDebuggerStatementFrames(frames) {
+ for (const frame of frames) {
+ const source = await getSourceFormById(gThreadFront, frame.where.actor);
+ Assert.ok(!source.isBlackBoxed);
+ }
+ }
+ );
+};
+
+function evalCode() {
+ /* eslint-disable mozilla/var-only-at-top-level, no-undef */
+ // prettier-ignore
+ Cu.evalInSandbox(
+ "" + function doStuff(k) { // line 1
+ var arg = 15; // line 2 - Step in here
+ k(arg); // line 3
+ }, // line 4
+ gDebuggee,
+ "1.8",
+ BLACK_BOXED_URL,
+ 1
+ );
+
+ // prettier-ignore
+ Cu.evalInSandbox(
+ "" + function runTest() { // line 1
+ doStuff( // line 2 - Break here
+ function (n) { // line 3 - Step through `doStuff` to here
+ (() => {})(); // line 4
+ debugger; // line 5
+ } // line 6
+ ); // line 7
+ } + "\n" // line 8
+ + "debugger;", // line 9
+ gDebuggee,
+ "1.8",
+ SOURCE_URL,
+ 1
+ );
+}
+
+const runTest = async function (onSteppedLocation, onDebuggerStatementFrames) {
+ let packet = await executeOnNextTickAndWaitForPause(
+ gDebuggee.runTest,
+ gThreadFront
+ );
+ Assert.equal(packet.why.type, "breakpoint");
+
+ await stepIn(gThreadFront);
+
+ const location = await getCurrentLocation();
+ await onSteppedLocation(location);
+
+ packet = await resumeAndWaitForPause(gThreadFront);
+ Assert.equal(packet.why.type, "debuggerStatement");
+
+ const { frames } = await getFrames(gThreadFront, 0, 100);
+ await onDebuggerStatementFrames(frames);
+
+ return resume(gThreadFront);
+};
+
+const getCurrentLocation = async function () {
+ const response = await getFrames(gThreadFront, 0, 1);
+ return response.frames[0].where;
+};
diff --git a/devtools/server/tests/xpcshell/test_blackboxing-02.js b/devtools/server/tests/xpcshell/test_blackboxing-02.js
new file mode 100644
index 0000000000..66efaee6c8
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_blackboxing-02.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that we don't hit breakpoints in black boxed sources, and that when we
+ * unblack box the source again, the breakpoint hasn't disappeared and we will
+ * hit it again.
+ */
+
+const BLACK_BOXED_URL = "http://example.com/blackboxme.js";
+const SOURCE_URL = "http://example.com/source.js";
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ // Set up
+ await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ threadFront.setBreakpoint({ sourceUrl: BLACK_BOXED_URL, line: 2 }, {});
+ await threadFront.resume();
+
+ // Test the breakpoint in the black boxed source
+ const { error, sources } = await threadFront.getSources();
+ Assert.ok(!error, "Should not get an error: " + error);
+ const sourceFront = threadFront.source(
+ sources.filter(s => s.url == BLACK_BOXED_URL)[0]
+ );
+
+ await blackBox(sourceFront);
+
+ const packet1 = await executeOnNextTickAndWaitForPause(
+ debuggee.runTest,
+ threadFront
+ );
+
+ Assert.equal(
+ packet1.why.type,
+ "debuggerStatement",
+ "We should pass over the breakpoint since the source is black boxed."
+ );
+
+ await threadFront.resume();
+
+ // Test the breakpoint in the unblack boxed source
+ await unBlackBox(sourceFront);
+
+ const packet2 = await executeOnNextTickAndWaitForPause(
+ debuggee.runTest,
+ threadFront
+ );
+
+ Assert.equal(
+ packet2.why.type,
+ "breakpoint",
+ "We should hit the breakpoint again"
+ );
+
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ /* eslint-disable no-undef */
+ // prettier-ignore
+ Cu.evalInSandbox(
+ "" + function doStuff(k) { // line 1
+ const arg = 15; // line 2 - Break here
+ k(arg); // line 3
+ }, // line 4
+ debuggee,
+ "1.8",
+ BLACK_BOXED_URL,
+ 1
+ );
+ // prettier-ignore
+ Cu.evalInSandbox(
+ "" + function runTest() { // line 1
+ doStuff( // line 2
+ function(n) { // line 3
+ debugger; // line 5
+ } // line 6
+ ); // line 7
+ } // line 8
+ + "\n debugger;", // line 9
+ debuggee,
+ "1.8",
+ SOURCE_URL,
+ 1
+ );
+ /* eslint-enable no-undef */
+}
diff --git a/devtools/server/tests/xpcshell/test_blackboxing-03.js b/devtools/server/tests/xpcshell/test_blackboxing-03.js
new file mode 100644
index 0000000000..f97c8e70f4
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_blackboxing-03.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that we don't stop at debugger statements inside black boxed sources.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ // Set up
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const source = await getSourceById(threadFront, packet.frame.where.actor);
+ threadFront.setBreakpoint({ sourceUrl: source.url, line: 4 }, {});
+ await threadFront.resume();
+
+ // Test the debugger statement in the black boxed source
+ await threadFront.getSources();
+ const sourceFront = await getSource(threadFront, BLACK_BOXED_URL);
+
+ await blackBox(sourceFront);
+
+ const packet2 = await executeOnNextTickAndWaitForPause(
+ debuggee.runTest,
+ threadFront
+ );
+
+ Assert.equal(
+ packet2.why.type,
+ "breakpoint",
+ "We should pass over the debugger statement."
+ );
+
+ threadFront.removeBreakpoint({ sourceUrl: source.url, line: 4 }, {});
+
+ await threadFront.resume();
+
+ // Test the debugger statement in the unblack boxed source
+ await unBlackBox(sourceFront);
+
+ const packet3 = await executeOnNextTickAndWaitForPause(
+ debuggee.runTest,
+ threadFront
+ );
+
+ Assert.equal(
+ packet3.why.type,
+ "debuggerStatement",
+ "We should stop at the debugger statement again"
+ );
+ await threadFront.resume();
+
+ // Test the debugger statement in the black boxed range
+ threadFront.setBreakpoint({ sourceUrl: source.url, line: 4 }, {});
+
+ await blackBox(sourceFront, {
+ start: { line: 1, column: 0 },
+ end: { line: 9, column: 0 },
+ });
+
+ const packet4 = await executeOnNextTickAndWaitForPause(
+ debuggee.runTest,
+ threadFront
+ );
+
+ Assert.equal(
+ packet4.why.type,
+ "breakpoint",
+ "We should pass over the debugger statement."
+ );
+
+ threadFront.removeBreakpoint({ sourceUrl: source.url, line: 4 }, {});
+ await unBlackBox(sourceFront);
+ await threadFront.resume();
+ })
+);
+
+const BLACK_BOXED_URL = "http://example.com/blackboxme.js";
+const SOURCE_URL = "http://example.com/source.js";
+
+function evalCode(debuggee) {
+ /* eslint-disable no-multi-spaces, no-undef */
+ // prettier-ignore
+ Cu.evalInSandbox(
+ "" + function doStuff(k) { // line 1
+ debugger; // line 2 - Break here
+ k(100); // line 3
+ }, // line 4
+ debuggee,
+ "1.8",
+ BLACK_BOXED_URL,
+ 1
+ );
+ // prettier-ignore
+ Cu.evalInSandbox(
+ "" + function runTest() { // line 1
+ doStuff( // line 2
+ function(n) { // line 3
+ Math.abs(n); // line 4 - Break here
+ } // line 5
+ ); // line 6
+ } // line 7
+ + "\n debugger;", // line 8
+ debuggee,
+ "1.8",
+ SOURCE_URL,
+ 1
+ );
+ /* eslint-enable no-multi-spaces, no-undef */
+}
diff --git a/devtools/server/tests/xpcshell/test_blackboxing-04.js b/devtools/server/tests/xpcshell/test_blackboxing-04.js
new file mode 100644
index 0000000000..13345c40e8
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_blackboxing-04.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test behavior of blackboxing sources we are currently paused in.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ // Set up
+ await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ threadFront.setBreakpoint({ sourceUrl: BLACK_BOXED_URL, line: 2 }, {});
+
+ // Test black boxing a source while pausing in the source
+ const { error, sources } = await threadFront.getSources();
+ Assert.ok(!error, "Should not get an error: " + error);
+ const sourceFront = threadFront.source(
+ sources.filter(s => s.url == BLACK_BOXED_URL)[0]
+ );
+
+ const pausedInSource = await blackBox(sourceFront);
+ Assert.ok(
+ pausedInSource,
+ "We should be notified that we are currently paused in this source"
+ );
+ await threadFront.resume();
+ })
+);
+
+const BLACK_BOXED_URL = "http://example.com/blackboxme.js";
+const SOURCE_URL = "http://example.com/source.js";
+
+function evalCode(debuggee) {
+ /* eslint-disable no-multi-spaces, no-undef */
+ // prettier-ignore
+ Cu.evalInSandbox(
+ "" +
+ function doStuff(k) { // line 1
+ debugger; // line 2
+ k(100); // line 3
+ }, // line 4
+ debuggee,
+ "1.8",
+ BLACK_BOXED_URL,
+ 1
+ );
+ // prettier-ignore
+ Cu.evalInSandbox(
+ "" +
+ function runTest() { // line 1
+ doStuff( // line 2
+ function(n) { // line 3
+ return n; // line 4
+ } // line 5
+ ); // line 6
+ } + // line 7
+ "\n runTest();", // line 8
+ debuggee,
+ "1.8",
+ SOURCE_URL,
+ 1
+ );
+ /* eslint-enable no-multi-spaces, no-undef */
+}
diff --git a/devtools/server/tests/xpcshell/test_blackboxing-05.js b/devtools/server/tests/xpcshell/test_blackboxing-05.js
new file mode 100644
index 0000000000..388c87da88
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_blackboxing-05.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test exceptions inside black boxed sources.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee, commands }) => {
+ await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const { error } = await threadFront.getSources();
+ Assert.ok(!error, "Should not get an error: " + error);
+
+ const sourceFront = await getSource(threadFront, BLACK_BOXED_URL);
+ await blackBox(sourceFront);
+ await commands.threadConfigurationCommand.updateConfiguration({
+ pauseOnExceptions: true,
+ ignoreCaughtExceptions: false,
+ });
+
+ const packet = await resumeAndWaitForPause(threadFront);
+ const source = await getSourceById(threadFront, packet.frame.where.actor);
+
+ Assert.equal(
+ source.url,
+ SOURCE_URL,
+ "We shouldn't pause while in the black boxed source."
+ );
+
+ await unBlackBox(sourceFront);
+ await blackBox(sourceFront, {
+ start: { line: 1, column: 0 },
+ end: { line: 4, column: 0 },
+ });
+
+ await threadFront.resume();
+
+ await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const packet2 = await resumeAndWaitForPause(threadFront);
+ const source2 = await getSourceById(threadFront, packet2.frame.where.actor);
+
+ Assert.equal(
+ source2.url,
+ SOURCE_URL,
+ "We shouldn't pause while in the black boxed source."
+ );
+
+ await threadFront.resume();
+ })
+);
+
+const BLACK_BOXED_URL = "http://example.com/blackboxme.js";
+const SOURCE_URL = "http://example.com/source.js";
+
+function evalCode(debuggee) {
+ /* eslint-disable no-multi-spaces, no-unreachable, no-undef */
+ // prettier-ignore
+ Cu.evalInSandbox(
+ "" +
+ function doStuff(k) { // line 1
+ throw new Error("error msg"); // line 2
+ k(100); // line 3
+ }, // line 4
+ debuggee,
+ "1.8",
+ BLACK_BOXED_URL,
+ 1
+ );
+ // prettier-ignore
+ Cu.evalInSandbox(
+ "" +
+ function runTest() { // line 1
+ doStuff( // line 2
+ function(n) { // line 3
+ debugger; // line 4
+ } // line 5
+ ); // line 6
+ } + // line 7
+ "\ndebugger;\n" + // line 8
+ "try { runTest() } catch (ex) { }", // line 9
+ debuggee,
+ "1.8",
+ SOURCE_URL,
+ 1
+ );
+ /* eslint-enable no-multi-spaces, no-unreachable, no-undef */
+}
diff --git a/devtools/server/tests/xpcshell/test_blackboxing-08.js b/devtools/server/tests/xpcshell/test_blackboxing-08.js
new file mode 100644
index 0000000000..d20d8b3966
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_blackboxing-08.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test blackbox ranges
+ */
+
+async function testFinish({ threadFront, devToolsClient }) {
+ await threadFront.resume();
+ await close(devToolsClient);
+
+ do_test_finished();
+}
+
+async function invokeAndPause({ global, threadFront }, expression) {
+ return executeOnNextTickAndWaitForPause(
+ () => Cu.evalInSandbox(expression, global),
+ threadFront
+ );
+}
+
+function run_test() {
+ return (async function () {
+ const dbg = await setupTestFromUrl("stepping.js");
+ const { threadFront } = dbg;
+
+ await invokeAndPause(dbg, `chaining()`);
+
+ const { sources } = await getSources(threadFront);
+ const sourceFront = threadFront.source(sources[0]);
+
+ await setBreakpoint(threadFront, { sourceUrl: sourceFront.url, line: 7 });
+ await setBreakpoint(threadFront, { sourceUrl: sourceFront.url, line: 11 });
+
+ // 1. lets blackbox function a, and assert that we pause in b
+ const range = { start: { line: 6, column: 0 }, end: { line: 8, colum: 1 } };
+ blackBox(sourceFront, range);
+ const paused = await resumeAndWaitForPause(threadFront);
+ equal(paused.frame.where.line, 11, "paused inside of b");
+ await threadFront.resume();
+
+ // 2. lets unblackbox function a, and assert that we pause in a
+ unBlackBox(sourceFront, range);
+ await invokeAndPause(dbg, `chaining()`);
+ const paused2 = await resumeAndWaitForPause(threadFront);
+ equal(paused2.frame.where.line, 7, "paused inside of a");
+
+ await testFinish(dbg);
+ })();
+}
diff --git a/devtools/server/tests/xpcshell/test_breakpoint-01.js b/devtools/server/tests/xpcshell/test_breakpoint-01.js
new file mode 100644
index 0000000000..be46d97cfb
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_breakpoint-01.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check basic breakpoint functionality.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ info("Wait for the debugger statement to be hit");
+ const packet1 = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+ const source = await getSourceById(threadFront, packet1.frame.where.actor);
+ const location = { sourceUrl: source.url, line: debuggee.line0 + 3 };
+
+ threadFront.setBreakpoint(location, {});
+
+ const packet2 = await resumeAndWaitForPause(threadFront);
+
+ info("Paused at the breakpoint");
+ Assert.equal(packet2.frame.where.actor, source.actor);
+ Assert.equal(packet2.frame.where.line, location.line);
+ Assert.equal(packet2.why.type, "breakpoint");
+
+ info("Check that the breakpoint worked.");
+ Assert.equal(debuggee.a, 1);
+ Assert.equal(debuggee.b, undefined);
+
+ await threadFront.resume();
+ })
+);
+
+function evaluateTestCode(debuggee) {
+ /*
+ * Be sure to run debuggee code in its own HTML 'task', so that when we call
+ * the onDebuggerStatement hook, the test's own microtasks don't get suspended
+ * along with the debuggee's.
+ */
+ do_timeout(0, () => {
+ // prettier-ignore
+ Cu.evalInSandbox(
+ "var line0 = Error().lineNumber;\n" +
+ "debugger;\n" + // line0 + 1
+ "var a = 1;\n" + // line0 + 2
+ "var b = 2;\n", // line0 + 3
+ debuggee
+ );
+ });
+}
diff --git a/devtools/server/tests/xpcshell/test_breakpoint-03.js b/devtools/server/tests/xpcshell/test_breakpoint-03.js
new file mode 100644
index 0000000000..f598660a98
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_breakpoint-03.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-shadow, max-nested-callbacks */
+
+"use strict";
+
+/**
+ * Check that setting a breakpoint on a line without code will skip
+ * forward when we know the script isn't GCed (the debugger is connected,
+ * so it's kept alive).
+ */
+
+var test_no_skip_breakpoint = async function (source, location, debuggee) {
+ const [response, bpClient] = await source.setBreakpoint(
+ Object.assign({}, location, { noSliding: true })
+ );
+
+ Assert.ok(!response.actualLocation);
+ Assert.equal(bpClient.location.line, debuggee.line0 + 3);
+ await bpClient.remove();
+};
+
+add_task(
+ threadFrontTest(({ threadFront, debuggee }) => {
+ return new Promise(resolve => {
+ threadFront.once("paused", async function (packet) {
+ const location = { line: debuggee.line0 + 3 };
+ const source = await getSourceById(
+ threadFront,
+ packet.frame.where.actor
+ );
+ // First, make sure that we can disable sliding with the
+ // `noSliding` option.
+ await test_no_skip_breakpoint(source, location, debuggee);
+
+ // Now make sure that the breakpoint properly slides forward one line.
+ const [response, bpClient] = await source.setBreakpoint(location);
+ Assert.ok(!!response.actualLocation);
+ Assert.equal(response.actualLocation.source.actor, source.actor);
+ Assert.equal(response.actualLocation.line, location.line + 1);
+
+ threadFront.once("paused", function (packet) {
+ // Check the return value.
+ Assert.equal(packet.frame.where.actor, source.actor);
+ Assert.equal(packet.frame.where.line, location.line + 1);
+ Assert.equal(packet.why.type, "breakpoint");
+ Assert.equal(packet.why.actors[0], bpClient.actor);
+ // Check that the breakpoint worked.
+ Assert.equal(debuggee.a, 1);
+ Assert.equal(debuggee.b, undefined);
+
+ // Remove the breakpoint.
+ bpClient.remove(function (response) {
+ threadFront.resume().then(resolve);
+ });
+ });
+
+ threadFront.resume();
+ });
+
+ // Use `evalInSandbox` to make the debugger treat it as normal
+ // globally-scoped code, where breakpoint sliding rules apply.
+ // prettier-ignore
+ Cu.evalInSandbox(
+ "var line0 = Error().lineNumber;\n" +
+ "debugger;\n" + // line0 + 1
+ "var a = 1;\n" + // line0 + 2
+ "// A comment.\n" + // line0 + 3
+ "var b = 2;", // line0 + 4
+ debuggee
+ );
+ });
+ })
+);
diff --git a/devtools/server/tests/xpcshell/test_breakpoint-04.js b/devtools/server/tests/xpcshell/test_breakpoint-04.js
new file mode 100644
index 0000000000..8b7137f85d
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_breakpoint-04.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that setting a breakpoint in a line in a child script works.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, client, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+ const source = await getSourceById(threadFront, packet.frame.where.actor);
+ const location = { sourceUrl: source.url, line: debuggee.line0 + 3 };
+
+ //Pause at debugger statement.
+ Assert.equal(packet.frame.where.line, debuggee.line0 + 5);
+ Assert.equal(packet.why.type, "debuggerStatement");
+
+ threadFront.setBreakpoint(location, {});
+ await client.waitForRequestsToSettle();
+ await resume(threadFront);
+
+ const packet2 = await waitForPause(threadFront);
+ // Check the return value.
+ Assert.equal(packet2.frame.where.actor, source.actor);
+ Assert.equal(packet2.frame.where.line, location.line);
+ Assert.equal(packet2.why.type, "breakpoint");
+ // Check that the breakpoint worked.
+ Assert.equal(debuggee.a, 1);
+ Assert.equal(debuggee.b, undefined);
+
+ // Remove the breakpoint.
+ threadFront.removeBreakpoint(location);
+ await client.waitForRequestsToSettle();
+
+ await resume(threadFront);
+ })
+);
+
+function evaluateTestCode(debuggee) {
+ // prettier-ignore
+ Cu.evalInSandbox(
+ "var line0 = Error().lineNumber;\n" +
+ "function foo() {\n" + // line0 + 1
+ " this.a = 1;\n" + // line0 + 2
+ " this.b = 2;\n" + // line0 + 3
+ "}\n" + // line0 + 4
+ "debugger;\n" + // line0 + 5
+ "foo();\n", // line0 + 6
+ debuggee
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_breakpoint-05.js b/devtools/server/tests/xpcshell/test_breakpoint-05.js
new file mode 100644
index 0000000000..f678b285b1
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_breakpoint-05.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-shadow, max-nested-callbacks */
+
+"use strict";
+
+/**
+ * Check that setting a breakpoint in a line without code in a child script
+ * will skip forward.
+ */
+
+add_task(
+ threadFrontTest(({ threadFront, debuggee }) => {
+ return new Promise(resolve => {
+ threadFront.once("paused", async function (packet) {
+ const source = await getSourceById(
+ threadFront,
+ packet.frame.where.actor
+ );
+ const location = { line: debuggee.line0 + 3 };
+
+ source.setBreakpoint(location).then(function ([response, bpClient]) {
+ // Check that the breakpoint has properly skipped forward one line.
+ Assert.equal(response.actualLocation.source.actor, source.actor);
+ Assert.equal(response.actualLocation.line, location.line + 1);
+
+ threadFront.once("paused", function (packet) {
+ // Check the return value.
+ Assert.equal(packet.frame.where.actor, source.actor);
+ Assert.equal(packet.frame.where.line, location.line + 1);
+ Assert.equal(packet.why.type, "breakpoint");
+ Assert.equal(packet.why.actors[0], bpClient.actor);
+ // Check that the breakpoint worked.
+ Assert.equal(debuggee.a, 1);
+ Assert.equal(debuggee.b, undefined);
+
+ // Remove the breakpoint.
+ bpClient.remove(function (response) {
+ threadFront.resume().then(resolve);
+ });
+ });
+
+ // Continue until the breakpoint is hit.
+ threadFront.resume();
+ });
+ });
+
+ // prettier-ignore
+ Cu.evalInSandbox(
+ "var line0 = Error().lineNumber;\n" +
+ "function foo() {\n" + // line0 + 1
+ " this.a = 1;\n" + // line0 + 2
+ " // A comment.\n" + // line0 + 3
+ " this.b = 2;\n" + // line0 + 4
+ "}\n" + // line0 + 5
+ "debugger;\n" + // line0 + 6
+ "foo();\n", // line0 + 7
+ debuggee
+ );
+ });
+ })
+);
diff --git a/devtools/server/tests/xpcshell/test_breakpoint-06.js b/devtools/server/tests/xpcshell/test_breakpoint-06.js
new file mode 100644
index 0000000000..79ddcdc3d4
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_breakpoint-06.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-shadow, max-nested-callbacks */
+
+"use strict";
+
+/**
+ * Check that setting a breakpoint in a line without code in a deeply-nested
+ * child script will skip forward.
+ */
+
+add_task(
+ threadFrontTest(({ threadFront, debuggee }) => {
+ return new Promise(resolve => {
+ threadFront.once("paused", async function (packet) {
+ const source = await getSourceById(
+ threadFront,
+ packet.frame.where.actor
+ );
+ const location = { line: debuggee.line0 + 5 };
+
+ source.setBreakpoint(location).then(function ([response, bpClient]) {
+ // Check that the breakpoint has properly skipped forward one line.
+ Assert.equal(response.actualLocation.source.actor, source.actor);
+ Assert.equal(response.actualLocation.line, location.line + 1);
+
+ threadFront.once("paused", function (packet) {
+ // Check the return value.
+ Assert.equal(packet.frame.where.actor, source.actor);
+ Assert.equal(packet.frame.where.line, location.line + 1);
+ Assert.equal(packet.why.type, "breakpoint");
+ Assert.equal(packet.why.actors[0], bpClient.actor);
+ // Check that the breakpoint worked.
+ Assert.equal(debuggee.a, 1);
+ Assert.equal(debuggee.b, undefined);
+
+ // Remove the breakpoint.
+ bpClient.remove(function (response) {
+ threadFront.resume().then(resolve);
+ });
+ });
+
+ // Continue until the breakpoint is hit.
+ threadFront.resume();
+ });
+ });
+
+ // prettier-ignore
+ Cu.evalInSandbox(
+ "var line0 = Error().lineNumber;\n" +
+ "function foo() {\n" + // line0 + 1
+ " function bar() {\n" + // line0 + 2
+ " function baz() {\n" + // line0 + 3
+ " this.a = 1;\n" + // line0 + 4
+ " // A comment.\n" + // line0 + 5
+ " this.b = 2;\n" + // line0 + 6
+ " }\n" + // line0 + 7
+ " baz();\n" + // line0 + 8
+ " }\n" + // line0 + 9
+ " bar();\n" + // line0 + 10
+ "}\n" + // line0 + 11
+ "debugger;\n" + // line0 + 12
+ "foo();\n", // line0 + 13
+ debuggee
+ );
+ });
+ })
+);
diff --git a/devtools/server/tests/xpcshell/test_breakpoint-07.js b/devtools/server/tests/xpcshell/test_breakpoint-07.js
new file mode 100644
index 0000000000..e6391747bb
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_breakpoint-07.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-shadow, max-nested-callbacks */
+
+"use strict";
+
+/**
+ * Check that setting a breakpoint in a line without code in the second child
+ * script will skip forward.
+ */
+
+add_task(
+ threadFrontTest(({ threadFront, debuggee }) => {
+ return new Promise(resolve => {
+ threadFront.once("paused", async function (packet) {
+ const source = await getSourceById(
+ threadFront,
+ packet.frame.where.actor
+ );
+ const location = { line: debuggee.line0 + 6 };
+
+ source.setBreakpoint(location).then(function ([response, bpClient]) {
+ // Check that the breakpoint has properly skipped forward one line.
+ Assert.equal(response.actualLocation.source.actor, source.actor);
+ Assert.equal(response.actualLocation.line, location.line + 1);
+
+ threadFront.once("paused", function (packet) {
+ // Check the return value.
+ Assert.equal(packet.frame.where.actor, source.actor);
+ Assert.equal(packet.frame.where.line, location.line + 1);
+ Assert.equal(packet.why.type, "breakpoint");
+ Assert.equal(packet.why.actors[0], bpClient.actor);
+ // Check that the breakpoint worked.
+ Assert.equal(debuggee.a, 1);
+ Assert.equal(debuggee.b, undefined);
+
+ // Remove the breakpoint.
+ bpClient.remove(function (response) {
+ threadFront.resume().then(resolve);
+ });
+ });
+
+ // Continue until the breakpoint is hit.
+ threadFront.resume();
+ });
+ });
+
+ // prettier-ignore
+ Cu.evalInSandbox(
+ "var line0 = Error().lineNumber;\n" +
+ "function foo() {\n" + // line0 + 1
+ " bar();\n" + // line0 + 2
+ "}\n" + // line0 + 3
+ "function bar() {\n" + // line0 + 4
+ " this.a = 1;\n" + // line0 + 5
+ " // A comment.\n" + // line0 + 6
+ " this.b = 2;\n" + // line0 + 7
+ "}\n" + // line0 + 8
+ "debugger;\n" + // line0 + 9
+ "foo();\n", // line0 + 10
+ debuggee
+ );
+ });
+ })
+);
diff --git a/devtools/server/tests/xpcshell/test_breakpoint-08.js b/devtools/server/tests/xpcshell/test_breakpoint-08.js
new file mode 100644
index 0000000000..bff0cc3b52
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_breakpoint-08.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-shadow, max-nested-callbacks */
+
+"use strict";
+
+/**
+ * Check that setting a breakpoint in a line without code in a child script
+ * will skip forward, in a file with two scripts.
+ */
+
+add_task(
+ threadFrontTest(({ threadFront, debuggee }) => {
+ return new Promise(resolve => {
+ threadFront.once("paused", async function (packet) {
+ const line = debuggee.line0 + 3;
+ const source = await getSourceById(
+ threadFront,
+ packet.frame.where.actor
+ );
+
+ // this test has been disabled for a long time so the functionality doesn't work
+ const response = await threadFront.setBreakpoint(
+ { sourceUrl: source.url, line },
+ {}
+ );
+ // check that the breakpoint has properly skipped forward one line.
+ assert.equal(response.actuallocation.source.actor, source.actor);
+ // This is wrong - location is not defined, but the test has been disabled
+ // for a long time and currently doesn't work.
+ // eslint-disable-next-line no-undef
+ Assert.equal(response.actualLocation.line, location.line + 1);
+
+ threadFront.once("paused", function (packet) {
+ // Check the return value.
+ Assert.equal(packet.frame.where.actor, source.actor);
+ // eslint-disable-next-line no-undef
+ Assert.equal(packet.frame.where.line, location.line + 1);
+ Assert.equal(packet.why.type, "breakpoint");
+ Assert.equal(packet.why.actors[0], response.bpClient.actor);
+ // Check that the breakpoint worked.
+ Assert.equal(debuggee.a, 1);
+ Assert.equal(debuggee.b, undefined);
+
+ // Remove the breakpoint.
+ response.bpClient.remove(function (response) {
+ threadFront.resume().then(resolve);
+ });
+ });
+
+ // Continue until the breakpoint is hit.
+ threadFront.resume();
+ });
+
+ // prettier-ignore
+ Cu.evalInSandbox("var line0 = Error().lineNumber;\n" +
+ "function foo() {\n" + // line0 + 1
+ " this.a = 1;\n" + // line0 + 2
+ " // A comment.\n" + // line0 + 3
+ " this.b = 2;\n" + // line0 + 4
+ "}\n", // line0 + 5
+ debuggee,
+ "1.7",
+ "script1.js");
+
+ // prettier-ignore
+ Cu.evalInSandbox("var line1 = Error().lineNumber;\n" +
+ "debugger;\n" + // line1 + 1
+ "foo();\n", // line1 + 2
+ debuggee,
+ "1.7",
+ "script2.js");
+ });
+ })
+);
diff --git a/devtools/server/tests/xpcshell/test_breakpoint-09.js b/devtools/server/tests/xpcshell/test_breakpoint-09.js
new file mode 100644
index 0000000000..90b334102d
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_breakpoint-09.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-shadow, max-nested-callbacks */
+
+"use strict";
+
+/**
+ * Check that removing a breakpoint works.
+ */
+
+let done = false;
+
+add_task(
+ threadFrontTest(async ({ threadFront, client, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+
+ const source = await getSourceById(threadFront, packet.frame.where.actor);
+ const location = { sourceUrl: source.url, line: debuggee.line0 + 2 };
+
+ //Pause at debugger statement.
+ Assert.equal(packet.frame.where.line, debuggee.line0 + 7);
+ Assert.equal(packet.why.type, "debuggerStatement");
+
+ threadFront.setBreakpoint(location, {});
+ await client.waitForRequestsToSettle();
+
+ await resume(threadFront);
+
+ const packet2 = await waitForPause(threadFront);
+
+ // Check the return value.
+ Assert.equal(packet2.frame.where.actor, source.actorID);
+ Assert.equal(packet2.frame.where.line, location.line);
+ Assert.equal(packet2.why.type, "breakpoint");
+ // Check that the breakpoint worked.
+ Assert.equal(debuggee.a, undefined);
+
+ // Remove the breakpoint.
+ threadFront.removeBreakpoint(location);
+ await client.waitForRequestsToSettle();
+
+ done = true;
+ threadFront.once("paused", function (packet) {
+ // The breakpoint should not be hit again.
+ threadFront.resume().then(function () {
+ Assert.ok(false);
+ });
+ });
+
+ await resume(threadFront);
+ })
+);
+
+function evaluateTestCode(debuggee) {
+ // prettier-ignore
+ Cu.evalInSandbox("var line0 = Error().lineNumber;\n" +
+ "function foo(stop) {\n" + // line0 + 1
+ " this.a = 1;\n" + // line0 + 2
+ " if (stop) return;\n" + // line0 + 3
+ " delete this.a;\n" + // line0 + 4
+ " foo(true);\n" + // line0 + 5
+ "}\n" + // line0 + 6
+ "debugger;\n" + // line0 + 7
+ "foo();\n", // line0 + 8
+ debuggee);
+ if (!done) {
+ Assert.ok(false);
+ }
+}
diff --git a/devtools/server/tests/xpcshell/test_breakpoint-10.js b/devtools/server/tests/xpcshell/test_breakpoint-10.js
new file mode 100644
index 0000000000..fd114f173d
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_breakpoint-10.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that setting a breakpoint in a line with multiple entry points
+ * triggers no matter which entry point we reach.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, client, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+ const source = await getSourceById(threadFront, packet.frame.where.actor);
+ const location = {
+ sourceUrl: source.url,
+ line: debuggee.line0 + 3,
+ column: 5,
+ };
+
+ //Pause at debugger statement.
+ Assert.equal(packet.frame.where.line, debuggee.line0 + 1);
+ Assert.equal(packet.why.type, "debuggerStatement");
+
+ threadFront.setBreakpoint(location, {});
+ await client.waitForRequestsToSettle();
+
+ await resume(threadFront);
+
+ const packet2 = await waitForPause(threadFront);
+ // Check the return value.
+ Assert.equal(packet2.why.type, "breakpoint");
+ // Check that the breakpoint worked.
+ Assert.equal(debuggee.i, 0);
+ // Check pause location
+ Assert.equal(packet2.frame.where.line, debuggee.line0 + 3);
+ Assert.equal(packet2.frame.where.column, 5);
+
+ // Remove the breakpoint.
+ threadFront.removeBreakpoint(location);
+ await client.waitForRequestsToSettle();
+
+ const location2 = {
+ sourceUrl: source.url,
+ line: debuggee.line0 + 3,
+ column: 12,
+ };
+ threadFront.setBreakpoint(location2, {});
+ await client.waitForRequestsToSettle();
+
+ await resume(threadFront);
+ const packet3 = await waitForPause(threadFront);
+ // Check the return value.
+ Assert.equal(packet3.why.type, "breakpoint");
+ // Check that the breakpoint worked.
+ Assert.equal(debuggee.i, 1);
+ // Check execution location
+ Assert.equal(packet3.frame.where.line, debuggee.line0 + 3);
+ Assert.equal(packet3.frame.where.column, 12);
+
+ // Remove the breakpoint.
+ threadFront.removeBreakpoint(location2);
+ await client.waitForRequestsToSettle();
+
+ await resume(threadFront);
+ })
+);
+
+function evaluateTestCode(debuggee) {
+ // prettier-ignore
+ Cu.evalInSandbox("var line0 = Error().lineNumber;\n" +
+ "debugger;\n" + // line0 + 1
+ "var a, i = 0;\n" + // line0 + 2
+ "for (i = 1; i <= 2; i++) {\n" + // line0 + 3
+ " a = i;\n" + // line0 + 4
+ "}\n", // line0 + 5
+ debuggee);
+}
diff --git a/devtools/server/tests/xpcshell/test_breakpoint-11.js b/devtools/server/tests/xpcshell/test_breakpoint-11.js
new file mode 100644
index 0000000000..a29cd2f768
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_breakpoint-11.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Make sure that setting a breakpoint in a line with bytecodes in multiple
+ * scripts, sets the breakpoint in all of them (bug 793214).
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, client, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+ const source = await getSourceById(threadFront, packet.frame.where.actor);
+ const location = {
+ sourceUrl: source.url,
+ line: debuggee.line0 + 2,
+ column: 8,
+ };
+
+ //Pause at debugger statement.
+ Assert.equal(packet.frame.where.line, debuggee.line0 + 1);
+ Assert.equal(packet.why.type, "debuggerStatement");
+
+ threadFront.setBreakpoint(location, {});
+ await resume(threadFront);
+
+ const packet2 = await waitForPause(threadFront);
+
+ // Check the return value.
+ Assert.equal(packet2.why.type, "breakpoint");
+ // Check that the breakpoint worked.
+ Assert.equal(debuggee.a, undefined);
+ // Check execution location
+ Assert.equal(packet2.frame.where.line, debuggee.line0 + 2);
+ Assert.equal(packet2.frame.where.column, 8);
+
+ // Remove the breakpoint.
+ threadFront.removeBreakpoint(location);
+
+ const location2 = {
+ sourceUrl: source.url,
+ line: debuggee.line0 + 2,
+ column: 32,
+ };
+ threadFront.setBreakpoint(location2, {});
+
+ await resume(threadFront);
+ const packet3 = await waitForPause(threadFront);
+
+ // Check the return value.
+ Assert.equal(packet3.why.type, "breakpoint");
+ // Check that the breakpoint worked.
+ Assert.equal(debuggee.a.b, 1);
+ Assert.equal(debuggee.res, undefined);
+ // Check execution location
+ Assert.equal(packet3.frame.where.line, debuggee.line0 + 2);
+ Assert.equal(packet3.frame.where.column, 32);
+
+ // Remove the breakpoint.
+ threadFront.removeBreakpoint(location2);
+
+ await resume(threadFront);
+ })
+);
+
+function evaluateTestCode(debuggee) {
+ // prettier-ignore
+ Cu.evalInSandbox("var line0 = Error().lineNumber;\n" +
+ "debugger;\n" + // line0 + 1
+ "var a = { b: 1, f: function() { return 2; } };\n" + // line0+2
+ "var res = a.f();\n", // line0 + 3
+ debuggee);
+}
diff --git a/devtools/server/tests/xpcshell/test_breakpoint-12.js b/devtools/server/tests/xpcshell/test_breakpoint-12.js
new file mode 100644
index 0000000000..44b524f1cf
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_breakpoint-12.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-shadow, max-nested-callbacks */
+
+"use strict";
+
+/**
+ * Make sure that setting a breakpoint twice in a line without bytecodes works
+ * as expected.
+ */
+
+const NUM_BREAKPOINTS = 10;
+var gBpActor;
+var gCount;
+
+add_task(
+ threadFrontTest(({ threadFront, debuggee }) => {
+ return new Promise(resolve => {
+ threadFront.once("paused", async function (packet) {
+ const source = await getSourceById(
+ threadFront,
+ packet.frame.where.actor
+ );
+ const location = { line: debuggee.line0 + 3 };
+
+ source.setBreakpoint(location).then(function ([response, bpClient]) {
+ // Check that the breakpoint has properly skipped forward one line.
+ Assert.equal(response.actualLocation.source.actor, source.actor);
+ Assert.equal(response.actualLocation.line, location.line + 1);
+ gBpActor = response.actor;
+
+ // Set more breakpoints at the same location.
+ set_breakpoints(source, location);
+ });
+ });
+
+ /* eslint-disable no-multi-spaces */
+ Cu.evalInSandbox(
+ "var line0 = Error().lineNumber;\n" +
+ "function foo() {\n" + // line0 + 1
+ " this.a = 1;\n" + // line0 + 2
+ " // A comment.\n" + // line0 + 3
+ " this.b = 2;\n" + // line0 + 4
+ "}\n" + // line0 + 5
+ "debugger;\n" + // line0 + 6
+ "foo();\n", // line0 + 7
+ debuggee
+ );
+ /* eslint-enable no-multi-spaces */
+
+ // Set many breakpoints at the same location.
+ function set_breakpoints(source, location) {
+ Assert.notEqual(gCount, NUM_BREAKPOINTS);
+ source.setBreakpoint(location).then(function ([response, bpClient]) {
+ // Check that the breakpoint has properly skipped forward one line.
+ Assert.equal(response.actualLocation.source.actor, source.actor);
+ Assert.equal(response.actualLocation.line, location.line + 1);
+ // Check that the same breakpoint actor was returned.
+ Assert.equal(response.actor, gBpActor);
+
+ if (++gCount < NUM_BREAKPOINTS) {
+ set_breakpoints(source, location);
+ return;
+ }
+
+ // After setting all the breakpoints, check that only one has effectively
+ // remained.
+ threadFront.once("paused", function (packet) {
+ // Check the return value.
+ Assert.equal(packet.frame.where.actor, source.actor);
+ Assert.equal(packet.frame.where.line, location.line + 1);
+ Assert.equal(packet.why.type, "breakpoint");
+ Assert.equal(packet.why.actors[0], bpClient.actor);
+ // Check that the breakpoint worked.
+ Assert.equal(debuggee.a, 1);
+ Assert.equal(debuggee.b, undefined);
+
+ threadFront.once("paused", function (packet) {
+ // We don't expect any more pauses after the breakpoint was hit once.
+ Assert.ok(false);
+ });
+ threadFront.resume().then(function () {
+ // Give any remaining breakpoints a chance to trigger.
+ do_timeout(1000, resolve);
+ });
+ });
+ // Continue until the breakpoint is hit.
+ threadFront.resume();
+ });
+ }
+ });
+ })
+);
diff --git a/devtools/server/tests/xpcshell/test_breakpoint-13.js b/devtools/server/tests/xpcshell/test_breakpoint-13.js
new file mode 100644
index 0000000000..2265f3449a
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_breakpoint-13.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that execution doesn't pause twice while stepping, when encountering
+ * either a breakpoint or a debugger statement.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+
+ const source = await getSourceById(threadFront, packet.frame.where.actor);
+ await threadFront.setBreakpoint(
+ { sourceUrl: source.url, line: 3, column: 6 },
+ {}
+ );
+
+ info("Check that the stepping worked.");
+ const packet1 = await stepIn(threadFront);
+ Assert.equal(packet1.frame.where.line, 6);
+ Assert.equal(packet1.why.type, "resumeLimit");
+
+ info("Entered the foo function call frame.");
+ const packet2 = await stepIn(threadFront);
+ Assert.equal(packet2.frame.where.line, 3);
+ Assert.equal(packet2.why.type, "resumeLimit");
+
+ info("Check that the breakpoint wasn't the reason for this pause");
+ const packet3 = await stepIn(threadFront);
+ Assert.equal(packet3.frame.where.line, 4);
+ Assert.equal(packet3.why.type, "resumeLimit");
+ Assert.equal(packet3.why.frameFinished.return.type, "undefined");
+
+ info("Check that the debugger statement wasn't the reason for this pause.");
+ const packet4 = await stepIn(threadFront);
+ Assert.equal(debuggee.a, 1);
+ Assert.equal(debuggee.b, undefined);
+ Assert.equal(packet4.frame.where.line, 7);
+ Assert.equal(packet4.why.type, "resumeLimit");
+
+ info("Check that the debugger statement wasn't the reason for this pause.");
+ const packet5 = await stepIn(threadFront);
+ Assert.equal(packet5.frame.where.line, 8);
+ Assert.equal(packet5.why.type, "resumeLimit");
+
+ info("Remove the breakpoint and finish.");
+ await stepIn(threadFront);
+ threadFront.removeBreakpoint({ sourceUrl: source.url, line: 3 });
+
+ await resume(threadFront);
+ })
+);
+
+function evaluateTestCode(debuggee) {
+ /* eslint-disable */
+ Cu.evalInSandbox(
+ `
+ function foo() {
+ this.a = 1; // <-- breakpoint set here
+ }
+ debugger;
+ foo();
+ debugger;
+ var b = 2;
+ `,
+ debuggee,
+ "1.8",
+ "test_breakpoint-13.js",
+ 1
+ );
+ /* eslint-enable */
+}
diff --git a/devtools/server/tests/xpcshell/test_breakpoint-14.js b/devtools/server/tests/xpcshell/test_breakpoint-14.js
new file mode 100644
index 0000000000..835edb1385
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_breakpoint-14.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-shadow */
+
+"use strict";
+
+/**
+ * Check that a breakpoint or a debugger statement cause execution to pause even
+ * in a stepped-over function.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, client, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+ const source = await getSourceById(threadFront, packet.frame.where.actor);
+ const location = {
+ sourceUrl: source.url,
+ line: debuggee.line0 + 2,
+ };
+
+ //Pause at debugger statement.
+ Assert.equal(packet.frame.where.line, debuggee.line0 + 4);
+ Assert.equal(packet.why.type, "debuggerStatement");
+
+ threadFront.setBreakpoint(location, {});
+
+ const testCallbacks = [
+ function (packet) {
+ // Check that the stepping worked.
+ Assert.equal(packet.frame.where.line, debuggee.line0 + 5);
+ Assert.equal(packet.why.type, "resumeLimit");
+ },
+ function (packet) {
+ // Reached the breakpoint.
+ Assert.equal(packet.frame.where.line, location.line);
+ Assert.equal(packet.why.type, "breakpoint");
+ Assert.notEqual(packet.why.type, "resumeLimit");
+ },
+ function (packet) {
+ // The frame is about to be popped while stepping.
+ Assert.equal(packet.frame.where.line, debuggee.line0 + 3);
+ Assert.notEqual(packet.why.type, "breakpoint");
+ Assert.equal(packet.why.type, "resumeLimit");
+ Assert.equal(packet.why.frameFinished.return.type, "undefined");
+ },
+ function (packet) {
+ // Check that the debugger statement wasn't the reason for this pause.
+ Assert.equal(debuggee.a, 1);
+ Assert.equal(debuggee.b, undefined);
+ Assert.equal(packet.frame.where.line, debuggee.line0 + 6);
+ Assert.notEqual(packet.why.type, "debuggerStatement");
+ Assert.equal(packet.why.type, "resumeLimit");
+ },
+ function (packet) {
+ // Check that the debugger statement wasn't the reason for this pause.
+ Assert.equal(packet.frame.where.line, debuggee.line0 + 7);
+ Assert.notEqual(packet.why.type, "debuggerStatement");
+ Assert.equal(packet.why.type, "resumeLimit");
+ },
+ ];
+
+ for (const callback of testCallbacks) {
+ const waiter = waitForPause(threadFront);
+ threadFront.stepOver();
+ const packet = await waiter;
+ callback(packet);
+ }
+
+ // Remove the breakpoint and finish.
+ threadFront.removeBreakpoint(location);
+
+ await threadFront.resume();
+ })
+);
+
+function evaluateTestCode(debuggee) {
+ // prettier-ignore
+ Cu.evalInSandbox("var line0 = Error().lineNumber;\n" +
+ "function foo() {\n" + // line0 + 1
+ " this.a = 1;\n" + // line0 + 2 <-- Breakpoint is set here.
+ "}\n" + // line0 + 3
+ "debugger;\n" + // line0 + 4
+ "foo();\n" + // line0 + 5
+ "debugger;\n" + // line0 + 6
+ "var b = 2;\n", // line0 + 7
+ debuggee);
+}
diff --git a/devtools/server/tests/xpcshell/test_breakpoint-16.js b/devtools/server/tests/xpcshell/test_breakpoint-16.js
new file mode 100644
index 0000000000..a42306eee1
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_breakpoint-16.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-shadow, max-nested-callbacks */
+
+"use strict";
+
+/**
+ * Check that we can set breakpoints in columns, not just lines.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, client, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+ const source = await getSourceById(threadFront, packet.frame.where.actor);
+ const location = {
+ sourceUrl: source.url,
+ line: debuggee.line0 + 1,
+ column: 55,
+ };
+
+ let timesBreakpointHit = 0;
+ threadFront.setBreakpoint(location, {});
+
+ while (timesBreakpointHit < 3) {
+ await resume(threadFront);
+ const packet = await waitForPause(threadFront);
+ await testAssertions(
+ packet,
+ debuggee,
+ source,
+ location,
+ timesBreakpointHit
+ );
+
+ timesBreakpointHit++;
+ }
+
+ threadFront.removeBreakpoint(location);
+ await threadFront.resume();
+ })
+);
+
+function evaluateTestCode(debuggee) {
+ // prettier-ignore
+ Cu.evalInSandbox(
+ "var line0 = Error().lineNumber;\n" +
+ "(function () { debugger; this.acc = 0; for (var i = 0; i < 3; i++) this.acc++; }());",
+ debuggee
+ );
+}
+
+async function testAssertions(
+ packet,
+ debuggee,
+ source,
+ location,
+ timesBreakpointHit
+) {
+ Assert.equal(packet.why.type, "breakpoint");
+ Assert.equal(packet.frame.where.actor, source.actor);
+ Assert.equal(packet.frame.where.line, location.line);
+ Assert.equal(packet.frame.where.column, location.column);
+
+ Assert.equal(debuggee.acc, timesBreakpointHit);
+ const environment = await packet.frame.getEnvironment();
+ Assert.equal(environment.bindings.variables.i.value, timesBreakpointHit);
+}
diff --git a/devtools/server/tests/xpcshell/test_breakpoint-17.js b/devtools/server/tests/xpcshell/test_breakpoint-17.js
new file mode 100644
index 0000000000..c52e6547ef
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_breakpoint-17.js
@@ -0,0 +1,130 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-shadow */
+
+"use strict";
+
+/**
+ * Test that when we add 2 breakpoints to the same line at different columns and
+ * then remove one of them, we don't remove them both.
+ */
+
+const code =
+ "(" +
+ function (global) {
+ global.foo = function () {
+ Math.abs(-1);
+ Math.log(0.5);
+ debugger;
+ };
+ debugger;
+ } +
+ "(this))";
+
+const firstLocation = {
+ line: 3,
+ column: 4,
+};
+
+const secondLocation = {
+ line: 3,
+ column: 18,
+};
+
+add_task(
+ threadFrontTest(({ threadFront, debuggee }) => {
+ return new Promise(resolve => {
+ threadFront.on("paused", async packet => {
+ const [first, second] = await set_breakpoints(packet, threadFront);
+ test_different_actors(first, second);
+ await test_remove_one(first, second, threadFront, debuggee);
+ resolve();
+ });
+
+ Cu.evalInSandbox(code, debuggee, "1.8", "http://example.com/", 1);
+ });
+ })
+);
+
+async function set_breakpoints(packet, threadFront) {
+ const source = await getSourceById(threadFront, packet.frame.where.actor);
+ return new Promise(resolve => {
+ let first, second;
+
+ source
+ .setBreakpoint(firstLocation)
+ .then(function ([{ actualLocation }, breakpointClient]) {
+ Assert.ok(!actualLocation, "Should not get an actualLocation");
+ first = breakpointClient;
+
+ source
+ .setBreakpoint(secondLocation)
+ .then(function ([{ actualLocation }, breakpointClient]) {
+ Assert.ok(!actualLocation, "Should not get an actualLocation");
+ second = breakpointClient;
+
+ resolve([first, second]);
+ });
+ });
+ });
+}
+
+function test_different_actors(first, second) {
+ Assert.notEqual(
+ first.actor,
+ second.actor,
+ "Each breakpoint should have a different actor"
+ );
+}
+
+function test_remove_one(first, second, threadFront, debuggee) {
+ return new Promise(resolve => {
+ first.remove(function ({ error }) {
+ Assert.ok(!error, "Should not get an error removing a breakpoint");
+
+ let hitSecond;
+ threadFront.on("paused", function _onPaused({ why, frame }) {
+ if (why.type == "breakpoint") {
+ hitSecond = true;
+ Assert.equal(
+ why.actors.length,
+ 1,
+ "Should only be paused because of one breakpoint actor"
+ );
+ Assert.equal(
+ why.actors[0],
+ second.actor,
+ "Should be paused because of the correct breakpoint actor"
+ );
+ Assert.equal(
+ frame.where.line,
+ secondLocation.line,
+ "Should be at the right line"
+ );
+ Assert.equal(
+ frame.where.column,
+ secondLocation.column,
+ "Should be at the right column"
+ );
+ threadFront.resume();
+ return;
+ }
+
+ if (why.type == "debuggerStatement") {
+ threadFront.off("paused", _onPaused);
+ Assert.ok(
+ hitSecond,
+ "We should still hit `second`, but not `first`."
+ );
+
+ resolve();
+ return;
+ }
+
+ Assert.ok(false, "Should never get here");
+ });
+
+ threadFront.resume().then(() => debuggee.foo());
+ });
+ });
+}
diff --git a/devtools/server/tests/xpcshell/test_breakpoint-18.js b/devtools/server/tests/xpcshell/test_breakpoint-18.js
new file mode 100644
index 0000000000..b2c86458d0
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_breakpoint-18.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that we only break on offsets that are entry points for the line we are
+ * breaking on. Bug 907278.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, client, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+
+ const source = await getSourceById(threadFront, packet.frame.where.actor);
+ const location = { sourceUrl: source.url, line: 3 };
+ threadFront.setBreakpoint(location, {});
+ await client.waitForRequestsToSettle();
+
+ debuggee.console = { log: x => void x };
+
+ await resume(threadFront);
+
+ const packet2 = await executeOnNextTickAndWaitForPause(
+ debuggee.test,
+ threadFront
+ );
+ Assert.equal(packet2.why.type, "breakpoint");
+
+ const packet3 = await resumeAndWaitForPause(threadFront);
+ testDbgStatement(packet3);
+
+ await resume(threadFront);
+ })
+);
+
+function evaluateTestCode(debuggee) {
+ Cu.evalInSandbox(
+ "debugger;\n" +
+ function test() {
+ console.log("foo bar");
+ debugger;
+ },
+ debuggee,
+ "1.8",
+ "http://example.com/",
+ 1
+ );
+}
+
+function testDbgStatement({ why }) {
+ // Should continue to the debugger statement.
+ Assert.equal(why.type, "debuggerStatement");
+ // Not break on another offset from the same line (that isn't an entry point
+ // to the line)
+ Assert.notEqual(why.type, "breakpoint");
+}
diff --git a/devtools/server/tests/xpcshell/test_breakpoint-19.js b/devtools/server/tests/xpcshell/test_breakpoint-19.js
new file mode 100644
index 0000000000..013acdfaf1
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_breakpoint-19.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Make sure that setting a breakpoint in a not-yet-existing script doesn't throw
+ * an error (see bug 897567). Also make sure that this breakpoint works.
+ */
+
+const URL = "test.js";
+
+function setUpCode(debuggee) {
+ /* eslint-disable mozilla/var-only-at-top-level, no-unused-vars */
+ // prettier-ignore
+ Cu.evalInSandbox(
+ "" + function test() { // 1
+ var a = 1; // 2
+ debugger; // 3
+ } + // 4
+ "\ndebugger;", // 5
+ debuggee,
+ "1.8",
+ URL
+ );
+ /* eslint-enable mozilla/var-only-at-top-level, no-unused-vars */
+}
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ setBreakpoint(threadFront, { sourceUrl: URL, line: 2 });
+
+ await executeOnNextTickAndWaitForPause(
+ () => setUpCode(debuggee),
+ threadFront
+ );
+ await resume(threadFront);
+
+ const packet = await executeOnNextTickAndWaitForPause(
+ debuggee.test,
+ threadFront
+ );
+ equal(packet.why.type, "breakpoint");
+ })
+);
diff --git a/devtools/server/tests/xpcshell/test_breakpoint-20.js b/devtools/server/tests/xpcshell/test_breakpoint-20.js
new file mode 100644
index 0000000000..886d44164d
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_breakpoint-20.js
@@ -0,0 +1,109 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Verify that when two of the "same" source are loaded concurrently (like e10s
+ * frame scripts), breakpoints get hit in scripts defined by all sources.
+ */
+
+var gDebuggee;
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ gDebuggee = debuggee;
+ await testBreakpoint(threadFront);
+ })
+);
+
+const testBreakpoint = async function (threadFront) {
+ evalSetupCode();
+
+ // Load the test source once.
+
+ evalTestCode();
+ equal(
+ gDebuggee.functions.length,
+ 1,
+ "The test code should have added a function."
+ );
+
+ // Set a breakpoint in the test source.
+
+ const source = await getSource(threadFront, "test.js");
+ setBreakpoint(threadFront, { sourceUrl: source.url, line: 3 });
+
+ // Load the test source again.
+
+ evalTestCode();
+ equal(
+ gDebuggee.functions.length,
+ 2,
+ "The test code should have added another function."
+ );
+
+ // Should hit our breakpoint in a script defined by the first instance of the
+ // test source.
+
+ const bpPause1 = await executeOnNextTickAndWaitForPause(
+ gDebuggee.functions[0],
+ threadFront
+ );
+ equal(
+ bpPause1.why.type,
+ "breakpoint",
+ "Should pause because of hitting our breakpoint (not debugger statement)."
+ );
+ const dbgStmtPause1 = await executeOnNextTickAndWaitForPause(
+ () => resume(threadFront),
+ threadFront
+ );
+ equal(
+ dbgStmtPause1.why.type,
+ "debuggerStatement",
+ "And we should hit the debugger statement after the pause."
+ );
+ await resume(threadFront);
+
+ // Should also hit our breakpoint in a script defined by the second instance
+ // of the test source.
+
+ const bpPause2 = await executeOnNextTickAndWaitForPause(
+ gDebuggee.functions[1],
+ threadFront
+ );
+ equal(
+ bpPause2.why.type,
+ "breakpoint",
+ "Should pause because of hitting our breakpoint (not debugger statement)."
+ );
+ const dbgStmtPause2 = await executeOnNextTickAndWaitForPause(
+ () => resume(threadFront),
+ threadFront
+ );
+ equal(
+ dbgStmtPause2.why.type,
+ "debuggerStatement",
+ "And we should hit the debugger statement after the pause."
+ );
+};
+
+function evalSetupCode() {
+ Cu.evalInSandbox("this.functions = [];", gDebuggee, "1.8", "setup.js", 1);
+}
+
+function evalTestCode() {
+ Cu.evalInSandbox(
+ ` // 1
+ this.functions.push(function () { // 2
+ var setBreakpointHere = 1; // 3
+ debugger; // 4
+ }); // 5
+ `,
+ gDebuggee,
+ "1.8",
+ "test.js",
+ 1
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_breakpoint-21.js b/devtools/server/tests/xpcshell/test_breakpoint-21.js
new file mode 100644
index 0000000000..da7a87f91c
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_breakpoint-21.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Bug 1122064 - make sure that scripts introduced via onNewScripts
+ * properly populate the `ScriptStore` with all there nested child
+ * scripts, so you can set breakpoints on deeply nested scripts
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ // Populate the `ScriptStore` so that we only test that the script
+ // is added through `onNewScript`
+ await getSources(threadFront);
+
+ let packet = await executeOnNextTickAndWaitForPause(() => {
+ evalCode(debuggee);
+ }, threadFront);
+ const source = await getSourceById(threadFront, packet.frame.where.actor);
+ const location = {
+ sourceUrl: source.url,
+ line: debuggee.line0 + 8,
+ };
+
+ setBreakpoint(threadFront, location);
+
+ await resume(threadFront);
+ packet = await waitForPause(threadFront);
+ Assert.equal(packet.why.type, "breakpoint");
+ Assert.equal(packet.frame.where.actor, source.actor);
+ Assert.equal(packet.frame.where.line, location.line);
+
+ await resume(threadFront);
+ })
+);
+
+function evalCode(debuggee) {
+ // Start a new script
+ /* eslint-disable mozilla/var-only-at-top-level, max-nested-callbacks, no-unused-vars */
+ // prettier-ignore
+ Cu.evalInSandbox(
+ "var line0 = Error().lineNumber;\n(" + function () {
+ debugger;
+ var a = (function () {
+ return (function () {
+ return (function () {
+ return (function () {
+ return (function () {
+ var x = 10; // This line gets a breakpoint
+ return 1;
+ })();
+ })();
+ })();
+ })();
+ })();
+ } + ")()",
+ debuggee
+ );
+ /* eslint-enable mozilla/var-only-at-top-level, max-nested-callbacks, no-unused-vars */
+}
diff --git a/devtools/server/tests/xpcshell/test_breakpoint-22.js b/devtools/server/tests/xpcshell/test_breakpoint-22.js
new file mode 100644
index 0000000000..067dfa3fa2
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_breakpoint-22.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Bug 1333219 - make that setBreakpoint fails when script is not found
+ * at the specified line.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ // Populate the `ScriptStore` so that we only test that the script
+ // is added through `onNewScript`
+ await getSources(threadFront);
+
+ const packet = await executeOnNextTickAndWaitForPause(() => {
+ evalCode(debuggee);
+ }, threadFront);
+ const source = await getSourceById(threadFront, packet.frame.where.actor);
+
+ const location = {
+ line: debuggee.line0 + 2,
+ };
+
+ const [res] = await setBreakpoint(source, location);
+ ok(!res.error);
+
+ const location2 = {
+ line: debuggee.line0 + 7,
+ };
+
+ await source.setBreakpoint(location2).then(
+ () => {
+ do_throw("no code shall not be found the specified line or below it");
+ },
+ reason => {
+ Assert.equal(reason.error, "noCodeAtLineColumn");
+ ok(reason.message);
+ }
+ );
+
+ await resume(threadFront);
+ })
+);
+
+function evalCode(debuggee) {
+ // Start a new script
+ Cu.evalInSandbox(
+ `
+var line0 = Error().lineNumber;
+function some_function() {
+ // breakpoint is valid here -- it slides one line below (line0 + 2)
+}
+debugger;
+// no breakpoint is allowed after the EOF (line0 + 6)
+`,
+ debuggee
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_breakpoint-23.js b/devtools/server/tests/xpcshell/test_breakpoint-23.js
new file mode 100644
index 0000000000..8f07190ea9
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_breakpoint-23.js
@@ -0,0 +1,35 @@
+/* eslint-disable max-nested-callbacks */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Bug 1552453 - Verify that breakpoints are hit for evaluated
+ * scripts that contain a source url pragma.
+ */
+add_task(
+ threadFrontTest(async ({ commands, threadFront }) => {
+ await threadFront.setBreakpoint(
+ { sourceUrl: "http://example.com/code.js", line: 2, column: 1 },
+ {}
+ );
+
+ info("Create a new script with the displayUrl code.js");
+ const onNewSource = waitForEvent(threadFront, "newSource");
+ await commands.scriptCommand.execute(
+ "function f() {\n return 5; \n}\n//# sourceURL=http://example.com/code.js"
+ );
+ const sourcePacket = await onNewSource;
+
+ equal(sourcePacket.source.url, "http://example.com/code.js");
+
+ info("Evaluate f() and pause at line 2");
+ const onExecutionDone = commands.scriptCommand.execute("f()");
+ const pausedPacket = await waitForPause(threadFront);
+ equal(pausedPacket.why.type, "breakpoint");
+ equal(pausedPacket.frame.where.line, 2);
+ resume(threadFront);
+ await onExecutionDone;
+ })
+);
diff --git a/devtools/server/tests/xpcshell/test_breakpoint-24.js b/devtools/server/tests/xpcshell/test_breakpoint-24.js
new file mode 100644
index 0000000000..a240a237f0
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_breakpoint-24.js
@@ -0,0 +1,239 @@
+/* eslint-disable max-nested-callbacks */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Bug 1441183 - Verify that the debugger advances to a new location
+ * when encountering debugger statements and brakpoints
+ *
+ * Bug 1613165 - Verify that debugger statement is not disabled by
+ * adding/removing a breakpoint
+ */
+add_task(
+ threadFrontTest(async props => {
+ await testDebuggerStatements(props);
+ await testBreakpoints(props);
+ await testBreakpointsAndDebuggerStatements(props);
+ await testLoops(props);
+ await testRemovingBreakpoint(props);
+ await testAddingBreakpoint(props);
+ })
+);
+
+// Ensure that we advance to the next line when we
+// step to a debugger statement and resume.
+async function testDebuggerStatements({ commands, threadFront }) {
+ commands.scriptCommand.execute(`function foo(stop) {
+ debugger;
+ debugger;
+ debugger;
+ }
+ foo();
+ //# sourceURL=http://example.com/code.js`);
+
+ await performActions(threadFront, [
+ [
+ "paused at first debugger statement",
+ { line: 2, type: "debuggerStatement" },
+ "stepOver",
+ ],
+ [
+ "paused at the second debugger statement",
+ { line: 3, type: "resumeLimit" },
+ "resume",
+ ],
+ [
+ "paused at the third debugger statement",
+ { line: 4, type: "debuggerStatement" },
+ "resume",
+ ],
+ ]);
+}
+
+// Ensure that we advance to the next line when we hit a breakpoint
+// on a line with a debugger statement and resume.
+async function testBreakpointsAndDebuggerStatements({ commands, threadFront }) {
+ commands.scriptCommand.execute(`function foo(stop) {
+ debugger;
+ debugger;
+ debugger;
+ }
+ foo();
+ //# sourceURL=http://example.com/testBreakpointsAndDebuggerStatements.js`);
+
+ threadFront.setBreakpoint(
+ {
+ sourceUrl: "http://example.com/testBreakpointsAndDebuggerStatements.js",
+ line: 3,
+ },
+ {}
+ );
+
+ await performActions(threadFront, [
+ [
+ "paused at first debugger statement",
+ { line: 2, type: "debuggerStatement" },
+ "resume",
+ ],
+ [
+ "paused at the breakpoint at the second debugger statement",
+ { line: 3, type: "breakpoint" },
+ "resume",
+ ],
+ [
+ "pause at the third debugger statement",
+ { line: 4, type: "debuggerStatement" },
+ "resume",
+ ],
+ ]);
+}
+
+// Ensure that we advance to the next line when we step to
+// a line with a breakpoint and resume.
+async function testBreakpoints({ commands, threadFront }) {
+ commands.scriptCommand.execute(`function foo(stop) {
+ debugger;
+ a();
+ debugger;
+ }
+ function a() {}
+ foo();
+ //# sourceURL=http://example.com/testBreakpoints.js`);
+
+ threadFront.setBreakpoint(
+ { sourceUrl: "http://example.com/testBreakpoints.js", line: 3, column: 6 },
+ {}
+ );
+
+ await performActions(threadFront, [
+ [
+ "paused at first debugger statement",
+ { line: 2, type: "debuggerStatement" },
+ "stepOver",
+ ],
+ ["paused at a()", { line: 3, type: "resumeLimit" }, "resume"],
+ [
+ "pause at the second debugger satement",
+ { line: 4, type: "debuggerStatement" },
+ "resume",
+ ],
+ ]);
+}
+
+// Ensure that we advance to the next line when we step to
+// a line with a breakpoint and resume.
+async function testLoops({ commands, threadFront }) {
+ commands.scriptCommand.execute(`function foo(stop) {
+ let i = 0;
+ debugger;
+ while (i++ < 2) {
+ debugger;
+ }
+ debugger;
+ }
+ foo();
+ //# sourceURL=http://example.com/testLoops.js`);
+
+ await performActions(threadFront, [
+ [
+ "paused at first debugger statement",
+ { line: 3, type: "debuggerStatement" },
+ "resume",
+ ],
+ [
+ "pause at the second debugger satement",
+ { line: 5, type: "debuggerStatement" },
+ "resume",
+ ],
+ [
+ "pause at the second debugger satement (2nd time)",
+ { line: 5, type: "debuggerStatement" },
+ "resume",
+ ],
+ [
+ "pause at the third debugger satement",
+ { line: 7, type: "debuggerStatement" },
+ "resume",
+ ],
+ ]);
+}
+
+// Bug 1613165 - ensure that if you pause on a breakpoint on a line with
+// debugger statement, remove the breakpoint, and try to pause on the
+// debugger statement before pausing anywhere else, debugger pauses instead of
+// skipping debugger statement.
+async function testRemovingBreakpoint({ commands, threadFront }) {
+ commands.scriptCommand.execute(`function foo(stop) {
+ debugger;
+ }
+ foo();
+ foo();
+ //# sourceURL=http://example.com/testRemovingBreakpoint.js`);
+
+ const location = {
+ sourceUrl: "http://example.com/testRemovingBreakpoint.js",
+ line: 2,
+ column: 6,
+ };
+
+ threadFront.setBreakpoint(location, {});
+
+ info("paused at the breakpoint at the first debugger statement");
+ const packet = await waitForEvent(threadFront, "paused");
+ Assert.equal(packet.frame.where.line, 2);
+ Assert.equal(packet.why.type, "breakpoint");
+ threadFront.removeBreakpoint(location);
+
+ info("paused at the first debugger statement");
+ const packet2 = await resumeAndWaitForPause(threadFront);
+ Assert.equal(packet2.frame.where.line, 2);
+ Assert.equal(packet2.why.type, "debuggerStatement");
+ await threadFront.resume();
+}
+
+// Bug 1613165 - ensure if you pause on a debugger statement, add a
+// breakpoint on the same line, and try to pause on the breakpoint
+// before pausing anywhere else, debugger pauses on that line instead of
+// skipping breakpoint.
+async function testAddingBreakpoint({ commands, threadFront }) {
+ commands.scriptCommand.execute(`function foo(stop) {
+ debugger;
+ }
+ foo();
+ foo();
+ //# sourceURL=http://example.com/testAddingBreakpoint.js`);
+
+ const location = {
+ sourceUrl: "http://example.com/testAddingBreakpoint.js",
+ line: 2,
+ column: 6,
+ };
+
+ info("paused at the first debugger statement");
+ const packet = await waitForEvent(threadFront, "paused");
+ Assert.equal(packet.frame.where.line, 2);
+ Assert.equal(packet.why.type, "debuggerStatement");
+ threadFront.setBreakpoint(location, {});
+
+ info("paused at the breakpoint at the first debugger statement");
+ const packet2 = await resumeAndWaitForPause(threadFront);
+ Assert.equal(packet2.frame.where.line, 2);
+ Assert.equal(packet2.why.type, "breakpoint");
+ await threadFront.resume();
+}
+
+async function performActions(threadFront, actions) {
+ for (const action of actions) {
+ await performAction(threadFront, action);
+ }
+}
+
+async function performAction(threadFront, [description, result, action]) {
+ info(description);
+ const packet = await waitForEvent(threadFront, "paused");
+ Assert.equal(packet.frame.where.line, result.line);
+ Assert.equal(packet.why.type, result.type);
+ await threadFront[action]();
+}
diff --git a/devtools/server/tests/xpcshell/test_breakpoint-25.js b/devtools/server/tests/xpcshell/test_breakpoint-25.js
new file mode 100644
index 0000000000..f155234c96
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_breakpoint-25.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Ensure that the debugger resume page execution when the connection drops
+ * and when the target is detached.
+ */
+
+add_task(
+ threadFrontTest(({ threadFront, debuggee, targetFront }) => {
+ return new Promise(resolve => {
+ (async () => {
+ await executeOnNextTickAndWaitForPause(evalCode, threadFront);
+
+ ok(true, "The page is paused");
+ ok(!debuggee.foo, "foo is still false after we hit the breakpoint");
+
+ await targetFront.detach();
+
+ // Closing the connection will force the thread actor to resume page
+ // execution
+ ok(debuggee.foo, "foo is true after target's detach request");
+
+ resolve();
+ })();
+
+ function evalCode() {
+ /* eslint-disable */
+ Cu.evalInSandbox("var foo = false;\n", debuggee);
+ /* eslint-enable */
+ ok(!debuggee.foo, "foo is false at startup");
+
+ /* eslint-disable */
+ Cu.evalInSandbox("debugger;\n" + "foo = true;\n", debuggee);
+ /* eslint-enable */
+ }
+ });
+ })
+);
+
+add_task(
+ threadFrontTest(({ threadFront, client, debuggee }) => {
+ return new Promise(resolve => {
+ (async () => {
+ await executeOnNextTickAndWaitForPause(evalCode, threadFront);
+
+ ok(true, "The page is paused");
+ ok(!debuggee.foo, "foo is still false after we hit the breakpoint");
+
+ await client.close();
+
+ // `close` will force the destruction of the thread actor, which,
+ // will resume the page execution. But all of that seems to be
+ // synchronous and we have to spin the event loop in order to ensure
+ // having the content javascript to execute the resumed code.
+ await new Promise(executeSoon);
+
+ // Closing the connection will force the thread actor to resume page
+ // execution
+ ok(debuggee.foo, "foo is true after client close");
+ executeSoon(resolve);
+ dump("resolved\n");
+ })();
+
+ function evalCode() {
+ /* eslint-disable */
+ Cu.evalInSandbox("var foo = false;\n", debuggee);
+ /* eslint-enable */
+ ok(!debuggee.foo, "foo is false at startup");
+
+ /* eslint-disable */
+ Cu.evalInSandbox("debugger;\n" + "foo = true;\n", debuggee);
+ /* eslint-enable */
+ }
+ });
+ })
+);
diff --git a/devtools/server/tests/xpcshell/test_breakpoint-26.js b/devtools/server/tests/xpcshell/test_breakpoint-26.js
new file mode 100644
index 0000000000..8624171252
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_breakpoint-26.js
@@ -0,0 +1,63 @@
+/* eslint-disable max-nested-callbacks */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Bug 925269 - Verify that debugger statements are skipped
+ * if there is a falsey conditional breakpoint at the same location.
+ */
+add_task(
+ threadFrontTest(async props => {
+ await testBreakpointsAndDebuggerStatements(props);
+ })
+);
+
+async function testBreakpointsAndDebuggerStatements({ commands, threadFront }) {
+ commands.scriptCommand.execute(
+ `function foo(stop) {
+ debugger;
+ debugger;
+ debugger;
+ }
+ foo();
+ //# sourceURL=http://example.com/testBreakpointsAndDebuggerStatements.js`
+ );
+
+ threadFront.setBreakpoint(
+ {
+ sourceUrl: "http://example.com/testBreakpointsAndDebuggerStatements.js",
+ line: 3,
+ column: 6,
+ },
+ { condition: "false" }
+ );
+
+ await performActions(threadFront, [
+ [
+ "paused at first debugger statement",
+ { line: 2, type: "debuggerStatement" },
+ "resume",
+ ],
+ [
+ "pause at the third debugger statement",
+ { line: 4, type: "debuggerStatement" },
+ "resume",
+ ],
+ ]);
+}
+
+async function performActions(threadFront, actions) {
+ for (const action of actions) {
+ await performAction(threadFront, action);
+ }
+}
+
+async function performAction(threadFront, [description, result, action]) {
+ info(description);
+ const packet = await waitForEvent(threadFront, "paused");
+ Assert.equal(packet.frame.where.line, result.line);
+ Assert.equal(packet.why.type, result.type);
+ await threadFront[action]();
+}
diff --git a/devtools/server/tests/xpcshell/test_breakpoint-actor-map.js b/devtools/server/tests/xpcshell/test_breakpoint-actor-map.js
new file mode 100644
index 0000000000..e45096095e
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_breakpoint-actor-map.js
@@ -0,0 +1,257 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the functionality of the BreakpointActorMap object.
+
+const {
+ BreakpointActorMap,
+} = require("resource://devtools/server/actors/utils/breakpoint-actor-map.js");
+
+function run_test() {
+ test_get_actor();
+ test_set_actor();
+ test_delete_actor();
+ test_find_actors();
+ test_duplicate_actors();
+}
+
+function test_get_actor() {
+ const bpStore = new BreakpointActorMap();
+ const location = {
+ generatedSourceActor: { actor: "actor1" },
+ generatedLine: 3,
+ };
+ const columnLocation = {
+ generatedSourceActor: { actor: "actor2" },
+ generatedLine: 5,
+ generatedColumn: 15,
+ };
+
+ // Shouldn't have breakpoint
+ Assert.equal(
+ null,
+ bpStore.getActor(location),
+ "Breakpoint not added and shouldn't exist."
+ );
+
+ bpStore.setActor(location, {});
+ Assert.ok(
+ !!bpStore.getActor(location),
+ "Breakpoint added but not found in Breakpoint Store."
+ );
+
+ bpStore.deleteActor(location);
+ Assert.equal(
+ null,
+ bpStore.getActor(location),
+ "Breakpoint removed but still exists."
+ );
+
+ // Same checks for breakpoint with a column
+ Assert.equal(
+ null,
+ bpStore.getActor(columnLocation),
+ "Breakpoint with column not added and shouldn't exist."
+ );
+
+ bpStore.setActor(columnLocation, {});
+ Assert.ok(
+ !!bpStore.getActor(columnLocation),
+ "Breakpoint with column added but not found in Breakpoint Store."
+ );
+
+ bpStore.deleteActor(columnLocation);
+ Assert.equal(
+ null,
+ bpStore.getActor(columnLocation),
+ "Breakpoint with column removed but still exists in Breakpoint Store."
+ );
+}
+
+function test_set_actor() {
+ // Breakpoint with column
+ const bpStore = new BreakpointActorMap();
+ let location = {
+ generatedSourceActor: { actor: "actor1" },
+ generatedLine: 10,
+ generatedColumn: 9,
+ };
+ bpStore.setActor(location, {});
+ Assert.ok(
+ !!bpStore.getActor(location),
+ "We should have the column breakpoint we just added"
+ );
+
+ // Breakpoint without column (whole line breakpoint)
+ location = {
+ generatedSourceActor: { actor: "actor2" },
+ generatedLine: 103,
+ };
+ bpStore.setActor(location, {});
+ Assert.ok(
+ !!bpStore.getActor(location),
+ "We should have the whole line breakpoint we just added"
+ );
+}
+
+function test_delete_actor() {
+ // Breakpoint with column
+ const bpStore = new BreakpointActorMap();
+ let location = {
+ generatedSourceActor: { actor: "actor1" },
+ generatedLine: 10,
+ generatedColumn: 9,
+ };
+ bpStore.setActor(location, {});
+ bpStore.deleteActor(location);
+ Assert.equal(
+ bpStore.getActor(location),
+ null,
+ "We should not have the column breakpoint anymore"
+ );
+
+ // Breakpoint without column (whole line breakpoint)
+ location = {
+ generatedSourceActor: { actor: "actor2" },
+ generatedLine: 103,
+ };
+ bpStore.setActor(location, {});
+ bpStore.deleteActor(location);
+ Assert.equal(
+ bpStore.getActor(location),
+ null,
+ "We should not have the whole line breakpoint anymore"
+ );
+}
+
+function test_find_actors() {
+ const bps = [
+ { generatedSourceActor: { actor: "actor1" }, generatedLine: 10 },
+ {
+ generatedSourceActor: { actor: "actor1" },
+ generatedLine: 10,
+ generatedColumn: 3,
+ },
+ {
+ generatedSourceActor: { actor: "actor1" },
+ generatedLine: 10,
+ generatedColumn: 10,
+ },
+ {
+ generatedSourceActor: { actor: "actor1" },
+ generatedLine: 23,
+ generatedColumn: 89,
+ },
+ {
+ generatedSourceActor: { actor: "actor2" },
+ generatedLine: 10,
+ generatedColumn: 1,
+ },
+ {
+ generatedSourceActor: { actor: "actor2" },
+ generatedLine: 20,
+ generatedColumn: 5,
+ },
+ {
+ generatedSourceActor: { actor: "actor2" },
+ generatedLine: 30,
+ generatedColumn: 34,
+ },
+ {
+ generatedSourceActor: { actor: "actor2" },
+ generatedLine: 40,
+ generatedColumn: 56,
+ },
+ ];
+
+ const bpStore = new BreakpointActorMap();
+
+ for (const bp of bps) {
+ bpStore.setActor(bp, bp);
+ }
+
+ // All breakpoints
+
+ let bpSet = new Set(bps);
+ for (const bp of bpStore.findActors()) {
+ bpSet.delete(bp);
+ }
+ Assert.equal(bpSet.size, 0, "Should be able to iterate over all breakpoints");
+
+ // Breakpoints by URL
+
+ bpSet = new Set(
+ bps.filter(bp => {
+ return bp.generatedSourceActor.actorID === "actor1";
+ })
+ );
+ for (const bp of bpStore.findActors({
+ generatedSourceActor: { actorID: "actor1" },
+ })) {
+ bpSet.delete(bp);
+ }
+ Assert.equal(bpSet.size, 0, "Should be able to filter the iteration by url");
+
+ // Breakpoints by URL and line
+
+ bpSet = new Set(
+ bps.filter(bp => {
+ return (
+ bp.generatedSourceActor.actorID === "actor1" && bp.generatedLine === 10
+ );
+ })
+ );
+ let first = true;
+ for (const bp of bpStore.findActors({
+ generatedSourceActor: { actorID: "actor1" },
+ generatedLine: 10,
+ })) {
+ if (first) {
+ Assert.equal(
+ bp.generatedColumn,
+ undefined,
+ "Should always get the whole line breakpoint first"
+ );
+ first = false;
+ } else {
+ Assert.notEqual(
+ bp.generatedColumn,
+ undefined,
+ "Should not get the whole line breakpoint any time other than first."
+ );
+ }
+ bpSet.delete(bp);
+ }
+ Assert.equal(
+ bpSet.size,
+ 0,
+ "Should be able to filter the iteration by url and line"
+ );
+}
+
+function test_duplicate_actors() {
+ const bpStore = new BreakpointActorMap();
+
+ // Breakpoint with column
+ let location = {
+ generatedSourceActor: { actorID: "foo-actor" },
+ generatedLine: 10,
+ generatedColumn: 9,
+ };
+ bpStore.setActor(location, {});
+ bpStore.setActor(location, {});
+ Assert.equal(bpStore.size, 1, "We should have only 1 column breakpoint");
+ bpStore.deleteActor(location);
+
+ // Breakpoint without column (whole line breakpoint)
+ location = {
+ generatedSourceActor: { actorID: "foo-actor" },
+ generatedLine: 15,
+ };
+ bpStore.setActor(location, {});
+ bpStore.setActor(location, {});
+ Assert.equal(bpStore.size, 1, "We should have only 1 whole line breakpoint");
+ bpStore.deleteActor(location);
+}
diff --git a/devtools/server/tests/xpcshell/test_client_request.js b/devtools/server/tests/xpcshell/test_client_request.js
new file mode 100644
index 0000000000..837bee5047
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_client_request.js
@@ -0,0 +1,220 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the DevToolsClient.request API.
+
+var gClient, gActorId;
+
+const { Actor } = require("resource://devtools/shared/protocol/Actor.js");
+
+class TestActor extends Actor {
+ constructor(conn) {
+ super(conn, { typeName: "test", methods: [] });
+
+ this.requestTypes = {
+ hello: this.hello,
+ error: this.error,
+ };
+ }
+
+ hello() {
+ return { hello: "world" };
+ }
+
+ error() {
+ return { error: "code", message: "human message" };
+ }
+}
+
+function run_test() {
+ ActorRegistry.addGlobalActor(
+ {
+ constructorName: "TestActor",
+ constructorFun: TestActor,
+ },
+ "test"
+ );
+
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+
+ add_test(init);
+ add_test(test_client_request_promise);
+ add_test(test_client_request_promise_error);
+ add_test(test_client_request_event_emitter);
+ add_test(test_close_client_while_sending_requests);
+ add_test(test_client_request_after_close);
+ run_next_test();
+}
+
+function init() {
+ gClient = new DevToolsClient(DevToolsServer.connectPipe());
+ gClient
+ .connect()
+ .then(() => gClient.mainRoot.rootForm)
+ .then(response => {
+ gActorId = response.test;
+ run_next_test();
+ });
+}
+
+function checkStack(expectedName) {
+ let stack = Components.stack;
+ while (stack) {
+ info(stack.name);
+ if (stack.name == expectedName) {
+ // Reached back to outer function before request
+ ok(true, "Complete stack");
+ return;
+ }
+ stack = stack.asyncCaller || stack.caller;
+ }
+ ok(false, "Incomplete stack");
+}
+
+function test_client_request_promise() {
+ // Test that DevToolsClient.request returns a promise that resolves on response
+ const request = gClient.request({
+ to: gActorId,
+ type: "hello",
+ });
+
+ request.then(response => {
+ Assert.equal(response.from, gActorId);
+ Assert.equal(response.hello, "world");
+ checkStack("test_client_request_promise/<");
+ run_next_test();
+ });
+}
+
+function test_client_request_promise_error() {
+ // Test that DevToolsClient.request returns a promise that reject when server
+ // returns an explicit error message
+ const request = gClient.request({
+ to: gActorId,
+ type: "error",
+ });
+
+ request.then(
+ () => {
+ do_throw("Promise shouldn't be resolved on error");
+ },
+ response => {
+ Assert.equal(response.from, gActorId);
+ Assert.equal(response.error, "code");
+ Assert.equal(response.message, "human message");
+ checkStack("test_client_request_promise_error/<");
+ run_next_test();
+ }
+ );
+}
+
+function test_client_request_event_emitter() {
+ // Test that DevToolsClient.request returns also an EventEmitter object
+ const request = gClient.request({
+ to: gActorId,
+ type: "hello",
+ });
+ request.on("json-reply", reply => {
+ Assert.equal(reply.from, gActorId);
+ Assert.equal(reply.hello, "world");
+ checkStack("test_client_request_event_emitter");
+ run_next_test();
+ });
+}
+
+function test_close_client_while_sending_requests() {
+ // First send a first request that will be "active"
+ // while the connection is closed.
+ // i.e. will be sent but no response received yet.
+ const activeRequest = gClient.request({
+ to: gActorId,
+ type: "hello",
+ });
+
+ // Pile up a second one that will be "pending".
+ // i.e. won't event be sent.
+ const pendingRequest = gClient.request({
+ to: gActorId,
+ type: "hello",
+ });
+
+ const expectReply = new Promise(resolve => {
+ gClient.expectReply("root", function (response) {
+ Assert.equal(response.error, "connectionClosed");
+ Assert.equal(
+ response.message,
+ "server side packet can't be received as the connection just closed."
+ );
+ resolve();
+ });
+ });
+
+ gClient.close().then(() => {
+ activeRequest
+ .then(
+ () => {
+ ok(
+ false,
+ "First request unexpectedly succeed while closing the connection"
+ );
+ },
+ response => {
+ Assert.equal(response.error, "connectionClosed");
+ Assert.equal(
+ response.message,
+ "'hello' active request packet to '" +
+ gActorId +
+ "' can't be sent as the connection just closed."
+ );
+ }
+ )
+ .then(() => pendingRequest)
+ .then(
+ () => {
+ ok(
+ false,
+ "Second request unexpectedly succeed while closing the connection"
+ );
+ },
+ response => {
+ Assert.equal(response.error, "connectionClosed");
+ Assert.equal(
+ response.message,
+ "'hello' pending request packet to '" +
+ gActorId +
+ "' can't be sent as the connection just closed."
+ );
+ }
+ )
+ .then(() => expectReply)
+ .then(run_next_test);
+ });
+}
+
+function test_client_request_after_close() {
+ // Test that DevToolsClient.request fails after we called client.close()
+ // (with promise API)
+ const request = gClient.request({
+ to: gActorId,
+ type: "hello",
+ });
+
+ request.then(
+ response => {
+ ok(false, "Request succeed even after client.close");
+ },
+ response => {
+ ok(true, "Request failed after client.close");
+ Assert.equal(response.error, "connectionClosed");
+ ok(
+ response.message.match(
+ /'hello' request packet to '.*' can't be sent as the connection is closed./
+ )
+ );
+ run_next_test();
+ }
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_conditional_breakpoint-01.js b/devtools/server/tests/xpcshell/test_conditional_breakpoint-01.js
new file mode 100644
index 0000000000..8f2e58f651
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_conditional_breakpoint-01.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check conditional breakpoint when condition evaluates to true.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ let hitBreakpoint = false;
+
+ const packet1 = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const source = await getSourceById(threadFront, packet1.frame.where.actor);
+ const location = { sourceUrl: source.url, line: 3 };
+ threadFront.setBreakpoint(location, { condition: "a === 1" });
+
+ // Continue until the breakpoint is hit.
+ const packet2 = await resumeAndWaitForPause(threadFront);
+
+ Assert.equal(hitBreakpoint, false);
+ hitBreakpoint = true;
+
+ // Check the return value.
+ Assert.equal(packet2.why.type, "breakpoint");
+ Assert.equal(packet2.frame.where.line, 3);
+
+ // Remove the breakpoint.
+ await threadFront.removeBreakpoint(location);
+
+ await threadFront.resume();
+
+ Assert.equal(hitBreakpoint, true);
+ })
+);
+
+function evalCode(debuggee) {
+ /* eslint-disable */
+ Cu.evalInSandbox(
+ "debugger;\n" + // line 1
+ "var a = 1;\n" + // line 2
+ "var b = 2;\n", // line 3
+ debuggee,
+ "1.8",
+ "test.js",
+ 1
+ );
+ /* eslint-enable */
+}
diff --git a/devtools/server/tests/xpcshell/test_conditional_breakpoint-02.js b/devtools/server/tests/xpcshell/test_conditional_breakpoint-02.js
new file mode 100644
index 0000000000..18742c4048
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_conditional_breakpoint-02.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check conditional breakpoint when condition evaluates to false.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet1 = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const source = await getSourceById(threadFront, packet1.frame.where.actor);
+ const location1 = { sourceUrl: source.url, line: 3 };
+ threadFront.setBreakpoint(location1, { condition: "a === 2" });
+
+ const location2 = { sourceUrl: source.url, line: 4 };
+ threadFront.setBreakpoint(location2, { condition: "a === 1" });
+
+ // Continue until the breakpoint is hit.
+ const packet2 = await resumeAndWaitForPause(threadFront);
+
+ // Check the return value.
+ Assert.equal(packet2.why.type, "breakpoint");
+ Assert.equal(packet2.frame.where.line, 4);
+
+ // Remove the breakpoint.
+ await threadFront.removeBreakpoint(location2);
+
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ /* eslint-disable */
+ Cu.evalInSandbox(
+ "debugger;\n" + // line 1
+ "var a = 1;\n" + // line 2
+ "var b = 2;\n" + // line 3
+ "b++;" + // line 4
+ "debugger;", // line 5
+ debuggee,
+ "1.8",
+ "test.js",
+ 1
+ );
+ /* eslint-enable */
+}
diff --git a/devtools/server/tests/xpcshell/test_conditional_breakpoint-03.js b/devtools/server/tests/xpcshell/test_conditional_breakpoint-03.js
new file mode 100644
index 0000000000..94ac46c307
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_conditional_breakpoint-03.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * If pauseOnExceptions is checked, when condition throws,
+ * make sure conditional breakpoint pauses but doesn't trigger an exception breakpoint.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee, commands }) => {
+ const packet1 = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const source = await getSourceById(threadFront, packet1.frame.where.actor);
+
+ await commands.threadConfigurationCommand.updateConfiguration({
+ pauseOnExceptions: true,
+ ignoreCaughtExceptions: false,
+ });
+ const location = { sourceUrl: source.url, line: 3 };
+ threadFront.setBreakpoint(location, { condition: "throw new Error()" });
+
+ // Continue until the breakpoint is hit.
+ const packet2 = await resumeAndWaitForPause(threadFront);
+
+ // Check the return value.
+ Assert.equal(packet2.why.type, "breakpointConditionThrown");
+ Assert.equal(packet2.frame.where.line, 3);
+
+ // Remove the breakpoint.
+ await threadFront.removeBreakpoint(location);
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ /* eslint-disable */
+ Cu.evalInSandbox(
+ "debugger;\n" + // line 1
+ "var a = 1;\n" + // line 2
+ "var b = 2;\n", // line 3
+ debuggee,
+ "1.8",
+ "test.js",
+ 1
+ );
+ /* eslint-enable */
+}
diff --git a/devtools/server/tests/xpcshell/test_conditional_breakpoint-04.js b/devtools/server/tests/xpcshell/test_conditional_breakpoint-04.js
new file mode 100644
index 0000000000..b270b92974
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_conditional_breakpoint-04.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Confirm that conditional breakpoint are triggered in case of exceptions,
+ * even when pause-on-exceptions is disabled.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee, commands }) => {
+ await threadFront.setBreakpoint(
+ { sourceUrl: "conditional_breakpoint-04.js", line: 3 },
+ { condition: "throw new Error()" }
+ );
+
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ Assert.equal(packet.frame.where.line, 1);
+ Assert.equal(packet.why.type, "debuggerStatement");
+
+ const pausedPacket = await resumeAndWaitForPause(threadFront);
+ Assert.equal(pausedPacket.frame.where.line, 3);
+ Assert.equal(pausedPacket.why.type, "breakpointConditionThrown");
+
+ const secondPausedPacket = await resumeAndWaitForPause(threadFront);
+ Assert.equal(secondPausedPacket.frame.where.line, 4);
+ Assert.equal(secondPausedPacket.why.type, "debuggerStatement");
+
+ // Remove the breakpoint.
+ await threadFront.removeBreakpoint({
+ sourceUrl: "conditional_breakpoint-04.js",
+ line: 3,
+ });
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ /* eslint-disable */
+ Cu.evalInSandbox(
+ `debugger;
+ var a = 1;
+ var b = 2;
+ debugger;`,
+ debuggee,
+ "1.8",
+ "conditional_breakpoint-04.js",
+ 1
+ );
+ /* eslint-enable */
+}
diff --git a/devtools/server/tests/xpcshell/test_connection_closes_all_pools.js b/devtools/server/tests/xpcshell/test_connection_closes_all_pools.js
new file mode 100644
index 0000000000..d69291485d
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_connection_closes_all_pools.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { Pool } = require("resource://devtools/shared/protocol/Pool.js");
+const {
+ DevToolsServerConnection,
+} = require("resource://devtools/server/devtools-server-connection.js");
+const {
+ LocalDebuggerTransport,
+} = require("resource://devtools/shared/transport/local-transport.js");
+
+// Helper class to assert how many times a Pool was destroyed
+class FakeActor extends Pool {
+ constructor(...args) {
+ super(...args);
+ this.destroyedCount = 0;
+ }
+
+ destroy() {
+ this.destroyedCount++;
+ super.destroy();
+ }
+}
+
+add_task(async function () {
+ const transport = new LocalDebuggerTransport();
+ const conn = new DevToolsServerConnection("prefix", transport);
+
+ // Setup a flat pool hierarchy with multiple pools:
+ //
+ // - pool1
+ // |
+ // \- actor1
+ //
+ // - pool2
+ // |
+ // |- actor2a
+ // |
+ // \- actor2b
+ //
+ // From the point of view of the DevToolsServerConnection, the only pools
+ // registered in _extraPools should be pool1 and pool2. Even though actor1,
+ // actor2a and actor2b extend Pool, they don't manage other pools.
+ const actor1 = new FakeActor(conn);
+ const pool1 = new Pool(conn, "pool-1");
+ pool1.manage(actor1);
+
+ const actor2a = new FakeActor(conn);
+ const actor2b = new FakeActor(conn);
+ const pool2 = new Pool(conn, "pool-2");
+ pool2.manage(actor2a);
+ pool2.manage(actor2b);
+
+ ok(!!actor1.actorID, "actor1 has a valid actorID");
+ ok(!!actor2a.actorID, "actor2a has a valid actorID");
+ ok(!!actor2b.actorID, "actor2b has a valid actorID");
+
+ conn.close();
+
+ equal(actor1.destroyedCount, 1, "actor1 was successfully destroyed");
+ equal(actor2a.destroyedCount, 1, "actor2 was successfully destroyed");
+ equal(actor2b.destroyedCount, 1, "actor2 was successfully destroyed");
+});
+
+add_task(async function () {
+ const transport = new LocalDebuggerTransport();
+ const conn = new DevToolsServerConnection("prefix", transport);
+
+ // Setup a nested pool hierarchy:
+ //
+ // - pool
+ // |
+ // \- parentActor
+ // |
+ // \- childActor
+ //
+ // Since parentActor is also a Pool from the point of view of the
+ // DevToolsServerConnection, it will attempt to destroy it when looping on
+ // this._extraPools. But since `parentActor` is also a direct child of `pool`,
+ // it has already been destroyed by the Pool destroy() mechanism.
+ //
+ // Here we check that we don't call destroy() too many times on a single Pool.
+ // Even though Pool::destroy() is stable when called multiple times, we can't
+ // guarantee the same for classes inheriting Pool.
+ const childActor = new FakeActor(conn);
+ const parentActor = new FakeActor(conn);
+ const pool = new Pool(conn, "pool");
+ pool.manage(parentActor);
+ parentActor.manage(childActor);
+
+ ok(!!parentActor.actorID, "customActor has a valid actorID");
+ ok(!!childActor.actorID, "childActor has a valid actorID");
+
+ conn.close();
+
+ equal(parentActor.destroyedCount, 1, "parentActor was destroyed once");
+ equal(parentActor.destroyedCount, 1, "customActor was destroyed once");
+});
diff --git a/devtools/server/tests/xpcshell/test_console_eval-01.js b/devtools/server/tests/xpcshell/test_console_eval-01.js
new file mode 100644
index 0000000000..abb6ddc605
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_console_eval-01.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/*
+ * Check that is possible to evaluate JS with evaluation timeouts in place.
+ */
+
+add_task(
+ threadFrontTest(async ({ commands }) => {
+ await commands.scriptCommand.execute(`
+ function fib(n) {
+ if (n == 1 || n == 0) {
+ return 1;
+ }
+
+ return fib(n-1) + fib(n-2)
+ }
+ `);
+
+ const normalResult = await commands.scriptCommand.execute("fib(1)", {
+ eager: true,
+ });
+ Assert.equal(normalResult.result, 1, "normal eval");
+
+ const timeoutResult = await commands.scriptCommand.execute("fib(100)", {
+ eager: true,
+ });
+ Assert.equal(typeof timeoutResult.result, "object", "timeout eval");
+ Assert.equal(timeoutResult.result.type, "undefined", "timeout eval type");
+ })
+);
diff --git a/devtools/server/tests/xpcshell/test_console_eval-02.js b/devtools/server/tests/xpcshell/test_console_eval-02.js
new file mode 100644
index 0000000000..11b3d130b4
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_console_eval-02.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/*
+ * Check that bound functions can be eagerly evaluated.
+ */
+
+add_task(
+ threadFrontTest(async ({ commands }) => {
+ await commands.scriptCommand.execute(`
+ var obj = [1, 2, 3];
+ var fn = obj.includes.bind(obj, 2);
+ `);
+
+ const normalResult = await commands.scriptCommand.execute("fn()", {
+ eager: true,
+ });
+ Assert.equal(normalResult.result, true, "normal eval");
+ })
+);
diff --git a/devtools/server/tests/xpcshell/test_dbgactor.js b/devtools/server/tests/xpcshell/test_dbgactor.js
new file mode 100644
index 0000000000..cb0cf8f7d7
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_dbgactor.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const xpcInspector = Cc["@mozilla.org/jsinspector;1"].getService(
+ Ci.nsIJSInspector
+);
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ Assert.equal(xpcInspector.eventLoopNestLevel, 0);
+
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ Assert.equal(false, "error" in packet);
+ Assert.ok("actor" in packet);
+ Assert.ok("why" in packet);
+ Assert.equal(packet.why.type, "debuggerStatement");
+
+ // Reach around the protocol to check that the debuggee is in the state
+ // we expect.
+ Assert.ok(debuggee.a);
+ Assert.ok(!debuggee.b);
+
+ Assert.equal(xpcInspector.eventLoopNestLevel, 1);
+
+ // Let the debuggee continue execution.
+ await threadFront.resume();
+
+ // Now make sure that we've run the code after the debugger statement...
+ Assert.ok(debuggee.b);
+
+ Assert.equal(xpcInspector.eventLoopNestLevel, 0);
+ })
+);
+
+function evalCode(debuggee) {
+ Cu.evalInSandbox(
+ "var a = true; var b = false; debugger; var b = true;",
+ debuggee
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_dbgclient_debuggerstatement.js b/devtools/server/tests/xpcshell/test_dbgclient_debuggerstatement.js
new file mode 100644
index 0000000000..254f582460
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_dbgclient_debuggerstatement.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const xpcInspector = Cc["@mozilla.org/jsinspector;1"].getService(
+ Ci.nsIJSInspector
+);
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ Assert.equal(threadFront.state, "paused");
+ // Reach around the protocol to check that the debuggee is in the state
+ // we expect.
+ Assert.ok(debuggee.a);
+ Assert.ok(!debuggee.b);
+
+ Assert.equal(xpcInspector.eventLoopNestLevel, 1);
+
+ await threadFront.resume();
+
+ // Now make sure that we've run the code after the debugger statement...
+ Assert.ok(debuggee.b);
+
+ Assert.equal(xpcInspector.eventLoopNestLevel, 0);
+ })
+);
+
+function evalCode(debuggee) {
+ Cu.evalInSandbox(
+ "var a = true; var b = false; debugger; var b = true;",
+ debuggee
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_dbgglobal.js b/devtools/server/tests/xpcshell/test_dbgglobal.js
new file mode 100644
index 0000000000..407e270da4
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_dbgglobal.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ SocketListener,
+} = require("resource://devtools/shared/security/socket.js");
+
+function run_test() {
+ // Should get an exception if we try to interact with DevToolsServer
+ // before we initialize it...
+ const socketListener = new SocketListener(DevToolsServer, {});
+ Assert.throws(
+ () => DevToolsServer.addSocketListener(socketListener),
+ /DevToolsServer has not been initialized/,
+ "addSocketListener should throw before it has been initialized"
+ );
+ Assert.throws(
+ DevToolsServer.closeAllSocketListeners,
+ /this is undefined/,
+ "closeAllSocketListeners should throw before it has been initialized"
+ );
+ Assert.throws(
+ DevToolsServer.connectPipe,
+ /this is undefined/,
+ "connectPipe should throw before it has been initialized"
+ );
+
+ // Allow incoming connections.
+ DevToolsServer.init();
+
+ // These should still fail because we haven't added a createRootActor
+ // implementation yet.
+ Assert.throws(
+ DevToolsServer.closeAllSocketListeners,
+ /this is undefined/,
+ "closeAllSocketListeners should throw if createRootActor hasn't been added"
+ );
+ Assert.throws(
+ DevToolsServer.connectPipe,
+ /this is undefined/,
+ "closeAllSocketListeners should throw if createRootActor hasn't been added"
+ );
+
+ const { createRootActor } = require("xpcshell-test/testactors");
+ DevToolsServer.setRootActor(createRootActor);
+
+ // Now they should work.
+ DevToolsServer.addSocketListener(socketListener);
+ DevToolsServer.closeAllSocketListeners();
+
+ // Make sure we got the test's root actor all set up.
+ const client1 = DevToolsServer.connectPipe();
+ client1.hooks = {
+ onPacket(packet1) {
+ Assert.equal(packet1.from, "root");
+ Assert.equal(packet1.applicationType, "xpcshell-tests");
+
+ // Spin up a second connection, make sure it has its own root
+ // actor.
+ const client2 = DevToolsServer.connectPipe();
+ client2.hooks = {
+ onPacket(packet2) {
+ Assert.equal(packet2.from, "root");
+ Assert.notEqual(
+ packet1.testConnectionPrefix,
+ packet2.testConnectionPrefix
+ );
+ client2.close();
+ },
+ onTransportClosed(result) {
+ client1.close();
+ },
+ };
+ client2.ready();
+ },
+
+ onTransportClosed(result) {
+ do_test_finished();
+ },
+ };
+
+ client1.ready();
+ do_test_pending();
+}
diff --git a/devtools/server/tests/xpcshell/test_extension_storage_actor.js b/devtools/server/tests/xpcshell/test_extension_storage_actor.js
new file mode 100644
index 0000000000..9816854cf8
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_extension_storage_actor.js
@@ -0,0 +1,1155 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* globals browser */
+
+"use strict";
+
+const { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+const {
+ createMissingIndexedDBDirs,
+ extensionScriptWithMessageListener,
+ ext_no_bg,
+ getExtensionConfig,
+ openAddonStoragePanel,
+ shutdown,
+ startupExtension,
+} = require("resource://test/webextension-helpers.js");
+
+const l10n = new Localization(["devtools/client/storage.ftl"], true);
+const sessionString = l10n.formatValueSync("storage-expires-session");
+
+// Ignore rejection related to the storage.onChanged listener being removed while the extension context is being closed.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(
+ /Message manager disconnected/
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(
+ /sendRemoveListener on closed conduit/
+);
+
+const { createAppInfo, promiseStartupManager } = AddonTestUtils;
+
+const LEAVE_UUID_PREF = "extensions.webextensions.keepUuidOnUninstall";
+const LEAVE_STORAGE_PREF = "extensions.webextensions.keepStorageOnUninstall";
+
+AddonTestUtils.init(this);
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
+
+ExtensionTestUtils.init(this);
+
+add_setup(async function setup() {
+ await promiseStartupManager();
+ const dir = createMissingIndexedDBDirs();
+
+ Assert.ok(
+ dir.exists(),
+ "Should have a 'storage/permanent' dir in the profile dir"
+ );
+});
+
+add_task(async function test_extension_store_exists() {
+ const extension = await startupExtension(getExtensionConfig());
+
+ const { commands, extensionStorage } = await openAddonStoragePanel(
+ extension.id
+ );
+
+ ok(extensionStorage, "Should have an extensionStorage store");
+
+ await shutdown(extension, commands);
+});
+
+add_task(
+ {
+ // This test currently fails if the extension runs in the main process
+ // like in Thunderbird (see bug 1575183 comment #15 for details).
+ skip_if: () => !WebExtensionPolicy.useRemoteWebExtensions,
+ },
+ async function test_extension_origin_matches_debugger_target() {
+ async function background() {
+ // window is available in background scripts
+ // eslint-disable-next-line no-undef
+ browser.test.sendMessage("extension-origin", window.location.origin);
+ }
+
+ const extension = await startupExtension(
+ getExtensionConfig({ background })
+ );
+
+ const { commands, extensionStorage } = await openAddonStoragePanel(
+ extension.id
+ );
+
+ const { hosts } = extensionStorage;
+ const expectedHost = await extension.awaitMessage("extension-origin");
+ ok(
+ expectedHost in hosts,
+ "Should have the expected extension host in the extensionStorage store"
+ );
+
+ await shutdown(extension, commands);
+ }
+);
+
+/**
+ * Test case: Background page modifies items while storage panel is open.
+ * - Load extension with background page.
+ * - Open the add-on debugger storage panel.
+ * - With the panel still open, from the extension background page:
+ * - Bulk add storage items
+ * - Edit the values of some of the storage items
+ * - Remove some storage items
+ * - Clear all storage items
+ * - For each modification, the storage data in the panel should match the
+ * changes made by the extension.
+ */
+add_task(async function test_panel_live_updates() {
+ const extension = await startupExtension(
+ getExtensionConfig({ background: extensionScriptWithMessageListener })
+ );
+
+ const { commands, extensionStorage } = await openAddonStoragePanel(
+ extension.id
+ );
+
+ const host = await extension.awaitMessage("extension-origin");
+
+ let { data } = await extensionStorage.getStoreObjects(host);
+ Assert.deepEqual(data, [], "Got the expected results on empty storage.local");
+
+ info("Waiting for extension to bulk add 50 items to storage local");
+ const bulkStorageItems = {};
+ // limited by MAX_STORE_OBJECT_COUNT in devtools/server/actors/resources/storage/index.js
+ const numItems = 2;
+ for (let i = 1; i <= numItems; i++) {
+ bulkStorageItems[i] = i;
+ }
+
+ // fireOnChanged avoids the race condition where the extension
+ // modifies storage then immediately tries to access storage before
+ // the storage actor has finished updating.
+ extension.sendMessage("storage-local-fireOnChanged");
+ extension.sendMessage("storage-local-set", {
+ ...bulkStorageItems,
+ a: 123,
+ b: [4, 5],
+ c: { d: 678 },
+ d: true,
+ e: "hi",
+ f: null,
+ });
+ await extension.awaitMessage("storage-local-set:done");
+ await extension.awaitMessage("storage-local-onChanged");
+
+ info(
+ "Confirming items added by extension match items in extensionStorage store"
+ );
+ const bulkStorageObjects = [];
+ for (const [name, value] of Object.entries(bulkStorageItems)) {
+ bulkStorageObjects.push({
+ area: "local",
+ name,
+ value: { str: String(value) },
+ isValueEditable: true,
+ });
+ }
+ data = (await extensionStorage.getStoreObjects(host, null, { sessionString }))
+ .data;
+ Assert.deepEqual(
+ data,
+ [
+ ...bulkStorageObjects,
+ {
+ area: "local",
+ name: "a",
+ value: { str: "123" },
+ isValueEditable: true,
+ },
+ {
+ area: "local",
+ name: "b",
+ value: { str: "[4,5]" },
+ isValueEditable: true,
+ },
+ {
+ area: "local",
+ name: "c",
+ value: { str: '{"d":678}' },
+ isValueEditable: true,
+ },
+ {
+ area: "local",
+ name: "d",
+ value: { str: "true" },
+ isValueEditable: true,
+ },
+ {
+ area: "local",
+ name: "e",
+ value: { str: "hi" },
+ isValueEditable: true,
+ },
+ {
+ area: "local",
+ name: "f",
+ value: { str: "null" },
+ isValueEditable: true,
+ },
+ ],
+ "Got the expected results on populated storage.local"
+ );
+
+ info("Waiting for extension to edit a few storage item values");
+ extension.sendMessage("storage-local-fireOnChanged");
+ extension.sendMessage("storage-local-set", {
+ a: ["c", "d"],
+ b: 456,
+ c: false,
+ });
+ await extension.awaitMessage("storage-local-set:done");
+ await extension.awaitMessage("storage-local-onChanged");
+
+ info(
+ "Confirming items edited by extension match items in extensionStorage store"
+ );
+ data = (await extensionStorage.getStoreObjects(host, null, { sessionString }))
+ .data;
+ Assert.deepEqual(
+ data,
+ [
+ ...bulkStorageObjects,
+ {
+ area: "local",
+ name: "a",
+ value: { str: '["c","d"]' },
+ isValueEditable: true,
+ },
+ {
+ area: "local",
+ name: "b",
+ value: { str: "456" },
+ isValueEditable: true,
+ },
+ {
+ area: "local",
+ name: "c",
+ value: { str: "false" },
+ isValueEditable: true,
+ },
+ {
+ area: "local",
+ name: "d",
+ value: { str: "true" },
+ isValueEditable: true,
+ },
+ {
+ area: "local",
+ name: "e",
+ value: { str: "hi" },
+ isValueEditable: true,
+ },
+ {
+ area: "local",
+ name: "f",
+ value: { str: "null" },
+ isValueEditable: true,
+ },
+ ],
+ "Got the expected results on populated storage.local"
+ );
+
+ info("Waiting for extension to remove a few storage item values");
+ extension.sendMessage("storage-local-fireOnChanged");
+ extension.sendMessage("storage-local-remove", ["d", "e", "f"]);
+ await extension.awaitMessage("storage-local-remove:done");
+ await extension.awaitMessage("storage-local-onChanged");
+
+ info(
+ "Confirming items removed by extension were removed in extensionStorage store"
+ );
+ data = (await extensionStorage.getStoreObjects(host, null, { sessionString }))
+ .data;
+ Assert.deepEqual(
+ data,
+ [
+ ...bulkStorageObjects,
+ {
+ area: "local",
+ name: "a",
+ value: { str: '["c","d"]' },
+ isValueEditable: true,
+ },
+ {
+ area: "local",
+ name: "b",
+ value: { str: "456" },
+ isValueEditable: true,
+ },
+ {
+ area: "local",
+ name: "c",
+ value: { str: "false" },
+ isValueEditable: true,
+ },
+ ],
+ "Got the expected results on populated storage.local"
+ );
+
+ info("Waiting for extension to remove all remaining storage items");
+ extension.sendMessage("storage-local-fireOnChanged");
+ extension.sendMessage("storage-local-clear");
+ await extension.awaitMessage("storage-local-clear:done");
+ await extension.awaitMessage("storage-local-onChanged");
+
+ info("Confirming extensionStorage store was cleared");
+ data = (await extensionStorage.getStoreObjects(host)).data;
+ Assert.deepEqual(
+ data,
+ [],
+ "Got the expected results on populated storage.local"
+ );
+
+ await shutdown(extension, commands);
+});
+
+/**
+ * Test case: No bg page. Transient page adds item before storage panel opened.
+ * - Load extension with no background page.
+ * - Open an extension page in a tab that adds a local storage item.
+ * - With the extension page still open, open the add-on storage panel.
+ * - The data in the storage panel should match the items added by the extension.
+ */
+add_task(
+ async function test_panel_data_matches_extension_with_transient_page_open() {
+ const extension = await startupExtension(
+ getExtensionConfig({ files: ext_no_bg.files })
+ );
+
+ const url = extension.extension.baseURI.resolve(
+ "extension_page_in_tab.html"
+ );
+ const contentPage = await ExtensionTestUtils.loadContentPage(url, {
+ extension,
+ });
+
+ const host = await extension.awaitMessage("extension-origin");
+
+ extension.sendMessage("storage-local-set", { a: 123 });
+ await extension.awaitMessage("storage-local-set:done");
+
+ const { commands, extensionStorage } = await openAddonStoragePanel(
+ extension.id
+ );
+
+ const { data } = await extensionStorage.getStoreObjects(host);
+ Assert.deepEqual(
+ data,
+ [
+ {
+ area: "local",
+ name: "a",
+ value: { str: "123" },
+ isValueEditable: true,
+ },
+ ],
+ "Got the expected results on populated storage.local"
+ );
+
+ await contentPage.close();
+ await shutdown(extension, commands);
+ }
+);
+
+/**
+ * Test case: No bg page. Transient page adds item then closes before storage panel opened.
+ * - Load extension with no background page.
+ * - Open an extension page in a tab that adds a local storage item.
+ * - Close all extension pages.
+ * - Open the add-on storage panel.
+ * - The data in the storage panel should match the item added by the extension.
+ */
+add_task(async function test_panel_data_matches_extension_with_no_pages_open() {
+ const extension = await startupExtension(
+ getExtensionConfig({ files: ext_no_bg.files })
+ );
+
+ const url = extension.extension.baseURI.resolve("extension_page_in_tab.html");
+ const contentPage = await ExtensionTestUtils.loadContentPage(url, {
+ extension,
+ });
+
+ const host = await extension.awaitMessage("extension-origin");
+
+ extension.sendMessage("storage-local-fireOnChanged");
+ extension.sendMessage("storage-local-set", { a: 123 });
+ await extension.awaitMessage("storage-local-set:done");
+ await extension.awaitMessage("storage-local-onChanged");
+ await contentPage.close();
+
+ const { commands, extensionStorage } = await openAddonStoragePanel(
+ extension.id
+ );
+
+ const { data } = await extensionStorage.getStoreObjects(host);
+ Assert.deepEqual(
+ data,
+ [
+ {
+ area: "local",
+ name: "a",
+ value: { str: "123" },
+ isValueEditable: true,
+ },
+ ],
+ "Got the expected results on populated storage.local"
+ );
+
+ await shutdown(extension, commands);
+});
+
+/**
+ * Test case: No bg page. Storage panel live updates when a transient page adds an item.
+ * - Load extension with no background page.
+ * - Open the add-on storage panel.
+ * - With the storage panel still open, open an extension page in a new tab that adds an
+ * item.
+ * - The data in the storage panel should live update to match the item added by the
+ * extension.
+ * - If an extension page adds the same data again, the data in the storage panel should
+ * not change.
+ */
+add_task(
+ async function test_panel_data_live_updates_for_extension_without_bg_page() {
+ const extension = await startupExtension(
+ getExtensionConfig({ files: ext_no_bg.files })
+ );
+
+ const { commands, extensionStorage } = await openAddonStoragePanel(
+ extension.id
+ );
+
+ const url = extension.extension.baseURI.resolve(
+ "extension_page_in_tab.html"
+ );
+ const contentPage = await ExtensionTestUtils.loadContentPage(url, {
+ extension,
+ });
+
+ const host = await extension.awaitMessage("extension-origin");
+
+ let { data } = await extensionStorage.getStoreObjects(host);
+ Assert.deepEqual(
+ data,
+ [],
+ "Got the expected results on empty storage.local"
+ );
+
+ extension.sendMessage("storage-local-fireOnChanged");
+ extension.sendMessage("storage-local-set", { a: 123 });
+ await extension.awaitMessage("storage-local-set:done");
+ await extension.awaitMessage("storage-local-onChanged");
+
+ data = (await extensionStorage.getStoreObjects(host)).data;
+ Assert.deepEqual(
+ data,
+ [
+ {
+ area: "local",
+ name: "a",
+ value: { str: "123" },
+ isValueEditable: true,
+ },
+ ],
+ "Got the expected results on populated storage.local"
+ );
+
+ extension.sendMessage("storage-local-fireOnChanged");
+ extension.sendMessage("storage-local-set", { a: 123 });
+ await extension.awaitMessage("storage-local-set:done");
+ await extension.awaitMessage("storage-local-onChanged");
+
+ data = (await extensionStorage.getStoreObjects(host)).data;
+ Assert.deepEqual(
+ data,
+ [
+ {
+ area: "local",
+ name: "a",
+ value: { str: "123" },
+ isValueEditable: true,
+ },
+ ],
+ "The results are unchanged when an extension page adds duplicate items"
+ );
+
+ await contentPage.close();
+ await shutdown(extension, commands);
+ }
+);
+
+/**
+ * Test case: Bg page adds item while storage panel is open. Panel edits item's value.
+ * - Load extension with background page.
+ * - Open the add-on storage panel.
+ * - With the storage panel still open, add item from the background page.
+ * - Edit the value of the item in the storage panel
+ * - The data in the storage panel should match the item added by the extension.
+ * - The storage actor is correctly parsing and setting the string representation of
+ * the value in the storage local database when the item's value is edited in the
+ * storage panel
+ */
+add_task(
+ async function test_editing_items_in_panel_parses_supported_values_correctly() {
+ const extension = await startupExtension(
+ getExtensionConfig({ background: extensionScriptWithMessageListener })
+ );
+
+ const host = await extension.awaitMessage("extension-origin");
+
+ const { commands, extensionStorage } = await openAddonStoragePanel(
+ extension.id
+ );
+
+ const oldItem = { a: 123 };
+ const key = Object.keys(oldItem)[0];
+ const oldValue = oldItem[key];
+ // A tuple representing information for a new value entered into the panel for oldItem:
+ // [
+ // value,
+ // editItem string representation of value,
+ // toStoreObject string representation of value,
+ // ]
+ const valueInfo = [
+ [true, "true", "true"],
+ ["hi", "hi", "hi"],
+ [456, "456", "456"],
+ [{ b: 789 }, "{b: 789}", '{"b":789}'],
+ [[1, 2, 3], "[1, 2, 3]", "[1,2,3]"],
+ [null, "null", "null"],
+ ];
+ for (const [value, editItemValueStr, toStoreObjectValueStr] of valueInfo) {
+ info("Setting a storage item through the extension");
+ extension.sendMessage("storage-local-fireOnChanged");
+ extension.sendMessage("storage-local-set", oldItem);
+ await extension.awaitMessage("storage-local-set:done");
+ await extension.awaitMessage("storage-local-onChanged");
+
+ info(
+ "Editing the storage item in the panel with a new value of a different type"
+ );
+ // When the user edits an item in the panel, they are entering a string into a
+ // textbox. This string is parsed by the storage actor's editItem method.
+ await extensionStorage.editItem({
+ host,
+ field: "value",
+ items: { name: key, value: editItemValueStr },
+ oldValue,
+ });
+
+ info(
+ "Verifying item in the storage actor matches the item edited in the panel"
+ );
+ const { data } = await extensionStorage.getStoreObjects(host);
+ Assert.deepEqual(
+ data,
+ [
+ {
+ area: "local",
+ name: key,
+ value: { str: toStoreObjectValueStr },
+ isValueEditable: true,
+ },
+ ],
+ "Got the expected results on populated storage.local"
+ );
+
+ // The view layer is separate from the database layer; therefore while values are
+ // stringified (via toStoreObject) for display in the client, the value (and its type)
+ // in the database is unchanged.
+ info(
+ "Verifying the expected new value matches the value fetched in the extension"
+ );
+ extension.sendMessage("storage-local-get", key);
+ const extItem = await extension.awaitMessage("storage-local-get:done");
+ Assert.deepEqual(
+ value,
+ extItem[key],
+ `The string value ${editItemValueStr} was correctly parsed to ${value}`
+ );
+ }
+
+ await shutdown(extension, commands);
+ }
+);
+
+/**
+ * Test case: Modifying storage items from the panel update extension storage local data.
+ * - Load extension with background page.
+ * - Open the add-on storage panel. From the panel:
+ * - Edit the value of a storage item,
+ * - Remove a storage item,
+ * - Remove all of the storage items,
+ * - For each modification, the storage data retrieved by the extension should match the
+ * data in the panel.
+ */
+add_task(
+ async function test_modifying_items_in_panel_updates_extension_storage_data() {
+ const extension = await startupExtension(
+ getExtensionConfig({ background: extensionScriptWithMessageListener })
+ );
+
+ const host = await extension.awaitMessage("extension-origin");
+
+ const { commands, extensionStorage } = await openAddonStoragePanel(
+ extension.id
+ );
+
+ const DEFAULT_VALUE = "value"; // global in devtools/server/actors/resources/storage/index.js
+ let items = {
+ guid_1: DEFAULT_VALUE,
+ guid_2: DEFAULT_VALUE,
+ guid_3: DEFAULT_VALUE,
+ };
+
+ info("Adding storage items from the extension");
+ let storesUpdate = extensionStorage.once("single-store-update");
+ extension.sendMessage("storage-local-set", items);
+ await extension.awaitMessage("storage-local-set:done");
+
+ info("Waiting for the storage actor to emit a 'stores-update' event");
+ let data = await storesUpdate;
+ Assert.deepEqual(
+ {
+ added: {
+ extensionStorage: {
+ [host]: ["guid_1", "guid_2", "guid_3"],
+ },
+ },
+ changed: undefined,
+ deleted: undefined,
+ },
+ data,
+ "The change data from the storage actor's 'stores-update' event matches the changes made in the client."
+ );
+
+ info("Waiting for panel to edit some items");
+ storesUpdate = extensionStorage.once("single-store-update");
+ await extensionStorage.editItem({
+ host,
+ field: "value",
+ items: { name: "guid_1", value: "anotherValue" },
+ DEFAULT_VALUE,
+ });
+
+ info("Waiting for the storage actor to emit a 'stores-update' event");
+ data = await storesUpdate;
+ Assert.deepEqual(
+ {
+ added: undefined,
+ changed: {
+ extensionStorage: {
+ [host]: ["guid_1"],
+ },
+ },
+ deleted: undefined,
+ },
+ data,
+ "The change data from the storage actor's 'stores-update' event matches the changes made in the client."
+ );
+
+ items = {
+ guid_1: "anotherValue",
+ guid_2: DEFAULT_VALUE,
+ guid_3: DEFAULT_VALUE,
+ };
+ extension.sendMessage("storage-local-get", Object.keys(items));
+ let extItems = await extension.awaitMessage("storage-local-get:done");
+ Assert.deepEqual(
+ items,
+ extItems,
+ `The storage items in the extension match the items in the panel`
+ );
+
+ info("Waiting for panel to remove an item");
+ storesUpdate = extensionStorage.once("single-store-update");
+ await extensionStorage.removeItem(host, "guid_3");
+
+ info("Waiting for the storage actor to emit a 'stores-update' event");
+ data = await storesUpdate;
+ Assert.deepEqual(
+ {
+ added: undefined,
+ changed: undefined,
+ deleted: {
+ extensionStorage: {
+ [host]: ["guid_3"],
+ },
+ },
+ },
+ data,
+ "The change data from the storage actor's 'stores-update' event matches the changes made in the client."
+ );
+
+ items = {
+ guid_1: "anotherValue",
+ guid_2: DEFAULT_VALUE,
+ };
+ extension.sendMessage("storage-local-get", Object.keys(items));
+ extItems = await extension.awaitMessage("storage-local-get:done");
+ Assert.deepEqual(
+ items,
+ extItems,
+ `The storage items in the extension match the items in the panel`
+ );
+
+ info("Waiting for panel to remove all items");
+ const storesCleared = extensionStorage.once("single-store-cleared");
+ await extensionStorage.removeAll(host);
+
+ info("Waiting for the storage actor to emit a 'stores-cleared' event");
+ data = await storesCleared;
+ Assert.deepEqual(
+ {
+ clearedHostsOrPaths: {
+ [host]: [],
+ },
+ },
+ data,
+ "The change data from the storage actor's 'stores-cleared' event matches the changes made in the client."
+ );
+
+ items = {};
+ extension.sendMessage("storage-local-get", Object.keys(items));
+ extItems = await extension.awaitMessage("storage-local-get:done");
+ Assert.deepEqual(
+ items,
+ extItems,
+ `The storage items in the extension match the items in the panel`
+ );
+
+ await shutdown(extension, commands);
+ }
+);
+
+/**
+ * Test case: Storage panel shows extension storage data added prior to extension startup
+ * - Load extension that adds a storage item
+ * - Uninstall the extension
+ * - Reinstall the extension
+ * - Open the add-on storage panel.
+ * - The data in the storage panel should match the data added the first time the extension
+ * was installed
+ * Related test case: Storage panel shows extension storage data when an extension that has
+ * already migrated to the IndexedDB storage backend prior to extension startup adds
+ * another storage item.
+ * - (Building from previous steps)
+ * - The reinstalled extension adds a storage item
+ * - The data in the storage panel should live update with both items: the item added from
+ * the first and the item added from the reinstall.
+ */
+add_task(
+ async function test_panel_data_matches_data_added_prior_to_ext_startup() {
+ // The pref to leave the addonid->uuid mapping around after uninstall so that we can
+ // re-attach to the same storage
+ Services.prefs.setBoolPref(LEAVE_UUID_PREF, true);
+
+ // The pref to prevent cleaning up storage on uninstall
+ Services.prefs.setBoolPref(LEAVE_STORAGE_PREF, true);
+
+ let extension = await startupExtension(
+ getExtensionConfig({ background: extensionScriptWithMessageListener })
+ );
+
+ const host = await extension.awaitMessage("extension-origin");
+
+ extension.sendMessage("storage-local-set", { a: 123 });
+ await extension.awaitMessage("storage-local-set:done");
+
+ await shutdown(extension);
+
+ // Reinstall the same extension
+ extension = await startupExtension(
+ getExtensionConfig({ background: extensionScriptWithMessageListener })
+ );
+
+ await extension.awaitMessage("extension-origin");
+
+ const { commands, extensionStorage } = await openAddonStoragePanel(
+ extension.id
+ );
+
+ let { data } = await extensionStorage.getStoreObjects(host);
+ Assert.deepEqual(
+ data,
+ [
+ {
+ area: "local",
+ name: "a",
+ value: { str: "123" },
+ isValueEditable: true,
+ },
+ ],
+ "Got the expected results on populated storage.local"
+ );
+
+ // Related test case
+ extension.sendMessage("storage-local-fireOnChanged");
+ extension.sendMessage("storage-local-set", { b: 456 });
+ await extension.awaitMessage("storage-local-set:done");
+ await extension.awaitMessage("storage-local-onChanged");
+
+ data = (
+ await extensionStorage.getStoreObjects(host, null, { sessionString })
+ ).data;
+ Assert.deepEqual(
+ data,
+ [
+ {
+ area: "local",
+ name: "a",
+ value: { str: "123" },
+ isValueEditable: true,
+ },
+ {
+ area: "local",
+ name: "b",
+ value: { str: "456" },
+ isValueEditable: true,
+ },
+ ],
+ "Got the expected results on populated storage.local"
+ );
+
+ Services.prefs.setBoolPref(LEAVE_STORAGE_PREF, false);
+ Services.prefs.setBoolPref(LEAVE_UUID_PREF, false);
+
+ await shutdown(extension, commands);
+ }
+);
+
+add_task(
+ function cleanup_for_test_panel_data_matches_data_added_prior_to_ext_startup() {
+ Services.prefs.clearUserPref(LEAVE_UUID_PREF);
+ Services.prefs.clearUserPref(LEAVE_STORAGE_PREF);
+ }
+);
+
+/**
+ * Test case: Transient page adds an item to storage. With storage panel open,
+ * reload extension.
+ * - Load extension with no background page.
+ * - Open transient page that adds a storage item on message.
+ * - Open the add-on storage panel.
+ * - With the storage panel still open, reload the extension.
+ * - The data in the storage panel should match the item added prior to reloading.
+ */
+add_task(async function test_panel_live_reload_for_extension_without_bg_page() {
+ const EXTENSION_ID = "test_local_storage_live_reload@xpcshell.mozilla.org";
+ let manifest = {
+ version: "1.0",
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ };
+
+ info("Loading and starting extension version 1.0");
+ const extension = await startupExtension(
+ getExtensionConfig({
+ manifest,
+ files: ext_no_bg.files,
+ })
+ );
+
+ info("Opening extension page in a tab");
+ const url = extension.extension.baseURI.resolve("extension_page_in_tab.html");
+ const contentPage = await ExtensionTestUtils.loadContentPage(url, {
+ extension,
+ });
+
+ const host = await extension.awaitMessage("extension-origin");
+
+ info("Waiting for extension page in a tab to add storage item");
+ extension.sendMessage("storage-local-fireOnChanged");
+ extension.sendMessage("storage-local-set", { a: 123 });
+ await extension.awaitMessage("storage-local-set:done");
+ await extension.awaitMessage("storage-local-onChanged");
+ await contentPage.close();
+
+ info("Opening storage panel");
+ const { commands, extensionStorage } = await openAddonStoragePanel(
+ extension.id
+ );
+
+ manifest = {
+ ...manifest,
+ version: "2.0",
+ };
+ // "Reload" is most similar to an upgrade, as e.g. storage data is preserved
+ info("Updating extension to version 2.0");
+ await extension.upgrade(
+ getExtensionConfig({
+ manifest,
+ files: ext_no_bg.files,
+ })
+ );
+
+ const { data } = await extensionStorage.getStoreObjects(host);
+ Assert.deepEqual(
+ data,
+ [
+ {
+ area: "local",
+ name: "a",
+ value: { str: "123" },
+ isValueEditable: true,
+ },
+ ],
+ "Got the expected results on populated storage.local"
+ );
+
+ await shutdown(extension, commands);
+});
+
+/**
+ * Test case: Bg page auto adds item(s). With storage panel open, reload extension.
+ * - Load extension with background page that automatically adds a storage item on startup.
+ * - Open the add-on storage panel.
+ * - With the storage panel still open, reload the extension.
+ * - The data in the storage panel should match the item(s) added by the reloaded
+ * extension.
+ */
+add_task(
+ async function test_panel_live_reload_when_extension_auto_adds_items() {
+ async function background() {
+ await browser.storage.local.set({ a: { b: 123 }, c: { d: 456 } });
+ // window is available in background scripts
+ // eslint-disable-next-line no-undef
+ browser.test.sendMessage("extension-origin", window.location.origin);
+ }
+ const EXTENSION_ID = "test_local_storage_live_reload@xpcshell.mozilla.org";
+ let manifest = {
+ version: "1.0",
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ };
+
+ info("Loading and starting extension version 1.0");
+ const extension = await startupExtension(
+ getExtensionConfig({ manifest, background })
+ );
+
+ info("Waiting for message from test extension");
+ const host = await extension.awaitMessage("extension-origin");
+
+ info("Opening storage panel");
+ const { commands, extensionStorage } = await openAddonStoragePanel(
+ extension.id
+ );
+
+ manifest = {
+ ...manifest,
+ version: "2.0",
+ };
+ // "Reload" is most similar to an upgrade, as e.g. storage data is preserved
+ info("Update to version 2.0");
+ await extension.upgrade(
+ getExtensionConfig({
+ manifest,
+ background,
+ })
+ );
+
+ await extension.awaitMessage("extension-origin");
+
+ const { data } = await extensionStorage.getStoreObjects(host, null, {
+ sessionString,
+ });
+ Assert.deepEqual(
+ data,
+ [
+ {
+ area: "local",
+ name: "a",
+ value: { str: '{"b":123}' },
+ isValueEditable: true,
+ },
+ {
+ area: "local",
+ name: "c",
+ value: { str: '{"d":456}' },
+ isValueEditable: true,
+ },
+ ],
+ "Got the expected results on populated storage.local"
+ );
+
+ await shutdown(extension, commands);
+ }
+);
+
+/**
+ * Test case: Bg page adds one storage.local item and one storage.sync item.
+ * - Load extension with background page that automatically adds two storage items on startup.
+ * - Open the add-on storage panel.
+ * - Assert that only the storage.local item is shown in the panel.
+ */
+add_task(
+ async function test_panel_data_only_updates_for_storage_local_changes() {
+ async function background() {
+ await browser.storage.local.set({ a: { b: 123 } });
+ await browser.storage.sync.set({ c: { d: 456 } });
+ // window is available in background scripts
+ // eslint-disable-next-line no-undef
+ browser.test.sendMessage("extension-origin", window.location.origin);
+ }
+
+ // Using the storage.sync API requires a non-temporary extension ID, see Bug 1323228.
+ const EXTENSION_ID =
+ "test_panel_data_only_updates_for_storage_local_changes@xpcshell.mozilla.org";
+ const manifest = {
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ };
+
+ info("Loading and starting extension");
+ const extension = await startupExtension(
+ getExtensionConfig({ manifest, background })
+ );
+
+ info("Waiting for message from test extension");
+ const host = await extension.awaitMessage("extension-origin");
+
+ info("Opening storage panel");
+ const { commands, extensionStorage } = await openAddonStoragePanel(
+ extension.id
+ );
+
+ const { data } = await extensionStorage.getStoreObjects(host);
+ Assert.deepEqual(
+ data,
+ [
+ {
+ area: "local",
+ name: "a",
+ value: { str: '{"b":123}' },
+ isValueEditable: true,
+ },
+ ],
+ "Got the expected results on populated storage.local"
+ );
+
+ await shutdown(extension, commands);
+ }
+);
+
+// This test verifies that Bug 1802929 fix doesn't regress.
+add_task(async function test_live_update_with_no_extension_listener() {
+ const EXTENSION_ID = "test_with_no_listeners@xpcshell.mozilla.org";
+ let manifest = {
+ version: "1.0",
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ };
+
+ function background() {
+ browser.test.onMessage.addListener(async (msg, ...args) => {
+ if (msg !== "storage-local-api-call") {
+ browser.test.fail(`Got unexpected test message: ${msg}`);
+ return;
+ }
+
+ const [{ method, methodArgs }] = args;
+ const res = await browser.storage.local[method](...methodArgs);
+ browser.test.sendMessage(`${msg}:done`, res);
+ });
+ }
+
+ const extension = await startupExtension(
+ getExtensionConfig({ manifest, background })
+ );
+
+ const { target, extensionStorage } = await openAddonStoragePanel(
+ extension.id
+ );
+
+ const { baseURI } = extension.extension;
+ const host = `${baseURI.scheme}://${baseURI.host}`;
+
+ let { data } = await extensionStorage.getStoreObjects(host);
+ Assert.deepEqual(data, [], "Got the expected results on empty storage.local");
+
+ async function testStorageLocalUpdate(storageValue) {
+ info("Store extension data");
+ await extension.sendMessage("storage-local-api-call", {
+ method: "set",
+ methodArgs: [{ storageKeyName: storageValue }],
+ });
+ await extension.awaitMessage("storage-local-api-call:done");
+
+ info("Verify stored extension data");
+ await extension.sendMessage("storage-local-api-call", {
+ method: "get",
+ methodArgs: [],
+ });
+
+ Assert.deepEqual(
+ await extension.awaitMessage("storage-local-api-call:done"),
+ { storageKeyName: storageValue },
+ "Got the expected value from browser.storage.local.get"
+ );
+
+ await TestUtils.waitForCondition(async () => {
+ const res = await extensionStorage.getStoreObjects(host);
+ return res.data?.length > 0;
+ }, "Wait for the extension storage panel updates");
+
+ data = (await extensionStorage.getStoreObjects(host)).data;
+ Assert.deepEqual(
+ data,
+ [
+ {
+ area: "local",
+ name: "storageKeyName",
+ value: { str: `${storageValue}` },
+ isValueEditable: true,
+ },
+ ],
+ "Expected DevTools Storage panel data to have been updated"
+ );
+ }
+
+ await testStorageLocalUpdate("aStorageValue 01");
+
+ manifest = {
+ ...manifest,
+ version: "2.0",
+ };
+ // "Reload" is most similar to an upgrade, as e.g. storage data is preserved
+ info("Update to version 2.0");
+ await extension.upgrade(getExtensionConfig({ manifest, background }));
+
+ await testStorageLocalUpdate("aStorageValue 02");
+
+ await shutdown(extension, target);
+});
diff --git a/devtools/server/tests/xpcshell/test_extension_storage_actor_upgrade.js b/devtools/server/tests/xpcshell/test_extension_storage_actor_upgrade.js
new file mode 100644
index 0000000000..5d2285b9e8
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_extension_storage_actor_upgrade.js
@@ -0,0 +1,142 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Note: this test used to be in test_extension_storage_actor.js, but seems to
+ * fail frequently as soon as we start auto-attaching targets.
+ * See Bug 1618059.
+ */
+
+const { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+const {
+ createMissingIndexedDBDirs,
+ extensionScriptWithMessageListener,
+ getExtensionConfig,
+ openAddonStoragePanel,
+ shutdown,
+ startupExtension,
+} = require("resource://test/webextension-helpers.js");
+
+const l10n = new Localization(["devtools/client/storage.ftl"], true);
+const sessionString = l10n.formatValueSync("storage-expires-session");
+
+// Ignore rejection related to the storage.onChanged listener being removed while the extension context is being closed.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(
+ /Message manager disconnected/
+);
+
+const { createAppInfo, promiseStartupManager } = AddonTestUtils;
+
+AddonTestUtils.init(this);
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
+
+ExtensionTestUtils.init(this);
+
+add_task(async function setup() {
+ await promiseStartupManager();
+ const dir = createMissingIndexedDBDirs();
+
+ Assert.ok(
+ dir.exists(),
+ "Should have a 'storage/permanent' dir in the profile dir"
+ );
+});
+
+/**
+ * Test case: Bg page adds an item to storage. With storage panel open, reload extension.
+ * - Load extension with background page that adds a storage item on message.
+ * - Open the add-on storage panel.
+ * - With the storage panel still open, reload the extension.
+ * - The data in the storage panel should match the item added prior to reloading.
+ */
+add_task(async function test_panel_live_reload() {
+ const EXTENSION_ID = "test_panel_live_reload@xpcshell.mozilla.org";
+ let manifest = {
+ version: "1.0",
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ };
+
+ info("Loading extension version 1.0");
+ const extension = await startupExtension(
+ getExtensionConfig({
+ manifest,
+ background: extensionScriptWithMessageListener,
+ })
+ );
+
+ info("Waiting for message from test extension");
+ const host = await extension.awaitMessage("extension-origin");
+
+ info("Adding storage item");
+ extension.sendMessage("storage-local-set", { a: 123 });
+ await extension.awaitMessage("storage-local-set:done");
+
+ const { commands, extensionStorage } = await openAddonStoragePanel(
+ extension.id
+ );
+
+ manifest = {
+ ...manifest,
+ version: "2.0",
+ };
+ // "Reload" is most similar to an upgrade, as e.g. storage data is preserved
+ info("Update to version 2.0");
+
+ // Wait for the storage front to receive an event for the storage panel refresh
+ // when the extension has been reloaded.
+ const promiseStoragePanelUpdated = new Promise(resolve => {
+ extensionStorage.on(
+ "single-store-update",
+ function updateListener(updates) {
+ info(`Got stores-update event: ${JSON.stringify(updates)}`);
+ const extStorageAdded = updates.added?.extensionStorage;
+ if (host in extStorageAdded && extStorageAdded[host].length) {
+ extensionStorage.off("single-store-update", updateListener);
+ resolve();
+ }
+ }
+ );
+ });
+
+ await extension.upgrade(
+ getExtensionConfig({
+ manifest,
+ background: extensionScriptWithMessageListener,
+ })
+ );
+
+ await Promise.all([
+ extension.awaitMessage("extension-origin"),
+ promiseStoragePanelUpdated,
+ ]);
+
+ const { data } = await extensionStorage.getStoreObjects(host, null, {
+ sessionString,
+ });
+ Assert.deepEqual(
+ data,
+ [
+ {
+ area: "local",
+ name: "a",
+ value: { str: "123" },
+ isValueEditable: true,
+ },
+ ],
+ "Got the expected results on populated storage.local"
+ );
+
+ await shutdown(extension, commands);
+});
diff --git a/devtools/server/tests/xpcshell/test_forwardingprefix.js b/devtools/server/tests/xpcshell/test_forwardingprefix.js
new file mode 100644
index 0000000000..e917350da5
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_forwardingprefix.js
@@ -0,0 +1,226 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* Exercise prefix-based forwarding of packets to other transports. */
+
+const { RootActor } = require("resource://devtools/server/actors/root.js");
+
+var gMainConnection, gMainTransport;
+var gSubconnection1, gSubconnection2;
+var gClient;
+
+function run_test() {
+ DevToolsServer.init();
+
+ add_test(createMainConnection);
+ add_test(TestNoForwardingYet);
+ add_test(createSubconnection1);
+ add_test(TestForwardPrefix1OnlyRoot);
+ add_test(createSubconnection2);
+ add_test(TestForwardPrefix12OnlyRoot);
+ add_test(TestForwardPrefix12WithActor1);
+ add_test(TestForwardPrefix12WithActor12);
+ run_next_test();
+}
+
+/*
+ * Create a pipe connection, and return an object |{ conn, transport }|,
+ * where |conn| is the new DevToolsServerConnection instance, and
+ * |transport| is the client side of the transport on which it communicates
+ * (that is, packets sent on |transport| go to the new connection, and
+ * |transport|'s hooks receive replies).
+ *
+ * |prefix| is optional; if present, it's the prefix (minus the '/') for
+ * actors in the new connection.
+ */
+function newConnection(prefix) {
+ let conn;
+ DevToolsServer.createRootActor = function (connection) {
+ conn = connection;
+ return new RootActor(connection, {});
+ };
+
+ const transport = DevToolsServer.connectPipe(prefix);
+
+ return { conn, transport };
+}
+
+/* Create the main connection for these tests. */
+function createMainConnection() {
+ ({ conn: gMainConnection, transport: gMainTransport } = newConnection());
+ gClient = new DevToolsClient(gMainTransport);
+ gClient.connect().then(([type, traits]) => run_next_test());
+}
+
+/*
+ * Exchange 'echo' messages with five actors:
+ * - root
+ * - prefix1/root
+ * - prefix1/actor
+ * - prefix2/root
+ * - prefix2/actor
+ *
+ * Expect proper echos from those named in |reachables|, and 'noSuchActor'
+ * errors from the others. When we've gotten all our replies (errors or
+ * otherwise), call |completed|.
+ *
+ * To avoid deep stacks, we call completed from the next tick.
+ */
+async function tryActors(reachables, completed) {
+ for (const actor of [
+ "root",
+ "prefix1/root",
+ "prefix1/actor",
+ "prefix2/root",
+ "prefix2/actor",
+ ]) {
+ let response;
+ try {
+ if (actor.endsWith("root")) {
+ // Root actor doesn't expose any echo method,
+ // so fallback on getRoot which returns `{ from: "root" }`.
+ // For the top level root actor, we have to use its front.
+ if (actor == "root") {
+ response = await gClient.mainRoot.getRoot();
+ } else {
+ response = await gClient.request({ to: actor, type: "getRoot" });
+ }
+ } else {
+ response = await gClient.request({
+ to: actor,
+ type: "echo",
+ value: "tango",
+ });
+ }
+ } catch (e) {
+ response = e;
+ }
+ if (reachables.has(actor)) {
+ if (actor.endsWith("root")) {
+ // RootActor's getRoot response is almost empty on xpcshell
+ Assert.deepEqual({ from: actor }, response);
+ } else {
+ Assert.deepEqual(
+ { from: actor, to: actor, type: "echo", value: "tango" },
+ response
+ );
+ }
+ } else {
+ Assert.deepEqual(
+ {
+ from: actor,
+ error: "noSuchActor",
+ message: "No such actor for ID: " + actor,
+ },
+ response
+ );
+ }
+ }
+ executeSoon(completed, "tryActors callback " + completed.name);
+}
+
+/*
+ * With no forwarding established, sending messages to root should work,
+ * but sending messages to prefixed actor names, or anyone else, should get
+ * an error.
+ */
+function TestNoForwardingYet() {
+ tryActors(new Set(["root"]), run_next_test);
+}
+
+/*
+ * Create a new pipe connection which forwards its reply packets to
+ * gMainConnection's client, and to which gMainConnection forwards packets
+ * directed to actors whose names begin with |prefix + '/'|, and.
+ *
+ * Return an object { conn, transport }, as for newConnection.
+ */
+function newSubconnection(prefix) {
+ const { conn, transport } = newConnection(prefix);
+ transport.hooks = {
+ onPacket: packet => gMainConnection.send(packet),
+ };
+ gMainConnection.setForwarding(prefix, transport);
+
+ return { conn, transport };
+}
+
+/* Create a second root actor, to which we can forward things. */
+function createSubconnection1() {
+ const { conn, transport } = newSubconnection("prefix1");
+ gSubconnection1 = conn;
+ transport.ready();
+ gClient.expectReply("prefix1/root", reply => run_next_test());
+}
+
+// Establish forwarding, but don't put any actors in that server.
+function TestForwardPrefix1OnlyRoot() {
+ tryActors(new Set(["root", "prefix1/root"]), run_next_test);
+}
+
+/* Create a third root actor, to which we can forward things. */
+function createSubconnection2() {
+ const { conn, transport } = newSubconnection("prefix2");
+ gSubconnection2 = conn;
+ transport.ready();
+ gClient.expectReply("prefix2/root", reply => run_next_test());
+}
+
+function TestForwardPrefix12OnlyRoot() {
+ tryActors(new Set(["root", "prefix1/root", "prefix2/root"]), run_next_test);
+}
+
+// A dumb actor that implements 'echo'.
+//
+// It's okay that both subconnections' actors behave identically, because
+// the reply-sending code attaches the replying actor's name to the packet,
+// so simply matching the 'from' field in the reply ensures that we heard
+// from the right actor.
+const { Actor } = require("resource://devtools/shared/protocol/Actor.js");
+class EchoActor extends Actor {
+ constructor(conn) {
+ super(conn, { typeName: "EchoActor", methods: [] });
+
+ this.requestTypes = {
+ echo: EchoActor.prototype.onEcho,
+ };
+ }
+
+ onEcho(request) {
+ /*
+ * Request packets are frozen. Copy request, so that
+ * DevToolsServerConnection.onPacket can attach a 'from' property.
+ */
+ return JSON.parse(JSON.stringify(request));
+ }
+}
+
+function TestForwardPrefix12WithActor1() {
+ const actor = new EchoActor(gSubconnection1);
+ actor.actorID = "prefix1/actor";
+ gSubconnection1.addActor(actor);
+
+ tryActors(
+ new Set(["root", "prefix1/root", "prefix1/actor", "prefix2/root"]),
+ run_next_test
+ );
+}
+
+function TestForwardPrefix12WithActor12() {
+ const actor = new EchoActor(gSubconnection2);
+ actor.actorID = "prefix2/actor";
+ gSubconnection2.addActor(actor);
+
+ tryActors(
+ new Set([
+ "root",
+ "prefix1/root",
+ "prefix1/actor",
+ "prefix2/root",
+ "prefix2/actor",
+ ]),
+ run_next_test
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_frameactor-01.js b/devtools/server/tests/xpcshell/test_frameactor-01.js
new file mode 100644
index 0000000000..18c75d0abe
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_frameactor-01.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Verify that we get a frame actor along with a debugger statement.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ Assert.ok(!!packet.frame);
+ Assert.ok(!!packet.frame.getActorByID);
+ Assert.equal(packet.frame.displayName, "stopMe");
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ debuggee.eval(
+ "(" +
+ function () {
+ function stopMe() {
+ debugger;
+ }
+ stopMe();
+ } +
+ ")()"
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_frameactor-02.js b/devtools/server/tests/xpcshell/test_frameactor-02.js
new file mode 100644
index 0000000000..9529d2f324
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_frameactor-02.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Verify that two pauses in a row will keep the same frame actor.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet1 = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const packet2 = await resumeAndWaitForPause(threadFront);
+
+ Assert.equal(packet1.frame.actor, packet2.frame.actor);
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ debuggee.eval(
+ "(" +
+ function () {
+ function stopMe() {
+ debugger;
+ debugger;
+ }
+ stopMe();
+ } +
+ ")()"
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_frameactor-03.js b/devtools/server/tests/xpcshell/test_frameactor-03.js
new file mode 100644
index 0000000000..7feecd14e0
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_frameactor-03.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Verify that a frame actor is properly expired when the frame goes away.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+ const frameActorID = packet.frame.actorID;
+ {
+ const { frames } = await threadFront.getFrames(0, null);
+ ok(
+ frames.some(f => f.actorID === frameActorID),
+ "The paused frame is returned by getFrames"
+ );
+
+ Assert.equal(frames.length, 3, "Thread front has 3 frames");
+ }
+
+ await resumeAndWaitForPause(threadFront);
+ await checkFramesLength(threadFront, 2);
+ {
+ const { frames } = await threadFront.getFrames(0, null);
+ ok(
+ !frames.some(f => f.actorID === frameActorID),
+ "The paused frame is no longer returned by getFrames"
+ );
+
+ Assert.equal(frames.length, 2, "Thread front has 2 frames");
+ }
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ debuggee.eval(
+ "(" +
+ function () {
+ function stopMe() {
+ debugger;
+ }
+ stopMe();
+ debugger;
+ } +
+ ")()"
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_frameactor-04.js b/devtools/server/tests/xpcshell/test_frameactor-04.js
new file mode 100644
index 0000000000..200ee9968d
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_frameactor-04.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Verify the "frames" request on the thread.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const response = await threadFront.getFrames(0, 1000);
+ for (let i = 0; i < response.frames.length; i++) {
+ const expected = frameFixtures[i];
+ const actual = response.frames[i];
+
+ Assert.equal(
+ expected.displayname,
+ actual.displayname,
+ "Frame displayname"
+ );
+ Assert.equal(expected.type, actual.type, "Frame displayname");
+ }
+
+ await threadFront.resume();
+ })
+);
+
+var frameFixtures = [
+ // Function calls...
+ { type: "call", displayName: "depth3" },
+ { type: "call", displayName: "depth2" },
+ { type: "call", displayName: "depth1" },
+
+ // Anonymous function call in our eval...
+ { type: "call", displayName: undefined },
+
+ // The eval itself.
+ { type: "eval", displayName: "(eval)" },
+];
+
+function evalCode(debuggee) {
+ debuggee.eval(
+ "(" +
+ function () {
+ function depth3() {
+ debugger;
+ }
+ function depth2() {
+ depth3();
+ }
+ function depth1() {
+ depth2();
+ }
+ depth1();
+ } +
+ ")()"
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_frameactor-05.js b/devtools/server/tests/xpcshell/test_frameactor-05.js
new file mode 100644
index 0000000000..90456191e7
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_frameactor-05.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+ await checkFramesLength(threadFront, 5);
+
+ await resumeAndWaitForPause(threadFront);
+ await checkFramesLength(threadFront, 2);
+
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ debuggee.eval(
+ "(" +
+ function () {
+ function depth3() {
+ debugger;
+ }
+ function depth2() {
+ depth3();
+ }
+ function depth1() {
+ depth2();
+ }
+ depth1();
+ debugger;
+ } +
+ ")()"
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_frameactor_wasm-01.js b/devtools/server/tests/xpcshell/test_frameactor_wasm-01.js
new file mode 100644
index 0000000000..5967e8a086
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_frameactor_wasm-01.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Verify that wasm frame(s) can be requested from the client.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ await threadFront.reconfigure({
+ observeAsmJS: true,
+ observeWasm: true,
+ });
+
+ await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const frameResponse = await threadFront.getFrames(0, null);
+
+ Assert.equal(frameResponse.frames.length, 4);
+
+ const wasmFrame = frameResponse.frames[1];
+ Assert.equal(wasmFrame.type, "wasmcall");
+ Assert.equal(wasmFrame.this, undefined);
+
+ const location = wasmFrame.where;
+ const source = await getSourceById(threadFront, location.actor);
+ Assert.equal(location.line > 0, true);
+ Assert.equal(location.column > 0, true);
+ Assert.equal(/^wasm:(?:[^:]*:)*?[0-9a-f]{16}$/.test(source.url), true);
+
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ /* eslint-disable comma-spacing, max-len */
+ debuggee.eval(
+ "(" +
+ function () {
+ // WebAssembly bytecode was generated by running:
+ // js -e 'print(wasmTextToBinary("(module(import \"a\" \"b\")(func(export \"c\")call 0))"))'
+ const m = new WebAssembly.Module(
+ new Uint8Array([
+ 0, 97, 115, 109, 1, 0, 0, 0, 1, 132, 128, 128, 128, 0, 1, 96, 0, 0,
+ 2, 135, 128, 128, 128, 0, 1, 1, 97, 1, 98, 0, 0, 3, 130, 128, 128,
+ 128, 0, 1, 0, 6, 129, 128, 128, 128, 0, 0, 7, 133, 128, 128, 128, 0,
+ 1, 1, 99, 0, 1, 10, 138, 128, 128, 128, 0, 1, 132, 128, 128, 128, 0,
+ 0, 16, 0, 11,
+ ])
+ );
+ const i = new WebAssembly.Instance(m, {
+ a: {
+ b: () => {
+ debugger;
+ },
+ },
+ });
+ i.exports.c();
+ } +
+ ")()"
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_framearguments-01.js b/devtools/server/tests/xpcshell/test_framearguments-01.js
new file mode 100644
index 0000000000..524d43f58c
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_framearguments-01.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check a frame actor's arguments property.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const args = packet.frame.arguments;
+ Assert.equal(args.length, 6);
+ Assert.equal(args[0], 42);
+ Assert.equal(args[1], true);
+ Assert.equal(args[2], "nasu");
+ Assert.equal(args[3].type, "null");
+ Assert.equal(args[4].type, "undefined");
+ Assert.equal(args[5].type, "object");
+ Assert.equal(args[5].class, "Object");
+ Assert.ok(!!args[5].actor);
+
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ debuggee.eval(
+ "(" +
+ function () {
+ function stopMe(number, bool, string, null_, undef, object) {
+ debugger;
+ }
+ stopMe(42, true, "nasu", null, undefined, { foo: "bar" });
+ } +
+ ")()"
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_framebindings-01.js b/devtools/server/tests/xpcshell/test_framebindings-01.js
new file mode 100644
index 0000000000..ecf6f02e97
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_framebindings-01.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check a frame actor's bindings property.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const environment = await packet.frame.getEnvironment();
+ const bindings = environment.bindings;
+ const args = bindings.arguments;
+ const vars = bindings.variables;
+
+ Assert.equal(args.length, 6);
+ Assert.equal(args[0].number.value, 42);
+ Assert.equal(args[1].bool.value, true);
+ Assert.equal(args[2].string.value, "nasu");
+ Assert.equal(args[3].null_.value.type, "null");
+ Assert.equal(args[4].undef.value.type, "undefined");
+ Assert.equal(args[5].object.value.type, "object");
+ Assert.equal(args[5].object.value.class, "Object");
+ Assert.ok(!!args[5].object.value.actor);
+
+ Assert.equal(vars.a.value, 1);
+ Assert.equal(vars.b.value, true);
+ Assert.equal(vars.c.value.type, "object");
+ Assert.equal(vars.c.value.class, "Object");
+ Assert.ok(!!vars.c.value.actor);
+
+ const objClient = threadFront.pauseGrip(vars.c.value);
+ const response = await objClient.getPrototypeAndProperties();
+ Assert.equal(response.ownProperties.a.configurable, true);
+ Assert.equal(response.ownProperties.a.enumerable, true);
+ Assert.equal(response.ownProperties.a.writable, true);
+ Assert.equal(response.ownProperties.a.value, "a");
+
+ Assert.equal(response.ownProperties.b.configurable, true);
+ Assert.equal(response.ownProperties.b.enumerable, true);
+ Assert.equal(response.ownProperties.b.writable, true);
+ Assert.equal(response.ownProperties.b.value.type, "undefined");
+ Assert.equal(false, "class" in response.ownProperties.b.value);
+
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ /* eslint-disable */
+ debuggee.eval(
+ "(" +
+ function () {
+ function stopMe(number, bool, string, null_, undef, object) {
+ var a = 1;
+ var b = true;
+ var c = { a: "a", b: undefined };
+ debugger;
+ }
+ stopMe(42, true, "nasu", null, undefined, { foo: "bar" });
+ } +
+ ")()"
+ );
+ /* eslint-enable */
+}
diff --git a/devtools/server/tests/xpcshell/test_framebindings-02.js b/devtools/server/tests/xpcshell/test_framebindings-02.js
new file mode 100644
index 0000000000..48c243193b
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_framebindings-02.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check a frame actor's parent bindings.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const environment = await packet.frame.getEnvironment();
+ let parentEnv = environment.parent;
+ const bindings = parentEnv.bindings;
+ const args = bindings.arguments;
+ const vars = bindings.variables;
+ Assert.notEqual(parentEnv, undefined);
+ Assert.equal(args.length, 0);
+ Assert.equal(vars.stopMe.value.type, "object");
+ Assert.equal(vars.stopMe.value.class, "Function");
+ Assert.ok(!!vars.stopMe.value.actor);
+
+ // Skip the global lexical scope.
+ parentEnv = parentEnv.parent.parent;
+ Assert.notEqual(parentEnv, undefined);
+ const objClient = threadFront.pauseGrip(parentEnv.object);
+ const response = await objClient.getPrototypeAndProperties();
+ Assert.equal(response.ownProperties.Object.value.getGrip().type, "object");
+ Assert.equal(
+ response.ownProperties.Object.value.getGrip().class,
+ "Function"
+ );
+ Assert.ok(!!response.ownProperties.Object.value.actorID);
+
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ /* eslint-disable */
+ debuggee.eval(
+ "(" +
+ function () {
+ function stopMe(number, bool, string, null_, undef, object) {
+ var a = 1;
+ var b = true;
+ var c = { a: "a" };
+ eval("");
+ debugger;
+ }
+ stopMe(42, true, "nasu", null, undefined, { foo: "bar" });
+ } +
+ ")()"
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_framebindings-03.js b/devtools/server/tests/xpcshell/test_framebindings-03.js
new file mode 100644
index 0000000000..46dc777ef1
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_framebindings-03.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* strict mode code may not contain 'with' statements */
+/* eslint-disable strict */
+
+/**
+ * Check a |with| frame actor's bindings.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const env = await packet.frame.getEnvironment();
+ Assert.notEqual(env, undefined);
+
+ const parentEnv = env.parent;
+ Assert.notEqual(parentEnv, undefined);
+
+ const bindings = parentEnv.bindings;
+ const args = bindings.arguments;
+ const vars = bindings.variables;
+ Assert.equal(args.length, 1);
+ Assert.equal(args[0].number.value, 10);
+ Assert.equal(vars.r.value, 10);
+ Assert.equal(vars.a.value, Math.PI * 100);
+ Assert.equal(vars.arguments.value.class, "Arguments");
+ Assert.ok(!!vars.arguments.value.actor);
+
+ const objClient = threadFront.pauseGrip(env.object);
+ const response = await objClient.getPrototypeAndProperties();
+ Assert.equal(response.ownProperties.PI.value, Math.PI);
+ Assert.equal(response.ownProperties.cos.value.getGrip().type, "object");
+ Assert.equal(response.ownProperties.cos.value.getGrip().class, "Function");
+ Assert.ok(!!response.ownProperties.cos.value.actorID);
+
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ /* eslint-disable */
+ debuggee.eval(
+ "(" +
+ function () {
+ function stopMe(number) {
+ var a;
+ var r = number;
+ with (Math) {
+ a = PI * r * r;
+ debugger;
+ }
+ }
+ stopMe(10);
+ } +
+ ")()"
+ );
+ /* eslint-enable */
+}
diff --git a/devtools/server/tests/xpcshell/test_framebindings-04.js b/devtools/server/tests/xpcshell/test_framebindings-04.js
new file mode 100644
index 0000000000..1e3cc1485c
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_framebindings-04.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* strict mode code may not contain 'with' statements */
+/* eslint-disable strict */
+
+/**
+ * Check the environment bindings of a |with| within a |with|.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const env = await packet.frame.getEnvironment();
+ Assert.notEqual(env, undefined);
+
+ const objClient = threadFront.pauseGrip(env.object);
+ let response = await objClient.getPrototypeAndProperties();
+ Assert.equal(response.ownProperties.one.value, 1);
+ Assert.equal(response.ownProperties.two.value, 2);
+ Assert.equal(response.ownProperties.foo, undefined);
+
+ let parentEnv = env.parent;
+ Assert.notEqual(parentEnv, undefined);
+
+ const parentClient = threadFront.pauseGrip(parentEnv.object);
+ response = await parentClient.getPrototypeAndProperties();
+ Assert.equal(response.ownProperties.PI.value, Math.PI);
+ Assert.equal(response.ownProperties.cos.value.getGrip().type, "object");
+ Assert.equal(response.ownProperties.cos.value.getGrip().class, "Function");
+ Assert.ok(!!response.ownProperties.cos.value.actorID);
+
+ parentEnv = parentEnv.parent;
+ Assert.notEqual(parentEnv, undefined);
+
+ const bindings = parentEnv.bindings;
+ const args = bindings.arguments;
+ const vars = bindings.variables;
+ Assert.equal(args.length, 1);
+ Assert.equal(args[0].number.value, 10);
+ Assert.equal(vars.r.value, 10);
+ Assert.equal(vars.a.value, Math.PI * 100);
+ Assert.equal(vars.arguments.value.class, "Arguments");
+ Assert.ok(!!vars.arguments.value.actor);
+ Assert.equal(vars.foo.value, 2 * Math.PI);
+
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ /* eslint-disable */
+ debuggee.eval(
+ "(" +
+ function () {
+ function stopMe(number) {
+ var a,
+ obj = { one: 1, two: 2 };
+ var r = number;
+ with (Math) {
+ a = PI * r * r;
+ with (obj) {
+ var foo = two * PI;
+ debugger;
+ }
+ }
+ }
+ stopMe(10);
+ } +
+ ")()"
+ );
+ /* eslint-enable */
+}
diff --git a/devtools/server/tests/xpcshell/test_framebindings-05.js b/devtools/server/tests/xpcshell/test_framebindings-05.js
new file mode 100644
index 0000000000..6206fe8668
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_framebindings-05.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check the environment bindings of a |with| in global scope.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const env = await packet.frame.getEnvironment();
+ Assert.notEqual(env, undefined);
+
+ const objClient = threadFront.pauseGrip(env.object);
+ let response = await objClient.getPrototypeAndProperties();
+ Assert.equal(response.ownProperties.PI.value, Math.PI);
+ Assert.equal(response.ownProperties.cos.value.getGrip().type, "object");
+ Assert.equal(response.ownProperties.cos.value.getGrip().class, "Function");
+ Assert.ok(!!response.ownProperties.cos.value.actorID);
+
+ // Skip the global lexical scope.
+ const parentEnv = env.parent.parent;
+ Assert.notEqual(parentEnv, undefined);
+
+ const parentClient = threadFront.pauseGrip(parentEnv.object);
+ response = await parentClient.getPrototypeAndProperties();
+ Assert.equal(response.ownProperties.a.value, Math.PI * 100);
+ Assert.equal(response.ownProperties.r.value, 10);
+ Assert.equal(response.ownProperties.Object.value.getGrip().type, "object");
+ Assert.equal(
+ response.ownProperties.Object.value.getGrip().class,
+ "Function"
+ );
+ Assert.ok(!!response.ownProperties.Object.value.actorID);
+
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ debuggee.eval(
+ "var a, r = 10;\n" +
+ "with (Math) {\n" +
+ " a = PI * r * r;\n" +
+ " debugger;\n" +
+ "}"
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_framebindings-06.js b/devtools/server/tests/xpcshell/test_framebindings-06.js
new file mode 100644
index 0000000000..52ab0cfe7c
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_framebindings-06.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const env = await packet.frame.getEnvironment();
+ equal(env.type, "function");
+ equal(env.function.displayName, "banana3");
+ let parent = env.parent;
+ equal(parent.type, "block");
+ ok("banana3" in parent.bindings.variables);
+ parent = parent.parent;
+ equal(parent.type, "function");
+ equal(parent.function.displayName, "banana2");
+ parent = parent.parent;
+ equal(parent.type, "block");
+ ok("banana2" in parent.bindings.variables);
+ parent = parent.parent;
+ equal(parent.type, "function");
+ equal(parent.function.displayName, "banana");
+
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ debuggee.eval(
+ "function banana(x) {\n" +
+ " return function banana2(y) {\n" +
+ " return function banana3(z) {\n" +
+ ' eval("");\n' +
+ " debugger;\n" +
+ " };\n" +
+ " };\n" +
+ "}\n" +
+ "banana('x')('y')('z');\n"
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_framebindings-07.js b/devtools/server/tests/xpcshell/test_framebindings-07.js
new file mode 100644
index 0000000000..77d43dfba8
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_framebindings-07.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee, client }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const environment = await packet.frame.getEnvironment();
+ Assert.equal(environment.type, "function");
+ Assert.equal(environment.bindings.arguments[0].z.value, "z");
+
+ const parent = environment.parent;
+ Assert.equal(parent.type, "block");
+ Assert.equal(parent.bindings.variables.banana3.value.class, "Function");
+
+ const grandpa = parent.parent;
+ Assert.equal(grandpa.type, "function");
+ Assert.equal(grandpa.bindings.arguments[0].y.value, "y");
+
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ debuggee.eval(
+ "function banana(x) {\n" +
+ " return function banana2(y) {\n" +
+ " return function banana3(z) {\n" +
+ ' eval("");\n' +
+ " debugger;\n" +
+ " };\n" +
+ " };\n" +
+ "}\n" +
+ "banana('x')('y')('z');\n"
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_front_destroy.js b/devtools/server/tests/xpcshell/test_front_destroy.js
new file mode 100644
index 0000000000..33e2ac827a
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_front_destroy.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that fronts throw errors if they are called after being destroyed.
+ */
+
+"use strict";
+
+// HACK: ServiceWorkerManager requires the "profile-change-teardown" to cleanly
+// shutdown, and setting _profileInitialized to `true` will trigger those
+// notifications (see /testing/xpcshell/head.js).
+// eslint-disable-next-line no-undef
+_profileInitialized = true;
+
+add_task(async function test() {
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+
+ info("Create and connect the DevToolsClient");
+ const transport = DevToolsServer.connectPipe();
+ const client = new DevToolsClient(transport);
+ await client.connect();
+
+ info("Get the device front and check calling getDescription() on it");
+ const front = await client.mainRoot.getFront("device");
+ const description = await front.getDescription();
+ ok(
+ !!description,
+ "Check that the getDescription() method returns a valid response."
+ );
+
+ info("Destroy the device front and try calling getDescription again");
+ front.destroy();
+ Assert.throws(
+ () => front.getDescription(),
+ /Can not send request 'getDescription' because front 'device' is already destroyed\./,
+ "Check device front throws when getDescription() is called after destroy()"
+ );
+
+ await client.close();
+});
diff --git a/devtools/server/tests/xpcshell/test_functiongrips-01.js b/devtools/server/tests/xpcshell/test_functiongrips-01.js
new file mode 100644
index 0000000000..5abce26875
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_functiongrips-01.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ // Test named function
+ function evalCode() {
+ debuggee.eval(
+ function stopMe(arg1) {
+ debugger;
+ }.toString()
+ );
+ debuggee.eval("stopMe(stopMe)");
+ }
+
+ const packet1 = await executeOnNextTickAndWaitForPause(
+ () => evalCode(),
+ threadFront
+ );
+
+ const args1 = packet1.frame.arguments;
+
+ Assert.equal(args1[0].class, "Function");
+ Assert.equal(args1[0].name, "stopMe");
+ Assert.equal(args1[0].displayName, "stopMe");
+
+ await threadFront.resume();
+
+ // Test inferred name function
+ const packet2 = await executeOnNextTickAndWaitForPause(
+ () =>
+ debuggee.eval(
+ "var o = { m: function(foo, bar, baz) { } }; stopMe(o.m)"
+ ),
+ threadFront
+ );
+
+ const args2 = packet2.frame.arguments;
+
+ Assert.equal(args2[0].class, "Function");
+ // No name for an anonymous function, but it should have an inferred name.
+ Assert.equal(args2[0].name, undefined);
+ Assert.equal(args2[0].displayName, "m");
+
+ await threadFront.resume();
+
+ // Test anonymous function
+ const packet3 = await executeOnNextTickAndWaitForPause(
+ () => debuggee.eval("stopMe(function(foo, bar, baz) { })"),
+ threadFront
+ );
+
+ const args3 = packet3.frame.arguments;
+
+ Assert.equal(args3[0].class, "Function");
+ // No name for an anonymous function, and no inferred name, either.
+ Assert.equal(args3[0].name, undefined);
+ Assert.equal(args3[0].displayName, undefined);
+
+ await threadFront.resume();
+ })
+);
diff --git a/devtools/server/tests/xpcshell/test_getRuleText.js b/devtools/server/tests/xpcshell/test_getRuleText.js
new file mode 100644
index 0000000000..fe53dca158
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_getRuleText.js
@@ -0,0 +1,143 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ getRuleText,
+} = require("resource://devtools/server/actors/utils/style-utils.js");
+
+const TEST_DATA = [
+ {
+ desc: "Empty input",
+ input: "",
+ line: 1,
+ column: 1,
+ throws: true,
+ },
+ {
+ desc: "Simplest test case",
+ input: "#id{color:red;background:yellow;}",
+ line: 1,
+ column: 1,
+ expected: { offset: 4, text: "color:red;background:yellow;" },
+ },
+ {
+ desc: "Multiple rules test case",
+ input:
+ "#id{color:red;background:yellow;}.class-one .class-two " +
+ "{ position:absolute; line-height: 45px}",
+ line: 1,
+ column: 34,
+ expected: { offset: 56, text: " position:absolute; line-height: 45px" },
+ },
+ {
+ desc: "Unclosed rule",
+ input: "#id{color:red;background:yellow;",
+ line: 1,
+ column: 1,
+ expected: { offset: 4, text: "color:red;background:yellow;" },
+ },
+ {
+ desc: "Null input",
+ input: null,
+ line: 1,
+ column: 1,
+ throws: true,
+ },
+ {
+ desc: "Missing loc",
+ input: "#id{color:red;background:yellow;}",
+ throws: true,
+ },
+ {
+ desc: "Multi-lines CSS",
+ input: [
+ "/* this is a multi line css */",
+ "body {",
+ " color: green;",
+ " background-repeat: no-repeat",
+ "}",
+ " /*something else here */",
+ "* {",
+ " color: purple;",
+ "}",
+ ].join("\n"),
+ line: 7,
+ column: 1,
+ expected: { offset: 116, text: "\n color: purple;\n" },
+ },
+ {
+ desc: "Multi-lines CSS and multi-line rule",
+ input: [
+ "/* ",
+ "* some comments",
+ "*/",
+ "",
+ "body {",
+ " margin: 0;",
+ " padding: 15px 15px 2px 15px;",
+ " color: red;",
+ "}",
+ "",
+ "#header .btn, #header .txt {",
+ " font-size: 100%;",
+ "}",
+ "",
+ "#header #information {",
+ " color: #dddddd;",
+ " font-size: small;",
+ "}",
+ ].join("\n"),
+ line: 5,
+ column: 1,
+ expected: {
+ offset: 30,
+ text: "\n margin: 0;\n padding: 15px 15px 2px 15px;\n color: red;\n",
+ },
+ },
+ {
+ desc: "Content string containing a } character",
+ input: " #id{border:1px solid red;content: '}';color:red;}",
+ line: 1,
+ column: 4,
+ expected: {
+ offset: 7,
+ text: "border:1px solid red;content: '}';color:red;",
+ },
+ },
+ {
+ desc: "Rule contains no tokens",
+ input: "div{}",
+ line: 1,
+ column: 1,
+ expected: { offset: 4, text: "" },
+ },
+];
+
+function run_test() {
+ for (const test of TEST_DATA) {
+ info("Starting test: " + test.desc);
+ info("Input string " + test.input);
+ let output;
+ try {
+ output = getRuleText(test.input, test.line, test.column);
+ if (test.throws) {
+ info("Test should have thrown");
+ Assert.ok(false);
+ }
+ } catch (e) {
+ info("getRuleText threw an exception with the given input string");
+ if (test.throws) {
+ info("Exception expected");
+ Assert.ok(true);
+ } else {
+ info("Exception unexpected\n" + e);
+ Assert.ok(false);
+ }
+ }
+ if (output) {
+ deepEqual(output, test.expected);
+ }
+ }
+}
diff --git a/devtools/server/tests/xpcshell/test_getTextAtLineColumn.js b/devtools/server/tests/xpcshell/test_getTextAtLineColumn.js
new file mode 100644
index 0000000000..3aa9915192
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_getTextAtLineColumn.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ getTextAtLineColumn,
+} = require("resource://devtools/server/actors/utils/style-utils.js");
+
+const TEST_DATA = [
+ {
+ desc: "simplest",
+ input: "#id{color:red;background:yellow;}",
+ line: 1,
+ column: 5,
+ expected: { offset: 4, text: "color:red;background:yellow;}" },
+ },
+ {
+ desc: "multiple lines",
+ input: "one\n two\n three",
+ line: 3,
+ column: 3,
+ expected: { offset: 11, text: "three" },
+ },
+];
+
+function run_test() {
+ for (const test of TEST_DATA) {
+ info("Starting test: " + test.desc);
+ info("Input string " + test.input);
+
+ const output = getTextAtLineColumn(test.input, test.line, test.column);
+ deepEqual(output, test.expected);
+ }
+}
diff --git a/devtools/server/tests/xpcshell/test_get_command_and_arg.js b/devtools/server/tests/xpcshell/test_get_command_and_arg.js
new file mode 100644
index 0000000000..b3f0ab8ec4
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_get_command_and_arg.js
@@ -0,0 +1,121 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ getCommandAndArgs,
+} = require("resource://devtools/server/actors/webconsole/commands/parser.js");
+
+const testcases = [
+ { input: ":help", expectedOutput: "help()" },
+ {
+ input: ":screenshot --fullscreen",
+ expectedOutput: 'screenshot({"fullscreen":true})',
+ },
+ {
+ input: ":screenshot --fullscreen true",
+ expectedOutput: 'screenshot({"fullscreen":true})',
+ },
+ { input: ":screenshot ", expectedOutput: "screenshot()" },
+ {
+ input: ":screenshot --dpr 0.5 --fullpage --chrome",
+ expectedOutput: 'screenshot({"dpr":0.5,"fullpage":true,"chrome":true})',
+ },
+ {
+ input: ":screenshot 'filename'",
+ expectedOutput: 'screenshot({"filename":"filename"})',
+ },
+ {
+ input: ":screenshot filename",
+ expectedOutput: 'screenshot({"filename":"filename"})',
+ },
+ {
+ input:
+ ":screenshot --name 'filename' --name `filename` --name \"filename\"",
+ expectedOutput: 'screenshot({"name":["filename","filename","filename"]})',
+ },
+ {
+ input: ":screenshot 'filename1' 'filename2' 'filename3'",
+ expectedOutput: 'screenshot({"filename":"filename1"})',
+ },
+ {
+ input: ":screenshot --chrome --chrome",
+ expectedOutput: 'screenshot({"chrome":true})',
+ },
+ {
+ input: ':screenshot "file name with spaces"',
+ expectedOutput: 'screenshot({"filename":"file name with spaces"})',
+ },
+ {
+ input: ":screenshot 'filename1' --name 'filename2'",
+ expectedOutput: 'screenshot({"filename":"filename1","name":"filename2"})',
+ },
+ {
+ input: ":screenshot --name 'filename1' 'filename2'",
+ expectedOutput: 'screenshot({"name":"filename1","filename":"filename2"})',
+ },
+ {
+ input: ':screenshot "fo\\"o bar"',
+ expectedOutput: 'screenshot({"filename":"fo\\\\\\"o bar"})',
+ },
+ {
+ input: ':screenshot "foo b\\"ar"',
+ expectedOutput: 'screenshot({"filename":"foo b\\\\\\"ar"})',
+ },
+];
+
+const edgecases = [
+ { input: ":", expectedError: /Missing a command name after ':'/ },
+ { input: ":invalid", expectedError: /'invalid' is not a valid command/ },
+ {
+ input: ":screenshot :help",
+ expectedError:
+ /Executing multiple commands in one evaluation is not supported/,
+ },
+ { input: ":screenshot --", expectedError: /invalid flag/ },
+ {
+ input: ':screenshot "fo"o bar',
+ expectedError:
+ /String has unescaped `"` in \["fo"o\.\.\.\], may miss a space between arguments/,
+ },
+ {
+ input: ':screenshot "foo b"ar',
+ expectedError:
+ // eslint-disable-next-line max-len
+ /String has unescaped `"` in \["foo b"ar\.\.\.\], may miss a space between arguments/,
+ },
+ { input: ": screenshot", expectedError: /Missing a command name after ':'/ },
+ {
+ input: ':screenshot "file name',
+ expectedError: /String does not terminate/,
+ },
+ {
+ input: ':screenshot "file name --clipboard',
+ expectedError: /String does not terminate before flag "clipboard"/,
+ },
+ {
+ input: "::screenshot",
+ expectedError: /':screenshot' is not a valid command/,
+ },
+];
+
+function formatArgs(args) {
+ return Object.keys(args).length ? JSON.stringify(args) : "";
+}
+
+function run_test() {
+ testcases.forEach(testcase => {
+ const { command, args } = getCommandAndArgs(testcase.input);
+ const argsString = formatArgs(args);
+ Assert.equal(`${command}(${argsString})`, testcase.expectedOutput);
+ });
+
+ edgecases.forEach(testcase => {
+ Assert.throws(
+ () => getCommandAndArgs(testcase.input),
+ testcase.expectedError,
+ `"${testcase.input}" should throw expected error`
+ );
+ });
+}
diff --git a/devtools/server/tests/xpcshell/test_getyoungestframe.js b/devtools/server/tests/xpcshell/test_getyoungestframe.js
new file mode 100644
index 0000000000..f08628b7ed
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_getyoungestframe.js
@@ -0,0 +1,38 @@
+/* eslint-disable strict */
+function run_test() {
+ Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+ });
+ const { addDebuggerToGlobal } = ChromeUtils.importESModule(
+ "resource://gre/modules/jsdebugger.sys.mjs"
+ );
+ addDebuggerToGlobal(globalThis);
+ const xpcInspector = Cc["@mozilla.org/jsinspector;1"].getService(
+ Ci.nsIJSInspector
+ );
+ const g = createTestGlobal("test1");
+
+ const dbg = makeDebugger();
+ dbg.uncaughtExceptionHook = testExceptionHook;
+
+ dbg.addDebuggee(g);
+ dbg.onDebuggerStatement = function (frame) {
+ Assert.ok(frame === dbg.getNewestFrame());
+ // Execute from the nested event loop, dbg.getNewestFrame() won't
+ // be working anymore.
+
+ executeSoon(function () {
+ try {
+ Assert.ok(frame === dbg.getNewestFrame());
+ } finally {
+ xpcInspector.exitNestedEventLoop("test");
+ }
+ });
+ xpcInspector.enterNestedEventLoop("test");
+ };
+
+ g.eval("function debuggerStatement() { debugger; }; debuggerStatement();");
+
+ dbg.disable();
+}
diff --git a/devtools/server/tests/xpcshell/test_ignore_caught_exceptions.js b/devtools/server/tests/xpcshell/test_ignore_caught_exceptions.js
new file mode 100644
index 0000000000..fe04161aab
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_ignore_caught_exceptions.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that setting ignoreCaughtExceptions will cause the debugger to ignore
+ * caught exceptions, but not uncaught ones.
+ */
+
+add_task(
+ threadFrontTest(
+ async ({ threadFront, debuggee, commands }) => {
+ await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+
+ await commands.threadConfigurationCommand.updateConfiguration({
+ pauseOnExceptions: true,
+ ignoreCaughtExceptions: true,
+ });
+ await resume(threadFront);
+ const paused = await waitForPause(threadFront);
+ Assert.equal(paused.why.type, "exception");
+ equal(paused.frame.where.line, 6, "paused at throw");
+
+ await resume(threadFront);
+ },
+ {
+ // Bug 1508289, exception tests fails in worker scope
+ doNotRunWorker: true,
+ }
+ )
+);
+
+function evaluateTestCode(debuggee) {
+ // prettier-ignore
+ try {
+ Cu.evalInSandbox(` // 1
+ debugger; // 2
+ try { // 3
+ throw "foo"; // 4
+ } catch (e) {} // 5
+ throw "bar"; // 6
+ `, // 7
+ debuggee,
+ "1.8",
+ "test_pause_exceptions-03.js",
+ 1
+ );
+ } catch (e) {}
+}
diff --git a/devtools/server/tests/xpcshell/test_ignore_no_interface_exceptions.js b/devtools/server/tests/xpcshell/test_ignore_no_interface_exceptions.js
new file mode 100644
index 0000000000..50d28ffdc0
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_ignore_no_interface_exceptions.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that the debugger automatically ignores NS_ERROR_NO_INTERFACE
+ * exceptions, but not normal ones.
+ */
+
+add_task(
+ threadFrontTest(
+ async ({ threadFront, debuggee }) => {
+ await threadFront.pauseOnExceptions(true, false);
+ const paused = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+ equal(paused.frame.where.line, 6, "paused at throw");
+
+ await resume(threadFront);
+ },
+ {
+ // Bug 1508289, exception tests fails in worker scope
+ doNotRunWorker: true,
+ }
+ )
+);
+
+function evaluateTestCode(debuggee) {
+ // prettier-ignore
+ Cu.evalInSandbox(` // 1
+ function QueryInterface() { // 2
+ throw Cr.NS_ERROR_NO_INTERFACE; // 3
+ } // 4
+ function stopMe() { // 5
+ throw 42; // 6
+ } // 7
+ try { // 8
+ QueryInterface(); // 9
+ } catch (e) {} // 10
+ try { // 11
+ stopMe(); // 12
+ } catch (e) {}`, // 13
+ debuggee,
+ "1.8",
+ "test_ignore_no_interface_exceptions.js",
+ 1
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_interrupt.js b/devtools/server/tests/xpcshell/test_interrupt.js
new file mode 100644
index 0000000000..07593a7360
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_interrupt.js
@@ -0,0 +1,15 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee, client, targetFront }) => {
+ const onPaused = waitForEvent(threadFront, "paused");
+ await threadFront.interrupt();
+ await onPaused;
+ Assert.equal(threadFront.paused, true);
+ await threadFront.resume();
+ Assert.equal(threadFront.paused, false);
+ })
+);
diff --git a/devtools/server/tests/xpcshell/test_layout-reflows-observer.js b/devtools/server/tests/xpcshell/test_layout-reflows-observer.js
new file mode 100644
index 0000000000..74f31b97fe
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_layout-reflows-observer.js
@@ -0,0 +1,311 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the LayoutChangesObserver
+
+/* eslint-disable mozilla/use-chromeutils-generateqi */
+
+var {
+ getLayoutChangesObserver,
+ releaseLayoutChangesObserver,
+ LayoutChangesObserver,
+} = require("resource://devtools/server/actors/reflow.js");
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+// Override set/clearTimeout on LayoutChangesObserver to avoid depending on
+// time in this unit test. This means that LayoutChangesObserver.eventLoopTimer
+// will be the timeout callback instead of the timeout itself, so test cases
+// will need to execute it to fake a timeout
+LayoutChangesObserver.prototype._setTimeout = cb => cb;
+LayoutChangesObserver.prototype._clearTimeout = function () {};
+
+// Mock the targetActor since we only really want to test the LayoutChangesObserver
+// and don't want to depend on a window object, nor want to test protocol.js
+class MockTargetActor extends EventEmitter {
+ constructor() {
+ super();
+ this.docShell = new MockDocShell();
+ this.window = new MockWindow(this.docShell);
+ this.windows = [this.window];
+ this.attached = true;
+ }
+
+ get chromeEventHandler() {
+ return this.docShell.chromeEventHandler;
+ }
+
+ isDestroyed() {
+ return false;
+ }
+}
+
+function MockWindow(docShell) {
+ this.docShell = docShell;
+}
+MockWindow.prototype = {
+ QueryInterface() {
+ const self = this;
+ return {
+ getInterface() {
+ return {
+ QueryInterface() {
+ return self.docShell;
+ },
+ };
+ },
+ };
+ },
+ setTimeout(cb) {
+ // Simply return the cb itself so that we can execute it in the test instead
+ // of depending on a real timeout
+ return cb;
+ },
+ clearTimeout() {},
+};
+
+function MockDocShell() {
+ this.observer = null;
+}
+MockDocShell.prototype = {
+ addWeakReflowObserver(observer) {
+ this.observer = observer;
+ },
+ removeWeakReflowObserver() {},
+ get chromeEventHandler() {
+ return {
+ addEventListener: (type, cb) => {
+ if (type === "resize") {
+ this.resizeCb = cb;
+ }
+ },
+ removeEventListener: (type, cb) => {
+ if (type === "resize" && cb === this.resizeCb) {
+ this.resizeCb = null;
+ }
+ },
+ };
+ },
+ mockResize() {
+ if (this.resizeCb) {
+ this.resizeCb();
+ }
+ },
+};
+
+function run_test() {
+ instancesOfObserversAreSharedBetweenWindows();
+ eventsAreBatched();
+ noEventsAreSentWhenThereAreNoReflowsAndLoopTimeouts();
+ observerIsAlreadyStarted();
+ destroyStopsObserving();
+ stoppingAndStartingSeveralTimesWorksCorrectly();
+ reflowsArentStackedWhenStopped();
+ stackedReflowsAreResetOnStop();
+}
+
+function instancesOfObserversAreSharedBetweenWindows() {
+ info(
+ "Checking that when requesting twice an instances of the observer " +
+ "for the same WindowGlobalTargetActor, the instance is shared"
+ );
+
+ info("Checking 2 instances of the observer for the targetActor 1");
+ const targetActor1 = new MockTargetActor();
+ const obs11 = getLayoutChangesObserver(targetActor1);
+ const obs12 = getLayoutChangesObserver(targetActor1);
+ Assert.equal(obs11, obs12);
+
+ info("Checking 2 instances of the observer for the targetActor 2");
+ const targetActor2 = new MockTargetActor();
+ const obs21 = getLayoutChangesObserver(targetActor2);
+ const obs22 = getLayoutChangesObserver(targetActor2);
+ Assert.equal(obs21, obs22);
+
+ info(
+ "Checking that observers instances for 2 different targetActors are " +
+ "different"
+ );
+ Assert.notEqual(obs11, obs21);
+
+ releaseLayoutChangesObserver(targetActor1);
+ releaseLayoutChangesObserver(targetActor1);
+ releaseLayoutChangesObserver(targetActor2);
+ releaseLayoutChangesObserver(targetActor2);
+}
+
+function eventsAreBatched() {
+ info(
+ "Checking that reflow events are batched and only sent when the " +
+ "timeout expires"
+ );
+
+ // Note that in this test, we mock the target actor and its window property, so we also
+ // mock the setTimeout/clearTimeout mechanism and just call the callback manually
+ const targetActor = new MockTargetActor();
+ const observer = getLayoutChangesObserver(targetActor);
+
+ const reflowsEvents = [];
+ const onReflows = reflows => reflowsEvents.push(reflows);
+ observer.on("reflows", onReflows);
+
+ const resizeEvents = [];
+ const onResize = () => resizeEvents.push("resize");
+ observer.on("resize", onResize);
+
+ info("Fake one reflow event");
+ targetActor.window.docShell.observer.reflow();
+ info("Checking that no batched reflow event has been emitted");
+ Assert.equal(reflowsEvents.length, 0);
+
+ info("Fake another reflow event");
+ targetActor.window.docShell.observer.reflow();
+ info("Checking that still no batched reflow event has been emitted");
+ Assert.equal(reflowsEvents.length, 0);
+
+ info("Fake a few of resize events too");
+ targetActor.window.docShell.mockResize();
+ targetActor.window.docShell.mockResize();
+ targetActor.window.docShell.mockResize();
+ info("Checking that still no batched resize event has been emitted");
+ Assert.equal(resizeEvents.length, 0);
+
+ info("Faking timeout expiration and checking that events are sent");
+ observer.eventLoopTimer();
+ Assert.equal(reflowsEvents.length, 1);
+ Assert.equal(reflowsEvents[0].length, 2);
+ Assert.equal(resizeEvents.length, 1);
+
+ observer.off("reflows", onReflows);
+ observer.off("resize", onResize);
+ releaseLayoutChangesObserver(targetActor);
+}
+
+function noEventsAreSentWhenThereAreNoReflowsAndLoopTimeouts() {
+ info(
+ "Checking that if no reflows were detected and the event batching " +
+ "loop expires, then no reflows event is sent"
+ );
+
+ const targetActor = new MockTargetActor();
+ const observer = getLayoutChangesObserver(targetActor);
+
+ const reflowsEvents = [];
+ const onReflows = reflows => reflowsEvents.push(reflows);
+ observer.on("reflows", onReflows);
+
+ info("Faking timeout expiration and checking for reflows");
+ observer.eventLoopTimer();
+ Assert.equal(reflowsEvents.length, 0);
+
+ observer.off("reflows", onReflows);
+ releaseLayoutChangesObserver(targetActor);
+}
+
+function observerIsAlreadyStarted() {
+ info("Checking that the observer is already started when getting it");
+
+ const targetActor = new MockTargetActor();
+ const observer = getLayoutChangesObserver(targetActor);
+ Assert.ok(observer.isObserving);
+
+ observer.stop();
+ Assert.ok(!observer.isObserving);
+
+ observer.start();
+ Assert.ok(observer.isObserving);
+
+ releaseLayoutChangesObserver(targetActor);
+}
+
+function destroyStopsObserving() {
+ info("Checking that the destroying the observer stops it");
+
+ const targetActor = new MockTargetActor();
+ const observer = getLayoutChangesObserver(targetActor);
+ Assert.ok(observer.isObserving);
+
+ observer.destroy();
+ Assert.ok(!observer.isObserving);
+
+ releaseLayoutChangesObserver(targetActor);
+}
+
+function stoppingAndStartingSeveralTimesWorksCorrectly() {
+ info(
+ "Checking that the stopping and starting several times the observer" +
+ " works correctly"
+ );
+
+ const targetActor = new MockTargetActor();
+ const observer = getLayoutChangesObserver(targetActor);
+
+ Assert.ok(observer.isObserving);
+ observer.start();
+ observer.start();
+ observer.start();
+ Assert.ok(observer.isObserving);
+
+ observer.stop();
+ Assert.ok(!observer.isObserving);
+
+ observer.stop();
+ observer.stop();
+ Assert.ok(!observer.isObserving);
+
+ releaseLayoutChangesObserver(targetActor);
+}
+
+function reflowsArentStackedWhenStopped() {
+ info("Checking that when stopped, reflows aren't stacked in the observer");
+
+ const targetActor = new MockTargetActor();
+ const observer = getLayoutChangesObserver(targetActor);
+
+ info("Stoping the observer");
+ observer.stop();
+
+ info("Faking reflows");
+ targetActor.window.docShell.observer.reflow();
+ targetActor.window.docShell.observer.reflow();
+ targetActor.window.docShell.observer.reflow();
+
+ info("Checking that reflows aren't recorded");
+ Assert.equal(observer.reflows.length, 0);
+
+ info("Starting the observer and faking more reflows");
+ observer.start();
+ targetActor.window.docShell.observer.reflow();
+ targetActor.window.docShell.observer.reflow();
+ targetActor.window.docShell.observer.reflow();
+
+ info("Checking that reflows are recorded");
+ Assert.equal(observer.reflows.length, 3);
+
+ releaseLayoutChangesObserver(targetActor);
+}
+
+function stackedReflowsAreResetOnStop() {
+ info("Checking that stacked reflows are reset on stop");
+
+ const targetActor = new MockTargetActor();
+ const observer = getLayoutChangesObserver(targetActor);
+
+ targetActor.window.docShell.observer.reflow();
+ Assert.equal(observer.reflows.length, 1);
+
+ observer.stop();
+ Assert.equal(observer.reflows.length, 0);
+
+ targetActor.window.docShell.observer.reflow();
+ Assert.equal(observer.reflows.length, 0);
+
+ observer.start();
+ Assert.equal(observer.reflows.length, 0);
+
+ targetActor.window.docShell.observer.reflow();
+ Assert.equal(observer.reflows.length, 1);
+
+ releaseLayoutChangesObserver(targetActor);
+}
diff --git a/devtools/server/tests/xpcshell/test_listsources-01.js b/devtools/server/tests/xpcshell/test_listsources-01.js
new file mode 100644
index 0000000000..306825278c
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_listsources-01.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check basic getSources functionality.
+ */
+
+var gNumTimesSourcesSent = 0;
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee, client }) => {
+ client.request = (function (origRequest) {
+ return function (request, onResponse) {
+ if (request.type === "sources") {
+ ++gNumTimesSourcesSent;
+ }
+ return origRequest.call(this, request, onResponse);
+ };
+ })(client.request);
+
+ await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const response = await threadFront.getSources();
+
+ Assert.ok(
+ response.sources.some(function (s) {
+ return s.url && s.url.match(/test_listsources-01.js/);
+ })
+ );
+
+ Assert.ok(
+ gNumTimesSourcesSent <= 1,
+ "Should only send one sources request at most, even though we" +
+ " might have had to send one to determine feature support."
+ );
+
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ /* eslint-disable */
+ Cu.evalInSandbox(
+ "var line0 = Error().lineNumber;\n" +
+ "debugger;\n" + // line0 + 1
+ "var a = 1;\n" + // line0 + 2
+ "var b = 2;\n", // line0 + 3
+ debuggee
+ );
+ /* eslint-enable */
+}
diff --git a/devtools/server/tests/xpcshell/test_listsources-02.js b/devtools/server/tests/xpcshell/test_listsources-02.js
new file mode 100644
index 0000000000..a2f9cc3bda
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_listsources-02.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check getting sources before there are any.
+ */
+
+var gNumTimesSourcesSent = 0;
+
+add_task(
+ threadFrontTest(async ({ threadFront, client }) => {
+ client.request = (function (origRequest) {
+ return function (request, onResponse) {
+ if (request.type === "sources") {
+ ++gNumTimesSourcesSent;
+ }
+ return origRequest.call(this, request, onResponse);
+ };
+ })(client.request);
+
+ // Test listing zero sources
+ const packet = await threadFront.getSources();
+
+ Assert.ok(!packet.error);
+ Assert.ok(!!packet.sources);
+ Assert.equal(packet.sources.length, 0);
+
+ Assert.ok(
+ gNumTimesSourcesSent <= 1,
+ "Should only send one sources request at most, even though we" +
+ " might have had to send one to determine feature support."
+ );
+ })
+);
diff --git a/devtools/server/tests/xpcshell/test_listsources-03.js b/devtools/server/tests/xpcshell/test_listsources-03.js
new file mode 100644
index 0000000000..f8af5aca6e
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_listsources-03.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check getSources functionality when there are lots of sources.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const response = await threadFront.getSources();
+
+ Assert.ok(
+ !response.error,
+ "There shouldn't be an error fetching large amounts of sources."
+ );
+
+ Assert.ok(
+ response.sources.some(function (s) {
+ return s.url.match(/foo-999.js$/);
+ })
+ );
+
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ for (let i = 0; i < 1000; i++) {
+ Cu.evalInSandbox(
+ "function foo###() {return ###;}".replace(/###/g, i),
+ debuggee,
+ "1.8",
+ "http://example.com/foo-" + i + ".js",
+ 1
+ );
+ }
+ debuggee.eval("debugger;");
+}
diff --git a/devtools/server/tests/xpcshell/test_logpoint-01.js b/devtools/server/tests/xpcshell/test_logpoint-01.js
new file mode 100644
index 0000000000..a5cb4f2197
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_logpoint-01.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that logpoints generate console messages.
+ */
+
+const Resources = require("resource://devtools/server/actors/resources/index.js");
+
+add_task(
+ threadFrontTest(async ({ threadActor, threadFront, debuggee, client }) => {
+ let lastMessage, lastExpression;
+ const targetActor = threadActor._parent;
+ // Only Workers are evaluating through the WebConsoleActor.
+ // Tabs will be evaluating directly via the frame object.
+ targetActor._consoleActor = {
+ evaluateJS(expression) {
+ lastExpression = expression;
+ },
+ };
+
+ // And then listen for resource RDP event.
+ // Bug 1646677: But we should probably migrate this test to ResourceCommand so that
+ // we don't have to hack the server side via Resource.watchResources call.
+ targetActor.on("resource-available-form", resources => {
+ if (resources[0].resourceType == Resources.TYPES.CONSOLE_MESSAGE) {
+ lastMessage = resources[0].message;
+ }
+ });
+
+ // But both tabs and processes will be going through the ConsoleMessages module
+ // We force watching for console message first,
+ await Resources.watchResources(targetActor, [
+ Resources.TYPES.CONSOLE_MESSAGE,
+ ]);
+
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const source = await getSourceById(threadFront, packet.frame.where.actor);
+
+ // Set a logpoint which should invoke console.log.
+ threadFront.setBreakpoint(
+ {
+ sourceUrl: source.url,
+ line: 3,
+ },
+ { logValue: "a" }
+ );
+ await client.waitForRequestsToSettle();
+
+ // Execute the rest of the code.
+ await threadFront.resume();
+
+ // NOTE: logpoints evaluated in a worker have a lastExpression
+ if (lastMessage) {
+ Assert.equal(lastMessage.level, "logPoint");
+ Assert.equal(lastMessage.arguments[0], "three");
+ Assert.ok(/\d+\.\d+/.test(lastMessage.timeStamp));
+ } else {
+ Assert.equal(lastExpression.text, "console.log(...[a])");
+ Assert.equal(lastExpression.lineNumber, 3);
+ }
+ })
+);
+
+function evalCode(debuggee) {
+ /* eslint-disable */
+ Cu.evalInSandbox(
+ "debugger;\n" + // 1
+ "var a = 'three';\n" + // 2
+ "var b = 2;\n", // 3
+ debuggee,
+ "1.8",
+ "test.js",
+ 1
+ );
+ /* eslint-enable */
+}
diff --git a/devtools/server/tests/xpcshell/test_logpoint-02.js b/devtools/server/tests/xpcshell/test_logpoint-02.js
new file mode 100644
index 0000000000..d84d3fc324
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_logpoint-02.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that conditions are respected when specified in a logpoint.
+ */
+
+const Resources = require("resource://devtools/server/actors/resources/index.js");
+
+add_task(
+ threadFrontTest(async ({ threadActor, threadFront, debuggee, client }) => {
+ let lastMessage, lastExpression;
+ const targetActor = threadActor._parent;
+ // Only Workers are evaluating through the WebConsoleActor.
+ // Tabs will be evaluating directly via the frame object.
+ targetActor._consoleActor = {
+ evaluateJS(expression) {
+ lastExpression = expression;
+ },
+ };
+
+ // And then listen for resource RDP event.
+ // Bug 1646677: But we should probably migrate this test to ResourceCommand so that
+ // we don't have to hack the server side via Resource.watchResources call.
+ targetActor.on("resource-available-form", resources => {
+ if (resources[0].resourceType == Resources.TYPES.CONSOLE_MESSAGE) {
+ lastMessage = resources[0].message;
+ }
+ });
+
+ // But both tabs and processes will be going through the ConsoleMessages module
+ // We force watching for console message first,
+ await Resources.watchResources(targetActor, [
+ Resources.TYPES.CONSOLE_MESSAGE,
+ ]);
+
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const source = await getSourceById(threadFront, packet.frame.where.actor);
+
+ // Set a logpoint which should invoke console.log.
+ threadFront.setBreakpoint(
+ {
+ sourceUrl: source.url,
+ line: 4,
+ },
+ { logValue: "a", condition: "a === 5" }
+ );
+ await client.waitForRequestsToSettle();
+
+ // Execute the rest of the code.
+ await threadFront.resume();
+
+ // NOTE: logpoints evaluated in a worker have a lastExpression
+ if (lastMessage) {
+ Assert.equal(lastMessage.level, "logPoint");
+ Assert.equal(lastMessage.arguments[0], 5);
+ Assert.ok(/\d+\.\d+/.test(lastMessage.timeStamp));
+ } else {
+ Assert.equal(lastExpression.text, "console.log(...[a])");
+ Assert.equal(lastExpression.lineNumber, 4);
+ }
+ })
+);
+
+function evalCode(debuggee) {
+ /* eslint-disable */
+ Cu.evalInSandbox(
+ "debugger;\n" + // 1
+ "var a = 1;\n" + // 2
+ "while (a < 10) {\n" + // 3
+ " a++;\n" + // 4
+ "}",
+ debuggee,
+ "1.8",
+ "test.js",
+ 1
+ );
+ /* eslint-enable */
+}
diff --git a/devtools/server/tests/xpcshell/test_logpoint-03.js b/devtools/server/tests/xpcshell/test_logpoint-03.js
new file mode 100644
index 0000000000..b5d4440889
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_logpoint-03.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that logpoints generate console errors if the logpoint statement is invalid.
+ */
+
+const Resources = require("resource://devtools/server/actors/resources/index.js");
+
+add_task(
+ threadFrontTest(async ({ threadActor, threadFront, debuggee, client }) => {
+ let lastMessage, lastExpression;
+ const targetActor = threadActor._parent;
+ // Only Workers are evaluating through the WebConsoleActor.
+ // Tabs will be evaluating directly via the frame object.
+ targetActor._consoleActor = {
+ evaluateJS(expression) {
+ lastExpression = expression;
+ },
+ };
+
+ // And then listen for resource RDP event.
+ // Bug 1646677: But we should probably migrate this test to ResourceCommand so that
+ // we don't have to hack the server side via Resource.watchResources call.
+ targetActor.on("resource-available-form", resources => {
+ if (resources[0].resourceType == Resources.TYPES.CONSOLE_MESSAGE) {
+ lastMessage = resources[0].message;
+ }
+ });
+
+ // But both tabs and processes will be going through the ConsoleMessages module
+ // We force watching for console message first,
+ await Resources.watchResources(targetActor, [
+ Resources.TYPES.CONSOLE_MESSAGE,
+ ]);
+
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const source = await getSourceById(threadFront, packet.frame.where.actor);
+
+ // Set a logpoint which should throw an error message.
+ await threadFront.setBreakpoint(
+ {
+ sourceUrl: source.url,
+ line: 3,
+ },
+ { logValue: "c" }
+ );
+
+ // Execute the rest of the code.
+ await threadFront.resume();
+
+ // NOTE: logpoints evaluated in a worker have a lastExpression
+ if (lastMessage) {
+ Assert.equal(lastMessage.level, "logPointError");
+ Assert.equal(lastMessage.arguments[0], "c is not defined");
+ Assert.ok(/\d+\.\d+/.test(lastMessage.timeStamp));
+ } else {
+ Assert.equal(lastExpression.text, "console.log(...[c])");
+ Assert.equal(lastExpression.lineNumber, 3);
+ }
+ })
+);
+
+function evalCode(debuggee) {
+ /* eslint-disable */
+ Cu.evalInSandbox(
+ "debugger;\n" + // 1
+ "var a = 'three';\n" + // 2
+ "var b = 2;\n", // 3
+ debuggee,
+ "1.8",
+ "test.js",
+ 1
+ );
+ /* eslint-enable */
+}
diff --git a/devtools/server/tests/xpcshell/test_longstringgrips-01.js b/devtools/server/tests/xpcshell/test_longstringgrips-01.js
new file mode 100644
index 0000000000..ac0b228c17
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_longstringgrips-01.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var gDebuggee;
+var gClient;
+var gThreadFront;
+
+add_task(
+ threadFrontTest(
+ async ({ threadFront, debuggee, client }) => {
+ gThreadFront = threadFront;
+ gDebuggee = debuggee;
+ gClient = client;
+ test_longstring_grip();
+ },
+ { waitForFinish: true }
+ )
+);
+
+function test_longstring_grip() {
+ const longString =
+ "All I want is to be a monkey of moderate intelligence who" +
+ " wears a suit... that's why I'm transferring to business school! Maybe I" +
+ " love you so much, I love you no matter who you are pretending to be." +
+ " Enough about your promiscuous mother, Hermes! We have bigger problems." +
+ " For example, if you killed your grandfather, you'd cease to exist! What" +
+ " kind of a father would I be if I said no? Yep, I remember. They came in" +
+ " last at the Olympics, then retired to promote alcoholic beverages! And" +
+ " remember, don't do anything that affects anything, unless it turns out" +
+ " you were supposed to, in which case, for the love of God, don't not do" +
+ " it!";
+
+ DevToolsServer.LONG_STRING_LENGTH = 200;
+
+ gThreadFront.once("paused", function (packet) {
+ const args = packet.frame.arguments;
+ Assert.equal(args.length, 1);
+ const grip = args[0];
+
+ try {
+ Assert.equal(grip.type, "longString");
+ Assert.equal(grip.length, longString.length);
+ Assert.equal(
+ grip.initial,
+ longString.substr(0, DevToolsServer.LONG_STRING_INITIAL_LENGTH)
+ );
+
+ const longStringFront = createLongStringFront(gClient, grip);
+ longStringFront.substring(22, 28).then(function (response) {
+ try {
+ Assert.equal(response, "monkey");
+ } finally {
+ gThreadFront.resume().then(function () {
+ finishClient(gClient);
+ });
+ }
+ });
+ } catch (error) {
+ gThreadFront.resume().then(function () {
+ finishClient(gClient);
+ do_throw(error);
+ });
+ }
+ });
+
+ gDebuggee.eval(
+ function stopMe(arg1) {
+ debugger;
+ }.toString()
+ );
+
+ gDebuggee.eval('stopMe("' + longString + '")');
+}
diff --git a/devtools/server/tests/xpcshell/test_nativewrappers.js b/devtools/server/tests/xpcshell/test_nativewrappers.js
new file mode 100644
index 0000000000..170a2a1e6e
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_nativewrappers.js
@@ -0,0 +1,39 @@
+/* eslint-disable strict */
+function run_test() {
+ Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+ });
+ const { addDebuggerToGlobal } = ChromeUtils.importESModule(
+ "resource://gre/modules/jsdebugger.sys.mjs"
+ );
+ addDebuggerToGlobal(globalThis);
+ const g = createTestGlobal("test1");
+
+ const dbg = makeDebugger();
+ dbg.addDebuggee(g);
+ dbg.onDebuggerStatement = function (frame) {
+ const args = frame.arguments;
+ try {
+ args[0];
+ Assert.ok(true);
+ } catch (ex) {
+ Assert.ok(false);
+ }
+ };
+
+ g.eval("function stopMe(arg) {debugger;}");
+
+ const g2 = createTestGlobal("test2");
+ g2.g = g;
+ // Not using the "stringify a function" trick because that runs afoul of the
+ // Cu.importGlobalProperties lint and we don't need it here anyway.
+ g2.eval(`(function createBadEvent() {
+ Cu.importGlobalProperties(["DOMParser"]);
+ let parser = new DOMParser();
+ let doc = parser.parseFromString("<foo></foo>", "text/xml");
+ g.stopMe(doc.createEvent("MouseEvent"));
+ } )()`);
+
+ dbg.disable();
+}
diff --git a/devtools/server/tests/xpcshell/test_nesting-03.js b/devtools/server/tests/xpcshell/test_nesting-03.js
new file mode 100644
index 0000000000..0a64e751cd
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_nesting-03.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that we can detect nested event loops in tabs with the same URL.
+
+add_task(async function () {
+ const GLOBAL_NAME = "test-nesting1";
+
+ initTestDevToolsServer();
+ addTestGlobal(GLOBAL_NAME);
+ addTestGlobal(GLOBAL_NAME);
+
+ // Connect two thread actors, debugging the same debuggee, and both being paused.
+ const firstClient = new DevToolsClient(DevToolsServer.connectPipe());
+ await firstClient.connect();
+ const { threadFront: firstThreadFront } = await attachTestThread(
+ firstClient,
+ GLOBAL_NAME
+ );
+ await firstThreadFront.interrupt();
+
+ const secondClient = new DevToolsClient(DevToolsServer.connectPipe());
+ await secondClient.connect();
+ const { threadFront: secondThreadFront } = await attachTestThread(
+ secondClient,
+ GLOBAL_NAME
+ );
+ await secondThreadFront.interrupt();
+
+ // Then check how concurrent resume work
+ let result;
+ try {
+ result = await firstThreadFront.resume();
+ } catch (e) {
+ Assert.ok(e.includes("wrongOrder"), "rejects with the wrong order");
+ }
+ Assert.ok(!result, "no response");
+
+ result = await secondThreadFront.resume();
+ Assert.ok(true, "resumed as expected");
+
+ await firstThreadFront.resume();
+
+ Assert.ok(true, "resumed as expected");
+ await firstClient.close();
+
+ await finishClient(secondClient);
+});
diff --git a/devtools/server/tests/xpcshell/test_nesting-04.js b/devtools/server/tests/xpcshell/test_nesting-04.js
new file mode 100644
index 0000000000..dcee257c40
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_nesting-04.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Verify that we never pause while being already paused.
+ * i.e. we don't support more than one nested event loops.
+ */
+
+add_task(
+ threadFrontTest(async ({ commands, threadFront, debuggee }) => {
+ await threadFront.setBreakpoint({ sourceUrl: "nesting-04.js", line: 2 });
+
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ Assert.equal(packet.frame.where.line, 5);
+ Assert.equal(packet.why.type, "debuggerStatement");
+
+ info("Test calling interrupt");
+ const onPaused = waitForPause(threadFront);
+ await threadFront.interrupt();
+ // interrupt() doesn't return anything, but bailout while emitting a paused packet
+ // But we don't pause again, the reason prove it so
+ const paused = await onPaused;
+ equal(paused.why.type, "alreadyPaused");
+
+ info("Test by evaluating code via the console");
+ const { result } = await commands.scriptCommand.execute(
+ "debugger; functionWithDebuggerStatement()",
+ {
+ frameActor: packet.frame.actorID,
+ }
+ );
+ // The fact that it returned immediately means that we did not pause
+ equal(result, 42);
+
+ info("Test by calling code from chrome context");
+ // This should be equivalent to any actor somehow triggering some page's JS
+ const rv = debuggee.functionWithDebuggerStatement();
+ // The fact that it returned immediately means that we did not pause
+ equal(rv, 42);
+
+ info("Test by stepping over a function that breaks");
+ // This will only step over the debugger; statement we just break on
+ const step1 = await stepOver(threadFront);
+ equal(step1.why.type, "resumeLimit");
+ equal(step1.frame.where.line, 6);
+
+ // stepOver will actually resume and re-pause on the breakpoint
+ const step2 = await stepOver(threadFront);
+ equal(step2.why.type, "breakpoint");
+ equal(step2.frame.where.line, 2);
+
+ // Sanity check to ensure that the functionWithDebuggerStatement really pauses
+ info("Resume and pause on the breakpoint");
+ const pausedPacket = await resumeAndWaitForPause(threadFront);
+ Assert.equal(pausedPacket.frame.where.line, 2);
+ // The breakpoint takes over the debugger statement
+ Assert.equal(pausedPacket.why.type, "breakpoint");
+
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ /* eslint-disable */
+ Cu.evalInSandbox(
+ `function functionWithDebuggerStatement() {
+ debugger;
+ return 42;
+ }
+ debugger;
+ functionWithDebuggerStatement();
+ var a = 1;
+ functionWithDebuggerStatement();`,
+ debuggee,
+ "1.8",
+ "nesting-04.js",
+ 1
+ );
+ /* eslint-enable */
+}
diff --git a/devtools/server/tests/xpcshell/test_new_source-01.js b/devtools/server/tests/xpcshell/test_new_source-01.js
new file mode 100644
index 0000000000..929865baa8
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_new_source-01.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check basic newSource packet sent from server.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ Cu.evalInSandbox(
+ function inc(n) {
+ return n + 1;
+ }.toString(),
+ debuggee
+ );
+
+ const sourcePacket = await waitForEvent(threadFront, "newSource");
+
+ Assert.ok(!!sourcePacket.source);
+ Assert.ok(!!sourcePacket.source.url.match(/test_new_source-01.js$/));
+ })
+);
diff --git a/devtools/server/tests/xpcshell/test_new_source-02.js b/devtools/server/tests/xpcshell/test_new_source-02.js
new file mode 100644
index 0000000000..15259b884a
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_new_source-02.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that sourceURL has the correct effect when using threadFront.eval.
+ */
+
+add_task(
+ threadFrontTest(async ({ commands, threadFront, debuggee }) => {
+ await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const packet1 = await waitForEvent(threadFront, "newSource");
+
+ Assert.ok(!!packet1.source);
+ Assert.ok(packet1.source.introductionType, "eval");
+
+ commands.scriptCommand.execute(
+ "function f() { }\n//# sourceURL=http://example.com/code.js"
+ );
+
+ const packet2 = await waitForEvent(threadFront, "newSource");
+ dump(JSON.stringify(packet2, null, 2));
+ Assert.ok(!!packet2.source);
+ Assert.ok(!!packet2.source.url.match(/example\.com/));
+ })
+);
+
+function evalCode(debuggee) {
+ /* eslint-disable */
+ debuggee.eval(
+ "(" +
+ function () {
+ function stopMe(arg1) {
+ debugger;
+ }
+ stopMe({ obj: true });
+ } +
+ ")()"
+ );
+ /* eslint-enable */
+}
diff --git a/devtools/server/tests/xpcshell/test_nodelistactor.js b/devtools/server/tests/xpcshell/test_nodelistactor.js
new file mode 100644
index 0000000000..eab6bb07e8
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_nodelistactor.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that a NodeListActor initialized with null nodelist doesn't cause
+// exceptions when calling NodeListActor.form.
+
+const {
+ NodeListActor,
+} = require("resource://devtools/server/actors/inspector/node.js");
+
+function run_test() {
+ check_actor_for_list(null);
+ check_actor_for_list([]);
+ check_actor_for_list(["fakenode"]);
+}
+
+function check_actor_for_list(nodelist) {
+ info("Checking NodeListActor with nodelist '" + nodelist + "' works.");
+ const actor = new NodeListActor({}, nodelist);
+ const form = actor.form();
+
+ // No exception occured as a exceptions abort the test.
+ ok(true, "No exceptions occured.");
+ equal(
+ form.length,
+ nodelist ? nodelist.length : 0,
+ "NodeListActor reported correct length."
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_objectgrips-02.js b/devtools/server/tests/xpcshell/test_objectgrips-02.js
new file mode 100644
index 0000000000..810a5009c0
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_objectgrips-02.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const args = packet.frame.arguments;
+
+ Assert.equal(args[0].class, "Object");
+
+ const objectFront = threadFront.pauseGrip(args[0]);
+ const response = await objectFront.getPrototype();
+ Assert.ok(response.prototype != undefined);
+
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ debuggee.eval(
+ function stopMe(arg1) {
+ debugger;
+ }.toString()
+ );
+ debuggee.eval(
+ function Constr() {
+ this.a = 1;
+ }.toString()
+ );
+ debuggee.eval(
+ "Constr.prototype = { b: true, c: 'foo' }; var o = new Constr(); stopMe(o)"
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_objectgrips-03.js b/devtools/server/tests/xpcshell/test_objectgrips-03.js
new file mode 100644
index 0000000000..c8a51d41d3
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_objectgrips-03.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+ const args = packet.frame.arguments;
+
+ Assert.equal(args[0].class, "Object");
+
+ const objClient = threadFront.pauseGrip(args[0]);
+ let response = await objClient.getProperty("x");
+ Assert.equal(response.descriptor.configurable, true);
+ Assert.equal(response.descriptor.enumerable, true);
+ Assert.equal(response.descriptor.writable, true);
+ Assert.equal(response.descriptor.value, 10);
+
+ response = await objClient.getProperty("y");
+ Assert.equal(response.descriptor.configurable, true);
+ Assert.equal(response.descriptor.enumerable, true);
+ Assert.equal(response.descriptor.writable, true);
+ Assert.equal(response.descriptor.value, "kaiju");
+
+ response = await objClient.getProperty("a");
+ Assert.equal(response.descriptor.configurable, true);
+ Assert.equal(response.descriptor.enumerable, true);
+ Assert.equal(response.descriptor.get.type, "object");
+ Assert.equal(response.descriptor.get.class, "Function");
+ Assert.equal(response.descriptor.set.type, "undefined");
+
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ debuggee.eval(
+ function stopMe(arg1) {
+ debugger;
+ }.toString()
+ );
+ debuggee.eval("stopMe({ x: 10, y: 'kaiju', get a() { return 42; } })");
+}
diff --git a/devtools/server/tests/xpcshell/test_objectgrips-04.js b/devtools/server/tests/xpcshell/test_objectgrips-04.js
new file mode 100644
index 0000000000..d08705db3c
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_objectgrips-04.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const args = packet.frame.arguments;
+
+ Assert.equal(args[0].class, "Object");
+
+ const objectFront = threadFront.pauseGrip(args[0]);
+ const { ownProperties, prototype } =
+ await objectFront.getPrototypeAndProperties();
+ Assert.equal(ownProperties.x.configurable, true);
+ Assert.equal(ownProperties.x.enumerable, true);
+ Assert.equal(ownProperties.x.writable, true);
+ Assert.equal(ownProperties.x.value, 10);
+
+ Assert.equal(ownProperties.y.configurable, true);
+ Assert.equal(ownProperties.y.enumerable, true);
+ Assert.equal(ownProperties.y.writable, true);
+ Assert.equal(ownProperties.y.value, "kaiju");
+
+ Assert.equal(ownProperties.a.configurable, true);
+ Assert.equal(ownProperties.a.enumerable, true);
+ Assert.equal(ownProperties.a.get.getGrip().type, "object");
+ Assert.equal(ownProperties.a.get.getGrip().class, "Function");
+ Assert.equal(ownProperties.a.set.type, "undefined");
+
+ Assert.ok(prototype != undefined);
+
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ debuggee.eval(
+ function stopMe(arg1) {
+ debugger;
+ }.toString()
+ );
+ debuggee.eval("stopMe({ x: 10, y: 'kaiju', get a() { return 42; } })");
+}
diff --git a/devtools/server/tests/xpcshell/test_objectgrips-05.js b/devtools/server/tests/xpcshell/test_objectgrips-05.js
new file mode 100644
index 0000000000..4c6f0f107a
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_objectgrips-05.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test checks that frozen objects report themselves as frozen in their
+ * grip.
+ */
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const obj1 = packet.frame.arguments[0];
+ Assert.ok(obj1.frozen);
+
+ const obj1Client = threadFront.pauseGrip(obj1);
+ Assert.ok(obj1Client.isFrozen);
+
+ const obj2 = packet.frame.arguments[1];
+ Assert.ok(!obj2.frozen);
+
+ const obj2Client = threadFront.pauseGrip(obj2);
+ Assert.ok(!obj2Client.isFrozen);
+
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ debuggee.eval(
+ function stopMe(arg1) {
+ debugger;
+ }.toString()
+ );
+ /* eslint-disable no-undef */
+ debuggee.eval(
+ "(" +
+ function () {
+ const obj1 = {};
+ Object.freeze(obj1);
+ stopMe(obj1, {});
+ } +
+ "())"
+ );
+ /* eslint-enable no-undef */
+}
diff --git a/devtools/server/tests/xpcshell/test_objectgrips-06.js b/devtools/server/tests/xpcshell/test_objectgrips-06.js
new file mode 100644
index 0000000000..ef3d2b5b66
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_objectgrips-06.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test checks that sealed objects report themselves as sealed in their
+ * grip.
+ */
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const obj1 = packet.frame.arguments[0];
+ Assert.ok(obj1.sealed);
+
+ const obj1Client = threadFront.pauseGrip(obj1);
+ Assert.ok(obj1Client.isSealed);
+
+ const obj2 = packet.frame.arguments[1];
+ Assert.ok(!obj2.sealed);
+
+ const obj2Client = threadFront.pauseGrip(obj2);
+ Assert.ok(!obj2Client.isSealed);
+
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ debuggee.eval(
+ function stopMe(arg1) {
+ debugger;
+ }.toString()
+ );
+ /* eslint-disable no-undef */
+ debuggee.eval(
+ "(" +
+ function () {
+ const obj1 = {};
+ Object.seal(obj1);
+ stopMe(obj1, {});
+ } +
+ "())"
+ );
+ /* eslint-enable no-undef */
+}
diff --git a/devtools/server/tests/xpcshell/test_objectgrips-07.js b/devtools/server/tests/xpcshell/test_objectgrips-07.js
new file mode 100644
index 0000000000..2a3a0bf00e
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_objectgrips-07.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test checks that objects which are not extensible report themselves as
+ * such.
+ */
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const [f, s, ne, e] = packet.frame.arguments;
+ const [fClient, sClient, neClient, eClient] = packet.frame.arguments.map(
+ a => threadFront.pauseGrip(a)
+ );
+
+ Assert.ok(!f.extensible);
+ Assert.ok(!fClient.isExtensible);
+
+ Assert.ok(!s.extensible);
+ Assert.ok(!sClient.isExtensible);
+
+ Assert.ok(!ne.extensible);
+ Assert.ok(!neClient.isExtensible);
+
+ Assert.ok(e.extensible);
+ Assert.ok(eClient.isExtensible);
+
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ debuggee.eval(
+ function stopMe(arg1) {
+ debugger;
+ }.toString()
+ );
+ /* eslint-disable no-undef */
+ debuggee.eval(
+ "(" +
+ function () {
+ const f = {};
+ Object.freeze(f);
+ const s = {};
+ Object.seal(s);
+ const ne = {};
+ Object.preventExtensions(ne);
+ stopMe(f, s, ne, {});
+ } +
+ "())"
+ );
+ /* eslint-enable no-undef */
+}
diff --git a/devtools/server/tests/xpcshell/test_objectgrips-08.js b/devtools/server/tests/xpcshell/test_objectgrips-08.js
new file mode 100644
index 0000000000..1a37f19fb8
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_objectgrips-08.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const args = packet.frame.arguments;
+
+ Assert.equal(args[0].class, "Object");
+
+ const objClient = threadFront.pauseGrip(args[0]);
+ const response = await objClient.getPrototypeAndProperties();
+ const { a, b, c, d, e, f, g } = response.ownProperties;
+ testPropertyType(a, "Infinity");
+ testPropertyType(b, "-Infinity");
+ testPropertyType(c, "NaN");
+ testPropertyType(d, "-0");
+ testPropertyType(e, "BigInt");
+ testPropertyType(f, "BigInt");
+ testPropertyType(g, "BigInt");
+
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ debuggee.eval(
+ function stopMe(arg1) {
+ debugger;
+ }.toString()
+ );
+ debuggee.eval(
+ `stopMe({
+ a: Infinity,
+ b: -Infinity,
+ c: NaN,
+ d: -0,
+ e: 1n,
+ f: -2n,
+ g: 0n,
+ })`
+ );
+}
+
+function testPropertyType(prop, expectedType) {
+ Assert.equal(prop.configurable, true);
+ Assert.equal(prop.enumerable, true);
+ Assert.equal(prop.writable, true);
+ Assert.equal(prop.value.type, expectedType);
+}
diff --git a/devtools/server/tests/xpcshell/test_objectgrips-14.js b/devtools/server/tests/xpcshell/test_objectgrips-14.js
new file mode 100644
index 0000000000..cff8611e7d
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_objectgrips-14.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test out of scope objects with synchronous functions.
+ */
+
+var gDebuggee;
+var gThreadFront;
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ gThreadFront = threadFront;
+ gDebuggee = debuggee;
+ await testObjectGroup();
+ })
+);
+
+function evalCode() {
+ evalCallback(gDebuggee, function runTest() {
+ const ugh = [];
+ let i = 0;
+
+ (function () {
+ (function () {
+ ugh.push(i++);
+ debugger;
+ })();
+ })();
+
+ debugger;
+ });
+}
+
+const testObjectGroup = async function () {
+ let packet = await executeOnNextTickAndWaitForPause(evalCode, gThreadFront);
+
+ const environment = await packet.frame.getEnvironment();
+ const ugh = environment.parent.parent.bindings.variables.ugh;
+ const ughClient = await gThreadFront.pauseGrip(ugh.value);
+
+ packet = await getPrototypeAndProperties(ughClient);
+ packet = await resumeAndWaitForPause(gThreadFront);
+
+ const environment2 = await packet.frame.getEnvironment();
+ const ugh2 = environment2.bindings.variables.ugh;
+ const ugh2Client = gThreadFront.pauseGrip(ugh2.value);
+
+ packet = await getPrototypeAndProperties(ugh2Client);
+ Assert.equal(packet.ownProperties.length.value, 1);
+
+ await resume(gThreadFront);
+};
diff --git a/devtools/server/tests/xpcshell/test_objectgrips-15.js b/devtools/server/tests/xpcshell/test_objectgrips-15.js
new file mode 100644
index 0000000000..3a7aba89c8
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_objectgrips-15.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test out of scope objects with async functions.
+ */
+
+var gDebuggee;
+var gThreadFront;
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ gThreadFront = threadFront;
+ gDebuggee = debuggee;
+ await testObjectGroup();
+ })
+);
+
+function evalCode() {
+ evalCallback(gDebuggee, function runTest() {
+ const ugh = [];
+ let i = 0;
+
+ function foo() {
+ ugh.push(i++);
+ debugger;
+ }
+
+ Promise.resolve().then(foo).then(foo);
+ });
+}
+
+const testObjectGroup = async function () {
+ let packet = await executeOnNextTickAndWaitForPause(evalCode, gThreadFront);
+
+ const environment = await packet.frame.getEnvironment();
+ const ugh = environment.parent.bindings.variables.ugh;
+ const ughClient = await gThreadFront.pauseGrip(ugh.value);
+
+ packet = await getPrototypeAndProperties(ughClient);
+
+ packet = await resumeAndWaitForPause(gThreadFront);
+ const environment2 = await packet.frame.getEnvironment();
+ const ugh2 = environment2.parent.bindings.variables.ugh;
+ const ugh2Client = gThreadFront.pauseGrip(ugh2.value);
+
+ packet = await getPrototypeAndProperties(ugh2Client);
+ Assert.equal(packet.ownProperties.length.value, 2);
+
+ await resume(gThreadFront);
+};
diff --git a/devtools/server/tests/xpcshell/test_objectgrips-16.js b/devtools/server/tests/xpcshell/test_objectgrips-16.js
new file mode 100644
index 0000000000..785c3bc36d
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_objectgrips-16.js
@@ -0,0 +1,139 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-shadow, max-nested-callbacks */
+
+"use strict";
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ eval_code,
+ threadFront
+ );
+ const [grip] = packet.frame.arguments;
+
+ // Checks grip.preview properties.
+ check_preview(grip);
+
+ const objClient = threadFront.pauseGrip(grip);
+ const response = await objClient.getPrototypeAndProperties();
+ // Checks the result of getPrototypeAndProperties.
+ check_prototype_and_properties(response);
+
+ await threadFront.resume();
+
+ function eval_code() {
+ debuggee.eval(
+ function stopMe(arg1) {
+ debugger;
+ }.toString()
+ );
+ debuggee.eval(`
+ stopMe({
+ [Symbol()]: "first unnamed symbol",
+ [Symbol()]: "second unnamed symbol",
+ [Symbol("named")] : "named symbol",
+ [Symbol.iterator] : function* () {
+ yield 1;
+ yield 2;
+ },
+ x: 10,
+ });
+ `);
+ }
+
+ function check_preview(grip) {
+ Assert.equal(grip.class, "Object");
+
+ const { preview } = grip;
+ Assert.equal(preview.ownProperties.x.configurable, true);
+ Assert.equal(preview.ownProperties.x.enumerable, true);
+ Assert.equal(preview.ownProperties.x.writable, true);
+ Assert.equal(preview.ownProperties.x.value, 10);
+
+ const [
+ firstUnnamedSymbol,
+ secondUnnamedSymbol,
+ namedSymbol,
+ iteratorSymbol,
+ ] = preview.ownSymbols;
+
+ Assert.equal(firstUnnamedSymbol.name, undefined);
+ Assert.equal(firstUnnamedSymbol.type, "symbol");
+ Assert.equal(firstUnnamedSymbol.descriptor.configurable, true);
+ Assert.equal(firstUnnamedSymbol.descriptor.enumerable, true);
+ Assert.equal(firstUnnamedSymbol.descriptor.writable, true);
+ Assert.equal(firstUnnamedSymbol.descriptor.value, "first unnamed symbol");
+
+ Assert.equal(secondUnnamedSymbol.name, undefined);
+ Assert.equal(secondUnnamedSymbol.type, "symbol");
+ Assert.equal(secondUnnamedSymbol.descriptor.configurable, true);
+ Assert.equal(secondUnnamedSymbol.descriptor.enumerable, true);
+ Assert.equal(secondUnnamedSymbol.descriptor.writable, true);
+ Assert.equal(
+ secondUnnamedSymbol.descriptor.value,
+ "second unnamed symbol"
+ );
+
+ Assert.equal(namedSymbol.name, "named");
+ Assert.equal(namedSymbol.type, "symbol");
+ Assert.equal(namedSymbol.descriptor.configurable, true);
+ Assert.equal(namedSymbol.descriptor.enumerable, true);
+ Assert.equal(namedSymbol.descriptor.writable, true);
+ Assert.equal(namedSymbol.descriptor.value, "named symbol");
+
+ Assert.equal(iteratorSymbol.name, "Symbol.iterator");
+ Assert.equal(iteratorSymbol.type, "symbol");
+ Assert.equal(iteratorSymbol.descriptor.configurable, true);
+ Assert.equal(iteratorSymbol.descriptor.enumerable, true);
+ Assert.equal(iteratorSymbol.descriptor.writable, true);
+ Assert.equal(iteratorSymbol.descriptor.value.class, "Function");
+ }
+
+ function check_prototype_and_properties(response) {
+ Assert.equal(response.ownProperties.x.configurable, true);
+ Assert.equal(response.ownProperties.x.enumerable, true);
+ Assert.equal(response.ownProperties.x.writable, true);
+ Assert.equal(response.ownProperties.x.value, 10);
+
+ const [
+ firstUnnamedSymbol,
+ secondUnnamedSymbol,
+ namedSymbol,
+ iteratorSymbol,
+ ] = response.ownSymbols;
+
+ Assert.equal(firstUnnamedSymbol.name, "Symbol()");
+ Assert.equal(firstUnnamedSymbol.descriptor.configurable, true);
+ Assert.equal(firstUnnamedSymbol.descriptor.enumerable, true);
+ Assert.equal(firstUnnamedSymbol.descriptor.writable, true);
+ Assert.equal(firstUnnamedSymbol.descriptor.value, "first unnamed symbol");
+
+ Assert.equal(secondUnnamedSymbol.name, "Symbol()");
+ Assert.equal(secondUnnamedSymbol.descriptor.configurable, true);
+ Assert.equal(secondUnnamedSymbol.descriptor.enumerable, true);
+ Assert.equal(secondUnnamedSymbol.descriptor.writable, true);
+ Assert.equal(
+ secondUnnamedSymbol.descriptor.value,
+ "second unnamed symbol"
+ );
+
+ Assert.equal(namedSymbol.name, "Symbol(named)");
+ Assert.equal(namedSymbol.descriptor.configurable, true);
+ Assert.equal(namedSymbol.descriptor.enumerable, true);
+ Assert.equal(namedSymbol.descriptor.writable, true);
+ Assert.equal(namedSymbol.descriptor.value, "named symbol");
+
+ Assert.equal(iteratorSymbol.name, "Symbol(Symbol.iterator)");
+ Assert.equal(iteratorSymbol.descriptor.configurable, true);
+ Assert.equal(iteratorSymbol.descriptor.enumerable, true);
+ Assert.equal(iteratorSymbol.descriptor.writable, true);
+ Assert.equal(iteratorSymbol.descriptor.value.class, "Function");
+ }
+ })
+);
diff --git a/devtools/server/tests/xpcshell/test_objectgrips-17.js b/devtools/server/tests/xpcshell/test_objectgrips-17.js
new file mode 100644
index 0000000000..edaea88eaa
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_objectgrips-17.js
@@ -0,0 +1,320 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+async function testPrincipal(options, globalPrincipal, debuggeeHasXrays) {
+ const { debuggee } = options;
+ // Create a global object with the specified security principal.
+ // If none is specified, use the debuggee.
+ if (globalPrincipal === undefined) {
+ await test(options, {
+ global: debuggee,
+ subsumes: true,
+ isOpaque: false,
+ globalIsInvisible: false,
+ });
+ return;
+ }
+
+ const debuggeePrincipal = Cu.getObjectPrincipal(debuggee);
+ const sameOrigin = debuggeePrincipal.origin === globalPrincipal.origin;
+ const subsumes = debuggeePrincipal.subsumes(globalPrincipal);
+ for (const globalHasXrays of [true, false]) {
+ const isOpaque =
+ subsumes &&
+ globalPrincipal !== systemPrincipal &&
+ ((sameOrigin && debuggeeHasXrays) || globalHasXrays);
+ for (const globalIsInvisible of [true, false]) {
+ let global = Cu.Sandbox(globalPrincipal, {
+ wantXrays: globalHasXrays,
+ invisibleToDebugger: globalIsInvisible,
+ });
+ // Previously, the Sandbox constructor would (bizarrely) waive xrays on
+ // the return Sandbox if wantXrays was false. This has now been fixed,
+ // but we need to mimic that behavior here to make the test continue
+ // to pass.
+ if (!globalHasXrays) {
+ global = Cu.waiveXrays(global);
+ }
+ await test(options, { global, subsumes, isOpaque, globalIsInvisible });
+ }
+ }
+}
+
+async function test({ threadFront, debuggee }, testOptions) {
+ const { global } = testOptions;
+ const packet = await executeOnNextTickAndWaitForPause(eval_code, threadFront);
+ // Get the grips.
+ const [proxyGrip, inheritsProxyGrip, inheritsProxy2Grip] =
+ packet.frame.arguments;
+
+ // Check the grip of the proxy object.
+ check_proxy_grip(debuggee, testOptions, proxyGrip);
+
+ // Check the target and handler slots of the proxy object.
+ const proxyClient = threadFront.pauseGrip(proxyGrip);
+ const proxySlots = await proxyClient.getProxySlots();
+ check_proxy_slots(debuggee, testOptions, proxyGrip, proxySlots);
+
+ // Check the prototype and properties of the proxy object.
+ const proxyResponse = await proxyClient.getPrototypeAndProperties();
+ check_properties(testOptions, proxyResponse.ownProperties, true, false);
+ check_prototype(debuggee, testOptions, proxyResponse.prototype, true, false);
+
+ // Check the prototype and properties of the object which inherits from the proxy.
+ const inheritsProxyClient = threadFront.pauseGrip(inheritsProxyGrip);
+ const inheritsProxyResponse =
+ await inheritsProxyClient.getPrototypeAndProperties();
+ check_properties(
+ testOptions,
+ inheritsProxyResponse.ownProperties,
+ false,
+ false
+ );
+ check_prototype(
+ debuggee,
+ testOptions,
+ inheritsProxyResponse.prototype,
+ false,
+ false
+ );
+
+ // The prototype chain was not iterated if the object was inaccessible, so now check
+ // another object which inherits from the proxy, but was created in the debuggee.
+ const inheritsProxy2Client = threadFront.pauseGrip(inheritsProxy2Grip);
+ const inheritsProxy2Response =
+ await inheritsProxy2Client.getPrototypeAndProperties();
+ check_properties(
+ testOptions,
+ inheritsProxy2Response.ownProperties,
+ false,
+ true
+ );
+ check_prototype(
+ debuggee,
+ testOptions,
+ inheritsProxy2Response.prototype,
+ false,
+ true
+ );
+
+ // Check that none of the above ran proxy traps.
+ strictEqual(global.trapDidRun, false, "No proxy trap did run.");
+
+ // Resume the debugger and finish the current test.
+ await threadFront.resume();
+
+ function eval_code() {
+ // Create objects in `global`, and debug them in `debuggee`. They may get various
+ // kinds of security wrappers, or no wrapper at all.
+ // To detect that no proxy trap runs, the proxy handler should define all possible
+ // traps, but the list is long and may change. Therefore a second proxy is used as
+ // the handler, so that a single `get` trap suffices.
+ global.eval(`
+ var trapDidRun = false;
+ var proxy = new Proxy({}, new Proxy({}, {get: (_, trap) => {
+ trapDidRun = true;
+ throw new Error("proxy trap '" + trap + "' was called.");
+ }}));
+ var inheritsProxy = Object.create(proxy, {x:{value:1}});
+ `);
+ const data = Cu.createObjectIn(debuggee, { defineAs: "data" });
+ data.proxy = global.proxy;
+ data.inheritsProxy = global.inheritsProxy;
+ debuggee.eval(`
+ var inheritsProxy2 = Object.create(data.proxy, {x:{value:1}});
+ stopMe(data.proxy, data.inheritsProxy, inheritsProxy2);
+ `);
+ }
+}
+
+function check_proxy_grip(debuggee, testOptions, grip) {
+ const { global, isOpaque, subsumes, globalIsInvisible } = testOptions;
+ const { preview } = grip;
+
+ if (global === debuggee) {
+ // The proxy has no security wrappers.
+ strictEqual(grip.class, "Proxy", "The grip has a Proxy class.");
+ strictEqual(
+ preview.ownPropertiesLength,
+ 2,
+ "The preview has 2 properties."
+ );
+ const props = preview.ownProperties;
+ ok(props["<target>"].value, "<target> contains the [[ProxyTarget]].");
+ ok(props["<handler>"].value, "<handler> contains the [[ProxyHandler]].");
+ } else if (isOpaque) {
+ // The proxy has opaque security wrappers.
+ strictEqual(grip.class, "Opaque", "The grip has an Opaque class.");
+ strictEqual(grip.ownPropertyLength, 0, "The grip has no properties.");
+ } else if (!subsumes) {
+ // The proxy belongs to compartment not subsumed by the debuggee.
+ strictEqual(grip.class, "Restricted", "The grip has a Restricted class.");
+ strictEqual(
+ grip.ownPropertyLength,
+ undefined,
+ "The grip doesn't know the number of properties."
+ );
+ } else if (globalIsInvisible) {
+ // The proxy belongs to an invisible-to-debugger compartment.
+ strictEqual(
+ grip.class,
+ "InvisibleToDebugger: Object",
+ "The grip has an InvisibleToDebugger class."
+ );
+ ok(
+ !("ownPropertyLength" in grip),
+ "The grip doesn't know the number of properties."
+ );
+ } else {
+ // The proxy has non-opaque security wrappers.
+ strictEqual(grip.class, "Proxy", "The grip has a Proxy class.");
+ strictEqual(
+ preview.ownPropertiesLength,
+ 0,
+ "The preview has no properties."
+ );
+ ok(!("<target>" in preview), "The preview has no <target> property.");
+ ok(!("<handler>" in preview), "The preview has no <handler> property.");
+ }
+}
+
+function check_proxy_slots(debuggee, testOptions, grip, proxySlots) {
+ const { global } = testOptions;
+
+ if (grip.class !== "Proxy") {
+ strictEqual(
+ proxySlots,
+ null,
+ "Slots can only be retrived for Proxy grips."
+ );
+ } else if (global === debuggee) {
+ const { proxyTarget, proxyHandler } = proxySlots;
+ strictEqual(
+ proxyTarget.getGrip().type,
+ "object",
+ "There is a [[ProxyTarget]] grip."
+ );
+ strictEqual(
+ proxyHandler.getGrip().type,
+ "object",
+ "There is a [[ProxyHandler]] grip."
+ );
+ } else {
+ const { proxyTarget, proxyHandler } = proxySlots;
+ strictEqual(
+ proxyTarget.type,
+ "undefined",
+ "There is no [[ProxyTarget]] grip."
+ );
+ strictEqual(
+ proxyHandler.type,
+ "undefined",
+ "There is no [[ProxyHandler]] grip."
+ );
+ }
+}
+
+function check_properties(testOptions, props, isProxy, createdInDebuggee) {
+ const { subsumes, globalIsInvisible } = testOptions;
+ const ownPropertiesLength = Reflect.ownKeys(props).length;
+
+ if (createdInDebuggee || (!isProxy && subsumes && !globalIsInvisible)) {
+ // The debuggee can access the properties.
+ strictEqual(ownPropertiesLength, 1, "1 own property was retrieved.");
+ strictEqual(props.x.value, 1, "The property has the right value.");
+ } else {
+ // The debuggee is not allowed to access the object.
+ strictEqual(ownPropertiesLength, 0, "No own property could be retrieved.");
+ }
+}
+
+function check_prototype(
+ debuggee,
+ testOptions,
+ proto,
+ isProxy,
+ createdInDebuggee
+) {
+ const { global, isOpaque, subsumes, globalIsInvisible } = testOptions;
+ if (isOpaque && !globalIsInvisible && !createdInDebuggee) {
+ // The object is or inherits from a proxy with opaque security wrappers.
+ // The debuggee sees `Object.prototype` when retrieving the prototype.
+ strictEqual(
+ proto.getGrip().class,
+ "Object",
+ "The prototype has a Object class."
+ );
+ } else if (isProxy && isOpaque && globalIsInvisible) {
+ // The object is a proxy with opaque security wrappers in an invisible global.
+ // The debuggee sees an inaccessible `Object.prototype` when retrieving the prototype.
+ strictEqual(
+ proto.getGrip().class,
+ "InvisibleToDebugger: Object",
+ "The prototype has an InvisibleToDebugger class."
+ );
+ } else if (
+ createdInDebuggee ||
+ (!isProxy && subsumes && !globalIsInvisible)
+ ) {
+ // The object inherits from a proxy and has no security wrappers or non-opaque ones.
+ // The debuggee sees the proxy when retrieving the prototype.
+ check_proxy_grip(
+ debuggee,
+ { global, isOpaque, subsumes, globalIsInvisible },
+ proto.getGrip()
+ );
+ } else {
+ // The debuggee is not allowed to access the object. It sees a null prototype.
+ strictEqual(proto.type, "null", "The prototype is null.");
+ }
+}
+
+function createNullPrincipal() {
+ return Services.scriptSecurityManager.createNullPrincipal({});
+}
+
+async function run_tests_in_principal(
+ options,
+ debuggeePrincipal,
+ debuggeeHasXrays
+) {
+ const { debuggee } = options;
+ debuggee.eval(
+ function stopMe(arg1, arg2) {
+ debugger;
+ }.toString()
+ );
+
+ // Test objects created in the debuggee.
+ await testPrincipal(options, undefined, debuggeeHasXrays);
+
+ // Test objects created in a system principal new global.
+ await testPrincipal(options, systemPrincipal, debuggeeHasXrays);
+
+ // Test objects created in a cross-origin null principal new global.
+ await testPrincipal(options, createNullPrincipal(), debuggeeHasXrays);
+
+ if (debuggeePrincipal != systemPrincipal) {
+ // Test objects created in a same-origin principal new global.
+ await testPrincipal(options, debuggeePrincipal, debuggeeHasXrays);
+ }
+}
+
+for (const principal of [systemPrincipal, createNullPrincipal()]) {
+ for (const wantXrays of [true, false]) {
+ add_task(
+ threadFrontTest(
+ options => run_tests_in_principal(options, principal, wantXrays),
+ { principal, wantXrays }
+ )
+ );
+ }
+}
diff --git a/devtools/server/tests/xpcshell/test_objectgrips-18.js b/devtools/server/tests/xpcshell/test_objectgrips-18.js
new file mode 100644
index 0000000000..90c38d99a9
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_objectgrips-18.js
@@ -0,0 +1,173 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ eval_code,
+ threadFront
+ );
+
+ const [grip] = packet.frame.arguments;
+
+ const objectFront = threadFront.pauseGrip(grip);
+
+ // Checks the result of enumProperties.
+ let response = await objectFront.enumProperties({});
+ await check_enum_properties(response);
+
+ // Checks the result of enumSymbols.
+ response = await objectFront.enumSymbols();
+ await check_enum_symbols(response);
+
+ await threadFront.resume();
+
+ function eval_code() {
+ debuggee.eval(
+ function stopMe(arg1) {
+ debugger;
+ }.toString()
+ );
+
+ debuggee.eval(`
+ var obj = Array.from({length: 10})
+ .reduce((res, _, i) => {
+ res["property_" + i + "_key"] = "property_" + i + "_value";
+ res[Symbol("symbol_" + i)] = "symbol_" + i + "_value";
+ return res;
+ }, {});
+
+ obj[Symbol()] = "first unnamed symbol";
+ obj[Symbol()] = "second unnamed symbol";
+ obj[Symbol.iterator] = function* () {
+ yield 1;
+ yield 2;
+ };
+
+ stopMe(obj);
+ `);
+ }
+
+ async function check_enum_properties(iterator) {
+ equal(iterator.count, 10, "iterator.count has the expected value");
+
+ info("Check iterator.slice response for all properties");
+ let sliceResponse = await iterator.slice(0, iterator.count);
+ ok(
+ sliceResponse &&
+ Object.getOwnPropertyNames(sliceResponse).includes("ownProperties"),
+ "The response object has an ownProperties property"
+ );
+
+ let { ownProperties } = sliceResponse;
+ let names = Object.keys(ownProperties);
+ equal(
+ names.length,
+ iterator.count,
+ "The response has the expected number of properties"
+ );
+ for (let i = 0; i < names.length; i++) {
+ const name = names[i];
+ equal(name, `property_${i}_key`);
+ equal(ownProperties[name].value, `property_${i}_value`);
+ }
+
+ info("Check iterator.all response");
+ const allResponse = await iterator.all();
+ deepEqual(
+ allResponse,
+ sliceResponse,
+ "iterator.all response has the expected data"
+ );
+
+ info("Check iterator response for 2 properties only");
+ sliceResponse = await iterator.slice(2, 2);
+ ok(
+ sliceResponse &&
+ Object.getOwnPropertyNames(sliceResponse).includes("ownProperties"),
+ "The response object has an ownProperties property"
+ );
+
+ ownProperties = sliceResponse.ownProperties;
+ names = Object.keys(ownProperties);
+ equal(
+ names.length,
+ 2,
+ "The response has the expected number of properties"
+ );
+ equal(names[0], `property_2_key`);
+ equal(names[1], `property_3_key`);
+ equal(ownProperties[names[0]].value, `property_2_value`);
+ equal(ownProperties[names[1]].value, `property_3_value`);
+ }
+
+ async function check_enum_symbols(iterator) {
+ equal(iterator.count, 13, "iterator.count has the expected value");
+
+ info("Check iterator.slice response for all symbols");
+ let sliceResponse = await iterator.slice(0, iterator.count);
+ ok(
+ sliceResponse &&
+ Object.getOwnPropertyNames(sliceResponse).includes("ownSymbols"),
+ "The response object has an ownSymbols property"
+ );
+
+ let { ownSymbols } = sliceResponse;
+ equal(
+ ownSymbols.length,
+ iterator.count,
+ "The response has the expected number of symbols"
+ );
+ for (let i = 0; i < 10; i++) {
+ const symbol = ownSymbols[i];
+ equal(symbol.name, `Symbol(symbol_${i})`);
+ equal(symbol.descriptor.value, `symbol_${i}_value`);
+ }
+ const firstUnnamedSymbol = ownSymbols[10];
+ equal(firstUnnamedSymbol.name, "Symbol()");
+ equal(firstUnnamedSymbol.descriptor.value, "first unnamed symbol");
+
+ const secondUnnamedSymbol = ownSymbols[11];
+ equal(secondUnnamedSymbol.name, "Symbol()");
+ equal(secondUnnamedSymbol.descriptor.value, "second unnamed symbol");
+
+ const iteratorSymbol = ownSymbols[12];
+ equal(iteratorSymbol.name, "Symbol(Symbol.iterator)");
+ equal(iteratorSymbol.descriptor.value.getGrip().class, "Function");
+
+ info("Check iterator.all response");
+ const allResponse = await iterator.all();
+ deepEqual(
+ allResponse,
+ sliceResponse,
+ "iterator.all response has the expected data"
+ );
+
+ info("Check iterator response for 2 symbols only");
+ sliceResponse = await iterator.slice(9, 2);
+ ok(
+ sliceResponse &&
+ Object.getOwnPropertyNames(sliceResponse).includes("ownSymbols"),
+ "The response object has an ownSymbols property"
+ );
+
+ ownSymbols = sliceResponse.ownSymbols;
+ equal(
+ ownSymbols.length,
+ 2,
+ "The response has the expected number of symbols"
+ );
+ equal(ownSymbols[0].name, "Symbol(symbol_9)");
+ equal(ownSymbols[0].descriptor.value, "symbol_9_value");
+ equal(ownSymbols[1].name, "Symbol()");
+ equal(ownSymbols[1].descriptor.value, "first unnamed symbol");
+ }
+ })
+);
diff --git a/devtools/server/tests/xpcshell/test_objectgrips-19.js b/devtools/server/tests/xpcshell/test_objectgrips-19.js
new file mode 100644
index 0000000000..655c7d0f43
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_objectgrips-19.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee, client }) => {
+ debuggee.eval(
+ function stopMe(arg1) {
+ debugger;
+ }.toString()
+ );
+ const tests = [
+ {
+ value: true,
+ class: "Boolean",
+ },
+ {
+ value: 123,
+ class: "Number",
+ },
+ {
+ value: "foo",
+ class: "String",
+ },
+ {
+ value: Symbol("bar"),
+ class: "Symbol",
+ name: "bar",
+ },
+ ];
+ for (const data of tests) {
+ debuggee.primitive = data.value;
+ const packet = await executeOnNextTickAndWaitForPause(() => {
+ debuggee.eval("stopMe(Object(primitive));");
+ }, threadFront);
+
+ const [grip] = packet.frame.arguments;
+ check_wrapped_primitive_grip(grip, data);
+
+ await threadFront.resume();
+ }
+ })
+);
+
+function check_wrapped_primitive_grip(grip, data) {
+ strictEqual(grip.class, data.class, "The grip has the proper class.");
+
+ if (!grip.preview) {
+ // In a worker thread Cu does not exist, the objects are considered unsafe and
+ // can't be unwrapped, so there is no preview.
+ return;
+ }
+
+ const value = grip.preview.wrappedValue;
+ if (data.class === "Symbol") {
+ strictEqual(
+ value.type,
+ "symbol",
+ "The wrapped value grip has symbol type."
+ );
+ strictEqual(
+ value.name,
+ data.name,
+ "The wrapped value grip has the proper name."
+ );
+ } else {
+ strictEqual(value, data.value, "The wrapped value is the primitive one.");
+ }
+}
diff --git a/devtools/server/tests/xpcshell/test_objectgrips-20.js b/devtools/server/tests/xpcshell/test_objectgrips-20.js
new file mode 100644
index 0000000000..5027ca31a7
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_objectgrips-20.js
@@ -0,0 +1,387 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that onEnumProperties returns the expected data
+// when passing `ignoreNonIndexedProperties` and `ignoreIndexedProperties` options
+// with various objects. (See Bug 1403065)
+
+const DO_NOT_CHECK_VALUE = Symbol();
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee, client }) => {
+ debuggee.eval(
+ function stopMe(arg1) {
+ debugger;
+ }.toString()
+ );
+
+ const testCases = [
+ {
+ evaledObject: { a: 10 },
+ expectedIndexedProperties: [],
+ expectedNonIndexedProperties: [["a", 10]],
+ },
+ {
+ evaledObject: { length: 10 },
+ expectedIndexedProperties: [],
+ expectedNonIndexedProperties: [["length", 10]],
+ },
+ {
+ evaledObject: { a: 10, 0: "indexed" },
+ expectedIndexedProperties: [["0", "indexed"]],
+ expectedNonIndexedProperties: [["a", 10]],
+ },
+ {
+ evaledObject: { 1: 1, length: 42, a: 10 },
+ expectedIndexedProperties: [["1", 1]],
+ expectedNonIndexedProperties: [
+ ["length", 42],
+ ["a", 10],
+ ],
+ },
+ {
+ evaledObject: { 1: 1, length: 2.34, a: 10 },
+ expectedIndexedProperties: [["1", 1]],
+ expectedNonIndexedProperties: [
+ ["length", 2.34],
+ ["a", 10],
+ ],
+ },
+ {
+ evaledObject: { 1: 1, length: -0, a: 10 },
+ expectedIndexedProperties: [["1", 1]],
+ expectedNonIndexedProperties: [
+ ["length", -0],
+ ["a", 10],
+ ],
+ },
+ {
+ evaledObject: { 1: 1, length: -10, a: 10 },
+ expectedIndexedProperties: [["1", 1]],
+ expectedNonIndexedProperties: [
+ ["length", -10],
+ ["a", 10],
+ ],
+ },
+ {
+ evaledObject: { 1: 1, length: true, a: 10 },
+ expectedIndexedProperties: [["1", 1]],
+ expectedNonIndexedProperties: [
+ ["length", true],
+ ["a", 10],
+ ],
+ },
+ {
+ evaledObject: { 1: 1, length: null, a: 10 },
+ expectedIndexedProperties: [["1", 1]],
+ expectedNonIndexedProperties: [
+ ["length", DO_NOT_CHECK_VALUE],
+ ["a", 10],
+ ],
+ },
+ {
+ evaledObject: { 1: 1, length: Math.pow(2, 53), a: 10 },
+ expectedIndexedProperties: [["1", 1]],
+ expectedNonIndexedProperties: [
+ ["length", 9007199254740992],
+ ["a", 10],
+ ],
+ },
+ {
+ evaledObject: { 1: 1, length: "fake", a: 10 },
+ expectedIndexedProperties: [["1", 1]],
+ expectedNonIndexedProperties: [
+ ["length", "fake"],
+ ["a", 10],
+ ],
+ },
+ {
+ evaledObject: { 1: 1, length: Infinity, a: 10 },
+ expectedIndexedProperties: [["1", 1]],
+ expectedNonIndexedProperties: [
+ ["length", DO_NOT_CHECK_VALUE],
+ ["a", 10],
+ ],
+ },
+ {
+ evaledObject: { 0: 0, length: 0 },
+ expectedIndexedProperties: [["0", 0]],
+ expectedNonIndexedProperties: [["length", 0]],
+ },
+ {
+ evaledObject: { 0: 0, 1: 1, length: 1 },
+ expectedIndexedProperties: [
+ ["0", 0],
+ ["1", 1],
+ ],
+ expectedNonIndexedProperties: [["length", 1]],
+ },
+ {
+ evaledObject: { length: 0 },
+ expectedIndexedProperties: [],
+ expectedNonIndexedProperties: [["length", 0]],
+ },
+ {
+ evaledObject: { 1: 1 },
+ expectedIndexedProperties: [["1", 1]],
+ expectedNonIndexedProperties: [],
+ },
+ {
+ evaledObject: { a: 1, [2 ** 32 - 2]: 2, [2 ** 32 - 1]: 3 },
+ expectedIndexedProperties: [["4294967294", 2]],
+ expectedNonIndexedProperties: [
+ ["a", 1],
+ ["4294967295", 3],
+ ],
+ },
+ {
+ evaledObject: `(() => {
+ x = [12, 42];
+ x.foo = 90;
+ return x;
+ })()`,
+ expectedIndexedProperties: [
+ ["0", 12],
+ ["1", 42],
+ ],
+ expectedNonIndexedProperties: [
+ ["length", 2],
+ ["foo", 90],
+ ],
+ },
+ {
+ evaledObject: `(() => {
+ x = [12, 42];
+ x.length = 3;
+ return x;
+ })()`,
+ expectedIndexedProperties: [
+ ["0", 12],
+ ["1", 42],
+ ["2", undefined],
+ ],
+ expectedNonIndexedProperties: [["length", 3]],
+ },
+ {
+ evaledObject: `(() => {
+ x = [12, 42];
+ x.length = 1;
+ return x;
+ })()`,
+ expectedIndexedProperties: [["0", 12]],
+ expectedNonIndexedProperties: [["length", 1]],
+ },
+ {
+ evaledObject: `(() => {
+ x = [, 42,,];
+ x.foo = 90;
+ return x;
+ })()`,
+ expectedIndexedProperties: [
+ ["0", undefined],
+ ["1", 42],
+ ["2", undefined],
+ ],
+ expectedNonIndexedProperties: [
+ ["length", 3],
+ ["foo", 90],
+ ],
+ },
+ {
+ evaledObject: `(() => {
+ x = Array(2);
+ x.foo = "bar";
+ x.bar = "foo";
+ return x;
+ })()`,
+ expectedIndexedProperties: [
+ ["0", undefined],
+ ["1", undefined],
+ ],
+ expectedNonIndexedProperties: [
+ ["length", 2],
+ ["foo", "bar"],
+ ["bar", "foo"],
+ ],
+ },
+ {
+ evaledObject: `(() => {
+ x = new Int8Array(new ArrayBuffer(2));
+ x.foo = "bar";
+ x.bar = "foo";
+ return x;
+ })()`,
+ expectedIndexedProperties: [
+ ["0", 0],
+ ["1", 0],
+ ],
+ expectedNonIndexedProperties: [
+ ["foo", "bar"],
+ ["bar", "foo"],
+ ["length", 2],
+ ["buffer", DO_NOT_CHECK_VALUE],
+ ["byteLength", 2],
+ ["byteOffset", 0],
+ ],
+ },
+ {
+ evaledObject: `(() => {
+ x = new Int8Array([1, 2]);
+ Object.defineProperty(x, 'length', {value: 0});
+ return x;
+ })()`,
+ expectedIndexedProperties: [
+ ["0", 1],
+ ["1", 2],
+ ],
+ expectedNonIndexedProperties: [
+ ["length", 0],
+ ["buffer", DO_NOT_CHECK_VALUE],
+ ["byteLength", 2],
+ ["byteOffset", 0],
+ ],
+ },
+ {
+ evaledObject: `(() => {
+ x = new Int32Array([1, 2]);
+ Object.setPrototypeOf(x, null);
+ return x;
+ })()`,
+ expectedIndexedProperties: [
+ ["0", 1],
+ ["1", 2],
+ ],
+ expectedNonIndexedProperties: [],
+ },
+ {
+ evaledObject: `(() => {
+ return new (class extends Int8Array {})([1, 2]);
+ })()`,
+ expectedIndexedProperties: [
+ ["0", 1],
+ ["1", 2],
+ ],
+ expectedNonIndexedProperties: [
+ ["length", 2],
+ ["buffer", DO_NOT_CHECK_VALUE],
+ ["byteLength", 2],
+ ["byteOffset", 0],
+ ],
+ },
+ ];
+
+ for (const test of testCases) {
+ await test_object_grip(debuggee, client, threadFront, test);
+ }
+ })
+);
+
+async function test_object_grip(
+ debuggee,
+ dbgClient,
+ threadFront,
+ testData = {}
+) {
+ const {
+ evaledObject,
+ expectedIndexedProperties,
+ expectedNonIndexedProperties,
+ } = testData;
+
+ const packet = await executeOnNextTickAndWaitForPause(eval_code, threadFront);
+
+ const [grip] = packet.frame.arguments;
+
+ const objClient = threadFront.pauseGrip(grip);
+
+ info(`
+ Check enumProperties response for
+ ${
+ typeof evaledObject === "string"
+ ? evaledObject
+ : JSON.stringify(evaledObject)
+ }
+ `);
+
+ // Checks the result of enumProperties.
+ let response = await objClient.enumProperties({
+ ignoreNonIndexedProperties: true,
+ });
+ await check_enum_properties(response, expectedIndexedProperties);
+
+ response = await objClient.enumProperties({
+ ignoreIndexedProperties: true,
+ });
+ await check_enum_properties(response, expectedNonIndexedProperties);
+
+ await threadFront.resume();
+
+ function eval_code() {
+ // Be sure to run debuggee code in its own HTML 'task', so that when we call
+ // the onDebuggerStatement hook, the test's own microtasks don't get suspended
+ // along with the debuggee's.
+ do_timeout(0, () => {
+ debuggee.eval(`
+ stopMe(${
+ typeof evaledObject === "string"
+ ? evaledObject
+ : JSON.stringify(evaledObject)
+ });
+ `);
+ });
+ }
+}
+
+async function check_enum_properties(iterator, expected = []) {
+ equal(
+ iterator.count,
+ expected.length,
+ "iterator.count has the expected value"
+ );
+
+ info("Check iterator.slice response for all properties");
+ const sliceResponse = await iterator.slice(0, iterator.count);
+ ok(
+ sliceResponse &&
+ Object.getOwnPropertyNames(sliceResponse).includes("ownProperties"),
+ "The response object has an ownProperties property"
+ );
+
+ const { ownProperties } = sliceResponse;
+ const names = Object.getOwnPropertyNames(ownProperties);
+ equal(
+ names.length,
+ expected.length,
+ "The response has the expected number of properties"
+ );
+ for (let i = 0; i < names.length; i++) {
+ const name = names[i];
+ const [key, value] = expected[i];
+ equal(name, key, "Property has the expected name");
+ const property = ownProperties[name];
+
+ if (value === DO_NOT_CHECK_VALUE) {
+ return;
+ }
+
+ if (value === undefined) {
+ equal(
+ property,
+ undefined,
+ `Response has no value for the "${key}" property`
+ );
+ } else {
+ const propValue = property.hasOwnProperty("value")
+ ? property.value
+ : property.getterValue;
+ equal(propValue, value, `Property "${key}" has the expected value`);
+ }
+ }
+}
diff --git a/devtools/server/tests/xpcshell/test_objectgrips-21.js b/devtools/server/tests/xpcshell/test_objectgrips-21.js
new file mode 100644
index 0000000000..88296f7786
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_objectgrips-21.js
@@ -0,0 +1,396 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+// Run test_unsafe_grips twice, one against a system principal debuggee
+// and another time with a null principal debuggee
+
+// The following tests work like this:
+// - The specified code is evaluated in a system principal.
+// `Cu`, `systemPrincipal` and `Services` are provided as global variables.
+// - The resulting object is debugged in a system or null principal debuggee,
+// depending on in which list the test is placed.
+// It is tested according to the specified test parameters.
+// - An ordinary object that inherits from the resulting one is also debugged.
+// This is just to check that it can be normally debugged even with an unsafe
+// object in the prototype. The specified test parameters do not apply.
+
+// The following tests are defined via properties with the following defaults.
+const defaults = {
+ // The class of the grip.
+ class: "Restricted",
+
+ // The stringification of the object
+ string: "",
+
+ // Whether the object (not its grip) has class "Function".
+ isFunction: false,
+
+ // Whether the grip has a preview property.
+ hasPreview: true,
+
+ // Code that assigns the object to be tested into the obj variable.
+ code: "var obj = {}",
+
+ // The type of the grip of the prototype.
+ protoType: "null",
+
+ // Whether the object has some own string properties.
+ hasOwnPropertyNames: false,
+
+ // Whether the object has some own symbol properties.
+ hasOwnPropertySymbols: false,
+
+ // The descriptor obtained when retrieving property "x" or Symbol("x").
+ property: undefined,
+
+ // Code evaluated after the test, whose result is expected to be true.
+ afterTest: "true == true",
+};
+
+// The following tests use a system principal debuggee.
+const systemPrincipalTests = [
+ {
+ // Dead objects throw a TypeError when accessing properties.
+ class: "DeadObject",
+ string: "<dead object>",
+ code: `
+ var obj = Cu.Sandbox(null);
+ Cu.nukeSandbox(obj);
+ `,
+ property: descriptor({ value: "TypeError" }),
+ },
+ {
+ // This proxy checks that no trap runs (using a second proxy as the handler
+ // there is no need to maintain a list of all possible traps).
+ class: "Proxy",
+ string: "<proxy>",
+ code: `
+ var trapDidRun = false;
+ var obj = new Proxy({}, new Proxy({}, {get: (_, trap) => {
+ trapDidRun = true;
+ throw new Error("proxy trap '" + trap + "' was called.");
+ }}));
+ `,
+ afterTest: "trapDidRun === false",
+ },
+ {
+ // Like the previous test, but now the proxy has a Function class.
+ class: "Proxy",
+ string: "<proxy>",
+ isFunction: true,
+ code: `
+ var trapDidRun = false;
+ var obj = new Proxy(function(){}, new Proxy({}, {get: (_, trap) => {
+ trapDidRun = true;
+ throw new Error("proxy trap '" + trap + "' was called.(function)");
+ }}));
+ `,
+ afterTest: "trapDidRun === false",
+ },
+ {
+ // Invisisible-to-debugger objects can't be unwrapped, so we don't know if
+ // they are proxies. Thus they shouldn't be accessed.
+ class: "InvisibleToDebugger: Array",
+ string: "<invisibleToDebugger>",
+ hasPreview: false,
+ code: `
+ var s = Cu.Sandbox(systemPrincipal, {invisibleToDebugger: true});
+ var obj = s.eval("[1, 2, 3]");
+ `,
+ },
+ {
+ // Like the previous test, but now the object has a Function class.
+ class: "InvisibleToDebugger: Function",
+ string: "<invisibleToDebugger>",
+ isFunction: true,
+ hasPreview: false,
+ code: `
+ var s = Cu.Sandbox(systemPrincipal, {invisibleToDebugger: true});
+ var obj = s.eval("(function func(arg){})");
+ `,
+ },
+ {
+ // Cu.Sandbox is a WrappedNative that throws when accessing properties.
+ class: "nsXPCComponents_utils_Sandbox",
+ string: "[object nsXPCComponents_utils_Sandbox]",
+ code: `var obj = Cu.Sandbox;`,
+ protoType: "object",
+ },
+];
+
+// The following tests run code in a system principal, but the resulting object
+// is debugged in a null principal.
+const nullPrincipalTests = [
+ {
+ // The null principal gets undefined when attempting to access properties.
+ string: "[object Object]",
+ code: `var obj = {x: -1};`,
+ },
+ {
+ // For arrays it's an error instead of undefined.
+ string: "[object Object]",
+ code: `var obj = [1, 2, 3];`,
+ property: descriptor({ value: "Error" }),
+ },
+ {
+ // For functions it's also an error.
+ string: "function func(arg){}",
+ isFunction: true,
+ hasPreview: false,
+ code: `var obj = function func(arg){};`,
+ property: descriptor({ value: "Error" }),
+ },
+ {
+ // Check that no proxy trap runs.
+ string: "[object Object]",
+ code: `
+ var trapDidRun = false;
+ var obj = new Proxy([], new Proxy({}, {get: (_, trap) => {
+ trapDidRun = true;
+ throw new Error("proxy trap '" + trap + "' was called.");
+ }}));
+ `,
+ property: descriptor({ value: "Error" }),
+ afterTest: `trapDidRun === false`,
+ },
+ {
+ // Like the previous test, but now the object is a callable Proxy.
+ string: "function () {\n [native code]\n}",
+ isFunction: true,
+ hasPreview: false,
+ code: `
+ var trapDidRun = false;
+ var obj = new Proxy(function(){}, new Proxy({}, {get: (_, trap) => {
+ trapDidRun = true;
+ throw new Error("proxy trap '" + trap + "' was called.");
+ }}));
+ `,
+ property: descriptor({ value: "Error" }),
+ afterTest: `trapDidRun === false`,
+ },
+ {
+ // Cross-origin Window objects do expose some properties and have a preview.
+ string: "[object Object]",
+ code: `var obj = Services.appShell.createWindowlessBrowser().document.defaultView;`,
+ hasOwnPropertyNames: true,
+ hasOwnPropertySymbols: true,
+ property: descriptor({ value: "SecurityError" }),
+ previewUrl: "about:blank",
+ },
+ {
+ // Cross-origin Location objects do expose some properties and have a preview.
+ string: "[object Object]",
+ code: `var obj = Services.appShell.createWindowlessBrowser().document.defaultView
+ .location;`,
+ hasOwnPropertyNames: true,
+ hasOwnPropertySymbols: true,
+ property: descriptor({ value: "SecurityError" }),
+ },
+];
+
+function descriptor(descr) {
+ return Object.assign(
+ {
+ configurable: false,
+ writable: false,
+ enumerable: false,
+ value: undefined,
+ },
+ descr
+ );
+}
+
+async function test_unsafe_grips(
+ { threadFront, debuggee, isWorkerServer },
+ tests
+) {
+ debuggee.eval(
+ function stopMe(arg1, arg2) {
+ debugger;
+ }.toString()
+ );
+
+ for (let data of tests) {
+ data = { ...defaults, ...data };
+
+ // Run the code and test the results.
+ const sandbox = Cu.Sandbox(systemPrincipal);
+ Object.assign(sandbox, { Services, systemPrincipal, Cu });
+ sandbox.eval(data.code);
+ debuggee.obj = sandbox.obj;
+ const inherits = `Object.create(obj, {
+ x: {value: 1},
+ [Symbol.for("x")]: {value: 2}
+ })`;
+
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => debuggee.eval(`stopMe(obj, ${inherits});`),
+ threadFront
+ );
+
+ const [objGrip, inheritsGrip] = packet.frame.arguments;
+ for (const grip of [objGrip, inheritsGrip]) {
+ const isUnsafe = grip === objGrip;
+ // If `isUnsafe` is true, the parameters in `data` will be used to assert
+ // against `objGrip`, the grip of the object `obj` created by the test.
+ // Otherwise, the grip will refer to `inherits`, an ordinary object which
+ // inherits from `obj`. Then all checks are hardcoded because in every test
+ // all methods are expected to work the same on `inheritsGrip`.
+ check_grip(grip, data, isUnsafe, isWorkerServer);
+
+ const objClient = threadFront.pauseGrip(grip);
+ let response, slice;
+
+ response = await objClient.getPrototypeAndProperties();
+ check_properties(response.ownProperties, data, isUnsafe);
+ check_symbols(response.ownSymbols, data, isUnsafe);
+ check_prototype(response.prototype, data, isUnsafe, isWorkerServer);
+
+ response = await objClient.enumProperties({
+ ignoreIndexedProperties: true,
+ });
+ slice = await response.slice(0, response.count);
+ check_properties(slice.ownProperties, data, isUnsafe);
+
+ response = await objClient.enumProperties({});
+ slice = await response.slice(0, response.count);
+ check_properties(slice.ownProperties, data, isUnsafe);
+
+ response = await objClient.getProperty("x");
+ check_property(response.descriptor, data, isUnsafe);
+
+ response = await objClient.enumSymbols();
+ slice = await response.slice(0, response.count);
+ check_symbol_names(slice.ownSymbols, data, isUnsafe);
+
+ response = await objClient.getProperty(Symbol.for("x"));
+ check_symbol(response.descriptor, data, isUnsafe);
+
+ response = await objClient.getPrototype();
+ check_prototype(response.prototype, data, isUnsafe, isWorkerServer);
+ }
+
+ await threadFront.resume();
+
+ ok(sandbox.eval(data.afterTest), "Check after test passes");
+ }
+}
+
+function check_grip(grip, data, isUnsafe, isWorkerServer) {
+ if (isUnsafe) {
+ strictEqual(grip.class, data.class, "The grip has the proper class.");
+ strictEqual("preview" in grip, data.hasPreview, "Check preview presence.");
+ // preview.url isn't populated on worker server.
+ if (data.previewUrl && !isWorkerServer) {
+ console.trace();
+ strictEqual(
+ grip.preview.url,
+ data.previewUrl,
+ `Check preview.url for "${data.code}".`
+ );
+ }
+ } else {
+ strictEqual(grip.class, "Object", "The grip has 'Object' class.");
+ ok("preview" in grip, "The grip has a preview.");
+ }
+}
+
+function check_properties(props, data, isUnsafe) {
+ const propNames = Reflect.ownKeys(props);
+ check_property_names(propNames, data, isUnsafe);
+ if (isUnsafe) {
+ deepEqual(props.x, undefined, "The property does not exist.");
+ } else {
+ strictEqual(props.x.value, 1, "The property has the right value.");
+ }
+}
+
+function check_property_names(props, data, isUnsafe) {
+ if (isUnsafe) {
+ strictEqual(
+ !!props.length,
+ data.hasOwnPropertyNames,
+ "Check presence of own string properties."
+ );
+ } else {
+ strictEqual(props.length, 1, "1 own property was retrieved.");
+ strictEqual(props[0], "x", "The property has the right name.");
+ }
+}
+
+function check_property(descr, data, isUnsafe) {
+ if (isUnsafe) {
+ deepEqual(descr, data.property, "Got the right property descriptor.");
+ } else {
+ strictEqual(descr.value, 1, "The property has the right value.");
+ }
+}
+
+function check_symbols(symbols, data, isUnsafe) {
+ check_symbol_names(symbols, data, isUnsafe);
+ if (!isUnsafe) {
+ check_symbol(symbols[0].descriptor, data, isUnsafe);
+ }
+}
+
+function check_symbol_names(props, data, isUnsafe) {
+ if (isUnsafe) {
+ strictEqual(
+ !!props.length,
+ data.hasOwnPropertySymbols,
+ "Check presence of own symbol properties."
+ );
+ } else {
+ strictEqual(props.length, 1, "1 own symbol property was retrieved.");
+ strictEqual(props[0].name, "Symbol(x)", "The symbol has the right name.");
+ }
+}
+
+function check_symbol(descr, data, isUnsafe) {
+ if (isUnsafe) {
+ deepEqual(
+ descr,
+ data.property,
+ "Got the right symbol property descriptor."
+ );
+ } else {
+ strictEqual(descr.value, 2, "The symbol property has the right value.");
+ }
+}
+
+function check_prototype(proto, data, isUnsafe, isWorkerServer) {
+ const protoGrip = proto && proto.getGrip ? proto.getGrip() : proto;
+ if (isUnsafe) {
+ deepEqual(protoGrip.type, data.protoType, "Got the right prototype type.");
+ } else {
+ check_grip(protoGrip, data, true, isWorkerServer);
+ }
+}
+
+// threadFrontTest uses systemPrincipal by default, but let's be explicit here.
+add_task(
+ threadFrontTest(
+ options => {
+ return test_unsafe_grips(options, systemPrincipalTests, "system");
+ },
+ { principal: systemPrincipal }
+ )
+);
+
+const nullPrincipal = Services.scriptSecurityManager.createNullPrincipal({});
+add_task(
+ threadFrontTest(
+ options => {
+ return test_unsafe_grips(options, nullPrincipalTests, "null");
+ },
+ { principal: nullPrincipal }
+ )
+);
diff --git a/devtools/server/tests/xpcshell/test_objectgrips-22.js b/devtools/server/tests/xpcshell/test_objectgrips-22.js
new file mode 100644
index 0000000000..34264f5534
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_objectgrips-22.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const [grip] = packet.frame.arguments;
+ const objClient = threadFront.pauseGrip(grip);
+ const iterator = await objClient.enumSymbols();
+ const { ownSymbols } = await iterator.slice(0, iterator.count);
+
+ strictEqual(ownSymbols.length, 1, "There is 1 symbol property.");
+ const { name, descriptor } = ownSymbols[0];
+ strictEqual(name, "Symbol(sym)", "Got right symbol name.");
+ deepEqual(
+ descriptor,
+ {
+ configurable: false,
+ enumerable: false,
+ writable: false,
+ value: 1,
+ },
+ "Got right property descriptor."
+ );
+
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ debuggee.eval(
+ function stopMe(arg1) {
+ debugger;
+ }.toString()
+ );
+ debuggee.eval(
+ `stopMe(Object.defineProperty({}, Symbol("sym"), {value: 1}));`
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_objectgrips-23.js b/devtools/server/tests/xpcshell/test_objectgrips-23.js
new file mode 100644
index 0000000000..b44beb2c2d
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_objectgrips-23.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that ES6 classes grip have the expected properties.
+
+"use strict";
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const [grip] = packet.frame.arguments;
+ strictEqual(
+ grip.class,
+ "Function",
+ `Grip has expected value for "class" property`
+ );
+ strictEqual(
+ grip.isClassConstructor,
+ true,
+ `Grip has expected value for "isClassConstructor" property`
+ );
+
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ debuggee.eval(`
+ class MyClass {};
+ stopMe(MyClass);
+
+ function stopMe(arg1) {
+ debugger;
+ }
+ `);
+}
diff --git a/devtools/server/tests/xpcshell/test_objectgrips-24.js b/devtools/server/tests/xpcshell/test_objectgrips-24.js
new file mode 100644
index 0000000000..9d541c108d
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_objectgrips-24.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that ES6 classes grip have the expected properties.
+
+"use strict";
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ debuggee.eval(
+ function stopMe() {
+ debugger;
+ }.toString()
+ );
+
+ const tests = [
+ {
+ fn: `function(){}`,
+ isAsync: false,
+ isGenerator: false,
+ },
+ {
+ fn: `async function(){}`,
+ isAsync: true,
+ isGenerator: false,
+ },
+ {
+ fn: `function *(){}`,
+ isAsync: false,
+ isGenerator: true,
+ },
+ {
+ fn: `async function *(){}`,
+ isAsync: true,
+ isGenerator: true,
+ },
+ ];
+
+ for (const { fn, isAsync, isGenerator } of tests) {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => debuggee.eval(`stopMe(${fn})`),
+ threadFront
+ );
+ const [grip] = packet.frame.arguments;
+ strictEqual(grip.class, "Function");
+ strictEqual(grip.isAsync, isAsync);
+ strictEqual(grip.isGenerator, isGenerator);
+
+ await threadFront.resume();
+ }
+ })
+);
diff --git a/devtools/server/tests/xpcshell/test_objectgrips-25.js b/devtools/server/tests/xpcshell/test_objectgrips-25.js
new file mode 100644
index 0000000000..f80572bb19
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_objectgrips-25.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test object with private properties (preview + enumPrivateProperties)
+
+"use strict";
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+function evalCode(debuggee) {
+ debuggee.eval(
+ function stopMe(obj) {
+ debugger;
+ }.toString()
+ );
+ debuggee.eval(`
+ class MyClass {
+ constructor(name, password) {
+ this.name = name;
+ this.#password = password;
+ }
+
+ #password;
+ #salt = "sEcr3t";
+ #getSaltedPassword() {
+ return this.#password + this.#salt;
+ }
+ }
+
+ stopMe(new MyClass("Susie", "p4$$w0rD"));
+ `);
+}
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const [grip] = packet.frame.arguments;
+
+ let { privateProperties } = grip.preview;
+ strictEqual(
+ privateProperties.length,
+ 2,
+ "There is 2 private properties in the grip preview"
+ );
+ let [password, salt] = privateProperties;
+
+ strictEqual(
+ password.name,
+ "#password",
+ "Got expected name for #password private property in preview"
+ );
+ deepEqual(
+ password.descriptor,
+ {
+ configurable: true,
+ enumerable: false,
+ writable: true,
+ value: "p4$$w0rD",
+ },
+ "Got expected property descriptor for #password in preview"
+ );
+
+ strictEqual(
+ salt.name,
+ "#salt",
+ "Got expected name for #salt private property in preview"
+ );
+ deepEqual(
+ salt.descriptor,
+ {
+ configurable: true,
+ enumerable: false,
+ writable: true,
+ value: "sEcr3t",
+ },
+ "Got expected property descriptor for #salt in preview"
+ );
+
+ const objClient = threadFront.pauseGrip(grip);
+ const iterator = await objClient.enumPrivateProperties();
+ ({ privateProperties } = await iterator.slice(0, iterator.count));
+
+ strictEqual(
+ privateProperties.length,
+ 2,
+ "enumPrivateProperties returned 2 private properties."
+ );
+ [password, salt] = privateProperties;
+
+ strictEqual(
+ password.name,
+ "#password",
+ "Got expected name for #password private property via enumPrivateProperties"
+ );
+ deepEqual(
+ password.descriptor,
+ {
+ configurable: true,
+ enumerable: false,
+ writable: true,
+ value: "p4$$w0rD",
+ },
+ "Got expected property descriptor for #password via enumPrivateProperties"
+ );
+
+ strictEqual(
+ salt.name,
+ "#salt",
+ "Got expected name for #salt private property via enumPrivateProperties"
+ );
+ deepEqual(
+ salt.descriptor,
+ {
+ configurable: true,
+ enumerable: false,
+ writable: true,
+ value: "sEcr3t",
+ },
+ "Got expected property descriptor for #salt via enumPrivateProperties"
+ );
+
+ await threadFront.resume();
+ })
+);
diff --git a/devtools/server/tests/xpcshell/test_objectgrips-fn-apply-01.js b/devtools/server/tests/xpcshell/test_objectgrips-fn-apply-01.js
new file mode 100644
index 0000000000..f576f16a5e
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_objectgrips-fn-apply-01.js
@@ -0,0 +1,117 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const arg1 = packet.frame.arguments[0];
+ Assert.equal(arg1.class, "Object");
+
+ const objectFront = threadFront.pauseGrip(arg1);
+
+ const obj1 = (
+ await objectFront.getPropertyValue("obj1", null)
+ ).value.return.getGrip();
+ const obj2 = (
+ await objectFront.getPropertyValue("obj2", null)
+ ).value.return.getGrip();
+
+ info(`Retrieve "context" function reference`);
+ const context = (await objectFront.getPropertyValue("context", null)).value
+ .return;
+ info(`Retrieve "sum" function reference`);
+ const sum = (await objectFront.getPropertyValue("sum", null)).value.return;
+ info(`Retrieve "error" function reference`);
+ const error = (await objectFront.getPropertyValue("error", null)).value
+ .return;
+
+ assert_response(await context.apply(obj1, [obj1]), {
+ return: "correct context",
+ });
+ assert_response(await context.apply(obj2, [obj2]), {
+ return: "correct context",
+ });
+ assert_response(await context.apply(obj1, [obj2]), {
+ return: "wrong context",
+ });
+ assert_response(await context.apply(obj2, [obj1]), {
+ return: "wrong context",
+ });
+ // eslint-disable-next-line no-useless-call
+ assert_response(await sum.apply(null, [1, 2, 3, 4, 5, 6, 7]), {
+ return: 1 + 2 + 3 + 4 + 5 + 6 + 7,
+ });
+ // eslint-disable-next-line no-useless-call
+ assert_response(await error.apply(null, []), {
+ throw: "an error",
+ });
+
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ debuggee.eval(
+ function stopMe(arg1) {
+ debugger;
+ }.toString()
+ );
+
+ debuggee.eval(`
+ stopMe({
+ obj1: {},
+ obj2: {},
+ context(arg) {
+ return this === arg ? "correct context" : "wrong context";
+ },
+ sum(...parts) {
+ return parts.reduce((acc, v) => acc + v, 0);
+ },
+ error() {
+ throw "an error";
+ },
+ });
+ `);
+}
+
+function assert_response({ value }, expected) {
+ assert_completion(value, expected);
+}
+
+function assert_completion(value, expected) {
+ if (expected && "return" in expected) {
+ assert_value(value.return, expected.return);
+ }
+ if (expected && "throw" in expected) {
+ assert_value(value.throw, expected.throw);
+ }
+ if (!expected) {
+ assert_value(value, expected);
+ }
+}
+
+function assert_value(actual, expected) {
+ Assert.equal(typeof actual, typeof expected);
+
+ if (typeof expected === "object") {
+ // Note: We aren't using deepEqual here because we're only doing a cursory
+ // check of a few properties, not a full comparison of the result, since
+ // the full outputs includes stuff like preview info that we don't need.
+ for (const key of Object.keys(expected)) {
+ assert_value(actual[key], expected[key]);
+ }
+ } else {
+ Assert.equal(actual, expected);
+ }
+}
diff --git a/devtools/server/tests/xpcshell/test_objectgrips-fn-apply-02.js b/devtools/server/tests/xpcshell/test_objectgrips-fn-apply-02.js
new file mode 100644
index 0000000000..743286281c
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_objectgrips-fn-apply-02.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const arg1 = packet.frame.arguments[0];
+ Assert.equal(arg1.class, "Object");
+
+ await threadFront.pauseGrip(arg1).threadGrip();
+ const obj = arg1;
+ await threadFront.resume();
+
+ const objectFront = threadFront.pauseGrip(obj);
+
+ const method = (await objectFront.getPropertyValue("method", null)).value
+ .return;
+
+ const methodCalled = method.apply(obj, []);
+
+ // Ensure that we actually paused at the `debugger;` line.
+ const packet2 = await waitForPause(threadFront);
+ Assert.equal(packet2.frame.where.line, 4);
+ Assert.equal(packet2.frame.where.column, 8);
+
+ await threadFront.resume();
+ await methodCalled;
+ })
+);
+
+function evalCode(debuggee) {
+ debuggee.eval(
+ function stopMe(arg1) {
+ debugger;
+ }.toString()
+ );
+
+ debuggee.eval(`
+ stopMe({
+ method(){
+ debugger;
+ },
+ });
+ `);
+}
diff --git a/devtools/server/tests/xpcshell/test_objectgrips-fn-apply-03.js b/devtools/server/tests/xpcshell/test_objectgrips-fn-apply-03.js
new file mode 100644
index 0000000000..6a3e919661
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_objectgrips-fn-apply-03.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const arg1 = packet.frame.arguments[0];
+ Assert.equal(arg1.class, "Object");
+
+ await threadFront.pauseGrip(arg1).threadGrip();
+ const obj = arg1;
+
+ const objectFront = threadFront.pauseGrip(obj);
+
+ const method = (await objectFront.getPropertyValue("method", null)).value
+ .return;
+
+ try {
+ await method.apply(obj, []);
+ Assert.ok(false, "expected exception");
+ } catch (err) {
+ Assert.ok(!!err.message.match(/debugee object is not callable/));
+ }
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ debuggee.eval(
+ function stopMe(arg1) {
+ debugger;
+ }.toString()
+ );
+
+ debuggee.eval(`
+ stopMe({
+ method: {},
+ });
+ `);
+}
diff --git a/devtools/server/tests/xpcshell/test_objectgrips-nested-promise.js b/devtools/server/tests/xpcshell/test_objectgrips-nested-promise.js
new file mode 100644
index 0000000000..b60b7328c2
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_objectgrips-nested-promise.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const [grip1, grip2] = packet.frame.arguments;
+ strictEqual(grip1.class, "Promise", "promise1 has a promise grip.");
+ strictEqual(grip2.class, "Promise", "promise2 has a promise grip.");
+
+ const objClient1 = threadFront.pauseGrip(grip1);
+ const objClient2 = threadFront.pauseGrip(grip2);
+ const { promiseState: state1 } = await objClient1.getPromiseState();
+ const { promiseState: state2 } = await objClient2.getPromiseState();
+
+ strictEqual(state1.state, "fulfilled", "promise1 was fulfilled.");
+ strictEqual(state1.value, objClient2, "promise1 fulfilled with promise2.");
+ ok(!state1.hasOwnProperty("reason"), "promise1 has no rejection reason.");
+
+ strictEqual(state2.state, "rejected", "promise2 was rejected.");
+ strictEqual(state2.reason, objClient1, "promise2 rejected with promise1.");
+ ok(!state2.hasOwnProperty("value"), "promise2 has no resolution value.");
+
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ debuggee.eval(
+ function stopMe(arg) {
+ debugger;
+ }.toString()
+ );
+
+ debuggee.eval(`
+ var resolve;
+ var promise1 = new Promise(r => {resolve = r});
+ Object.setPrototypeOf(promise1, null);
+ var promise2 = Promise.reject(promise1);
+ promise2.catch(() => {});
+ Object.setPrototypeOf(promise2, null);
+ resolve(promise2);
+ stopMe(promise1, promise2);
+ `);
+}
diff --git a/devtools/server/tests/xpcshell/test_objectgrips-nested-proxy.js b/devtools/server/tests/xpcshell/test_objectgrips-nested-proxy.js
new file mode 100644
index 0000000000..5b0667c055
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_objectgrips-nested-proxy.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const [grip] = packet.frame.arguments;
+ const objClient = threadFront.pauseGrip(grip);
+ const { proxyTarget, proxyHandler } = await objClient.getProxySlots();
+
+ strictEqual(grip.class, "Proxy", "Its a proxy grip.");
+ strictEqual(
+ proxyTarget.getGrip().class,
+ "Proxy",
+ "The target is also a proxy."
+ );
+ strictEqual(
+ proxyHandler.getGrip().class,
+ "Proxy",
+ "The handler is also a proxy."
+ );
+
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ debuggee.eval(
+ function stopMe(arg) {
+ debugger;
+ }.toString()
+ );
+
+ debuggee.eval(`
+ var proxy = new Proxy({}, {});
+ for (let i = 0; i < 1e5; ++i)
+ proxy = new Proxy(proxy, proxy);
+ stopMe(proxy);
+ `);
+}
diff --git a/devtools/server/tests/xpcshell/test_objectgrips-property-value-01.js b/devtools/server/tests/xpcshell/test_objectgrips-property-value-01.js
new file mode 100644
index 0000000000..69da96a741
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_objectgrips-property-value-01.js
@@ -0,0 +1,148 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const arg1 = packet.frame.arguments[0];
+ Assert.equal(arg1.class, "Object");
+
+ const objFront = threadFront.pauseGrip(arg1);
+
+ const expectedValues = {
+ stringProp: {
+ return: "a value",
+ },
+ stringNormal: {
+ return: "a value",
+ },
+ stringAbrupt: {
+ throw: "a value",
+ },
+ objectNormal: {
+ return: {
+ _grip: {
+ type: "object",
+ class: "Object",
+ ownPropertyLength: 1,
+ preview: {
+ kind: "Object",
+ ownProperties: {
+ prop: {
+ value: 4,
+ },
+ },
+ },
+ },
+ },
+ },
+ objectAbrupt: {
+ throw: {
+ _grip: {
+ type: "object",
+ class: "Object",
+ ownPropertyLength: 1,
+ preview: {
+ kind: "Object",
+ ownProperties: {
+ prop: {
+ value: 4,
+ },
+ },
+ },
+ },
+ },
+ },
+ context: {
+ return: "correct context",
+ },
+ method: {
+ return: {
+ _grip: {
+ type: "object",
+ class: "Function",
+ name: "method",
+ },
+ },
+ },
+ };
+
+ for (const [key, expected] of Object.entries(expectedValues)) {
+ const { value } = await objFront.getPropertyValue(key, null);
+ assert_completion(value, expected);
+ }
+
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ debuggee.eval(
+ function stopMe(arg1) {
+ debugger;
+ }.toString()
+ );
+
+ debuggee.eval(`
+ var obj = {
+ stringProp: "a value",
+ get stringNormal(){
+ return "a value";
+ },
+ get stringAbrupt() {
+ throw "a value";
+ },
+ get objectNormal() {
+ return { prop: 4 };
+ },
+ get objectAbrupt() {
+ throw { prop: 4 };
+ },
+ get context(){
+ return this === obj ? "correct context" : "wrong context";
+ },
+ method() {
+ return "a value";
+ },
+ };
+ stopMe(obj);
+ `);
+}
+
+function assert_completion(value, expected) {
+ if (expected && "return" in expected) {
+ assert_value(value.return, expected.return);
+ }
+ if (expected && "throw" in expected) {
+ assert_value(value.throw, expected.throw);
+ }
+ if (!expected) {
+ assert_value(value, expected);
+ }
+}
+
+function assert_value(actual, expected) {
+ Assert.equal(typeof actual, typeof expected);
+
+ if (typeof expected === "object") {
+ // Note: We aren't using deepEqual here because we're only doing a cursory
+ // check of a few properties, not a full comparison of the result, since
+ // the full outputs includes stuff like preview info that we don't need.
+ for (const key of Object.keys(expected)) {
+ assert_value(actual[key], expected[key]);
+ }
+ } else {
+ Assert.equal(actual, expected);
+ }
+}
diff --git a/devtools/server/tests/xpcshell/test_objectgrips-property-value-02.js b/devtools/server/tests/xpcshell/test_objectgrips-property-value-02.js
new file mode 100644
index 0000000000..bc7337128c
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_objectgrips-property-value-02.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const arg1 = packet.frame.arguments[0];
+ Assert.equal(arg1.class, "Object");
+
+ const obj = threadFront.pauseGrip(arg1);
+ await obj.threadGrip();
+
+ const objClient = obj;
+ await threadFront.resume();
+
+ const objClientCalled = objClient.getPropertyValue("prop", null);
+
+ // Ensure that we actually paused at the `debugger;` line.
+ const packet2 = await waitForPause(threadFront);
+ Assert.equal(packet2.frame.where.line, 4);
+ Assert.equal(packet2.frame.where.column, 8);
+
+ await threadFront.resume();
+ await objClientCalled;
+ })
+);
+
+function evalCode(debuggee) {
+ debuggee.eval(
+ function stopMe(arg1) {
+ debugger;
+ }.toString()
+ );
+
+ debuggee.eval(`
+ stopMe({
+ get prop(){
+ debugger;
+ },
+ });
+ `);
+}
diff --git a/devtools/server/tests/xpcshell/test_objectgrips-property-value-03.js b/devtools/server/tests/xpcshell/test_objectgrips-property-value-03.js
new file mode 100644
index 0000000000..e9b130db79
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_objectgrips-property-value-03.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const { frame } = packet;
+ try {
+ const grips = frame.arguments;
+ const objClient = threadFront.pauseGrip(grips[0]);
+ const classes = [
+ "Object",
+ "Object",
+ "Array",
+ "Boolean",
+ "Number",
+ "String",
+ ];
+ for (const [i, grip] of grips.entries()) {
+ Assert.equal(grip.class, classes[i]);
+ await check_getter(objClient, grip.actor, i);
+ }
+ await check_getter(objClient, null, 0);
+ await check_getter(objClient, "invalid receiver actorId", 0);
+ } finally {
+ await threadFront.resume();
+ }
+ })
+);
+
+function evalCode(debuggee) {
+ debuggee.eval(
+ function stopMe() {
+ debugger;
+ }.toString()
+ );
+
+ debuggee.eval(`
+ var obj = {
+ get getter() {
+ return objects.indexOf(this);
+ },
+ };
+ var objects = [obj, {}, [], new Boolean(), new Number(), new String()];
+ stopMe(...objects);
+ `);
+}
+
+async function check_getter(objClient, receiverId, expected) {
+ const { value } = await objClient.getPropertyValue("getter", receiverId);
+ Assert.equal(value.return, expected);
+}
diff --git a/devtools/server/tests/xpcshell/test_objectgrips-sparse-array.js b/devtools/server/tests/xpcshell/test_objectgrips-sparse-array.js
new file mode 100644
index 0000000000..76a6b32f4b
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_objectgrips-sparse-array.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const [grip] = packet.frame.arguments;
+ await threadFront.resume();
+
+ strictEqual(grip.class, "Array", "The grip has an Array class");
+
+ const { items } = grip.preview;
+ strictEqual(items[0], null, "The empty slot has null as grip preview");
+ deepEqual(
+ items[1],
+ { type: "undefined" },
+ "The undefined value has grip value of type undefined"
+ );
+ })
+);
+
+function evalCode(debuggee) {
+ debuggee.eval(
+ function stopMe(arr) {
+ debugger;
+ }.toString()
+ );
+ debuggee.eval("stopMe([, undefined])");
+}
diff --git a/devtools/server/tests/xpcshell/test_pause_exceptions-01.js b/devtools/server/tests/xpcshell/test_pause_exceptions-01.js
new file mode 100644
index 0000000000..74bbae55c3
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_pause_exceptions-01.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that setting pauseOnExceptions to true will cause the debuggee to pause
+ * when an exception is thrown.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee, commands }) => {
+ await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+
+ await commands.threadConfigurationCommand.updateConfiguration({
+ pauseOnExceptions: true,
+ ignoreCaughtExceptions: false,
+ });
+ const packet = await resumeAndWaitForPause(threadFront);
+ Assert.equal(packet.why.type, "exception");
+ Assert.equal(packet.why.exception, 42);
+
+ await threadFront.resume();
+ })
+);
+
+function evaluateTestCode(debuggee) {
+ /* eslint-disable no-throw-literal */
+ // prettier-ignore
+ debuggee.eval("(" + function () {
+ function stopMe() {
+ debugger;
+ throw 42;
+ }
+ try {
+ stopMe();
+ } catch (e) {}
+ } + ")()");
+ /* eslint-enable no-throw-literal */
+}
diff --git a/devtools/server/tests/xpcshell/test_pause_exceptions-02.js b/devtools/server/tests/xpcshell/test_pause_exceptions-02.js
new file mode 100644
index 0000000000..00631b071f
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_pause_exceptions-02.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that setting pauseOnExceptions to true when the debugger isn't in a
+ * paused state will not cause the debuggee to pause when an exception is thrown.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee, commands }) => {
+ await commands.threadConfigurationCommand.updateConfiguration({
+ pauseOnExceptions: true,
+ ignoreCaughtExceptions: false,
+ });
+
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+
+ Assert.equal(packet.why.type, "exception");
+ Assert.equal(packet.why.exception, 42);
+ await threadFront.resume();
+ })
+);
+
+function evaluateTestCode(debuggee) {
+ /* eslint-disable no-throw-literal */
+ // prettier-ignore
+ debuggee.eval("(" + function () { // 1
+ function stopMe() { // 2
+ throw 42; // 3
+ } // 4
+ try { // 5
+ stopMe(); // 6
+ } catch (e) {} // 7
+ } + ")()");
+}
diff --git a/devtools/server/tests/xpcshell/test_pause_exceptions-03.js b/devtools/server/tests/xpcshell/test_pause_exceptions-03.js
new file mode 100644
index 0000000000..4fb13f4cf9
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_pause_exceptions-03.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that setting pauseOnExceptions to true will cause the debuggee to pause
+ * when an exception is thrown.
+ */
+
+add_task(
+ threadFrontTest(
+ async ({ threadFront, debuggee, commands }) => {
+ await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+
+ await commands.threadConfigurationCommand.updateConfiguration({
+ pauseOnExceptions: true,
+ ignoreCaughtExceptions: false,
+ });
+ await resume(threadFront);
+ const paused = await waitForPause(threadFront);
+ Assert.equal(paused.why.type, "exception");
+ equal(paused.frame.where.line, 4, "paused at throw");
+
+ await resume(threadFront);
+ },
+ {
+ // Bug 1508289, exception tests fails in worker scope
+ doNotRunWorker: true,
+ }
+ )
+);
+
+function evaluateTestCode(debuggee) {
+ // prettier-ignore
+ Cu.evalInSandbox(
+ ` // 1
+ function stopMe() { // 2
+ debugger; // 3
+ throw 42; // 4
+ } // 5
+ try { // 6
+ stopMe(); // 7
+ } catch (e) {}`, // 8
+ debuggee,
+ "1.8",
+ "test_pause_exceptions-03.js",
+ 1
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_pause_exceptions-04.js b/devtools/server/tests/xpcshell/test_pause_exceptions-04.js
new file mode 100644
index 0000000000..6246b112e0
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_pause_exceptions-04.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { waitForTick } = require("resource://devtools/shared/DevToolsUtils.js");
+
+/**
+ * Test that setting pauseOnExceptions to true and then to false will not cause
+ * the debuggee to pause when an exception is thrown.
+ */
+
+add_task(
+ threadFrontTest(
+ async ({ threadFront, client, debuggee, commands }) => {
+ let onResume = null;
+ let packet = null;
+
+ threadFront.once("paused", function (pkt) {
+ packet = pkt;
+ onResume = threadFront.resume();
+ });
+
+ await commands.threadConfigurationCommand.updateConfiguration({
+ pauseOnExceptions: true,
+ ignoreCaughtExceptions: true,
+ });
+
+ await evaluateTestCode(debuggee, "42");
+
+ await onResume;
+
+ Assert.equal(!!packet, true);
+ Assert.equal(packet.why.type, "exception");
+ Assert.equal(packet.why.exception, "42");
+ packet = null;
+
+ threadFront.once("paused", function (pkt) {
+ packet = pkt;
+ onResume = threadFront.resume();
+ });
+
+ await commands.threadConfigurationCommand.updateConfiguration({
+ pauseOnExceptions: false,
+ ignoreCaughtExceptions: true,
+ });
+
+ await evaluateTestCode(debuggee, "43");
+
+ // Test that the paused listener callback hasn't been called
+ // on the thrown error from dontStopMe()
+ Assert.equal(!!packet, false);
+
+ await commands.threadConfigurationCommand.updateConfiguration({
+ pauseOnExceptions: true,
+ ignoreCaughtExceptions: true,
+ });
+
+ await evaluateTestCode(debuggee, "44");
+
+ await onResume;
+
+ // Test that the paused listener callback has been called
+ // on the thrown error from stopMeAgain()
+ Assert.equal(!!packet, true);
+ Assert.equal(packet.why.type, "exception");
+ Assert.equal(packet.why.exception, "44");
+ },
+ {
+ // Bug 1508289, exception tests fails in worker scope
+ doNotRunWorker: true,
+ }
+ )
+);
+
+async function evaluateTestCode(debuggee, throwValue) {
+ await waitForTick();
+ try {
+ // prettier-ignore
+ Cu.evalInSandbox(
+ ` // 1
+ function stopMeAgain() { // 2
+ throw ${throwValue}; // 3
+ } // 4
+ stopMeAgain(); // 5
+ `, // 6
+ debuggee,
+ "1.8",
+ "test_pause_exceptions-04.js",
+ 1
+ );
+ } catch (e) {}
+}
diff --git a/devtools/server/tests/xpcshell/test_pauselifetime-01.js b/devtools/server/tests/xpcshell/test_pauselifetime-01.js
new file mode 100644
index 0000000000..db20a02521
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_pauselifetime-01.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that pause-lifetime grips go away correctly after a resume.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee, client }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+
+ const pauseActor = packet.actor;
+
+ // Make a bogus request to the pause-lifetime actor. Should get
+ // unrecognized-packet-type (and not no-such-actor).
+ try {
+ await client.request({ to: pauseActor, type: "bogusRequest" });
+ ok(false, "bogusRequest should throw");
+ } catch (e) {
+ ok(true, "bogusRequest thrown");
+ Assert.equal(e.error, "unrecognizedPacketType");
+ }
+
+ await threadFront.resume();
+
+ // Now that we've resumed, should get no-such-actor for the
+ // same request.
+ try {
+ await client.request({ to: pauseActor, type: "bogusRequest" });
+ ok(false, "bogusRequest should throw");
+ } catch (e) {
+ ok(true, "bogusRequest thrown");
+ Assert.equal(e.error, "noSuchActor");
+ }
+ })
+);
+
+function evaluateTestCode(debuggee) {
+ debuggee.eval(
+ "(" +
+ function () {
+ function stopMe() {
+ debugger;
+ }
+ stopMe();
+ } +
+ ")()"
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_pauselifetime-02.js b/devtools/server/tests/xpcshell/test_pauselifetime-02.js
new file mode 100644
index 0000000000..e936df6177
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_pauselifetime-02.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that pause-lifetime grips go away correctly after a resume.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee, client }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+
+ const args = packet.frame.arguments;
+ const objActor = args[0].actor;
+ Assert.equal(args[0].class, "Object");
+ Assert.ok(!!objActor);
+
+ // Make a bogus request to the grip actor. Should get
+ // unrecognized-packet-type (and not no-such-actor).
+ try {
+ await client.request({ to: objActor, type: "bogusRequest" });
+ ok(false, "bogusRequest should throw");
+ } catch (e) {
+ ok(true, "bogusRequest thrown");
+ Assert.equal(e.error, "unrecognizedPacketType");
+ }
+
+ await threadFront.resume();
+
+ // Now that we've resumed, should get no-such-actor for the
+ // same request.
+ try {
+ await client.request({ to: objActor, type: "bogusRequest" });
+ ok(false, "bogusRequest should throw");
+ } catch (e) {
+ ok(true, "bogusRequest thrown");
+ Assert.equal(e.error, "noSuchActor");
+ }
+ })
+);
+
+function evaluateTestCode(debuggee) {
+ debuggee.eval(
+ "(" +
+ function () {
+ function stopMe(obj) {
+ debugger;
+ }
+ stopMe({ foo: "bar" });
+ } +
+ ")()"
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_pauselifetime-03.js b/devtools/server/tests/xpcshell/test_pauselifetime-03.js
new file mode 100644
index 0000000000..558ac8b910
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_pauselifetime-03.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that pause-lifetime grip clients are marked invalid after a resume.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee, client }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+
+ const args = packet.frame.arguments;
+ const objActor = args[0].actor;
+ Assert.equal(args[0].class, "Object");
+ Assert.ok(!!objActor);
+
+ const objectFront = threadFront.pauseGrip(args[0]);
+ Assert.ok(objectFront.valid);
+
+ // Make a bogus request to the grip actor. Should get
+ // unrecognized-packet-type (and not no-such-actor).
+ try {
+ const objFront = client.getFrontByID(objActor);
+ await objFront.request({ to: objActor, type: "bogusRequest" });
+ ok(false, "bogusRequest should throw");
+ } catch (e) {
+ ok(true, "bogusRequest thrown");
+ Assert.ok(!!e.message.match(/unrecognizedPacketType/));
+ }
+ Assert.ok(objectFront.valid);
+
+ await threadFront.resume();
+
+ // Now that we've resumed, should get no-such-actor for the
+ // same request.
+ try {
+ const objFront = client.getFrontByID(objActor);
+ await objFront.request({ to: objActor, type: "bogusRequest" });
+ ok(false, "bogusRequest should throw");
+ } catch (e) {
+ ok(true, "bogusRequest thrown");
+ Assert.ok(!!e.message.match(/noSuchActor/));
+ }
+ Assert.ok(!objectFront.valid);
+ })
+);
+
+function evaluateTestCode(debuggee) {
+ debuggee.eval(
+ "(" +
+ function () {
+ function stopMe(obj) {
+ debugger;
+ }
+ stopMe({ foo: "bar" });
+ } +
+ ")()"
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_pauselifetime-04.js b/devtools/server/tests/xpcshell/test_pauselifetime-04.js
new file mode 100644
index 0000000000..7d226260f0
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_pauselifetime-04.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that requesting a pause actor for the same value multiple
+ * times returns the same actor.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+
+ const args = packet.frame.arguments;
+ const objActor1 = args[0].actor;
+
+ const response = await threadFront.getFrames(0, 1);
+ const frame = response.frames[0];
+ Assert.equal(objActor1, frame.arguments[0].actor);
+
+ await threadFront.resume();
+ })
+);
+
+function evaluateTestCode(debuggee) {
+ debuggee.eval(
+ "(" +
+ function () {
+ function stopMe(obj) {
+ debugger;
+ }
+ stopMe({ foo: "bar" });
+ } +
+ ")()"
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_promise_state-01.js b/devtools/server/tests/xpcshell/test_promise_state-01.js
new file mode 100644
index 0000000000..d02b64a67e
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_promise_state-01.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable max-nested-callbacks */
+
+"use strict";
+
+/**
+ * Test that the preview in a Promise's grip is correct when the Promise is
+ * pending.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const environment = await packet.frame.getEnvironment();
+ const grip = environment.bindings.variables.p.value;
+
+ ok(grip.preview);
+ equal(grip.class, "Promise");
+ equal(grip.preview.ownProperties["<state>"].value, "pending");
+
+ const objClient = threadFront.pauseGrip(grip);
+ const { promiseState } = await objClient.getPromiseState();
+ equal(promiseState.state, "pending");
+ })
+);
+
+function evalCode(debuggee) {
+ /* eslint-disable mozilla/var-only-at-top-level, no-unused-vars */
+ // prettier-ignore
+ Cu.evalInSandbox(
+ "doTest();\n" +
+ function doTest() {
+ var p = new Promise(function () {});
+ debugger;
+ },
+ debuggee
+ );
+ /* eslint-enable mozilla/var-only-at-top-level, no-unused-vars */
+}
diff --git a/devtools/server/tests/xpcshell/test_promise_state-02.js b/devtools/server/tests/xpcshell/test_promise_state-02.js
new file mode 100644
index 0000000000..e1219f545c
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_promise_state-02.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable max-nested-callbacks */
+
+"use strict";
+
+/**
+ * Test that the preview in a Promise's grip is correct when the Promise is
+ * fulfilled.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const environment = await packet.frame.getEnvironment();
+ const grip = environment.bindings.variables.p.value;
+
+ ok(grip.preview);
+ equal(grip.class, "Promise");
+ equal(grip.preview.ownProperties["<state>"].value, "fulfilled");
+ equal(
+ grip.preview.ownProperties["<value>"].value.actorID,
+ packet.frame.arguments[0].actorID,
+ "The promise's fulfilled state value in the preview should be the same " +
+ "value passed to the then function"
+ );
+
+ const objClient = threadFront.pauseGrip(grip);
+ const { promiseState } = await objClient.getPromiseState();
+ equal(promiseState.state, "fulfilled");
+ equal(
+ promiseState.value.getGrip().actorID,
+ packet.frame.arguments[0].actorID,
+ "The promise's fulfilled state value in getPromiseState() should be " +
+ "the same value passed to the then function"
+ );
+ })
+);
+
+function evalCode(debuggee) {
+ /* eslint-disable mozilla/var-only-at-top-level, no-unused-vars */
+ // prettier-ignore
+ Cu.evalInSandbox(
+ "doTest();\n" +
+ function doTest() {
+ var resolved = Promise.resolve({});
+ resolved.then(() => {
+ var p = resolved;
+ debugger;
+ });
+ },
+ debuggee
+ );
+ /* eslint-enable mozilla/var-only-at-top-level, no-unused-vars */
+}
diff --git a/devtools/server/tests/xpcshell/test_promise_state-03.js b/devtools/server/tests/xpcshell/test_promise_state-03.js
new file mode 100644
index 0000000000..8ec1fa3717
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_promise_state-03.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable max-nested-callbacks */
+
+"use strict";
+
+/**
+ * Test that the preview in a Promise's grip is correct when the Promise is
+ * rejected.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ const environment = await packet.frame.getEnvironment();
+ const grip = environment.bindings.variables.p.value;
+ ok(grip.preview);
+ equal(grip.class, "Promise");
+ equal(grip.preview.ownProperties["<state>"].value, "rejected");
+ equal(
+ grip.preview.ownProperties["<reason>"].value.actorID,
+ packet.frame.arguments[0].actorID,
+ "The promise's rejected state reason in the preview should be the same " +
+ "value passed to the then function"
+ );
+
+ const objClient = threadFront.pauseGrip(grip);
+ const { promiseState } = await objClient.getPromiseState();
+ equal(promiseState.state, "rejected");
+ equal(
+ promiseState.reason.getGrip().actorID,
+ packet.frame.arguments[0].actorID,
+ "The promise's rejected state value in getPromiseState() should be " +
+ "the same value passed to the then function"
+ );
+ })
+);
+
+function evalCode(debuggee) {
+ /* eslint-disable mozilla/var-only-at-top-level, no-unused-vars */
+ // prettier-ignore
+ Cu.evalInSandbox(
+ "doTest();\n" +
+ function doTest() {
+ var resolved = Promise.reject(new Error("uh oh"));
+ resolved.catch(() => {
+ var p = resolved;
+ debugger;
+ });
+ },
+ debuggee
+ );
+ /* eslint-enable mozilla/var-only-at-top-level, no-unused-vars */
+}
diff --git a/devtools/server/tests/xpcshell/test_promises_run_to_completion.js b/devtools/server/tests/xpcshell/test_promises_run_to_completion.js
new file mode 100644
index 0000000000..4d1e8745fe
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_promises_run_to_completion.js
@@ -0,0 +1,132 @@
+// Bug 1145201: Promise then-handlers can still be executed while the debugger is paused.
+//
+// When a promise is resolved, for each of its callbacks, a microtask is queued
+// to run the callback. At various points, the HTML spec says the browser must
+// "perform a microtask checkpoint", which means to draw microtasks from the
+// queue and run them, until the queue is empty.
+//
+// The HTML spec is careful to perform a microtask checkpoint directly after
+// each invocation of an event handler or DOM callback, so that code using
+// promises can trust that its promise callbacks run promptly, in a
+// deterministic order, without DOM events or other outside influences
+// intervening.
+//
+// When the JavaScript debugger interrupts the execution of debuggee content
+// code, it naturally must process events for its own user interface and promise
+// callbacks. However, it must not run any debuggee microtasks. The debuggee has
+// been interrupted in the midst of executing some other code, and the
+// JavaScript spec promises developers: "Once execution of a Job is initiated,
+// the Job always executes to completion. No other Job may be initiated until
+// the currently running Job completes." [1] This promise would be broken if the
+// debugger's own event processing ran debuggee microtasks during the
+// interruption.
+//
+// Looking at things from the other side, a microtask checkpoint must be
+// performed before returning from a debugger callback, rather than being put
+// off until the debuggee performs its next microtask checkpoint, so that
+// debugger microtasks are not interleaved with debuggee microtasks. A debuggee
+// microtask could hit a breakpoint or otherwise re-enter the debugger, which
+// might be quite surprised to see a new debugger callback begin before its
+// previous promise callbacks could finish.
+//
+// [1]: https://www.ecma-international.org/ecma-262/9.0/index.html#sec-jobs-and-job-queues
+
+"use strict";
+
+const Debugger = require("Debugger");
+
+function test_promises_run_to_completion() {
+ const g = createTestGlobal(
+ "test global for test_promises_run_to_completion.js"
+ );
+ const dbg = new Debugger(g);
+ g.Assert = Assert;
+ const log = [""];
+ g.log = log;
+
+ dbg.onDebuggerStatement = function handleDebuggerStatement(frame) {
+ dbg.onDebuggerStatement = undefined;
+
+ // Exercise the promise machinery: resolve a promise and perform a microtask
+ // queue. When called from a debugger hook, the debuggee's microtasks should not
+ // run.
+ log[0] += "debug-handler(";
+ Promise.resolve(42).then(v => {
+ Assert.equal(
+ v,
+ 42,
+ "debugger callback promise handler got the right value"
+ );
+ log[0] += "debug-react";
+ });
+ log[0] += "(";
+ force_microtask_checkpoint();
+ log[0] += ")";
+
+ Promise.resolve(42).then(v => {
+ // The microtask running this callback should be handled as we leave the
+ // onDebuggerStatement Debugger callback, and should not be interleaved
+ // with debuggee microtasks.
+ log[0] += "(trailing)";
+ });
+
+ log[0] += ")";
+ };
+
+ // Evaluate some debuggee code that resolves a promise, and then enters the debugger.
+ Cu.evalInSandbox(
+ `
+ log[0] += "eval(";
+ Promise.resolve(42).then(function debuggeePromiseCallback(v) {
+ Assert.equal(v, 42, "debuggee promise handler got the right value");
+ // Debugger microtask checkpoints must not run debuggee microtasks, so
+ // this callback should run at the next microtask checkpoint *not*
+ // performed by the debugger.
+ log[0] += "eval-react";
+ });
+
+ log[0] += "debugger(";
+ debugger;
+ log[0] += "))";
+ `,
+ g
+ );
+
+ // Let other microtasks run. This should run the debuggee's promise callback.
+ log[0] += "final(";
+ force_microtask_checkpoint();
+ log[0] += ")";
+
+ Assert.equal(
+ log[0],
+ `\
+eval(\
+debugger(\
+debug-handler(\
+(debug-react)\
+)\
+(trailing)\
+))\
+final(\
+eval-react\
+)`,
+ "microtasks ran as expected"
+ );
+
+ run_next_test();
+}
+
+function force_microtask_checkpoint() {
+ // Services.tm.spinEventLoopUntilEmpty only performs a microtask checkpoint if
+ // there is actually an event to run. So make one up.
+ let ran = false;
+ Services.tm.dispatchToMainThread(() => {
+ ran = true;
+ });
+ Services.tm.spinEventLoopUntil(
+ "Test(test_promises_run_to_completion.js:force_microtask_checkpoint)",
+ () => ran
+ );
+}
+
+add_test(test_promises_run_to_completion);
diff --git a/devtools/server/tests/xpcshell/test_register_actor.js b/devtools/server/tests/xpcshell/test_register_actor.js
new file mode 100644
index 0000000000..f38ab73572
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_register_actor.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function run_test() {
+ // Allow incoming connections.
+ DevToolsServer.keepAlive = true;
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+
+ add_test(test_lazy_api);
+ add_test(manual_remove);
+ add_test(cleanup);
+ run_next_test();
+}
+
+// Bug 988237: Test the new lazy actor actor-register
+function test_lazy_api() {
+ let isActorLoaded = false;
+ let isActorInstantiated = false;
+ function onActorEvent(subject, topic, data) {
+ if (data == "loaded") {
+ isActorLoaded = true;
+ } else if (data == "instantiated") {
+ isActorInstantiated = true;
+ }
+ }
+ Services.obs.addObserver(onActorEvent, "actor");
+ ActorRegistry.registerModule("xpcshell-test/registertestactors-lazy", {
+ prefix: "lazy",
+ constructor: "LazyActor",
+ type: { global: true, target: true },
+ });
+ // The actor is immediatly registered, but not loaded
+ Assert.ok(
+ ActorRegistry.targetScopedActorFactories.hasOwnProperty("lazyActor")
+ );
+ Assert.ok(ActorRegistry.globalActorFactories.hasOwnProperty("lazyActor"));
+ Assert.ok(!isActorLoaded);
+ Assert.ok(!isActorInstantiated);
+
+ const client = new DevToolsClient(DevToolsServer.connectPipe());
+ client.connect().then(function onConnect() {
+ client.mainRoot.rootForm.then(onRootForm);
+ });
+ function onRootForm(response) {
+ // On rootForm, the actor is still not loaded,
+ // but we can see its name in the list of available actors
+ Assert.ok(!isActorLoaded);
+ Assert.ok(!isActorInstantiated);
+ Assert.ok("lazyActor" in response);
+
+ const { LazyFront } = require("xpcshell-test/registertestactors-lazy");
+ const front = new LazyFront(client);
+ // As this Front isn't instantiated by protocol.js, we have to manually
+ // set its actor ID and manage it:
+ front.actorID = response.lazyActor;
+ client.addActorPool(front);
+ front.manage(front);
+
+ front.hello().then(onRequest);
+ }
+ function onRequest(response) {
+ Assert.equal(response, "world");
+
+ // Finally, the actor is loaded on the first request being made to it
+ Assert.ok(isActorLoaded);
+ Assert.ok(isActorInstantiated);
+
+ Services.obs.removeObserver(onActorEvent, "actor");
+ client.close().then(() => run_next_test());
+ }
+}
+
+function manual_remove() {
+ Assert.ok(ActorRegistry.globalActorFactories.hasOwnProperty("lazyActor"));
+ ActorRegistry.removeGlobalActor("lazyActor");
+ Assert.ok(!ActorRegistry.globalActorFactories.hasOwnProperty("lazyActor"));
+
+ run_next_test();
+}
+
+function cleanup() {
+ DevToolsServer.destroy();
+
+ // Check that all actors are unregistered on server destruction
+ Assert.ok(
+ !ActorRegistry.targetScopedActorFactories.hasOwnProperty("lazyActor")
+ );
+ Assert.ok(!ActorRegistry.globalActorFactories.hasOwnProperty("lazyActor"));
+
+ run_next_test();
+}
diff --git a/devtools/server/tests/xpcshell/test_requestTypes.js b/devtools/server/tests/xpcshell/test_requestTypes.js
new file mode 100644
index 0000000000..8787ae5f85
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_requestTypes.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { rootSpec } = require("resource://devtools/shared/specs/root.js");
+const {
+ generateRequestTypes,
+} = require("resource://devtools/shared/protocol/Actor.js");
+
+add_task(async function () {
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+
+ const client = new DevToolsClient(DevToolsServer.connectPipe());
+ await client.connect();
+
+ const response = await client.mainRoot.requestTypes();
+ const expectedRequestTypes = Object.keys(generateRequestTypes(rootSpec));
+
+ Assert.ok(Array.isArray(response.requestTypes));
+ Assert.equal(
+ JSON.stringify(response.requestTypes),
+ JSON.stringify(expectedRequestTypes)
+ );
+
+ await client.close();
+});
diff --git a/devtools/server/tests/xpcshell/test_restartFrame-01.js b/devtools/server/tests/xpcshell/test_restartFrame-01.js
new file mode 100644
index 0000000000..cb13ae2d7e
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_restartFrame-01.js
@@ -0,0 +1,118 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check restarting a frame and stepping out of the
+ * restarted frame.
+ */
+
+async function testFinish({ threadFront, devToolsClient }) {
+ await close(devToolsClient);
+
+ do_test_finished();
+}
+
+async function invokeAndPause({ global, threadFront }, expression) {
+ return executeOnNextTickAndWaitForPause(
+ () => Cu.evalInSandbox(expression, global),
+ threadFront
+ );
+}
+
+async function steps(threadFront, sequence) {
+ const locations = [];
+ for (const cmd of sequence) {
+ const packet = await step(threadFront, cmd);
+ locations.push(getPauseLocation(packet));
+ }
+ return locations;
+}
+
+async function step(threadFront, cmd) {
+ return cmd(threadFront);
+}
+
+function getPauseLocation(packet) {
+ const { line, column } = packet.frame.where;
+ return { line, column };
+}
+
+async function restartFrame0(dbg, func, expectedLocation) {
+ const { threadFront } = dbg;
+
+ info("pause and step into a()");
+ await invokeAndPause(dbg, `${func}()`);
+ await steps(threadFront, [stepOver, stepIn]);
+
+ info("restart the youngest frame a()");
+ const { frames } = await threadFront.frames(0, 5);
+ const frameActorID = frames[0].actorID;
+ const packet = await restartFrame(threadFront, frameActorID);
+
+ deepEqual(
+ getPauseLocation(packet),
+ expectedLocation,
+ "pause location in the restarted frame a()"
+ );
+}
+
+async function restartFrame1(dbg, func, expectedLocation) {
+ const { threadFront } = dbg;
+
+ info("pause and step into b()");
+ await invokeAndPause(dbg, `${func}()`);
+ await steps(threadFront, [stepOver, stepIn, stepIn]);
+
+ info("restart the frame with index 1");
+ const { frames } = await threadFront.frames(0, 5);
+ const frameActorID = frames[1].actorID;
+ const packet = await restartFrame(threadFront, frameActorID);
+
+ deepEqual(
+ getPauseLocation(packet),
+ expectedLocation,
+ "pause location in the restarted frame c()"
+ );
+}
+
+async function stepOutRestartedFrame(
+ dbg,
+ restartedFrameName,
+ expectedLocation,
+ expectedCallstackLength
+) {
+ const { threadFront } = dbg;
+ const { frames } = await threadFront.frames(0, 5);
+
+ Assert.equal(
+ frames.length,
+ expectedCallstackLength,
+ `the callstack length after restarting frame ${restartedFrameName}()`
+ );
+
+ info(`step out of the restarted frame ${restartedFrameName}()`);
+ const frameActorID = frames[0].actorID;
+ const packet = await stepOut(threadFront, frameActorID);
+
+ deepEqual(getPauseLocation(packet), expectedLocation, `step out location`);
+}
+
+function run_test() {
+ return (async function () {
+ const dbg = await setupTestFromUrl("stepping.js");
+
+ info(`Test restarting the youngest frame`);
+ await restartFrame0(dbg, "arithmetic", { line: 7, column: 2 });
+ await stepOutRestartedFrame(dbg, "a", { line: 16, column: 8 }, 3);
+ await dbg.threadFront.resume();
+
+ info(`Test restarting the frame with the index 1`);
+ await restartFrame1(dbg, "nested", { line: 30, column: 2 });
+ await stepOutRestartedFrame(dbg, "c", { line: 36, column: 0 }, 3);
+ await dbg.threadFront.resume();
+
+ await testFinish(dbg);
+ })();
+}
diff --git a/devtools/server/tests/xpcshell/test_safe-getter.js b/devtools/server/tests/xpcshell/test_safe-getter.js
new file mode 100644
index 0000000000..65bf3414ea
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_safe-getter.js
@@ -0,0 +1,54 @@
+/* eslint-disable strict */
+function run_test() {
+ Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+ });
+ const { addDebuggerToGlobal } = ChromeUtils.importESModule(
+ "resource://gre/modules/jsdebugger.sys.mjs"
+ );
+ addDebuggerToGlobal(globalThis);
+ const g = createTestGlobal("test", {
+ wantGlobalProperties: ["ChromeUtils"],
+ });
+ const dbg = new Debugger();
+ const gw = dbg.addDebuggee(g);
+
+ g.eval(`
+ // This is not a CCW.
+ Object.defineProperty(this, "bar", {
+ get: function() { return "bar"; },
+ configurable: true,
+ enumerable: true
+ });
+
+ const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+ );
+
+ // This is a CCW.
+ XPCOMUtils.defineLazyScriptGetter(
+ this, "foo", "chrome://global/content/viewZoomOverlay.js");
+ `);
+
+ // Neither scripted getter should be considered safe.
+ assert(!DevToolsUtils.hasSafeGetter(gw.getOwnPropertyDescriptor("bar")));
+ assert(!DevToolsUtils.hasSafeGetter(gw.getOwnPropertyDescriptor("foo")));
+
+ // Create an object in a less privileged sandbox.
+ const obj = gw.makeDebuggeeValue(
+ Cu.waiveXrays(
+ Cu.Sandbox(null).eval(`
+ Object.defineProperty({}, "bar", {
+ get: function() { return "bar"; },
+ configurable: true,
+ enumerable: true
+ });
+ `)
+ )
+ );
+
+ // After waiving Xrays, the object has 2 wrappers. Both must be removed
+ // in order to detect that the getter is not safe.
+ assert(!DevToolsUtils.hasSafeGetter(obj.getOwnPropertyDescriptor("bar")));
+}
diff --git a/devtools/server/tests/xpcshell/test_sessionDataHelpers.js b/devtools/server/tests/xpcshell/test_sessionDataHelpers.js
new file mode 100644
index 0000000000..e0dcc3b21b
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_sessionDataHelpers.js
@@ -0,0 +1,124 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test SessionDataHelpers.
+ */
+
+"use strict";
+
+const { SessionDataHelpers } = ChromeUtils.import(
+ "resource://devtools/server/actors/watcher/SessionDataHelpers.jsm"
+);
+const { SUPPORTED_DATA } = SessionDataHelpers;
+const { TARGETS } = SUPPORTED_DATA;
+
+function run_test() {
+ const sessionData = {
+ [TARGETS]: [],
+ };
+
+ info("Test adding a new entry");
+ SessionDataHelpers.addOrSetSessionDataEntry(
+ sessionData,
+ TARGETS,
+ ["frame", "worker"],
+ "add"
+ );
+ deepEqual(
+ sessionData[TARGETS],
+ ["frame", "worker"],
+ "the two elements were added"
+ );
+
+ info("Test adding a duplicated entry");
+ SessionDataHelpers.addOrSetSessionDataEntry(
+ sessionData,
+ TARGETS,
+ ["frame"],
+ "add"
+ );
+ deepEqual(
+ sessionData[TARGETS],
+ ["frame", "worker"],
+ "addOrSetSessionDataEntry ignore duplicates"
+ );
+
+ SessionDataHelpers.addOrSetSessionDataEntry(
+ sessionData,
+ TARGETS,
+ ["process"],
+ "add"
+ );
+ deepEqual(
+ sessionData[TARGETS],
+ ["frame", "worker", "process"],
+ "the third element is added"
+ );
+
+ info("Test removing an existing entry");
+ let removed = SessionDataHelpers.removeSessionDataEntry(
+ sessionData,
+ TARGETS,
+ ["process"]
+ );
+ ok(removed, "removedSessionDataEntry returned true as it removed an element");
+ deepEqual(
+ sessionData[TARGETS],
+ ["frame", "worker"],
+ "the element has been remove"
+ );
+
+ info("Test removing non-existing entry");
+ removed = SessionDataHelpers.removeSessionDataEntry(sessionData, TARGETS, [
+ "not-existing",
+ ]);
+ ok(
+ !removed,
+ "removedSessionDataEntry returned false as no element has been removed"
+ );
+ deepEqual(
+ sessionData[TARGETS],
+ ["frame", "worker"],
+ "no change made to the array"
+ );
+
+ removed = SessionDataHelpers.removeSessionDataEntry(sessionData, TARGETS, [
+ "frame",
+ "worker",
+ ]);
+ ok(
+ removed,
+ "removedSessionDataEntry returned true as elements have been removed"
+ );
+ deepEqual(sessionData[TARGETS], [], "all elements were removed");
+
+ info("Test settting instead of adding data entries");
+ SessionDataHelpers.addOrSetSessionDataEntry(
+ sessionData,
+ TARGETS,
+ ["frame"],
+ "add"
+ );
+ deepEqual(sessionData[TARGETS], ["frame"], "frame was re-added");
+
+ SessionDataHelpers.addOrSetSessionDataEntry(
+ sessionData,
+ TARGETS,
+ ["process", "worker"],
+ "set"
+ );
+ deepEqual(
+ sessionData[TARGETS],
+ ["process", "worker"],
+ "frame was replaced by process and worker"
+ );
+
+ info("Test setting an empty array");
+ SessionDataHelpers.addOrSetSessionDataEntry(sessionData, TARGETS, [], "set");
+ deepEqual(
+ sessionData[TARGETS],
+ [],
+ "Setting an empty array of entries clears the data entry"
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_setBreakpoint-at-the-beginning-of-a-minified-fn.js b/devtools/server/tests/xpcshell/test_setBreakpoint-at-the-beginning-of-a-minified-fn.js
new file mode 100644
index 0000000000..9140e92d7c
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_setBreakpoint-at-the-beginning-of-a-minified-fn.js
@@ -0,0 +1,41 @@
+"use strict";
+
+const SOURCE_URL = getFileUrl("setBreakpoint-on-column-minified.js");
+
+add_task(
+ threadFrontTest(
+ async ({ threadFront, debuggee }) => {
+ const promise = waitForNewSource(threadFront, SOURCE_URL);
+ loadSubScript(SOURCE_URL, debuggee);
+ const { source } = await promise;
+
+ // Pause inside of the nested function so we can make sure that we don't
+ // add any other breakpoints at other places on this line.
+ const location = { sourceUrl: source.url, line: 3, column: 56 };
+ setBreakpoint(threadFront, location);
+
+ const packet = await executeOnNextTickAndWaitForPause(function () {
+ Cu.evalInSandbox("f()", debuggee);
+ }, threadFront);
+
+ const why = packet.why;
+ Assert.equal(why.type, "breakpoint");
+ Assert.equal(why.actors.length, 1);
+
+ const frame = packet.frame;
+ const where = frame.where;
+ Assert.equal(where.actor, source.actor);
+ Assert.equal(where.line, location.line);
+ Assert.equal(where.column, 56);
+
+ const environment = await packet.frame.getEnvironment();
+ const variables = environment.bindings.variables;
+ Assert.equal(variables.a.value.type, "undefined");
+ Assert.equal(variables.b.value.type, "undefined");
+ Assert.equal(variables.c.value.type, "undefined");
+
+ await resume(threadFront);
+ },
+ { doNotRunWorker: true }
+ )
+);
diff --git a/devtools/server/tests/xpcshell/test_setBreakpoint-at-the-end-of-a-minified-fn.js b/devtools/server/tests/xpcshell/test_setBreakpoint-at-the-end-of-a-minified-fn.js
new file mode 100644
index 0000000000..f9df5adad4
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_setBreakpoint-at-the-end-of-a-minified-fn.js
@@ -0,0 +1,41 @@
+"use strict";
+
+const SOURCE_URL = getFileUrl("setBreakpoint-on-column-minified.js");
+
+add_task(
+ threadFrontTest(
+ async ({ threadFront, debuggee }) => {
+ const promise = waitForNewSource(threadFront, SOURCE_URL);
+ loadSubScript(SOURCE_URL, debuggee);
+ const { source } = await promise;
+
+ // Pause inside of the nested function so we can make sure that we don't
+ // add any other breakpoints at other places on this line.
+ const location = { sourceUrl: source.url, line: 3, column: 81 };
+ setBreakpoint(threadFront, location);
+
+ const packet = await executeOnNextTickAndWaitForPause(function () {
+ Cu.evalInSandbox("f()", debuggee);
+ }, threadFront);
+
+ const why = packet.why;
+ Assert.equal(why.type, "breakpoint");
+ Assert.equal(why.actors.length, 1);
+
+ const frame = packet.frame;
+ const where = frame.where;
+ Assert.equal(where.actor, source.actor);
+ Assert.equal(where.line, location.line);
+ Assert.equal(where.column, 81);
+
+ const environment = await packet.frame.getEnvironment();
+ const variables = environment.bindings.variables;
+ Assert.equal(variables.a.value, 1);
+ Assert.equal(variables.b.value, 2);
+ Assert.equal(variables.c.value, 3);
+
+ await resume(threadFront);
+ },
+ { doNotRunWorker: true }
+ )
+);
diff --git a/devtools/server/tests/xpcshell/test_setBreakpoint-on-column-in-gcd-script.js b/devtools/server/tests/xpcshell/test_setBreakpoint-on-column-in-gcd-script.js
new file mode 100644
index 0000000000..797cb6cd65
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_setBreakpoint-on-column-in-gcd-script.js
@@ -0,0 +1,46 @@
+"use strict";
+
+const SOURCE_URL = getFileUrl("setBreakpoint-on-column-in-gcd-script.js");
+
+add_task(
+ threadFrontTest(
+ async ({ threadFront, debuggee, targetFront }) => {
+ const promise = waitForNewSource(threadFront, SOURCE_URL);
+ loadSubScriptWithOptions(SOURCE_URL, {
+ target: debuggee,
+ ignoreCache: true,
+ });
+ Cu.forceGC();
+ Cu.forceGC();
+ Cu.forceGC();
+
+ const { source } = await promise;
+
+ const location = { sourceUrl: source.url, line: 6, column: 21 };
+ setBreakpoint(threadFront, location);
+
+ const packet = await executeOnNextTickAndWaitForPause(function () {
+ reload(targetFront).then(function () {
+ loadSubScriptWithOptions(SOURCE_URL, {
+ target: debuggee,
+ ignoreCache: true,
+ });
+ });
+ }, threadFront);
+ const environment = await packet.frame.getEnvironment();
+ const why = packet.why;
+ Assert.equal(why.type, "breakpoint");
+ Assert.equal(why.actors.length, 1);
+ const frame = packet.frame;
+ const where = frame.where;
+ Assert.equal(where.line, location.line);
+ Assert.equal(where.column, location.column);
+ const variables = environment.bindings.variables;
+ Assert.equal(variables.a.value, 1);
+ Assert.equal(variables.b.value.type, "undefined");
+ Assert.equal(variables.c.value.type, "undefined");
+ await resume(threadFront);
+ },
+ { doNotRunWorker: true }
+ )
+);
diff --git a/devtools/server/tests/xpcshell/test_setBreakpoint-on-column.js b/devtools/server/tests/xpcshell/test_setBreakpoint-on-column.js
new file mode 100644
index 0000000000..200d8b44e6
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_setBreakpoint-on-column.js
@@ -0,0 +1,36 @@
+"use strict";
+
+const SOURCE_URL = getFileUrl("setBreakpoint-on-column.js");
+
+add_task(
+ threadFrontTest(
+ async ({ threadFront, debuggee }) => {
+ const promise = waitForNewSource(threadFront, SOURCE_URL);
+ loadSubScript(SOURCE_URL, debuggee);
+ const { source } = await promise;
+
+ const location = { sourceUrl: source.url, line: 4, column: 21 };
+ setBreakpoint(threadFront, location);
+
+ const packet = await executeOnNextTickAndWaitForPause(function () {
+ Cu.evalInSandbox("f()", debuggee);
+ }, threadFront);
+ const environment = await packet.frame.getEnvironment();
+ const why = packet.why;
+ Assert.equal(why.type, "breakpoint");
+ Assert.equal(why.actors.length, 1);
+ const frame = packet.frame;
+ const where = frame.where;
+ Assert.equal(where.actor, source.actor);
+ Assert.equal(where.line, location.line);
+ Assert.equal(where.column, location.column);
+ const variables = environment.bindings.variables;
+ Assert.equal(variables.a.value, 1);
+ Assert.equal(variables.b.value.type, "undefined");
+ Assert.equal(variables.c.value.type, "undefined");
+
+ await resume(threadFront);
+ },
+ { doNotRunWorker: true }
+ )
+);
diff --git a/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-in-gcd-script.js b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-in-gcd-script.js
new file mode 100644
index 0000000000..565402551e
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-in-gcd-script.js
@@ -0,0 +1,45 @@
+"use strict";
+
+const SOURCE_URL = getFileUrl("setBreakpoint-on-line-in-gcd-script.js");
+
+add_task(
+ threadFrontTest(
+ async ({ threadFront, debuggee, targetFront }) => {
+ const promise = waitForNewSource(threadFront, SOURCE_URL);
+ loadSubScriptWithOptions(SOURCE_URL, {
+ target: debuggee,
+ ignoreCache: true,
+ });
+ Cu.forceGC();
+ Cu.forceGC();
+ Cu.forceGC();
+
+ const { source } = await promise;
+
+ const location = { sourceUrl: source.url, line: 7 };
+ setBreakpoint(threadFront, location);
+
+ const packet = await executeOnNextTickAndWaitForPause(function () {
+ reload(targetFront).then(function () {
+ loadSubScriptWithOptions(SOURCE_URL, {
+ target: debuggee,
+ ignoreCache: true,
+ });
+ });
+ }, threadFront);
+ const why = packet.why;
+ const environment = await packet.frame.getEnvironment();
+ Assert.equal(why.type, "breakpoint");
+ Assert.equal(why.actors.length, 1);
+ const frame = packet.frame;
+ const where = frame.where;
+ Assert.equal(where.line, location.line);
+ const variables = environment.bindings.variables;
+ Assert.equal(variables.a.value, 1);
+ Assert.equal(variables.b.value.type, "undefined");
+ Assert.equal(variables.c.value.type, "undefined");
+ await resume(threadFront);
+ },
+ { doNotRunWorker: true }
+ )
+);
diff --git a/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-multiple-offsets.js b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-multiple-offsets.js
new file mode 100644
index 0000000000..2debc26b93
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-multiple-offsets.js
@@ -0,0 +1,52 @@
+"use strict";
+
+const SOURCE_URL = getFileUrl("setBreakpoint-on-line-with-multiple-offsets.js");
+
+add_task(
+ threadFrontTest(
+ async ({ threadFront, debuggee }) => {
+ const promise = waitForNewSource(threadFront, SOURCE_URL);
+ loadSubScript(SOURCE_URL, debuggee);
+ const { source } = await promise;
+ const sourceFront = threadFront.source(source);
+
+ const location = { sourceUrl: sourceFront.url, line: 4 };
+ setBreakpoint(threadFront, location);
+
+ let packet = await executeOnNextTickAndWaitForPause(function () {
+ Cu.evalInSandbox("f()", debuggee);
+ }, threadFront);
+ let why = packet.why;
+ let environment = await packet.frame.getEnvironment();
+ Assert.equal(why.type, "breakpoint");
+ Assert.equal(why.actors.length, 1);
+ let frame = packet.frame;
+ let where = frame.where;
+ Assert.equal(where.actor, source.actor);
+ Assert.equal(where.line, location.line);
+ let variables = environment.bindings.variables;
+ Assert.equal(variables.i.value.type, "undefined");
+
+ const location2 = { sourceUrl: sourceFront.url, line: 7 };
+ setBreakpoint(threadFront, location2);
+
+ packet = await executeOnNextTickAndWaitForPause(
+ () => resume(threadFront),
+ threadFront
+ );
+ environment = await packet.frame.getEnvironment();
+ why = packet.why;
+ Assert.equal(why.type, "breakpoint");
+ Assert.equal(why.actors.length, 1);
+ frame = packet.frame;
+ where = frame.where;
+ Assert.equal(where.actor, source.actor);
+ Assert.equal(where.line, location2.line);
+ variables = environment.bindings.variables;
+ Assert.equal(variables.i.value, 1);
+
+ await resume(threadFront);
+ },
+ { doNotRunWorker: true }
+ )
+);
diff --git a/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-multiple-statements.js b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-multiple-statements.js
new file mode 100644
index 0000000000..f5ec75a353
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-multiple-statements.js
@@ -0,0 +1,38 @@
+"use strict";
+
+const SOURCE_URL = getFileUrl(
+ "setBreakpoint-on-line-with-multiple-statements.js"
+);
+
+add_task(
+ threadFrontTest(
+ async ({ threadFront, debuggee }) => {
+ const promise = waitForNewSource(threadFront, SOURCE_URL);
+ loadSubScript(SOURCE_URL, debuggee);
+ const { source } = await promise;
+ const sourceFront = threadFront.source(source);
+
+ const location = { sourceUrl: sourceFront.url, line: 4 };
+ setBreakpoint(threadFront, location);
+
+ const packet = await executeOnNextTickAndWaitForPause(function () {
+ Cu.evalInSandbox("f()", debuggee);
+ }, threadFront);
+ const why = packet.why;
+ const environment = await packet.frame.getEnvironment();
+ Assert.equal(why.type, "breakpoint");
+ Assert.equal(why.actors.length, 1);
+ const frame = packet.frame;
+ const where = frame.where;
+ Assert.equal(where.actor, source.actor);
+ Assert.equal(where.line, location.line);
+ const variables = environment.bindings.variables;
+ Assert.equal(variables.a.value.type, "undefined");
+ Assert.equal(variables.b.value.type, "undefined");
+ Assert.equal(variables.c.value.type, "undefined");
+
+ await resume(threadFront);
+ },
+ { doNotRunWorker: true }
+ )
+);
diff --git a/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-no-offsets-in-gcd-script.js b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-no-offsets-in-gcd-script.js
new file mode 100644
index 0000000000..1bcdadbe4a
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-no-offsets-in-gcd-script.js
@@ -0,0 +1,56 @@
+"use strict";
+
+const SOURCE_URL = getFileUrl(
+ "setBreakpoint-on-line-with-no-offsets-in-gcd-script.js"
+);
+
+add_task(
+ threadFrontTest(
+ async ({ threadFront, debuggee, targetFront }) => {
+ const promise = waitForNewSource(threadFront, SOURCE_URL);
+ loadSubScriptWithOptions(SOURCE_URL, {
+ target: debuggee,
+ ignoreCache: true,
+ });
+ Cu.forceGC();
+ Cu.forceGC();
+ Cu.forceGC();
+
+ const { source } = await promise;
+ const sourceFront = threadFront.source(source);
+
+ const location = { line: 7 };
+ let [packet, breakpointClient] = await setBreakpoint(
+ sourceFront,
+ location
+ );
+ Assert.ok(packet.isPending);
+ Assert.equal(false, "actualLocation" in packet);
+
+ packet = await executeOnNextTickAndWaitForPause(function () {
+ reload(targetFront).then(function () {
+ loadSubScriptWithOptions(SOURCE_URL, {
+ target: debuggee,
+ ignoreCache: true,
+ });
+ });
+ }, threadFront);
+ const environment = await packet.frame.getEnvironment();
+ Assert.equal(packet.type, "paused");
+ const why = packet.why;
+ Assert.equal(why.type, "breakpoint");
+ Assert.equal(why.actors.length, 1);
+ Assert.equal(why.actors[0], breakpointClient.actor);
+ const frame = packet.frame;
+ const where = frame.where;
+ Assert.equal(where.actor, source.actor);
+ Assert.equal(where.line, 8);
+ const variables = environment.bindings.variables;
+ Assert.equal(variables.a.value, 1);
+ Assert.equal(variables.c.value.type, "undefined");
+
+ await resume(threadFront);
+ },
+ { doNotRunWorker: true }
+ )
+);
diff --git a/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-no-offsets.js b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-no-offsets.js
new file mode 100644
index 0000000000..5700097ea6
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-no-offsets.js
@@ -0,0 +1,44 @@
+"use strict";
+
+const SOURCE_URL = getFileUrl("setBreakpoint-on-line-with-no-offsets.js");
+
+add_task(
+ threadFrontTest(
+ async ({ threadFront, debuggee }) => {
+ const promise = waitForNewSource(threadFront, SOURCE_URL);
+ loadSubScript(SOURCE_URL, debuggee);
+ const { source } = await promise;
+ const sourceFront = threadFront.source(source);
+
+ const location = { line: 5 };
+ let [packet, breakpointClient] = await setBreakpoint(
+ sourceFront,
+ location
+ );
+ Assert.ok(!packet.isPending);
+ Assert.ok("actualLocation" in packet);
+ const actualLocation = packet.actualLocation;
+ Assert.equal(actualLocation.line, 6);
+
+ packet = await executeOnNextTickAndWaitForPause(function () {
+ Cu.evalInSandbox("f()", debuggee);
+ }, threadFront);
+ const environment = await packet.frame.getEnvironment();
+ Assert.equal(packet.type, "paused");
+ const why = packet.why;
+ Assert.equal(why.type, "breakpoint");
+ Assert.equal(why.actors.length, 1);
+ Assert.equal(why.actors[0], breakpointClient.actor);
+ const frame = packet.frame;
+ const where = frame.where;
+ Assert.equal(where.actor, source.actor);
+ Assert.equal(where.line, actualLocation.line);
+ const variables = environment.bindings.variables;
+ Assert.equal(variables.a.value, 1);
+ Assert.equal(variables.c.value.type, "undefined");
+
+ await resume(threadFront);
+ },
+ { doNotRunWorker: true }
+ )
+);
diff --git a/devtools/server/tests/xpcshell/test_setBreakpoint-on-line.js b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line.js
new file mode 100644
index 0000000000..93e01b757c
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line.js
@@ -0,0 +1,36 @@
+"use strict";
+
+const SOURCE_URL = getFileUrl("setBreakpoint-on-line.js");
+
+add_task(
+ threadFrontTest(
+ async ({ threadFront, debuggee }) => {
+ const promise = waitForNewSource(threadFront, SOURCE_URL);
+ loadSubScript(SOURCE_URL, debuggee);
+ const { source } = await promise;
+ const sourceFront = threadFront.source(source);
+
+ const location = { sourceUrl: sourceFront.url, line: 5 };
+ setBreakpoint(threadFront, location);
+
+ const packet = await executeOnNextTickAndWaitForPause(function () {
+ Cu.evalInSandbox("f()", debuggee);
+ }, threadFront);
+ const environment = await packet.frame.getEnvironment();
+ const why = packet.why;
+ Assert.equal(why.type, "breakpoint");
+ Assert.equal(why.actors.length, 1);
+ const frame = packet.frame;
+ const where = frame.where;
+ Assert.equal(where.actor, source.actor);
+ Assert.equal(where.line, location.line);
+ const variables = environment.bindings.variables;
+ Assert.equal(variables.a.value, 1);
+ Assert.equal(variables.b.value.type, "undefined");
+ Assert.equal(variables.c.value.type, "undefined");
+
+ await resume(threadFront);
+ },
+ { doNotRunWorker: true }
+ )
+);
diff --git a/devtools/server/tests/xpcshell/test_shapes_highlighter_helpers.js b/devtools/server/tests/xpcshell/test_shapes_highlighter_helpers.js
new file mode 100644
index 0000000000..6876f0a532
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_shapes_highlighter_helpers.js
@@ -0,0 +1,274 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test the helper functions of the shapes highlighter.
+ */
+
+"use strict";
+
+const {
+ splitCoords,
+ coordToPercent,
+ evalCalcExpression,
+ shapeModeToCssPropertyName,
+ getCirclePath,
+ getDecimalPrecision,
+ getUnit,
+} = require("resource://devtools/server/actors/highlighters/shapes.js");
+
+function run_test() {
+ test_split_coords();
+ test_coord_to_percent();
+ test_eval_calc_expression();
+ test_shape_mode_to_css_property_name();
+ test_get_circle_path();
+ test_get_decimal_precision();
+ test_get_unit();
+ run_next_test();
+}
+
+function test_split_coords() {
+ const tests = [
+ {
+ desc: "splitCoords for basic coordinate pair",
+ expr: "30% 20%",
+ expected: ["30%", "20%"],
+ },
+ {
+ desc: "splitCoords for coord pair with calc()",
+ expr: "calc(50px + 20%) 30%",
+ expected: ["calc(50px\u00a0+\u00a020%)", "30%"],
+ },
+ ];
+
+ for (const { desc, expr, expected } of tests) {
+ deepEqual(splitCoords(expr), expected, desc);
+ }
+}
+
+function test_coord_to_percent() {
+ const size = 1000;
+ const tests = [
+ {
+ desc: "coordToPercent for percent value",
+ expr: "50%",
+ expected: 50,
+ },
+ {
+ desc: "coordToPercent for px value",
+ expr: "500px",
+ expected: 50,
+ },
+ {
+ desc: "coordToPercent for zero value",
+ expr: "0",
+ expected: 0,
+ },
+ ];
+
+ for (const { desc, expr, expected } of tests) {
+ equal(coordToPercent(expr, size), expected, desc);
+ }
+}
+
+function test_eval_calc_expression() {
+ const size = 1000;
+ const tests = [
+ {
+ desc: "evalCalcExpression with one value",
+ expr: "50%",
+ expected: 50,
+ },
+ {
+ desc: "evalCalcExpression with percent and px values",
+ expr: "50% + 100px",
+ expected: 60,
+ },
+ {
+ desc: "evalCalcExpression with a zero value",
+ expr: "0 + 100px",
+ expected: 10,
+ },
+ {
+ desc: "evalCalcExpression with a negative value",
+ expr: "-200px+50%",
+ expected: 30,
+ },
+ ];
+
+ for (const { desc, expr, expected } of tests) {
+ equal(evalCalcExpression(expr, size), expected, desc);
+ }
+}
+
+function test_shape_mode_to_css_property_name() {
+ const tests = [
+ {
+ desc: "shapeModeToCssPropertyName for clip-path",
+ expr: "cssClipPath",
+ expected: "clipPath",
+ },
+ {
+ desc: "shapeModeToCssPropertyName for shape-outside",
+ expr: "cssShapeOutside",
+ expected: "shapeOutside",
+ },
+ ];
+
+ for (const { desc, expr, expected } of tests) {
+ equal(shapeModeToCssPropertyName(expr), expected, desc);
+ }
+}
+
+function test_get_circle_path() {
+ const tests = [
+ {
+ desc: "getCirclePath with size 5, no resizing, no zoom, 1:1 ratio",
+ size: 5,
+ cx: 0,
+ cy: 0,
+ width: 100,
+ height: 100,
+ zoom: 1,
+ expected: "M-5,0a5,5 0 1,0 10,0a5,5 0 1,0 -10,0",
+ },
+ {
+ desc: "getCirclePath with size 7, resizing, no zoom, 1:1 ratio",
+ size: 7,
+ cx: 0,
+ cy: 0,
+ width: 200,
+ height: 200,
+ zoom: 1,
+ expected: "M-3.5,0a3.5,3.5 0 1,0 7,0a3.5,3.5 0 1,0 -7,0",
+ },
+ {
+ desc: "getCirclePath with size 5, resizing, zoom, 1:1 ratio",
+ size: 5,
+ cx: 0,
+ cy: 0,
+ width: 200,
+ height: 200,
+ zoom: 2,
+ expected: "M-1.25,0a1.25,1.25 0 1,0 2.5,0a1.25,1.25 0 1,0 -2.5,0",
+ },
+ {
+ desc: "getCirclePath with size 5, resizing, zoom, non-square ratio",
+ size: 5,
+ cx: 0,
+ cy: 0,
+ width: 100,
+ height: 200,
+ zoom: 2,
+ expected: "M-2.5,0a2.5,1.25 0 1,0 5,0a2.5,1.25 0 1,0 -5,0",
+ },
+ ];
+
+ for (const { desc, size, cx, cy, width, height, zoom, expected } of tests) {
+ equal(getCirclePath(size, cx, cy, width, height, zoom), expected, desc);
+ }
+}
+
+function test_get_decimal_precision() {
+ const tests = [
+ {
+ desc: "getDecimalPrecision with px",
+ expr: "px",
+ expected: 0,
+ },
+ {
+ desc: "getDecimalPrecision with %",
+ expr: "%",
+ expected: 2,
+ },
+ {
+ desc: "getDecimalPrecision with em",
+ expr: "em",
+ expected: 2,
+ },
+ {
+ desc: "getDecimalPrecision with undefined",
+ expr: undefined,
+ expected: 0,
+ },
+ {
+ desc: "getDecimalPrecision with empty string",
+ expr: "",
+ expected: 0,
+ },
+ ];
+
+ for (const { desc, expr, expected } of tests) {
+ equal(getDecimalPrecision(expr), expected, desc);
+ }
+}
+
+function test_get_unit() {
+ const tests = [
+ {
+ desc: "getUnit with %",
+ expr: "30%",
+ expected: "%",
+ },
+ {
+ desc: "getUnit with px",
+ expr: "400px",
+ expected: "px",
+ },
+ {
+ desc: "getUnit with em",
+ expr: "4em",
+ expected: "em",
+ },
+ {
+ desc: "getUnit with 0",
+ expr: "0",
+ expected: "px",
+ },
+ {
+ desc: "getUnit with 0%",
+ expr: "0%",
+ expected: "%",
+ },
+ {
+ desc: "getUnit with 0.00%",
+ expr: "0.00%",
+ expected: "%",
+ },
+ {
+ desc: "getUnit with 0px",
+ expr: "0px",
+ expected: "px",
+ },
+ {
+ desc: "getUnit with 0em",
+ expr: "0em",
+ expected: "em",
+ },
+ {
+ desc: "getUnit with calc",
+ expr: "calc(30px + 5%)",
+ expected: "px",
+ },
+ {
+ desc: "getUnit with var",
+ expr: "var(--variable)",
+ expected: "px",
+ },
+ {
+ desc: "getUnit with closest-side",
+ expr: "closest-side",
+ expected: "px",
+ },
+ {
+ desc: "getUnit with farthest-side",
+ expr: "farthest-side",
+ expected: "px",
+ },
+ ];
+
+ for (const { desc, expr, expected } of tests) {
+ equal(getUnit(expr), expected, desc);
+ }
+}
diff --git a/devtools/server/tests/xpcshell/test_source-01.js b/devtools/server/tests/xpcshell/test_source-01.js
new file mode 100644
index 0000000000..5cb7a6da52
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_source-01.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// This test ensures that we can create SourceActors and SourceFronts properly,
+// and that they can communicate over the protocol to fetch the source text for
+// a given script.
+
+const SOURCE_URL = "http://example.com/foobar.js";
+const SOURCE_CONTENT = "stopMe()";
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ DevToolsServer.LONG_STRING_LENGTH = 200;
+
+ await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+ const response = await threadFront.getSources();
+
+ Assert.ok(!!response);
+ Assert.ok(!!response.sources);
+
+ const source = response.sources.filter(function (s) {
+ return s.url === SOURCE_URL;
+ })[0];
+
+ Assert.ok(!!source);
+
+ const sourceFront = threadFront.source(source);
+ const response2 = await sourceFront.source();
+
+ Assert.ok(!!response2);
+ Assert.ok(!!response2.contentType);
+ Assert.ok(response2.contentType.includes("javascript"));
+
+ Assert.ok(!!response2.source);
+ Assert.equal(SOURCE_CONTENT, response2.source);
+
+ await threadFront.resume();
+ })
+);
+
+function evaluateTestCode(debuggee) {
+ Cu.evalInSandbox(
+ "" +
+ function stopMe(arg1) {
+ debugger;
+ },
+ debuggee,
+ "1.8",
+ getFileUrl("test_source-01.js")
+ );
+
+ Cu.evalInSandbox(SOURCE_CONTENT, debuggee, "1.8", SOURCE_URL);
+}
diff --git a/devtools/server/tests/xpcshell/test_source-02.js b/devtools/server/tests/xpcshell/test_source-02.js
new file mode 100644
index 0000000000..9cb88cb0e4
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_source-02.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// This test ensures that we can create SourceActors and SourceFronts properly,
+// and that they can communicate over the protocol to fetch the source text for
+// a given script.
+
+const SOURCE_URL = "http://example.com/foobar.js";
+const SOURCE_CONTENT = `
+ stopMe();
+ for(var i = 0; i < 2; i++) {
+ debugger;
+ }
+`;
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ DevToolsServer.LONG_STRING_LENGTH = 200;
+
+ await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+
+ let response = await threadFront.getSources();
+ Assert.ok(!!response);
+ Assert.ok(!!response.sources);
+
+ const source = response.sources.filter(function (s) {
+ return s.url === SOURCE_URL;
+ })[0];
+
+ Assert.ok(!!source);
+
+ const sourceFront = threadFront.source(source);
+ response = await sourceFront.getBreakpointPositionsCompressed();
+ Assert.ok(!!response);
+
+ Assert.deepEqual(response, {
+ 2: [2],
+ 3: [14, 17, 24],
+ 4: [4],
+ 6: [0],
+ });
+
+ await threadFront.resume();
+ })
+);
+
+function evaluateTestCode(debuggee) {
+ Cu.evalInSandbox(
+ "" +
+ function stopMe(arg1) {
+ debugger;
+ },
+ debuggee,
+ "1.8",
+ getFileUrl("test_source-02.js")
+ );
+
+ Cu.evalInSandbox(SOURCE_CONTENT, debuggee, "1.8", SOURCE_URL);
+}
diff --git a/devtools/server/tests/xpcshell/test_source-03.js b/devtools/server/tests/xpcshell/test_source-03.js
new file mode 100644
index 0000000000..d0cd4839a0
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_source-03.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const SOURCE_URL = getFileUrl("source-03.js");
+
+add_task(
+ threadFrontTest(
+ async ({ threadFront, server }) => {
+ const promise = waitForNewSource(threadFront, SOURCE_URL);
+
+ // Create a two globals in the default junk sandbox compartment so that
+ // both globals are part of the same compartment.
+ server.allowNewThreadGlobals();
+ const debuggee1 = Cu.Sandbox(systemPrincipal);
+ debuggee1.__name = "debuggee2.js";
+ const debuggee2 = Cu.Sandbox(systemPrincipal);
+ debuggee2.__name = "debuggee2.js";
+ server.disallowNewThreadGlobals();
+
+ // Load two copies of the source file. The first call to "loadSubScript" will
+ // create a ScriptSourceObject and a JSScript which references it.
+ // The second call will attempt to re-use JSScript objects because that is
+ // what loadSubScript does for instances of the same file that are loaded
+ // in the system principal in the same compartment.
+ //
+ // We explicitly want this because it is an edge case of the server. Most
+ // of the time a Debugger.Source will only have a single Debugger.Script
+ // associated with a given function, but in the context of explicitly
+ // cloned JSScripts, this is not the case, and we need to handle that.
+ loadSubScript(SOURCE_URL, debuggee1);
+ loadSubScript(SOURCE_URL, debuggee2);
+
+ await promise;
+
+ // We want to set a breakpoint and make sure that the breakpoint is properly
+ // set on _both_ files backed
+ await setBreakpoint(threadFront, {
+ sourceUrl: SOURCE_URL,
+ line: 4,
+ });
+
+ const { sources } = await getSources(threadFront);
+
+ // Note: Since we load the file twice, we end up with two copies of the
+ // source object, and so two sources here.
+ Assert.equal(sources.length, 2);
+
+ // Ensure that the breakpoint was properly applied to the JSScipt loaded
+ // in the first global.
+ let pausedOne = false;
+ let onResumed = null;
+ threadFront.once("paused", function (packet) {
+ pausedOne = true;
+ onResumed = resume(threadFront);
+ });
+ Cu.evalInSandbox("init()", debuggee1, "1.8", "test.js", 1);
+ await onResumed;
+ Assert.equal(pausedOne, true);
+
+ // Ensure that the breakpoint was properly applied to the JSScipt loaded
+ // in the second global.
+ let pausedTwo = false;
+ threadFront.once("paused", function (packet) {
+ pausedTwo = true;
+ onResumed = resume(threadFront);
+ });
+ Cu.evalInSandbox("init()", debuggee2, "1.8", "test.js", 1);
+ await onResumed;
+ Assert.equal(pausedTwo, true);
+ },
+ { doNotRunWorker: true }
+ )
+);
diff --git a/devtools/server/tests/xpcshell/test_source-04.js b/devtools/server/tests/xpcshell/test_source-04.js
new file mode 100644
index 0000000000..a3e3bef25f
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_source-04.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const SOURCE_URL = getFileUrl("source-03.js");
+
+add_task(
+ threadFrontTest(
+ async ({ threadFront, server }) => {
+ const promise = waitForNewSource(threadFront, SOURCE_URL);
+
+ // Create two globals in the default junk sandbox compartment so that
+ // both globals are part of the same compartment.
+ server.allowNewThreadGlobals();
+ const debuggee1 = Cu.Sandbox(systemPrincipal);
+ debuggee1.__name = "debuggee2.js";
+ const debuggee2 = Cu.Sandbox(systemPrincipal);
+ debuggee2.__name = "debuggee2.js";
+ server.disallowNewThreadGlobals();
+
+ // Load first copy of the source file. The first call to "loadSubScript" will
+ // create a ScriptSourceObject and a JSScript which references it.
+ loadSubScript(SOURCE_URL, debuggee1);
+
+ await promise;
+
+ // We want to set a breakpoint and make sure that the breakpoint is properly
+ // set on _both_ files backed
+ await setBreakpoint(threadFront, {
+ sourceUrl: SOURCE_URL,
+ line: 4,
+ });
+
+ const { sources } = await getSources(threadFront);
+ Assert.equal(sources.length, 1);
+
+ // Ensure that the breakpoint was properly applied to the JSScipt loaded
+ // in the first global.
+ let pausedOne = false;
+ let onResumed = null;
+ threadFront.once("paused", function (packet) {
+ pausedOne = true;
+ onResumed = resume(threadFront);
+ });
+ Cu.evalInSandbox("init()", debuggee1, "1.8", "test.js", 1);
+ await onResumed;
+ Assert.equal(pausedOne, true);
+
+ // Load second copy of the source file. The second call will attempt to
+ // re-use JSScript objects because that is what loadSubScript does for
+ // instances of the same file that are loaded in the system principal in
+ // the same compartment.
+ //
+ // We explicitly want this because it is an edge case of the server. Most
+ // of the time a Debugger.Source will only have a single Debugger.Script
+ // associated with a given function, but in the context of explicitly
+ // cloned JSScripts, this is not the case, and we need to handle that.
+ loadSubScript(SOURCE_URL, debuggee2);
+
+ // Ensure that the breakpoint was properly applied to the JSScipt loaded
+ // in the second global.
+ let pausedTwo = false;
+ threadFront.once("paused", function (packet) {
+ pausedTwo = true;
+ onResumed = resume(threadFront);
+ });
+ Cu.evalInSandbox("init()", debuggee2, "1.8", "test.js", 1);
+ await onResumed;
+ Assert.equal(pausedTwo, true);
+ },
+ { doNotRunWorker: true }
+ )
+);
diff --git a/devtools/server/tests/xpcshell/test_stepping-01.js b/devtools/server/tests/xpcshell/test_stepping-01.js
new file mode 100644
index 0000000000..0c66404510
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_stepping-01.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check scenarios where we're leaving function a and
+ * going to the function b's call-site.
+ */
+
+async function testFinish({ threadFront, devToolsClient }) {
+ await close(devToolsClient);
+
+ do_test_finished();
+}
+
+async function invokeAndPause({ global, threadFront }, expression) {
+ return executeOnNextTickAndWaitForPause(
+ () => Cu.evalInSandbox(expression, global),
+ threadFront
+ );
+}
+
+async function step(threadFront, cmd) {
+ return cmd(threadFront);
+}
+
+function getPauseLocation(packet) {
+ const { line, column } = packet.frame.where;
+ return { line, column };
+}
+
+function getPauseReturn(packet) {
+ return packet.why.frameFinished.return;
+}
+
+async function steps(threadFront, sequence) {
+ const locations = [];
+ for (const cmd of sequence) {
+ const packet = await step(threadFront, cmd);
+ locations.push(getPauseLocation(packet));
+ }
+ return locations;
+}
+
+async function stepOutOfA(dbg, func, expectedLocation) {
+ await invokeAndPause(dbg, `${func}()`);
+ const { threadFront } = dbg;
+ await steps(threadFront, [stepOver, stepIn]);
+
+ const packet = await stepOut(threadFront);
+
+ deepEqual(
+ getPauseLocation(packet),
+ expectedLocation,
+ `step out location in ${func}`
+ );
+
+ await threadFront.resume();
+}
+
+async function stepOverInA(dbg, func, expectedLocation) {
+ await invokeAndPause(dbg, `${func}()`);
+ const { threadFront } = dbg;
+ await steps(threadFront, [stepOver, stepIn]);
+
+ let packet = await stepOver(threadFront);
+ equal(getPauseReturn(packet).ownPropertyLength, 1, "a() is returning obj");
+
+ packet = await stepOver(threadFront);
+ deepEqual(
+ getPauseLocation(packet),
+ expectedLocation,
+ `step out location in ${func}`
+ );
+ await dbg.threadFront.resume();
+}
+
+async function testStep(dbg, func, expectedValue) {
+ await stepOverInA(dbg, func, expectedValue);
+ await stepOutOfA(dbg, func, expectedValue);
+}
+
+function run_test() {
+ return (async function () {
+ const dbg = await setupTestFromUrl("stepping.js");
+
+ await testStep(dbg, "arithmetic", { line: 16, column: 8 });
+ await testStep(dbg, "composition", { line: 21, column: 3 });
+ await testStep(dbg, "chaining", { line: 26, column: 6 });
+
+ await testFinish(dbg);
+ })();
+}
diff --git a/devtools/server/tests/xpcshell/test_stepping-02.js b/devtools/server/tests/xpcshell/test_stepping-02.js
new file mode 100644
index 0000000000..c9df671839
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_stepping-02.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check basic step-in functionality.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ dumpn("Evaluating test code and waiting for first debugger statement");
+ const dbgStmt = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+ equal(
+ dbgStmt.frame.where.line,
+ 2,
+ "Should be at debugger statement on line 2"
+ );
+ equal(debuggee.a, undefined);
+ equal(debuggee.b, undefined);
+
+ const step1 = await stepIn(threadFront);
+ equal(step1.why.type, "resumeLimit");
+ equal(step1.frame.where.line, 3);
+ equal(debuggee.a, undefined);
+ equal(debuggee.b, undefined);
+
+ const step3 = await stepIn(threadFront);
+ equal(step3.why.type, "resumeLimit");
+ equal(step3.frame.where.line, 4);
+ equal(debuggee.a, 1);
+ equal(debuggee.b, undefined);
+
+ const step4 = await stepIn(threadFront);
+ equal(step4.why.type, "resumeLimit");
+ equal(step4.frame.where.line, 4);
+ equal(debuggee.a, 1);
+ equal(debuggee.b, 2);
+ })
+);
+
+function evaluateTestCode(debuggee) {
+ // prettier-ignore
+ Cu.evalInSandbox(
+ ` // 1
+ debugger; // 2
+ var a = 1; // 3
+ var b = 2;`, // 4
+ debuggee,
+ "1.8",
+ "test_stepping-01-test-code.js",
+ 1
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_stepping-03.js b/devtools/server/tests/xpcshell/test_stepping-03.js
new file mode 100644
index 0000000000..88422ac0cc
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_stepping-03.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check basic step-out functionality.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ dumpn("Evaluating test code and waiting for first debugger statement");
+ await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+
+ const step1 = await stepOut(threadFront);
+ equal(step1.frame.where.line, 8);
+ equal(step1.why.type, "resumeLimit");
+
+ equal(debuggee.a, 1);
+ equal(debuggee.b, 2);
+ })
+);
+
+function evaluateTestCode(debuggee) {
+ // prettier-ignore
+ Cu.evalInSandbox(
+ ` // 1
+ function f() { // 2
+ debugger; // 3
+ this.a = 1; // 4
+ this.b = 2; // 5
+ } // 6
+ f(); // 7
+ `, // 8
+ debuggee,
+ "1.8",
+ "test_stepping-01-test-code.js",
+ 1
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_stepping-04.js b/devtools/server/tests/xpcshell/test_stepping-04.js
new file mode 100644
index 0000000000..37a9f843d0
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_stepping-04.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that stepping over a function call does not pause inside the function.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ dumpn("Evaluating test code and waiting for first debugger statement");
+ await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+
+ dumpn("Step Over to f()");
+ const step1 = await stepOver(threadFront);
+ equal(step1.why.type, "resumeLimit");
+ equal(step1.frame.where.line, 6);
+ equal(debuggee.a, undefined);
+ equal(debuggee.b, undefined);
+
+ dumpn("Step Over f()");
+ const step2 = await stepOver(threadFront);
+ equal(step2.frame.where.line, 7);
+ equal(step2.why.type, "resumeLimit");
+ equal(debuggee.a, 1);
+ equal(debuggee.b, undefined);
+ })
+);
+
+function evaluateTestCode(debuggee) {
+ // prettier-ignore
+ Cu.evalInSandbox(
+ ` // 1
+ function f() { // 2
+ this.a = 1; // 3
+ } // 4
+ debugger; // 5
+ f(); // 6
+ let b = 2; // 7
+ `, // 8
+ debuggee,
+ "1.8",
+ "test_stepping-01-test-code.js",
+ 1
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_stepping-05.js b/devtools/server/tests/xpcshell/test_stepping-05.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_stepping-05.js
diff --git a/devtools/server/tests/xpcshell/test_stepping-06.js b/devtools/server/tests/xpcshell/test_stepping-06.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_stepping-06.js
diff --git a/devtools/server/tests/xpcshell/test_stepping-07.js b/devtools/server/tests/xpcshell/test_stepping-07.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_stepping-07.js
diff --git a/devtools/server/tests/xpcshell/test_stepping-08.js b/devtools/server/tests/xpcshell/test_stepping-08.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_stepping-08.js
diff --git a/devtools/server/tests/xpcshell/test_stepping-09.js b/devtools/server/tests/xpcshell/test_stepping-09.js
new file mode 100644
index 0000000000..da59ed963c
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_stepping-09.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that step out stops at the end of the parent if it fails to stop
+ * anywhere else. Bug 1504358.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ dumpn("Evaluating test code and waiting for first debugger statement");
+ const dbgStmt = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+ equal(
+ dbgStmt.frame.where.line,
+ 2,
+ "Should be at debugger statement on line 2"
+ );
+
+ dumpn("Step out of inner and into outer");
+ const step2 = await stepOut(threadFront);
+ // The bug was that we'd step right past the end of the function and never pause.
+ equal(step2.frame.where.line, 2);
+ equal(step2.frame.where.column, 31);
+ deepEqual(step2.why.frameFinished.return, { type: "undefined" });
+ })
+);
+
+function evaluateTestCode(debuggee) {
+ // By placing the inner and outer on the same line, this triggers the server's
+ // logic to skip steps for these functions, meaning that onPop is the only
+ // thing that will cause it to pop.
+ Cu.evalInSandbox(
+ `
+ function outer(){ inner(); return 42; } function inner(){ debugger; }
+ outer();
+ `,
+ debuggee,
+ "1.8",
+ "test_stepping-09-test-code.js",
+ 1
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_stepping-10.js b/devtools/server/tests/xpcshell/test_stepping-10.js
new file mode 100644
index 0000000000..6ea95c3fd3
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_stepping-10.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that step out stops at the parent and the parent's parent.
+ * This checks for the failure found in bug 1530549.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ dumpn("Evaluating test code and waiting for first debugger statement");
+ const dbgStmt = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+ equal(
+ dbgStmt.frame.where.line,
+ 3,
+ "Should be at debugger statement on line 3"
+ );
+
+ dumpn("Step out of inner and into var statement IIFE");
+ const step2 = await stepOut(threadFront);
+ equal(step2.frame.where.line, 4);
+ deepEqual(step2.why.frameFinished.return, { type: "undefined" });
+
+ dumpn("Step out of vars and into script body");
+ const step3 = await stepOut(threadFront);
+ equal(step3.frame.where.line, 9);
+ deepEqual(step3.why.frameFinished.return, { type: "undefined" });
+ })
+);
+
+function evaluateTestCode(debuggee) {
+ Cu.evalInSandbox(
+ `
+ (function() {
+ (function(){debugger;})();
+ var a = 1;
+ a = 2;
+ a = 3;
+ a = 4;
+ })();
+ `,
+ debuggee,
+ "1.8",
+ "test_stepping-10-test-code.js",
+ 1
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_stepping-11.js b/devtools/server/tests/xpcshell/test_stepping-11.js
new file mode 100644
index 0000000000..8cbd285d89
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_stepping-11.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check basic stepping for console evaluations.
+ */
+
+add_task(
+ threadFrontTest(async ({ commands, threadFront }) => {
+ dumpn("Evaluating test code and waiting for first debugger statement");
+
+ commands.scriptCommand.execute(`(function(){
+ debugger;
+ var a = 1;
+ var b = 2;
+ })();`);
+
+ await waitForEvent(threadFront, "paused");
+ const packet = await stepOver(threadFront);
+ Assert.equal(packet.frame.where.line, 3, "step to line 3");
+ await threadFront.resume();
+ })
+);
diff --git a/devtools/server/tests/xpcshell/test_stepping-12.js b/devtools/server/tests/xpcshell/test_stepping-12.js
new file mode 100644
index 0000000000..de96faf59f
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_stepping-12.js
@@ -0,0 +1,162 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that step out stops at the parent and the parent's parent.
+ * This checks for the failure found in bug 1530549.
+ */
+
+const sourceUrl = "test_stepping-10-test-code.js";
+
+add_task(
+ threadFrontTest(async args => {
+ dumpn("Evaluating test code and waiting for first debugger statement");
+
+ await testGenerator(args);
+ await testAwait(args);
+ await testInterleaving(args);
+ await testMultipleSteps(args);
+ })
+);
+
+async function testAwait({ threadFront, debuggee }) {
+ function evaluateTestCode() {
+ Cu.evalInSandbox(
+ `
+ (async function() {
+ debugger;
+ r = await Promise.resolve('yay');
+ a = 4;
+ })();
+ `,
+ debuggee,
+ "1.8",
+ sourceUrl,
+ 1
+ );
+ }
+
+ await executeOnNextTickAndWaitForPause(evaluateTestCode, threadFront);
+
+ dumpn("Step Over and land on line 5");
+ const step1 = await stepOver(threadFront);
+ equal(step1.frame.where.line, 4);
+ equal(step1.frame.where.column, 10);
+
+ const step2 = await stepOver(threadFront);
+ equal(step2.frame.where.line, 5);
+ equal(step2.frame.where.column, 10);
+ equal(debuggee.r, "yay");
+ await resume(threadFront);
+}
+
+async function testInterleaving({ threadFront, debuggee }) {
+ function evaluateTestCode() {
+ Cu.evalInSandbox(
+ `
+ (async function simpleRace() {
+ debugger;
+ this.result = await new Promise((r) => {
+ Promise.resolve().then(() => { debugger });
+ Promise.resolve().then(r('yay'))
+ })
+ var a = 2;
+ debugger;
+ })()
+ `,
+ debuggee,
+ "1.8",
+ sourceUrl,
+ 1
+ );
+ }
+
+ await executeOnNextTickAndWaitForPause(evaluateTestCode, threadFront);
+
+ dumpn("Step Over and land on line 5");
+ const step1 = await stepOver(threadFront);
+ equal(step1.frame.where.line, 4);
+
+ const step2 = await stepOver(threadFront);
+ equal(step2.frame.where.line, 5);
+ equal(step2.frame.where.column, 43);
+
+ const step3 = await resumeAndWaitForPause(threadFront);
+ equal(step3.frame.where.line, 9);
+ equal(debuggee.result, "yay");
+
+ await resume(threadFront);
+}
+
+async function testMultipleSteps({ threadFront, debuggee }) {
+ function evaluateTestCode() {
+ Cu.evalInSandbox(
+ `
+ (async function simpleRace() {
+ debugger;
+ await Promise.resolve();
+ var a = 2;
+ await Promise.resolve();
+ var b = 2;
+ await Promise.resolve();
+ debugger;
+ })()
+ `,
+ debuggee,
+ "1.8",
+ sourceUrl,
+ 1
+ );
+ }
+
+ await executeOnNextTickAndWaitForPause(evaluateTestCode, threadFront);
+
+ const step1 = await stepOver(threadFront);
+ equal(step1.frame.where.line, 4);
+
+ const step2 = await stepOver(threadFront);
+ equal(step2.frame.where.line, 5);
+
+ const step3 = await stepOver(threadFront);
+ equal(step3.frame.where.line, 6);
+ resume(threadFront);
+}
+
+async function testGenerator({ threadFront, debuggee }) {
+ function evaluateTestCode() {
+ Cu.evalInSandbox(
+ `
+ (async function() {
+ function* makeSteps() {
+ debugger;
+ yield 1;
+ yield 2;
+ return 3;
+ }
+ const s = makeSteps();
+ s.next();
+ s.next();
+ s.next();
+ })()
+ `,
+ debuggee,
+ "1.8",
+ sourceUrl,
+ 1
+ );
+ }
+
+ await executeOnNextTickAndWaitForPause(evaluateTestCode, threadFront);
+
+ const step1 = await stepOver(threadFront);
+ equal(step1.frame.where.line, 5);
+
+ const step2 = await stepOver(threadFront);
+ equal(step2.frame.where.line, 6);
+
+ const step3 = await stepOver(threadFront);
+ equal(step3.frame.where.line, 7);
+ await resume(threadFront);
+}
diff --git a/devtools/server/tests/xpcshell/test_stepping-13.js b/devtools/server/tests/xpcshell/test_stepping-13.js
new file mode 100644
index 0000000000..cbdb78ce2d
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_stepping-13.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/*
+ * Check that is possible to step into both the inner and outer function
+ * calls.
+ */
+
+add_task(
+ threadFrontTest(async ({ commands, threadFront }) => {
+ dumpn("Evaluating test code and waiting for first debugger statement");
+
+ commands.scriptCommand.execute(
+ `(function () {
+ const a = () => { return 2 };
+ debugger;
+ a(a())
+ })()`
+ );
+
+ await waitForEvent(threadFront, "paused");
+ const step1 = await stepOver(threadFront);
+ Assert.equal(step1.frame.where.line, 4, "step to line 4");
+
+ const step2 = await stepIn(threadFront);
+ Assert.equal(step2.frame.where.line, 2, "step in to line 2");
+
+ const step3 = await stepOut(threadFront);
+ Assert.equal(step3.frame.where.line, 4, "step back to line 4");
+ Assert.equal(step3.frame.where.column, 9, "step out to column 9");
+
+ const step4 = await stepIn(threadFront);
+ Assert.equal(step4.frame.where.line, 2, "step in to line 2");
+
+ await threadFront.resume();
+ })
+);
diff --git a/devtools/server/tests/xpcshell/test_stepping-14.js b/devtools/server/tests/xpcshell/test_stepping-14.js
new file mode 100644
index 0000000000..6d64a53a66
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_stepping-14.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/*
+ * Check that is possible to step into both the inner and outer function
+ * calls.
+ */
+
+add_task(
+ threadFrontTest(async ({ commands, threadFront }) => {
+ dumpn("Evaluating test code and waiting for first debugger statement");
+
+ commands.scriptCommand.execute(`(function () {
+ async function f() {
+ const p = Promise.resolve(43);
+ await p;
+ return p;
+ }
+
+ function call_f() {
+ Promise.resolve(42).then(forty_two => {
+ return forty_two;
+ });
+
+ f().then(v => {
+ return v;
+ });
+ }
+ debugger;
+ call_f();
+ })()`);
+
+ const packet = await waitForEvent(threadFront, "paused");
+ const location = {
+ sourceId: packet.frame.where.actor,
+ line: 4,
+ column: 10,
+ };
+
+ await threadFront.setBreakpoint(location, {});
+
+ const packet2 = await resumeAndWaitForPause(threadFront);
+ Assert.equal(packet2.frame.where.line, 4, "landed at await");
+
+ const packet3 = await stepIn(threadFront);
+ Assert.equal(packet3.frame.where.line, 5, "step to the next line");
+
+ await threadFront.resume();
+ })
+);
diff --git a/devtools/server/tests/xpcshell/test_stepping-15.js b/devtools/server/tests/xpcshell/test_stepping-15.js
new file mode 100644
index 0000000000..9e79b93687
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_stepping-15.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test stepping from inside a blackboxed function
+ * test-page: https://dbg-blackbox-stepping.glitch.me/
+ */
+
+async function invokeAndPause({ global, threadFront }, expression, url) {
+ return executeOnNextTickAndWaitForPause(
+ () => Cu.evalInSandbox(expression, global, "1.8", url, 1),
+ threadFront
+ );
+}
+add_task(
+ threadFrontTest(async ({ commands, threadFront, debuggee }) => {
+ const dbg = { global: debuggee, threadFront };
+
+ // Test stepping from a blackboxed location
+ async function testStepping(action, expectedLine) {
+ commands.scriptCommand.execute(`outermost()`);
+ await waitForPause(threadFront);
+ await blackBox(blackboxedSourceFront);
+ const packet = await action(threadFront);
+ const { line, actor } = packet.frame.where;
+ equal(actor, unblackboxedActor, "paused in unblackboxed source");
+ equal(line, expectedLine, "paused at correct line");
+ await threadFront.resume();
+ await unBlackBox(blackboxedSourceFront);
+ }
+
+ invokeAndPause(
+ dbg,
+ `function outermost() {
+ const value = blackboxed1();
+ return value + 1;
+ }
+ function innermost() {
+ return 1;
+ }`,
+ "http://example.com/unblackboxed.js"
+ );
+ invokeAndPause(
+ dbg,
+ `function blackboxed1() {
+ return blackboxed2();
+ }
+ function blackboxed2() {
+ return innermost();
+ }`,
+ "http://example.com/blackboxed.js"
+ );
+
+ const { sources } = await getSources(threadFront);
+ const blackboxedSourceFront = threadFront.source(
+ sources.find(source => source.url == "http://example.com/blackboxed.js")
+ );
+ const unblackboxedActor = sources.find(
+ source => source.url == "http://example.com/unblackboxed.js"
+ ).actor;
+
+ await setBreakpoint(threadFront, {
+ sourceUrl: blackboxedSourceFront.url,
+ line: 5,
+ });
+
+ info("Step Out to outermost");
+ await testStepping(stepOut, 3);
+
+ info("Step Over to outermost");
+ await testStepping(stepOver, 3);
+
+ info("Step In to innermost");
+ await testStepping(stepIn, 6);
+ })
+);
diff --git a/devtools/server/tests/xpcshell/test_stepping-16.js b/devtools/server/tests/xpcshell/test_stepping-16.js
new file mode 100644
index 0000000000..e3bd94b747
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_stepping-16.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test stepping from inside a blackboxed function
+ * test-page: https://dbg-blackbox-stepping2.glitch.me/
+ */
+
+async function invokeAndPause({ global, threadFront }, expression, url) {
+ return executeOnNextTickAndWaitForPause(
+ () => Cu.evalInSandbox(expression, global, "1.8", url, 1),
+ threadFront
+ );
+}
+
+add_task(
+ threadFrontTest(async ({ commands, threadFront, debuggee }) => {
+ const dbg = { global: debuggee, threadFront };
+ invokeAndPause(
+ dbg,
+ `function outermost() {
+ blackboxed(
+ function inner1() {
+ return 1;
+ },
+ function inner2() {
+ return 2;
+ }
+ );
+ }`,
+ "http://example.com/unblackboxed.js"
+ );
+ invokeAndPause(
+ dbg,
+ `function blackboxed(...args) {
+ for (const arg of args) {
+ arg();
+ }
+ }`,
+ "http://example.com/blackboxed.js"
+ );
+
+ const { sources } = await getSources(threadFront);
+ const blackboxedSourceFront = threadFront.source(
+ sources.find(source => source.url == "http://example.com/blackboxed.js")
+ );
+ const unblackboxedSource = sources.find(
+ source => source.url == "http://example.com/unblackboxed.js"
+ );
+ const unblackboxedActor = unblackboxedSource.actor;
+ const unblackboxedSourceFront = threadFront.source(unblackboxedSource);
+
+ await setBreakpoint(threadFront, {
+ sourceUrl: unblackboxedSourceFront.url,
+ line: 4,
+ });
+ blackBox(blackboxedSourceFront);
+
+ async function testStepping(action, expectedLine) {
+ commands.scriptCommand.execute("outermost()");
+ await waitForPause(threadFront);
+ await stepOver(threadFront);
+ const packet = await action(threadFront);
+ const { actor, line } = packet.frame.where;
+ equal(actor, unblackboxedActor, "Paused in unblackboxed source");
+ equal(line, expectedLine, "Paused at correct line");
+ await threadFront.resume();
+ }
+
+ info("Step Out to outermost");
+ await testStepping(stepOut, 10);
+
+ info("Step Over to outermost");
+ await testStepping(stepOver, 10);
+
+ info("Step In to inner2");
+ await testStepping(stepIn, 7);
+ })
+);
diff --git a/devtools/server/tests/xpcshell/test_stepping-17.js b/devtools/server/tests/xpcshell/test_stepping-17.js
new file mode 100644
index 0000000000..816946fa4c
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_stepping-17.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/*
+ * Check that you can step from one script or event to another
+ */
+
+add_task(
+ threadFrontTest(async ({ commands, threadFront, debuggee }) => {
+ Cu.evalInSandbox(
+ `function blackboxed(callback) { return () => callback(); }`,
+ debuggee,
+ "1.8",
+ "http://example.com/blackboxed.js",
+ 1
+ );
+
+ const { sources } = await getSources(threadFront);
+ const blackboxedSourceFront = threadFront.source(
+ sources.find(source => source.url == "http://example.com/blackboxed.js")
+ );
+ blackBox(blackboxedSourceFront);
+
+ const testStepping = async function (wrapperName, stepHandler, message) {
+ commands.scriptCommand.execute(`(function () {
+ const p = Promise.resolve();
+ p.then(${wrapperName}(() => { debugger; }))
+ .then(${wrapperName}(() => { }));
+ })();`);
+
+ await waitForEvent(threadFront, "paused");
+ const step = await stepHandler(threadFront);
+ Assert.equal(step.frame.where.line, 4, message);
+ await resume(threadFront);
+ };
+
+ const stepTwice = async function () {
+ await stepOver(threadFront);
+ return stepOver(threadFront);
+ };
+
+ await testStepping("", stepTwice, "Step over on the outermost frame");
+ await testStepping("blackboxed", stepTwice, "Step over with blackboxing");
+ await testStepping("", stepOut, "Step out on the outermost frame");
+ await testStepping("blackboxed", stepOut, "Step out with blackboxing");
+
+ commands.scriptCommand.execute(`(async function () {
+ const p = Promise.resolve();
+ const p2 = p.then(() => {
+ debugger;
+ return "async stepping!";
+ });
+ debugger;
+ await p;
+ const result = await p2;
+ return result;
+ })();
+ `);
+
+ await waitForEvent(threadFront, "paused");
+ await stepOver(threadFront);
+ await stepOver(threadFront);
+ const step = await stepOut(threadFront);
+ await resume(threadFront);
+ Assert.equal(step.frame.where.line, 9, "Step out of promise into async fn");
+ })
+);
diff --git a/devtools/server/tests/xpcshell/test_stepping-18.js b/devtools/server/tests/xpcshell/test_stepping-18.js
new file mode 100644
index 0000000000..e8581835d3
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_stepping-18.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check scenarios where we're leaving function a and
+ * going to the function b's call-site.
+ */
+
+async function testFinish({ threadFront, devToolsClient }) {
+ await close(devToolsClient);
+
+ do_test_finished();
+}
+
+async function invokeAndPause({ global, threadFront }, expression) {
+ return executeOnNextTickAndWaitForPause(
+ () => Cu.evalInSandbox(expression, global),
+ threadFront
+ );
+}
+
+async function steps(threadFront, sequence) {
+ const locations = [];
+ for (const cmd of sequence) {
+ const packet = await step(threadFront, cmd);
+ locations.push(getPauseLocation(packet));
+ }
+ return locations;
+}
+
+async function step(threadFront, cmd) {
+ return cmd(threadFront);
+}
+
+function getPauseLocation(packet) {
+ const { line, column } = packet.frame.where;
+ return { line, column };
+}
+
+async function stepOutOfA(dbg, func, frameIndex, expectedLocation) {
+ const { threadFront } = dbg;
+
+ info("pause and step into a()");
+ await invokeAndPause(dbg, `${func}()`);
+ await steps(threadFront, [stepOver, stepIn, stepIn]);
+
+ const { frames } = await threadFront.frames(0, 5);
+ const frameActorID = frames[frameIndex].actorID;
+ const packet = await stepOut(threadFront, frameActorID);
+
+ deepEqual(
+ getPauseLocation(packet),
+ expectedLocation,
+ `step over location in ${func}`
+ );
+
+ await dbg.threadFront.resume();
+}
+
+async function stepOverInA(dbg, func, frameIndex, expectedLocation) {
+ const { threadFront } = dbg;
+
+ info("pause and step into a()");
+ await invokeAndPause(dbg, `${func}()`);
+ await steps(threadFront, [stepOver, stepIn]);
+
+ const { frames } = await threadFront.frames(0, 5);
+ const frameActorID = frames[frameIndex].actorID;
+ const packet = await stepOver(threadFront, frameActorID);
+
+ deepEqual(
+ getPauseLocation(packet),
+ expectedLocation,
+ `step over location in ${func}`
+ );
+
+ await dbg.threadFront.resume();
+}
+
+function run_test() {
+ return (async function () {
+ const dbg = await setupTestFromUrl("stepping.js");
+
+ info(`Test step over with the 1st frame`);
+ await stepOverInA(dbg, "arithmetic", 0, { line: 8, column: 0 });
+
+ info(`Test step over with the 2nd frame`);
+ await stepOverInA(dbg, "arithmetic", 1, { line: 17, column: 0 });
+
+ info(`Test step out with the 1st frame`);
+ await stepOutOfA(dbg, "nested", 0, { line: 31, column: 0 });
+
+ info(`Test step out with the 2nd frame`);
+ await stepOutOfA(dbg, "nested", 1, { line: 36, column: 0 });
+
+ await testFinish(dbg);
+ })();
+}
diff --git a/devtools/server/tests/xpcshell/test_stepping-19.js b/devtools/server/tests/xpcshell/test_stepping-19.js
new file mode 100644
index 0000000000..7ab21c7b66
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_stepping-19.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that step out stops at the async parent's frame.
+ */
+
+async function testFinish({ threadFront, devToolsClient }) {
+ await close(devToolsClient);
+
+ do_test_finished();
+}
+
+async function invokeAndPause({ global, threadFront }, expression) {
+ return executeOnNextTickAndWaitForPause(
+ () => Cu.evalInSandbox(expression, global),
+ threadFront
+ );
+}
+
+async function steps(threadFront, sequence) {
+ const locations = [];
+ for (const cmd of sequence) {
+ const packet = await step(threadFront, cmd);
+ locations.push(getPauseLocation(packet));
+ }
+ return locations;
+}
+
+async function step(threadFront, cmd) {
+ return cmd(threadFront);
+}
+
+function getPauseLocation(packet) {
+ const { line, column } = packet.frame.where;
+ return { line, column };
+}
+
+async function stepOutBeforeTimer(dbg, func, frameIndex, expectedLocation) {
+ const { threadFront } = dbg;
+
+ await invokeAndPause(dbg, `${func}()`);
+ await steps(threadFront, [stepOver, stepIn]);
+
+ const { frames } = await threadFront.frames(0, 5);
+ const frameActorID = frames[frameIndex].actorID;
+ const packet = await stepOut(threadFront, frameActorID);
+
+ deepEqual(
+ getPauseLocation(packet),
+ expectedLocation,
+ `step out location in ${func}`
+ );
+
+ await resumeAndWaitForPause(threadFront);
+ await resume(threadFront);
+}
+
+async function stepOutAfterTimer(dbg, func, frameIndex, expectedLocation) {
+ const { threadFront } = dbg;
+
+ await invokeAndPause(dbg, `${func}()`);
+ await steps(threadFront, [stepOver, stepIn, stepOver, stepOver]);
+
+ const { frames } = await threadFront.frames(0, 5);
+ const frameActorID = frames[frameIndex].actorID;
+ const packet = await stepOut(threadFront, frameActorID);
+
+ deepEqual(
+ getPauseLocation(packet),
+ expectedLocation,
+ `step out location in ${func}`
+ );
+
+ await resumeAndWaitForPause(threadFront);
+ await dbg.threadFront.resume();
+}
+
+function run_test() {
+ return (async function () {
+ const dbg = await setupTestFromUrl("stepping-async.js");
+
+ info(`Test stepping out before timer;`);
+ await stepOutBeforeTimer(dbg, "stuff", 0, { line: 27, column: 2 });
+
+ info(`Test stepping out after timer;`);
+ await stepOutAfterTimer(dbg, "stuff", 0, { line: 29, column: 2 });
+
+ await testFinish(dbg);
+ })();
+}
diff --git a/devtools/server/tests/xpcshell/test_stepping-with-skip-breakpoints.js b/devtools/server/tests/xpcshell/test_stepping-with-skip-breakpoints.js
new file mode 100644
index 0000000000..3ec4fd994d
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_stepping-with-skip-breakpoints.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check basic step-over functionality with pause points
+ * for the first statement and end of the last statement.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ dumpn("Evaluating test code and waiting for first debugger statement");
+ const dbgStmt = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+ equal(
+ dbgStmt.frame.where.line,
+ 2,
+ "Should be at debugger statement on line 2"
+ );
+ equal(debuggee.a, undefined);
+ equal(debuggee.b, undefined);
+
+ const source = await getSource(
+ threadFront,
+ "test_stepping-01-test-code.js"
+ );
+
+ // Add pause points for the first and end of the last statement.
+ // Note: we intentionally ignore the second statement.
+ source.setPausePoints([
+ {
+ location: { line: 3, column: 8 },
+ types: { breakpoint: true, stepOver: true },
+ },
+ {
+ location: { line: 4, column: 14 },
+ types: { breakpoint: true, stepOver: true },
+ },
+ ]);
+
+ dumpn("Step Over to line 3");
+ const step1 = await stepOver(threadFront);
+ equal(step1.why.type, "resumeLimit");
+ equal(step1.frame.where.line, 3);
+ equal(step1.frame.where.column, 12);
+
+ equal(debuggee.a, undefined);
+ equal(debuggee.b, undefined);
+
+ dumpn("Step Over to line 4");
+ const step2 = await stepOver(threadFront);
+ equal(step2.why.type, "resumeLimit");
+ equal(step2.frame.where.line, 4);
+ equal(step2.frame.where.column, 12);
+
+ equal(debuggee.a, 1);
+ equal(debuggee.b, undefined);
+
+ dumpn("Step Over to the end of line 4");
+ const step3 = await stepOver(threadFront);
+ equal(step3.why.type, "resumeLimit");
+ equal(step3.frame.where.line, 4);
+ equal(step3.frame.where.column, 14);
+ equal(debuggee.a, 1);
+ equal(debuggee.b, 2);
+ })
+);
+
+function evaluateTestCode(debuggee) {
+ // prettier-ignore
+ Cu.evalInSandbox(
+ ` // 1
+ debugger; // 2
+ var a = 1; // 3
+ var b = 2;`, // 4
+ debuggee,
+ "1.8",
+ "test_stepping-01-test-code.js",
+ 1
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_symbolactor.js b/devtools/server/tests/xpcshell/test_symbolactor.js
new file mode 100644
index 0000000000..0d04a2bd1d
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_symbolactor.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ SymbolActor,
+} = require("resource://devtools/server/actors/object/symbol.js");
+
+function run_test() {
+ test_SA_destroy();
+ test_SA_form();
+ test_SA_raw();
+}
+
+const SYMBOL_NAME = "abc";
+const TEST_SYMBOL = Symbol(SYMBOL_NAME);
+
+function makeMockSymbolActor() {
+ const symbol = TEST_SYMBOL;
+ const mockConn = null;
+ const actor = new SymbolActor(mockConn, symbol);
+ actor.actorID = "symbol1";
+ const parentPool = {
+ symbolActors: {
+ [symbol]: actor,
+ },
+ unmanage: () => {},
+ };
+ actor.getParent = () => parentPool;
+ return actor;
+}
+
+function test_SA_destroy() {
+ const actor = makeMockSymbolActor();
+ strictEqual(actor.getParent().symbolActors[TEST_SYMBOL], actor);
+
+ actor.destroy();
+ strictEqual(TEST_SYMBOL in actor.getParent().symbolActors, false);
+}
+
+function test_SA_form() {
+ const actor = makeMockSymbolActor();
+ const form = actor.form();
+ strictEqual(form.type, "symbol");
+ strictEqual(form.actor, actor.actorID);
+ strictEqual(form.name, SYMBOL_NAME);
+}
+
+function test_SA_raw() {
+ const actor = makeMockSymbolActor();
+ strictEqual(actor.rawValue(), TEST_SYMBOL);
+}
diff --git a/devtools/server/tests/xpcshell/test_symbols-01.js b/devtools/server/tests/xpcshell/test_symbols-01.js
new file mode 100644
index 0000000000..5352542e83
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_symbols-01.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that we can represent ES6 Symbols over the RDP.
+ */
+
+const URL = "foo.js";
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ await testSymbols(threadFront, debuggee);
+ })
+);
+
+async function testSymbols(threadFront, debuggee) {
+ const evalCode = () => {
+ /* eslint-disable mozilla/var-only-at-top-level, no-unused-vars */
+ // prettier-ignore
+ Cu.evalInSandbox(
+ "(" + function () {
+ var symbolWithName = Symbol("Chris");
+ var symbolWithoutName = Symbol();
+ var iteratorSymbol = Symbol.iterator;
+ debugger;
+ } + "())",
+ debuggee,
+ "1.8",
+ URL,
+ 1
+ );
+ /* eslint-enable mozilla/var-only-at-top-level, no-unused-vars */
+ };
+
+ const packet = await executeOnNextTickAndWaitForPause(evalCode, threadFront);
+ const environment = await packet.frame.getEnvironment();
+ const { symbolWithName, symbolWithoutName, iteratorSymbol } =
+ environment.bindings.variables;
+
+ equal(symbolWithName.value.type, "symbol");
+ equal(symbolWithName.value.name, "Chris");
+
+ equal(symbolWithoutName.value.type, "symbol");
+ ok(!("name" in symbolWithoutName.value));
+
+ equal(iteratorSymbol.value.type, "symbol");
+ equal(iteratorSymbol.value.name, "Symbol.iterator");
+}
diff --git a/devtools/server/tests/xpcshell/test_symbols-02.js b/devtools/server/tests/xpcshell/test_symbols-02.js
new file mode 100644
index 0000000000..12d4ef80c8
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_symbols-02.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that we don't run debuggee code when getting symbol names.
+ */
+
+const URL = "foo.js";
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ await testSymbols(threadFront, debuggee);
+ })
+);
+
+async function testSymbols(threadFront, debuggee) {
+ const evalCode = () => {
+ /* eslint-disable mozilla/var-only-at-top-level, no-extend-native, no-unused-vars */
+ // prettier-ignore
+ Cu.evalInSandbox(
+ "(" + function () {
+ Symbol.prototype.toString = () => {
+ throw new Error("lololol");
+ };
+ var sym = Symbol("le troll");
+ debugger;
+ } + "())",
+ debuggee,
+ "1.8",
+ URL,
+ 1
+ );
+ /* eslint-enable mozilla/var-only-at-top-level, no-extend-native, no-unused-vars */
+ };
+
+ const packet = await executeOnNextTickAndWaitForPause(evalCode, threadFront);
+ const environment = await packet.frame.getEnvironment();
+ const { sym } = environment.bindings.variables;
+
+ equal(sym.value.type, "symbol");
+ equal(sym.value.name, "le troll");
+}
diff --git a/devtools/server/tests/xpcshell/test_threadlifetime-01.js b/devtools/server/tests/xpcshell/test_threadlifetime-01.js
new file mode 100644
index 0000000000..d2e8234fb9
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_threadlifetime-01.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that thread-lifetime grips last past a resume.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee, client }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+
+ const pauseGrip = packet.frame.arguments[0];
+
+ // Create a thread-lifetime actor for this object.
+ const response = await client.request({
+ to: pauseGrip.actor,
+ type: "threadGrip",
+ });
+ // Successful promotion won't return an error.
+ Assert.equal(response.error, undefined);
+
+ const packet2 = await resumeAndWaitForPause(threadFront);
+
+ // Verify that the promoted actor is returned again.
+ Assert.equal(pauseGrip.actor, packet2.frame.arguments[0].actor);
+ // Now that we've resumed, should get unrecognizePacketType for the
+ // promoted grip.
+ try {
+ await client.request({ to: pauseGrip.actor, type: "bogusRequest" });
+ ok(false, "bogusRequest should throw");
+ } catch (e) {
+ Assert.equal(e.error, "unrecognizedPacketType");
+ ok(true, "bogusRequest thrown");
+ }
+ await threadFront.resume();
+ })
+);
+
+function evaluateTestCode(debuggee) {
+ debuggee.eval(
+ "(" +
+ function () {
+ function stopMe(arg1) {
+ debugger;
+ debugger;
+ }
+ stopMe({ obj: true });
+ } +
+ ")()"
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_threadlifetime-02.js b/devtools/server/tests/xpcshell/test_threadlifetime-02.js
new file mode 100644
index 0000000000..c35350a48c
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_threadlifetime-02.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that thread-lifetime grips last past a resume.
+ */
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee, client }) => {
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+
+ const pauseGrip = packet.frame.arguments[0];
+
+ // Create a thread-lifetime actor for this object.
+ const response = await client.request({
+ to: pauseGrip.actor,
+ type: "threadGrip",
+ });
+ // Successful promotion won't return an error.
+ Assert.equal(response.error, undefined);
+
+ const packet2 = await resumeAndWaitForPause(threadFront);
+
+ // Verify that the promoted actor is returned again.
+ Assert.equal(pauseGrip.actor, packet2.frame.arguments[0].actor);
+ // Now that we've resumed, release the thread-lifetime grip.
+ const objFront = new ObjectFront(
+ threadFront.conn,
+ threadFront.targetFront,
+ threadFront,
+ pauseGrip
+ );
+ await objFront.release();
+ const objFront2 = new ObjectFront(
+ threadFront.conn,
+ threadFront.targetFront,
+ threadFront,
+ pauseGrip
+ );
+
+ try {
+ await objFront2
+ .request({ to: pauseGrip.actor, type: "bogusRequest" })
+ .catch(function (error) {
+ Assert.ok(!!error.message.match(/noSuchActor/));
+ threadFront.resume();
+ throw new Error();
+ });
+ ok(false, "bogusRequest should throw");
+ } catch (e) {
+ ok(true, "bogusRequest thrown");
+ }
+ })
+);
+
+function evaluateTestCode(debuggee) {
+ debuggee.eval(
+ "(" +
+ function () {
+ function stopMe(arg1) {
+ debugger;
+ debugger;
+ }
+ stopMe({ obj: true });
+ } +
+ ")()"
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_threadlifetime-04.js b/devtools/server/tests/xpcshell/test_threadlifetime-04.js
new file mode 100644
index 0000000000..6b815c7933
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_threadlifetime-04.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-shadow, max-nested-callbacks */
+
+"use strict";
+
+/**
+ * Check that requesting a thread-lifetime actor twice for the same
+ * value returns the same actor.
+ */
+
+var gDebuggee;
+var gClient;
+var gThreadFront;
+
+add_task(
+ threadFrontTest(
+ async ({ threadFront, debuggee, client }) => {
+ gThreadFront = threadFront;
+ gClient = client;
+ gDebuggee = debuggee;
+ test_thread_lifetime();
+ },
+ { waitForFinish: true }
+ )
+);
+
+function test_thread_lifetime() {
+ gThreadFront.once("paused", async function (packet) {
+ const pauseGrip = packet.frame.arguments[0];
+
+ const response = await gClient.request({
+ to: pauseGrip.actor,
+ type: "threadGrip",
+ });
+ const threadGrip1 = response.from;
+
+ const response2 = await gClient.request({
+ to: pauseGrip.actor,
+ type: "threadGrip",
+ });
+ Assert.equal(threadGrip1, response2.from);
+ await gThreadFront.resume();
+
+ threadFrontTestFinished();
+ });
+
+ gDebuggee.eval(
+ "(" +
+ function () {
+ function stopMe(arg1) {
+ debugger;
+ }
+ stopMe({ obj: true });
+ } +
+ ")()"
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_unsafeDereference.js b/devtools/server/tests/xpcshell/test_unsafeDereference.js
new file mode 100644
index 0000000000..53b70420c6
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_unsafeDereference.js
@@ -0,0 +1,130 @@
+// Any copyright is dedicated to the Public Domain.
+// http://creativecommons.org/publicdomain/zero/1.0/
+
+/* eslint-disable strict */
+
+// Test Debugger.Object.prototype.unsafeDereference in the presence of
+// interesting cross-compartment wrappers.
+//
+// This is not really a devtools server test; it's more of a Debugger test.
+// But we need xpcshell and Components.utils.Sandbox to get
+// cross-compartment wrappers with interesting properties, and this is the
+// xpcshell test directory most closely related to the JS Debugger API.
+
+addDebuggerToGlobal(globalThis);
+
+// Add a method to Debugger.Object for fetching value properties
+// conveniently.
+Debugger.Object.prototype.getProperty = function (name) {
+ const desc = this.getOwnPropertyDescriptor(name);
+ if (!desc) {
+ return undefined;
+ }
+ if (!desc.value) {
+ throw Error(
+ "Debugger.Object.prototype.getProperty: " +
+ "not a value property: " +
+ name
+ );
+ }
+ return desc.value;
+};
+
+function run_test() {
+ // Create a low-privilege sandbox, and a chrome-privilege sandbox.
+ const contentBox = Cu.Sandbox("http://www.example.com");
+ const chromeBox = Cu.Sandbox(this);
+
+ // Create an objects in this compartment, and one in each sandbox. We'll
+ // refer to the objects as "mainObj", "contentObj", and "chromeObj", in
+ // variable and property names.
+ const mainObj = { name: "mainObj" };
+ Cu.evalInSandbox('var contentObj = { name: "contentObj" };', contentBox);
+ Cu.evalInSandbox('var chromeObj = { name: "chromeObj" };', chromeBox);
+
+ // Give each global a pointer to all the other globals' objects.
+ contentBox.mainObj = chromeBox.mainObj = mainObj;
+ const contentObj = (chromeBox.contentObj = contentBox.contentObj);
+ const chromeObj = (contentBox.chromeObj = chromeBox.chromeObj);
+
+ // First, a whole bunch of basic sanity checks, to ensure that JavaScript
+ // evaluated in various scopes really does see the world the way this
+ // test expects it to.
+
+ // The objects appear as global variables in the sandbox, and as
+ // the sandbox object's properties in chrome.
+ Assert.ok(Cu.evalInSandbox("mainObj", contentBox) === contentBox.mainObj);
+ Assert.ok(
+ Cu.evalInSandbox("contentObj", contentBox) === contentBox.contentObj
+ );
+ Assert.ok(Cu.evalInSandbox("chromeObj", contentBox) === contentBox.chromeObj);
+ Assert.ok(Cu.evalInSandbox("mainObj", chromeBox) === chromeBox.mainObj);
+ Assert.ok(Cu.evalInSandbox("contentObj", chromeBox) === chromeBox.contentObj);
+ Assert.ok(Cu.evalInSandbox("chromeObj", chromeBox) === chromeBox.chromeObj);
+
+ // We (the main global) can see properties of all objects in all globals.
+ Assert.ok(contentBox.mainObj.name === "mainObj");
+ Assert.ok(contentBox.contentObj.name === "contentObj");
+ Assert.ok(contentBox.chromeObj.name === "chromeObj");
+
+ // chromeBox can see properties of all objects in all globals.
+ Assert.equal(Cu.evalInSandbox("mainObj.name", chromeBox), "mainObj");
+ Assert.equal(Cu.evalInSandbox("contentObj.name", chromeBox), "contentObj");
+ Assert.equal(Cu.evalInSandbox("chromeObj.name", chromeBox), "chromeObj");
+
+ // contentBox can see properties of the content object, but not of either
+ // chrome object, because by default, content -> chrome wrappers hide all
+ // object properties.
+ Assert.equal(Cu.evalInSandbox("mainObj.name", contentBox), undefined);
+ Assert.equal(Cu.evalInSandbox("contentObj.name", contentBox), "contentObj");
+ Assert.equal(Cu.evalInSandbox("chromeObj.name", contentBox), undefined);
+
+ // When viewing an object in compartment A from the vantage point of
+ // compartment B, Debugger should give the same results as debuggee code
+ // would.
+
+ // Create a debugger, debugging our two sandboxes.
+ const dbg = new Debugger();
+
+ // Create Debugger.Object instances referring to the two sandboxes, as
+ // seen from their own compartments.
+ const contentBoxDO = dbg.addDebuggee(contentBox);
+ const chromeBoxDO = dbg.addDebuggee(chromeBox);
+
+ // Use Debugger to view the objects from contentBox. We should get the
+ // same D.O instance from both getProperty and makeDebuggeeValue, and the
+ // same property visibility we checked for above.
+ const mainFromContentDO = contentBoxDO.getProperty("mainObj");
+ Assert.equal(mainFromContentDO, contentBoxDO.makeDebuggeeValue(mainObj));
+ Assert.equal(mainFromContentDO.getProperty("name"), undefined);
+ Assert.equal(mainFromContentDO.unsafeDereference(), mainObj);
+
+ const contentFromContentDO = contentBoxDO.getProperty("contentObj");
+ Assert.equal(
+ contentFromContentDO,
+ contentBoxDO.makeDebuggeeValue(contentObj)
+ );
+ Assert.equal(contentFromContentDO.getProperty("name"), "contentObj");
+ Assert.equal(contentFromContentDO.unsafeDereference(), contentObj);
+
+ const chromeFromContentDO = contentBoxDO.getProperty("chromeObj");
+ Assert.equal(chromeFromContentDO, contentBoxDO.makeDebuggeeValue(chromeObj));
+ Assert.equal(chromeFromContentDO.getProperty("name"), undefined);
+ Assert.equal(chromeFromContentDO.unsafeDereference(), chromeObj);
+
+ // Similarly, viewing from chromeBox.
+ const mainFromChromeDO = chromeBoxDO.getProperty("mainObj");
+ Assert.equal(mainFromChromeDO, chromeBoxDO.makeDebuggeeValue(mainObj));
+ Assert.equal(mainFromChromeDO.getProperty("name"), "mainObj");
+ Assert.equal(mainFromChromeDO.unsafeDereference(), mainObj);
+
+ const contentFromChromeDO = chromeBoxDO.getProperty("contentObj");
+ Assert.equal(contentFromChromeDO, chromeBoxDO.makeDebuggeeValue(contentObj));
+ Assert.equal(contentFromChromeDO.getProperty("name"), "contentObj");
+ Assert.equal(contentFromChromeDO.unsafeDereference(), contentObj);
+
+ const chromeFromChromeDO = chromeBoxDO.getProperty("chromeObj");
+ Assert.equal(chromeFromChromeDO, chromeBoxDO.makeDebuggeeValue(chromeObj));
+ Assert.equal(chromeFromChromeDO.getProperty("name"), "chromeObj");
+ Assert.equal(chromeFromChromeDO.unsafeDereference(), chromeObj);
+}
diff --git a/devtools/server/tests/xpcshell/test_wasm_source-01.js b/devtools/server/tests/xpcshell/test_wasm_source-01.js
new file mode 100644
index 0000000000..fe8e43e236
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_wasm_source-01.js
@@ -0,0 +1,143 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-shadow, max-nested-callbacks */
+
+"use strict";
+
+/**
+ * Verify if client can receive binary wasm
+ */
+
+var gDebuggee;
+var gThreadFront;
+
+add_task(
+ threadFrontTest(
+ async ({ threadFront, debuggee, client }) => {
+ gThreadFront = threadFront;
+ gDebuggee = debuggee;
+
+ await gThreadFront.reconfigure({
+ observeAsmJS: true,
+ observeWasm: true,
+ });
+
+ test_source();
+ },
+ { waitForFinish: true, doNotRunWorker: true }
+ )
+);
+
+const EXPECTED_CONTENT = String.fromCharCode(
+ 0,
+ 97,
+ 115,
+ 109,
+ 1,
+ 0,
+ 0,
+ 0,
+ 1,
+ 132,
+ 128,
+ 128,
+ 128,
+ 0,
+ 1,
+ 96,
+ 0,
+ 0,
+ 3,
+ 130,
+ 128,
+ 128,
+ 128,
+ 0,
+ 1,
+ 0,
+ 6,
+ 129,
+ 128,
+ 128,
+ 128,
+ 0,
+ 0,
+ 7,
+ 133,
+ 128,
+ 128,
+ 128,
+ 0,
+ 1,
+ 1,
+ 102,
+ 0,
+ 0,
+ 10,
+ 136,
+ 128,
+ 128,
+ 128,
+ 0,
+ 1,
+ 130,
+ 128,
+ 128,
+ 128,
+ 0,
+ 0,
+ 11
+);
+
+function test_source() {
+ gThreadFront.once("paused", function (packet) {
+ gThreadFront.getSources().then(function (response) {
+ Assert.ok(!!response);
+ Assert.ok(!!response.sources);
+
+ const source = response.sources.filter(function (s) {
+ return s.introductionType === "wasm";
+ })[0];
+
+ Assert.ok(!!source);
+
+ const sourceFront = gThreadFront.source(source);
+ sourceFront.source().then(function (response) {
+ Assert.ok(!!response);
+ Assert.ok(!!response.contentType);
+ Assert.ok(response.contentType.includes("wasm"));
+
+ const sourceContent = response.source;
+ Assert.ok(!!sourceContent);
+ Assert.equal(typeof sourceContent, "object");
+ Assert.ok("binary" in sourceContent);
+ Assert.equal(EXPECTED_CONTENT, sourceContent.binary);
+
+ gThreadFront.resume().then(function () {
+ threadFrontTestFinished();
+ });
+ });
+ });
+ });
+
+ /* eslint-disable comma-spacing, max-len */
+ gDebuggee.eval(
+ "(" +
+ function () {
+ // WebAssembly bytecode was generated by running:
+ // js -e 'print(wasmTextToBinary("(module(func(export \"f\")))"))'
+ const m = new WebAssembly.Module(
+ new Uint8Array([
+ 0, 97, 115, 109, 1, 0, 0, 0, 1, 132, 128, 128, 128, 0, 1, 96, 0, 0,
+ 3, 130, 128, 128, 128, 0, 1, 0, 6, 129, 128, 128, 128, 0, 0, 7, 133,
+ 128, 128, 128, 0, 1, 1, 102, 0, 0, 10, 136, 128, 128, 128, 0, 1,
+ 130, 128, 128, 128, 0, 0, 11,
+ ])
+ );
+ const i = new WebAssembly.Instance(m);
+ debugger;
+ i.exports.f();
+ } +
+ ")()"
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_watchpoint-01.js b/devtools/server/tests/xpcshell/test_watchpoint-01.js
new file mode 100644
index 0000000000..2d1d0e78f4
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_watchpoint-01.js
@@ -0,0 +1,197 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-shadow */
+
+"use strict";
+
+/*
+- Tests adding set and get watchpoints.
+- Tests removing a watchpoint.
+- Tests removing all watchpoints.
+*/
+
+add_task(
+ threadFrontTest(async args => {
+ await testSetWatchpoint(args);
+ await testGetWatchpoint(args);
+ await testRemoveWatchpoint(args);
+ await testRemoveWatchpoints(args);
+ })
+);
+
+async function testSetWatchpoint({ commands, threadFront, debuggee }) {
+ async function evaluateJS(input) {
+ const { result } = await commands.scriptCommand.execute(input, {
+ thread: threadFront.actor,
+ frameActor: packet.frame.actorID,
+ });
+ return result;
+ }
+
+ function evaluateTestCode(debuggee) {
+ /* eslint-disable */
+ Cu.evalInSandbox(
+ ` // 1
+ function stopMe(obj) { // 2
+ debugger; // 3
+ obj.a = 2; // 4
+ } //
+ stopMe({a: { b: 1 }})`,
+ debuggee,
+ "1.8",
+ "test_watchpoint-01.js"
+ );
+ /* eslint-disable */
+ }
+
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+
+ info("Test that we paused on the debugger statement");
+ Assert.equal(packet.frame.where.line, 3);
+
+ info("Add set watchpoint");
+ const args = packet.frame.arguments;
+ const obj = args[0];
+ const objClient = threadFront.pauseGrip(obj);
+ await objClient.addWatchpoint("a", "obj.a", "set");
+
+ let result = await evaluateJS("obj.a");
+ Assert.equal(result.getGrip().preview.ownProperties.b.value, 1);
+
+ result = await evaluateJS("obj.a.b");
+ Assert.equal(result, 1);
+
+ info("Test that watchpoint triggers pause on set");
+ const packet2 = await resumeAndWaitForPause(threadFront);
+ Assert.equal(packet2.frame.where.line, 4);
+ Assert.equal(packet2.why.type, "setWatchpoint");
+ Assert.equal(obj.preview.ownProperties.a.value.ownPropertyLength, 1);
+
+ await resume(threadFront);
+}
+
+async function testGetWatchpoint({ threadFront, debuggee }) {
+ function evaluateTestCode(debuggee) {
+ /* eslint-disable */
+ Cu.evalInSandbox(
+ ` // 1
+ function stopMe(obj) { // 2
+ debugger; // 3
+ obj.a + 4; // 4
+ } //
+ stopMe({a: 1})`,
+ debuggee,
+ "1.8",
+ "test_watchpoint-01.js"
+ );
+ /* eslint-disable */
+ }
+
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+
+ info("Test that we paused on the debugger statement");
+ Assert.equal(packet.frame.where.line, 3);
+
+ info("Add get watchpoint.");
+ const args = packet.frame.arguments;
+ const obj = args[0];
+ const objClient = threadFront.pauseGrip(obj);
+ await objClient.addWatchpoint("a", "obj.a", "get");
+
+ info("Test that watchpoint triggers pause on get.");
+ const packet2 = await resumeAndWaitForPause(threadFront);
+ Assert.equal(packet2.frame.where.line, 4);
+ Assert.equal(packet2.why.type, "getWatchpoint");
+ Assert.equal(obj.preview.ownProperties.a.value, 1);
+
+ await resume(threadFront);
+}
+
+async function testRemoveWatchpoint({ threadFront, debuggee }) {
+ function evaluateTestCode(debuggee) {
+ /* eslint-disable */
+ Cu.evalInSandbox(
+ ` // 1
+ function stopMe(obj) { // 2
+ debugger; // 3
+ obj.a = 2; // 4
+ debugger; // 5
+ } //
+
+ stopMe({a: 1})`,
+ debuggee,
+ "1.8",
+ "test_watchpoint-01.js"
+ );
+ /* eslint-disable */
+ }
+
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+
+ info(`Test that we paused on the debugger statement`);
+ Assert.equal(packet.frame.where.line, 3);
+
+ info(`Add set watchpoint`);
+ const args = packet.frame.arguments;
+ const obj = args[0];
+ const objClient = threadFront.pauseGrip(obj);
+ await objClient.addWatchpoint("a", "obj.a", "set");
+
+ info(`Remove set watchpoint`);
+ await objClient.removeWatchpoint("a");
+
+ info(`Test that we do not pause on set`);
+ const packet2 = await resumeAndWaitForPause(threadFront);
+ Assert.equal(packet2.frame.where.line, 5);
+
+ await resume(threadFront);
+}
+
+async function testRemoveWatchpoints({ threadFront, debuggee }) {
+ function evaluateTestCode(debuggee) {
+ /* eslint-disable */
+ Cu.evalInSandbox(
+ ` // 1
+ function stopMe(obj) { // 2
+ debugger; // 3
+ obj.a = 2; // 4
+ debugger; // 5
+ } //
+ stopMe({a: 1})`,
+ debuggee,
+ "1.8",
+ "test_watchpoint-01.js"
+ );
+ /* eslint-disable */
+ }
+
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+
+ info("Test that we paused on the debugger statement");
+ Assert.equal(packet.frame.where.line, 3);
+
+ info("Add and then remove set watchpoint");
+ const args = packet.frame.arguments;
+ const obj = args[0];
+ const objClient = threadFront.pauseGrip(obj);
+ await objClient.addWatchpoint("a", "obj.a", "set");
+ await objClient.removeWatchpoints();
+
+ info("Test that we do not pause on set");
+ const packet2 = await resumeAndWaitForPause(threadFront);
+ Assert.equal(packet2.frame.where.line, 5);
+
+ await resume(threadFront);
+}
diff --git a/devtools/server/tests/xpcshell/test_watchpoint-02.js b/devtools/server/tests/xpcshell/test_watchpoint-02.js
new file mode 100644
index 0000000000..d0739c8a00
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_watchpoint-02.js
@@ -0,0 +1,223 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-shadow */
+
+"use strict";
+
+/*
+Test that debugger advances instead of pausing twice on the
+same line when encountering both a watchpoint and a breakpoint.
+*/
+
+add_task(
+ threadFrontTest(async args => {
+ await testBreakpointAndSetWatchpoint(args);
+ await testBreakpointAndGetWatchpoint(args);
+ await testLoops(args);
+ })
+);
+
+// Test that we advance to the next line when a location
+// has both a breakpoint and set watchpoint.
+async function testBreakpointAndSetWatchpoint({
+ commands,
+ threadFront,
+ debuggee,
+}) {
+ async function evaluateJS(input) {
+ const { result } = await commands.scriptCommand.execute(input, {
+ frameActor: packet.frame.actorID,
+ });
+ return result;
+ }
+
+ function evaluateTestCode(debuggee) {
+ /* eslint-disable */
+ Cu.evalInSandbox(
+ ` // 1
+ function stopMe(obj) { // 2
+ debugger; // 3
+ obj.a = 2; // 4
+ debugger; // 5
+ } //
+ stopMe({a: 1})`,
+ debuggee,
+ "1.8",
+ "test_watchpoint-02.js"
+ );
+ /* eslint-disable */
+ }
+
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+
+ info("Test that we pause on the debugger statement.");
+ Assert.equal(packet.frame.where.line, 3);
+ Assert.equal(packet.why.type, "debuggerStatement");
+
+ info("Add set watchpoint.");
+ const args = packet.frame.arguments;
+ const obj = args[0];
+ const objClient = threadFront.pauseGrip(obj);
+ await objClient.addWatchpoint("a", "obj.a", "set");
+
+ info("Add breakpoint.");
+ const source = await getSourceById(threadFront, packet.frame.where.actor);
+
+ const location = {
+ sourceUrl: source.url,
+ line: 4,
+ };
+
+ threadFront.setBreakpoint(location, {});
+
+ info("Test that pause occurs on breakpoint.");
+ const packet2 = await resumeAndWaitForPause(threadFront);
+ Assert.equal(packet2.frame.where.line, 4);
+ Assert.equal(packet2.why.type, "breakpoint");
+
+ const packet3 = await resumeAndWaitForPause(threadFront);
+
+ info("Test that we pause on the second debugger statement.");
+ Assert.equal(packet3.frame.where.line, 5);
+ Assert.equal(packet3.why.type, "debuggerStatement");
+
+ info("Test that the value has updated.");
+ const result = await evaluateJS("obj.a");
+ Assert.equal(result, 2);
+
+ info("Remove breakpoint and finish.");
+ threadFront.removeBreakpoint(location, {});
+
+ await resume(threadFront);
+}
+
+// Test that we advance to the next line when a location
+// has both a breakpoint and get watchpoint.
+async function testBreakpointAndGetWatchpoint({ threadFront, debuggee }) {
+ function evaluateTestCode(debuggee) {
+ /* eslint-disable */
+ Cu.evalInSandbox(
+ ` // 1
+ function stopMe(obj) { // 2
+ debugger; // 3
+ obj.a + 4; // 4
+ debugger; // 5
+ } //
+ stopMe({a: 1})`,
+ debuggee,
+ "1.8",
+ "test_watchpoint-02.js"
+ );
+ /* eslint-disable */
+ }
+
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+
+ info("Test that we pause on the debugger statement.");
+ Assert.equal(packet.frame.where.line, 3);
+
+ info("Add get watchpoint.");
+ const args = packet.frame.arguments;
+ const obj = args[0];
+ const objClient = threadFront.pauseGrip(obj);
+ await objClient.addWatchpoint("a", "obj.a", "get");
+
+ info("Add breakpoint.");
+ const source = await getSourceById(threadFront, packet.frame.where.actor);
+
+ const location = {
+ sourceUrl: source.url,
+ line: 4,
+ };
+
+ threadFront.setBreakpoint(location, {});
+
+ info("Test that pause occurs on breakpoint.");
+ const packet2 = await resumeAndWaitForPause(threadFront);
+ Assert.equal(packet2.frame.where.line, 4);
+ Assert.equal(packet2.why.type, "breakpoint");
+
+ const packet3 = await resumeAndWaitForPause(threadFront);
+
+ info("Test that we pause on the second debugger statement.");
+ Assert.equal(packet3.frame.where.line, 5);
+ Assert.equal(packet3.why.type, "debuggerStatement");
+
+ info("Remove breakpoint and finish.");
+ threadFront.removeBreakpoint(location, {});
+
+ await resume(threadFront);
+}
+
+// Test that we can pause multiple times
+// on the same line for a watchpoint.
+async function testLoops({ commands, threadFront, debuggee }) {
+ async function evaluateJS(input) {
+ const { result } = await commands.scriptCommand.execute(input, {
+ frameActor: packet.frame.actorID,
+ });
+ return result;
+ }
+
+ function evaluateTestCode(debuggee) {
+ /* eslint-disable */
+ Cu.evalInSandbox(
+ ` // 1
+ function stopMe(obj) { // 2
+ let i = 0; // 3
+ debugger; // 4
+ while (i++ < 2) { // 5
+ obj.a = 2; // 6
+ } // 7
+ debugger; // 8
+ } //
+ stopMe({a: 1})`,
+ debuggee,
+ "1.8",
+ "test_watchpoint-02.js"
+ );
+ /* eslint-disable */
+ }
+
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+
+ info("Test that we pause on the debugger statement.");
+ Assert.equal(packet.frame.where.line, 4);
+ Assert.equal(packet.why.type, "debuggerStatement");
+
+ info("Add set watchpoint.");
+ const args = packet.frame.arguments;
+ const obj = args[0];
+ const objClient = threadFront.pauseGrip(obj);
+ await objClient.addWatchpoint("a", "obj.a", "set");
+
+ info("Test that watchpoint triggers pause on set.");
+ const packet2 = await resumeAndWaitForPause(threadFront);
+ Assert.equal(packet2.frame.where.line, 6);
+ Assert.equal(packet2.why.type, "setWatchpoint");
+ let result = await evaluateJS("obj.a");
+ Assert.equal(result, 1);
+
+ info("Test that watchpoint triggers pause on set (2nd time).");
+ const packet3 = await resumeAndWaitForPause(threadFront);
+ Assert.equal(packet3.frame.where.line, 6);
+ Assert.equal(packet3.why.type, "setWatchpoint");
+ let result2 = await evaluateJS("obj.a");
+ Assert.equal(result2, 2);
+
+ info("Test that we pause on second debugger statement.");
+ const packet4 = await resumeAndWaitForPause(threadFront);
+ Assert.equal(packet4.frame.where.line, 8);
+ Assert.equal(packet4.why.type, "debuggerStatement");
+
+ await resume(threadFront);
+}
diff --git a/devtools/server/tests/xpcshell/test_watchpoint-03.js b/devtools/server/tests/xpcshell/test_watchpoint-03.js
new file mode 100644
index 0000000000..33f4fbd2a2
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_watchpoint-03.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-shadow */
+
+"use strict";
+/*
+See Bug 1601311.
+Tests that removing a watchpoint does not change the value of the property that had the watchpoint.
+*/
+
+add_task(
+ threadFrontTest(async ({ commands, threadFront, debuggee }) => {
+ async function evaluateJS(input) {
+ const { result } = await commands.scriptCommand.execute(input, {
+ frameActor: packet.frame.actorID,
+ });
+ return result;
+ }
+
+ function evaluateTestCode(debuggee) {
+ /* eslint-disable */
+ Cu.evalInSandbox(
+ ` // 1
+ function stopMe(obj) { // 2
+ debugger; // 3
+ obj.a = 2; // 4
+ debugger; // 5
+ } //
+
+ stopMe({a: 1})`,
+ debuggee,
+ "1.8",
+ "test_watchpoint-03.js"
+ );
+ /* eslint-disable */
+ }
+
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+
+ info("Test that we paused on the debugger statement.");
+ Assert.equal(packet.frame.where.line, 3);
+
+ info("Add set watchpoint.");
+ const args = packet.frame.arguments;
+ const obj = args[0];
+ const objClient = threadFront.pauseGrip(obj);
+ await objClient.addWatchpoint("a", "obj.a", "set");
+
+ info("Test that we pause on set.");
+ const packet2 = await resumeAndWaitForPause(threadFront);
+ Assert.equal(packet2.frame.where.line, 4);
+
+ const packet3 = await resumeAndWaitForPause(threadFront);
+
+ info("Test that we pause on the second debugger statement.");
+ Assert.equal(packet3.frame.where.line, 5);
+ Assert.equal(packet3.why.type, "debuggerStatement");
+
+ info("Remove watchpoint.");
+ await objClient.removeWatchpoint("a");
+
+ info("Test that the value has updated.");
+ const result = await evaluateJS("obj.a");
+ Assert.equal(result, 2);
+
+ info("Finish test.");
+ await resume(threadFront);
+ })
+);
diff --git a/devtools/server/tests/xpcshell/test_watchpoint-04.js b/devtools/server/tests/xpcshell/test_watchpoint-04.js
new file mode 100644
index 0000000000..4ee6eadd5a
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_watchpoint-04.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that watchpoints ignore blackboxed sources
+ */
+
+const BLACK_BOXED_URL = "http://example.com/blackboxme.js";
+const SOURCE_URL = "http://example.com/source.js";
+
+add_task(
+ threadFrontTest(async ({ threadFront, debuggee }) => {
+ await executeOnNextTickAndWaitForPause(
+ () => evalCode(debuggee),
+ threadFront
+ );
+
+ info(`blackbox the source`);
+ const { error, sources } = await threadFront.getSources();
+ Assert.ok(!error, "Should not get an error: " + error);
+ const sourceFront = threadFront.source(
+ sources.filter(s => s.url == BLACK_BOXED_URL)[0]
+ );
+
+ await blackBox(sourceFront);
+
+ await threadFront.resume();
+ const packet = await executeOnNextTickAndWaitForPause(
+ debuggee.runTest,
+ threadFront
+ );
+
+ Assert.equal(
+ packet.frame.where.line,
+ 3,
+ "Paused at first debugger statement"
+ );
+
+ await addWatchpoint(threadFront, packet.frame, "obj", "a", "set");
+
+ info(`Resume and skip the watchpoint`);
+ const pausePacket = await resumeAndWaitForPause(threadFront);
+
+ Assert.equal(
+ pausePacket.frame.where.line,
+ 5,
+ "Paused at second debugger statement"
+ );
+
+ await threadFront.resume();
+ })
+);
+
+function evalCode(debuggee) {
+ Cu.evalInSandbox(
+ `function doStuff(obj) {
+ obj.a = 2;
+ }`,
+ debuggee,
+ "1.8",
+ BLACK_BOXED_URL,
+ 1
+ );
+ Cu.evalInSandbox(
+ `function runTest() {
+ const obj = {a: 1}
+ debugger
+ doStuff(obj);
+ debugger
+ }; debugger;`,
+ debuggee,
+ "1.8",
+ SOURCE_URL,
+ 1
+ );
+}
diff --git a/devtools/server/tests/xpcshell/test_watchpoint-05.js b/devtools/server/tests/xpcshell/test_watchpoint-05.js
new file mode 100644
index 0000000000..4d25a59399
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_watchpoint-05.js
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-shadow */
+
+"use strict";
+
+/*
+- Adds a 'get or set' watchpoint. Tests that the debugger will pause on both get and set.
+*/
+
+add_task(
+ threadFrontTest(async args => {
+ await testGetPauseWithGetOrSetWatchpoint(args);
+ await testSetPauseWithGetOrSetWatchpoint(args);
+ })
+);
+
+async function testGetPauseWithGetOrSetWatchpoint({ threadFront, debuggee }) {
+ function evaluateTestCode(debuggee) {
+ /* eslint-disable */
+ Cu.evalInSandbox(
+ ` // 1
+ function stopMe(obj) { // 2
+ debugger; // 3
+ obj.a + 4; // 4
+ } //
+ stopMe({a: 1})`,
+ debuggee,
+ "1.8",
+ "test_watchpoint-05.js"
+ );
+ /* eslint-disable */
+ }
+
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+
+ info("Test that we paused on the debugger statement");
+ Assert.equal(packet.frame.where.line, 3);
+
+ info("Add get or set watchpoint.");
+ const args = packet.frame.arguments;
+ const obj = args[0];
+ const objClient = threadFront.pauseGrip(obj);
+ await objClient.addWatchpoint("a", "obj.a", "getorset");
+
+ info("Test that watchpoint triggers pause on get.");
+ const packet2 = await resumeAndWaitForPause(threadFront);
+ Assert.equal(packet2.frame.where.line, 4);
+ Assert.equal(packet2.why.type, "getWatchpoint");
+ Assert.equal(obj.preview.ownProperties.a.value, 1);
+
+ await resume(threadFront);
+}
+
+async function testSetPauseWithGetOrSetWatchpoint({
+ commands,
+ threadFront,
+ debuggee,
+}) {
+ async function evaluateJS(input) {
+ const { result } = await commands.scriptCommand.execute(input, {
+ frameActor: packet.frame.actorID,
+ });
+ return result;
+ }
+
+ function evaluateTestCode(debuggee) {
+ /* eslint-disable */
+ Cu.evalInSandbox(
+ ` // 1
+ function stopMe(obj) { // 2
+ debugger; // 3
+ obj.a = 2; // 4
+ } //
+ stopMe({a: { b: 1 }})`,
+ debuggee,
+ "1.8",
+ "test_watchpoint-05.js"
+ );
+ /* eslint-disable */
+ }
+
+ const packet = await executeOnNextTickAndWaitForPause(
+ () => evaluateTestCode(debuggee),
+ threadFront
+ );
+
+ info("Test that we paused on the debugger statement");
+ Assert.equal(packet.frame.where.line, 3);
+
+ info("Add get or set watchpoint");
+ const args = packet.frame.arguments;
+ const obj = args[0];
+ const objClient = threadFront.pauseGrip(obj);
+ await objClient.addWatchpoint("a", "obj.a", "getorset");
+
+ let result = await evaluateJS("obj.a");
+ Assert.equal(result.getGrip().preview.ownProperties.b.value, 1);
+
+ result = await evaluateJS("obj.a.b");
+ Assert.equal(result, 1);
+
+ info("Test that watchpoint triggers pause on set");
+ const packet2 = await resumeAndWaitForPause(threadFront);
+ Assert.equal(packet2.frame.where.line, 4);
+ Assert.equal(packet2.why.type, "setWatchpoint");
+ Assert.equal(obj.preview.ownProperties.a.value.ownPropertyLength, 1);
+
+ await resume(threadFront);
+}
diff --git a/devtools/server/tests/xpcshell/test_webext_apis.js b/devtools/server/tests/xpcshell/test_webext_apis.js
new file mode 100644
index 0000000000..5a2f2b990a
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_webext_apis.js
@@ -0,0 +1,162 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AddonManager } = ChromeUtils.importESModule(
+ "resource://gre/modules/AddonManager.sys.mjs"
+);
+const { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+const DistinctDevToolsServer = getDistinctDevToolsServer();
+ExtensionTestUtils.init(this);
+
+add_setup(async () => {
+ Services.prefs.setBoolPref("extensions.blocklist.enabled", false);
+ await startupAddonsManager();
+});
+
+// Basic request wrapper that sends a request and resolves on the next packet.
+// Will only work for very basic scenarios, without events emitted on the server
+// etc...
+async function sendRequest(transport, request) {
+ return new Promise(resolve => {
+ transport.hooks = {
+ onPacket: packet => {
+ dump(`received packet: ${JSON.stringify(packet)}\n`);
+ // Let's resolve only when we get a packet that is related to our
+ // request. It is needed because some methods do not return the correct
+ // response right away. This is the case of the `reload` method, which
+ // receives a `addonListChanged` message first and then a `reload`
+ // message.
+ if (packet.from === request.to) {
+ resolve(packet);
+ }
+ },
+ };
+ transport.send(request);
+ });
+}
+
+// If this test case fails, please reach out to webext peers because
+// https://github.com/mozilla/web-ext relies on the APIs tested here.
+add_task(async function test_webext_run_apis() {
+ DistinctDevToolsServer.init();
+ DistinctDevToolsServer.registerAllActors();
+
+ const transport = DistinctDevToolsServer.connectPipe();
+
+ // After calling connectPipe, the root actor will be created on the server
+ // and a packet will be emitted after a tick. Wait for the initial packet.
+ await new Promise(resolve => {
+ transport.hooks = { onPacket: resolve };
+ });
+
+ const getRootResponse = await sendRequest(transport, {
+ to: "root",
+ type: "getRoot",
+ });
+
+ ok(getRootResponse, "received a response after calling RootActor::getRoot");
+ ok(getRootResponse.addonsActor, "getRoot returned an addonsActor id");
+
+ // installTemporaryAddon
+ const addonId = "test-addons-actor@mozilla.org";
+ const addonPath = getFilePath("addons/web-extension", false, true);
+ const promiseStarted = AddonTestUtils.promiseWebExtensionStartup(addonId);
+ const { addon } = await sendRequest(transport, {
+ to: getRootResponse.addonsActor,
+ type: "installTemporaryAddon",
+ addonPath,
+ // The openDevTools parameter is not always passed by web-ext. This test
+ // omits it, to make sure that the request without the flag is accepted.
+ // openDevTools: false,
+ });
+ await promiseStarted;
+
+ ok(addon, "addonsActor allows to install a temporary add-on");
+ equal(addon.id, addonId, "temporary add-on is the expected one");
+ equal(addon.actor, false, "temporary add-on does not have an actor");
+
+ // listAddons
+ let { addons } = await sendRequest(transport, {
+ to: "root",
+ type: "listAddons",
+ });
+ ok(Array.isArray(addons), "listAddons() returns a list of add-ons");
+ equal(addons.length, 1, "expected an add-on installed");
+
+ const installedAddon = addons[0];
+ equal(installedAddon.id, addonId, "installed add-on is the expected one");
+ ok(installedAddon.actor, "returned add-on has an actor");
+
+ // reload
+ const promiseReloaded = AddonTestUtils.promiseAddonEvent("onInstalled");
+ const promiseRestarted = AddonTestUtils.promiseWebExtensionStartup(addonId);
+ await sendRequest(transport, {
+ to: installedAddon.actor,
+ type: "reload",
+ });
+ await Promise.all([promiseReloaded, promiseRestarted]);
+
+ // uninstallAddon
+ const promiseUninstalled = new Promise(resolve => {
+ const listener = {};
+ listener.onUninstalled = uninstalledAddon => {
+ if (uninstalledAddon.id == addonId) {
+ AddonManager.removeAddonListener(listener);
+ resolve();
+ }
+ };
+ AddonManager.addAddonListener(listener);
+ });
+ await sendRequest(transport, {
+ to: getRootResponse.addonsActor,
+ type: "uninstallAddon",
+ addonId,
+ });
+ await promiseUninstalled;
+
+ ({ addons } = await sendRequest(transport, {
+ to: "root",
+ type: "listAddons",
+ }));
+ equal(addons.length, 0, "expected no add-on installed");
+
+ // Attempt to uninstall an add-on that is (no longer) installed.
+ let error = await sendRequest(transport, {
+ to: getRootResponse.addonsActor,
+ type: "uninstallAddon",
+ addonId,
+ });
+ equal(
+ error?.message,
+ `Could not uninstall add-on "${addonId}"`,
+ "expected error"
+ );
+
+ // Attempt to uninstall a non-temporarily loaded extension, which we do not
+ // allow at the moment. We start by loading an extension, then we call the
+ // `uninstallAddon`.
+ const id = "not-a-temporary@extension";
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id } },
+ },
+ useAddonManager: "permanent",
+ });
+ await extension.startup();
+
+ error = await sendRequest(transport, {
+ to: getRootResponse.addonsActor,
+ type: "uninstallAddon",
+ addonId: id,
+ });
+ equal(error?.message, `Could not uninstall add-on "${id}"`, "expected error");
+
+ await extension.unload();
+
+ transport.close();
+});
diff --git a/devtools/server/tests/xpcshell/test_webextension_descriptor.js b/devtools/server/tests/xpcshell/test_webextension_descriptor.js
new file mode 100644
index 0000000000..00cdeea605
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_webextension_descriptor.js
@@ -0,0 +1,141 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+const DistinctDevToolsServer = getDistinctDevToolsServer();
+ExtensionTestUtils.init(this);
+
+add_setup(async () => {
+ Services.prefs.setBoolPref("extensions.blocklist.enabled", false);
+ await startupAddonsManager();
+
+ // We intentionally generate install-time manifest warnings, so don't trigger
+ // the special test-only mode of converting them to errors.
+ Services.prefs.setBoolPref(
+ "extensions.webextensions.warnings-as-errors",
+ false
+ );
+
+ DistinctDevToolsServer.init();
+ DistinctDevToolsServer.registerAllActors();
+});
+
+// Verifies:
+// - listAddons
+// - WebExtensionDescriptorActor output
+// Also a regression test for bug 1837185, that AddonManager.sys.mjs and
+// ExtensionParent.sys.mjs are imported from the correct loader.
+add_task(async function test_listAddons_and_WebExtensionDescriptor() {
+ const transport = DistinctDevToolsServer.connectPipe();
+ const client = new DevToolsClient(transport);
+ await client.connect();
+
+ const getRootResponse = await client.mainRoot.getRoot();
+
+ ok(getRootResponse, "received a response after calling RootActor::getRoot");
+ ok(getRootResponse.addonsActor, "getRoot returned an addonsActor id");
+
+ const ADDON_ID = "with@warning";
+ const extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ name: "DummyExtensionWithUnknownManifestKey",
+ unknown_manifest_key: "this is an unknown manifest key",
+ browser_specific_settings: { gecko: { id: ADDON_ID } },
+ },
+ background: `browser.test.sendMessage("background_started");`,
+ });
+ await extension.startup();
+ await extension.awaitMessage("background_started");
+
+ // listAddons: addon after new install.
+ {
+ const listAddonsResponse = await client.mainRoot.listAddons();
+ const addon = listAddonsResponse.find(a => a.id === ADDON_ID);
+ ok(addon, "listAddons() returns a list of add-ons including with@warning");
+
+ // Inspect all raw properties of the message, to make sure that we always
+ // have full coverage for all current and future properties.
+ const { actor, url, warnings, ...addonMinusSomeKeys } = addon._form;
+ const actorPattern = /^server\d+\.conn\d+\.webExtensionDescriptor\d+$/;
+ ok(actorPattern.test(actor), `actor is webExtensionDescriptor: ${actor}`);
+ // We don't care about the exact path, just a dummy check:
+ ok(url.endsWith(".xpi"), `url is path to the xpi file`);
+
+ deepEqual(
+ warnings,
+ [
+ "Reading manifest: Warning processing unknown_manifest_key: An unexpected property was found in the WebExtension manifest.",
+ ],
+ "Can retrieve warnings."
+ );
+
+ // Verify that the other remaining keys have a meaningful value.
+ // This is mainly to have some form of verification on the value of the
+ // properties. If this check ever fails, double-check whether the proposed
+ // change makes sense and if it does just update the test expectation here.
+ deepEqual(
+ addonMinusSomeKeys,
+ {
+ backgroundScriptStatus: undefined,
+ debuggable: true,
+ hidden: false,
+ iconDataURL: undefined,
+ iconURL: null,
+ id: ADDON_ID,
+ isSystem: false,
+ isWebExtension: true,
+ manifestURL: `moz-extension://${extension.uuid}/manifest.json`,
+ name: "DummyExtensionWithUnknownManifestKey",
+ persistentBackgroundScript: true,
+ temporarilyInstalled: false,
+ traits: {
+ supportsReloadDescriptor: true,
+ watcher: true,
+ },
+ },
+ "WebExtensionDescriptorActor content matches the add-on"
+ );
+ }
+
+ await extension.upgrade({
+ manifest: {
+ name: "Updated_extension",
+ new_unknown_manifest_key: "different warning than before",
+ browser_specific_settings: { gecko: { id: ADDON_ID } },
+ },
+ background: `browser.test.sendMessage("updated_done");`,
+ });
+ await extension.awaitMessage("updated_done");
+
+ // listAddons: addon after update.
+ {
+ const listAddonsResponse = await client.mainRoot.listAddons();
+ const addon = listAddonsResponse.find(a => a.id === ADDON_ID);
+ ok(addon, "listAddons() should still list the add-on after update");
+ equal(addon.name, "Updated_extension", "Got updated name");
+ deepEqual(
+ addon.warnings,
+ [
+ "Reading manifest: Warning processing new_unknown_manifest_key: An unexpected property was found in the WebExtension manifest.",
+ ],
+ "Can retrieve new warnings for updated add-on."
+ );
+ }
+
+ await extension.unload();
+
+ // listAddons: addon after removal - gone.
+ {
+ const listAddonsResponse = await client.mainRoot.listAddons();
+ const addon = listAddonsResponse.find(a => a.id === ADDON_ID);
+ deepEqual(addon, null, "Add-on should be gone after removal");
+ }
+
+ await client.close();
+});
diff --git a/devtools/server/tests/xpcshell/test_xpcshell_debugging.js b/devtools/server/tests/xpcshell/test_xpcshell_debugging.js
new file mode 100644
index 0000000000..ff54d7390d
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_xpcshell_debugging.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the xpcshell-test debug support. Ideally we should have this test
+// next to the xpcshell support code, but that's tricky...
+
+// HACK: ServiceWorkerManager requires the "profile-change-teardown" to cleanly
+// shutdown, and setting _profileInitialized to `true` will trigger those
+// notifications (see /testing/xpcshell/head.js).
+// eslint-disable-next-line no-undef
+_profileInitialized = true;
+
+add_task(async function () {
+ const testFile = do_get_file("xpcshell_debugging_script.js");
+
+ // _setupDevToolsServer is from xpcshell-test's head.js
+ /* global _setupDevToolsServer */
+ let testInitialized = false;
+ const { DevToolsServer } = _setupDevToolsServer([testFile.path], () => {
+ testInitialized = true;
+ });
+ const transport = DevToolsServer.connectPipe();
+ const client = new DevToolsClient(transport);
+ await client.connect();
+
+ // Ensure that global actors are available. Just test the device actor.
+ const deviceFront = await client.mainRoot.getFront("device");
+ const desc = await deviceFront.getDescription();
+ equal(
+ desc.geckobuildid,
+ Services.appinfo.platformBuildID,
+ "device actor works"
+ );
+
+ // Even though we have no tabs, getMainProcess gives us the chrome debugger.
+ const targetDescriptor = await client.mainRoot.getMainProcess();
+ const front = await targetDescriptor.getTarget();
+ const watcher = await targetDescriptor.getWatcher();
+
+ const threadFront = await front.attachThread();
+
+ // Checks that the thread actor initializes immediately and that _setupDevToolsServer
+ // callback gets called.
+ ok(testInitialized);
+
+ const onPause = waitForPause(threadFront);
+
+ // Now load our test script,
+ // in another event loop so that the test can keep running!
+ Services.tm.dispatchToMainThread(() => {
+ load(testFile.path);
+ });
+
+ // and our "paused" listener should get hit.
+ info("Wait for first paused event");
+ const packet1 = await onPause;
+ equal(
+ packet1.why.type,
+ "breakpoint",
+ "yay - hit the breakpoint at the first line in our script"
+ );
+
+ // Resume again - next stop should be our "debugger" statement.
+ info("Wait for second pause event");
+ const packet2 = await resumeAndWaitForPause(threadFront);
+ equal(
+ packet2.why.type,
+ "debuggerStatement",
+ "yay - hit the 'debugger' statement in our script"
+ );
+
+ info("Dynamically add a breakpoint after the debugger statement");
+ const breakpointsFront = await watcher.getBreakpointListActor();
+ await breakpointsFront.setBreakpoint(
+ { sourceUrl: testFile.path, line: 11, column: 0 },
+ {}
+ );
+
+ // Resume again - next stop should be the new breakpoint.
+ info("Wait for third pause event");
+ const packet3 = await resumeAndWaitForPause(threadFront);
+ equal(
+ packet3.why.type,
+ "breakpoint",
+ "yay - hit the breakpoint added after starting the test"
+ );
+ finishClient(client);
+});
diff --git a/devtools/server/tests/xpcshell/testactors.js b/devtools/server/tests/xpcshell/testactors.js
new file mode 100644
index 0000000000..af208fe93e
--- /dev/null
+++ b/devtools/server/tests/xpcshell/testactors.js
@@ -0,0 +1,242 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ LazyPool,
+ createExtraActors,
+} = require("resource://devtools/shared/protocol/lazy-pool.js");
+const { RootActor } = require("resource://devtools/server/actors/root.js");
+const { ThreadActor } = require("resource://devtools/server/actors/thread.js");
+const {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+const {
+ ActorRegistry,
+} = require("resource://devtools/server/actors/utils/actor-registry.js");
+const {
+ SourcesManager,
+} = require("resource://devtools/server/actors/utils/sources-manager.js");
+const makeDebugger = require("resource://devtools/server/actors/utils/make-debugger.js");
+const protocol = require("resource://devtools/shared/protocol.js");
+const {
+ windowGlobalTargetSpec,
+} = require("resource://devtools/shared/specs/targets/window-global.js");
+const {
+ tabDescriptorSpec,
+} = require("resource://devtools/shared/specs/descriptors/tab.js");
+const Targets = require("resource://devtools/server/actors/targets/index.js");
+const {
+ createContentProcessSessionContext,
+} = require("resource://devtools/server/actors/watcher/session-context.js");
+
+var gTestGlobals = new Set();
+DevToolsServer.addTestGlobal = function (global) {
+ gTestGlobals.add(global);
+};
+DevToolsServer.removeTestGlobal = function (global) {
+ gTestGlobals.delete(global);
+};
+
+DevToolsServer.getTestGlobal = function (name) {
+ for (const g of gTestGlobals) {
+ if (g.__name == name) {
+ return g;
+ }
+ }
+
+ return null;
+};
+
+var gAllowNewThreadGlobals = false;
+DevToolsServer.allowNewThreadGlobals = function () {
+ gAllowNewThreadGlobals = true;
+};
+DevToolsServer.disallowNewThreadGlobals = function () {
+ gAllowNewThreadGlobals = false;
+};
+
+// A mock tab list, for use by tests. This simply presents each global in
+// gTestGlobals as a tab, and the list is fixed: it never calls its
+// onListChanged handler.
+//
+// As implemented now, we consult gTestGlobals when we're constructed, not
+// when we're iterated over, so tests have to add their globals before the
+// root actor is created.
+function TestTabList(connection) {
+ this.conn = connection;
+
+ // An array of actors for each global added with
+ // DevToolsServer.addTestGlobal.
+ this._descriptorActors = [];
+
+ // A pool mapping those actors' names to the actors.
+ this._descriptorActorPool = new LazyPool(connection);
+
+ for (const global of gTestGlobals) {
+ const actor = new TestTargetActor(connection, global);
+ this._descriptorActorPool.manage(actor);
+
+ const descriptorActor = new TestDescriptorActor(connection, actor);
+ this._descriptorActorPool.manage(descriptorActor);
+
+ this._descriptorActors.push(descriptorActor);
+ }
+}
+
+TestTabList.prototype = {
+ constructor: TestTabList,
+ destroy() {},
+ getList() {
+ return Promise.resolve([...this._descriptorActors]);
+ },
+ // Helper method only available for the xpcshell implementation of tablist.
+ getTargetActorForTab(title) {
+ const descriptorActor = this._descriptorActors.find(d => d.title === title);
+ if (!descriptorActor) {
+ return null;
+ }
+ return descriptorActor._targetActor;
+ },
+};
+
+exports.createRootActor = function createRootActor(connection) {
+ ActorRegistry.registerModule("devtools/server/actors/webconsole", {
+ prefix: "console",
+ constructor: "WebConsoleActor",
+ type: { target: true },
+ });
+ const root = new RootActor(connection, {
+ tabList: new TestTabList(connection),
+ globalActorFactories: ActorRegistry.globalActorFactories,
+ });
+
+ root.applicationType = "xpcshell-tests";
+ return root;
+};
+
+class TestDescriptorActor extends protocol.Actor {
+ constructor(conn, targetActor) {
+ super(conn, tabDescriptorSpec);
+ this._targetActor = targetActor;
+ }
+
+ // We don't exercise the selected tab in xpcshell tests.
+ get selected() {
+ return false;
+ }
+
+ get title() {
+ return this._targetActor.title;
+ }
+
+ form() {
+ const form = {
+ actor: this.actorID,
+ traits: {},
+ selected: this.selected,
+ title: this._targetActor.title,
+ url: this._targetActor.url,
+ };
+
+ return form;
+ }
+
+ getFavicon() {
+ return "";
+ }
+
+ getTarget() {
+ return this._targetActor.form();
+ }
+}
+
+class TestTargetActor extends protocol.Actor {
+ constructor(conn, global) {
+ super(conn, windowGlobalTargetSpec);
+
+ this.sessionContext = createContentProcessSessionContext();
+ this._global = global;
+ this._global.wrappedJSObject = global;
+ this.threadActor = new ThreadActor(this, this._global);
+ this.conn.addActor(this.threadActor);
+ this._extraActors = {};
+ // This is a hack in order to enable threadActor to be accessed from getFront
+ this._extraActors.threadActor = this.threadActor;
+ this.makeDebugger = makeDebugger.bind(null, {
+ findDebuggees: () => [this._global],
+ shouldAddNewGlobalAsDebuggee: g => gAllowNewThreadGlobals,
+ });
+ this.dbg = this.makeDebugger();
+ this.notifyResources = this.notifyResources.bind(this);
+ }
+
+ targetType = Targets.TYPES.FRAME;
+
+ get window() {
+ return this._global;
+ }
+
+ // Both title and url point to this._global.__name
+ get title() {
+ return this._global.__name;
+ }
+
+ get url() {
+ return this._global.__name;
+ }
+
+ get sourcesManager() {
+ if (!this._sourcesManager) {
+ this._sourcesManager = new SourcesManager(this.threadActor);
+ }
+ return this._sourcesManager;
+ }
+
+ form() {
+ const response = {
+ actor: this.actorID,
+ title: this.title,
+ threadActor: this.threadActor.actorID,
+ };
+
+ // Walk over target-scoped actors and add them to a new LazyPool.
+ const actorPool = new LazyPool(this.conn);
+ const actors = createExtraActors(
+ ActorRegistry.targetScopedActorFactories,
+ actorPool,
+ this
+ );
+ if (actorPool?._poolMap.size > 0) {
+ this._descriptorActorPool = actorPool;
+ this.conn.addActorPool(this._descriptorActorPool);
+ }
+
+ return { ...response, ...actors };
+ }
+
+ detach(request) {
+ this.threadActor.destroy();
+ return { type: "detached" };
+ }
+
+ reload(request) {
+ this.sourcesManager.reset();
+ this.threadActor.clearDebuggees();
+ this.threadActor.dbg.addDebuggees();
+ return {};
+ }
+
+ removeActorByName(name) {
+ const actor = this._extraActors[name];
+ if (this._descriptorActorPool) {
+ this._descriptorActorPool.removeActor(actor);
+ }
+ delete this._extraActors[name];
+ }
+
+ notifyResources(updateType, resources) {
+ this.emit(`resource-${updateType}-form`, resources);
+ }
+}
diff --git a/devtools/server/tests/xpcshell/webextension-helpers.js b/devtools/server/tests/xpcshell/webextension-helpers.js
new file mode 100644
index 0000000000..46968f09e7
--- /dev/null
+++ b/devtools/server/tests/xpcshell/webextension-helpers.js
@@ -0,0 +1,197 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* globals browser */
+
+"use strict";
+
+/**
+ * Test helpers shared by the devtools server xpcshell tests related to webextensions.
+ */
+
+const { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+const { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+const {
+ CommandsFactory,
+} = require("resource://devtools/shared/commands/commands-factory.js");
+
+/**
+ * Loads and starts up a test extension given the provided extension configuration.
+ *
+ * @param {Object} extConfig - The extension configuration object
+ * @return {ExtensionWrapper} extension - Resolves with an extension object once the
+ * extension has started up.
+ */
+async function startupExtension(extConfig) {
+ const extension = ExtensionTestUtils.loadExtension(extConfig);
+
+ await extension.startup();
+
+ return extension;
+}
+exports.startupExtension = startupExtension;
+
+/**
+ * Initializes the extensionStorage actor for a given extension. This is effectively
+ * what happens when the addon storage panel is opened in the browser.
+ *
+ * @param {String} - id, The addon id
+ * @return {Object} - Resolves with the DevTools "commands" objact and the extensionStorage
+ * resource/front.
+ */
+async function openAddonStoragePanel(id) {
+ const commands = await CommandsFactory.forAddon(id);
+ await commands.targetCommand.startListening();
+
+ // Fetch the EXTENSION_STORAGE resource.
+ // Unfortunately, we can't use resourceCommand.waitForNextResource as it would destroy
+ // the actor by immediately unwatching for the resource type.
+ const extensionStorage = await new Promise(resolve => {
+ commands.resourceCommand.watchResources(
+ [commands.resourceCommand.TYPES.EXTENSION_STORAGE],
+ {
+ onAvailable(resources) {
+ resolve(resources[0]);
+ },
+ }
+ );
+ });
+
+ return { commands, extensionStorage };
+}
+exports.openAddonStoragePanel = openAddonStoragePanel;
+
+/**
+ * Builds the extension configuration object passed into ExtensionTestUtils.loadExtension
+ *
+ * @param {Object} options - Options, if any, to add to the configuration
+ * @param {Function} options.background - A function comprising the test extension's
+ * background script if provided
+ * @param {Object} options.files - An object whose keys correspond to file names and
+ * values map to the file contents
+ * @param {Object} options.manifest - An object representing the extension's manifest
+ * @return {Object} - The extension configuration object
+ */
+function getExtensionConfig(options = {}) {
+ const { manifest, ...otherOptions } = options;
+ const baseConfig = {
+ manifest: {
+ ...manifest,
+ permissions: ["storage"],
+ },
+ useAddonManager: "temporary",
+ };
+ return {
+ ...baseConfig,
+ ...otherOptions,
+ };
+}
+exports.getExtensionConfig = getExtensionConfig;
+
+/**
+ * Shared files for a test extension that has no background page but adds storage
+ * items via a transient extension page in a tab
+ */
+const ext_no_bg = {
+ files: {
+ "extension_page_in_tab.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <h1>Extension Page in a Tab</h1>
+ <script src="extension_page_in_tab.js"></script>
+ </body>
+ </html>`,
+ "extension_page_in_tab.js": extensionScriptWithMessageListener,
+ },
+};
+exports.ext_no_bg = ext_no_bg;
+
+/**
+ * An extension script that can be used in any extension context (e.g. as a background
+ * script or as an extension page script loaded in a tab).
+ */
+async function extensionScriptWithMessageListener() {
+ let fireOnChanged = false;
+ browser.storage.onChanged.addListener(() => {
+ if (fireOnChanged) {
+ // Do not fire it again until explicitly requested again using the "storage-local-fireOnChanged" test message.
+ fireOnChanged = false;
+ browser.test.sendMessage("storage-local-onChanged");
+ }
+ });
+
+ browser.test.onMessage.addListener(async (msg, ...args) => {
+ let item = null;
+ switch (msg) {
+ case "storage-local-set":
+ await browser.storage.local.set(args[0]);
+ break;
+ case "storage-local-get":
+ item = await browser.storage.local.get(args[0]);
+ break;
+ case "storage-local-remove":
+ await browser.storage.local.remove(args[0]);
+ break;
+ case "storage-local-clear":
+ await browser.storage.local.clear();
+ break;
+ case "storage-local-fireOnChanged": {
+ // Allow the storage.onChanged listener to send a test event
+ // message when onChanged is being fired.
+ fireOnChanged = true;
+ // Do not fire fireOnChanged:done.
+ return;
+ }
+ default:
+ browser.test.fail(`Unexpected test message: ${msg}`);
+ }
+
+ browser.test.sendMessage(`${msg}:done`, item);
+ });
+ // window is available in background scripts
+ // eslint-disable-next-line no-undef
+ browser.test.sendMessage("extension-origin", window.location.origin);
+}
+exports.extensionScriptWithMessageListener = extensionScriptWithMessageListener;
+
+/**
+ * Shutdown procedure common to all tasks.
+ *
+ * @param {Object} extension - The test extension
+ * @param {Object} commands - The web extension commands used by the DevTools to interact with the backend
+ */
+async function shutdown(extension, commands) {
+ if (commands) {
+ await commands.destroy();
+ }
+ await extension.unload();
+}
+exports.shutdown = shutdown;
+
+/**
+ * Mocks the missing 'storage/permanent' directory needed by the "indexedDB"
+ * storage actor's 'populateStoresForHosts' method. This
+ * directory exists in a full browser i.e. mochitest.
+ */
+function createMissingIndexedDBDirs() {
+ const dir = Services.dirsvc.get("ProfD", Ci.nsIFile).clone();
+ dir.append("storage");
+ if (!dir.exists()) {
+ dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ }
+ dir.append("permanent");
+ if (!dir.exists()) {
+ dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ }
+
+ return dir;
+}
+exports.createMissingIndexedDBDirs = createMissingIndexedDBDirs;
diff --git a/devtools/server/tests/xpcshell/xpcshell.toml b/devtools/server/tests/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..29ca414062
--- /dev/null
+++ b/devtools/server/tests/xpcshell/xpcshell.toml
@@ -0,0 +1,436 @@
+[DEFAULT]
+tags = "devtools"
+head = "head_dbg.js"
+firefox-appdir = "browser"
+skip-if = ["os == 'android'"]
+# While not every devtools test uses evalInSandbox over 80 do, so it's easier to
+# set allow_parent_unrestricted_js_loads for all the tests here.
+# Similar story for the eval restrictions
+prefs = [
+ "security.allow_parent_unrestricted_js_loads=true",
+ "security.allow_eval_with_system_principal=true",
+ "security.allow_eval_in_parent_process=true",
+]
+
+support-files = [
+ "completions.js",
+ "webextension-helpers.js",
+ "source-map-data/sourcemapped.coffee",
+ "source-map-data/sourcemapped.map",
+ "post_init_global_actors.js",
+ "post_init_target_scoped_actors.js",
+ "pre_init_global_actors.js",
+ "pre_init_target_scoped_actors.js",
+ "registertestactors-lazy.js",
+ "sourcemapped.js",
+ "testactors.js",
+ "hello-actor.js",
+ "stepping.js",
+ "stepping-async.js",
+ "source-03.js",
+ "setBreakpoint-on-column.js",
+ "setBreakpoint-on-column-minified.js",
+ "setBreakpoint-on-column-in-gcd-script.js",
+ "setBreakpoint-on-column-with-no-offsets.js",
+ "setBreakpoint-on-column-with-no-offsets-in-gcd-script.js",
+ "setBreakpoint-on-line.js",
+ "setBreakpoint-on-line-in-gcd-script.js",
+ "setBreakpoint-on-line-with-multiple-offsets.js",
+ "setBreakpoint-on-line-with-multiple-statements.js",
+ "setBreakpoint-on-line-with-no-offsets.js",
+ "setBreakpoint-on-line-with-no-offsets-in-gcd-script.js",
+ "addons/web-extension/manifest.json",
+ "addons/web-extension-upgrade/manifest.json",
+ "addons/web-extension2/manifest.json",
+]
+
+["test_MemoryActor_saveHeapSnapshot_01.js"]
+
+["test_MemoryActor_saveHeapSnapshot_02.js"]
+
+["test_MemoryActor_saveHeapSnapshot_03.js"]
+
+["test_add_actors.js"]
+
+["test_addon_debugging_connect.js"]
+
+["test_addon_events.js"]
+
+["test_addon_reload.js"]
+
+["test_addons_actor.js"]
+
+["test_animation_name.js"]
+
+["test_animation_type.js"]
+
+["test_attach.js"]
+
+["test_blackboxing-01.js"]
+
+["test_blackboxing-02.js"]
+
+["test_blackboxing-03.js"]
+
+["test_blackboxing-04.js"]
+
+["test_blackboxing-05.js"]
+
+["test_blackboxing-08.js"]
+
+["test_breakpoint-01.js"]
+
+["test_breakpoint-03.js"]
+skip-if = ["true"] # breakpoint sliding is not supported bug 1525685
+
+["test_breakpoint-04.js"]
+
+["test_breakpoint-05.js"]
+skip-if = ["true"] # breakpoint sliding is not supported bug 1525685
+
+["test_breakpoint-06.js"]
+skip-if = ["true"] # breakpoint sliding is not supported bug 1525685
+
+["test_breakpoint-07.js"]
+skip-if = ["true"] # breakpoint sliding is not supported bug 1525685
+
+["test_breakpoint-08.js"]
+skip-if = ["true"] # breakpoint sliding is not supported bug 1525685
+
+["test_breakpoint-09.js"]
+
+["test_breakpoint-10.js"]
+
+["test_breakpoint-11.js"]
+
+["test_breakpoint-12.js"]
+skip-if = ["true"] # breakpoint sliding is not supported bug 1525685
+
+["test_breakpoint-13.js"]
+
+["test_breakpoint-14.js"]
+
+["test_breakpoint-16.js"]
+
+["test_breakpoint-17.js"]
+skip-if = ["true"] # tests for breakpoint actors are obsolete bug 1524374
+
+["test_breakpoint-18.js"]
+
+["test_breakpoint-19.js"]
+skip-if = ["true"] # bug 1104838
+
+["test_breakpoint-20.js"]
+
+["test_breakpoint-21.js"]
+
+["test_breakpoint-22.js"]
+skip-if = ["true"] # breakpoint sliding is not supported bug 1525685
+
+["test_breakpoint-23.js"]
+
+["test_breakpoint-24.js"]
+
+["test_breakpoint-25.js"]
+
+["test_breakpoint-26.js"]
+
+["test_breakpoint-actor-map.js"]
+skip-if = ["true"] # tests for breakpoint actors are obsolete bug 1524374
+
+["test_client_request.js"]
+
+["test_conditional_breakpoint-01.js"]
+
+["test_conditional_breakpoint-02.js"]
+
+["test_conditional_breakpoint-03.js"]
+
+["test_conditional_breakpoint-04.js"]
+
+["test_connection_closes_all_pools.js"]
+
+["test_console_eval-01.js"]
+
+["test_console_eval-02.js"]
+
+["test_dbgactor.js"]
+
+["test_dbgclient_debuggerstatement.js"]
+
+["test_dbgglobal.js"]
+
+["test_extension_storage_actor.js"]
+skip-if = ["tsan"] # Unreasonably slow, bug 1612707
+
+["test_extension_storage_actor_upgrade.js"]
+
+["test_forwardingprefix.js"]
+
+["test_frameactor-01.js"]
+
+["test_frameactor-02.js"]
+
+["test_frameactor-03.js"]
+
+["test_frameactor-04.js"]
+
+["test_frameactor-05.js"]
+
+["test_frameactor_wasm-01.js"]
+
+["test_framearguments-01.js"]
+
+["test_framebindings-01.js"]
+
+["test_framebindings-02.js"]
+
+["test_framebindings-03.js"]
+
+["test_framebindings-04.js"]
+
+["test_framebindings-05.js"]
+
+["test_framebindings-06.js"]
+
+["test_framebindings-07.js"]
+
+["test_front_destroy.js"]
+
+["test_functiongrips-01.js"]
+
+["test_getRuleText.js"]
+
+["test_getTextAtLineColumn.js"]
+
+["test_get_command_and_arg.js"]
+
+["test_getyoungestframe.js"]
+
+["test_ignore_caught_exceptions.js"]
+
+["test_ignore_no_interface_exceptions.js"]
+
+["test_interrupt.js"]
+
+["test_layout-reflows-observer.js"]
+
+["test_listsources-01.js"]
+
+["test_listsources-02.js"]
+
+["test_listsources-03.js"]
+
+["test_logpoint-01.js"]
+
+["test_logpoint-02.js"]
+
+["test_logpoint-03.js"]
+
+["test_longstringgrips-01.js"]
+
+["test_nativewrappers.js"]
+
+["test_nesting-03.js"]
+
+["test_nesting-04.js"]
+
+["test_new_source-01.js"]
+
+["test_new_source-02.js"]
+
+["test_nodelistactor.js"]
+
+["test_objectgrips-02.js"]
+
+["test_objectgrips-03.js"]
+
+["test_objectgrips-04.js"]
+
+["test_objectgrips-05.js"]
+
+["test_objectgrips-06.js"]
+
+["test_objectgrips-07.js"]
+
+["test_objectgrips-08.js"]
+
+["test_objectgrips-14.js"]
+
+["test_objectgrips-15.js"]
+
+["test_objectgrips-16.js"]
+
+["test_objectgrips-17.js"]
+
+["test_objectgrips-18.js"]
+
+["test_objectgrips-19.js"]
+
+["test_objectgrips-20.js"]
+
+["test_objectgrips-21.js"]
+
+["test_objectgrips-22.js"]
+
+["test_objectgrips-23.js"]
+
+["test_objectgrips-24.js"]
+
+["test_objectgrips-25.js"]
+
+["test_objectgrips-fn-apply-01.js"]
+
+["test_objectgrips-fn-apply-02.js"]
+
+["test_objectgrips-fn-apply-03.js"]
+
+["test_objectgrips-nested-promise.js"]
+
+["test_objectgrips-nested-proxy.js"]
+
+["test_objectgrips-property-value-01.js"]
+
+["test_objectgrips-property-value-02.js"]
+
+["test_objectgrips-property-value-03.js"]
+
+["test_objectgrips-sparse-array.js"]
+
+["test_pause_exceptions-01.js"]
+
+["test_pause_exceptions-02.js"]
+
+["test_pause_exceptions-03.js"]
+
+["test_pause_exceptions-04.js"]
+
+["test_pauselifetime-01.js"]
+
+["test_pauselifetime-02.js"]
+
+["test_pauselifetime-03.js"]
+
+["test_pauselifetime-04.js"]
+
+["test_promise_state-01.js"]
+
+["test_promise_state-02.js"]
+
+["test_promise_state-03.js"]
+
+["test_register_actor.js"]
+
+["test_requestTypes.js"]
+
+["test_restartFrame-01.js"]
+
+["test_safe-getter.js"]
+
+["test_sessionDataHelpers.js"]
+
+["test_setBreakpoint-at-the-beginning-of-a-minified-fn.js"]
+
+["test_setBreakpoint-at-the-end-of-a-minified-fn.js"]
+
+["test_setBreakpoint-on-column-in-gcd-script.js"]
+
+["test_setBreakpoint-on-column.js"]
+
+["test_setBreakpoint-on-line-in-gcd-script.js"]
+
+["test_setBreakpoint-on-line-with-multiple-offsets.js"]
+
+["test_setBreakpoint-on-line-with-multiple-statements.js"]
+
+["test_setBreakpoint-on-line-with-no-offsets-in-gcd-script.js"]
+skip-if = ["true"] # breakpoint sliding is not supported bug 1525685
+
+["test_setBreakpoint-on-line-with-no-offsets.js"]
+skip-if = ["true"] # breakpoint sliding is not supported bug 1525685
+
+["test_setBreakpoint-on-line.js"]
+
+["test_shapes_highlighter_helpers.js"]
+
+["test_source-01.js"]
+
+["test_source-02.js"]
+
+["test_source-03.js"]
+
+["test_source-04.js"]
+
+["test_stepping-01.js"]
+
+["test_stepping-02.js"]
+
+["test_stepping-03.js"]
+
+["test_stepping-04.js"]
+
+["test_stepping-05.js"]
+
+["test_stepping-06.js"]
+
+["test_stepping-07.js"]
+
+["test_stepping-08.js"]
+
+["test_stepping-09.js"]
+
+["test_stepping-10.js"]
+
+["test_stepping-11.js"]
+
+["test_stepping-12.js"]
+
+["test_stepping-13.js"]
+
+["test_stepping-14.js"]
+
+["test_stepping-15.js"]
+
+["test_stepping-16.js"]
+
+["test_stepping-17.js"]
+
+["test_stepping-18.js"]
+
+["test_stepping-19.js"]
+
+["test_stepping-with-skip-breakpoints.js"]
+
+["test_symbolactor.js"]
+
+["test_symbols-01.js"]
+
+["test_symbols-02.js"]
+
+["test_threadlifetime-01.js"]
+
+["test_threadlifetime-02.js"]
+
+["test_threadlifetime-04.js"]
+
+["test_unsafeDereference.js"]
+
+["test_wasm_source-01.js"]
+
+["test_watchpoint-01.js"]
+
+["test_watchpoint-02.js"]
+
+["test_watchpoint-03.js"]
+
+["test_watchpoint-04.js"]
+skip-if = ["apple_silicon"] # Disabled due to bleedover with other tests when run in regular suites; passes in "failures" jobs
+
+["test_watchpoint-05.js"]
+
+["test_webext_apis.js"]
+
+["test_webextension_descriptor.js"]
+
+["test_xpcshell_debugging.js"]
+support-files = ["xpcshell_debugging_script.js"]
diff --git a/devtools/server/tests/xpcshell/xpcshell_debugging_script.js b/devtools/server/tests/xpcshell/xpcshell_debugging_script.js
new file mode 100644
index 0000000000..f762b1c3e8
--- /dev/null
+++ b/devtools/server/tests/xpcshell/xpcshell_debugging_script.js
@@ -0,0 +1,11 @@
+dump("hello from the debugee!\n");
+// We should hit the above dump as we set a breakpoint on the first line
+
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This is a file that test_xpcshell_debugging.js debugs.
+
+debugger; // and why not check we hit this!?
+
+dump("try to set a breakpoint here");