summaryrefslogtreecommitdiffstats
path: root/devtools/server/tests
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/tests')
-rw-r--r--devtools/server/tests/browser/animation-data.html115
-rw-r--r--devtools/server/tests/browser/animation.html170
-rw-r--r--devtools/server/tests/browser/application-manifest-404-manifest.html10
-rw-r--r--devtools/server/tests/browser/application-manifest-basic.html10
-rw-r--r--devtools/server/tests/browser/application-manifest-invalid-json.html11
-rw-r--r--devtools/server/tests/browser/application-manifest-no-manifest.html9
-rw-r--r--devtools/server/tests/browser/application-manifest-warnings.html10
-rw-r--r--devtools/server/tests/browser/browser.toml213
-rw-r--r--devtools/server/tests/browser/browser_accessibility_highlighter_infobar.js73
-rw-r--r--devtools/server/tests/browser/browser_accessibility_infobar_audit_keyboard.js157
-rw-r--r--devtools/server/tests/browser/browser_accessibility_infobar_audit_text_label.js164
-rw-r--r--devtools/server/tests/browser/browser_accessibility_infobar_show.js181
-rw-r--r--devtools/server/tests/browser/browser_accessibility_keyboard_audit.js367
-rw-r--r--devtools/server/tests/browser/browser_accessibility_node.js166
-rw-r--r--devtools/server/tests/browser/browser_accessibility_node_audit.js116
-rw-r--r--devtools/server/tests/browser/browser_accessibility_node_events.js197
-rw-r--r--devtools/server/tests/browser/browser_accessibility_node_tabbing_order_highlighter.js92
-rw-r--r--devtools/server/tests/browser/browser_accessibility_simple.js106
-rw-r--r--devtools/server/tests/browser/browser_accessibility_simulator.js88
-rw-r--r--devtools/server/tests/browser/browser_accessibility_tabbing_order_highlighter.js101
-rw-r--r--devtools/server/tests/browser/browser_accessibility_text_label_audit.js1134
-rw-r--r--devtools/server/tests/browser/browser_accessibility_text_label_audit_frame.js48
-rw-r--r--devtools/server/tests/browser/browser_accessibility_walker.js170
-rw-r--r--devtools/server/tests/browser/browser_accessibility_walker_audit.js155
-rw-r--r--devtools/server/tests/browser/browser_actor_error.js94
-rw-r--r--devtools/server/tests/browser/browser_animation_actor-lifetime.js80
-rw-r--r--devtools/server/tests/browser/browser_animation_emitMutations.js72
-rw-r--r--devtools/server/tests/browser/browser_animation_getMultipleStates.js63
-rw-r--r--devtools/server/tests/browser/browser_animation_getPlayers.js39
-rw-r--r--devtools/server/tests/browser/browser_animation_getStateAfterFinished.js76
-rw-r--r--devtools/server/tests/browser/browser_animation_getSubTreeAnimations.js50
-rw-r--r--devtools/server/tests/browser/browser_animation_keepFinished.js55
-rw-r--r--devtools/server/tests/browser/browser_animation_playPauseIframe.js70
-rw-r--r--devtools/server/tests/browser/browser_animation_playPauseSeveral.js67
-rw-r--r--devtools/server/tests/browser/browser_animation_playerState.js159
-rw-r--r--devtools/server/tests/browser/browser_animation_reconstructState.js40
-rw-r--r--devtools/server/tests/browser/browser_animation_refreshTransitions.js97
-rw-r--r--devtools/server/tests/browser/browser_animation_setCurrentTime.js47
-rw-r--r--devtools/server/tests/browser/browser_animation_setPlaybackRate.js49
-rw-r--r--devtools/server/tests/browser/browser_animation_simple.js39
-rw-r--r--devtools/server/tests/browser/browser_animation_updatedState.js66
-rw-r--r--devtools/server/tests/browser/browser_application_manifest.js87
-rw-r--r--devtools/server/tests/browser/browser_canvasframe_helper_01.js170
-rw-r--r--devtools/server/tests/browser/browser_canvasframe_helper_02.js53
-rw-r--r--devtools/server/tests/browser/browser_canvasframe_helper_03.js129
-rw-r--r--devtools/server/tests/browser/browser_canvasframe_helper_04.js142
-rw-r--r--devtools/server/tests/browser/browser_canvasframe_helper_05.js134
-rw-r--r--devtools/server/tests/browser/browser_canvasframe_helper_06.js116
-rw-r--r--devtools/server/tests/browser/browser_compatibility_cssIssues.js137
-rw-r--r--devtools/server/tests/browser/browser_connectToFrame.js142
-rw-r--r--devtools/server/tests/browser/browser_debugger_server.js198
-rw-r--r--devtools/server/tests/browser/browser_document_devtools_basics.js103
-rw-r--r--devtools/server/tests/browser/browser_document_rdp_basics.js129
-rw-r--r--devtools/server/tests/browser/browser_getProcess.js129
-rw-r--r--devtools/server/tests/browser/browser_inspector-anonymous.js204
-rw-r--r--devtools/server/tests/browser/browser_inspector-iframe.js93
-rw-r--r--devtools/server/tests/browser/browser_inspector-insert.js158
-rw-r--r--devtools/server/tests/browser/browser_inspector-isScrollable.js34
-rw-r--r--devtools/server/tests/browser/browser_inspector-mutations-childlist.js282
-rw-r--r--devtools/server/tests/browser/browser_inspector-release.js54
-rw-r--r--devtools/server/tests/browser/browser_inspector-remove.js102
-rw-r--r--devtools/server/tests/browser/browser_inspector-retain.js157
-rw-r--r--devtools/server/tests/browser/browser_inspector-search.js347
-rw-r--r--devtools/server/tests/browser/browser_inspector-shadow.js231
-rw-r--r--devtools/server/tests/browser/browser_inspector-traversal.js350
-rw-r--r--devtools/server/tests/browser/browser_inspector-utils.js25
-rw-r--r--devtools/server/tests/browser/browser_layout_getGrids.js145
-rw-r--r--devtools/server/tests/browser/browser_layout_simple.js31
-rw-r--r--devtools/server/tests/browser/browser_memory_allocations_01.js107
-rw-r--r--devtools/server/tests/browser/browser_perf-01.js57
-rw-r--r--devtools/server/tests/browser/browser_perf-02.js37
-rw-r--r--devtools/server/tests/browser/browser_perf-04.js53
-rw-r--r--devtools/server/tests/browser/browser_perf-getSupportedFeatures.js23
-rw-r--r--devtools/server/tests/browser/browser_storage_cookies-duplicate-names.js134
-rw-r--r--devtools/server/tests/browser/browser_storage_dynamic_windows.js410
-rw-r--r--devtools/server/tests/browser/browser_storage_listings.js743
-rw-r--r--devtools/server/tests/browser/browser_storage_updates.js343
-rw-r--r--devtools/server/tests/browser/browser_style_utils_getFontPreviewData.js137
-rw-r--r--devtools/server/tests/browser/browser_styles_getRuleText.js34
-rw-r--r--devtools/server/tests/browser/browser_stylesheets_getTextEmpty.js53
-rw-r--r--devtools/server/tests/browser/director-script-target.html18
-rw-r--r--devtools/server/tests/browser/doc_accessibility.html19
-rw-r--r--devtools/server/tests/browser/doc_accessibility_audit.html10
-rw-r--r--devtools/server/tests/browser/doc_accessibility_infobar.html12
-rw-r--r--devtools/server/tests/browser/doc_accessibility_keyboard_audit.html150
-rw-r--r--devtools/server/tests/browser/doc_accessibility_text_label_audit.html463
-rw-r--r--devtools/server/tests/browser/doc_accessibility_text_label_audit_frame.html10
-rw-r--r--devtools/server/tests/browser/doc_allocations.html23
-rw-r--r--devtools/server/tests/browser/doc_compatibility.html28
-rw-r--r--devtools/server/tests/browser/doc_force_cc.html32
-rw-r--r--devtools/server/tests/browser/doc_force_gc.html31
-rw-r--r--devtools/server/tests/browser/doc_iframe.html17
-rw-r--r--devtools/server/tests/browser/doc_iframe2.html15
-rw-r--r--devtools/server/tests/browser/doc_iframe_content.html14
-rw-r--r--devtools/server/tests/browser/doc_innerHTML.html21
-rw-r--r--devtools/server/tests/browser/error-actor.js25
-rw-r--r--devtools/server/tests/browser/grid.html42
-rw-r--r--devtools/server/tests/browser/head.js337
-rw-r--r--devtools/server/tests/browser/inspector-helpers.js161
-rw-r--r--devtools/server/tests/browser/inspector-isScrollable-data.html79
-rw-r--r--devtools/server/tests/browser/inspector-search-data.html54
-rw-r--r--devtools/server/tests/browser/inspector-shadow.html117
-rw-r--r--devtools/server/tests/browser/inspector-traversal-data.html98
-rw-r--r--devtools/server/tests/browser/storage-cookies-same-name.html29
-rw-r--r--devtools/server/tests/browser/storage-dynamic-windows.html117
-rw-r--r--devtools/server/tests/browser/storage-helpers.js50
-rw-r--r--devtools/server/tests/browser/storage-listings.html123
-rw-r--r--devtools/server/tests/browser/storage-secured-iframe.html94
-rw-r--r--devtools/server/tests/browser/storage-unsecured-iframe.html28
-rw-r--r--devtools/server/tests/browser/storage-updates.html47
-rw-r--r--devtools/server/tests/browser/test-errors-actor.js72
-rw-r--r--devtools/server/tests/browser/test-window.xhtml5
-rw-r--r--devtools/server/tests/chrome/Debugger.Source.prototype.element-2.js4
-rw-r--r--devtools/server/tests/chrome/Debugger.Source.prototype.element.html25
-rw-r--r--devtools/server/tests/chrome/Debugger.Source.prototype.element.js7
-rw-r--r--devtools/server/tests/chrome/chrome.toml150
-rw-r--r--devtools/server/tests/chrome/doc_Debugger.Source.prototype.introductionType.xhtml7
-rw-r--r--devtools/server/tests/chrome/hello-actor.js28
-rw-r--r--devtools/server/tests/chrome/iframe1_makeGlobalObjectReference.html1
-rw-r--r--devtools/server/tests/chrome/iframe2_makeGlobalObjectReference.html1
-rw-r--r--devtools/server/tests/chrome/inactive-property-helper/align-content.mjs92
-rw-r--r--devtools/server/tests/chrome/inactive-property-helper/border-image.mjs162
-rw-r--r--devtools/server/tests/chrome/inactive-property-helper/cue-pseudo-element.mjs371
-rw-r--r--devtools/server/tests/chrome/inactive-property-helper/first-letter-pseudo-element.mjs32
-rw-r--r--devtools/server/tests/chrome/inactive-property-helper/first-line-pseudo-element.mjs50
-rw-r--r--devtools/server/tests/chrome/inactive-property-helper/flex-grid-item-properties.mjs229
-rw-r--r--devtools/server/tests/chrome/inactive-property-helper/float.mjs76
-rw-r--r--devtools/server/tests/chrome/inactive-property-helper/gap.mjs133
-rw-r--r--devtools/server/tests/chrome/inactive-property-helper/grid-container-properties.mjs43
-rw-r--r--devtools/server/tests/chrome/inactive-property-helper/grid-with-absolute-properties.mjs71
-rw-r--r--devtools/server/tests/chrome/inactive-property-helper/highlight-pseudo-elements.mjs155
-rw-r--r--devtools/server/tests/chrome/inactive-property-helper/margin-padding.mjs260
-rw-r--r--devtools/server/tests/chrome/inactive-property-helper/max-min-width-height.mjs366
-rw-r--r--devtools/server/tests/chrome/inactive-property-helper/multicol-container-properties.mjs39
-rw-r--r--devtools/server/tests/chrome/inactive-property-helper/place-items-content.mjs159
-rw-r--r--devtools/server/tests/chrome/inactive-property-helper/placeholder-pseudo-element.mjs122
-rw-r--r--devtools/server/tests/chrome/inactive-property-helper/positioned.mjs82
-rw-r--r--devtools/server/tests/chrome/inactive-property-helper/scroll-padding.mjs159
-rw-r--r--devtools/server/tests/chrome/inactive-property-helper/table-cell.mjs21
-rw-r--r--devtools/server/tests/chrome/inactive-property-helper/table.mjs28
-rw-r--r--devtools/server/tests/chrome/inactive-property-helper/text-overflow.mjs92
-rw-r--r--devtools/server/tests/chrome/inactive-property-helper/text-wrap.mjs86
-rw-r--r--devtools/server/tests/chrome/inactive-property-helper/vertical-align.mjs56
-rw-r--r--devtools/server/tests/chrome/inactive-property-helper/width-height-ruby.mjs147
-rw-r--r--devtools/server/tests/chrome/inspector-delay-image-response.sjs46
-rw-r--r--devtools/server/tests/chrome/inspector-eyedropper.html20
-rw-r--r--devtools/server/tests/chrome/inspector-helpers.js133
-rw-r--r--devtools/server/tests/chrome/inspector-search-data.html54
-rw-r--r--devtools/server/tests/chrome/inspector-styles-data.css3
-rw-r--r--devtools/server/tests/chrome/inspector-styles-data.html85
-rw-r--r--devtools/server/tests/chrome/inspector-template.html17
-rw-r--r--devtools/server/tests/chrome/inspector-traversal-data.html103
-rw-r--r--devtools/server/tests/chrome/inspector_css-properties.html12
-rw-r--r--devtools/server/tests/chrome/inspector_display-type.html17
-rw-r--r--devtools/server/tests/chrome/inspector_getImageData.html23
-rw-r--r--devtools/server/tests/chrome/inspector_getOffsetParent.html18
-rw-r--r--devtools/server/tests/chrome/large-image.jpgbin0 -> 793541 bytes
-rw-r--r--devtools/server/tests/chrome/memory-helpers.js72
-rw-r--r--devtools/server/tests/chrome/nonchrome_unsafeDereference.html10
-rw-r--r--devtools/server/tests/chrome/small-image.gifbin0 -> 510655 bytes
-rw-r--r--devtools/server/tests/chrome/suspendTimeouts_content.html1
-rw-r--r--devtools/server/tests/chrome/suspendTimeouts_content.js75
-rw-r--r--devtools/server/tests/chrome/suspendTimeouts_worker.js12
-rw-r--r--devtools/server/tests/chrome/test_Debugger.Script.prototype.global.html48
-rw-r--r--devtools/server/tests/chrome/test_Debugger.Source.prototype.elementAttribute.html159
-rw-r--r--devtools/server/tests/chrome/test_Debugger.Source.prototype.introductionScript.html96
-rw-r--r--devtools/server/tests/chrome/test_Debugger.Source.prototype.introductionType.html159
-rw-r--r--devtools/server/tests/chrome/test_animation-type-longhand.html42
-rw-r--r--devtools/server/tests/chrome/test_css-logic-specificity.html84
-rw-r--r--devtools/server/tests/chrome/test_css-logic.html73
-rw-r--r--devtools/server/tests/chrome/test_css-properties.html72
-rw-r--r--devtools/server/tests/chrome/test_device.html79
-rw-r--r--devtools/server/tests/chrome/test_executeInGlobal-outerized_this.html73
-rw-r--r--devtools/server/tests/chrome/test_highlighter_paused_debugger.html88
-rw-r--r--devtools/server/tests/chrome/test_inspector-changeattrs.html90
-rw-r--r--devtools/server/tests/chrome/test_inspector-changevalue.html68
-rw-r--r--devtools/server/tests/chrome/test_inspector-display-type.html81
-rw-r--r--devtools/server/tests/chrome/test_inspector-duplicate-node.html61
-rw-r--r--devtools/server/tests/chrome/test_inspector-hide.html71
-rw-r--r--devtools/server/tests/chrome/test_inspector-inactive-property-helper.html124
-rw-r--r--devtools/server/tests/chrome/test_inspector-mutations-attr.html169
-rw-r--r--devtools/server/tests/chrome/test_inspector-mutations-events.html187
-rw-r--r--devtools/server/tests/chrome/test_inspector-mutations-value.html163
-rw-r--r--devtools/server/tests/chrome/test_inspector-pick-color.html94
-rw-r--r--devtools/server/tests/chrome/test_inspector-pseudoclass-lock.html185
-rw-r--r--devtools/server/tests/chrome/test_inspector-reload.html90
-rw-r--r--devtools/server/tests/chrome/test_inspector-resize.html69
-rw-r--r--devtools/server/tests/chrome/test_inspector-resolve-url.html87
-rw-r--r--devtools/server/tests/chrome/test_inspector-scroll-into-view.html60
-rw-r--r--devtools/server/tests/chrome/test_inspector-search-front.html163
-rw-r--r--devtools/server/tests/chrome/test_inspector-template.html66
-rw-r--r--devtools/server/tests/chrome/test_inspector_getImageData-wait-for-load.html133
-rw-r--r--devtools/server/tests/chrome/test_inspector_getImageData.html142
-rw-r--r--devtools/server/tests/chrome/test_inspector_getImageDataFromURL.html116
-rw-r--r--devtools/server/tests/chrome/test_inspector_getNodeFromActor.html84
-rw-r--r--devtools/server/tests/chrome/test_inspector_getOffsetParent.html129
-rw-r--r--devtools/server/tests/chrome/test_makeGlobalObjectReference.html96
-rw-r--r--devtools/server/tests/chrome/test_memory.html39
-rw-r--r--devtools/server/tests/chrome/test_memory_allocations_02.html80
-rw-r--r--devtools/server/tests/chrome/test_memory_allocations_03.html80
-rw-r--r--devtools/server/tests/chrome/test_memory_allocations_04.html62
-rw-r--r--devtools/server/tests/chrome/test_memory_allocations_05.html93
-rw-r--r--devtools/server/tests/chrome/test_memory_allocations_06.html51
-rw-r--r--devtools/server/tests/chrome/test_memory_allocations_07.html58
-rw-r--r--devtools/server/tests/chrome/test_memory_attach_01.html33
-rw-r--r--devtools/server/tests/chrome/test_memory_attach_02.html44
-rw-r--r--devtools/server/tests/chrome/test_memory_census.html35
-rw-r--r--devtools/server/tests/chrome/test_memory_gc_01.html50
-rw-r--r--devtools/server/tests/chrome/test_memory_gc_events.html44
-rw-r--r--devtools/server/tests/chrome/test_overflowing-body.html42
-rw-r--r--devtools/server/tests/chrome/test_overflowing-children.html131
-rw-r--r--devtools/server/tests/chrome/test_preference.html128
-rw-r--r--devtools/server/tests/chrome/test_styles-applied.html155
-rw-r--r--devtools/server/tests/chrome/test_styles-computed.html130
-rw-r--r--devtools/server/tests/chrome/test_styles-layout.html110
-rw-r--r--devtools/server/tests/chrome/test_styles-matched.html110
-rw-r--r--devtools/server/tests/chrome/test_styles-modify.html110
-rw-r--r--devtools/server/tests/chrome/test_styles-svg.html61
-rw-r--r--devtools/server/tests/chrome/test_suspendTimeouts.html20
-rw-r--r--devtools/server/tests/chrome/test_suspendTimeouts.js139
-rw-r--r--devtools/server/tests/chrome/test_unsafeDereference.html53
-rw-r--r--devtools/server/tests/chrome/test_webconsole-node-grip.html66
-rw-r--r--devtools/server/tests/chrome/webconsole-helpers.js54
-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
445 files changed, 41065 insertions, 0 deletions
diff --git a/devtools/server/tests/browser/animation-data.html b/devtools/server/tests/browser/animation-data.html
new file mode 100644
index 0000000000..1ee654cb17
--- /dev/null
+++ b/devtools/server/tests/browser/animation-data.html
@@ -0,0 +1,115 @@
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Animation Test Data</title>
+ <style>
+ .ball {
+ width: 80px;
+ height: 80px;
+ border-radius: 50%;
+ background: #f06;
+
+ position: absolute;
+ }
+
+ .still {
+ top: 0;
+ left: 10px;
+ }
+
+ .animated {
+ top: 100px;
+ left: 10px;
+
+ animation: simple-animation 2s infinite alternate;
+ }
+
+ .multi {
+ top: 200px;
+ left: 10px;
+
+ animation: simple-animation 2s infinite alternate,
+ other-animation 5s infinite alternate;
+ }
+
+ .delayed {
+ top: 300px;
+ left: 10px;
+ background: rebeccapurple;
+
+ animation: simple-animation 3s 60s 10;
+ }
+
+ .multi-finite {
+ top: 400px;
+ left: 10px;
+ background: yellow;
+
+ animation: simple-animation 3s,
+ other-animation 4s;
+ }
+
+ .short {
+ top: 500px;
+ left: 10px;
+ background: red;
+
+ animation: simple-animation 2s;
+ }
+
+ .long {
+ top: 600px;
+ left: 10px;
+ background: blue;
+
+ animation: simple-animation 120s;
+ }
+
+ .negative-delay {
+ top: 700px;
+ left: 10px;
+ background: gray;
+
+ animation: simple-animation 15s -10s;
+ animation-fill-mode: forwards;
+ }
+
+ .no-compositor {
+ top: 0;
+ right: 10px;
+ background: gold;
+
+ animation: no-compositor 10s cubic-bezier(.57,-0.02,1,.31) forwards;
+ }
+
+ @keyframes simple-animation {
+ 100% {
+ transform: translateX(300px);
+ }
+ }
+
+ @keyframes other-animation {
+ 100% {
+ background: blue;
+ }
+ }
+
+ @keyframes no-compositor {
+ 100% {
+ margin-right: 600px;
+ }
+ }
+ </style>
+</head>
+</body>
+ <div class="ball still"></div>
+ <div class="ball animated"></div>
+ <div class="ball multi"></div>
+ <div class="ball delayed"></div>
+ <div class="ball multi-finite"></div>
+ <div class="ball short"></div>
+ <div class="ball long"></div>
+ <div class="ball negative-delay"></div>
+ <div class="ball no-compositor"></div>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/animation.html b/devtools/server/tests/browser/animation.html
new file mode 100644
index 0000000000..f7b83df283
--- /dev/null
+++ b/devtools/server/tests/browser/animation.html
@@ -0,0 +1,170 @@
+<!DOCTYPE html>
+<style>
+ .not-animated {
+ display: inline-block;
+
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ background: #eee;
+ }
+
+ .simple-animation {
+ display: inline-block;
+
+ width: 64px;
+ height: 64px;
+ border-radius: 50%;
+ background: red;
+
+ animation: move 200s infinite;
+ }
+
+ .multiple-animations {
+ display: inline-block;
+
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ background: #eee;
+
+ animation: move 200s infinite , glow 100s 5;
+ animation-timing-function: ease-out;
+ animation-direction: reverse;
+ animation-fill-mode: both;
+ }
+
+ .transition {
+ display: inline-block;
+
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ background: #f06;
+
+ transition: width 500s ease-out;
+ }
+ .transition.get-round {
+ width: 200px;
+ }
+
+ .long-animation {
+ display: inline-block;
+
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ background: gold;
+
+ animation: move 100s;
+ }
+
+ .short-animation {
+ display: inline-block;
+
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ background: purple;
+
+ animation: move 1s;
+ }
+
+ .delayed-animation {
+ display: inline-block;
+
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ background: rebeccapurple;
+
+ animation: move 200s 5s infinite;
+ }
+
+ .delayed-transition {
+ display: inline-block;
+
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ background: black;
+
+ transition: width 500s 3s;
+ }
+ .delayed-transition.get-round {
+ width: 200px;
+ }
+
+ .delayed-multiple-animations {
+ display: inline-block;
+
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ background: green;
+
+ animation: move .5s 1s 10, glow 1s .75s 30;
+ }
+
+ .multiple-animations-2 {
+ display: inline-block;
+
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ background: blue;
+
+ animation: move .5s, glow 100s 2s infinite, grow 300s 1s 100;
+ }
+
+ .all-transitions {
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 50px;
+ height: 50px;
+ background: blue;
+ transition: all .2s;
+ }
+ .all-transitions.expand {
+ width: 200px;
+ height: 100px;
+ }
+
+ @keyframes move {
+ 100% {
+ transform: translateY(100px);
+ }
+ }
+
+ @keyframes glow {
+ 100% {
+ background: yellow;
+ }
+ }
+
+ @keyframes grow {
+ 100% {
+ width: 100px;
+ }
+ }
+</style>
+<div class="not-animated"></div>
+<div class="simple-animation"></div>
+<div class="multiple-animations"></div>
+<div class="transition"></div>
+<div class="long-animation"></div>
+<div class="short-animation"></div>
+<div class="delayed-animation"></div>
+<div class="delayed-transition"></div>
+<div class="delayed-multiple-animations"></div>
+<div class="multiple-animations-2"></div>
+<div class="all-transitions"></div>
+<script type="text/javascript">
+ "use strict";
+ // Get the transitions started when the page loads
+ addEventListener("load", function() {
+ document.querySelector(".transition").classList.add("get-round");
+ document.querySelector(".delayed-transition").classList.add("get-round");
+ });
+</script>
diff --git a/devtools/server/tests/browser/application-manifest-404-manifest.html b/devtools/server/tests/browser/application-manifest-404-manifest.html
new file mode 100644
index 0000000000..fd182a69a6
--- /dev/null
+++ b/devtools/server/tests/browser/application-manifest-404-manifest.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Simple manifest</title>
+ <link rel="manifest" href="non-existing-manifest.json">
+</head>
+<body>
+ <p>This page links to a manifest URL that is a 404.</p>
+</body>
diff --git a/devtools/server/tests/browser/application-manifest-basic.html b/devtools/server/tests/browser/application-manifest-basic.html
new file mode 100644
index 0000000000..a8e11a645f
--- /dev/null
+++ b/devtools/server/tests/browser/application-manifest-basic.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Simple manifest</title>
+ <link rel="manifest" href='data:application/manifest+json,{"name": "FooApp"}'>
+</head>
+<body>
+ <pre><code>{ "name": "Foo App" }</code></pre>
+</body>
diff --git a/devtools/server/tests/browser/application-manifest-invalid-json.html b/devtools/server/tests/browser/application-manifest-invalid-json.html
new file mode 100644
index 0000000000..2717a97ddd
--- /dev/null
+++ b/devtools/server/tests/browser/application-manifest-invalid-json.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Invalid JSON</title>
+ <link rel="manifest" href='data:application/manifest+json,foo:'>
+</head>
+<body>
+ <p>Invalid JSON:</p>
+ <pre><code>foo:</code></pre>
+</body>
diff --git a/devtools/server/tests/browser/application-manifest-no-manifest.html b/devtools/server/tests/browser/application-manifest-no-manifest.html
new file mode 100644
index 0000000000..5f0668aa50
--- /dev/null
+++ b/devtools/server/tests/browser/application-manifest-no-manifest.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>No manifest</title>
+</head>
+<body>
+ <p>This page does not link to a manifest</p>
+</body>
diff --git a/devtools/server/tests/browser/application-manifest-warnings.html b/devtools/server/tests/browser/application-manifest-warnings.html
new file mode 100644
index 0000000000..57f8b9b4e7
--- /dev/null
+++ b/devtools/server/tests/browser/application-manifest-warnings.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Empty manifest</title>
+ <link rel="manifest" href='data:application/manifest+json,{"name": 0}'>
+</head>
+<body>
+ <pre><code>{ }</code></pre>
+</body>
diff --git a/devtools/server/tests/browser/browser.toml b/devtools/server/tests/browser/browser.toml
new file mode 100644
index 0000000000..e03ab5649a
--- /dev/null
+++ b/devtools/server/tests/browser/browser.toml
@@ -0,0 +1,213 @@
+[DEFAULT]
+tags = "devtools"
+subsuite = "devtools"
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+]
+support-files = [
+ "head.js",
+ "animation.html",
+ "animation-data.html",
+ "application-manifest-404-manifest.html",
+ "application-manifest-basic.html",
+ "application-manifest-invalid-json.html",
+ "application-manifest-no-manifest.html",
+ "application-manifest-warnings.html",
+ "doc_accessibility_audit.html",
+ "doc_accessibility_infobar.html",
+ "doc_accessibility_keyboard_audit.html",
+ "doc_accessibility_text_label_audit_frame.html",
+ "doc_accessibility_text_label_audit.html",
+ "doc_accessibility.html",
+ "doc_allocations.html",
+ "doc_compatibility.html",
+ "doc_force_cc.html",
+ "doc_force_gc.html",
+ "doc_innerHTML.html",
+ "doc_iframe.html",
+ "doc_iframe_content.html",
+ "doc_iframe2.html",
+ "error-actor.js",
+ "grid.html",
+ "inspector-isScrollable-data.html",
+ "inspector-search-data.html",
+ "inspector-traversal-data.html",
+ "inspector-shadow.html",
+ "storage-cookies-same-name.html",
+ "storage-dynamic-windows.html",
+ "storage-listings.html",
+ "storage-unsecured-iframe.html",
+ "storage-updates.html",
+ "storage-secured-iframe.html",
+ "test-errors-actor.js",
+ "test-window.xhtml",
+ "inspector-helpers.js",
+ "storage-helpers.js",
+ "!/devtools/client/shared/test/shared-head.js",
+ "!/devtools/client/shared/test/telemetry-test-helpers.js",
+ "!/devtools/server/tests/chrome/hello-actor.js",
+]
+
+["browser_accessibility_highlighter_infobar.js"]
+
+["browser_accessibility_infobar_audit_keyboard.js"]
+
+["browser_accessibility_infobar_audit_text_label.js"]
+
+["browser_accessibility_infobar_show.js"]
+
+["browser_accessibility_keyboard_audit.js"]
+
+["browser_accessibility_node.js"]
+
+["browser_accessibility_node_audit.js"]
+
+["browser_accessibility_node_events.js"]
+
+["browser_accessibility_node_tabbing_order_highlighter.js"]
+
+["browser_accessibility_simple.js"]
+
+["browser_accessibility_simulator.js"]
+
+["browser_accessibility_tabbing_order_highlighter.js"]
+
+["browser_accessibility_text_label_audit.js"]
+
+["browser_accessibility_text_label_audit_frame.js"]
+
+["browser_accessibility_walker.js"]
+
+["browser_accessibility_walker_audit.js"]
+
+["browser_actor_error.js"]
+
+["browser_animation_actor-lifetime.js"]
+
+["browser_animation_emitMutations.js"]
+
+["browser_animation_getMultipleStates.js"]
+
+["browser_animation_getPlayers.js"]
+
+["browser_animation_getStateAfterFinished.js"]
+
+["browser_animation_getSubTreeAnimations.js"]
+
+["browser_animation_keepFinished.js"]
+
+["browser_animation_playPauseIframe.js"]
+
+["browser_animation_playPauseSeveral.js"]
+
+["browser_animation_playerState.js"]
+
+["browser_animation_reconstructState.js"]
+
+["browser_animation_refreshTransitions.js"]
+
+["browser_animation_setCurrentTime.js"]
+
+["browser_animation_setPlaybackRate.js"]
+
+["browser_animation_simple.js"]
+
+["browser_animation_updatedState.js"]
+
+["browser_application_manifest.js"]
+
+["browser_canvasframe_helper_01.js"]
+skip-if = ["true"] # Bug 1183605
+
+["browser_canvasframe_helper_02.js"]
+skip-if = ["true"] # iframe will not be loaded in xul:window with strict xhtml.
+
+["browser_canvasframe_helper_03.js"]
+skip-if = ["true"] # Bug 1183605
+
+["browser_canvasframe_helper_04.js"]
+skip-if = ["true"] # Bug 1183605
+
+["browser_canvasframe_helper_05.js"]
+skip-if = ["true"] # Bug 1183605
+
+["browser_canvasframe_helper_06.js"]
+skip-if = ["true"] # Bug 1183605
+
+["browser_compatibility_cssIssues.js"]
+
+["browser_connectToFrame.js"]
+
+["browser_debugger_server.js"]
+
+["browser_document_devtools_basics.js"]
+
+["browser_document_rdp_basics.js"]
+
+["browser_getProcess.js"]
+
+["browser_inspector-anonymous.js"]
+
+["browser_inspector-iframe.js"]
+
+["browser_inspector-insert.js"]
+
+["browser_inspector-isScrollable.js"]
+
+["browser_inspector-mutations-childlist.js"]
+
+["browser_inspector-release.js"]
+
+["browser_inspector-remove.js"]
+
+["browser_inspector-retain.js"]
+
+["browser_inspector-search.js"]
+
+["browser_inspector-shadow.js"]
+
+["browser_inspector-traversal.js"]
+
+["browser_inspector-utils.js"]
+
+["browser_layout_getGrids.js"]
+
+["browser_layout_simple.js"]
+
+["browser_memory_allocations_01.js"]
+
+["browser_perf-01.js"]
+skip-if = ["tsan"] # bug 1804081, profiler issues in TSAN
+
+["browser_perf-02.js"]
+skip-if = ["tsan"] # bug 1804081, profiler issues in TSAN
+
+["browser_perf-04.js"]
+skip-if = ["tsan"] # bug 1804081, profiler issues in TSAN
+
+["browser_perf-getSupportedFeatures.js"]
+skip-if = ["tsan"] # bug 1804081, profiler issues in TSAN
+
+["browser_storage_cookies-duplicate-names.js"]
+https_first_disabled = true
+
+["browser_storage_dynamic_windows.js"]
+https_first_disabled = true
+skip-if = [
+ "debug", # Bug 1715916 - test is having race conditions on slow hardware
+ "tsan", # high frequency intermittent
+ "win11_2009 && asan", # high frequency intermittent
+]
+
+["browser_storage_listings.js"]
+https_first_disabled = true
+
+["browser_storage_updates.js"]
+https_first_disabled = true
+
+["browser_style_utils_getFontPreviewData.js"]
+
+["browser_styles_getRuleText.js"]
+
+["browser_stylesheets_getTextEmpty.js"]
diff --git a/devtools/server/tests/browser/browser_accessibility_highlighter_infobar.js b/devtools/server/tests/browser/browser_accessibility_highlighter_infobar.js
new file mode 100644
index 0000000000..0979276230
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_highlighter_infobar.js
@@ -0,0 +1,73 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Test the accessible highlighter's infobar content.
+
+const {
+ truncateString,
+} = require("resource://devtools/shared/inspector/utils.js");
+const {
+ MAX_STRING_LENGTH,
+} = require("resource://devtools/server/actors/highlighters/utils/accessibility.js");
+
+add_task(async function () {
+ const { target, walker, parentAccessibility, a11yWalker } =
+ await initAccessibilityFrontsForUrl(
+ MAIN_DOMAIN + "doc_accessibility_infobar.html"
+ );
+
+ info("Button front checks");
+ await checkNameAndRole(walker, "#button", a11yWalker, "Accessible Button");
+
+ info("Front with long name checks");
+ await checkNameAndRole(
+ walker,
+ "#h1",
+ a11yWalker,
+ "Lorem ipsum dolor sit ame" + "\u2026" + "e et dolore magna aliqua."
+ );
+
+ await waitForA11yShutdown(parentAccessibility);
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+/**
+ * A helper function for testing the accessible's displayed name and roles.
+ *
+ * @param {Object} walker
+ * The DOM walker.
+ * @param {String} querySelector
+ * The selector for the node to retrieve accessible from.
+ * @param {Object} a11yWalker
+ * The accessibility walker.
+ * @param {String} expectedName
+ * Expected string content for displaying the accessible's name.
+ * We are testing this in particular because name can be truncated.
+ */
+async function checkNameAndRole(
+ walker,
+ querySelector,
+ a11yWalker,
+ expectedName
+) {
+ const node = await walker.querySelector(walker.rootNode, querySelector);
+ const accessibleFront = await a11yWalker.getAccessibleFor(node);
+
+ const { name, role } = accessibleFront;
+ const onHighlightEvent = a11yWalker.once("highlighter-event");
+
+ await a11yWalker.highlightAccessible(accessibleFront);
+ const { options } = await onHighlightEvent;
+ is(options.name, name, "Accessible highlight has correct name option");
+ is(options.role, role, "Accessible highlight has correct role option");
+
+ is(
+ `"${truncateString(name, MAX_STRING_LENGTH)}"`,
+ `"${expectedName}"`,
+ "Accessible has correct displayed name."
+ );
+}
diff --git a/devtools/server/tests/browser/browser_accessibility_infobar_audit_keyboard.js b/devtools/server/tests/browser/browser_accessibility_infobar_audit_keyboard.js
new file mode 100644
index 0000000000..73fc7127f4
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_infobar_audit_keyboard.js
@@ -0,0 +1,157 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Checks for the AccessibleHighlighter's infobar component and its keyboard
+// audit.
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: MAIN_DOMAIN + "doc_accessibility_infobar.html",
+ },
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [], async function () {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ HighlighterEnvironment,
+ } = require("resource://devtools/server/actors/highlighters.js");
+ const {
+ AccessibleHighlighter,
+ } = require("resource://devtools/server/actors/highlighters/accessible.js");
+ const {
+ LocalizationHelper,
+ } = require("resource://devtools/shared/l10n.js");
+ const L10N = new LocalizationHelper(
+ "devtools/shared/locales/accessibility.properties"
+ );
+
+ const {
+ accessibility: {
+ AUDIT_TYPE,
+ ISSUE_TYPE: {
+ [AUDIT_TYPE.KEYBOARD]: {
+ INTERACTIVE_NO_ACTION,
+ FOCUSABLE_NO_SEMANTICS,
+ },
+ },
+ SCORES: { FAIL, WARNING },
+ },
+ } = require("resource://devtools/shared/constants.js");
+
+ /**
+ * Checks for updated content for an infobar.
+ *
+ * @param {Object} infobar
+ * Accessible highlighter's infobar component.
+ * @param {Object} audit
+ * Audit information that is passed on highlighter show.
+ */
+ function checkKeyboard(infobar, audit) {
+ const { issue, score } = audit || {};
+ let expected = "";
+ if (issue) {
+ const { ISSUE_TO_INFOBAR_LABEL_MAP } =
+ infobar.audit.reports[AUDIT_TYPE.KEYBOARD].constructor;
+ expected = L10N.getStr(ISSUE_TO_INFOBAR_LABEL_MAP[issue]);
+ }
+
+ is(
+ infobar.getTextContent("keyboard"),
+ expected,
+ "infobar keyboard audit text content is correct"
+ );
+ if (score) {
+ ok(infobar.getElement("keyboard").classList.contains(score));
+ }
+ }
+
+ // Start testing. First, create highlighter environment and initialize.
+ const env = new HighlighterEnvironment();
+ env.initFromWindow(content.window);
+
+ // Wait for loading highlighter environment content to complete before creating the
+ // highlighter.
+ await new Promise(resolve => {
+ const doc = env.document;
+
+ function onContentLoaded() {
+ if (
+ doc.readyState === "interactive" ||
+ doc.readyState === "complete"
+ ) {
+ resolve();
+ } else {
+ doc.addEventListener("DOMContentLoaded", onContentLoaded, {
+ once: true,
+ });
+ }
+ }
+
+ onContentLoaded();
+ });
+
+ // Now, we can test the Infobar's audit content.
+ const node = content.document.createElement("div");
+ content.document.body.append(node);
+ const highlighter = new AccessibleHighlighter(env);
+ await highlighter.isReady;
+ const infobar = highlighter.accessibleInfobar;
+ const bounds = {
+ x: 0,
+ y: 0,
+ w: 250,
+ h: 100,
+ };
+
+ const tests = [
+ {
+ desc: "Infobar is shown with no keyboard audit content when no audit.",
+ },
+ {
+ desc: "Infobar is shown with no keyboard audit content when audit is null.",
+ audit: null,
+ },
+ {
+ desc:
+ "Infobar is shown with no keyboard audit content when empty " +
+ "keyboard audit.",
+ audit: { [AUDIT_TYPE.KEYBOARD]: null },
+ },
+ {
+ desc: "Infobar is shown with keyboard audit content for an error.",
+ audit: {
+ [AUDIT_TYPE.KEYBOARD]: {
+ score: FAIL,
+ issue: INTERACTIVE_NO_ACTION,
+ },
+ },
+ },
+ {
+ desc: "Infobar is shown with keyboard audit content for a warning.",
+ audit: {
+ [AUDIT_TYPE.KEYBOARD]: {
+ score: WARNING,
+ issue: FOCUSABLE_NO_SEMANTICS,
+ },
+ },
+ },
+ ];
+
+ for (const test of tests) {
+ const { desc, audit } = test;
+
+ info(desc);
+ highlighter.show(node, { ...bounds, audit });
+ checkKeyboard(infobar, audit && audit[AUDIT_TYPE.KEYBOARD]);
+ highlighter.hide();
+ }
+ });
+ }
+ );
+});
diff --git a/devtools/server/tests/browser/browser_accessibility_infobar_audit_text_label.js b/devtools/server/tests/browser/browser_accessibility_infobar_audit_text_label.js
new file mode 100644
index 0000000000..a4e2d895ee
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_infobar_audit_text_label.js
@@ -0,0 +1,164 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Checks for the AccessibleHighlighter's infobar component and its text label
+// audit.
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: MAIN_DOMAIN + "doc_accessibility_infobar.html",
+ },
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [], async function () {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ HighlighterEnvironment,
+ } = require("resource://devtools/server/actors/highlighters.js");
+ const {
+ AccessibleHighlighter,
+ } = require("resource://devtools/server/actors/highlighters/accessible.js");
+ const {
+ LocalizationHelper,
+ } = require("resource://devtools/shared/l10n.js");
+ const L10N = new LocalizationHelper(
+ "devtools/shared/locales/accessibility.properties"
+ );
+
+ const {
+ accessibility: {
+ AUDIT_TYPE,
+ ISSUE_TYPE: {
+ [AUDIT_TYPE.TEXT_LABEL]: {
+ DIALOG_NO_NAME,
+ FORM_NO_VISIBLE_NAME,
+ TOOLBAR_NO_NAME,
+ },
+ },
+ SCORES: { BEST_PRACTICES, FAIL, WARNING },
+ },
+ } = require("resource://devtools/shared/constants.js");
+
+ /**
+ * Checks for updated content for an infobar.
+ *
+ * @param {Object} infobar
+ * Accessible highlighter's infobar component.
+ * @param {Object} audit
+ * Audit information that is passed on highlighter show.
+ */
+ function checkTextLabel(infobar, audit) {
+ const { issue, score } = audit || {};
+ let expected = "";
+ if (issue) {
+ const { ISSUE_TO_INFOBAR_LABEL_MAP } =
+ infobar.audit.reports[AUDIT_TYPE.TEXT_LABEL].constructor;
+ expected = L10N.getStr(ISSUE_TO_INFOBAR_LABEL_MAP[issue]);
+ }
+
+ is(
+ infobar.getTextContent("text-label"),
+ expected,
+ "infobar text label audit text content is correct"
+ );
+ if (score) {
+ ok(infobar.getElement("text-label").classList.contains(score));
+ }
+ }
+
+ // Start testing. First, create highlighter environment and initialize.
+ const env = new HighlighterEnvironment();
+ env.initFromWindow(content.window);
+
+ // Wait for loading highlighter environment content to complete before creating the
+ // highlighter.
+ await new Promise(resolve => {
+ const doc = env.document;
+
+ function onContentLoaded() {
+ if (
+ doc.readyState === "interactive" ||
+ doc.readyState === "complete"
+ ) {
+ resolve();
+ } else {
+ doc.addEventListener("DOMContentLoaded", onContentLoaded, {
+ once: true,
+ });
+ }
+ }
+
+ onContentLoaded();
+ });
+
+ // Now, we can test the Infobar's audit content.
+ const node = content.document.createElement("div");
+ content.document.body.append(node);
+ const highlighter = new AccessibleHighlighter(env);
+ await highlighter.isReady;
+ const infobar = highlighter.accessibleInfobar;
+ const bounds = {
+ x: 0,
+ y: 0,
+ w: 250,
+ h: 100,
+ };
+
+ const tests = [
+ {
+ desc: "Infobar is shown with no text label audit content when no audit.",
+ },
+ {
+ desc: "Infobar is shown with no text label audit content when audit is null.",
+ audit: null,
+ },
+ {
+ desc:
+ "Infobar is shown with no text label audit content when empty " +
+ "text label audit.",
+ audit: { [AUDIT_TYPE.TEXT_LABEL]: null },
+ },
+ {
+ desc: "Infobar is shown with text label audit content for an error.",
+ audit: {
+ [AUDIT_TYPE.TEXT_LABEL]: { score: FAIL, issue: TOOLBAR_NO_NAME },
+ },
+ },
+ {
+ desc: "Infobar is shown with text label audit content for a warning.",
+ audit: {
+ [AUDIT_TYPE.TEXT_LABEL]: {
+ score: WARNING,
+ issue: FORM_NO_VISIBLE_NAME,
+ },
+ },
+ },
+ {
+ desc: "Infobar is shown with text label audit content for best practices.",
+ audit: {
+ [AUDIT_TYPE.TEXT_LABEL]: {
+ score: BEST_PRACTICES,
+ issue: DIALOG_NO_NAME,
+ },
+ },
+ },
+ ];
+
+ for (const test of tests) {
+ const { desc, audit } = test;
+
+ info(desc);
+ highlighter.show(node, { ...bounds, audit });
+ checkTextLabel(infobar, audit && audit[AUDIT_TYPE.TEXT_LABEL]);
+ highlighter.hide();
+ }
+ });
+ }
+ );
+});
diff --git a/devtools/server/tests/browser/browser_accessibility_infobar_show.js b/devtools/server/tests/browser/browser_accessibility_infobar_show.js
new file mode 100644
index 0000000000..9fedf6d3b4
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_infobar_show.js
@@ -0,0 +1,181 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Checks for the AccessibleHighlighter's and XULWindowHighlighter's infobar components.
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: MAIN_DOMAIN + "doc_accessibility_infobar.html",
+ },
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [], async function () {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ HighlighterEnvironment,
+ } = require("resource://devtools/server/actors/highlighters.js");
+ const {
+ AccessibleHighlighter,
+ } = require("resource://devtools/server/actors/highlighters/accessible.js");
+
+ /**
+ * Get whether or not infobar container is hidden.
+ *
+ * @param {Object} infobar
+ * Accessible highlighter's infobar component.
+ * @return {String|null} If the infobar container is hidden.
+ */
+ function isContainerHidden(infobar) {
+ return !!infobar
+ .getElement("infobar-container")
+ .getAttribute("hidden");
+ }
+
+ /**
+ * Get name of accessible object.
+ *
+ * @param {Object} infobar
+ * Accessible highlighter's infobar component.
+ * @return {String} The text content of the infobar-name element.
+ */
+ function getName(infobar) {
+ return infobar.getTextContent("infobar-name");
+ }
+
+ /**
+ * Get role of accessible object.
+ *
+ * @param {Object} infobar
+ * Accessible highlighter's infobar component.
+ * @return {String} The text content of the infobar-role element.
+ */
+ function getRole(infobar) {
+ return infobar.getTextContent("infobar-role");
+ }
+
+ /**
+ * Checks for updated content for an infobar with valid bounds.
+ *
+ * @param {Object} infobar
+ * Accessible highlighter's infobar component.
+ * @param {Object} options
+ * Options to pass for the highlighter's show method.
+ * Available options:
+ * - {String} role
+ * Role value of the accessible.
+ * - {String} name
+ * Name value of the accessible.
+ * - {Boolean} shouldBeHidden
+ * If the infobar component should be hidden.
+ */
+ function checkInfobar(infobar, { shouldBeHidden, role, name }) {
+ is(
+ isContainerHidden(infobar),
+ shouldBeHidden,
+ "Infobar's hidden state is correct."
+ );
+
+ if (shouldBeHidden) {
+ return;
+ }
+
+ is(getRole(infobar), role, "infobarRole text content is correct");
+ is(
+ getName(infobar),
+ `"${name}"`,
+ "infoBarName text content is correct"
+ );
+ }
+
+ /**
+ * Checks for updated content of an infobar with valid bounds.
+ *
+ * @param {Element} node
+ * Node to check infobar content on.
+ * @param {Object} highlighter
+ * Accessible highlighter.
+ */
+ function testInfobar(node, highlighter) {
+ const infobar = highlighter.accessibleInfobar;
+ const bounds = {
+ x: 0,
+ y: 0,
+ w: 250,
+ h: 100,
+ };
+
+ info("Check that infobar is shown with valid bounds.");
+ highlighter.show(node, {
+ ...bounds,
+ role: "button",
+ name: "Accessible Button",
+ });
+
+ checkInfobar(infobar, {
+ role: "button",
+ name: "Accessible Button",
+ shouldBeHidden: false,
+ });
+ highlighter.hide();
+
+ info("Check that infobar is hidden after .hide() is called.");
+ checkInfobar(infobar, { shouldBeHidden: true });
+
+ info("Check to make sure content is updated with new options.");
+ highlighter.show(node, {
+ ...bounds,
+ name: "Test link",
+ role: "link",
+ });
+ checkInfobar(infobar, {
+ name: "Test link",
+ role: "link",
+ shouldBeHidden: false,
+ });
+ highlighter.hide();
+ }
+
+ // Start testing. First, create highlighter environment and initialize.
+ const env = new HighlighterEnvironment();
+ env.initFromWindow(content.window);
+
+ // Wait for loading highlighter environment content to complete before creating the
+ // highlighter.
+ await new Promise(resolve => {
+ const doc = env.document;
+
+ function onContentLoaded() {
+ if (
+ doc.readyState === "interactive" ||
+ doc.readyState === "complete"
+ ) {
+ resolve();
+ } else {
+ doc.addEventListener("DOMContentLoaded", onContentLoaded, {
+ once: true,
+ });
+ }
+ }
+
+ onContentLoaded();
+ });
+
+ // Now, we can test the Infobar and XULWindowInfobar components with their
+ // respective highlighters.
+ const node = content.document.createElement("div");
+ content.document.body.append(node);
+
+ info("Checks for Infobar's show method");
+ const highlighter = new AccessibleHighlighter(env);
+ await highlighter.isReady;
+ testInfobar(node, highlighter);
+ });
+ }
+ );
+});
diff --git a/devtools/server/tests/browser/browser_accessibility_keyboard_audit.js b/devtools/server/tests/browser/browser_accessibility_keyboard_audit.js
new file mode 100644
index 0000000000..fee9814b6c
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_keyboard_audit.js
@@ -0,0 +1,367 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Checks functionality around text label audit for the AccessibleActor.
+ */
+
+const {
+ accessibility: {
+ AUDIT_TYPE: { KEYBOARD },
+ SCORES: { FAIL, WARNING },
+ ISSUE_TYPE: {
+ [KEYBOARD]: {
+ FOCUSABLE_NO_SEMANTICS,
+ FOCUSABLE_POSITIVE_TABINDEX,
+ INTERACTIVE_NOT_FOCUSABLE,
+ MOUSE_INTERACTIVE_ONLY,
+ NO_FOCUS_VISIBLE,
+ },
+ },
+ },
+} = require("resource://devtools/shared/constants.js");
+
+add_task(async function () {
+ const { target, walker, parentAccessibility, a11yWalker } =
+ await initAccessibilityFrontsForUrl(
+ `${MAIN_DOMAIN}doc_accessibility_keyboard_audit.html`
+ );
+
+ const tests = [
+ [
+ "Focusable element (styled button) with no semantics.",
+ "#button-1",
+ { score: WARNING, issue: FOCUSABLE_NO_SEMANTICS },
+ ],
+ ["Element (styled button) with no semantics.", "#button-2", null],
+ [
+ "Container element for out of order focusable element.",
+ "#input-container",
+ null,
+ ],
+ [
+ "Interactive element with focus out of order (-1).",
+ "#input-1",
+ {
+ score: FAIL,
+ issue: INTERACTIVE_NOT_FOCUSABLE,
+ },
+ ],
+ [
+ "Interactive element with focus out of order (-1) when disabled.",
+ "#input-2",
+ null,
+ ],
+ ["Interactive element when disabled.", "#input-3", null],
+ ["Focusable interactive element.", "#input-4", null],
+ [
+ "Interactive accesible (link with no attributes) with no accessible actions.",
+ "#link-1",
+ null,
+ ],
+ ["Interactive accessible (link with valid href).", "#link-2", null],
+ ["Interactive accessible (link with # as href).", "#link-3", null],
+ [
+ "Interactive accessible (link with empty string as href).",
+ "#link-4",
+ null,
+ ],
+ ["Interactive accessible with no tabindex.", "#button-3", null],
+ [
+ "Interactive accessible with -1 tabindex.",
+ "#button-4",
+ {
+ score: FAIL,
+ issue: INTERACTIVE_NOT_FOCUSABLE,
+ },
+ ],
+ ["Interactive accessible with 0 tabindex.", "#button-5", null],
+ [
+ "Interactive accessible with 1 tabindex.",
+ "#button-6",
+ { score: WARNING, issue: FOCUSABLE_POSITIVE_TABINDEX },
+ ],
+ [
+ "Focusable ARIA button with no focus styling.",
+ "#focusable-1",
+ { score: WARNING, issue: NO_FOCUS_VISIBLE },
+ ],
+ ["Focusable ARIA button with focus styling.", "#focusable-2", null],
+ ["Focusable ARIA button with browser styling.", "#focusable-3", null],
+ [
+ "Not focusable, non-semantic element that has a click handler.",
+ "#mouse-only-1",
+ { score: FAIL, issue: MOUSE_INTERACTIVE_ONLY },
+ ],
+ [
+ "Focusable, non-semantic element that has a click handler.",
+ "#focusable-4",
+ { score: WARNING, issue: FOCUSABLE_NO_SEMANTICS },
+ ],
+ [
+ "Not focusable, ARIA button that has a click handler.",
+ "#button-7",
+ { score: FAIL, issue: INTERACTIVE_NOT_FOCUSABLE },
+ ],
+ ["Focusable, ARIA button with a click handler.", "#button-8", null],
+ ["Regular image, no keyboard checks should flag an issue.", "#img-1", null],
+ [
+ "Image with a longdesc (accessible will have showlongdesc action).",
+ "#img-2",
+ null,
+ ],
+ [
+ "Clickable image with a longdesc (accessible will have click and showlongdesc actions).",
+ "#img-3",
+ { score: FAIL, issue: MOUSE_INTERACTIVE_ONLY },
+ ],
+ [
+ "Clickable image (accessible will have click action).",
+ "#img-4",
+ { score: FAIL, issue: MOUSE_INTERACTIVE_ONLY },
+ ],
+ ["Focusable button with aria-haspopup.", "#buttonmenu-1", null],
+ [
+ "Not focusable aria button with aria-haspopup.",
+ "#buttonmenu-2",
+ {
+ score: FAIL,
+ issue: INTERACTIVE_NOT_FOCUSABLE,
+ },
+ ],
+ ["Focusable checkbox.", "#checkbox-1", null],
+ ["Focusable select element size > 1", "#listbox-1", null],
+ ["Focusable select element with one option", "#combobox-1", null],
+ ["Focusable select element with no options", "#combobox-2", null],
+ ["Focusable select element with two options", "#combobox-3", null],
+ [
+ "Non-focusable aria combobox with one aria option.",
+ "#editcombobox-1",
+ null,
+ ],
+ ["Non-focusable aria combobox with no options.", "#editcombobox-2", null],
+ ["Focusable aria combobox with no options.", "#editcombobox-3", null],
+ [
+ "Non-focusable aria switch",
+ "#switch-1",
+ {
+ score: FAIL,
+ issue: INTERACTIVE_NOT_FOCUSABLE,
+ },
+ ],
+ ["Focusable aria switch", "#switch-2", null],
+ [
+ "Combobox list that is visible (has focusable state)",
+ "#owned_listbox",
+ null,
+ ],
+ [
+ "Mouse interactive, label that contains form element (linked)",
+ "#label-1",
+ null,
+ ],
+ ["Mouse interactive label for external element (linked)", "#label-2", null],
+ ["Not interactive unlinked label", "#label-3", null],
+ [
+ "Not interactive unlinked label with folloing form element",
+ "#label-4",
+ null,
+ ],
+ ["Image inside an anchor (href)", "#img-5", null],
+ ["Image inside an anchor (onmousedown)", "#img-6", null],
+ ["Image inside an anchor (onclick)", "#img-7", null],
+ ["Image inside an anchor (onmouseup)", "#img-8", null],
+ [
+ "Section with a collapse action from aria-expanded attribute",
+ "#section-1",
+ null,
+ ],
+ ["Tabindex -1 should not report an element as focusable", "#main", null],
+ [
+ "Not keyboard focusable element with no focus styling.",
+ "#not-keyboard-focusable-1",
+ null,
+ ],
+ ["Interactive grid that is not focusable.", "#grid-1", null],
+ ["Focusable interactive grid.", "#grid-2", null],
+ [
+ "Non interactive ARIA table does not need to be focusable.",
+ "#table-1",
+ null,
+ ],
+ [
+ "Focusable ARIA table does not have interactive semantics",
+ "#table-2",
+ { score: "WARNING", issue: "FOCUSABLE_NO_SEMANTICS" },
+ ],
+ ["Non interactive table does not need to be focusable.", "#table-3", null],
+ [
+ "Focusable table does not have interactive semantics",
+ "#table-4",
+ { score: "WARNING", issue: "FOCUSABLE_NO_SEMANTICS" },
+ ],
+ [
+ "Article that is not focusable is not considered interactive",
+ "#article-1",
+ null,
+ ],
+ ["Focusable article is considered interactive", "#article-2", null],
+ [
+ "Column header that is not focusable is not considered interactive (ARIA grid)",
+ "#columnheader-1",
+ null,
+ ],
+ [
+ "Column header that is not focusable is not considered interactive (ARIA table)",
+ "#columnheader-2",
+ null,
+ ],
+ [
+ "Column header that is not focusable is not considered interactive (table)",
+ "#columnheader-3",
+ null,
+ ],
+ [
+ "Column header that is focusable is considered interactive (table)",
+ "#columnheader-4",
+ null,
+ ],
+ [
+ "Column header that is not focusable is not considered interactive (table as ARIA grid)",
+ "#columnheader-5",
+ null,
+ ],
+ [
+ "Column header that is focusable is considered interactive (table as ARIA grid)",
+ "#columnheader-6",
+ null,
+ ],
+ [
+ "Row header that is not focusable is not considered interactive",
+ "#rowheader-1",
+ null,
+ ],
+ [
+ "Row header that is not focusable is not considered interactive",
+ "#rowheader-2",
+ null,
+ ],
+ [
+ "Row header that is not focusable is not considered interactive",
+ "#rowheader-3",
+ null,
+ ],
+ [
+ "Row header that is focusable is considered interactive",
+ "#rowheader-4",
+ null,
+ ],
+ [
+ "Row header that is not focusable is not considered interactive (table as ARIA grid)",
+ "#rowheader-5",
+ null,
+ ],
+ [
+ "Row header that is focusable is considered interactive (table as ARIA grid)",
+ "#rowheader-6",
+ null,
+ ],
+ [
+ "Gridcell that is not focusable is not considered interactive (ARIA grid)",
+ "#gridcell-1",
+ null,
+ ],
+ [
+ "Gridcell that is focusable is considered interactive (ARIA grid)",
+ "#gridcell-2",
+ null,
+ ],
+ [
+ "Gridcell that is not focusable is not considered interactive (table as ARIA grid)",
+ "#gridcell-3",
+ null,
+ ],
+ [
+ "Gridcell that is focusable is considered interactive (table as ARIA grid)",
+ "#gridcell-4",
+ null,
+ ],
+ [
+ "Tab list that is not focusable is not considered interactive",
+ "#tablist-1",
+ null,
+ ],
+ ["Focusable tab list is considered interactive", "#tablist-2", null],
+ [
+ "Scrollbar that is not focusable is not considered interactive",
+ "#scrollbar-1",
+ null,
+ ],
+ ["Focusable scrollbar is considered interactive", "#scrollbar-2", null],
+ [
+ "Separator that is not focusable is not considered interactive",
+ "#separator-1",
+ null,
+ ],
+ ["Focusable separator is considered interactive", "#separator-2", null],
+ [
+ "Toolbar that is not focusable is not considered interactive",
+ "#toolbar-1",
+ null,
+ ],
+ ["Focusable toolbar is considered interactive", "#toolbar-2", null],
+ [
+ "Menu popup that is not focusable is not considered interactive",
+ "#menu-1",
+ null,
+ ],
+ ["Focusable menu popup is considered interactive", "#menu-2", null],
+ [
+ "Menubar that is not focusable is not considered interactive",
+ "#menubar-1",
+ null,
+ ],
+ ["Focusable menubar is considered interactive", "#menubar-2", null],
+ ];
+
+ for (const [description, selector, expected] of tests) {
+ info(description);
+ const node = await walker.querySelector(walker.rootNode, selector);
+ const front = await a11yWalker.getAccessibleFor(node);
+ const audit = await front.audit({ types: [KEYBOARD] });
+ Assert.deepEqual(
+ audit[KEYBOARD],
+ expected,
+ `Audit result for ${selector} is correct.`
+ );
+ }
+
+ info("Text leaf inside a link (jump action is propagated to the text link)");
+ let node = await walker.querySelector(walker.rootNode, "#link-5");
+ let parent = await a11yWalker.getAccessibleFor(node);
+ let front = (await parent.children())[0];
+ let audit = await front.audit({ types: [KEYBOARD] });
+ Assert.deepEqual(
+ audit[KEYBOARD],
+ null,
+ "Text leafs are excluded from semantics rule."
+ );
+
+ info("Combobox list that is invisible");
+ node = await walker.querySelector(walker.rootNode, "#combobox-1");
+ parent = await a11yWalker.getAccessibleFor(node);
+ front = (await parent.children())[0];
+ audit = await front.audit({ types: [KEYBOARD] });
+ Assert.deepEqual(
+ audit[KEYBOARD],
+ null,
+ "Combobox lists (invisible) are excluded from semantics rule."
+ );
+
+ await waitForA11yShutdown(parentAccessibility);
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_accessibility_node.js b/devtools/server/tests/browser/browser_accessibility_node.js
new file mode 100644
index 0000000000..7aff9d9a5d
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_node.js
@@ -0,0 +1,166 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Checks for the AccessibleActor
+
+add_task(async function () {
+ const { target, walker, a11yWalker, parentAccessibility } =
+ await initAccessibilityFrontsForUrl(MAIN_DOMAIN + "doc_accessibility.html");
+ const modifiers =
+ Services.appinfo.OS === "Darwin" ? "\u2303\u2325" : "Alt+Shift+";
+
+ const buttonNode = await walker.querySelector(walker.rootNode, "#button");
+ const accessibleFront = await a11yWalker.getAccessibleFor(buttonNode);
+
+ checkA11yFront(accessibleFront, {
+ name: "Accessible Button",
+ role: "button",
+ childCount: 1,
+ });
+
+ await accessibleFront.hydrate();
+
+ checkA11yFront(accessibleFront, {
+ name: "Accessible Button",
+ role: "button",
+ value: "",
+ description: "Accessibility Test",
+ keyboardShortcut: modifiers + "b",
+ childCount: 1,
+ domNodeType: 1,
+ indexInParent: 1,
+ states: ["focusable", "opaque", "enabled", "sensitive"],
+ actions: ["Press"],
+ attributes: {
+ "margin-top": "0px",
+ display: "inline-block",
+ "text-align": "center",
+ "text-indent": "0px",
+ "margin-left": "0px",
+ tag: "button",
+ "margin-right": "0px",
+ id: "button",
+ "margin-bottom": "0px",
+ },
+ });
+
+ info("Children");
+ const children = await accessibleFront.children();
+ is(children.length, 1, "Accessible Front has correct number of children");
+ checkA11yFront(children[0], {
+ name: "Accessible Button",
+ role: "text leaf",
+ });
+
+ info("Relations");
+ const labelNode = await walker.querySelector(walker.rootNode, "#label");
+ const controlNode = await walker.querySelector(walker.rootNode, "#control");
+ const labelAccessibleFront = await a11yWalker.getAccessibleFor(labelNode);
+ const controlAccessibleFront = await a11yWalker.getAccessibleFor(controlNode);
+ const docAccessibleFront = await a11yWalker.getAccessibleFor(walker.rootNode);
+ const labelRelations = await labelAccessibleFront.getRelations();
+ is(labelRelations.length, 2, "Label has correct number of relations");
+ is(labelRelations[0].type, "label for", "Label has a label for relation");
+ is(labelRelations[0].targets.length, 1, "Label is a label for one target");
+ is(
+ labelRelations[0].targets[0],
+ controlAccessibleFront,
+ "Label is a label for control accessible front"
+ );
+ is(
+ labelRelations[1].type,
+ "containing document",
+ "Label has a containing document relation"
+ );
+ is(
+ labelRelations[1].targets.length,
+ 1,
+ "Label is contained by just one document"
+ );
+ is(
+ labelRelations[1].targets[0],
+ docAccessibleFront,
+ "Label's containing document is a root document"
+ );
+
+ const controlRelations = await controlAccessibleFront.getRelations();
+ is(controlRelations.length, 3, "Control has correct number of relations");
+ is(controlRelations[2].type, "details", "Control has a details relation");
+ is(controlRelations[2].targets.length, 1, "Control has one details target");
+ const detailsNode = await walker.querySelector(walker.rootNode, "#details");
+ const detailsAccessibleFront = await a11yWalker.getAccessibleFor(detailsNode);
+ is(
+ controlRelations[2].targets[0],
+ detailsAccessibleFront,
+ "Control has correct details target"
+ );
+
+ info("Snapshot");
+ const snapshot = await controlAccessibleFront.snapshot();
+ Assert.deepEqual(snapshot, {
+ name: "Label",
+ role: "textbox",
+ actions: ["Activate"],
+ value: "",
+ nodeCssSelector: "#control",
+ nodeType: 1,
+ description: "",
+ keyboardShortcut: "",
+ childCount: 0,
+ indexInParent: 1,
+ states: [
+ "focusable",
+ "autocompletion",
+ "selectable text",
+ "editable",
+ "opaque",
+ "single line",
+ "enabled",
+ "sensitive",
+ ],
+ children: [],
+ attributes: {
+ "margin-left": "0px",
+ "text-align": "start",
+ "text-indent": "0px",
+ id: "control",
+ tag: "input",
+ "margin-top": "0px",
+ "margin-bottom": "0px",
+ "margin-right": "0px",
+ display: "inline-block",
+ "explicit-name": "true",
+ },
+ });
+
+ // Check that we're using ARIA role tokens for landmarks implicit in native
+ // markup.
+ const headerNode = await walker.querySelector(walker.rootNode, "#header");
+ const headerAccessibleFront = await a11yWalker.getAccessibleFor(headerNode);
+ checkA11yFront(headerAccessibleFront, {
+ name: null,
+ role: "banner",
+ childCount: 1,
+ });
+ const navNode = await walker.querySelector(walker.rootNode, "#nav");
+ const navAccessibleFront = await a11yWalker.getAccessibleFor(navNode);
+ checkA11yFront(navAccessibleFront, {
+ name: null,
+ role: "navigation",
+ childCount: 1,
+ });
+ const footerNode = await walker.querySelector(walker.rootNode, "#footer");
+ const footerAccessibleFront = await a11yWalker.getAccessibleFor(footerNode);
+ checkA11yFront(footerAccessibleFront, {
+ name: null,
+ role: "contentinfo",
+ childCount: 1,
+ });
+
+ await waitForA11yShutdown(parentAccessibility);
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_accessibility_node_audit.js b/devtools/server/tests/browser/browser_accessibility_node_audit.js
new file mode 100644
index 0000000000..a3115a2846
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_node_audit.js
@@ -0,0 +1,116 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Checks functionality around audit for the AccessibleActor. This includes
+ * tests for the return value when calling the audit method, payload of the
+ * corresponding event as well as the AccesibleFront state being up to date.
+ */
+
+const {
+ accessibility: { AUDIT_TYPE, SCORES },
+} = require("resource://devtools/shared/constants.js");
+const EMPTY_AUDIT = Object.keys(AUDIT_TYPE).reduce((audit, key) => {
+ audit[key] = null;
+ return audit;
+}, {});
+
+const EXPECTED_CONTRAST_DATA = {
+ value: 21,
+ color: [0, 0, 0, 1],
+ backgroundColor: [255, 255, 255, 1],
+ isLargeText: true,
+ score: SCORES.AAA,
+};
+
+const EMPTY_CONTRAST_AUDIT = {
+ [AUDIT_TYPE.CONTRAST]: null,
+};
+
+const CONTRAST_AUDIT = {
+ [AUDIT_TYPE.CONTRAST]: EXPECTED_CONTRAST_DATA,
+};
+
+const FULL_AUDIT = {
+ ...EMPTY_AUDIT,
+ [AUDIT_TYPE.CONTRAST]: EXPECTED_CONTRAST_DATA,
+};
+
+async function checkAudit(a11yWalker, node, expected, options) {
+ const front = await a11yWalker.getAccessibleFor(node);
+ const [textLeafNode] = await front.children();
+
+ const onAudited = textLeafNode.once("audited");
+ const audit = await textLeafNode.audit(options);
+ const auditFromEvent = await onAudited;
+
+ Assert.deepEqual(audit, expected.audit, "Audit results are correct.");
+ Assert.deepEqual(textLeafNode.checks, expected.checks, "Checks are correct.");
+ Assert.deepEqual(
+ auditFromEvent,
+ expected.audit,
+ "Audit results from event are correct."
+ );
+}
+
+add_task(async function () {
+ const { target, walker, a11yWalker, parentAccessibility } =
+ await initAccessibilityFrontsForUrl(
+ MAIN_DOMAIN + "doc_accessibility_infobar.html"
+ );
+
+ const headerNode = await walker.querySelector(walker.rootNode, "#h1");
+ await checkAudit(
+ a11yWalker,
+ headerNode,
+ { audit: CONTRAST_AUDIT, checks: CONTRAST_AUDIT },
+ { types: [AUDIT_TYPE.CONTRAST] }
+ );
+ await checkAudit(a11yWalker, headerNode, {
+ audit: FULL_AUDIT,
+ checks: FULL_AUDIT,
+ });
+ await checkAudit(
+ a11yWalker,
+ headerNode,
+ { audit: CONTRAST_AUDIT, checks: FULL_AUDIT },
+ { types: [AUDIT_TYPE.CONTRAST] }
+ );
+ await checkAudit(
+ a11yWalker,
+ headerNode,
+ { audit: FULL_AUDIT, checks: FULL_AUDIT },
+ { types: [] }
+ );
+
+ const paragraphNode = await walker.querySelector(walker.rootNode, "#p");
+ await checkAudit(
+ a11yWalker,
+ paragraphNode,
+ { audit: EMPTY_CONTRAST_AUDIT, checks: EMPTY_CONTRAST_AUDIT },
+ { types: [AUDIT_TYPE.CONTRAST] }
+ );
+ await checkAudit(a11yWalker, paragraphNode, {
+ audit: EMPTY_AUDIT,
+ checks: EMPTY_AUDIT,
+ });
+ await checkAudit(
+ a11yWalker,
+ paragraphNode,
+ { audit: EMPTY_CONTRAST_AUDIT, checks: EMPTY_AUDIT },
+ { types: [AUDIT_TYPE.CONTRAST] }
+ );
+ await checkAudit(
+ a11yWalker,
+ paragraphNode,
+ { audit: EMPTY_AUDIT, checks: EMPTY_AUDIT },
+ { types: [] }
+ );
+
+ await waitForA11yShutdown(parentAccessibility);
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_accessibility_node_events.js b/devtools/server/tests/browser/browser_accessibility_node_events.js
new file mode 100644
index 0000000000..77a1e7892f
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_node_events.js
@@ -0,0 +1,197 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Checks for the AccessibleActor events
+
+add_task(async function () {
+ const { target, walker, a11yWalker, parentAccessibility } =
+ await initAccessibilityFrontsForUrl(MAIN_DOMAIN + "doc_accessibility.html");
+ const modifiers =
+ Services.appinfo.OS === "Darwin" ? "\u2303\u2325" : "Alt+Shift+";
+
+ const rootNode = await walker.getRootNode();
+ const a11yDoc = await a11yWalker.getAccessibleFor(rootNode);
+ const buttonNode = await walker.querySelector(walker.rootNode, "#button");
+ const accessibleFront = await a11yWalker.getAccessibleFor(buttonNode);
+ const sliderNode = await walker.querySelector(walker.rootNode, "#slider");
+ const accessibleSliderFront = await a11yWalker.getAccessibleFor(sliderNode);
+ const browser = gBrowser.selectedBrowser;
+
+ checkA11yFront(accessibleFront, {
+ name: "Accessible Button",
+ role: "button",
+ childCount: 1,
+ });
+
+ await accessibleFront.hydrate();
+
+ checkA11yFront(accessibleFront, {
+ name: "Accessible Button",
+ role: "button",
+ value: "",
+ description: "Accessibility Test",
+ keyboardShortcut: modifiers + "b",
+ childCount: 1,
+ domNodeType: 1,
+ indexInParent: 1,
+ states: ["focusable", "opaque", "enabled", "sensitive"],
+ actions: ["Press"],
+ attributes: {
+ "margin-top": "0px",
+ display: "inline-block",
+ "text-align": "center",
+ "text-indent": "0px",
+ "margin-left": "0px",
+ tag: "button",
+ "margin-right": "0px",
+ id: "button",
+ "margin-bottom": "0px",
+ },
+ });
+
+ info("Name change event");
+ await emitA11yEvent(
+ accessibleFront,
+ "name-change",
+ (name, parent) => {
+ checkA11yFront(accessibleFront, { name: "Renamed" });
+ checkA11yFront(parent, {}, a11yDoc);
+ },
+ () =>
+ SpecialPowers.spawn(browser, [], () =>
+ content.document
+ .getElementById("button")
+ .setAttribute("aria-label", "Renamed")
+ )
+ );
+
+ info("Description change event");
+ await emitA11yEvent(
+ accessibleFront,
+ "description-change",
+ () => checkA11yFront(accessibleFront, { description: "" }),
+ () =>
+ SpecialPowers.spawn(browser, [], () =>
+ content.document
+ .getElementById("button")
+ .removeAttribute("aria-describedby")
+ )
+ );
+
+ info("State change event");
+ const expectedStates = ["unavailable", "opaque"];
+ await emitA11yEvent(
+ accessibleFront,
+ "states-change",
+ newStates => {
+ checkA11yFront(accessibleFront, { states: expectedStates });
+ SimpleTest.isDeeply(newStates, expectedStates, "States are updated");
+ },
+ () =>
+ SpecialPowers.spawn(browser, [], () =>
+ content.document.getElementById("button").setAttribute("disabled", true)
+ )
+ );
+
+ info("Attributes change event");
+ await emitA11yEvent(
+ accessibleFront,
+ "attributes-change",
+ newAttrs => {
+ checkA11yFront(accessibleFront, {
+ attributes: {
+ "container-live": "polite",
+ display: "inline-block",
+ "event-from-input": "false",
+ "explicit-name": "true",
+ id: "button",
+ live: "polite",
+ "margin-bottom": "0px",
+ "margin-left": "0px",
+ "margin-right": "0px",
+ "margin-top": "0px",
+ tag: "button",
+ "text-align": "center",
+ "text-indent": "0px",
+ },
+ });
+ is(newAttrs.live, "polite", "Attributes are updated");
+ },
+ () =>
+ SpecialPowers.spawn(browser, [], () =>
+ content.document
+ .getElementById("button")
+ .setAttribute("aria-live", "polite")
+ )
+ );
+
+ info("Value change event");
+ await accessibleSliderFront.hydrate();
+ checkA11yFront(accessibleSliderFront, { value: "5" });
+ await emitA11yEvent(
+ accessibleSliderFront,
+ "value-change",
+ () => checkA11yFront(accessibleSliderFront, { value: "6" }),
+ () =>
+ SpecialPowers.spawn(browser, [], () =>
+ content.document
+ .getElementById("slider")
+ .setAttribute("aria-valuenow", "6")
+ )
+ );
+
+ info("Reorder event");
+ is(accessibleSliderFront.childCount, 1, "Slider has only 1 child");
+ const [firstChild] = await accessibleSliderFront.children();
+ await firstChild.hydrate();
+ is(
+ firstChild.indexInParent,
+ 0,
+ "Slider's first child has correct index in parent"
+ );
+ await emitA11yEvent(
+ accessibleSliderFront,
+ "reorder",
+ childCount => {
+ is(childCount, 2, "Child count is updated");
+ is(accessibleSliderFront.childCount, 2, "Child count is updated");
+ is(
+ firstChild.indexInParent,
+ 1,
+ "Slider's first child has an updated index in parent"
+ );
+ },
+ () =>
+ SpecialPowers.spawn(browser, [], () => {
+ const doc = content.document;
+ const slider = doc.getElementById("slider");
+ const button = doc.createElement("button");
+ button.innerText = "Slider button";
+ content.document
+ .getElementById("slider")
+ .insertBefore(button, slider.firstChild);
+ })
+ );
+
+ await emitA11yEvent(
+ firstChild,
+ "index-in-parent-change",
+ indexInParent =>
+ is(
+ indexInParent,
+ 0,
+ "Slider's first child has an updated index in parent"
+ ),
+ () =>
+ SpecialPowers.spawn(browser, [], () =>
+ content.document.getElementById("slider").firstChild.remove()
+ )
+ );
+
+ await waitForA11yShutdown(parentAccessibility);
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_accessibility_node_tabbing_order_highlighter.js b/devtools/server/tests/browser/browser_accessibility_node_tabbing_order_highlighter.js
new file mode 100644
index 0000000000..adb47c0ec6
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_node_tabbing_order_highlighter.js
@@ -0,0 +1,92 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Checks for the NodeTabbingOrderHighlighter.
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: MAIN_DOMAIN + "doc_accessibility_infobar.html",
+ },
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [], async function () {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ HighlighterEnvironment,
+ } = require("resource://devtools/server/actors/highlighters.js");
+ const {
+ NodeTabbingOrderHighlighter,
+ } = require("resource://devtools/server/actors/highlighters/node-tabbing-order.js");
+
+ // Checks for updated content for an infobar.
+ async function testShowHide(highlighter, node, index) {
+ const shown = highlighter.show(node, { index });
+ const infoBarText = highlighter.getElement("infobar-text");
+
+ ok(shown, "Highlighter is shown.");
+ is(
+ parseInt(infoBarText.getTextContent(), 10),
+ index,
+ "infobar text content is correct"
+ );
+
+ highlighter.hide();
+ }
+
+ // Start testing. First, create highlighter environment and initialize.
+ const env = new HighlighterEnvironment();
+ env.initFromWindow(content.window);
+
+ // Wait for loading highlighter environment content to complete before
+ // creating the highlighter.
+ await new Promise(resolve => {
+ const doc = env.document;
+
+ function onContentLoaded() {
+ if (
+ doc.readyState === "interactive" ||
+ doc.readyState === "complete"
+ ) {
+ resolve();
+ } else {
+ doc.addEventListener("DOMContentLoaded", onContentLoaded, {
+ once: true,
+ });
+ }
+ }
+
+ onContentLoaded();
+ });
+
+ // Now, we can test the Infobar's index content.
+ const node = content.document.createElement("div");
+ content.document.body.append(node);
+ const highlighter = new NodeTabbingOrderHighlighter(env);
+ await highlighter.isReady;
+
+ info("Showing Node tabbing order highlighter with index");
+ await testShowHide(highlighter, node, 1);
+
+ info("Showing Node tabbing order highlighter with new index");
+ await testShowHide(highlighter, node, 9);
+
+ info(
+ "Showing and highlighting focused node with the Node tabbing order highlighter"
+ );
+ highlighter.show(node, { index: 1 });
+ highlighter.updateFocus(true);
+ const { classList } = highlighter.getElement("root");
+ ok(classList.contains("focused"), "Focus styling is applied");
+ highlighter.updateFocus(false);
+ ok(!classList.contains("focused"), "Focus styling is removed");
+ highlighter.hide();
+ });
+ }
+ );
+});
diff --git a/devtools/server/tests/browser/browser_accessibility_simple.js b/devtools/server/tests/browser/browser_accessibility_simple.js
new file mode 100644
index 0000000000..518d4dbb99
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_simple.js
@@ -0,0 +1,106 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const PREF_ACCESSIBILITY_FORCE_DISABLED = "accessibility.force_disabled";
+
+function checkAccessibilityState(accessibility, parentAccessibility, expected) {
+ const { enabled } = accessibility;
+ const { canBeDisabled, canBeEnabled } = parentAccessibility;
+ is(enabled, expected.enabled, "Enabled state is correct.");
+ is(canBeDisabled, expected.canBeDisabled, "canBeDisabled state is correct.");
+ is(canBeEnabled, expected.canBeEnabled, "canBeEnabled state is correct.");
+}
+
+// Simple checks for the AccessibilityActor and AccessibleWalkerActor
+
+add_task(async function () {
+ const {
+ walker: domWalker,
+ target,
+ accessibility,
+ parentAccessibility,
+ a11yWalker,
+ } = await initAccessibilityFrontsForUrl(
+ "data:text/html;charset=utf-8,<title>test</title><div></div>",
+ { enableByDefault: false }
+ );
+
+ ok(accessibility, "The AccessibilityFront was created");
+ ok(accessibility.getWalker, "The getWalker method exists");
+ ok(accessibility.getSimulator, "The getSimulator method exists");
+
+ ok(accessibility.accessibleWalkerFront, "Accessible walker was initialized");
+
+ is(
+ a11yWalker,
+ accessibility.accessibleWalkerFront,
+ "The AccessibleWalkerFront was returned"
+ );
+
+ const a11ySimulator = accessibility.simulatorFront;
+ ok(accessibility.simulatorFront, "Accessible simulator was initialized");
+ is(
+ a11ySimulator,
+ accessibility.simulatorFront,
+ "The SimulatorFront was returned"
+ );
+
+ checkAccessibilityState(accessibility, parentAccessibility, {
+ enabled: false,
+ canBeDisabled: true,
+ canBeEnabled: true,
+ });
+
+ info("Force disable accessibility service: updates canBeEnabled flag");
+ let onEvent = parentAccessibility.once("can-be-enabled-change");
+ Services.prefs.setIntPref(PREF_ACCESSIBILITY_FORCE_DISABLED, 1);
+ await onEvent;
+ checkAccessibilityState(accessibility, parentAccessibility, {
+ enabled: false,
+ canBeDisabled: true,
+ canBeEnabled: false,
+ });
+
+ info("Clear force disable accessibility service: updates canBeEnabled flag");
+ onEvent = parentAccessibility.once("can-be-enabled-change");
+ Services.prefs.clearUserPref(PREF_ACCESSIBILITY_FORCE_DISABLED);
+ await onEvent;
+ checkAccessibilityState(accessibility, parentAccessibility, {
+ enabled: false,
+ canBeDisabled: true,
+ canBeEnabled: true,
+ });
+
+ info("Initialize accessibility service");
+ const initEvent = accessibility.once("init");
+ await parentAccessibility.enable();
+ await waitForA11yInit();
+ await initEvent;
+ checkAccessibilityState(accessibility, parentAccessibility, {
+ enabled: true,
+ canBeDisabled: true,
+ canBeEnabled: true,
+ });
+
+ const rootNode = await domWalker.getRootNode();
+ const a11yDoc = await accessibility.accessibleWalkerFront.getAccessibleFor(
+ rootNode
+ );
+ ok(a11yDoc, "Accessible document actor is created");
+
+ info("Shutdown accessibility service");
+ const shutdownEvent = accessibility.once("shutdown");
+ await waitForA11yShutdown(parentAccessibility);
+ await shutdownEvent;
+ checkAccessibilityState(accessibility, parentAccessibility, {
+ enabled: false,
+ canBeDisabled: true,
+ canBeEnabled: true,
+ });
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_accessibility_simulator.js b/devtools/server/tests/browser/browser_accessibility_simulator.js
new file mode 100644
index 0000000000..47e3b898a3
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_simulator.js
@@ -0,0 +1,88 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ accessibility: {
+ SIMULATION_TYPE: { PROTANOPIA },
+ },
+} = require("resource://devtools/shared/constants.js");
+const {
+ simulation: {
+ COLOR_TRANSFORMATION_MATRICES: {
+ PROTANOPIA: PROTANOPIA_MATRIX,
+ NONE: DEFAULT_MATRIX,
+ },
+ },
+} = require("resource://devtools/server/actors/accessibility/constants.js");
+
+// Checks for the SimulatorActor
+
+async function setup() {
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.window.testColorMatrix = function (actual, expected) {
+ for (const idx in actual) {
+ is(
+ actual[idx].toFixed(3),
+ expected[idx].toFixed(3),
+ "Color matrix value is set correctly."
+ );
+ }
+ };
+ });
+ SimpleTest.registerCleanupFunction(async function () {
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.window.testColorMatrix = null;
+ });
+ });
+}
+
+async function testSimulate(simulator, matrix, type = null) {
+ const matrixApplied = await simulator.simulate({ types: type ? [type] : [] });
+ ok(matrixApplied, "Simulation color matrix is successfully applied.");
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [[type, matrix]],
+ ([simulationType, simulationMatrix]) => {
+ const { window } = content;
+ info(
+ `Test that color matrix is set to ${
+ simulationType || "default"
+ } simulation values.`
+ );
+ window.testColorMatrix(
+ window.docShell.getColorMatrix(),
+ simulationMatrix
+ );
+ }
+ );
+}
+
+add_task(async function () {
+ const { target, accessibility } = await initAccessibilityFrontsForUrl(
+ MAIN_DOMAIN + "doc_accessibility.html",
+ { enableByDefault: false }
+ );
+
+ const simulator = accessibility.simulatorFront;
+ if (!simulator) {
+ ok(false, "Missing simulator actor.");
+ return;
+ }
+
+ await setup();
+
+ info("Test that protanopia is successfully simulated.");
+ await testSimulate(simulator, PROTANOPIA_MATRIX, PROTANOPIA);
+
+ info(
+ "Test that simulations are successfully removed by setting default color matrix."
+ );
+ await testSimulate(simulator, DEFAULT_MATRIX);
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_accessibility_tabbing_order_highlighter.js b/devtools/server/tests/browser/browser_accessibility_tabbing_order_highlighter.js
new file mode 100644
index 0000000000..fb99534318
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_tabbing_order_highlighter.js
@@ -0,0 +1,101 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Checks for the TabbingOrderHighlighter.
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: MAIN_DOMAIN + "doc_accessibility_infobar.html",
+ },
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [], async function () {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ HighlighterEnvironment,
+ } = require("resource://devtools/server/actors/highlighters.js");
+ const {
+ TabbingOrderHighlighter,
+ } = require("resource://devtools/server/actors/highlighters/tabbing-order.js");
+
+ // Start testing. First, create highlighter environment and initialize.
+ const env = new HighlighterEnvironment();
+ env.initFromWindow(content.window);
+
+ // Wait for loading highlighter environment content to complete before
+ // creating the highlighter.
+ await new Promise(resolve => {
+ const doc = env.document;
+
+ function onContentLoaded() {
+ if (
+ doc.readyState === "interactive" ||
+ doc.readyState === "complete"
+ ) {
+ resolve();
+ } else {
+ doc.addEventListener("DOMContentLoaded", onContentLoaded, {
+ once: true,
+ });
+ }
+ }
+
+ onContentLoaded();
+ });
+
+ // Now, we can test the Infobar's index content.
+ const node = content.document.createElement("div");
+ content.document.body.append(node);
+ const highlighter = new TabbingOrderHighlighter(env);
+ await highlighter.isReady;
+
+ info("Showing tabbing order highlighter for all tabbable nodes");
+ const { contentDOMReference, index } = await highlighter.show(
+ content.document,
+ {
+ index: 0,
+ }
+ );
+
+ is(
+ contentDOMReference,
+ null,
+ "No current element when at the end of the tab order"
+ );
+ is(index, 2, "Current index is correct");
+ is(
+ highlighter._highlighters.size,
+ 2,
+ "Number of node tabbing order highlighters is correct"
+ );
+ for (let i = 0; i < highlighter._highlighters.size; i++) {
+ const nodeHighlighter = [...highlighter._highlighters.values()][i];
+ const infoBarText = nodeHighlighter.getElement("infobar-text");
+
+ is(
+ parseInt(infoBarText.getTextContent(), 10),
+ i + 1,
+ "infobar text content is correct"
+ );
+ }
+
+ info("Showing focus highlighting");
+ const input = content.document.getElementById("input");
+ highlighter.updateFocus({ node: input, focused: true });
+ const nodeHighlighter = highlighter._highlighters.get(input);
+ const { classList } = nodeHighlighter.getElement("root");
+ ok(classList.contains("focused"), "Focus styling is applied");
+ highlighter.updateFocus({ node: input, focused: false });
+ ok(!classList.contains("focused"), "Focus styling is removed");
+
+ highlighter.hide();
+ });
+ }
+ );
+});
diff --git a/devtools/server/tests/browser/browser_accessibility_text_label_audit.js b/devtools/server/tests/browser/browser_accessibility_text_label_audit.js
new file mode 100644
index 0000000000..55afbdf936
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_text_label_audit.js
@@ -0,0 +1,1134 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Checks functionality around text label audit for the AccessibleActor.
+ */
+
+const {
+ accessibility: {
+ AUDIT_TYPE: { TEXT_LABEL },
+ SCORES: { BEST_PRACTICES, FAIL, WARNING },
+ ISSUE_TYPE: {
+ [TEXT_LABEL]: {
+ DIALOG_NO_NAME,
+ DOCUMENT_NO_TITLE,
+ EMBED_NO_NAME,
+ FIGURE_NO_NAME,
+ FORM_FIELDSET_NO_NAME,
+ FORM_FIELDSET_NO_NAME_FROM_LEGEND,
+ FORM_NO_NAME,
+ FORM_NO_VISIBLE_NAME,
+ FORM_OPTGROUP_NO_NAME_FROM_LABEL,
+ HEADING_NO_CONTENT,
+ HEADING_NO_NAME,
+ IFRAME_NO_NAME_FROM_TITLE,
+ IMAGE_NO_NAME,
+ INTERACTIVE_NO_NAME,
+ MATHML_GLYPH_NO_NAME,
+ TOOLBAR_NO_NAME,
+ },
+ },
+ },
+} = require("resource://devtools/shared/constants.js");
+
+add_task(async function () {
+ const { target, walker, a11yWalker, parentAccessibility } =
+ await initAccessibilityFrontsForUrl(
+ `${MAIN_DOMAIN}doc_accessibility_text_label_audit.html`
+ );
+
+ const tests = [
+ ["Button menu with inner content", "#buttonmenu-1", null],
+ ["Button menu nested inside a <label>", "#buttonmenu-2", null],
+ [
+ "Button menu with no name",
+ "#buttonmenu-3",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Button menu with aria-label", "#buttonmenu-4", null],
+ ["Button menu with <label>", "#buttonmenu-5", null],
+ ["Button menu with aria-labelledby", "#buttonmenu-6", null],
+ ["Paragraph with inner content", "#p1", null],
+ ["Empty paragraph", "#p2", null],
+ [
+ "<canvas> with no name",
+ "#canvas-1",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ ["<canvas> with aria-label", "#canvas-2", null],
+ ["<canvas> with aria-labelledby", "#canvas-3", null],
+ [
+ "<canvas> with inner content",
+ "#canvas-4",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ [
+ "Checkbox with no name",
+ "#checkbox-1",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Checkbox with unrelated label",
+ "#checkbox-2",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ ["Checkbox nested inside a <label>", "#checkbox-3", null],
+ ["Checkbox with a label", "#checkbox-4", null],
+ [
+ "Checkbox with aria-label",
+ "#checkbox-5",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ ["Checkbox with aria-labelledby visible label", "#checkbox-6", null],
+ [
+ "Empty aria checkbox",
+ "#checkbox-7",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Aria checkbox with aria-label",
+ "#checkbox-8",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ ["Aria checkbox with aria-labelledby visible label", "#checkbox-9", null],
+ ["Menuitem checkbox with inner content", "#menuitemcheckbox-1", null],
+ [
+ "Menuitem checkbox with unlabelled inner content",
+ "#menuitemcheckbox-2",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Empty menuitem checkbox",
+ "#menuitemcheckbox-3",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Menuitem checkbox with no textual inner content",
+ "#menuitemcheckbox-4",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Menuitem checkbox with labelled inner content",
+ "#menuitemcheckbox-5",
+ null,
+ ],
+ [
+ "Menuitem checkbox with white space inner content",
+ "#menuitemcheckbox-6",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Column header with inner content", "#columnheader-1", null],
+ [
+ "Empty column header",
+ "#columnheader-2",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Column header with white space inner content",
+ "#columnheader-3",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Column header with aria-label", "#columnheader-4", null],
+ [
+ "Column header with empty aria-label",
+ "#columnheader-5",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Column header with white space aria-label",
+ "#columnheader-6",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Column header with aria-labelledby", "#columnheader-7", null],
+ ["Aria column header with inner content", "#columnheader-8", null],
+ [
+ "Empty aria column header",
+ "#columnheader-9",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Aria column header with white space inner content",
+ "#columnheader-10",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria column header with aria-label", "#columnheader-11", null],
+ [
+ "Aria column header with empty aria-label",
+ "#columnheader-12",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Aria column header with white space aria-label",
+ "#columnheader-13",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria column header with aria-labelledby", "#columnheader-14", null],
+ ["Combobox with a <label>", "#combobox-1", null],
+ [
+ "Combobox with no label",
+ "#combobox-2",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Combobox with unrelated label",
+ "#combobox-3",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ ["Combobox nested inside a label", "#combobox-4", null],
+ [
+ "Combobox with aria-label",
+ "#combobox-5",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ ["Combobox with aria-labelledby a visible label", "#combobox-6", null],
+ ["Combobox option with inner content", "#combobox-option-1", null],
+ [
+ "Combobox option with no inner content",
+ "#combobox-option-2",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Combobox option with white string inner content",
+ "#combobox-option-3",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Combobox option with label attribute", "#combobox-option-4", null],
+ [
+ "Combobox option with empty label attribute",
+ "#combobox-option-5",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Combobox option with white string label attribute",
+ "#combobox-option-6",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Svg diagram with no name",
+ "#diagram-1",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ [
+ "Svg diagram with empty aria-label",
+ "#diagram-2",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ ["Svg diagram with aria-label", "#diagram-3", null],
+ ["Svg diagram with aria-labelledby", "#diagram-4", null],
+ [
+ "Svg diagram with aria-labelledby an element with empty content",
+ "#diagram-5",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ [
+ "Dialog with no name",
+ "#dialog-1",
+ { score: BEST_PRACTICES, issue: DIALOG_NO_NAME },
+ ],
+ [
+ "Dialog with empty aria-label",
+ "#dialog-2",
+ { score: BEST_PRACTICES, issue: DIALOG_NO_NAME },
+ ],
+ ["Dialog with aria-label", "#dialog-3", null],
+ ["Dialog with aria-labelledby", "#dialog-4", null],
+ [
+ "Aria dialog with no name",
+ "#dialog-5",
+ { score: BEST_PRACTICES, issue: DIALOG_NO_NAME },
+ ],
+ [
+ "Aria dialog with empty aria-label",
+ "#dialog-6",
+ { score: BEST_PRACTICES, issue: DIALOG_NO_NAME },
+ ],
+ ["Aria dialog with aria-label", "#dialog-7", null],
+ ["Aria dialog with aria-labelledby", "#dialog-8", null],
+ [
+ "Dialog with aria-labelledby an element with empty content",
+ "#dialog-9",
+ { score: BEST_PRACTICES, issue: DIALOG_NO_NAME },
+ ],
+ [
+ "Aria dialog with aria-labelledby an element with empty content",
+ "#dialog-10",
+ { score: BEST_PRACTICES, issue: DIALOG_NO_NAME },
+ ],
+ [
+ "Edit combobox with no name",
+ "#editcombobox-1",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Edit combobox with aria-label",
+ "#editcombobox-2",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ [
+ "Edit combobox with aria-labelled a visible label",
+ "#editcombobox-3",
+ null,
+ ],
+ ["Input nested inside a <label>", "#entry-1", null],
+ ["Input with no name", "#entry-2", { score: FAIL, issue: FORM_NO_NAME }],
+ [
+ "Input with aria-label",
+ "#entry-3",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ [
+ "Input with unrelated <label>",
+ "#entry-4",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ ["Input with <label>", "#entry-5", null],
+ ["Input with aria-labelledby", "#entry-6", null],
+ [
+ "Aria textbox with no name",
+ "#entry-7",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Aria textbox with aria-label",
+ "#entry-8",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ ["Aria textbox with aria-labelledby", "#entry-9", null],
+ ["Figure with <figcaption>", "#figure-1", null],
+ [
+ "Figore with no <figcaption>",
+ "#figure-2",
+ { score: BEST_PRACTICES, issue: FIGURE_NO_NAME },
+ ],
+ ["Aria figure with aria-labelledby", "#figure-3", null],
+ [
+ "Aria figure with aria-labelledby an element with empty content",
+ "#figure-4",
+ { score: BEST_PRACTICES, issue: FIGURE_NO_NAME },
+ ],
+ [
+ "Aria figure with no name",
+ "#figure-5",
+ { score: BEST_PRACTICES, issue: FIGURE_NO_NAME },
+ ],
+ ["Image with no alt text", "#img-1", { score: FAIL, issue: IMAGE_NO_NAME }],
+ ["Image with aria-label", "#img-2", null],
+ ["Image with aria-labelledby", "#img-3", null],
+ ["Image with alt text", "#img-4", null],
+ [
+ "Image with aria-labelledby an element with empty content",
+ "#img-5",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ [
+ "Aria image with no name",
+ "#img-6",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ ["Aria image with aria-label", "#img-7", null],
+ ["Aria image with aria-labelledby", "#img-8", null],
+ [
+ "Aria image with empty aria-label",
+ "#img-9",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ [
+ "Aria image with aria-labelledby an element with empty content",
+ "#img-10",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ ["<optgroup> with label", "#optgroup-1", null],
+ [
+ "<optgroup> with empty label",
+ "#optgroup-2",
+ { score: FAIL, issue: FORM_OPTGROUP_NO_NAME_FROM_LABEL },
+ ],
+ [
+ "<optgroup> with no label",
+ "#optgroup-3",
+ { score: FAIL, issue: FORM_OPTGROUP_NO_NAME_FROM_LABEL },
+ ],
+ [
+ "<optgroup> with aria-label",
+ "#optgroup-4",
+ { score: FAIL, issue: FORM_OPTGROUP_NO_NAME_FROM_LABEL },
+ ],
+ [
+ "<optgroup> with aria-labelledby",
+ "#optgroup-5",
+ { score: FAIL, issue: FORM_OPTGROUP_NO_NAME_FROM_LABEL },
+ ],
+ ["<fieldset> with <legend>", "#fieldset-1", null],
+ [
+ "<fieldset> with empty <legend>",
+ "#fieldset-2",
+ { score: FAIL, issue: FORM_FIELDSET_NO_NAME },
+ ],
+ [
+ "<fieldset> with no <legend>",
+ "#fieldset-3",
+ { score: FAIL, issue: FORM_FIELDSET_NO_NAME },
+ ],
+ [
+ "<fieldset> with aria-label",
+ "#fieldset-4",
+ { score: WARNING, issue: FORM_FIELDSET_NO_NAME_FROM_LEGEND },
+ ],
+ [
+ "<fieldset> with aria-labelledby",
+ "#fieldset-5",
+ { score: WARNING, issue: FORM_FIELDSET_NO_NAME_FROM_LEGEND },
+ ],
+ ["Empty <h1>", "#heading-1", { score: FAIL, issue: HEADING_NO_NAME }],
+ ["<h1> with inner content", "#heading-2", null],
+ [
+ "<h1> with white space inner content",
+ "#heading-3",
+ { score: FAIL, issue: HEADING_NO_NAME },
+ ],
+ [
+ "<h1> with aria-label",
+ "#heading-4",
+ { score: WARNING, issue: HEADING_NO_CONTENT },
+ ],
+ [
+ "<h1> with aria-labelledby",
+ "#heading-5",
+ { score: WARNING, issue: HEADING_NO_CONTENT },
+ ],
+ ["<h1> with inner content and aria-label", "#heading-6", null],
+ ["<h1> with inner content and aria-labelledby", "#heading-7", null],
+ [
+ "Empty aria heading",
+ "#heading-8",
+ { score: FAIL, issue: HEADING_NO_NAME },
+ ],
+ ["Aria heading with content", "#heading-9", null],
+ [
+ "Aria heading with white space inner content",
+ "#heading-10",
+ { score: FAIL, issue: HEADING_NO_NAME },
+ ],
+ [
+ "Aria heading with aria-label",
+ "#heading-11",
+ { score: WARNING, issue: HEADING_NO_CONTENT },
+ ],
+ [
+ "Aria heading with aria-labelledby",
+ "#heading-12",
+ { score: WARNING, issue: HEADING_NO_CONTENT },
+ ],
+ ["Aria heading with inner content and aria-label", "#heading-13", null],
+ [
+ "Aria heading with inner content and aria-labelledby",
+ "#heading-14",
+ null,
+ ],
+ [
+ "Image map with no name",
+ "#imagemap-1",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ ["Image map with aria-label", "#imagemap-2", null],
+ ["Image map with aria-labelledby", "#imagemap-3", null],
+ ["Image map with alt attribute", "#imagemap-4", null],
+ [
+ "Image map with aria-labelledby an element with empty content",
+ "#imagemap-5",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ ["<iframe> with title", "#iframe-1", null],
+ [
+ "<iframe> with empty title",
+ "#iframe-2",
+ { score: FAIL, issue: IFRAME_NO_NAME_FROM_TITLE },
+ ],
+ [
+ "<iframe> with no title",
+ "#iframe-3",
+ { score: FAIL, issue: IFRAME_NO_NAME_FROM_TITLE },
+ ],
+ [
+ "<iframe> with aria-label",
+ "#iframe-4",
+ { score: FAIL, issue: IFRAME_NO_NAME_FROM_TITLE },
+ ],
+ [
+ "<iframe> with aria-label and title",
+ "#iframe-5",
+ { score: FAIL, issue: IFRAME_NO_NAME_FROM_TITLE },
+ ],
+ [
+ "<object> with image data type and no name",
+ "#object-1",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ ["<object> with image data type and aria-label", "#object-2", null],
+ ["<object> with image data type and aria-labelledby", "#object-3", null],
+ ["<object> with non-image data type", "#object-4", null],
+ [
+ "<embed> with image data type and no name",
+ "#embed-1",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ [
+ "<embed> with video data type and no name",
+ "#embed-2",
+ { score: FAIL, issue: EMBED_NO_NAME },
+ ],
+ ["<embed> with video data type and aria-label", "#embed-3", null],
+ ["<embed> with video data type and aria-labelledby", "#embed-4", null],
+ ["Link with no inner content", "#link-1", null],
+ ["Link with inner content", "#link-2", null],
+ [
+ "Link with href and no inner content",
+ "#link-3",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Link with href and inner content", "#link-4", null],
+ [
+ "Link with empty href and no inner content",
+ "#link-5",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Link with empty href and inner content", "#link-6", null],
+ [
+ "Link with # href and no inner content",
+ "#link-7",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Link with # href and inner content", "#link-8", null],
+ [
+ "Link with non empty href and no inner content",
+ "#link-9",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Link with non empty href and inner content", "#link-10", null],
+ ["Link with aria-label", "#link-11", null],
+ ["Link with aria-labelledby", "#link-12", null],
+ [
+ "Aria link with no inner content",
+ "#link-13",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria link with inner content", "#link-14", null],
+ ["Aria link with aria-label", "#link-15", null],
+ ["Aria link with aria-labelledby", "#link-16", null],
+ ["<select> with a visible <label>", "#listbox-1", null],
+ [
+ "<select> with no name",
+ "#listbox-2",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "<select> with unrelated <label>",
+ "#listbox-3",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ ["<select> nested inside a <label>", "#listbox-4", null],
+ [
+ "<select> with aria-label",
+ "#listbox-5",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ ["<select> with aria-labelledby a visible element", "#listbox-6", null],
+ [
+ "MathML glyph with no name",
+ "#mglyph-1",
+ { score: FAIL, issue: MATHML_GLYPH_NO_NAME },
+ ],
+ ["MathML glyph with aria-label", "#mglyph-2", null],
+ ["MathML glyph with aria-labelledby", "#mglyph-3", null],
+ ["MathML glyph with alt text", "#mglyph-4", null],
+ [
+ "MathML glyph with empty alt text",
+ "#mglyph-5",
+ { score: FAIL, issue: MATHML_GLYPH_NO_NAME },
+ ],
+ [
+ "MathML glyph with aria-labelledby an element with no inner content",
+ "#mglyph-6",
+ { score: FAIL, issue: MATHML_GLYPH_NO_NAME },
+ ],
+ [
+ "Aria menu item with no name",
+ "#menuitem-1",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Aria menu item with empty aria-label",
+ "#menuitem-2",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria menu item with aria-label", "#menuitem-3", null],
+ ["Aria menu item with aria-labelledby", "#menuitem-4", null],
+ [
+ "Aria menu item with aria-labelledby element with empty inner content",
+ "#menuitem-5",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria menu item with inner content", "#menuitem-6", null],
+ ["Option with inner content", "#option-1", null],
+ [
+ "Option with no inner content",
+ "#option-2",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Option with white space inner ",
+ "#option-3",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Option with a label", "#option-4", null],
+ [
+ "Option with an empty label",
+ "#option-5",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Option with a white space label",
+ "#option-6",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria option with inner content", "#option-7", null],
+ [
+ "Aria option with no inner content",
+ "#option-8",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Aria option with white space inner content",
+ "#option-9",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria option with aria-label", "#option-10", null],
+ [
+ "Aria option with empty aria-label",
+ "#option-11",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Aria option with white space aria-label",
+ "#option-12",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria option with aria-labelledby", "#option-13", null],
+ [
+ "Aria option with aria-labelledby an element with empty content",
+ "#option-14",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Aria option with aria-labelledby an element with white space content",
+ "#option-15",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Empty aria treeitem",
+ "#treeitem-1",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Aria treeitem with empty aria-label",
+ "#treeitem-2",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria treeitem with aria-label", "#treeitem-3", null],
+ ["Aria treeitem with aria-labelledby", "#treeitem-4", null],
+ [
+ "Aria treeitem with aria-labelledby an element with empty content",
+ "#treeitem-5",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria treeitem with inner content", "#treeitem-6", null],
+ [
+ "Aria tab with no content",
+ "#tab-1",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Aria tab with empty aria-label",
+ "#tab-2",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria tab with aria-label", "#tab-3", null],
+ ["Aria tab with aria-labelledby", "#tab-4", null],
+ [
+ "Aria tab with aria-labelledby an element with empty content",
+ "#tab-5",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria tab with inner content", "#tab-6", null],
+ ["Password nested inside a <label>", "#password-1", null],
+ ["Password no name", "#password-2", { score: FAIL, issue: FORM_NO_NAME }],
+ [
+ "Password with aria-label",
+ "#password-3",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ [
+ "Password with unrelated label",
+ "#password-4",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ ["Password with <label>", "#password-5", null],
+ ["Password with aria-labelledby a visible element", "#password-6", null],
+ ["<progress> nested inside a label", "#progress-1", null],
+ [
+ "<progress> with no name",
+ "#progress-2",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "<progress> with aria-label",
+ "#progress-3",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ [
+ "<progress> with unrelated <label>",
+ "#progress-4",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ ["<progress> with <label>", "#progress-5", null],
+ ["<progress> with aria-labelledby a visible element", "#progress-6", null],
+ [
+ "Aria progressbar nested inside a <label>",
+ "#progress-7",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Aria progressbar with aria-labelledby a visible element",
+ "#progress-8",
+ null,
+ ],
+ [
+ "Aria progressbar no name",
+ "#progress-9",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Aria progressbar with aria-label",
+ "#progress-10",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ [
+ "Aria progressbar with unrelated <label>",
+ "#progress-11",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Aria progressbar with <label>",
+ "#progress-12",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Aria progressbar with aria-labelledby a visible <label>",
+ "#progress-13",
+ null,
+ ],
+ ["Button with inner content", "#button-1", null],
+ [
+ "Image button with no name",
+ "#button-2",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Button with no name",
+ "#button-3",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Image button with empty alt text",
+ "#button-4",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Image button with alt text", "#button-5", null],
+ [
+ "Button with white space inner content",
+ "#button-6",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Button inside a <label>", "#button-7", null],
+ ["Button with aria-label", "#button-8", null],
+ [
+ "Button with unrelated <label>",
+ "#button-9",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Button with <label>", "#button-10", null],
+ ["Button with aria-labelledby a visile <label>", "#button-11", null],
+ [
+ "Aria button inside a label",
+ "#button-12",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria button with aria-labelled by a <label>", "#button-13", null],
+ [
+ "Aria button with no content",
+ "#button-14",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria button with aria-label", "#button-15", null],
+ [
+ "Aria button with unrelated <label>",
+ "#button-16",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Aria button with <label>",
+ "#button-17",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria button with aria-labelledby a visible <label>", "#button-18", null],
+ ["Radio nested inside a label", "#radiobutton-1", null],
+ [
+ "Radio with no name",
+ "#radiobutton-2",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Radio with aria-label",
+ "#radiobutton-3",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ [
+ "Radio with unrelated <label>",
+ "#radiobutton-4",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ ["Radio with visible label>", "#radiobutton-5", null],
+ ["Radio with aria-labelledby a visible <label>", "#radiobutton-6", null],
+ [
+ "Aria radio with no name",
+ "#radiobutton-7",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Aria radio with aria-label",
+ "#radiobutton-8",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ [
+ "Aria radio with aria-labelledby a visible element",
+ "#radiobutton-9",
+ null,
+ ],
+ ["Aria menuitemradio with inner content", "#menuitemradio-1", null],
+ [
+ "Aria menuitemradio with no inner content",
+ "#menuitemradio-2",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Aria menuitemradio with white space inner content",
+ "#menuitemradio-3",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Rowheader with inner content", "#rowheader-1", null],
+ [
+ "Rowheader with no inner content",
+ "#rowheader-2",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Rowheader with white space inner content",
+ "#rowheader-3",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Rowheader with aria-label", "#rowheader-4", null],
+ [
+ "Rowheader with empty aria-label",
+ "#rowheader-5",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Rowheader with white space aria-label",
+ "#rowheader-6",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Rowheader with aria-labelledby", "#rowheader-7", null],
+ ["Aria rowheader with inner content", "#rowheader-8", null],
+ [
+ "Aria rowheader with no inner content",
+ "#rowheader-9",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Aria rowheader with white space inner content",
+ "#rowheader-10",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria rowheader with aria-label", "#rowheader-11", null],
+ [
+ "Aria rowheader with empty aria-label",
+ "#rowheader-12",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Aria rowheader with white space aria-label",
+ "#rowheader-13",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria rowheader with aria-labelledby", "#rowheader-14", null],
+ ["Slider nested inside a <label>", "#slider-1", null],
+ ["Slider with no name", "#slider-2", { score: FAIL, issue: FORM_NO_NAME }],
+ [
+ "Slider with aria-label",
+ "#slider-3",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ [
+ "Slider with unrelated <label>",
+ "#slider-4",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ ["Slider with a visible <label>", "#slider-5", null],
+ ["Slider with aria-labelled by a visible <label>", "#slider-6", null],
+ [
+ "Aria slider with no name",
+ "#slider-7",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Aria slider with aria-label",
+ "#slider-8",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ ["Aria slider with aria-labelledby a visible element", "#slider-9", null],
+ ["Number input inside a label", "#spinbutton-1", null],
+ [
+ "Number input with no label",
+ "#spinbutton-2",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Number input with aria-label",
+ "#spinbutton-3",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ [
+ "Number input with unrelated <label>",
+ "#spinbutton-4",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ ["Number input with visible <label>", "#spinbutton-5", null],
+ [
+ "Number input with aria-labelled by a visible <label>",
+ "#spinbutton-6",
+ null,
+ ],
+ [
+ "Aria spinbutton with no name",
+ "#spinbutton-7",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Aria spinbutton with aria-label",
+ "#spinbutton-8",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ [
+ "Aria spinbutton with aria-labelledby a visible element",
+ "#spinbutton-9",
+ null,
+ ],
+ [
+ "Aria switch with no name",
+ "#switch-1",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Aria switch wtih aria-label",
+ "#switch-2",
+ { score: WARNING, issue: FORM_NO_VISIBLE_NAME },
+ ],
+ ["Aria switch with aria-labelledby a visible element", "#switch-3", null],
+ [
+ "Aria switch with unrelated <label>",
+ "#switch-4",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ [
+ "Aria switch nested inside a <label>",
+ "#switch-5",
+ { score: FAIL, issue: FORM_NO_NAME },
+ ],
+ // See bug: https://bugzilla.mozilla.org/show_bug.cgi?id=559770
+ // ["Meter inside a label", "#meter-1", null],
+ // See bug: https://bugzilla.mozilla.org/show_bug.cgi?id=559770
+ // ["Meter with no name", "#meter-2", { score: FAIL, issue: FORM_NO_NAME }],
+ // See bug: https://bugzilla.mozilla.org/show_bug.cgi?id=559770
+ // ["Meter with aria-label", "#meter-3",
+ // { score: WARNING, issue: FORM_NO_VISIBLE_NAME}],
+ // See bug: https://bugzilla.mozilla.org/show_bug.cgi?id=559770
+ // ["Meter with unrelated <label>", "#meter-4", { score: FAIL, issue: FORM_NO_NAME }],
+ ["Meter with visible <label>", "#meter-5", null],
+ ["Meter with aria-labelledby a visible <label>", "#meter-6", null],
+ // See bug: https://bugzilla.mozilla.org/show_bug.cgi?id=559770
+ // ["Aria meter with no name", "#meter-7", { score: FAIL, issue: FORM_NO_NAME }],
+ // See bug: https://bugzilla.mozilla.org/show_bug.cgi?id=559770
+ // ["Aria meter with aria-label", "#meter-8",
+ // { score: WARNING, issue: FORM_NO_VISIBLE_NAME}],
+ ["Aria meter with aria-labelledby a visible element", "#meter-9", null],
+ ["Toggle button with inner content", "#togglebutton-1", null],
+ [
+ "Image toggle button with no name",
+ "#togglebutton-2",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Empty toggle button",
+ "#togglebutton-3",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Image toggle button with empty alt text",
+ "#togglebutton-4",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Image toggle button with alt text", "#togglebutton-5", null],
+ [
+ "Toggle button with white space inner content",
+ "#togglebutton-6",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Toggle button nested inside a label", "#togglebutton-7", null],
+ ["Toggle button with aria-label", "#togglebutton-8", null],
+ [
+ "Toggle button with unrelated <label>",
+ "#togglebutton-9",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Toggle button with <label>", "#togglebutton-10", null],
+ [
+ "Toggle button with aria-labelled by a visible <label>",
+ "#togglebutton-11",
+ null,
+ ],
+ [
+ "Aria toggle button nested inside a label",
+ "#togglebutton-12",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Aria toggle button with aria-labelled by and nested inside a label",
+ "#togglebutton-13",
+ null,
+ ],
+ [
+ "Aria toggle button with no name",
+ "#togglebutton-14",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ ["Aria toggle button with aria-label", "#togglebutton-15", null],
+ [
+ "Aria toggle button with unrelated <label>",
+ "#togglebutton-16",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Aria toggle button with <label>",
+ "#togglebutton-17",
+ { score: FAIL, issue: INTERACTIVE_NO_NAME },
+ ],
+ [
+ "Aria toggle button with aria-labelledby a visible <label>",
+ "#togglebutton-18",
+ null,
+ ],
+ ["Non-unique aria toolbar with aria-label", "#toolbar-1", null],
+ [
+ "Non-unique aria toolbar with no name (",
+ "#toolbar-2",
+ { score: FAIL, issue: TOOLBAR_NO_NAME },
+ ],
+ [
+ "Non-unique aAria toolbar with aria-labelledby an element with empty content",
+ "#toolbar-3",
+ { score: FAIL, issue: TOOLBAR_NO_NAME },
+ ],
+ ["Non-unique aria toolbar with aria-labelledby", "#toolbar-4", null],
+ ["SVGElement with role=img that has a title", "#svg-1", null],
+ ["SVGElement without role=img that has a title", "#svg-2", null],
+ [
+ "SVGElement with role=img and no name",
+ "#svg-3",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ [
+ "SVGElement with no name",
+ "#svg-4",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ ["SVGElement with a name", "#svg-5", null],
+ [
+ "SVGElement with a name and with ownerSVGElement with a name",
+ "#svg-6",
+ null,
+ ],
+ ["SVGElement with a title", "#svg-7", null],
+ [
+ "SVGElement with a name and with ownerSVGElement with a title",
+ "#svg-8",
+ null,
+ ],
+ ["SVGElement with role=img that has a title", "#svg-9", null],
+ [
+ "SVGElement with a name and with ownerSVGElement with role=img that has a title",
+ "#svg-10",
+ null,
+ ],
+ [
+ "SVGElement with role=img and no title",
+ "#svg-11",
+ { score: FAIL, issue: IMAGE_NO_NAME },
+ ],
+ [
+ "SVGElement with a name and with ownerSVGElement with role=img and no title",
+ "#svg-12",
+ null,
+ ],
+ ];
+
+ for (const [description, selector, expected] of tests) {
+ info(description);
+ const node = await walker.querySelector(walker.rootNode, selector);
+ const front = await a11yWalker.getAccessibleFor(node);
+ const audit = await front.audit({ types: [TEXT_LABEL] });
+ Assert.deepEqual(
+ audit[TEXT_LABEL],
+ expected,
+ `Audit result for ${selector} is correct.`
+ );
+ }
+
+ info("Test document rule:");
+ const front = await a11yWalker.getAccessibleFor(walker.rootNode);
+ let audit = await front.audit({ types: [TEXT_LABEL] });
+ info("Document with no title");
+ Assert.deepEqual(
+ audit[TEXT_LABEL],
+ { score: FAIL, issue: DOCUMENT_NO_TITLE },
+ "Audit result for document is correct."
+ );
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.document.title = "Hello world";
+ });
+ audit = await front.audit({ types: [TEXT_LABEL] });
+ info("Document with title");
+ Assert.deepEqual(
+ audit[TEXT_LABEL],
+ null,
+ "Audit result for document is correct."
+ );
+
+ await waitForA11yShutdown(parentAccessibility);
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_accessibility_text_label_audit_frame.js b/devtools/server/tests/browser/browser_accessibility_text_label_audit_frame.js
new file mode 100644
index 0000000000..fbd56cee60
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_text_label_audit_frame.js
@@ -0,0 +1,48 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Checks functionality around text label audit for the AccessibleActor that is
+ * created for frame elements.
+ */
+
+const {
+ accessibility: {
+ AUDIT_TYPE: { TEXT_LABEL },
+ SCORES: { FAIL },
+ ISSUE_TYPE: {
+ [TEXT_LABEL]: { FRAME_NO_NAME },
+ },
+ },
+} = require("resource://devtools/shared/constants.js");
+
+add_task(async function () {
+ const { target, walker, a11yWalker, parentAccessibility } =
+ await initAccessibilityFrontsForUrl(
+ `${MAIN_DOMAIN}doc_accessibility_text_label_audit_frame.html`
+ );
+
+ const tests = [
+ ["Frame with no name", "#frame-1", { score: FAIL, issue: FRAME_NO_NAME }],
+ ["Frame with aria-label", "#frame-2", null],
+ ];
+
+ for (const [description, selector, expected] of tests) {
+ info(description);
+ const node = await walker.querySelector(walker.rootNode, selector);
+ const front = await a11yWalker.getAccessibleFor(node);
+ const audit = await front.audit({ types: [TEXT_LABEL] });
+ Assert.deepEqual(
+ audit[TEXT_LABEL],
+ expected,
+ `Audit result for ${selector} is correct.`
+ );
+ }
+
+ await waitForA11yShutdown(parentAccessibility);
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_accessibility_walker.js b/devtools/server/tests/browser/browser_accessibility_walker.js
new file mode 100644
index 0000000000..282a49b19a
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_walker.js
@@ -0,0 +1,170 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Checks for the AccessibleWalkerActor
+
+add_task(async function () {
+ const { target, walker, a11yWalker, parentAccessibility } =
+ await initAccessibilityFrontsForUrl(MAIN_DOMAIN + "doc_accessibility.html");
+
+ ok(a11yWalker, "The AccessibleWalkerFront was returned");
+ const rootNode = await walker.getRootNode();
+ const a11yDoc = await a11yWalker.getAccessibleFor(rootNode);
+ ok(a11yDoc, "The AccessibleFront for root doc is created");
+
+ const children = await a11yWalker.children();
+ is(
+ children.length,
+ 1,
+ "AccessibleWalker only has 1 child - root doc accessible"
+ );
+ is(
+ a11yDoc,
+ children[0],
+ "Root accessible must be AccessibleWalker's only child"
+ );
+
+ const buttonNode = await walker.querySelector(walker.rootNode, "#button");
+ const accessibleFront = await a11yWalker.getAccessibleFor(buttonNode);
+
+ checkA11yFront(accessibleFront, {
+ name: "Accessible Button",
+ role: "button",
+ });
+
+ const ancestry = await a11yWalker.getAncestry(accessibleFront);
+ is(ancestry.length, 1, "Button is a direct child of a root document.");
+ is(
+ ancestry[0].accessible,
+ a11yDoc,
+ "Button's only ancestor is a root document"
+ );
+ is(
+ ancestry[0].children.length,
+ 8,
+ "Root doc should have correct number of children"
+ );
+ ok(
+ ancestry[0].children.includes(accessibleFront),
+ "Button accessible front is in root doc's children"
+ );
+
+ const browser = gBrowser.selectedBrowser;
+
+ // Ensure name-change event is emitted by walker when cached accessible's name
+ // gets updated (via DOM manipularion).
+ await emitA11yEvent(
+ a11yWalker,
+ "name-change",
+ (front, parent) => {
+ checkA11yFront(front, { name: "Renamed" }, accessibleFront);
+ checkA11yFront(parent, {}, a11yDoc);
+ },
+ () =>
+ SpecialPowers.spawn(browser, [], () =>
+ content.document
+ .getElementById("button")
+ .setAttribute("aria-label", "Renamed")
+ )
+ );
+
+ // Ensure reorder event is emitted by walker when DOM tree changes.
+ let docChildren = await a11yDoc.children();
+ is(docChildren.length, 8, "Root doc should have correct number of children");
+
+ await emitA11yEvent(
+ a11yWalker,
+ "reorder",
+ front => checkA11yFront(front, {}, a11yDoc),
+ () =>
+ SpecialPowers.spawn(browser, [], () => {
+ const input = content.document.createElement("input");
+ input.type = "text";
+ input.title = "This is a tooltip";
+ input.value = "New input";
+ content.document.body.appendChild(input);
+ })
+ );
+
+ docChildren = await a11yDoc.children();
+ is(docChildren.length, 9, "Root doc should have correct number of children");
+
+ let shown = await a11yWalker.highlightAccessible(docChildren[0]);
+ ok(shown, "AccessibleHighlighter highlighted the node");
+
+ shown = await a11yWalker.highlightAccessible(a11yDoc);
+ ok(shown, "AccessibleHighlighter highlights the document correctly.");
+ await a11yWalker.unhighlight();
+
+ info("Checking AccessibleWalker picker functionality");
+ ok(a11yWalker.pick, "AccessibleWalker pick method exists");
+ ok(a11yWalker.pickAndFocus, "AccessibleWalker pickAndFocus method exists");
+ ok(a11yWalker.cancelPick, "AccessibleWalker cancelPick method exists");
+
+ let onPickerEvent = a11yWalker.once("picker-accessible-hovered");
+ await a11yWalker.pick();
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#h1",
+ { type: "mousemove" },
+ browser
+ );
+ let acc = await onPickerEvent;
+ checkA11yFront(acc, { name: "Accessibility Test" }, docChildren[0]);
+
+ onPickerEvent = a11yWalker.once("picker-accessible-previewed");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#h1",
+ { shiftKey: true },
+ browser
+ );
+ acc = await onPickerEvent;
+ checkA11yFront(acc, { name: "Accessibility Test" }, docChildren[0]);
+
+ onPickerEvent = a11yWalker.once("picker-accessible-canceled");
+ await BrowserTestUtils.synthesizeKey(
+ "VK_ESCAPE",
+ { type: "keydown" },
+ browser
+ );
+ await onPickerEvent;
+
+ onPickerEvent = a11yWalker.once("picker-accessible-hovered");
+ await a11yWalker.pick();
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#h1",
+ { type: "mousemove" },
+ browser
+ );
+ await onPickerEvent;
+
+ onPickerEvent = a11yWalker.once("picker-accessible-picked");
+ await BrowserTestUtils.synthesizeMouseAtCenter("#h1", {}, browser);
+ acc = await onPickerEvent;
+ checkA11yFront(acc, { name: "Accessibility Test" }, docChildren[0]);
+
+ await a11yWalker.cancelPick();
+
+ info("Checking tabbing order highlighter");
+ let { elm, index } = await a11yWalker.showTabbingOrder(rootNode, 0);
+ isnot(!!elm, "No current element when at the end of the tab order");
+ is(index, 3, "Current index is correct");
+ await a11yWalker.hideTabbingOrder();
+
+ ({ elm, index } = await a11yWalker.showTabbingOrder(buttonNode, 0));
+ isnot(!!elm, "No current element when at the end of the tab order");
+ is(index, 2, "Current index is correct");
+ await a11yWalker.hideTabbingOrder();
+
+ info(
+ "When targets follow the WindowGlobal lifecycle and handle only one document, " +
+ "only check that the panel refreshes correctly and emit its 'reloaded' event"
+ );
+ await reloadBrowser();
+
+ await waitForA11yShutdown(parentAccessibility);
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_accessibility_walker_audit.js b/devtools/server/tests/browser/browser_accessibility_walker_audit.js
new file mode 100644
index 0000000000..289023043f
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_walker_audit.js
@@ -0,0 +1,155 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ accessibility: { AUDIT_TYPE, ISSUE_TYPE, SCORES },
+} = require("resource://devtools/shared/constants.js");
+
+// Checks for the AccessibleWalkerActor audit.
+add_task(async function () {
+ const { target, a11yWalker, parentAccessibility } =
+ await initAccessibilityFrontsForUrl(
+ MAIN_DOMAIN + "doc_accessibility_audit.html"
+ );
+
+ const accessibles = [
+ {
+ name: "",
+ role: "document",
+ childCount: 2,
+ checks: {
+ [AUDIT_TYPE.CONTRAST]: null,
+ [AUDIT_TYPE.KEYBOARD]: null,
+ [AUDIT_TYPE.TEXT_LABEL]: {
+ score: SCORES.FAIL,
+ issue: ISSUE_TYPE.DOCUMENT_NO_TITLE,
+ },
+ },
+ },
+ {
+ name:
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do " +
+ "eiusmod tempor incididunt ut labore et dolore magna aliqua.",
+ role: "paragraph",
+ childCount: 1,
+ checks: {
+ [AUDIT_TYPE.CONTRAST]: null,
+ [AUDIT_TYPE.KEYBOARD]: null,
+ [AUDIT_TYPE.TEXT_LABEL]: null,
+ },
+ },
+ {
+ name:
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do " +
+ "eiusmod tempor incididunt ut labore et dolore magna aliqua.",
+ role: "text leaf",
+ childCount: 0,
+ checks: {
+ [AUDIT_TYPE.CONTRAST]: {
+ value: 4.0,
+ color: [255, 0, 0, 1],
+ backgroundColor: [255, 255, 255, 1],
+ isLargeText: false,
+ score: SCORES.FAIL,
+ },
+ [AUDIT_TYPE.KEYBOARD]: null,
+ [AUDIT_TYPE.TEXT_LABEL]: null,
+ },
+ },
+ {
+ name: "",
+ role: "paragraph",
+ childCount: 1,
+ checks: {
+ [AUDIT_TYPE.CONTRAST]: null,
+ [AUDIT_TYPE.KEYBOARD]: null,
+ [AUDIT_TYPE.TEXT_LABEL]: null,
+ },
+ },
+ {
+ name: "Accessible Paragraph",
+ role: "text leaf",
+ childCount: 0,
+ checks: {
+ [AUDIT_TYPE.CONTRAST]: {
+ value: 4.0,
+ color: [255, 0, 0, 1],
+ backgroundColor: [255, 255, 255, 1],
+ isLargeText: false,
+ score: SCORES.FAIL,
+ },
+ [AUDIT_TYPE.KEYBOARD]: null,
+ [AUDIT_TYPE.TEXT_LABEL]: null,
+ },
+ },
+ ];
+ const total = accessibles.length;
+ const auditProgress = [
+ { total, percentage: 20, completed: 1 },
+ { total, percentage: 40, completed: 2 },
+ { total, percentage: 60, completed: 3 },
+ { total, percentage: 80, completed: 4 },
+ { total, percentage: 100, completed: 5 },
+ ];
+
+ function findAccessible(name, role) {
+ return accessibles.find(
+ accessible => accessible.name === name && accessible.role === role
+ );
+ }
+
+ async function checkWalkerAudit(walker, expectedSize, options) {
+ info("Checking AccessibleWalker audit functionality");
+ const expectedProgress = Array.from(auditProgress);
+ const ancestries = await new Promise((resolve, reject) => {
+ const auditEventHandler = ({ type, ancestries: response, progress }) => {
+ switch (type) {
+ case "error":
+ walker.off("audit-event", auditEventHandler);
+ reject();
+ break;
+ case "completed":
+ walker.off("audit-event", auditEventHandler);
+ resolve(response);
+ is(expectedProgress.length, 0, "All progress events fired");
+ break;
+ case "progress":
+ SimpleTest.isDeeply(
+ progress,
+ expectedProgress.shift(),
+ "Progress data is correct"
+ );
+ break;
+ default:
+ break;
+ }
+ };
+
+ walker.on("audit-event", auditEventHandler);
+ walker.startAudit(options);
+ });
+
+ is(ancestries.length, expectedSize, "The size of ancestries is correct");
+ for (const ancestry of ancestries) {
+ for (const { accessible, children } of ancestry) {
+ checkA11yFront(
+ accessible,
+ findAccessible(accessibles.name, accessibles.role)
+ );
+ for (const child of children) {
+ checkA11yFront(child, findAccessible(child.name, child.role));
+ }
+ }
+ }
+ }
+
+ await checkWalkerAudit(a11yWalker, 3);
+ await checkWalkerAudit(a11yWalker, 2, { types: [AUDIT_TYPE.CONTRAST] });
+
+ await waitForA11yShutdown(parentAccessibility);
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_actor_error.js b/devtools/server/tests/browser/browser_actor_error.js
new file mode 100644
index 0000000000..0c28d77cca
--- /dev/null
+++ b/devtools/server/tests/browser/browser_actor_error.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that clients can catch errors in actors.
+ */
+
+const ACTORS_URL =
+ "chrome://mochitests/content/browser/devtools/server/tests/browser/error-actor.js";
+
+add_task(async function test_old_actor() {
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+
+ ActorRegistry.registerModule(ACTORS_URL, {
+ prefix: "error",
+ constructor: "ErrorActor",
+ type: { global: true },
+ });
+
+ const transport = DevToolsServer.connectPipe();
+ const gClient = new DevToolsClient(transport);
+ await gClient.connect();
+
+ const { errorActor } = await gClient.mainRoot.rootForm;
+ ok(errorActor, "Found the error actor.");
+
+ await Assert.rejects(
+ gClient.request({ to: errorActor, type: "error" }),
+ err =>
+ err.error == "unknownError" &&
+ /error occurred while processing 'error/.test(err.message),
+ "The request should be rejected"
+ );
+
+ await gClient.close();
+});
+
+const TEST_ERRORS_ACTOR_URL =
+ "chrome://mochitests/content/browser/devtools/server/tests/browser/test-errors-actor.js";
+add_task(async function test_protocoljs_actor() {
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+
+ info("Register the new TestErrorsActor");
+ require(TEST_ERRORS_ACTOR_URL);
+ ActorRegistry.registerModule(TEST_ERRORS_ACTOR_URL, {
+ prefix: "testErrors",
+ constructor: "TestErrorsActor",
+ type: { global: true },
+ });
+
+ info("Create a DevTools client/server pair");
+ const transport = DevToolsServer.connectPipe();
+ const gClient = new DevToolsClient(transport);
+ await gClient.connect();
+
+ info("Retrieve a TestErrorsFront instance");
+ const testErrorsFront = await gClient.mainRoot.getFront("testErrors");
+ ok(testErrorsFront, "has a TestErrorsFront instance");
+
+ await Assert.rejects(testErrorsFront.throwsComponentsException(), e => {
+ return new RegExp(
+ `NS_ERROR_NOT_IMPLEMENTED from: ${testErrorsFront.actorID} ` +
+ `\\(${TEST_ERRORS_ACTOR_URL}:\\d+:\\d+\\)`
+ ).test(e.message);
+ });
+ await Assert.rejects(testErrorsFront.throwsException(), e => {
+ // Not asserting the specific error message here, as it changes depending
+ // on the channel.
+ return new RegExp(
+ `Protocol error \\(TypeError\\):.* from: ${testErrorsFront.actorID} ` +
+ `\\(${TEST_ERRORS_ACTOR_URL}:\\d+:\\d+\\)`
+ ).test(e.message);
+ });
+ await Assert.rejects(testErrorsFront.throwsJSError(), e => {
+ return new RegExp(
+ `Protocol error \\(Error\\): JSError from: ${testErrorsFront.actorID} ` +
+ `\\(${TEST_ERRORS_ACTOR_URL}:\\d+:\\d+\\)`
+ ).test(e.message);
+ });
+ await Assert.rejects(testErrorsFront.throwsString(), e => {
+ return new RegExp(`ErrorString from: ${testErrorsFront.actorID}`).test(
+ e.message
+ );
+ });
+ await Assert.rejects(testErrorsFront.throwsObject(), e => {
+ return new RegExp(`foo from: ${testErrorsFront.actorID}`).test(e.message);
+ });
+
+ await gClient.close();
+});
diff --git a/devtools/server/tests/browser/browser_animation_actor-lifetime.js b/devtools/server/tests/browser/browser_animation_actor-lifetime.js
new file mode 100644
index 0000000000..ef157d31fc
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_actor-lifetime.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for Bug 1247243
+
+add_task(async function () {
+ info("Setting up inspector and animation actors.");
+ const { animations, walker } = await initAnimationsFrontForUrl(
+ MAIN_DOMAIN + "animation-data.html"
+ );
+
+ info("Testing animated node actor");
+ const animatedNodeActor = await walker.querySelector(
+ walker.rootNode,
+ ".animated"
+ );
+ await animations.getAnimationPlayersForNode(animatedNodeActor);
+
+ await assertNumberOfAnimationActors(
+ 1,
+ "AnimationActor have 1 AnimationPlayerActors"
+ );
+
+ info("Testing AnimationPlayerActors release");
+ const stillNodeActor = await walker.querySelector(walker.rootNode, ".still");
+ await animations.getAnimationPlayersForNode(stillNodeActor);
+ await assertNumberOfAnimationActors(
+ 0,
+ "AnimationActor does not have any AnimationPlayerActors anymore"
+ );
+
+ info("Testing multi animated node actor");
+ const multiNodeActor = await walker.querySelector(walker.rootNode, ".multi");
+ await animations.getAnimationPlayersForNode(multiNodeActor);
+ await assertNumberOfAnimationActors(
+ 2,
+ "AnimationActor has now 2 AnimationPlayerActors"
+ );
+
+ info("Testing single animated node actor");
+ await animations.getAnimationPlayersForNode(animatedNodeActor);
+ await assertNumberOfAnimationActors(
+ 1,
+ "AnimationActor has only one AnimationPlayerActors"
+ );
+
+ info("Testing AnimationPlayerActors release again");
+ await animations.getAnimationPlayersForNode(stillNodeActor);
+ await assertNumberOfAnimationActors(
+ 0,
+ "AnimationActor does not have any AnimationPlayerActors anymore"
+ );
+
+ async function assertNumberOfAnimationActors(expected, message) {
+ const actors = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [[animations.actorID]],
+ function (actorID) {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ DevToolsServer,
+ } = require("resource://devtools/server/devtools-server.js");
+ // Convert actorID to current compartment string otherwise
+ // searchAllConnectionsForActor is confused and won't find the actor.
+ actorID = String(actorID);
+ const animationActors =
+ DevToolsServer.searchAllConnectionsForActor(actorID);
+ if (!animationActors) {
+ return 0;
+ }
+ return animationActors.actors.length;
+ }
+ );
+ is(actors, expected, message);
+ }
+});
diff --git a/devtools/server/tests/browser/browser_animation_emitMutations.js b/devtools/server/tests/browser/browser_animation_emitMutations.js
new file mode 100644
index 0000000000..796418c937
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_emitMutations.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the AnimationsActor emits events about changed animations on a
+// node after getAnimationPlayersForNode was called on that node.
+
+add_task(async function () {
+ const { target, walker, animations } = await initAnimationsFrontForUrl(
+ MAIN_DOMAIN + "animation.html"
+ );
+
+ info("Retrieve a non-animated node");
+ const node = await walker.querySelector(walker.rootNode, ".not-animated");
+
+ info("Retrieve the animation player for the node");
+ const players = await animations.getAnimationPlayersForNode(node);
+ is(players.length, 0, "The node has no animation players");
+
+ info("Listen for new animations");
+ let onMutations = once(animations, "mutations");
+
+ info("Add a couple of animation on the node");
+ await node.modifyAttributes([
+ { attributeName: "class", newValue: "multiple-animations" },
+ ]);
+ let changes = await onMutations;
+
+ ok(true, "The mutations event was emitted");
+ is(changes.length, 2, "There are 2 changes in the mutation event");
+ ok(
+ changes.every(({ type }) => type === "added"),
+ "Both changes are additions"
+ );
+
+ const names = changes.map(c => c.player.initialState.name).sort();
+ is(names[0], "glow", "The animation 'glow' was added");
+ is(names[1], "move", "The animation 'move' was added");
+
+ info("Store the 2 new players for comparing later");
+ const p1 = changes[0].player;
+ const p2 = changes[1].player;
+
+ info("Listen for removed animations");
+ onMutations = once(animations, "mutations");
+
+ info("Remove the animation css class on the node");
+ await node.modifyAttributes([
+ { attributeName: "class", newValue: "not-animated" },
+ ]);
+
+ changes = await onMutations;
+
+ ok(true, "The mutations event was emitted");
+ is(changes.length, 2, "There are 2 changes in the mutation event");
+ ok(
+ changes.every(({ type }) => type === "removed"),
+ "Both are removals"
+ );
+ ok(
+ changes[0].player === p1 || changes[0].player === p2,
+ "The first removed player was one of the previously added players"
+ );
+ ok(
+ changes[1].player === p1 || changes[1].player === p2,
+ "The second removed player was one of the previously added players"
+ );
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_animation_getMultipleStates.js b/devtools/server/tests/browser/browser_animation_getMultipleStates.js
new file mode 100644
index 0000000000..77e6a7722b
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_getMultipleStates.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that the duration, iterationCount and delay are retrieved correctly for
+// multiple animations.
+
+add_task(async function () {
+ const { target, walker, animations } = await initAnimationsFrontForUrl(
+ MAIN_DOMAIN + "animation.html"
+ );
+
+ await playerHasAnInitialState(walker, animations);
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+async function playerHasAnInitialState(walker, animations) {
+ let state = await getAnimationStateForNode(
+ walker,
+ animations,
+ ".delayed-multiple-animations",
+ 0
+ );
+
+ is(state.duration, 500, "The duration of the first animation is correct");
+ is(
+ state.iterationCount,
+ 10,
+ "The iterationCount of the first animation is correct"
+ );
+ is(state.delay, 1000, "The delay of the first animation is correct");
+
+ state = await getAnimationStateForNode(
+ walker,
+ animations,
+ ".delayed-multiple-animations",
+ 1
+ );
+
+ is(state.duration, 1000, "The duration of the second animation is correct");
+ is(
+ state.iterationCount,
+ 30,
+ "The iterationCount of the second animation is correct"
+ );
+ is(state.delay, 750, "The delay of the second animation is correct");
+}
+
+async function getAnimationStateForNode(
+ walker,
+ animations,
+ selector,
+ playerIndex
+) {
+ const node = await walker.querySelector(walker.rootNode, selector);
+ const players = await animations.getAnimationPlayersForNode(node);
+ const player = players[playerIndex];
+ const state = await player.getCurrentState();
+ return state;
+}
diff --git a/devtools/server/tests/browser/browser_animation_getPlayers.js b/devtools/server/tests/browser/browser_animation_getPlayers.js
new file mode 100644
index 0000000000..de78bab02f
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_getPlayers.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check the output of getAnimationPlayersForNode
+
+add_task(async function () {
+ const { target, walker, animations } = await initAnimationsFrontForUrl(
+ MAIN_DOMAIN + "animation.html"
+ );
+
+ await theRightNumberOfPlayersIsReturned(walker, animations);
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+async function theRightNumberOfPlayersIsReturned(walker, animations) {
+ let node = await walker.querySelector(walker.rootNode, ".not-animated");
+ let players = await animations.getAnimationPlayersForNode(node);
+ is(players.length, 0, "0 players were returned for the unanimated node");
+
+ node = await walker.querySelector(walker.rootNode, ".simple-animation");
+ players = await animations.getAnimationPlayersForNode(node);
+ is(players.length, 1, "One animation player was returned");
+
+ node = await walker.querySelector(walker.rootNode, ".multiple-animations");
+ players = await animations.getAnimationPlayersForNode(node);
+ is(players.length, 2, "Two animation players were returned");
+
+ node = await walker.querySelector(walker.rootNode, ".transition");
+ players = await animations.getAnimationPlayersForNode(node);
+ is(
+ players.length,
+ 1,
+ "One animation player was returned for the transitioned node"
+ );
+}
diff --git a/devtools/server/tests/browser/browser_animation_getStateAfterFinished.js b/devtools/server/tests/browser/browser_animation_getStateAfterFinished.js
new file mode 100644
index 0000000000..038d7b4911
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_getStateAfterFinished.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+"use strict";
+
+// Check that the right duration/iterationCount/delay are retrieved even when
+// the node has multiple animations and one of them already ended before getting
+// the player objects.
+// See devtools/server/actors/animation.js |getPlayerIndex| for more
+// information.
+
+add_task(async function () {
+ const { target, walker, animations } = await initAnimationsFrontForUrl(
+ MAIN_DOMAIN + "animation.html"
+ );
+
+ info("Retrieve a non animated node");
+ const node = await walker.querySelector(walker.rootNode, ".not-animated");
+
+ info("Apply the multiple-animations-2 class to start the animations");
+ await node.modifyAttributes([
+ { attributeName: "class", newValue: "multiple-animations-2" },
+ ]);
+
+ info(
+ "Get the list of players, by the time this executes, the first, " +
+ "short, animation should have ended."
+ );
+ let players = await animations.getAnimationPlayersForNode(node);
+ if (players.length === 3) {
+ info("The short animation hasn't ended yet, wait for a bit.");
+ // The animation lasts for 500ms, so 1000ms should do it.
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ info("And get the list again");
+ players = await animations.getAnimationPlayersForNode(node);
+ }
+
+ is(players.length, 2, "2 animations remain on the node");
+
+ is(
+ players[0].state.duration,
+ 100000,
+ "The duration of the first animation is correct"
+ );
+ is(
+ players[0].state.delay,
+ 2000,
+ "The delay of the first animation is correct"
+ );
+ is(
+ players[0].state.iterationCount,
+ null,
+ "The iterationCount of the first animation is correct"
+ );
+
+ is(
+ players[1].state.duration,
+ 300000,
+ "The duration of the second animation is correct"
+ );
+ is(
+ players[1].state.delay,
+ 1000,
+ "The delay of the second animation is correct"
+ );
+ is(
+ players[1].state.iterationCount,
+ 100,
+ "The iterationCount of the second animation is correct"
+ );
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_animation_getSubTreeAnimations.js b/devtools/server/tests/browser/browser_animation_getSubTreeAnimations.js
new file mode 100644
index 0000000000..0a8a420c18
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_getSubTreeAnimations.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that the AnimationsActor can retrieve all animations inside a node's
+// subtree (but not going into iframes).
+
+const URL = MAIN_DOMAIN + "animation.html";
+
+// Import inspector's shared head.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
+ this
+);
+
+add_task(async function () {
+ info("Creating a test document with 2 iframes containing animated nodes");
+
+ const { inspector, target, walker, animations } =
+ await initAnimationsFrontForUrl(
+ "data:text/html;charset=utf-8," +
+ "<iframe id='iframe' src='" +
+ URL +
+ "'></iframe>"
+ );
+
+ info("Try retrieving all animations from the root doc's <body> node");
+ const rootBody = await walker.querySelector(walker.rootNode, "body");
+ let players = await animations.getAnimationPlayersForNode(rootBody);
+ is(players.length, 0, "The node has no animation players");
+
+ info("Retrieve all animations from the iframe's <body> node");
+ const frameBody = await getNodeFrontInFrames(["#iframe", "body"], inspector);
+ const animationsForFrame = await frameBody.targetFront.getFront("animations");
+ players = await animationsForFrame.getAnimationPlayersForNode(frameBody);
+
+ // Testing for a hard-coded number of animations here would intermittently
+ // fail depending on how fast or slow the test is (indeed, the test page
+ // contains short transitions, and delayed animations). So just make sure we
+ // at least have the infinitely running animations.
+ Assert.greaterOrEqual(
+ players.length,
+ 4,
+ "All subtree animations were retrieved"
+ );
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_animation_keepFinished.js b/devtools/server/tests/browser/browser_animation_keepFinished.js
new file mode 100644
index 0000000000..0adb98ad69
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_keepFinished.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+"use strict";
+
+// Test that the AnimationsActor doesn't report finished animations as removed.
+// Indeed, animations that only have the "finished" playState can be modified
+// still, so we want the AnimationsActor to preserve the corresponding
+// AnimationPlayerActor.
+
+add_task(async function () {
+ const { target, walker, animations } = await initAnimationsFrontForUrl(
+ MAIN_DOMAIN + "animation.html"
+ );
+
+ info("Retrieve a non-animated node");
+ const node = await walker.querySelector(walker.rootNode, ".not-animated");
+
+ info("Retrieve the animation player for the node");
+ let players = await animations.getAnimationPlayersForNode(node);
+ is(players.length, 0, "The node has no animation players");
+
+ info("Listen for new animations");
+ let reportedMutations = [];
+ function onMutations(mutations) {
+ reportedMutations = [...reportedMutations, ...mutations];
+ }
+ animations.on("mutations", onMutations);
+
+ info("Add a short animation on the node");
+ await node.modifyAttributes([
+ { attributeName: "class", newValue: "short-animation" },
+ ]);
+
+ info("Wait for longer than the animation's duration");
+ await wait(2000);
+
+ players = await animations.getAnimationPlayersForNode(node);
+ is(players.length, 0, "The added animation is surely finished");
+
+ is(reportedMutations.length, 1, "Only one mutation was reported");
+ is(reportedMutations[0].type, "added", "The mutation was an addition");
+
+ animations.off("mutations", onMutations);
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function wait(ms) {
+ return new Promise(resolve => {
+ setTimeout(resolve, ms);
+ });
+}
diff --git a/devtools/server/tests/browser/browser_animation_playPauseIframe.js b/devtools/server/tests/browser/browser_animation_playPauseIframe.js
new file mode 100644
index 0000000000..e10fceb0bc
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_playPauseIframe.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that the AnimationsActor can pause/play all animations even those
+// within iframes.
+
+const URL = MAIN_DOMAIN + "animation.html";
+
+// Import inspector's shared head.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
+ this
+);
+
+add_task(async function () {
+ info("Creating a test document with 2 iframes containing animated nodes");
+
+ const { inspector, target } = await initAnimationsFrontForUrl(
+ "data:text/html;charset=utf-8," +
+ "<iframe id='i1' src='" +
+ URL +
+ "'></iframe>" +
+ "<iframe id='i2' src='" +
+ URL +
+ "'></iframe>"
+ );
+
+ info("Getting the 2 iframe container nodes and animated nodes in them");
+ const nodeInFrame1 = await getNodeFrontInFrames(
+ ["#i1", ".simple-animation"],
+ inspector
+ );
+ const nodeInFrame2 = await getNodeFrontInFrames(
+ ["#i2", ".simple-animation"],
+ inspector
+ );
+
+ info("Pause all animations in the test document");
+ await toggleAndCheckStates(nodeInFrame1, "paused");
+ await toggleAndCheckStates(nodeInFrame2, "paused");
+
+ info("Play all animations in the test document");
+ await toggleAndCheckStates(nodeInFrame1, "running");
+ await toggleAndCheckStates(nodeInFrame2, "running");
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+async function toggleAndCheckStates(nodeFront, playState) {
+ const animations = await nodeFront.targetFront.getFront("animations");
+ const [player] = await animations.getAnimationPlayersForNode(nodeFront);
+
+ if (playState === "paused") {
+ await animations.pauseSome([player]);
+ } else {
+ await animations.playSome([player]);
+ }
+
+ info("Getting the AnimationPlayerFront for the test node");
+ await player.ready;
+ const state = await player.getCurrentState();
+ is(
+ state.playState,
+ playState,
+ "The playState of the test node is " + playState
+ );
+}
diff --git a/devtools/server/tests/browser/browser_animation_playPauseSeveral.js b/devtools/server/tests/browser/browser_animation_playPauseSeveral.js
new file mode 100644
index 0000000000..d478a801d0
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_playPauseSeveral.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that the AnimationsActor can pause/play a given list of animations at once.
+
+// List of selectors that match "all" animated nodes in the test page.
+// This list misses a bunch of animated nodes on purpose. Only the ones that
+// have infinite animations are listed. This is done to avoid intermittents
+// caused when finite animations are already done playing by the time the test
+// runs.
+const ALL_ANIMATED_NODES = [
+ ".simple-animation",
+ ".multiple-animations",
+ ".delayed-animation",
+];
+
+add_task(async function () {
+ const { target, walker, animations } = await initAnimationsFrontForUrl(
+ MAIN_DOMAIN + "animation.html"
+ );
+
+ info("Pause all animations in the test document");
+ await toggleAndCheckStates(walker, animations, ALL_ANIMATED_NODES, "paused");
+
+ info("Play all animations in the test document");
+ await toggleAndCheckStates(walker, animations, ALL_ANIMATED_NODES, "running");
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+async function toggleAndCheckStates(walker, animations, selectors, playState) {
+ info(
+ "Checking the playState of all the nodes that have infinite running " +
+ "animations"
+ );
+
+ for (const selector of selectors) {
+ const players = await getPlayersFor(walker, animations, selector);
+
+ if (playState === "paused") {
+ await animations.pauseSome(players);
+ } else {
+ await animations.playSome(players);
+ }
+
+ info("Getting the AnimationPlayerFront for node " + selector);
+ const player = players[0];
+ await checkPlayState(player, selector, playState);
+ }
+}
+
+async function getPlayersFor(walker, animations, selector) {
+ const node = await walker.querySelector(walker.rootNode, selector);
+ return animations.getAnimationPlayersForNode(node);
+}
+
+async function checkPlayState(player, selector, expectedState) {
+ const state = await player.getCurrentState();
+ is(
+ state.playState,
+ expectedState,
+ "The playState of node " + selector + " is " + expectedState
+ );
+}
diff --git a/devtools/server/tests/browser/browser_animation_playerState.js b/devtools/server/tests/browser/browser_animation_playerState.js
new file mode 100644
index 0000000000..e010b576b5
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_playerState.js
@@ -0,0 +1,159 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check the animation player's initial state
+
+add_task(async function () {
+ const { target, walker, animations } = await initAnimationsFrontForUrl(
+ MAIN_DOMAIN + "animation.html"
+ );
+
+ await playerHasAnInitialState(walker, animations);
+ await playerStateIsCorrect(walker, animations);
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+async function playerHasAnInitialState(walker, animations) {
+ const node = await walker.querySelector(walker.rootNode, ".simple-animation");
+ const [player] = await animations.getAnimationPlayersForNode(node);
+
+ ok(player.initialState, "The player front has an initial state");
+ ok("startTime" in player.initialState, "Player's state has startTime");
+ ok("currentTime" in player.initialState, "Player's state has currentTime");
+ ok("playState" in player.initialState, "Player's state has playState");
+ ok("playbackRate" in player.initialState, "Player's state has playbackRate");
+ ok("name" in player.initialState, "Player's state has name");
+ ok("duration" in player.initialState, "Player's state has duration");
+ ok("delay" in player.initialState, "Player's state has delay");
+ ok(
+ "iterationCount" in player.initialState,
+ "Player's state has iterationCount"
+ );
+ ok("fill" in player.initialState, "Player's state has fill");
+ ok("easing" in player.initialState, "Player's state has easing");
+ ok("direction" in player.initialState, "Player's state has direction");
+ ok(
+ "isRunningOnCompositor" in player.initialState,
+ "Player's state has isRunningOnCompositor"
+ );
+ ok("type" in player.initialState, "Player's state has type");
+ ok(
+ "documentCurrentTime" in player.initialState,
+ "Player's state has documentCurrentTime"
+ );
+ ok("properties" in player.initialState, "Player's state has properties");
+}
+
+async function playerStateIsCorrect(walker, animations) {
+ info("Checking the state of the simple animation");
+
+ let player = await getAnimationPlayerForNode(
+ walker,
+ animations,
+ ".simple-animation",
+ 0
+ );
+ let state = await player.getCurrentState();
+ is(state.name, "move", "Name is correct");
+ is(state.duration, 200000, "Duration is correct");
+ // null = infinite count
+ is(state.iterationCount, null, "Iteration count is correct");
+ is(state.fill, "none", "Fill is correct");
+ is(state.easing, "linear", "Easing is correct");
+ is(state.direction, "normal", "Direction is correct");
+ is(state.playState, "running", "PlayState is correct");
+ is(state.playbackRate, 1, "PlaybackRate is correct");
+ is(state.type, "cssanimation", "Type is correct");
+
+ info("Checking the state of the transition");
+
+ player = await getAnimationPlayerForNode(
+ walker,
+ animations,
+ ".transition",
+ 0
+ );
+ state = await player.getCurrentState();
+ is(state.name, "width", "Transition name matches transition property");
+ is(state.duration, 500000, "Transition duration is correct");
+ // transitions run only once
+ is(state.iterationCount, 1, "Transition iteration count is correct");
+ is(state.fill, "backwards", "Transition fill is correct");
+ is(state.easing, "ease-out", "Transition easing is correct");
+ is(state.direction, "normal", "Transition direction is correct");
+ is(state.playState, "running", "Transition playState is correct");
+ is(state.playbackRate, 1, "Transition playbackRate is correct");
+ is(state.type, "csstransition", "Transition type is correct");
+ // check easing in properties
+ let properties = state.properties;
+ is(properties.length, 1, "Length of animated properties is correct");
+ let keyframes = properties[0].values;
+ is(keyframes.length, 2, "Transition length of keyframe is correct");
+ is(keyframes[0].easing, "linear", "Transition keyframes's easing is correct");
+
+ info("Checking the state of one of multiple animations on a node");
+
+ // Checking the 2nd player
+ player = await getAnimationPlayerForNode(
+ walker,
+ animations,
+ ".multiple-animations",
+ 1
+ );
+ state = await player.getCurrentState();
+ is(state.name, "glow", "The 2nd animation's name is correct");
+ is(state.duration, 100000, "The 2nd animation's duration is correct");
+ is(state.iterationCount, 5, "The 2nd animation's iteration count is correct");
+ is(state.fill, "both", "The 2nd animation's fill is correct");
+ is(state.easing, "linear", "The 2nd animation's easing is correct");
+ is(state.direction, "reverse", "The 2nd animation's direction is correct");
+ is(state.playState, "running", "The 2nd animation's playState is correct");
+ is(state.playbackRate, 1, "The 2nd animation's playbackRate is correct");
+ // chech easing in keyframe
+ properties = state.properties;
+ keyframes = properties[0].values;
+ is(keyframes.length, 2, "The 2nd animation's length of keyframe is correct");
+ is(
+ keyframes[0].easing,
+ "ease-out",
+ "The 2nd animation's easing of keyframes is correct"
+ );
+
+ info("Checking the state of an animation with delay");
+
+ player = await getAnimationPlayerForNode(
+ walker,
+ animations,
+ ".delayed-animation",
+ 0
+ );
+ state = await player.getCurrentState();
+ is(state.delay, 5000, "The animation delay is correct");
+
+ info("Checking the state of an transition with delay");
+
+ player = await getAnimationPlayerForNode(
+ walker,
+ animations,
+ ".delayed-transition",
+ 0
+ );
+ state = await player.getCurrentState();
+ is(state.delay, 3000, "The transition delay is correct");
+}
+
+async function getAnimationPlayerForNode(
+ walker,
+ animations,
+ nodeSelector,
+ index
+) {
+ const node = await walker.querySelector(walker.rootNode, nodeSelector);
+ const players = await animations.getAnimationPlayersForNode(node);
+ const player = players[index];
+ return player;
+}
diff --git a/devtools/server/tests/browser/browser_animation_reconstructState.js b/devtools/server/tests/browser/browser_animation_reconstructState.js
new file mode 100644
index 0000000000..d7174562d9
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_reconstructState.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that, even though the AnimationPlayerActor only sends the bits of its
+// state that change, the front reconstructs the whole state everytime.
+
+add_task(async function () {
+ const { target, walker, animations } = await initAnimationsFrontForUrl(
+ MAIN_DOMAIN + "animation.html"
+ );
+
+ await playerHasCompleteStateAtAllTimes(walker, animations);
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+async function playerHasCompleteStateAtAllTimes(walker, animations) {
+ const node = await walker.querySelector(walker.rootNode, ".simple-animation");
+ const [player] = await animations.getAnimationPlayersForNode(node);
+
+ // Get the list of state key names from the initialstate.
+ const keys = Object.keys(player.initialState);
+
+ // Get the state over and over again and check that the object returned
+ // contains all keys.
+ // Normally, only the currentTime will have changed in between 2 calls.
+ for (let i = 0; i < 10; i++) {
+ await player.refreshState();
+ keys.forEach(key => {
+ Assert.notStrictEqual(
+ typeof player.state[key],
+ "undefined",
+ "The state retrieved has key " + key
+ );
+ });
+ }
+}
diff --git a/devtools/server/tests/browser/browser_animation_refreshTransitions.js b/devtools/server/tests/browser/browser_animation_refreshTransitions.js
new file mode 100644
index 0000000000..a48ea90e3d
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_refreshTransitions.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// When a transition finishes, no "removed" event is sent because it may still
+// be used, but when it restarts again (transitions back), then a new
+// AnimationPlayerFront should be sent, and the old one should be removed.
+
+add_task(async function () {
+ const { target, walker, animations } = await initAnimationsFrontForUrl(
+ MAIN_DOMAIN + "animation.html"
+ );
+
+ info("Retrieve the test node");
+ const node = await walker.querySelector(walker.rootNode, ".all-transitions");
+
+ info("Retrieve the animation players for the node");
+ const players = await animations.getAnimationPlayersForNode(node);
+ is(players.length, 0, "The node has no animation players yet");
+
+ info("Play a transition by adding the expand class, wait for mutations");
+ let onMutations = expectMutationEvents(animations, 2);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ const el = content.document.querySelector(".all-transitions");
+ el.classList.add("expand");
+ });
+ let reportedMutations = await onMutations;
+
+ is(reportedMutations.length, 2, "2 mutation events were received");
+ is(reportedMutations[0].type, "added", "The first event was 'added'");
+ is(reportedMutations[1].type, "added", "The second event was 'added'");
+
+ info("Wait for the transitions to be finished");
+ await waitForEnd(reportedMutations[0].player);
+ await waitForEnd(reportedMutations[1].player);
+
+ info("Play the transition back by removing the class, wait for mutations");
+ onMutations = expectMutationEvents(animations, 4);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ const el = content.document.querySelector(".all-transitions");
+ el.classList.remove("expand");
+ });
+ reportedMutations = await onMutations;
+
+ is(reportedMutations.length, 4, "4 new mutation events were received");
+ is(
+ reportedMutations.filter(m => m.type === "removed").length,
+ 2,
+ "2 'removed' events were sent (for the old transitions)"
+ );
+ is(
+ reportedMutations.filter(m => m.type === "added").length,
+ 2,
+ "2 'added' events were sent (for the new transitions)"
+ );
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function expectMutationEvents(animationsFront, nbOfEvents) {
+ return new Promise(resolve => {
+ let reportedMutations = [];
+ function onMutations(mutations) {
+ reportedMutations = [...reportedMutations, ...mutations];
+ info(
+ "Received " +
+ reportedMutations.length +
+ " mutation events, " +
+ "expecting " +
+ nbOfEvents
+ );
+ if (reportedMutations.length === nbOfEvents) {
+ animationsFront.off("mutations", onMutations);
+ resolve(reportedMutations);
+ }
+ }
+
+ info("Start listening for mutation events from the AnimationsFront");
+ animationsFront.on("mutations", onMutations);
+ });
+}
+
+async function waitForEnd(animationFront) {
+ let playState;
+ while (playState !== "finished") {
+ const state = await animationFront.getCurrentState();
+ playState = state.playState;
+ info(
+ "Wait for transition " +
+ animationFront.state.name +
+ " to finish, playState=" +
+ playState
+ );
+ }
+}
diff --git a/devtools/server/tests/browser/browser_animation_setCurrentTime.js b/devtools/server/tests/browser/browser_animation_setCurrentTime.js
new file mode 100644
index 0000000000..8f7228cdd8
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_setCurrentTime.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that the AnimationsActor allows changing many players' currentTimes at once.
+
+add_task(async function () {
+ const { target, walker, animations } = await initAnimationsFrontForUrl(
+ MAIN_DOMAIN + "animation.html"
+ );
+
+ await testSetCurrentTimes(walker, animations);
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+async function testSetCurrentTimes(walker, animations) {
+ ok(animations.setCurrentTimes, "The AnimationsActor has the right method");
+
+ info("Retrieve multiple animated node and its animation players");
+
+ const nodeMulti = await walker.querySelector(
+ walker.rootNode,
+ ".multiple-animations"
+ );
+ const players = await animations.getAnimationPlayersForNode(nodeMulti);
+
+ Assert.greater(players.length, 1, "Node has more than 1 animation player");
+
+ info("Try to set multiple current times at once");
+ // Assume that all animations were created at same time.
+ const createdTime = players[1].state.createdTime;
+ await animations.setCurrentTimes(players, createdTime + 500, true);
+
+ info("Get the states of players and verify their correctness");
+ for (let i = 0; i < players.length; i++) {
+ const state = await players[i].getCurrentState();
+ is(state.playState, "paused", `Player ${i + 1} is paused`);
+ is(
+ parseInt(state.currentTime.toPrecision(4), 10),
+ 500,
+ `Player ${i + 1} has the right currentTime`
+ );
+ }
+}
diff --git a/devtools/server/tests/browser/browser_animation_setPlaybackRate.js b/devtools/server/tests/browser/browser_animation_setPlaybackRate.js
new file mode 100644
index 0000000000..b14751b114
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_setPlaybackRate.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that a player's playbackRate can be changed, and that multiple players
+// can have their rates changed at the same time.
+
+add_task(async function () {
+ const { target, walker, animations } = await initAnimationsFrontForUrl(
+ MAIN_DOMAIN + "animation.html"
+ );
+
+ info("Retrieve an animated node");
+ let node = await walker.querySelector(walker.rootNode, ".simple-animation");
+
+ info("Retrieve the animation player for the node");
+ const [player] = await animations.getAnimationPlayersForNode(node);
+
+ info("Change the rate to 10");
+ await animations.setPlaybackRates([player], 10);
+
+ info("Query the state again");
+ let state = await player.getCurrentState();
+ is(state.playbackRate, 10, "The playbackRate was updated");
+
+ info("Change the rate back to 1");
+ await animations.setPlaybackRates([player], 1);
+
+ info("Query the state again");
+ state = await player.getCurrentState();
+ is(state.playbackRate, 1, "The playbackRate was changed back");
+
+ info("Retrieve several animation players and set their rates");
+ node = await walker.querySelector(walker.rootNode, "body");
+ const players = await animations.getAnimationPlayersForNode(node);
+
+ info("Change all animations in <body> to .5 rate");
+ await animations.setPlaybackRates(players, 0.5);
+
+ info("Query their states and check they are correct");
+ for (const animPlayer of players) {
+ const animPlayerState = await animPlayer.getCurrentState();
+ is(animPlayerState.playbackRate, 0.5, "The playbackRate was updated");
+ }
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_animation_simple.js b/devtools/server/tests/browser/browser_animation_simple.js
new file mode 100644
index 0000000000..0dd8adfde9
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_simple.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Simple checks for the AnimationsActor
+
+add_task(async function () {
+ const { target, walker, animations } = await initAnimationsFrontForUrl(
+ "data:text/html;charset=utf-8,<title>test</title><div></div>"
+ );
+
+ ok(animations, "The AnimationsFront was created");
+ ok(
+ animations.getAnimationPlayersForNode,
+ "The getAnimationPlayersForNode method exists"
+ );
+ ok(animations.pauseSome, "The pauseSome method exists");
+ ok(animations.playSome, "The playSome method exists");
+ ok(animations.setCurrentTimes, "The setCurrentTimes method exists");
+ ok(animations.setPlaybackRates, "The setPlaybackRates method exists");
+ ok(animations.setWalkerActor, "The setWalkerActor method exists");
+
+ let didThrow = false;
+ try {
+ await animations.getAnimationPlayersForNode(null);
+ } catch (e) {
+ didThrow = true;
+ }
+ ok(didThrow, "An exception was thrown for a missing NodeActor");
+
+ const invalidNode = await walker.querySelector(walker.rootNode, "title");
+ const players = await animations.getAnimationPlayersForNode(invalidNode);
+ ok(Array.isArray(players), "An array of players was returned");
+ is(players.length, 0, "0 players have been returned for the invalid node");
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_animation_updatedState.js b/devtools/server/tests/browser/browser_animation_updatedState.js
new file mode 100644
index 0000000000..4b1420de52
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_updatedState.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+"use strict";
+
+// Check the animation player's updated state
+
+add_task(async function () {
+ const { target, walker, animations } = await initAnimationsFrontForUrl(
+ MAIN_DOMAIN + "animation.html"
+ );
+
+ await playStateIsUpdatedDynamically(walker, animations);
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+async function playStateIsUpdatedDynamically(walker, animations) {
+ info("Getting the test node (which runs a very long animation)");
+ // The animation lasts for 100s, to avoid intermittents.
+ const node = await walker.querySelector(walker.rootNode, ".long-animation");
+
+ info("Getting the animation player front for this node");
+ const [player] = await animations.getAnimationPlayersForNode(node);
+
+ let state = await player.getCurrentState();
+ is(
+ state.playState,
+ "running",
+ "The playState is running while the animation is running"
+ );
+
+ info(
+ "Change the animation's currentTime to be near the end and wait for " +
+ "it to finish"
+ );
+ const onFinished = waitForAnimationPlayState(player, "finished");
+ // Set the currentTime to 98s, knowing that the animation lasts for 100s.
+ await animations.setCurrentTimes([player], 98 * 1000, false);
+ state = await onFinished;
+ is(
+ state.playState,
+ "finished",
+ "The animation has ended and the state has been updated"
+ );
+ Assert.greater(
+ state.currentTime,
+ player.initialState.currentTime,
+ "The currentTime has been updated"
+ );
+}
+
+async function waitForAnimationPlayState(player, playState) {
+ let state = {};
+ while (state.playState !== playState) {
+ state = await player.getCurrentState();
+ await wait(500);
+ }
+ return state;
+}
+
+function wait(ms) {
+ return new Promise(r => setTimeout(r, ms));
+}
diff --git a/devtools/server/tests/browser/browser_application_manifest.js b/devtools/server/tests/browser/browser_application_manifest.js
new file mode 100644
index 0000000000..c92a3c0a2f
--- /dev/null
+++ b/devtools/server/tests/browser/browser_application_manifest.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Enable web manifest processing.
+Services.prefs.setBoolPref("dom.manifest.enabled", true);
+
+add_task(async function () {
+ info("Testing fetching a valid manifest");
+ const response = await fetchManifest("application-manifest-basic.html");
+
+ ok(
+ response.manifest && response.manifest.name == "FooApp",
+ "Returns an object populated with the manifest data"
+ );
+});
+
+add_task(async function () {
+ info("Testing fetching an existing manifest with invalid values");
+ const response = await fetchManifest("application-manifest-warnings.html");
+
+ ok(
+ response.manifest && response.manifest.moz_validation,
+ "Returns an object populated with the manifest data"
+ );
+
+ const warnings = response.manifest.moz_validation;
+ ok(
+ warnings.length === 1 &&
+ warnings[0].warn &&
+ warnings[0].warn.includes("name member to be a string"),
+ "The returned object contains the expected warning info"
+ );
+});
+
+add_task(async function () {
+ info("Testing fetching a manifest in a page that does not have one");
+ const response = await fetchManifest("application-manifest-no-manifest.html");
+
+ is(response.manifest, null, "Returns an object with a `null` manifest");
+ ok(!response.errorMessage, "Does not return an error message");
+});
+
+add_task(async function () {
+ info("Testing an error happening fetching a manifest");
+ // the page that we are testing contains an invalid URL for the manifest
+ const response = await fetchManifest(
+ "application-manifest-404-manifest.html"
+ );
+
+ is(response.manifest, null, "Returns an object with a `null` manifest");
+ ok(
+ response.errorMessage &&
+ response.errorMessage.toLowerCase().includes("404 - not found"),
+ "Returns the expected error message"
+ );
+});
+
+add_task(async function () {
+ info("Testing a validation error when fetching a manifest with invalid JSON");
+ const response = await fetchManifest(
+ "application-manifest-invalid-json.html"
+ );
+ ok(
+ response.manifest && response.manifest.moz_validation,
+ "Returns an object with validation data"
+ );
+ const validation = response.manifest.moz_validation;
+ ok(
+ validation.find(x => x.error && x.type === "json"),
+ "Has the expected error in the validation field"
+ );
+});
+
+async function fetchManifest(filename) {
+ const url = MAIN_DOMAIN + filename;
+ const target = await addTabTarget(url);
+
+ info("Initializing manifest front for tab");
+ const manifestFront = await target.getFront("manifest");
+
+ info("Fetching manifest");
+ const response = await manifestFront.fetchCanonicalManifest();
+
+ return response;
+}
diff --git a/devtools/server/tests/browser/browser_canvasframe_helper_01.js b/devtools/server/tests/browser/browser_canvasframe_helper_01.js
new file mode 100644
index 0000000000..14c947db7e
--- /dev/null
+++ b/devtools/server/tests/browser/browser_canvasframe_helper_01.js
@@ -0,0 +1,170 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Simple CanvasFrameAnonymousContentHelper tests.
+
+const TEST_URL =
+ "data:text/html;charset=utf-8,CanvasFrameAnonymousContentHelper test";
+
+add_task(async function () {
+ const tab = await addTab(TEST_URL);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ HighlighterEnvironment,
+ } = require("resource://devtools/server/actors/highlighters.js");
+ const {
+ CanvasFrameAnonymousContentHelper,
+ } = require("resource://devtools/server/actors/highlighters/utils/markup.js");
+ const doc = content.document;
+
+ const nodeBuilder = () => {
+ const root = doc.createElement("div");
+ const child = doc.createElement("div");
+ child.style = "width:200px;height:200px;background:red;";
+ child.id = "child-element";
+ child.className = "child-element";
+ child.textContent = "test element";
+ root.appendChild(child);
+ return root;
+ };
+
+ info("Building the helper");
+ const env = new HighlighterEnvironment();
+ env.initFromWindow(doc.defaultView);
+ const helper = new CanvasFrameAnonymousContentHelper(env, nodeBuilder);
+ await helper.initialize();
+
+ ok(
+ content.AnonymousContent.isInstance(helper.content),
+ "The helper owns the AnonymousContent object"
+ );
+ ok(
+ helper.getTextContentForElement,
+ "The helper has the getTextContentForElement method"
+ );
+ ok(
+ helper.setTextContentForElement,
+ "The helper has the setTextContentForElement method"
+ );
+ ok(
+ helper.setAttributeForElement,
+ "The helper has the setAttributeForElement method"
+ );
+ ok(
+ helper.getAttributeForElement,
+ "The helper has the getAttributeForElement method"
+ );
+ ok(
+ helper.removeAttributeForElement,
+ "The helper has the removeAttributeForElement method"
+ );
+ ok(
+ helper.addEventListenerForElement,
+ "The helper has the addEventListenerForElement method"
+ );
+ ok(
+ helper.removeEventListenerForElement,
+ "The helper has the removeEventListenerForElement method"
+ );
+ ok(helper.getElement, "The helper has the getElement method");
+ ok(helper.scaleRootElement, "The helper has the scaleRootElement method");
+
+ is(
+ helper.getTextContentForElement("child-element"),
+ "test element",
+ "The text content was retrieve correctly"
+ );
+ is(
+ helper.getAttributeForElement("child-element", "id"),
+ "child-element",
+ "The ID attribute was retrieve correctly"
+ );
+ is(
+ helper.getAttributeForElement("child-element", "class"),
+ "child-element",
+ "The class attribute was retrieve correctly"
+ );
+
+ const el = helper.getElement("child-element");
+ ok(el, "The DOMNode-like element was created");
+
+ is(
+ el.getTextContent(),
+ "test element",
+ "The text content was retrieve correctly"
+ );
+ is(
+ el.getAttribute("id"),
+ "child-element",
+ "The ID attribute was retrieve correctly"
+ );
+ is(
+ el.getAttribute("class"),
+ "child-element",
+ "The class attribute was retrieve correctly"
+ );
+
+ info("Test the toggle API");
+ el.classList.toggle("test"); // This will set the class
+ is(
+ el.getAttribute("class"),
+ "child-element test",
+ "After toggling the class 'test', the class attribute contained the 'test' class"
+ );
+ el.classList.toggle("test"); // This will remove the class
+ is(
+ el.getAttribute("class"),
+ "child-element",
+ "After toggling the class 'test' again, the class attribute removed the 'test' class"
+ );
+ el.classList.toggle("test", true); // This will set the class
+ is(
+ el.getAttribute("class"),
+ "child-element test",
+ "After toggling the class 'test' again and keeping force=true, the class attribute added the 'test' class"
+ );
+ el.classList.toggle("test", true); // This will keep the class set
+ is(
+ el.getAttribute("class"),
+ "child-element test",
+ "After toggling the class 'test' again and keeping force=true,the class attribute contained the 'test' class"
+ );
+ el.classList.toggle("test", false); // This will remove the class
+ is(
+ el.getAttribute("class"),
+ "child-element",
+ "After toggling the class 'test' again and keeping force=false, the class attribute removed the 'test' class"
+ );
+ el.classList.toggle("test", false); // This will keep the class removed
+ is(
+ el.getAttribute("class"),
+ "child-element",
+ "After toggling the class 'test' again and keeping force=false, the class attribute removed the 'test' class"
+ );
+
+ info("Destroying the helper");
+ helper.destroy();
+ env.destroy();
+
+ ok(
+ !helper.getTextContentForElement("child-element"),
+ "No text content was retrieved after the helper was destroyed"
+ );
+ ok(
+ !helper.getAttributeForElement("child-element", "id"),
+ "No ID attribute was retrieved after the helper was destroyed"
+ );
+ ok(
+ !helper.getAttributeForElement("child-element", "class"),
+ "No class attribute was retrieved after the helper was destroyed"
+ );
+ });
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_canvasframe_helper_02.js b/devtools/server/tests/browser/browser_canvasframe_helper_02.js
new file mode 100644
index 0000000000..bd54a03933
--- /dev/null
+++ b/devtools/server/tests/browser/browser_canvasframe_helper_02.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the CanvasFrameAnonymousContentHelper does not insert content in
+// XUL windows.
+
+add_task(async function () {
+ const tab = await addTab(
+ "chrome://mochitests/content/browser/devtools/server/tests/browser/test-window.xhtml"
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ HighlighterEnvironment,
+ } = require("resource://devtools/server/actors/highlighters.js");
+ const {
+ CanvasFrameAnonymousContentHelper,
+ } = require("resource://devtools/server/actors/highlighters/utils/markup.js");
+ const doc = content.document;
+
+ const nodeBuilder = () => {
+ const root = doc.createElement("div");
+ const child = doc.createElement("div");
+ child.style = "width:200px;height:200px;background:red;";
+ child.id = "child-element";
+ child.className = "child-element";
+ child.textContent = "test element";
+ root.appendChild(child);
+ return root;
+ };
+
+ info("Building the helper");
+ const env = new HighlighterEnvironment();
+ env.initFromWindow(doc.defaultView);
+ const helper = new CanvasFrameAnonymousContentHelper(env, nodeBuilder);
+
+ ok(!helper.content, "The AnonymousContent was not inserted in the window");
+ ok(
+ !helper.getTextContentForElement("child-element"),
+ "No text content is returned"
+ );
+
+ env.destroy();
+ helper.destroy();
+ });
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_canvasframe_helper_03.js b/devtools/server/tests/browser/browser_canvasframe_helper_03.js
new file mode 100644
index 0000000000..52aa4b5a6f
--- /dev/null
+++ b/devtools/server/tests/browser/browser_canvasframe_helper_03.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the CanvasFrameAnonymousContentHelper event handling mechanism.
+
+const TEST_URL =
+ "data:text/html;charset=utf-8,CanvasFrameAnonymousContentHelper test";
+
+add_task(async function () {
+ const tab = await addTab(TEST_URL);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ HighlighterEnvironment,
+ } = require("resource://devtools/server/actors/highlighters.js");
+ const {
+ CanvasFrameAnonymousContentHelper,
+ } = require("resource://devtools/server/actors/highlighters/utils/markup.js");
+ const doc = content.document;
+
+ const nodeBuilder = () => {
+ const root = doc.createElement("div");
+ const child = doc.createElement("div");
+ child.style =
+ "pointer-events:auto;width:200px;height:200px;background:red;";
+ child.id = "child-element";
+ child.className = "child-element";
+ root.appendChild(child);
+ return root;
+ };
+
+ info("Building the helper");
+ const env = new HighlighterEnvironment();
+ env.initFromWindow(doc.defaultView);
+ const helper = new CanvasFrameAnonymousContentHelper(env, nodeBuilder);
+ await helper.initialize();
+
+ const el = helper.getElement("child-element");
+
+ info("Adding an event listener on the inserted element");
+ let mouseDownHandled = 0;
+ function onMouseDown(e, id) {
+ is(
+ id,
+ "child-element",
+ "The mousedown event was triggered on the element"
+ );
+ ok(!e.originalTarget, "The originalTarget property isn't available");
+ mouseDownHandled++;
+ }
+ el.addEventListener("mousedown", onMouseDown);
+
+ function once(target, event) {
+ return new Promise(done => {
+ target.addEventListener(event, done, { once: true });
+ });
+ }
+
+ info("Synthesizing an event on the inserted element");
+ let onDocMouseDown = once(doc, "mousedown");
+ synthesizeMouseDown(100, 100, doc.defaultView);
+ await onDocMouseDown;
+
+ is(
+ mouseDownHandled,
+ 1,
+ "The mousedown event was handled once on the element"
+ );
+
+ info("Synthesizing an event somewhere else");
+ onDocMouseDown = once(doc, "mousedown");
+ synthesizeMouseDown(400, 400, doc.defaultView);
+ await onDocMouseDown;
+
+ is(
+ mouseDownHandled,
+ 1,
+ "The mousedown event was not handled on the element"
+ );
+
+ info("Removing the event listener");
+ el.removeEventListener("mousedown", onMouseDown);
+
+ info("Synthesizing another event after the listener has been removed");
+ // Using a document event listener to know when the event has been synthesized.
+ onDocMouseDown = once(doc, "mousedown");
+ synthesizeMouseDown(100, 100, doc.defaultView);
+ await onDocMouseDown;
+
+ is(
+ mouseDownHandled,
+ 1,
+ "The mousedown event hasn't been handled after the listener was removed"
+ );
+
+ info("Adding again the event listener");
+ el.addEventListener("mousedown", onMouseDown);
+
+ info("Destroying the helper");
+ env.destroy();
+ helper.destroy();
+
+ info("Synthesizing another event after the helper has been destroyed");
+ // Using a document event listener to know when the event has been synthesized.
+ onDocMouseDown = once(doc, "mousedown");
+ synthesizeMouseDown(100, 100, doc.defaultView);
+ await onDocMouseDown;
+
+ is(
+ mouseDownHandled,
+ 1,
+ "The mousedown event hasn't been handled after the helper was destroyed"
+ );
+
+ function synthesizeMouseDown(x, y, win) {
+ // We need to make sure the inserted anonymous content can be targeted by the
+ // event right after having been inserted, and so we need to force a sync
+ // reflow.
+ win.document.documentElement.offsetWidth;
+ EventUtils.synthesizeMouseAtPoint(x, y, { type: "mousedown" }, win);
+ }
+ });
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_canvasframe_helper_04.js b/devtools/server/tests/browser/browser_canvasframe_helper_04.js
new file mode 100644
index 0000000000..85368ff2b5
--- /dev/null
+++ b/devtools/server/tests/browser/browser_canvasframe_helper_04.js
@@ -0,0 +1,142 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the CanvasFrameAnonymousContentHelper re-inserts the content when the
+// page reloads.
+
+const TEST_URL_1 =
+ "data:text/html;charset=utf-8,CanvasFrameAnonymousContentHelper test 1";
+const TEST_URL_2 =
+ "data:text/html;charset=utf-8,CanvasFrameAnonymousContentHelper test 2";
+
+add_task(async function () {
+ const tab = await addTab(TEST_URL_1);
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [TEST_URL_2],
+ async function (url2) {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ HighlighterEnvironment,
+ } = require("resource://devtools/server/actors/highlighters.js");
+ const {
+ CanvasFrameAnonymousContentHelper,
+ } = require("resource://devtools/server/actors/highlighters/utils/markup.js");
+ let doc = content.document;
+
+ const nodeBuilder = () => {
+ const root = doc.createElement("div");
+ const child = doc.createElement("div");
+ child.style =
+ "pointer-events:auto;width:200px;height:200px;background:red;";
+ child.id = "child-element";
+ child.className = "child-element";
+ child.textContent = "test content";
+ root.appendChild(child);
+ return root;
+ };
+
+ info("Building the helper");
+ const env = new HighlighterEnvironment();
+ env.initFromWindow(doc.defaultView);
+ const helper = new CanvasFrameAnonymousContentHelper(env, nodeBuilder);
+ await helper.initialize();
+
+ info("Get an element from the helper");
+ const el = helper.getElement("child-element");
+
+ info("Try to access the element");
+ is(
+ el.getAttribute("class"),
+ "child-element",
+ "The attribute is correct before navigation"
+ );
+ is(
+ el.getTextContent(),
+ "test content",
+ "The text content is correct before navigation"
+ );
+
+ info("Add an event listener on the element");
+ let mouseDownHandled = 0;
+ const onMouseDown = (e, id) => {
+ is(
+ id,
+ "child-element",
+ "The mousedown event was triggered on the element"
+ );
+ mouseDownHandled++;
+ };
+ el.addEventListener("mousedown", onMouseDown);
+
+ const once = function once(target, event) {
+ return new Promise(done => {
+ target.addEventListener(event, done, { once: true });
+ });
+ };
+
+ const synthesizeMouseDown = function synthesizeMouseDown(x, y, win) {
+ // We need to make sure the inserted anonymous content can be targeted by the
+ // event right after having been inserted, and so we need to force a sync
+ // reflow.
+ win.document.documentElement.offsetWidth;
+ EventUtils.synthesizeMouseAtPoint(x, y, { type: "mousedown" }, win);
+ };
+
+ info("Synthesizing an event on the element");
+ let onDocMouseDown = once(doc, "mousedown");
+ synthesizeMouseDown(100, 100, doc.defaultView);
+ await onDocMouseDown;
+ is(
+ mouseDownHandled,
+ 1,
+ "The mousedown event was handled once before navigation"
+ );
+
+ info("Navigating to a new page");
+ const loaded = once(this, "load");
+ content.location = url2;
+ await loaded;
+
+ // Wait for the next event tick to make sure the remaining part of the
+ // test is not executed in the microtask checkpoint for load event
+ // itself. Otherwise the synthesizeMouseDown doesn't work.
+ await new Promise(r => content.setTimeout(r, 0));
+
+ // Update to the new document we just loaded
+ doc = content.document;
+
+ info("Try to access the element again");
+ is(
+ el.getAttribute("class"),
+ "child-element",
+ "The attribute is correct after navigation"
+ );
+ is(
+ el.getTextContent(),
+ "test content",
+ "The text content is correct after navigation"
+ );
+
+ info("Synthesizing an event on the element again");
+ onDocMouseDown = once(doc, "mousedown");
+ synthesizeMouseDown(100, 100, doc.defaultView);
+ await onDocMouseDown;
+ is(
+ mouseDownHandled,
+ 1,
+ "The mousedown event was not handled after navigation"
+ );
+
+ info("Destroying the helper");
+ env.destroy();
+ helper.destroy();
+ }
+ );
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_canvasframe_helper_05.js b/devtools/server/tests/browser/browser_canvasframe_helper_05.js
new file mode 100644
index 0000000000..b542b14221
--- /dev/null
+++ b/devtools/server/tests/browser/browser_canvasframe_helper_05.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test some edge cases of the CanvasFrameAnonymousContentHelper event handling
+// mechanism.
+
+const TEST_URL =
+ "data:text/html;charset=utf-8,CanvasFrameAnonymousContentHelper test";
+
+add_task(async function () {
+ const tab = await addTab(TEST_URL);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ HighlighterEnvironment,
+ } = require("resource://devtools/server/actors/highlighters.js");
+ const {
+ CanvasFrameAnonymousContentHelper,
+ } = require("resource://devtools/server/actors/highlighters/utils/markup.js");
+ const doc = content.document;
+
+ const nodeBuilder = () => {
+ const root = doc.createElement("div");
+
+ const parent = doc.createElement("div");
+ parent.style =
+ "pointer-events:auto;width:300px;height:300px;background:yellow;";
+ parent.id = "parent-element";
+ root.appendChild(parent);
+
+ const child = doc.createElement("div");
+ child.style =
+ "pointer-events:auto;width:200px;height:200px;background:red;";
+ child.id = "child-element";
+ parent.appendChild(child);
+
+ return root;
+ };
+
+ info("Building the helper");
+ const env = new HighlighterEnvironment();
+ env.initFromWindow(doc.defaultView);
+ const helper = new CanvasFrameAnonymousContentHelper(env, nodeBuilder);
+ await helper.initialize();
+
+ info("Getting the parent and child elements");
+ const parentEl = helper.getElement("parent-element");
+ const childEl = helper.getElement("child-element");
+
+ info("Adding an event listener on both elements");
+ let mouseDownHandled = [];
+ function onMouseDown(e, id) {
+ mouseDownHandled.push(id);
+ }
+ parentEl.addEventListener("mousedown", onMouseDown);
+ childEl.addEventListener("mousedown", onMouseDown);
+
+ function once(target, event) {
+ return new Promise(done => {
+ target.addEventListener(event, done, { once: true });
+ });
+ }
+
+ info("Synthesizing an event on the child element");
+ let onDocMouseDown = once(doc, "mousedown");
+ synthesizeMouseDown(100, 100, doc.defaultView);
+ await onDocMouseDown;
+
+ is(mouseDownHandled.length, 2, "The mousedown event was handled twice");
+ is(
+ mouseDownHandled[0],
+ "child-element",
+ "The mousedown event was handled on the child element"
+ );
+ is(
+ mouseDownHandled[1],
+ "parent-element",
+ "The mousedown event was handled on the parent element"
+ );
+
+ info("Synthesizing an event on the parent, outside of the child element");
+ mouseDownHandled = [];
+ onDocMouseDown = once(doc, "mousedown");
+ synthesizeMouseDown(250, 250, doc.defaultView);
+ await onDocMouseDown;
+
+ is(mouseDownHandled.length, 1, "The mousedown event was handled only once");
+ is(
+ mouseDownHandled[0],
+ "parent-element",
+ "The mousedown event was handled on the parent element"
+ );
+
+ info("Removing the event listener");
+ parentEl.removeEventListener("mousedown", onMouseDown);
+ childEl.removeEventListener("mousedown", onMouseDown);
+
+ info("Adding an event listener on the parent element only");
+ mouseDownHandled = [];
+ parentEl.addEventListener("mousedown", onMouseDown);
+
+ info("Synthesizing an event on the child element");
+ onDocMouseDown = once(doc, "mousedown");
+ synthesizeMouseDown(100, 100, doc.defaultView);
+ await onDocMouseDown;
+
+ is(mouseDownHandled.length, 1, "The mousedown event was handled once");
+ is(
+ mouseDownHandled[0],
+ "parent-element",
+ "The mousedown event did bubble to the parent element"
+ );
+
+ info("Removing the parent listener");
+ parentEl.removeEventListener("mousedown", onMouseDown);
+
+ env.destroy();
+ helper.destroy();
+
+ function synthesizeMouseDown(x, y, win) {
+ // We need to make sure the inserted anonymous content can be targeted by the
+ // event right after having been inserted, and so we need to force a sync
+ // reflow.
+ win.document.documentElement.offsetWidth;
+ EventUtils.synthesizeMouseAtPoint(x, y, { type: "mousedown" }, win);
+ }
+ });
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_canvasframe_helper_06.js b/devtools/server/tests/browser/browser_canvasframe_helper_06.js
new file mode 100644
index 0000000000..e0222b33b1
--- /dev/null
+++ b/devtools/server/tests/browser/browser_canvasframe_helper_06.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test support for event propagation stop in the
+// CanvasFrameAnonymousContentHelper event handling mechanism.
+
+const TEST_URL =
+ "data:text/html;charset=utf-8,CanvasFrameAnonymousContentHelper test";
+
+add_task(async function () {
+ const tab = await addTab(TEST_URL);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ HighlighterEnvironment,
+ } = require("resource://devtools/server/actors/highlighters.js");
+ const {
+ CanvasFrameAnonymousContentHelper,
+ } = require("resource://devtools/server/actors/highlighters/utils/markup.js");
+ const doc = content.document;
+
+ const nodeBuilder = () => {
+ const root = doc.createElement("div");
+
+ const parent = doc.createElement("div");
+ parent.style =
+ "pointer-events:auto;width:300px;height:300px;background:yellow;";
+ parent.id = "parent-element";
+ root.appendChild(parent);
+
+ const child = doc.createElement("div");
+ child.style =
+ "pointer-events:auto;width:200px;height:200px;background:red;";
+ child.id = "child-element";
+ parent.appendChild(child);
+
+ return root;
+ };
+
+ info("Building the helper");
+ const env = new HighlighterEnvironment();
+ env.initFromWindow(doc.defaultView);
+ const helper = new CanvasFrameAnonymousContentHelper(env, nodeBuilder);
+ await helper.initialize();
+
+ info("Getting the parent and child elements");
+ const parentEl = helper.getElement("parent-element");
+ const childEl = helper.getElement("child-element");
+
+ info("Adding an event listener on both elements");
+ let mouseDownHandled = [];
+
+ function onParentMouseDown(e, id) {
+ mouseDownHandled.push(id);
+ }
+ parentEl.addEventListener("mousedown", onParentMouseDown);
+
+ function onChildMouseDown(e, id) {
+ mouseDownHandled.push(id);
+ e.stopPropagation();
+ }
+ childEl.addEventListener("mousedown", onChildMouseDown);
+
+ function once(target, event) {
+ return new Promise(done => {
+ target.addEventListener(event, done, { once: true });
+ });
+ }
+
+ info("Synthesizing an event on the child element");
+ let onDocMouseDown = once(doc, "mousedown");
+ synthesizeMouseDown(100, 100, doc.defaultView);
+ await onDocMouseDown;
+
+ is(mouseDownHandled.length, 1, "The mousedown event was handled only once");
+ is(
+ mouseDownHandled[0],
+ "child-element",
+ "The mousedown event was handled on the child element"
+ );
+
+ info("Synthesizing an event on the parent, outside of the child element");
+ mouseDownHandled = [];
+ onDocMouseDown = once(doc, "mousedown");
+ synthesizeMouseDown(250, 250, doc.defaultView);
+ await onDocMouseDown;
+
+ is(mouseDownHandled.length, 1, "The mousedown event was handled only once");
+ is(
+ mouseDownHandled[0],
+ "parent-element",
+ "The mousedown event was handled on the parent element"
+ );
+
+ info("Removing the event listener");
+ parentEl.removeEventListener("mousedown", onParentMouseDown);
+ childEl.removeEventListener("mousedown", onChildMouseDown);
+
+ env.destroy();
+ helper.destroy();
+
+ function synthesizeMouseDown(x, y, win) {
+ // We need to make sure the inserted anonymous content can be targeted by the
+ // event right after having been inserted, and so we need to force a sync
+ // reflow.
+ win.document.documentElement.offsetWidth;
+ EventUtils.synthesizeMouseAtPoint(x, y, { type: "mousedown" }, win);
+ }
+ });
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_compatibility_cssIssues.js b/devtools/server/tests/browser/browser_compatibility_cssIssues.js
new file mode 100644
index 0000000000..4cd244688c
--- /dev/null
+++ b/devtools/server/tests/browser/browser_compatibility_cssIssues.js
@@ -0,0 +1,137 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check the output of getNodeCssIssues
+
+const {
+ COMPATIBILITY_ISSUE_TYPE,
+} = require("resource://devtools/shared/constants.js");
+const URL = MAIN_DOMAIN + "doc_compatibility.html";
+
+const CHROME_81 = {
+ id: "chrome",
+ version: "81",
+};
+
+const CHROME_ANDROID = {
+ id: "chrome_android",
+ version: "81",
+};
+
+const EDGE_81 = {
+ id: "edge",
+ version: "81",
+};
+
+const FIREFOX_1 = {
+ id: "firefox",
+ version: "1",
+};
+
+const FIREFOX_60 = {
+ id: "firefox",
+ version: "60",
+};
+
+const FIREFOX_69 = {
+ id: "firefox",
+ version: "69",
+};
+
+const FIREFOX_MOBILE = {
+ id: "firefox_android",
+ version: "68",
+};
+
+const SAFARI_13 = {
+ id: "safari",
+ version: "13",
+};
+
+const SAFARI_MOBILE = {
+ id: "safari_ios",
+ version: "13.4",
+};
+
+const TARGET_BROWSERS = [
+ FIREFOX_1,
+ FIREFOX_60,
+ FIREFOX_69,
+ FIREFOX_MOBILE,
+ CHROME_81,
+ CHROME_ANDROID,
+ SAFARI_13,
+ SAFARI_MOBILE,
+ EDGE_81,
+];
+
+const ISSUE_USER_SELECT = {
+ type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY_ALIASES,
+ property: "user-select",
+ aliases: ["-moz-user-select"],
+ url: "https://developer.mozilla.org/docs/Web/CSS/user-select",
+ specUrl: "https://drafts.csswg.org/css-ui/#content-selection",
+ deprecated: false,
+ experimental: false,
+ prefixNeeded: true,
+ unsupportedBrowsers: [
+ CHROME_81,
+ CHROME_ANDROID,
+ SAFARI_13,
+ SAFARI_MOBILE,
+ EDGE_81,
+ ],
+};
+
+const ISSUE_CLIP = {
+ type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY,
+ property: "clip",
+ url: "https://developer.mozilla.org/docs/Web/CSS/clip",
+ specUrl: "https://drafts.fxtf.org/css-masking/#clip-property",
+ deprecated: true,
+ experimental: false,
+ unsupportedBrowsers: [],
+};
+
+async function testNodeCssIssues(selector, walker, compatibility, expected) {
+ const node = await walker.querySelector(walker.rootNode, selector);
+ const cssCompatibilityIssues = await compatibility.getNodeCssIssues(
+ node,
+ TARGET_BROWSERS
+ );
+ info("Ensure result is correct");
+ Assert.deepEqual(
+ cssCompatibilityIssues,
+ expected,
+ "Expected CSS browser compat data is correct."
+ );
+}
+
+add_task(async function () {
+ const { inspector, walker, target } = await initInspectorFront(URL);
+ const compatibility = await inspector.getCompatibilityFront();
+
+ info('Test CSS properties linked with the "div" tag');
+ await testNodeCssIssues("div", walker, compatibility, []);
+
+ info('Test CSS properties linked with class "class-user-select"');
+ await testNodeCssIssues(".class-user-select", walker, compatibility, [
+ ISSUE_USER_SELECT,
+ ]);
+
+ info("Test CSS properties linked with multiple classes and id");
+ await testNodeCssIssues(
+ "div#id-clip.class-clip.class-user-select",
+ walker,
+ compatibility,
+ [ISSUE_CLIP, ISSUE_USER_SELECT]
+ );
+
+ info("Repeated incompatible CSS rule should be only reported once");
+ await testNodeCssIssues(".duplicate", walker, compatibility, [ISSUE_CLIP]);
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_connectToFrame.js b/devtools/server/tests/browser/browser_connectToFrame.js
new file mode 100644
index 0000000000..568eb1acc1
--- /dev/null
+++ b/devtools/server/tests/browser/browser_connectToFrame.js
@@ -0,0 +1,142 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test `connectToFrame` method
+ */
+
+"use strict";
+
+const {
+ connectToFrame,
+} = require("resource://devtools/server/connectors/frame-connector.js");
+
+add_task(async function () {
+ // Create a minimal browser with a message manager
+ const browser = document.createXULElement("browser");
+ browser.setAttribute("type", "content");
+ document.body.appendChild(browser);
+
+ await TestUtils.waitForCondition(
+ () => browser.browsingContext.currentWindowGlobal,
+ "browser has no window global"
+ );
+
+ // Register a test actor in the child process so that we can know if and when
+ // this fake actor is destroyed.
+ await SpecialPowers.spawn(browser, [], () => {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ DevToolsServer,
+ } = require("resource://devtools/server/devtools-server.js");
+ const {
+ ActorRegistry,
+ } = require("resource://devtools/server/actors/utils/actor-registry.js");
+
+ DevToolsServer.init();
+
+ const { Actor } = require("resource://devtools/shared/protocol/Actor.js");
+ class ConnectToFrameTestActor extends Actor {
+ constructor(conn, tab) {
+ super(conn, { typeName: "connectToFrameTest", methods: [] });
+ dump("instantiate test actor\n");
+ this.requestTypes = {
+ hello: this.hello,
+ };
+ }
+ hello() {
+ return { msg: "world" };
+ }
+
+ destroy() {
+ SpecialPowers.notifyObserversInParentProcess(
+ null,
+ "devtools-test-actor-destroyed",
+ ""
+ );
+ }
+ }
+
+ ActorRegistry.addTargetScopedActor(
+ {
+ constructorName: "ConnectToFrameTestActor",
+ constructorFun: ConnectToFrameTestActor,
+ },
+ "connectToFrameTestActor"
+ );
+ });
+
+ // Instantiate a minimal server
+ DevToolsServer.init();
+ if (!DevToolsServer.createRootActor) {
+ DevToolsServer.registerAllActors();
+ }
+
+ async function initAndCloseFirstClient() {
+ // Fake a first connection to a browser
+ const transport = DevToolsServer.connectPipe();
+ const conn = transport._serverConnection;
+ const client = new DevToolsClient(transport);
+ const actor = await connectToFrame(conn, browser);
+ ok(actor.connectToFrameTestActor, "Got the test actor");
+
+ // Ensure sending at least one request to our actor,
+ // otherwise it won't be instantiated, nor be destroyed...
+ await client.request({
+ to: actor.connectToFrameTestActor,
+ type: "hello",
+ });
+
+ // Connect a second client in parallel to assert that it received a distinct set of
+ // target actors
+ await initAndCloseSecondClient(actor.connectToFrameTestActor);
+
+ ok(
+ DevToolsServer.initialized,
+ "DevToolsServer isn't destroyed until all clients are disconnected"
+ );
+
+ // Ensure that our test actor got cleaned up;
+ // its destroy method should be called
+ const onActorDestroyed = TestUtils.topicObserved(
+ "devtools-test-actor-destroyed"
+ );
+
+ // Then close the client. That should end up cleaning our test actor
+ await client.close();
+
+ await onActorDestroyed;
+
+ // This test loads a frame in the parent process, so that we end up sharing the same
+ // DevToolsServer instance
+ ok(
+ !DevToolsServer.initialized,
+ "DevToolsServer is destroyed when all clients are disconnected"
+ );
+ }
+
+ async function initAndCloseSecondClient(firstActor) {
+ // Then fake a second one, that should spawn a new set of target-scoped actors
+ const transport = DevToolsServer.connectPipe();
+ const conn = transport._serverConnection;
+ const client = new DevToolsClient(transport);
+ const actor = await connectToFrame(conn, browser);
+ ok(
+ actor.connectToFrameTestActor,
+ "Got a test actor for the second connection"
+ );
+ isnot(
+ actor.connectToFrameTestActor,
+ firstActor,
+ "We get different actor instances between two connections"
+ );
+ return client.close();
+ }
+
+ await initAndCloseFirstClient();
+
+ DevToolsServer.destroy();
+ browser.remove();
+});
diff --git a/devtools/server/tests/browser/browser_debugger_server.js b/devtools/server/tests/browser/browser_debugger_server.js
new file mode 100644
index 0000000000..8b36076b34
--- /dev/null
+++ b/devtools/server/tests/browser/browser_debugger_server.js
@@ -0,0 +1,198 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test basic features of DevToolsServer
+
+add_task(async function () {
+ // When running some other tests before, they may not destroy the main server.
+ // Do it manually before running our tests.
+ if (DevToolsServer.initialized) {
+ DevToolsServer.destroy();
+ }
+
+ await testDevToolsServerInitialized();
+ await testDevToolsServerKeepAlive();
+});
+
+async function testDevToolsServerInitialized() {
+ const tab = await addTab("data:text/html;charset=utf-8,foo");
+
+ ok(
+ !DevToolsServer.initialized,
+ "By default, the DevToolsServer isn't initialized in parent process"
+ );
+ await assertServerInitialized(
+ tab,
+ false,
+ "By default, the DevToolsServer isn't initialized not in content process"
+ );
+ await assertDevToolsOpened(
+ tab,
+ false,
+ "By default, the DevTools are reported as closed"
+ );
+
+ const commands = await CommandsFactory.forTab(tab);
+
+ ok(
+ DevToolsServer.initialized,
+ "Creating the commands will initialize the DevToolsServer in parent process"
+ );
+ await assertServerInitialized(
+ tab,
+ false,
+ "Creating the commands isn't enough to initialize the DevToolsServer in content process"
+ );
+ await assertDevToolsOpened(
+ tab,
+ false,
+ "DevTools are still reported as closed after having created the commands"
+ );
+
+ await commands.targetCommand.startListening();
+
+ await assertServerInitialized(
+ tab,
+ true,
+ "Initializing the TargetCommand will initialize the DevToolsServer in content process"
+ );
+ await assertDevToolsOpened(
+ tab,
+ true,
+ "Initializing the TargetCommand will start reporting the DevTools as opened"
+ );
+
+ await commands.destroy();
+
+ // Disconnecting the client will remove all connections from both server, in parent and content process.
+ ok(
+ !DevToolsServer.initialized,
+ "Destroying the commands destroys the DevToolsServer in the parent process"
+ );
+ await assertServerInitialized(
+ tab,
+ false,
+ "But destroying the commands ends up destroying the DevToolsServer in the content process"
+ );
+ await assertDevToolsOpened(
+ tab,
+ false,
+ "Destroying the commands will report DevTools as being closed"
+ );
+
+ gBrowser.removeCurrentTab();
+ DevToolsServer.destroy();
+}
+
+async function testDevToolsServerKeepAlive() {
+ const tab = await addTab("data:text/html;charset=utf-8,foo");
+
+ await assertServerInitialized(
+ tab,
+ false,
+ "Server not started in content process"
+ );
+ await assertDevToolsOpened(tab, false, "DevTools are reported as closed");
+
+ const commands = await CommandsFactory.forTab(tab);
+ await commands.targetCommand.startListening();
+
+ await assertServerInitialized(tab, true, "Server started in content process");
+ await assertDevToolsOpened(tab, true, "DevTools are reported as opened");
+
+ info("Set DevToolsServer.keepAlive to true in the content process");
+ DevToolsServer.keepAlive = true;
+ await setContentServerKeepAlive(tab, true);
+
+ info("Destroy the commands, the content server should be kept alive");
+ await commands.destroy();
+
+ await assertServerInitialized(
+ tab,
+ true,
+ "Server still running in content process"
+ );
+ await assertDevToolsOpened(
+ tab,
+ false,
+ "DevTools are reported as close, even if the server is still running because there is no more client connected"
+ );
+
+ ok(
+ DevToolsServer.initialized,
+ "Destroying the commands never destroys the DevToolsServer in the parent process when keepAlive is true"
+ );
+
+ info("Set DevToolsServer.keepAlive back to false");
+ DevToolsServer.keepAlive = false;
+ await setContentServerKeepAlive(tab, false);
+
+ info("Create and destroy a commands again");
+ const newCommands = await CommandsFactory.forTab(tab);
+ await newCommands.targetCommand.startListening();
+
+ await newCommands.destroy();
+
+ await assertServerInitialized(
+ tab,
+ false,
+ "Server stopped in content process"
+ );
+ await assertDevToolsOpened(
+ tab,
+ false,
+ "DevTools are reported as closed after destroying the second commands"
+ );
+
+ ok(
+ !DevToolsServer.initialized,
+ "When turning keepAlive to false, the server in the parent process is destroyed"
+ );
+
+ gBrowser.removeCurrentTab();
+ DevToolsServer.destroy();
+}
+
+async function assertServerInitialized(tab, expected, message) {
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [expected, message],
+ function (_expected, _message) {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ DevToolsServer,
+ } = require("resource://devtools/server/devtools-server.js");
+ is(DevToolsServer.initialized, _expected, _message);
+ }
+ );
+}
+
+async function assertDevToolsOpened(tab, expected, message) {
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [expected, message],
+ function (_expected, _message) {
+ is(ChromeUtils.isDevToolsOpened(), _expected, _message);
+ }
+ );
+}
+
+async function setContentServerKeepAlive(tab, keepAlive, message) {
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [keepAlive],
+ function (_keepAlive) {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ DevToolsServer,
+ } = require("resource://devtools/server/devtools-server.js");
+ DevToolsServer.keepAlive = _keepAlive;
+ }
+ );
+}
diff --git a/devtools/server/tests/browser/browser_document_devtools_basics.js b/devtools/server/tests/browser/browser_document_devtools_basics.js
new file mode 100644
index 0000000000..1d15420559
--- /dev/null
+++ b/devtools/server/tests/browser/browser_document_devtools_basics.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Document the basics of DevTools backend via Fronts in a test.
+ */
+
+"use strict";
+
+const TEST_URL = "data:text/html,new-tab";
+
+add_task(async () => {
+ // Allow logging all RDP packets
+ await pushPref("devtools.debugger.log", true);
+ // Really all of them
+ await pushPref("devtools.debugger.log.verbose", true);
+
+ // Instantiate a DevTools server
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+
+ // Instantiate a client connected to this server
+ const transport = DevToolsServer.connectPipe();
+ const client = new DevToolsClient(transport);
+
+ // This will trigger some handshake with the server
+ await client.connect();
+
+ // You need to call listTabs once to retrieve the existing list of Tab Descriptor actors...
+ const tabs = await client.mainRoot.listTabs();
+
+ // ... which will let you receive the 'tabListChanged' event.
+ // This is an empty RDP packet, you need to re-call listTabs to get the full new updated list of actors.
+ const onTabListUpdated = client.mainRoot.once("tabListChanged");
+
+ // Open a new tab.
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_URL,
+ });
+
+ await onTabListUpdated;
+
+ // The new list of Tab descriptors should contain the newly opened tab
+ const newTabs = await client.mainRoot.listTabs();
+ is(newTabs.length, tabs.length + 1);
+
+ const tabDescriptorActor = newTabs.pop();
+ is(tabDescriptorActor.url, TEST_URL);
+
+ // Query the Tab Descriptor actor to retrieve its related Watcher actor.
+ // Each Descriptor actor has a dedicated watcher which will be scoped to the context of the descriptor.
+ // Here the watcher will focus on the related tab.
+ const watcherActor = await tabDescriptorActor.getWatcher();
+
+ // The call to Watcher Actor's watchTargets will emit target-available-form RDP events.
+ // One per available target. It will emit one for each immediatly available target,
+ // but also for any available later. That, until you call unwatchTarget method.
+ //
+ // Here I'm listening to "target-available" to get a Front instance, which helps call RDP methods.
+ // But this isn't an RDP event. This is a frontend-only thing.
+ const onTopTargetAvailable = watcherActor.once("target-available");
+
+ // watchTargets accepts "frame", "process" and "worker"
+ // When debugging a web page you want to listen to frame and worker targets.
+ // "frame" would better be named "WindowGlobal" as it will notify you about all the WindowGlobal of the page.
+ // Each top level documents and any iframe documents will have a related WindowGlobal,
+ // if any of these documents navigate, a new WindowGlobal will be instantiated.
+ // If you care about workers, listen to worker targets as well.
+ await watcherActor.watchTargets("frame");
+
+ // This is a trivial example so we have a unique WindowGlobal target for the top level document
+ const topTarget = await onTopTargetAvailable;
+ is(topTarget.url, TEST_URL);
+
+ // Similarly to watchTarget, the next call to watchResources will emit new resources right away as well as later.
+ const onConsoleMessages = topTarget.once("resource-available-form");
+
+ // If you want to observe anything, you have to use Watcher Actor's watchrResources API.
+ // The list of all available resources is here:
+ // https://searchfox.org/mozilla-central/source/devtools/server/actors/resources/index.js#9
+ // And you might have a look at each ResourceWatcher subclass to learn more about the fields exposed by each resource type:
+ // https://searchfox.org/mozilla-central/source/devtools/server/actors/resources
+ await watcherActor.watchResources(["console-message"]);
+
+ // You may use many useful actors on each target actor, like console, thread, ...
+ // You can get the full list of available actors in:
+ // https://searchfox.org/mozilla-central/source/devtools/server/actors/utils/actor-registry.js#176
+ // And then look into the mentioned path for implementation.
+ const webConsoleActor = await topTarget.getFront("console");
+
+ // Call the Console API in order to force emitting a console-message resource
+ await webConsoleActor.evaluateJSAsync({ text: "console.log('42')" });
+
+ // Wait for the related console-message resource
+ const resources = await onConsoleMessages;
+
+ // Note that resource-available-form comes with a "resources" attribute which is an array of resources
+ // which may contain various resource types.
+ is(resources[0].message.arguments[0], "42");
+
+ await client.close();
+});
diff --git a/devtools/server/tests/browser/browser_document_rdp_basics.js b/devtools/server/tests/browser/browser_document_rdp_basics.js
new file mode 100644
index 0000000000..552837ff7c
--- /dev/null
+++ b/devtools/server/tests/browser/browser_document_rdp_basics.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Document the basics of RDP packets via a test.
+ */
+
+"use strict";
+
+const TEST_URL = "data:text/html,new-tab";
+
+add_task(async () => {
+ // Allow logging all RDP packets
+ await pushPref("devtools.debugger.log", true);
+ // Really all of them
+ await pushPref("devtools.debugger.log.verbose", true);
+
+ // Instantiate a DevTools server
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+
+ // Instantiate a client connected to this server
+ const transport = DevToolsServer.connectPipe();
+ const client = new DevToolsClient(transport);
+
+ // This will trigger some handshake with the server
+ await client.connect();
+
+ // Ignore this gross hack, this is to be able to emit raw RDP packet via client.request
+ // (a Front is instantiated by DevToolsClient which would be confused with us sending
+ // RDP packets for the Root actor)
+ client.mainRoot.destroy();
+
+ // You need to call listTabs once to retrieve the existing list of Tab Descriptor actors...
+ const { tabs } = await client.request({ to: "root", type: "listTabs" });
+
+ // ... which will let you receive the 'tabListChanged' event.
+ // This is an empty RDP packet, you need to re-call listTabs to get the full new updated list of actors.
+ const onTabListUpdated = client.once("tabListChanged");
+
+ // Open a new tab.
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_URL,
+ });
+
+ await onTabListUpdated;
+
+ // The new list of Tab descriptors should contain the newly opened tab
+ const { tabs: newTabs } = await client.request({
+ to: "root",
+ type: "listTabs",
+ });
+ is(newTabs.length, tabs.length + 1);
+
+ const tabDescriptorActor = newTabs.pop();
+ is(tabDescriptorActor.url, TEST_URL);
+
+ // Query the Tab Descriptor actor to retrieve its related Watcher actor.
+ // Each Descriptor actor has a dedicated watcher which will be scoped to the context of the descriptor.
+ // Here the watcher will focus on the related tab.
+ //
+ // You want to pass isServerTargetSwitchingEnabled set to true in order to be notified about the top level document,
+ // as well as navigations to subsequent documents.
+ const watcherActor = await client.request({
+ to: tabDescriptorActor.actor,
+ type: "getWatcher",
+ isServerTargetSwitchingEnabled: true,
+ });
+
+ // The call to Watcher Actor's watchTargets will emit target-available-form RDP events.
+ // One per available target. It will emit one for each immediatly available target,
+ // but also for any available later. That, until you call unwatchTarget method.
+ const onTopTargetAvailable = client.once("target-available-form");
+
+ // watchTargets accepts "frame", "process" and "worker"
+ // When debugging a web page you want to listen to frame and worker targets.
+ // "frame" would better be named "WindowGlobal" as it will notify you about all the WindowGlobal of the page.
+ // Each top level documents and any iframe documents will have a related WindowGlobal,
+ // if any of these documents navigate, a new WindowGlobal will be instantiated.
+ // If you care about workers, listen to worker targets as well.
+ await client.request({
+ to: watcherActor.actor,
+ type: "watchTargets",
+ targetType: "frame",
+ });
+
+ // This is a trivial example so we have a unique WindowGlobal target for the top level document
+ const { target: topTarget } = await onTopTargetAvailable;
+ is(topTarget.url, TEST_URL);
+
+ // Similarly to watchTarget, the next call to watchResources will emit new resources right away as well as later.
+ const onConsoleMessages = client.once("resource-available-form");
+
+ // If you want to observe anything, you have to use Watcher Actor's watchrResources API.
+ // The list of all available resources is here:
+ // https://searchfox.org/mozilla-central/source/devtools/server/actors/resources/index.js#9
+ // And you might have a look at each ResourceWatcher subclass to learn more about the fields exposed by each resource type:
+ // https://searchfox.org/mozilla-central/source/devtools/server/actors/resources
+ await client.request({
+ to: watcherActor.actor,
+ type: "watchResources",
+ resourceTypes: ["console-message"],
+ });
+
+ // You may use many useful actors on each target actor, like console, thread, ...
+ // You can get the full list of available actors in:
+ // https://searchfox.org/mozilla-central/source/devtools/server/actors/utils/actor-registry.js#176
+ // And then look into the mentioned path for implementation.
+ //
+ // The "target form" contains the list of all these actor IDs
+ const webConsoleActorID = topTarget.consoleActor;
+
+ // Call the Console API in order to force emitting a console-message resource
+ await client.request({
+ to: webConsoleActorID,
+ type: "evaluateJSAsync",
+ text: "console.log('42')",
+ });
+
+ // Wait for the related console-message resource
+ const { resources } = await onConsoleMessages;
+
+ // Note that resource-available-form comes with a "resources" attribute which is an array of resources
+ // which may contain various resource types.
+ is(resources[0].message.arguments[0], "42");
+
+ await client.close();
+});
diff --git a/devtools/server/tests/browser/browser_getProcess.js b/devtools/server/tests/browser/browser_getProcess.js
new file mode 100644
index 0000000000..30c9fff589
--- /dev/null
+++ b/devtools/server/tests/browser/browser_getProcess.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test `RootActor.getProcess` method
+ */
+
+"use strict";
+
+add_task(async () => {
+ let client, tab;
+
+ function connect() {
+ // Fake a first connection to the content process
+ const transport = DevToolsServer.connectPipe();
+ client = new DevToolsClient(transport);
+ return client.connect();
+ }
+
+ async function listProcess() {
+ const onNewProcess = new Promise(resolve => {
+ // Call listProcesses in order to start receiving new process notifications
+ client.mainRoot.on("processListChanged", function listener() {
+ client.off("processListChanged", listener);
+ ok(true, "Received processListChanged event");
+ resolve();
+ });
+ });
+ await client.mainRoot.listProcesses();
+ await createNewProcess();
+ return onNewProcess;
+ }
+
+ async function createNewProcess() {
+ tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "data:text/html,new-process",
+ forceNewProcess: true,
+ });
+ }
+
+ async function getProcess() {
+ // Note that we can't assert process count as the number of processes
+ // is affected by previous tests.
+ const processes = await client.mainRoot.listProcesses();
+ const { osPid } = tab.linkedBrowser.browsingContext.currentWindowGlobal;
+ const descriptor = processes.find(process => process.id == osPid);
+ ok(descriptor, "Got the new process descriptor");
+
+ // Connect to the first content process available
+ const content = processes.filter(p => !p.isParentProcessDescriptor)[0];
+
+ const processDescriptor = await client.mainRoot.getProcess(content.id);
+ const front = await processDescriptor.getTarget();
+ const targetForm = front.targetForm;
+ ok(targetForm.consoleActor, "Got the console actor");
+ ok(targetForm.threadActor, "Got the thread actor");
+
+ // Process target are no longer really used/supported beyond listing their workers
+ // from RootFront.
+ const { workers } = await front.listWorkers();
+ is(workers.length, 0, "listWorkers worked and reported no workers");
+
+ return [front, content.id];
+ }
+
+ // Assert that calling client.getProcess against the same process id is
+ // returning the same actor.
+ async function getProcessAgain(firstTargetFront, id) {
+ const processDescriptor = await client.mainRoot.getProcess(id);
+ const front = await processDescriptor.getTarget();
+ is(
+ front,
+ firstTargetFront,
+ "Second call to getProcess with the same id returns the same form"
+ );
+ }
+
+ function processScript() {
+ /* eslint-env mozilla/process-script */
+ const listener = function () {
+ Services.obs.removeObserver(listener, "devtools:loader:destroy");
+ sendAsyncMessage("test:getProcess-destroy", null);
+ };
+ Services.obs.addObserver(listener, "devtools:loader:destroy");
+ }
+
+ async function closeClient() {
+ const onLoaderDestroyed = new Promise(done => {
+ const processListener = function () {
+ Services.ppmm.removeMessageListener(
+ "test:getProcess-destroy",
+ processListener
+ );
+ done();
+ };
+ Services.ppmm.addMessageListener(
+ "test:getProcess-destroy",
+ processListener
+ );
+ });
+ const script = `data:,(${encodeURI(processScript)})()`;
+ Services.ppmm.loadProcessScript(script, true);
+ await client.close();
+
+ await onLoaderDestroyed;
+ Services.ppmm.removeDelayedProcessScript(script);
+ info("Loader destroyed in the content process");
+ }
+
+ // Instantiate a minimal server
+ DevToolsServer.init();
+ DevToolsServer.allowChromeProcess = true;
+ if (!DevToolsServer.createRootActor) {
+ DevToolsServer.registerAllActors();
+ }
+
+ await connect();
+ await listProcess();
+
+ const [front, contentId] = await getProcess();
+
+ await getProcessAgain(front, contentId);
+
+ await closeClient();
+
+ BrowserTestUtils.removeTab(tab);
+ DevToolsServer.destroy();
+});
diff --git a/devtools/server/tests/browser/browser_inspector-anonymous.js b/devtools/server/tests/browser/browser_inspector-anonymous.js
new file mode 100644
index 0000000000..024b7af1bb
--- /dev/null
+++ b/devtools/server/tests/browser/browser_inspector-anonymous.js
@@ -0,0 +1,204 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for Bug 777674
+
+add_task(async function () {
+ await SpecialPowers.pushPermissions([
+ { type: "allowXULXBL", allow: true, context: MAIN_DOMAIN },
+ ]);
+
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+
+ await testXBLAnonymousInHTMLDocument(walker);
+ await testNativeAnonymous(walker);
+ await testNativeAnonymousStartingNode(walker);
+
+ await testPseudoElements(walker);
+ await testEmptyWithPseudo(walker);
+ await testShadowAnonymous(walker);
+});
+
+async function testXBLAnonymousInHTMLDocument(walker) {
+ info("Testing XBL anonymous in an HTML document.");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ const XUL_NS =
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ const rawToolbarbutton = content.document.createElementNS(
+ XUL_NS,
+ "toolbarbutton"
+ );
+ content.document.documentElement.appendChild(rawToolbarbutton);
+ });
+
+ const toolbarbutton = await walker.querySelector(
+ walker.rootNode,
+ "toolbarbutton"
+ );
+ const children = await walker.children(toolbarbutton);
+
+ is(toolbarbutton.numChildren, 0, "XBL content is not visible in HTML doc");
+ is(children.nodes.length, 0, "XBL content is not returned in HTML doc");
+}
+
+async function testNativeAnonymous(walker) {
+ info("Testing native anonymous content with walker.");
+
+ const select = await walker.querySelector(walker.rootNode, "select");
+ const children = await walker.children(select);
+
+ is(select.numChildren, 2, "No native anon content for form control");
+ is(children.nodes.length, 2, "No native anon content for form control");
+}
+
+async function testNativeAnonymousStartingNode(walker) {
+ info("Tests attaching an element that a walker can't see.");
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [[walker.actorID]],
+ async function (actorID) {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ DevToolsServer,
+ } = require("resource://devtools/server/devtools-server.js");
+
+ const {
+ DocumentWalker,
+ } = require("resource://devtools/server/actors/inspector/document-walker.js");
+ const nodeFilterConstants = require("resource://devtools/shared/dom-node-filter-constants.js");
+
+ const docwalker = new DocumentWalker(
+ content.document.querySelector("select"),
+ content,
+ {
+ filter: () => {
+ return nodeFilterConstants.FILTER_ACCEPT;
+ },
+ }
+ );
+ const scrollbar = docwalker.lastChild();
+ is(scrollbar.tagName, "scrollbar", "An anonymous child has been fetched");
+
+ // Convert actorID to current compartment string otherwise
+ // searchAllConnectionsForActor is confused and won't find the actor.
+ actorID = String(actorID);
+ const serverWalker = DevToolsServer.searchAllConnectionsForActor(actorID);
+ const node = await serverWalker.attachElement(scrollbar);
+
+ ok(node, "A response has arrived");
+ ok(node.node, "A node is in the response");
+ is(
+ node.node.rawNode.tagName,
+ "SELECT",
+ "The node has changed to a parent that the walker recognizes"
+ );
+ }
+ );
+}
+
+async function testPseudoElements(walker) {
+ info("Testing pseudo elements with walker.");
+
+ // Markup looks like: <div><::before /><span /><::after /></div>
+ const pseudo = await walker.querySelector(walker.rootNode, "#pseudo");
+ const children = await walker.children(pseudo);
+
+ is(
+ pseudo.numChildren,
+ 1,
+ "::before/::after are not counted if there is a child"
+ );
+ is(children.nodes.length, 3, "Correct number of children");
+
+ const before = children.nodes[0];
+ ok(before.isAnonymous, "Child is anonymous");
+ ok(before._form.isNativeAnonymous, "Child is native anonymous");
+
+ const span = children.nodes[1];
+ ok(!span.isAnonymous, "Child is not anonymous");
+
+ const after = children.nodes[2];
+ ok(after.isAnonymous, "Child is anonymous");
+ ok(after._form.isNativeAnonymous, "Child is native anonymous");
+}
+
+async function testEmptyWithPseudo(walker) {
+ info("Testing elements with no childrent, except for pseudos.");
+
+ info("Checking an element whose only child is a pseudo element");
+ const pseudo = await walker.querySelector(walker.rootNode, "#pseudo-empty");
+ const children = await walker.children(pseudo);
+
+ is(
+ pseudo.numChildren,
+ 1,
+ "::before/::after are is counted if there are no other children"
+ );
+ is(children.nodes.length, 1, "Correct number of children");
+
+ const before = children.nodes[0];
+ ok(before.isAnonymous, "Child is anonymous");
+ ok(before._form.isNativeAnonymous, "Child is native anonymous");
+}
+
+async function testShadowAnonymous(walker) {
+ info("Testing shadow DOM content.");
+
+ const host = await walker.querySelector(walker.rootNode, "#shadow");
+ const children = await walker.children(host);
+
+ // #shadow-root, ::before, light dom
+ is(host.numChildren, 3, "Children of the shadow root are counted");
+ is(children.nodes.length, 3, "Children returned from walker");
+
+ const before = children.nodes[1];
+ is(
+ before._form.nodeName,
+ "_moz_generated_content_before",
+ "Should be the ::before pseudo-element"
+ );
+ ok(before.isAnonymous, "::before is anonymous");
+ ok(before._form.isNativeAnonymous, "::before is native anonymous");
+ info(JSON.stringify(before._form));
+
+ const shadow = children.nodes[0];
+ const shadowChildren = await walker.children(shadow);
+ // <h3>...</h3>, <select multiple></select>
+ is(shadow.numChildren, 2, "Children of the shadow root are counted");
+ is(shadowChildren.nodes.length, 2, "Children returned from walker");
+
+ // <h3>Shadow <em>DOM</em></h3>
+ const shadowChild1 = shadowChildren.nodes[0];
+ ok(!shadowChild1.isAnonymous, "Shadow child is not anonymous");
+ ok(
+ !shadowChild1._form.isNativeAnonymous,
+ "Shadow child is not native anonymous"
+ );
+
+ const shadowSubChildren = await walker.children(shadowChild1);
+ is(shadowChild1.numChildren, 2, "Subchildren of the shadow root are counted");
+ is(shadowSubChildren.nodes.length, 2, "Subchildren are returned from walker");
+
+ // <em>DOM</em>
+ const shadowSubChild = shadowSubChildren.nodes[1];
+ ok(
+ !shadowSubChild.isAnonymous,
+ "Subchildren of shadow root are not anonymous"
+ );
+ ok(
+ !shadowSubChild._form.isNativeAnonymous,
+ "Subchildren of shadow root is not native anonymous"
+ );
+
+ // <select multiple></select>
+ const shadowChild2 = shadowChildren.nodes[1];
+ ok(!shadowChild2.isAnonymous, "Child is anonymous");
+ ok(!shadowChild2._form.isNativeAnonymous, "Child is not native anonymous");
+}
diff --git a/devtools/server/tests/browser/browser_inspector-iframe.js b/devtools/server/tests/browser/browser_inspector-iframe.js
new file mode 100644
index 0000000000..e9c3fd93a1
--- /dev/null
+++ b/devtools/server/tests/browser/browser_inspector-iframe.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const BYPASS_WALKERFRONT_CHILDREN_IFRAME_GUARD_PREF =
+ "devtools.testing.bypass-walker-children-iframe-guard";
+
+add_task(async function testIframe() {
+ info("Check that dedicated walker is used for retrieving iframe children");
+
+ const TEST_URI = `https://example.com/document-builder.sjs?html=${encodeURIComponent(`
+ <h1>Test iframe</h1>
+ <iframe src="https://example.com/document-builder.sjs?html=Hello"></iframe>
+`)}`;
+
+ const { walker } = await initInspectorFront(TEST_URI);
+ const iframeNodeFront = await walker.querySelector(walker.rootNode, "iframe");
+
+ is(
+ iframeNodeFront.useChildTargetToFetchChildren,
+ isEveryFrameTargetEnabled(),
+ "useChildTargetToFetchChildren has expected value"
+ );
+ is(
+ iframeNodeFront.numChildren,
+ 1,
+ "numChildren is set to 1 (for the #document node)"
+ );
+
+ const res = await walker.children(iframeNodeFront);
+ is(
+ res.nodes.length,
+ 1,
+ "Retrieving the iframe children return an array with one element"
+ );
+ const documentNodeFront = res.nodes[0];
+ is(
+ documentNodeFront.nodeName,
+ "#document",
+ "The child is the #document element"
+ );
+ if (isEveryFrameTargetEnabled()) {
+ Assert.notStrictEqual(
+ documentNodeFront.walkerFront,
+ walker,
+ "The child walker is different from the top level document one when EFT is enabled"
+ );
+ }
+ is(
+ documentNodeFront.parentNode(),
+ iframeNodeFront,
+ "The child parent was set to the original iframe nodeFront"
+ );
+});
+
+add_task(async function testIframeBlockedByCSP() {
+ info("Check that iframe blocked by CSP don't have any children");
+
+ const TEST_URI = `https://example.com/document-builder.sjs?html=${encodeURIComponent(`
+ <h1>Test CSP-blocked iframe</h1>
+ <iframe src="https://example.org/document-builder.sjs?html=Hello"></iframe>
+`)}&headers=content-security-policy:default-src 'self'`;
+
+ const { walker } = await initInspectorFront(TEST_URI);
+ const iframeNodeFront = await walker.querySelector(walker.rootNode, "iframe");
+
+ is(
+ iframeNodeFront.useChildTargetToFetchChildren,
+ false,
+ "useChildTargetToFetchChildren is false"
+ );
+ is(iframeNodeFront.numChildren, 0, "numChildren is set to 0");
+
+ info("Test calling WalkerFront#children with the safe guard removed");
+ await pushPref(BYPASS_WALKERFRONT_CHILDREN_IFRAME_GUARD_PREF, true);
+
+ let res = await walker.children(iframeNodeFront);
+ is(
+ res.nodes.length,
+ 0,
+ "Retrieving the iframe children return an empty array"
+ );
+
+ info("Test calling WalkerFront#children again, but with the safe guard");
+ Services.prefs.clearUserPref(BYPASS_WALKERFRONT_CHILDREN_IFRAME_GUARD_PREF);
+ res = await walker.children(iframeNodeFront);
+ is(
+ res.nodes.length,
+ 0,
+ "Retrieving the iframe children return an empty array"
+ );
+});
diff --git a/devtools/server/tests/browser/browser_inspector-insert.js b/devtools/server/tests/browser/browser_inspector-insert.js
new file mode 100644
index 0000000000..d3f2ea482d
--- /dev/null
+++ b/devtools/server/tests/browser/browser_inspector-insert.js
@@ -0,0 +1,158 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+
+ await testRearrange(walker);
+ await testInsertInvalidInput(walker);
+});
+
+async function testRearrange(walker) {
+ const longlist = await walker.querySelector(walker.rootNode, "#longlist");
+ let children = await walker.children(longlist);
+ const nodeA = children.nodes[0];
+ is(nodeA.id, "a", "Got the expected node.");
+
+ // Move nodeA to the end of the list.
+ await walker.insertBefore(nodeA, longlist, null);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ ok(
+ !content.document.querySelector("#a").nextSibling,
+ "a should now be at the end of the list."
+ );
+ });
+
+ children = await walker.children(longlist);
+ is(
+ nodeA,
+ children.nodes[children.nodes.length - 1],
+ "a should now be the last returned child."
+ );
+
+ // Now move it to the middle of the list.
+ const nextNode = children.nodes[13];
+ await walker.insertBefore(nodeA, longlist, nextNode);
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [[nextNode.actorID]],
+ async function (actorID) {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ DevToolsServer,
+ } = require("resource://devtools/server/devtools-server.js");
+ const {
+ DocumentWalker,
+ } = require("resource://devtools/server/actors/inspector/document-walker.js");
+ const sibling = new DocumentWalker(
+ content.document.querySelector("#a"),
+ content
+ ).nextSibling();
+ // Convert actorID to current compartment string otherwise
+ // searchAllConnectionsForActor is confused and won't find the actor.
+ actorID = String(actorID);
+ const nodeActor = DevToolsServer.searchAllConnectionsForActor(actorID);
+ is(
+ sibling,
+ nodeActor.rawNode,
+ "Node should match the expected next node."
+ );
+ }
+ );
+
+ children = await walker.children(longlist);
+ is(nodeA, children.nodes[13], "a should be where we expect it.");
+ is(nextNode, children.nodes[14], "next node should be where we expect it.");
+}
+
+async function testInsertInvalidInput(walker) {
+ const longlist = await walker.querySelector(walker.rootNode, "#longlist");
+ const children = await walker.children(longlist);
+ const nodeA = children.nodes[0];
+ const nextSibling = children.nodes[1];
+
+ // Now move it to the original location and make sure no mutation happens.
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [[longlist.actorID]],
+ async function (actorID) {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ DevToolsServer,
+ } = require("resource://devtools/server/devtools-server.js");
+ // Convert actorID to current compartment string otherwise
+ // searchAllConnectionsForActor is confused and won't find the actor.
+ actorID = String(actorID);
+ const nodeActor = DevToolsServer.searchAllConnectionsForActor(actorID);
+ content.hasMutated = false;
+ content.observer = new content.MutationObserver(() => {
+ content.hasMutated = true;
+ });
+ content.observer.observe(nodeActor.rawNode, {
+ childList: true,
+ });
+ }
+ );
+
+ await walker.insertBefore(nodeA, longlist, nodeA);
+ let hasMutated = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function () {
+ const state = content.hasMutated;
+ content.hasMutated = false;
+ return state;
+ }
+ );
+ ok(!hasMutated, "hasn't mutated");
+
+ await walker.insertBefore(nodeA, longlist, nextSibling);
+ hasMutated = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function () {
+ const state = content.hasMutated;
+ content.hasMutated = false;
+ return state;
+ }
+ );
+ ok(!hasMutated, "still hasn't mutated after inserting before nextSibling");
+
+ await walker.insertBefore(nodeA, longlist);
+ hasMutated = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function () {
+ const state = content.hasMutated;
+ content.hasMutated = false;
+ return state;
+ }
+ );
+ ok(hasMutated, "has mutated after inserting with null sibling");
+
+ await walker.insertBefore(nodeA, longlist);
+ hasMutated = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function () {
+ const state = content.hasMutated;
+ content.hasMutated = false;
+ return state;
+ }
+ );
+ ok(!hasMutated, "hasn't mutated after inserting with null sibling again");
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ content.observer.disconnect();
+ });
+}
diff --git a/devtools/server/tests/browser/browser_inspector-isScrollable.js b/devtools/server/tests/browser/browser_inspector-isScrollable.js
new file mode 100644
index 0000000000..e28fc01ce9
--- /dev/null
+++ b/devtools/server/tests/browser/browser_inspector-isScrollable.js
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const URL = MAIN_DOMAIN + "inspector-isScrollable-data.html";
+
+const CASES = [
+ { id: "body", expected: false },
+ { id: "no_children", expected: false },
+ { id: "one_child_no_overflow", expected: false },
+ { id: "margin_left_overflow", expected: true },
+ { id: "transform_overflow", expected: true },
+ { id: "nested_overflow", expected: true },
+ { id: "intermediate_overflow", expected: true },
+ { id: "multiple_overflow_at_different_depths", expected: true },
+ { id: "overflow_hidden", expected: false },
+ { id: "scrollbar_none", expected: false },
+];
+
+add_task(async function () {
+ info(
+ "Test that elements with scrollbars have a true value for isScrollable, and elements without scrollbars have a false value."
+ );
+ const { walker } = await initInspectorFront(URL);
+
+ for (const { id, expected } of CASES) {
+ info(`Checking element id ${id}.`);
+
+ const el = await walker.querySelector(walker.rootNode, `#${id}`);
+ is(el.isScrollable, expected, `${id} has expected value for isScrollable.`);
+ }
+});
diff --git a/devtools/server/tests/browser/browser_inspector-mutations-childlist.js b/devtools/server/tests/browser/browser_inspector-mutations-childlist.js
new file mode 100644
index 0000000000..6818c9c8dc
--- /dev/null
+++ b/devtools/server/tests/browser/browser_inspector-mutations-childlist.js
@@ -0,0 +1,282 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/server/tests/browser/inspector-helpers.js",
+ this
+);
+
+function loadSelector(walker, selector) {
+ return walker.querySelectorAll(walker.rootNode, selector).then(nodeList => {
+ return nodeList.items();
+ });
+}
+
+function loadSelectors(walker, selectors) {
+ return Promise.all(Array.from(selectors, sel => loadSelector(walker, sel)));
+}
+
+function doMoves(movesArg) {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [movesArg],
+ function (moves) {
+ function setParent(nodeSelector, newParentSelector) {
+ const node = content.document.querySelector(nodeSelector);
+ if (newParentSelector) {
+ const newParent = content.document.querySelector(newParentSelector);
+ newParent.appendChild(node);
+ } else {
+ node.remove();
+ }
+ }
+ for (const move of moves) {
+ setParent(move[0], move[1]);
+ }
+ }
+ );
+}
+
+/**
+ * Test a set of tree rearrangements and make sure they cause the expected changes.
+ */
+
+var gDummySerial = 0;
+
+function mutationTest(testSpec) {
+ return async function () {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+ await loadSelectors(walker, testSpec.load || ["html"]);
+ walker.autoCleanup = !!testSpec.autoCleanup;
+ if (testSpec.preCheck) {
+ testSpec.preCheck();
+ }
+ const onMutations = walker.once("mutations");
+
+ await doMoves(testSpec.moves || []);
+
+ // Some of these moves will trigger no mutation events,
+ // so do a dummy change to the root node to trigger
+ // a mutation event anyway.
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [[gDummySerial++]],
+ function (serial) {
+ content.document.documentElement.setAttribute("data-dummy", serial);
+ }
+ );
+
+ let mutations = await onMutations;
+
+ // Filter out our dummy mutation.
+ mutations = mutations.filter(change => {
+ if (change.type == "attributes" && change.attributeName == "data-dummy") {
+ return false;
+ }
+ return true;
+ });
+ await assertOwnershipTrees(walker);
+ if (testSpec.postCheck) {
+ testSpec.postCheck(walker, mutations);
+ }
+ };
+}
+
+// Verify that our dummy mutation works.
+add_task(
+ mutationTest({
+ autoCleanup: false,
+ postCheck(walker, mutations) {
+ is(mutations.length, 0, "Dummy mutation is filtered out.");
+ },
+ })
+);
+
+// Test a simple move to a different location in the sibling list for the same
+// parent.
+add_task(
+ mutationTest({
+ autoCleanup: false,
+ load: ["#longlist div"],
+ moves: [["#a", "#longlist"]],
+ postCheck(walker, mutations) {
+ const remove = mutations[0];
+ is(remove.type, "childList", "First mutation should be a childList.");
+ ok(!!remove.removed.length, "First mutation should be a removal.");
+ const add = mutations[1];
+ is(
+ add.type,
+ "childList",
+ "Second mutation should be a childList removal."
+ );
+ ok(!!add.added.length, "Second mutation should be an addition.");
+ const a = add.added[0];
+ is(a.id, "a", "Added node should be #a");
+ is(a.parentNode(), remove.target, "Should still be a child of longlist.");
+ is(
+ remove.target,
+ add.target,
+ "First and second mutations should be against the same node."
+ );
+ },
+ })
+);
+
+// Test a move to another location that is within our ownership tree.
+add_task(
+ mutationTest({
+ autoCleanup: false,
+ load: ["#longlist div", "#longlist-sibling"],
+ moves: [["#a", "#longlist-sibling"]],
+ postCheck(walker, mutations) {
+ const remove = mutations[0];
+ is(remove.type, "childList", "First mutation should be a childList.");
+ ok(!!remove.removed.length, "First mutation should be a removal.");
+ const add = mutations[1];
+ is(
+ add.type,
+ "childList",
+ "Second mutation should be a childList removal."
+ );
+ ok(!!add.added.length, "Second mutation should be an addition.");
+ const a = add.added[0];
+ is(a.id, "a", "Added node should be #a");
+ is(a.parentNode(), add.target, "Should still be a child of longlist.");
+ is(
+ add.target.id,
+ "longlist-sibling",
+ "long-sibling should be the target."
+ );
+ },
+ })
+);
+
+// Move an unseen node with a seen parent into our ownership tree - should generate a
+// childList pair with no adds or removes.
+add_task(
+ mutationTest({
+ autoCleanup: false,
+ load: ["#longlist"],
+ moves: [["#longlist-sibling", "#longlist"]],
+ postCheck(walker, mutations) {
+ is(mutations.length, 2, "Should generate two mutations");
+ is(mutations[0].type, "childList", "Should be childList mutations.");
+ is(mutations[0].added.length, 0, "Should have no adds.");
+ is(mutations[0].removed.length, 0, "Should have no removes.");
+ is(mutations[1].type, "childList", "Should be childList mutations.");
+ is(mutations[1].added.length, 0, "Should have no adds.");
+ is(mutations[1].removed.length, 0, "Should have no removes.");
+ },
+ })
+);
+
+// Move an unseen node with an unseen parent into our ownership tree. Should only
+// generate one childList mutation with no adds or removes.
+add_task(
+ mutationTest({
+ autoCleanup: false,
+ load: ["#longlist div"],
+ moves: [["#longlist-sibling-firstchild", "#longlist"]],
+ postCheck(walker, mutations) {
+ is(mutations.length, 1, "Should generate two mutations");
+ is(mutations[0].type, "childList", "Should be childList mutations.");
+ is(mutations[0].added.length, 0, "Should have no adds.");
+ is(mutations[0].removed.length, 0, "Should have no removes.");
+ },
+ })
+);
+
+// Move a node between unseen nodes, should generate no mutations.
+add_task(
+ mutationTest({
+ autoCleanup: false,
+ load: ["html"],
+ moves: [["#longlist-sibling", "#longlist"]],
+ postCheck(walker, mutations) {
+ is(mutations.length, 0, "Should generate no mutations.");
+ },
+ })
+);
+
+// Orphan a node and don't clean it up
+add_task(
+ mutationTest({
+ autoCleanup: false,
+ load: ["#longlist div"],
+ moves: [["#longlist", null]],
+ postCheck(walker, mutations) {
+ is(mutations.length, 1, "Should generate one mutation.");
+ const change = mutations[0];
+ is(change.type, "childList", "Should be a childList.");
+ is(change.removed.length, 1, "Should have removed a child.");
+ const ownership = clientOwnershipTree(walker);
+ is(ownership.orphaned.length, 1, "Should have one orphaned subtree.");
+ is(
+ ownershipTreeSize(ownership.orphaned[0]),
+ 1 + 26 + 26,
+ "Should have orphaned longlist, and 26 children, and 26 singleTextChilds"
+ );
+ },
+ })
+);
+
+// Orphan a node, and do clean it up.
+add_task(
+ mutationTest({
+ autoCleanup: true,
+ load: ["#longlist div"],
+ moves: [["#longlist", null]],
+ postCheck(walker, mutations) {
+ is(mutations.length, 1, "Should generate one mutation.");
+ const change = mutations[0];
+ is(change.type, "childList", "Should be a childList.");
+ is(change.removed.length, 1, "Should have removed a child.");
+ const ownership = clientOwnershipTree(walker);
+ is(ownership.orphaned.length, 0, "Should have no orphaned subtrees.");
+ },
+ })
+);
+
+// Orphan a node by moving it into the tree but out of our visible subtree.
+add_task(
+ mutationTest({
+ autoCleanup: false,
+ load: ["#longlist div"],
+ moves: [["#longlist", "#longlist-sibling"]],
+ postCheck(walker, mutations) {
+ is(mutations.length, 1, "Should generate one mutation.");
+ const change = mutations[0];
+ is(change.type, "childList", "Should be a childList.");
+ is(change.removed.length, 1, "Should have removed a child.");
+ const ownership = clientOwnershipTree(walker);
+ is(ownership.orphaned.length, 1, "Should have one orphaned subtree.");
+ is(
+ ownershipTreeSize(ownership.orphaned[0]),
+ 1 + 26 + 26,
+ "Should have orphaned longlist, 26 children, and 26 singleTextChilds."
+ );
+ },
+ })
+);
+
+// Orphan a node by moving it into the tree but out of our visible subtree,
+// and clean it up.
+add_task(
+ mutationTest({
+ autoCleanup: true,
+ load: ["#longlist div"],
+ moves: [["#longlist", "#longlist-sibling"]],
+ postCheck(walker, mutations) {
+ is(mutations.length, 1, "Should generate one mutation.");
+ const change = mutations[0];
+ is(change.type, "childList", "Should be a childList.");
+ is(change.removed.length, 1, "Should have removed a child.");
+ const ownership = clientOwnershipTree(walker);
+ is(ownership.orphaned.length, 0, "Should have no orphaned subtrees.");
+ },
+ })
+);
diff --git a/devtools/server/tests/browser/browser_inspector-release.js b/devtools/server/tests/browser/browser_inspector-release.js
new file mode 100644
index 0000000000..5546da605a
--- /dev/null
+++ b/devtools/server/tests/browser/browser_inspector-release.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/server/tests/browser/inspector-helpers.js",
+ this
+);
+
+add_task(async function loadNewChild() {
+ const { target, walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+
+ let originalOwnershipSize = 0;
+ let longlist = null;
+ let firstChild = null;
+ const list = await walker.querySelectorAll(walker.rootNode, "#longlist div");
+ // Make sure we have the 26 children of longlist in our ownership tree.
+ is(list.length, 26, "Expect 26 div children.");
+ // Make sure we've read in all those children and incorporated them
+ // in our ownership tree.
+ const items = await list.items();
+ originalOwnershipSize = await assertOwnershipTrees(walker);
+
+ // Here is how the ownership tree is summed up:
+ // #document 1
+ // <html> 1
+ // <body> 1
+ // <div id=longlist> 1
+ // <div id=a>a</div> 26*2 (each child plus it's singleTextChild)
+ // ...
+ // <div id=z>z</div>
+ // -----
+ // 56
+ is(originalOwnershipSize, 56, "Correct number of items in ownership tree");
+ firstChild = items[0].actorID;
+ // Now get the longlist and release it from the ownership tree.
+ const node = await walker.querySelector(walker.rootNode, "#longlist");
+ longlist = node.actorID;
+ await walker.releaseNode(node);
+ // Our ownership size should now be 53 fewer
+ // (we forgot about #longlist + 26 children + 26 singleTextChild nodes)
+ const newOwnershipSize = await assertOwnershipTrees(walker);
+ is(
+ newOwnershipSize,
+ originalOwnershipSize - 53,
+ "Ownership tree should be lower"
+ );
+ // Now verify that some nodes have gone away
+ await checkMissing(target, longlist);
+ await checkMissing(target, firstChild);
+});
diff --git a/devtools/server/tests/browser/browser_inspector-remove.js b/devtools/server/tests/browser/browser_inspector-remove.js
new file mode 100644
index 0000000000..8338e40ea2
--- /dev/null
+++ b/devtools/server/tests/browser/browser_inspector-remove.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/server/tests/browser/inspector-helpers.js",
+ this
+);
+
+add_task(async function testRemoveSubtree() {
+ const { target, walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ function ignoreNode(node) {
+ // Duplicate the walker logic to skip blank nodes...
+ return (
+ node.nodeType === content.Node.TEXT_NODE &&
+ !/[^\s]/.test(node.nodeValue)
+ );
+ }
+
+ let nextSibling = content.document.querySelector("#longlist").nextSibling;
+ while (nextSibling && ignoreNode(nextSibling)) {
+ nextSibling = nextSibling.nextSibling;
+ }
+
+ let previousSibling =
+ content.document.querySelector("#longlist").previousSibling;
+ while (previousSibling && ignoreNode(previousSibling)) {
+ previousSibling = previousSibling.previousSibling;
+ }
+ content.nextSibling = nextSibling;
+ content.previousSibling = previousSibling;
+ });
+
+ let originalOwnershipSize = 0;
+ const longlist = await walker.querySelector(walker.rootNode, "#longlist");
+ const longlistID = longlist.actorID;
+ await walker.children(longlist);
+ originalOwnershipSize = await assertOwnershipTrees(walker);
+ // Here is how the ownership tree is summed up:
+ // #document 1
+ // <html> 1
+ // <body> 1
+ // <div id=longlist> 1
+ // <div id=a>a</div> 26*2 (each child plus it's singleTextChild)
+ // ...
+ // <div id=z>z</div>
+ // -----
+ // 56
+ is(originalOwnershipSize, 56, "Correct number of items in ownership tree");
+
+ const onMutation = waitForMutation(walker, isChildList);
+ const siblings = await walker.removeNode(longlist);
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [[siblings.previousSibling.actorID, siblings.nextSibling.actorID]],
+ function ([previousActorID, nextActorID]) {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ DevToolsServer,
+ } = require("resource://devtools/server/devtools-server.js");
+
+ // Convert actorID to current compartment string otherwise
+ // searchAllConnectionsForActor is confused and won't find the actor.
+ previousActorID = String(previousActorID);
+ nextActorID = String(nextActorID);
+ const previous =
+ DevToolsServer.searchAllConnectionsForActor(previousActorID);
+ const next = DevToolsServer.searchAllConnectionsForActor(nextActorID);
+
+ is(
+ previous.rawNode,
+ content.previousSibling,
+ "Should have returned the previous sibling."
+ );
+ is(
+ next.rawNode,
+ content.nextSibling,
+ "Should have returned the next sibling."
+ );
+ }
+ );
+ await onMutation;
+ // Our ownership size should now be 51 fewer (we forgot about #longlist + 26
+ // children + 26 singleTextChild nodes, but learned about #longlist's
+ // prev/next sibling)
+ const newOwnershipSize = await assertOwnershipTrees(walker);
+ is(
+ newOwnershipSize,
+ originalOwnershipSize - 51,
+ "Ownership tree should be lower"
+ );
+ // Now verify that some nodes have gone away
+ return checkMissing(target, longlistID);
+});
diff --git a/devtools/server/tests/browser/browser_inspector-retain.js b/devtools/server/tests/browser/browser_inspector-retain.js
new file mode 100644
index 0000000000..43d156675e
--- /dev/null
+++ b/devtools/server/tests/browser/browser_inspector-retain.js
@@ -0,0 +1,157 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/server/tests/browser/inspector-helpers.js",
+ this
+);
+
+// Retain a node, and a second-order child (in another document, for kicks)
+// Release the parent of the top item, which should cause one retained orphan.
+
+// Then unretain the top node, which should retain the orphan.
+
+// Then change the source of the iframe, which should kill that orphan.
+
+add_task(async function testRetain() {
+ // The test does not make sense when EFT is enabled, as different documents will have
+ // different walkers.
+ if (isEveryFrameTargetEnabled()) {
+ return;
+ }
+
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+
+ // Get the toplevel body element and retain it.
+ const bodyFront = await walker.querySelector(walker.rootNode, "body");
+ await walker.retainNode(bodyFront);
+ // Get an element in the child frame and retain it.
+ const frame = await walker.querySelector(walker.rootNode, "#childFrame");
+ const children = await walker.children(frame, { maxNodes: 1 });
+ const childDoc = children.nodes[0];
+ const childListFront = await walker.querySelector(childDoc, "#longlist");
+ const originalOwnershipSize = await assertOwnershipTrees(walker);
+ // and retain it.
+ await walker.retainNode(childListFront);
+ // OK, try releasing the parent of the first retained.
+ await walker.releaseNode(bodyFront.parentNode());
+ const clientTree = clientOwnershipTree(walker);
+
+ // That request should have freed the parent of the first retained
+ // but moved the rest into the retained orphaned tree.
+ is(
+ ownershipTreeSize(clientTree.root) +
+ ownershipTreeSize(clientTree.retained[0]) +
+ 1,
+ originalOwnershipSize,
+ "Should have only lost one item overall."
+ );
+ is(walker._retainedOrphans.size, 1, "Should have retained one orphan");
+ ok(
+ walker._retainedOrphans.has(bodyFront),
+ "Should have retained the expected node."
+ );
+ // Unretain the body, which should promote the childListFront to a retained orphan.
+ await walker.unretainNode(bodyFront);
+ await assertOwnershipTrees(walker);
+
+ is(
+ walker._retainedOrphans.size,
+ 1,
+ "Should still only have one retained orphan."
+ );
+ ok(
+ !walker._retainedOrphans.has(bodyFront),
+ "Should have dropped the body node."
+ );
+ ok(
+ walker._retainedOrphans.has(childListFront),
+ "Should have retained the child node."
+ );
+
+ // Change the source of the iframe, which should kill the retained orphan.
+ const onMutations = waitForMutation(walker, isUnretained);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ content.document.querySelector("#childFrame").src =
+ "data:text/html,<html>new child</html>";
+ });
+ await onMutations;
+
+ await assertOwnershipTrees(walker);
+ is(walker._retainedOrphans.size, 0, "Should have no more retained orphans.");
+});
+
+// Get a hold of a node, remove it from the doc and retain it at the same time.
+// We should always win that race (even though the mutation happens before the
+// retain request), because we haven't issued `getMutations` yet.
+add_task(async function testWinRace() {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+
+ const front = await walker.querySelector(walker.rootNode, "#a");
+ const onMutation = waitForMutation(walker, isChildList);
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ const contentNode = content.document.querySelector("#a");
+ contentNode.remove();
+ });
+ // Now wait for that mutation and retain response to come in.
+ await walker.retainNode(front);
+ await onMutation;
+
+ await assertOwnershipTrees(walker);
+ is(walker._retainedOrphans.size, 1, "Should have a retained orphan.");
+ ok(
+ walker._retainedOrphans.has(front),
+ "Should have retained our expected node."
+ );
+ await walker.unretainNode(front);
+
+ // Make sure we're clear for the next test.
+ await assertOwnershipTrees(walker);
+ is(walker._retainedOrphans.size, 0, "Should have no more retained orphans.");
+});
+
+// Same as above, but issue the request right after the 'new-mutations' event, so that
+// we *lose* the race.
+add_task(async function testLoseRace() {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+
+ const front = await walker.querySelector(walker.rootNode, "#z");
+ const onMutation = walker.once("new-mutations");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ const contentNode = content.document.querySelector("#z");
+ contentNode.remove();
+ });
+ await onMutation;
+
+ // Verify that we have an outstanding request (no good way to tell that it's a
+ // getMutations request, but there's nothing else it would be).
+ is(walker._requests.length, 1, "Should have an outstanding request.");
+ try {
+ await walker.retainNode(front);
+ ok(false, "Request should not have succeeded!");
+ } catch (err) {
+ // XXX: Switched to from ok() to todo_is() in Bug 1467712. Follow up in
+ // 1500960
+ // This is throwing because of
+ // `gInspectee.querySelector("#z").parentNode = null;` two blocks above...
+ // Even if you fix that, the test is still failing because "#a" was removed
+ // by the previous test. I am switching this to "#z" because I think that
+ // was the original intent. Still not failing with the expected error message
+ // Needs more work.
+ // ok(err, "noSuchActor", "Should have lost the race.");
+ is(
+ walker._retainedOrphans.size,
+ 0,
+ "Should have no more retained orphans."
+ );
+ // Don't re-throw the error.
+ }
+});
diff --git a/devtools/server/tests/browser/browser_inspector-search.js b/devtools/server/tests/browser/browser_inspector-search.js
new file mode 100644
index 0000000000..21cf745ce1
--- /dev/null
+++ b/devtools/server/tests/browser/browser_inspector-search.js
@@ -0,0 +1,347 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/server/tests/browser/inspector-helpers.js",
+ this
+);
+
+// Test for Bug 835896
+// WalkerSearch specific tests. This is to make sure search results are
+// coming back as expected.
+// See also test_inspector-search-front.html.
+
+add_task(async function () {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-search-data.html"
+ );
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [[walker.actorID]],
+ async function (actorID) {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ DevToolsServer,
+ } = require("resource://devtools/server/devtools-server.js");
+ const {
+ DocumentWalker: _documentWalker,
+ } = require("resource://devtools/server/actors/inspector/document-walker.js");
+
+ // Convert actorID to current compartment string otherwise
+ // searchAllConnectionsForActor is confused and won't find the actor.
+ actorID = String(actorID);
+ const walkerActor = DevToolsServer.searchAllConnectionsForActor(actorID);
+ const walkerSearch = walkerActor.walkerSearch;
+ const {
+ WalkerSearch,
+ WalkerIndex,
+ } = require("resource://devtools/server/actors/utils/walker-search.js");
+
+ info("Testing basic index APIs exist.");
+ const index = new WalkerIndex(walkerActor);
+ Assert.greater(
+ index.data.size,
+ 0,
+ "public index is filled after getting"
+ );
+
+ index.clearIndex();
+ ok(!index._data, "private index is empty after clearing");
+ Assert.greater(
+ index.data.size,
+ 0,
+ "public index is filled after getting"
+ );
+
+ index.destroy();
+
+ info("Testing basic search APIs exist.");
+
+ ok(walkerSearch, "walker search exists on the WalkerActor");
+ ok(walkerSearch.search, "walker search has `search` method");
+ ok(walkerSearch.index, "walker search has `index` property");
+ is(
+ walkerSearch.walker,
+ walkerActor,
+ "referencing the correct WalkerActor"
+ );
+
+ const walkerSearch2 = new WalkerSearch(walkerActor);
+ ok(walkerSearch2, "a new search instance can be created");
+ ok(walkerSearch2.search, "new search instance has `search` method");
+ ok(walkerSearch2.index, "new search instance has `index` property");
+ isnot(
+ walkerSearch2,
+ walkerSearch,
+ "new search instance differs from the WalkerActor's"
+ );
+
+ walkerSearch2.destroy();
+
+ info("Testing search with an empty query.");
+ let results = walkerSearch.search("");
+ is(results.length, 0, "No results when searching for ''");
+
+ results = walkerSearch.search(null);
+ is(results.length, 0, "No results when searching for null");
+
+ results = walkerSearch.search(undefined);
+ is(results.length, 0, "No results when searching for undefined");
+
+ results = walkerSearch.search(10);
+ is(results.length, 0, "No results when searching for 10");
+
+ const inspectee = content.document;
+ const testData = [
+ {
+ desc: "Search for tag with one result.",
+ search: "body",
+ expected: [{ node: inspectee.body, type: "tag" }],
+ },
+ {
+ desc: "Search for tag with multiple results",
+ search: "h2",
+ expected: [
+ { node: inspectee.querySelectorAll("h2")[0], type: "tag" },
+ { node: inspectee.querySelectorAll("h2")[1], type: "tag" },
+ { node: inspectee.querySelectorAll("h2")[2], type: "tag" },
+ ],
+ },
+ {
+ desc: "Search for selector with multiple results",
+ search: "body > h2",
+ expected: [
+ { node: inspectee.querySelectorAll("h2")[0], type: "selector" },
+ { node: inspectee.querySelectorAll("h2")[1], type: "selector" },
+ { node: inspectee.querySelectorAll("h2")[2], type: "selector" },
+ ],
+ },
+ {
+ desc: "Search for selector with multiple results",
+ search: ":root h2",
+ expected: [
+ { node: inspectee.querySelectorAll("h2")[0], type: "selector" },
+ { node: inspectee.querySelectorAll("h2")[1], type: "selector" },
+ { node: inspectee.querySelectorAll("h2")[2], type: "selector" },
+ ],
+ },
+ {
+ desc: "Search for selector with multiple results",
+ search: "* h2",
+ expected: [
+ { node: inspectee.querySelectorAll("h2")[0], type: "selector" },
+ { node: inspectee.querySelectorAll("h2")[1], type: "selector" },
+ { node: inspectee.querySelectorAll("h2")[2], type: "selector" },
+ ],
+ },
+ {
+ desc: "Search with multiple matches in a single tag expecting a single result",
+ search: "💩",
+ expected: [
+ { node: inspectee.getElementById("💩"), type: "attributeValue" },
+ ],
+ },
+ {
+ desc: "Search that has tag and text results",
+ search: "h1",
+ expected: [
+ { node: inspectee.querySelector("h1"), type: "tag" },
+ {
+ node: inspectee.querySelector("h1 + p").childNodes[0],
+ type: "text",
+ },
+ {
+ node: inspectee.querySelector("h1 + p > strong").childNodes[0],
+ type: "text",
+ },
+ ],
+ },
+ {
+ desc: "Search for XPath with one result",
+ search: "//strong",
+ expected: [
+ { node: inspectee.querySelector("strong"), type: "xpath" },
+ ],
+ },
+ {
+ desc: "Search for XPath with multiple results",
+ search: "//h2",
+ expected: [
+ { node: inspectee.querySelectorAll("h2")[0], type: "xpath" },
+ { node: inspectee.querySelectorAll("h2")[1], type: "xpath" },
+ { node: inspectee.querySelectorAll("h2")[2], type: "xpath" },
+ ],
+ },
+ {
+ desc: "Search for XPath via containing text",
+ search: "//*[contains(text(), 'p tag')]",
+ expected: [{ node: inspectee.querySelector("p"), type: "xpath" }],
+ },
+ {
+ desc: "Search for XPath matching text node",
+ search: "//strong/text()",
+ expected: [
+ {
+ node: inspectee.querySelector("strong").firstChild,
+ type: "xpath",
+ },
+ ],
+ },
+ {
+ desc: "Search using XPath grouping expression",
+ search: "(//*)[2]",
+ expected: [{ node: inspectee.querySelector("head"), type: "xpath" }],
+ },
+ {
+ desc: "Search using XPath function",
+ search: "id('arrows')",
+ expected: [
+ { node: inspectee.querySelector("#arrows"), type: "xpath" },
+ ],
+ },
+ ];
+
+ const isDeeply = (a, b, msg) => {
+ return is(JSON.stringify(a), JSON.stringify(b), msg);
+ };
+ for (const { desc, search, expected } of testData) {
+ info("Running test: " + desc);
+ results = walkerSearch.search(search);
+ isDeeply(
+ results,
+ expected,
+ "Search returns correct results with '" + search + "'"
+ );
+ }
+
+ info("Testing ::before and ::after element matching");
+
+ const beforeElt = new _documentWalker(
+ inspectee.querySelector("#pseudo"),
+ inspectee.defaultView
+ ).firstChild();
+ const afterElt = new _documentWalker(
+ inspectee.querySelector("#pseudo"),
+ inspectee.defaultView
+ ).lastChild();
+ const styleText = inspectee.querySelector("style").childNodes[0];
+
+ // ::before
+ results = walkerSearch.search("::before");
+ isDeeply(
+ results,
+ [{ node: beforeElt, type: "tag" }],
+ "Tag search works for pseudo element"
+ );
+
+ results = walkerSearch.search("_moz_generated_content_before");
+ is(results.length, 0, "No results for anon tag name");
+
+ results = walkerSearch.search("before element");
+ isDeeply(
+ results,
+ [
+ { node: styleText, type: "text" },
+ { node: beforeElt, type: "text" },
+ ],
+ "Text search works for pseudo element"
+ );
+
+ // ::after
+ results = walkerSearch.search("::after");
+ isDeeply(
+ results,
+ [{ node: afterElt, type: "tag" }],
+ "Tag search works for pseudo element"
+ );
+
+ results = walkerSearch.search("_moz_generated_content_after");
+ is(results.length, 0, "No results for anon tag name");
+
+ results = walkerSearch.search("after element");
+ isDeeply(
+ results,
+ [
+ { node: styleText, type: "text" },
+ { node: afterElt, type: "text" },
+ ],
+ "Text search works for pseudo element"
+ );
+
+ info("Testing search before and after a mutation.");
+ const expected = [
+ { node: inspectee.querySelectorAll("h3")[0], type: "tag" },
+ { node: inspectee.querySelectorAll("h3")[1], type: "tag" },
+ { node: inspectee.querySelectorAll("h3")[2], type: "tag" },
+ ];
+
+ results = walkerSearch.search("h3");
+ isDeeply(results, expected, "Search works with tag results");
+
+ function mutateDocumentAndWaitForMutation(mutationFn) {
+ // eslint-disable-next-line new-cap
+ return new Promise(resolve => {
+ info("Listening to markup mutation on the inspectee");
+ const observer = new inspectee.defaultView.MutationObserver(resolve);
+ observer.observe(inspectee, { childList: true, subtree: true });
+ mutationFn();
+ });
+ }
+ await mutateDocumentAndWaitForMutation(() => {
+ expected[0].node.remove();
+ });
+
+ results = walkerSearch.search("h3");
+ isDeeply(
+ results,
+ [expected[1], expected[2]],
+ "Results are updated after removal"
+ );
+
+ // eslint-disable-next-line new-cap
+ await new Promise(resolve => {
+ info("Waiting for a mutation to happen");
+ const observer = new inspectee.defaultView.MutationObserver(() => {
+ resolve();
+ });
+ observer.observe(inspectee, { attributes: true, subtree: true });
+ inspectee.body.setAttribute("h3", "true");
+ });
+
+ results = walkerSearch.search("h3");
+ isDeeply(
+ results,
+ [
+ { node: inspectee.body, type: "attributeName" },
+ expected[1],
+ expected[2],
+ ],
+ "Results are updated after addition"
+ );
+
+ // eslint-disable-next-line new-cap
+ await new Promise(resolve => {
+ info("Waiting for a mutation to happen");
+ const observer = new inspectee.defaultView.MutationObserver(() => {
+ resolve();
+ });
+ observer.observe(inspectee, {
+ attributes: true,
+ childList: true,
+ subtree: true,
+ });
+ inspectee.body.removeAttribute("h3");
+ expected[1].node.remove();
+ expected[2].node.remove();
+ });
+
+ results = walkerSearch.search("h3");
+ is(results.length, 0, "Results are updated after removal");
+ }
+ );
+});
diff --git a/devtools/server/tests/browser/browser_inspector-shadow.js b/devtools/server/tests/browser/browser_inspector-shadow.js
new file mode 100644
index 0000000000..7675593c96
--- /dev/null
+++ b/devtools/server/tests/browser/browser_inspector-shadow.js
@@ -0,0 +1,231 @@
+"use strict";
+
+const URL = MAIN_DOMAIN + "inspector-shadow.html";
+
+add_task(async function () {
+ info("Test that a shadow host has a shadow root");
+ const { walker } = await initInspectorFront(URL);
+
+ const el = await walker.querySelector(walker.rootNode, "#empty");
+ const children = await walker.children(el);
+
+ is(el.displayName, "test-empty", "#empty exists");
+ ok(el.isShadowHost, "#empty is a shadow host");
+
+ const shadowRoot = children.nodes[0];
+ ok(shadowRoot.isShadowRoot, "#empty has a shadow-root child");
+ is(children.nodes.length, 1, "#empty has no other children");
+});
+
+add_task(async function () {
+ info("Test that a shadow host has its children too");
+ const { walker } = await initInspectorFront(URL);
+
+ const el = await walker.querySelector(walker.rootNode, "#one-child");
+ const children = await walker.children(el);
+
+ is(
+ children.nodes.length,
+ 2,
+ "#one-child has two children " + "(shadow root + another child)"
+ );
+ ok(children.nodes[0].isShadowRoot, "First child is a shadow-root");
+ is(children.nodes[1].displayName, "h1", "Second child is <h1>");
+});
+
+add_task(async function () {
+ info("Test that shadow-root has its children");
+ const { walker } = await initInspectorFront(URL);
+
+ const el = await walker.querySelector(walker.rootNode, "#shadow-children");
+ ok(el.isShadowHost, "#shadow-children is a shadow host");
+
+ const children = await walker.children(el);
+ ok(
+ children.nodes.length === 1 && children.nodes[0].isShadowRoot,
+ "#shadow-children has only one child and it's a shadow-root"
+ );
+
+ const shadowRoot = children.nodes[0];
+ const shadowChildren = await walker.children(shadowRoot);
+ is(shadowChildren.nodes.length, 2, "shadow-root has two children");
+ is(shadowChildren.nodes[0].displayName, "h1", "First child is <h1>");
+ is(shadowChildren.nodes[1].displayName, "p", "Second child is <p>");
+});
+
+add_task(async function () {
+ info("Test that shadow root has its children and slotted nodes");
+ const { walker } = await initInspectorFront(URL);
+
+ const el = await walker.querySelector(walker.rootNode, "#named-slot");
+ ok(el.isShadowHost, "#named-slot is a shadow host");
+
+ const children = await walker.children(el);
+ is(children.nodes.length, 2, "#named-slot has two children");
+ const shadowRoot = children.nodes[0];
+ ok(shadowRoot.isShadowRoot, "#named-slot has a shadow-root child");
+
+ const slotted = children.nodes[1];
+ is(
+ slotted.getAttribute("slot"),
+ "slot1",
+ "#named-slot as a child that is slotted"
+ );
+
+ const shadowChildren = await walker.children(shadowRoot);
+ is(
+ shadowChildren.nodes[0].displayName,
+ "h1",
+ "shadow-root first child is a regular <h1> tag"
+ );
+ is(
+ shadowChildren.nodes[1].displayName,
+ "slot",
+ "shadow-root second child is a slot"
+ );
+
+ const slottedChildren = await walker.children(shadowChildren.nodes[1]);
+ is(
+ slottedChildren.nodes[0],
+ slotted,
+ "The slot has the slotted node as a child"
+ );
+});
+
+add_task(async function () {
+ info("Test pseudoelements in shadow host");
+ const { walker } = await initInspectorFront(URL);
+
+ const el = await walker.querySelector(walker.rootNode, "#host-pseudo");
+ const children = await walker.children(el);
+
+ ok(children.nodes[0].isShadowRoot, "#host-pseudo 1st child is a shadow root");
+ ok(
+ children.nodes[1].isBeforePseudoElement,
+ "#host-pseudo 2nd child is ::before"
+ );
+ ok(
+ children.nodes[2].isAfterPseudoElement,
+ "#host-pseudo 3rd child is ::after"
+ );
+});
+
+add_task(async function () {
+ info("Test pseudoelements in slotted nodes");
+ const { walker } = await initInspectorFront(URL);
+
+ const el = await walker.querySelector(walker.rootNode, "#slot-pseudo");
+ const shadowRoot = (await walker.children(el)).nodes[0];
+ ok(shadowRoot.isShadowRoot, "#slot-pseudo has a shadow-root child");
+
+ const shadowChildren = await walker.children(shadowRoot);
+ is(shadowChildren.nodes[1].displayName, "slot", "shadow-root has a slot");
+
+ const slottedChildren = await walker.children(shadowChildren.nodes[1]);
+ ok(slottedChildren.nodes[0].isBeforePseudoElement, "slot has ::before");
+ ok(
+ slottedChildren.nodes[slottedChildren.nodes.length - 1]
+ .isAfterPseudoElement,
+ "slot has ::after"
+ );
+});
+
+add_task(async function () {
+ info("Test open/closed modes in shadow roots");
+ const { walker } = await initInspectorFront(URL);
+
+ const openEl = await walker.querySelector(walker.rootNode, "#mode-open");
+ const openShadowRoot = (await walker.children(openEl)).nodes[0];
+ const closedEl = await walker.querySelector(walker.rootNode, "#mode-closed");
+ const closedShadowRoot = (await walker.children(closedEl)).nodes[0];
+
+ is(
+ openShadowRoot.shadowRootMode,
+ "open",
+ "#mode-open has a shadow root with open mode"
+ );
+ is(
+ closedShadowRoot.shadowRootMode,
+ "closed",
+ "#mode-closed has a shadow root with closed mode"
+ );
+});
+
+add_task(async function () {
+ info("Test that slotted inline text nodes appear in the Shadow DOM tree");
+ const { walker } = await initInspectorFront(URL);
+
+ const el = await walker.querySelector(walker.rootNode, "#slot-inline-text");
+ const hostChildren = await walker.children(el);
+ const originalSlot = hostChildren.nodes[1];
+ is(
+ originalSlot.displayName,
+ "#text",
+ "Shadow host as a text node to be slotted"
+ );
+
+ const shadowRoot = hostChildren.nodes[0];
+ const shadowChildren = await walker.children(shadowRoot);
+ const slot = shadowChildren.nodes[0];
+ is(slot.displayName, "slot", "shadow-root has a slot child");
+ ok(!slot._form.inlineTextChild, "Slotted node is not an inline text");
+
+ const slotChildren = await walker.children(slot);
+ const slotted = slotChildren.nodes[0];
+ is(slotted.displayName, "#text", "Slotted node is a text node");
+ is(
+ slotted._form.nodeValue,
+ originalSlot._form.nodeValue,
+ "Slotted content is the same as original's"
+ );
+});
+
+add_task(async function () {
+ info("Test UA widgets when showAllAnonymousContent is true");
+ await SpecialPowers.pushPrefEnv({
+ set: [["devtools.inspector.showAllAnonymousContent", true]],
+ });
+
+ const { walker } = await initInspectorFront(URL);
+
+ let el = await walker.querySelector(walker.rootNode, "#video-controls");
+ let hostChildren = await walker.children(el);
+ is(hostChildren.nodes.length, 3, "#video-controls tag has 3 children");
+ const shadowRoot = hostChildren.nodes[0];
+ ok(shadowRoot.isShadowRoot, "#video-controls has a shadow-root child");
+
+ el = await walker.querySelector(
+ walker.rootNode,
+ "#video-controls-with-children"
+ );
+ hostChildren = await walker.children(el);
+ is(
+ hostChildren.nodes.length,
+ 4,
+ "#video-controls-with-children has 4 children"
+ );
+});
+
+add_task(async function () {
+ info("Test UA widgets when showAllAnonymousContent is false");
+ await SpecialPowers.pushPrefEnv({
+ set: [["devtools.inspector.showAllAnonymousContent", false]],
+ });
+
+ const { walker } = await initInspectorFront(URL);
+
+ let el = await walker.querySelector(walker.rootNode, "#video-controls");
+ let hostChildren = await walker.children(el);
+ is(hostChildren.nodes.length, 0, "#video-controls tag has no children");
+
+ el = await walker.querySelector(
+ walker.rootNode,
+ "#video-controls-with-children"
+ );
+ hostChildren = await walker.children(el);
+ is(
+ hostChildren.nodes.length,
+ 1,
+ "#video-controls-with-children has one child"
+ );
+});
diff --git a/devtools/server/tests/browser/browser_inspector-traversal.js b/devtools/server/tests/browser/browser_inspector-traversal.js
new file mode 100644
index 0000000000..786521cc18
--- /dev/null
+++ b/devtools/server/tests/browser/browser_inspector-traversal.js
@@ -0,0 +1,350 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/server/tests/browser/inspector-helpers.js",
+ this
+);
+
+const checkActorIDs = [];
+
+add_task(async function loadNewChild() {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+
+ // Make sure that refetching the root document of the walker returns the same
+ // actor as the getWalker returned.
+ const root = await walker.document();
+ Assert.strictEqual(
+ root,
+ walker.rootNode,
+ "Re-fetching the document node should match the root document node."
+ );
+ checkActorIDs.push(root.actorID);
+ await assertOwnershipTrees(walker);
+});
+
+add_task(async function testInnerHTML() {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+
+ const docElement = await walker.documentElement();
+ const longstring = await walker.innerHTML(docElement);
+ const innerHTML = await longstring.string();
+ const actualInnerHTML = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ function () {
+ return content.document.documentElement.innerHTML;
+ }
+ );
+ Assert.strictEqual(innerHTML, actualInnerHTML, "innerHTML should match");
+});
+
+add_task(async function testOuterHTML() {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+
+ const docElement = await walker.documentElement();
+ const longstring = await walker.outerHTML(docElement);
+ const outerHTML = await longstring.string();
+ const actualOuterHTML = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ function () {
+ return content.document.documentElement.outerHTML;
+ }
+ );
+ Assert.strictEqual(outerHTML, actualOuterHTML, "outerHTML should match");
+});
+
+add_task(async function testSetOuterHTMLNode() {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+ const newHTML = '<p id="edit-html-done">after edit</p>';
+ let node = await walker.querySelector(walker.rootNode, "#edit-html");
+ await walker.setOuterHTML(node, newHTML);
+ node = await walker.querySelector(walker.rootNode, "#edit-html-done");
+ const longstring = await walker.outerHTML(node);
+ const outerHTML = await longstring.string();
+ is(outerHTML, newHTML, "outerHTML has been updated");
+ node = await walker.querySelector(walker.rootNode, "#edit-html");
+ ok(!node, "The node with the old ID cannot be selected anymore");
+});
+
+add_task(async function testQuerySelector() {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+ let node = await walker.querySelector(walker.rootNode, "#longlist");
+ is(
+ node.getAttribute("data-test"),
+ "exists",
+ "should have found the right node"
+ );
+ await assertOwnershipTrees(walker);
+ node = await walker.querySelector(walker.rootNode, "unknownqueryselector");
+ ok(!node, "Should not find a node here.");
+ await assertOwnershipTrees(walker);
+});
+
+add_task(async function testQuerySelectors() {
+ const { target, walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+ const nodeList = await walker.querySelectorAll(
+ walker.rootNode,
+ "#longlist div"
+ );
+ is(nodeList.length, 26, "Expect 26 div children.");
+ await assertOwnershipTrees(walker);
+ const firstNode = await nodeList.item(0);
+ checkActorIDs.push(firstNode.actorID);
+ is(firstNode.id, "a", "First child should be a");
+ await assertOwnershipTrees(walker);
+ let nodes = await nodeList.items();
+ is(nodes.length, 26, "Expect 26 nodes");
+ is(nodes[0], firstNode, "First node should be reused.");
+ ok(nodes[0]._parent, "Parent node should be set.");
+ ok(nodes[0]._next || nodes[0]._prev, "Siblings should be set.");
+ ok(
+ nodes[25]._next || nodes[25]._prev,
+ "Siblings of " + nodes[25] + " should be set."
+ );
+ await assertOwnershipTrees(walker);
+ nodes = await nodeList.items(-1);
+ is(nodes.length, 1, "Expect 1 node");
+ is(nodes[0].id, "z", "Expect it to be the last node.");
+ checkActorIDs.push(nodes[0].actorID);
+ // Save the node list ID so we can ensure it was destroyed.
+ const nodeListID = nodeList.actorID;
+ await assertOwnershipTrees(walker);
+ await nodeList.release();
+ ok(!nodeList.actorID, "Actor should have been destroyed.");
+ await assertOwnershipTrees(walker);
+ await checkMissing(target, nodeListID);
+});
+
+// Helper to check the response of requests that return hasFirst/hasLast/nodes
+// node lists (like `children` and `siblings`)
+async function checkArray(walker, children, first, last, ids) {
+ is(
+ children.hasFirst,
+ first,
+ "Should " + (first ? "" : "not ") + " have the first node."
+ );
+ is(
+ children.hasLast,
+ last,
+ "Should " + (last ? "" : "not ") + " have the last node."
+ );
+ is(
+ children.nodes.length,
+ ids.length,
+ "Should have " + ids.length + " children listed."
+ );
+ let responseIds = "";
+ for (const node of children.nodes) {
+ responseIds += node.id;
+ }
+ is(responseIds, ids, "Correct nodes were returned.");
+ await assertOwnershipTrees(walker);
+}
+
+add_task(async function testNoChildren() {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+ const empty = await walker.querySelector(walker.rootNode, "#empty");
+ await assertOwnershipTrees(walker);
+ const children = await walker.children(empty);
+ await checkArray(walker, children, true, true, "");
+});
+
+add_task(async function testLongListTraversal() {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+ const longList = await walker.querySelector(walker.rootNode, "#longlist");
+ // First call with no options, expect all children.
+ await assertOwnershipTrees(walker);
+ let children = await walker.children(longList);
+ await checkArray(walker, children, true, true, "abcdefghijklmnopqrstuvwxyz");
+ const allChildren = children.nodes;
+ await assertOwnershipTrees(walker);
+ // maxNodes should limit us to the first 5 nodes.
+ await assertOwnershipTrees(walker);
+ children = await walker.children(longList, { maxNodes: 5 });
+ await checkArray(walker, children, true, false, "abcde");
+ await assertOwnershipTrees(walker);
+ // maxNodes with the second item centered should still give us the first 5 nodes.
+ children = await walker.children(longList, {
+ maxNodes: 5,
+ center: allChildren[1],
+ });
+ await checkArray(walker, children, true, false, "abcde");
+ // maxNodes with a center in the middle of the list should put that item in the middle
+ const center = allChildren[13];
+ is(center.id, "n", "Make sure I know how to count letters.");
+ children = await walker.children(longList, { maxNodes: 5, center });
+ await checkArray(walker, children, false, false, "lmnop");
+ // maxNodes with the second-to-last item centered should give us the last 5 nodes.
+ children = await walker.children(longList, {
+ maxNodes: 5,
+ center: allChildren[24],
+ });
+ await checkArray(walker, children, false, true, "vwxyz");
+ // maxNodes with a start in the middle should start at that node and fetch 5
+ const start = allChildren[13];
+ is(start.id, "n", "Make sure I know how to count letters.");
+ children = await walker.children(longList, { maxNodes: 5, start });
+ await checkArray(walker, children, false, false, "nopqr");
+ // maxNodes near the end should only return what's left
+ children = await walker.children(longList, {
+ maxNodes: 5,
+ start: allChildren[24],
+ });
+ await checkArray(walker, children, false, true, "yz");
+});
+
+add_task(async function testObjectNodeChildren() {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+ const object = await walker.querySelector(walker.rootNode, "object");
+ const children = await walker.children(object);
+ await checkArray(walker, children, true, true, "1");
+});
+
+add_task(async function testNextSibling() {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+ const y = await walker.querySelector(walker.rootNode, "#y");
+ is(y.id, "y", "Got the right node.");
+ const z = await walker.nextSibling(y);
+ is(z.id, "z", "nextSibling got the next node.");
+ const nothing = await walker.nextSibling(z);
+ is(nothing, null, "nextSibling on the last node returned null.");
+});
+
+add_task(async function testPreviousSibling() {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+ const b = await walker.querySelector(walker.rootNode, "#b");
+ is(b.id, "b", "Got the right node.");
+ const a = await walker.previousSibling(b);
+ is(a.id, "a", "nextSibling got the next node.");
+ const nothing = await walker.previousSibling(a);
+ is(nothing, null, "previousSibling on the first node returned null.");
+});
+
+add_task(async function testFrameTraversal() {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+ const childFrame = await walker.querySelector(walker.rootNode, "#childFrame");
+ const children = await walker.children(childFrame);
+ const nodes = children.nodes;
+ is(nodes.length, 1, "There should be only one child of the iframe");
+ is(
+ nodes[0].nodeType,
+ Node.DOCUMENT_NODE,
+ "iframe child should be a document node"
+ );
+ await walker.querySelector(nodes[0], "#z");
+});
+
+add_task(async function testLongValue() {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+
+ SimpleTest.registerCleanupFunction(async function () {
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const WalkerActor = require("resource://devtools/server/actors/inspector/walker.js");
+ WalkerActor.setValueSummaryLength(
+ WalkerActor.DEFAULT_VALUE_SUMMARY_LENGTH
+ );
+ });
+ });
+
+ const longstringText = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ function () {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const testSummaryLength = 10;
+ const WalkerActor = require("resource://devtools/server/actors/inspector/walker.js");
+
+ WalkerActor.setValueSummaryLength(testSummaryLength);
+ return content.document.getElementById("longstring").firstChild.nodeValue;
+ }
+ );
+
+ const node = await walker.querySelector(walker.rootNode, "#longstring");
+ ok(!node.inlineTextChild, "Text is too long to be inlined");
+ // Now we need to get the text node child...
+ const children = await walker.children(node, { maxNodes: 1 });
+ const textNode = children.nodes[0];
+ is(textNode.nodeType, Node.TEXT_NODE, "Value should be a text node");
+ const value = await textNode.getNodeValue();
+ const valueStr = await value.string();
+ is(
+ valueStr,
+ longstringText,
+ "Full node value should match the string from the document."
+ );
+});
+
+add_task(async function testShortValue() {
+ const { walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+ const shortstringText = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ function () {
+ return content.document.getElementById("shortstring").firstChild
+ .nodeValue;
+ }
+ );
+
+ const node = await walker.querySelector(walker.rootNode, "#shortstring");
+ ok(!!node.inlineTextChild, "Text is short enough to be inlined");
+ // Now we need to get the text node child...
+ const children = await walker.children(node, { maxNodes: 1 });
+ const textNode = children.nodes[0];
+ is(textNode.nodeType, Node.TEXT_NODE, "Value should be a text node");
+ const value = await textNode.getNodeValue();
+ const valueStr = await value.string();
+ is(
+ valueStr,
+ shortstringText,
+ "Full node value should match the string from the document."
+ );
+});
+
+add_task(async function testReleaseWalker() {
+ const { target, walker } = await initInspectorFront(
+ MAIN_DOMAIN + "inspector-traversal-data.html"
+ );
+ checkActorIDs.push(walker.actorID);
+
+ await walker.release();
+ for (const id of checkActorIDs) {
+ await checkMissing(target, id);
+ }
+});
diff --git a/devtools/server/tests/browser/browser_inspector-utils.js b/devtools/server/tests/browser/browser_inspector-utils.js
new file mode 100644
index 0000000000..b81eeb0178
--- /dev/null
+++ b/devtools/server/tests/browser/browser_inspector-utils.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/server/tests/browser/inspector-helpers.js",
+ this
+);
+
+const COLOR_WHITE = [255, 255, 255, 1];
+
+add_task(async function loadNewChild() {
+ const { walker } = await initInspectorFront(
+ `data:text/html,<style>body{color:red;background-color:white;}body::before{content:"test";}</style>`
+ );
+
+ const body = await walker.querySelector(walker.rootNode, "body");
+ const color = await body.getBackgroundColor();
+ Assert.deepEqual(
+ color.value,
+ COLOR_WHITE,
+ "Background color is calculated correctly for an element with a pseudo child."
+ );
+});
diff --git a/devtools/server/tests/browser/browser_layout_getGrids.js b/devtools/server/tests/browser/browser_layout_getGrids.js
new file mode 100644
index 0000000000..ce40cf7a22
--- /dev/null
+++ b/devtools/server/tests/browser/browser_layout_getGrids.js
@@ -0,0 +1,145 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check the output of getGrids for the LayoutActor
+
+const GRID_FRAGMENT_DATA = {
+ areas: [
+ {
+ columnEnd: 3,
+ columnStart: 2,
+ name: "header",
+ rowEnd: 2,
+ rowStart: 1,
+ type: "explicit",
+ },
+ {
+ columnEnd: 2,
+ columnStart: 1,
+ name: "sidebar",
+ rowEnd: 3,
+ rowStart: 2,
+ type: "explicit",
+ },
+ {
+ columnEnd: 3,
+ columnStart: 2,
+ name: "content",
+ rowEnd: 3,
+ rowStart: 2,
+ type: "explicit",
+ },
+ ],
+ cols: {
+ lines: [
+ {
+ breadth: 0,
+ names: ["col-1", "col-start-1", "sidebar-start"],
+ number: 1,
+ start: 0,
+ type: "explicit",
+ },
+ {
+ breadth: 0,
+ names: ["col-2", "header-start", "sidebar-end", "content-start"],
+ number: 2,
+ start: 100,
+ type: "explicit",
+ },
+ {
+ breadth: 0,
+ names: ["header-end", "content-end"],
+ number: 3,
+ start: 200,
+ type: "explicit",
+ },
+ ],
+ tracks: [
+ {
+ breadth: 100,
+ start: 0,
+ state: "static",
+ type: "explicit",
+ },
+ {
+ breadth: 100,
+ start: 100,
+ state: "static",
+ type: "explicit",
+ },
+ ],
+ },
+ rows: {
+ lines: [
+ {
+ breadth: 0,
+ names: ["header-start"],
+ number: 1,
+ start: 0,
+ type: "explicit",
+ },
+ {
+ breadth: 0,
+ names: ["header-end", "sidebar-start", "content-start"],
+ number: 2,
+ start: 100,
+ type: "explicit",
+ },
+ {
+ breadth: 0,
+ names: ["sidebar-end", "content-end"],
+ number: 3,
+ start: 200,
+ type: "explicit",
+ },
+ ],
+ tracks: [
+ {
+ breadth: 100,
+ start: 0,
+ state: "static",
+ type: "explicit",
+ },
+ {
+ breadth: 100,
+ start: 100,
+ state: "static",
+ type: "explicit",
+ },
+ ],
+ },
+};
+
+add_task(async function () {
+ const { target, walker, layout } = await initLayoutFrontForUrl(
+ MAIN_DOMAIN + "grid.html"
+ );
+ const grids = await layout.getGrids(walker.rootNode);
+ const grid = grids[0];
+ const { gridFragments } = grid;
+
+ is(grids.length, 1, "One grid was returned.");
+ is(gridFragments.length, 1, "One grid fragment was returned.");
+ ok(Array.isArray(gridFragments), "An array of grid fragments was returned.");
+ Assert.deepEqual(
+ gridFragments[0],
+ GRID_FRAGMENT_DATA,
+ "Got the correct grid fragment data."
+ );
+
+ info("Get the grid container node front.");
+
+ try {
+ const nodeFront = await walker.getNodeFromActor(grids[0].actorID, [
+ "containerEl",
+ ]);
+ ok(nodeFront, "Got the grid container node front.");
+ } catch (e) {
+ ok(false, "Did not get grid container node front.");
+ }
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_layout_simple.js b/devtools/server/tests/browser/browser_layout_simple.js
new file mode 100644
index 0000000000..d4caba572e
--- /dev/null
+++ b/devtools/server/tests/browser/browser_layout_simple.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Simple checks for the LayoutActor and GridActor
+
+add_task(async function () {
+ const { target, walker, layout } = await initLayoutFrontForUrl(
+ "data:text/html;charset=utf-8,<title>test</title><div></div>"
+ );
+
+ ok(layout, "The LayoutFront was created");
+ ok(layout.getGrids, "The getGrids method exists");
+
+ let didThrow = false;
+ try {
+ await layout.getGrids(null);
+ } catch (e) {
+ didThrow = true;
+ }
+ ok(didThrow, "An exception was thrown for a missing NodeActor in getGrids");
+
+ const invalidNode = await walker.querySelector(walker.rootNode, "title");
+ const grids = await layout.getGrids(invalidNode);
+ ok(Array.isArray(grids), "An array of grids was returned");
+ is(grids.length, 0, "0 grids have been returned for the invalid node");
+
+ await target.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_memory_allocations_01.js b/devtools/server/tests/browser/browser_memory_allocations_01.js
new file mode 100644
index 0000000000..cc6d5b0f58
--- /dev/null
+++ b/devtools/server/tests/browser/browser_memory_allocations_01.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ const target = await addTabTarget("data:text/html;charset=utf-8,test-doc");
+ const memory = await target.getFront("memory");
+
+ await memory.attach();
+
+ await memory.startRecordingAllocations();
+ ok(true, "Can start recording allocations");
+
+ // Allocate some objects.
+ const [line1, line2, line3] = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ function () {
+ // Use eval to ensure allocating the object in the page's compartment
+ return content.eval(
+ "(" +
+ function () {
+ let alloc1, alloc2, alloc3;
+
+ /* eslint-disable max-nested-callbacks */
+ (function outer() {
+ (function middle() {
+ (function inner() {
+ alloc1 = {};
+ alloc1.line = Error().lineNumber;
+ alloc2 = [];
+ alloc2.line = Error().lineNumber;
+ // eslint-disable-next-line new-parens
+ alloc3 = new (function () {})();
+ alloc3.line = Error().lineNumber;
+ })();
+ })();
+ })();
+ /* eslint-enable max-nested-callbacks */
+
+ return [alloc1.line, alloc2.line, alloc3.line];
+ } +
+ ")()"
+ );
+ }
+ );
+
+ const response = await memory.getAllocations();
+
+ await memory.stopRecordingAllocations();
+ ok(true, "Can stop recording allocations");
+
+ // Filter out allocations by library and test code, and get only the
+ // allocations that occurred in our test case above.
+
+ function isTestAllocation(alloc) {
+ const frame = response.frames[alloc];
+ return (
+ frame &&
+ frame.functionDisplayName === "inner" &&
+ (frame.line === line1 || frame.line === line2 || frame.line === line3)
+ );
+ }
+
+ const testAllocations = response.allocations.filter(isTestAllocation);
+ Assert.greaterOrEqual(
+ testAllocations.length,
+ 3,
+ "Should find our 3 test allocations (plus some allocations for the error " +
+ "objects used to get line numbers)"
+ );
+
+ // For each of the test case's allocations, ensure that the parent frame
+ // indices are correct. Also test that we did get an allocation at each
+ // line we expected (rather than a bunch on the first line and none on the
+ // others, etc).
+
+ const expectedLines = new Set([line1, line2, line3]);
+ is(expectedLines.size, 3, "We are expecting 3 allocations");
+
+ for (const alloc of testAllocations) {
+ const innerFrame = response.frames[alloc];
+ ok(innerFrame, "Should get the inner frame");
+ is(innerFrame.functionDisplayName, "inner");
+ expectedLines.delete(innerFrame.line);
+
+ const middleFrame = response.frames[innerFrame.parent];
+ ok(middleFrame, "Should get the middle frame");
+ is(middleFrame.functionDisplayName, "middle");
+
+ const outerFrame = response.frames[middleFrame.parent];
+ ok(outerFrame, "Should get the outer frame");
+ is(outerFrame.functionDisplayName, "outer");
+
+ // Not going to test the rest of the frames because they are Task.jsm
+ // and promise frames and it gets gross. Plus, I wouldn't want this test
+ // to start failing if they changed their implementations in a way that
+ // added or removed stack frames here.
+ }
+
+ is(expectedLines.size, 0, "Should have found all the expected lines");
+
+ await memory.detach();
+
+ await target.destroy();
+});
diff --git a/devtools/server/tests/browser/browser_perf-01.js b/devtools/server/tests/browser/browser_perf-01.js
new file mode 100644
index 0000000000..96afc8151e
--- /dev/null
+++ b/devtools/server/tests/browser/browser_perf-01.js
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// This test is at the edge of timing out, probably because of LUL
+// initialization on Linux. This is also happening only once, which is why only
+// this test needs it: for other tests LUL is already initialized because
+// they're running in the same Firefox instance.
+// See also bug 1635442.
+requestLongerTimeout(2);
+
+/**
+ * Run through a series of basic recording actions for the perf actor.
+ */
+add_task(async function () {
+ const { front, client } = await initPerfFront();
+
+ // Assert the initial state.
+ is(
+ await front.isSupportedPlatform(),
+ true,
+ "This test only runs on supported platforms."
+ );
+ is(await front.isActive(), false, "The profiler is not active yet.");
+
+ // Start the profiler.
+ const profilerStarted = once(front, "profiler-started");
+ await front.startProfiler();
+ await profilerStarted;
+ is(await front.isActive(), true, "The profiler was started.");
+
+ // Stop the profiler and assert the results.
+ const profilerStopped1 = once(front, "profiler-stopped");
+ const profile = await front.getProfileAndStopProfiler();
+ await profilerStopped1;
+ is(await front.isActive(), false, "The profiler was stopped.");
+ ok("threads" in profile, "The actor was used to record a profile.");
+
+ // Restart the profiler.
+ await front.startProfiler();
+ is(await front.isActive(), true, "The profiler was re-started.");
+
+ // Stop and discard.
+ const profilerStopped2 = once(front, "profiler-stopped");
+ await front.stopProfilerAndDiscardProfile();
+ await profilerStopped2;
+ is(
+ await front.isActive(),
+ false,
+ "The profiler was stopped and the profile discarded."
+ );
+
+ // Clean up.
+ await front.destroy();
+ await client.close();
+});
diff --git a/devtools/server/tests/browser/browser_perf-02.js b/devtools/server/tests/browser/browser_perf-02.js
new file mode 100644
index 0000000000..c7276d8a3f
--- /dev/null
+++ b/devtools/server/tests/browser/browser_perf-02.js
@@ -0,0 +1,37 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+/**
+ * Test what happens when other tools control the profiler.
+ */
+add_task(async function () {
+ const { front, client } = await initPerfFront();
+
+ // Simulate other tools by getting an independent handle on the Gecko Profiler.
+ // eslint-disable-next-line mozilla/use-services
+ const geckoProfiler = Cc["@mozilla.org/tools/profiler;1"].getService(
+ Ci.nsIProfiler
+ );
+
+ is(await front.isActive(), false, "The profiler hasn't been started yet.");
+
+ // Start the profiler.
+ await front.startProfiler();
+ is(await front.isActive(), true, "The profiler was started.");
+
+ // Stop the profiler manually through the Gecko Profiler interface.
+ const profilerStopped = once(front, "profiler-stopped");
+ geckoProfiler.StopProfiler();
+ await profilerStopped;
+ is(
+ await front.isActive(),
+ false,
+ "The profiler was stopped by another tool."
+ );
+
+ // Clean up.
+ await front.destroy();
+ await client.close();
+});
diff --git a/devtools/server/tests/browser/browser_perf-04.js b/devtools/server/tests/browser/browser_perf-04.js
new file mode 100644
index 0000000000..9fba77d053
--- /dev/null
+++ b/devtools/server/tests/browser/browser_perf-04.js
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+/**
+ * Run through a series of basic recording actions for the perf actor.
+ */
+add_task(async function () {
+ const { front, client } = await initPerfFront();
+
+ // Assert the initial state.
+ is(
+ await front.isSupportedPlatform(),
+ true,
+ "This test only runs on supported platforms."
+ );
+ is(await front.isActive(), false, "The profiler is not active yet.");
+
+ // Getting the active Browser ID to assert in the "profiler-started" event.
+ const win = Services.wm.getMostRecentWindow("navigator:browser");
+ const activeTabID = win.gBrowser.selectedBrowser.browsingContext.browserId;
+
+ front.once(
+ "profiler-started",
+ (entries, interval, features, duration, activeTID) => {
+ is(entries, 1024, "Should apply entries by startProfiler");
+ is(interval, 0.1, "Should apply interval by startProfiler");
+ is(typeof features, "number", "Should apply features by startProfiler");
+ is(duration, 2, "Should apply duration by startProfiler");
+ is(
+ activeTID,
+ activeTabID,
+ "Should apply active browser ID by startProfiler"
+ );
+ }
+ );
+
+ // Start the profiler.
+ await front.startProfiler({
+ entries: 1000,
+ duration: 2,
+ interval: 0.1,
+ features: ["js", "stackwalk"],
+ });
+
+ is(await front.isActive(), true, "The profiler is active.");
+
+ // clean up
+ await front.stopProfilerAndDiscardProfile();
+ await front.destroy();
+ await client.close();
+});
diff --git a/devtools/server/tests/browser/browser_perf-getSupportedFeatures.js b/devtools/server/tests/browser/browser_perf-getSupportedFeatures.js
new file mode 100644
index 0000000000..331d6d329c
--- /dev/null
+++ b/devtools/server/tests/browser/browser_perf-getSupportedFeatures.js
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+add_task(async function () {
+ const { front, client } = await initPerfFront();
+
+ info("Get the supported features from the perf actor.");
+ const features = await front.getSupportedFeatures();
+
+ ok(Array.isArray(features), "The features are an array.");
+ ok(!!features.length, "There are many features supported.");
+ ok(
+ features.includes("js"),
+ "All platforms support the js feature, and it's in this list."
+ );
+
+ // clean up
+ await front.stopProfilerAndDiscardProfile();
+ await front.destroy();
+ await client.close();
+});
diff --git a/devtools/server/tests/browser/browser_storage_cookies-duplicate-names.js b/devtools/server/tests/browser/browser_storage_cookies-duplicate-names.js
new file mode 100644
index 0000000000..0342e1b896
--- /dev/null
+++ b/devtools/server/tests/browser/browser_storage_cookies-duplicate-names.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the storage panel is able to display multiple cookies with the same
+// name (and different paths).
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/server/tests/browser/storage-helpers.js",
+ this
+);
+
+const l10n = new Localization(["devtools/client/storage.ftl"], true);
+const sessionString = l10n.formatValueSync("storage-expires-session");
+
+const TESTDATA = {
+ "http://test1.example.org": [
+ {
+ name: "name",
+ value: "value1",
+ expires: 0,
+ path: "/",
+ host: "test1.example.org",
+ hostOnly: true,
+ isSecure: false,
+ },
+ {
+ name: "name",
+ value: "value2",
+ expires: 0,
+ path: "/path2/",
+ host: "test1.example.org",
+ hostOnly: true,
+ isSecure: false,
+ },
+ {
+ name: "name",
+ value: "value3",
+ expires: 0,
+ path: "/path3/",
+ host: "test1.example.org",
+ hostOnly: true,
+ isSecure: false,
+ },
+ ],
+};
+
+add_task(async function () {
+ const { commands } = await openTabAndSetupStorage(
+ MAIN_DOMAIN + "storage-cookies-same-name.html"
+ );
+
+ const { resourceCommand } = commands;
+ const { TYPES } = resourceCommand;
+ const data = {};
+ await resourceCommand.watchResources(
+ [TYPES.COOKIE, TYPES.LOCAL_STORAGE, TYPES.SESSION_STORAGE],
+ {
+ async onAvailable(resources) {
+ for (const resource of resources) {
+ const { resourceType } = resource;
+ if (!data[resourceType]) {
+ data[resourceType] = { hosts: {}, dataByHost: {} };
+ }
+
+ for (const host in resource.hosts) {
+ if (!data[resourceType].hosts[host]) {
+ data[resourceType].hosts[host] = [];
+ }
+ // For indexed DB, we have some values, the database names. Other are empty arrays.
+ const hostValues = resource.hosts[host];
+ data[resourceType].hosts[host].push(...hostValues);
+ data[resourceType].dataByHost[host] =
+ await resource.getStoreObjects(host, null, { sessionString });
+ }
+ }
+ },
+ }
+ );
+
+ ok(data.cookies, "Cookies storage actor is present");
+
+ await testCookies(data.cookies);
+ await clearStorage();
+
+ // Forcing GC/CC to get rid of docshells and windows created by this test.
+ forceCollections();
+ await commands.destroy();
+ forceCollections();
+});
+
+function testCookies({ hosts, dataByHost }) {
+ const numHosts = Object.keys(hosts).length;
+ is(numHosts, 1, "Correct number of host entries for cookies");
+ return testCookiesObjects(0, hosts, dataByHost);
+}
+
+var testCookiesObjects = async function (index, hosts, dataByHost) {
+ const host = Object.keys(hosts)[index];
+ const data = dataByHost[host];
+ is(
+ data.total,
+ TESTDATA[host].length,
+ "Number of cookies in host " + host + " matches"
+ );
+ for (const item of data.data) {
+ let found = false;
+ for (const toMatch of TESTDATA[host]) {
+ if (
+ item.name === toMatch.name &&
+ item.host === toMatch.host &&
+ item.path === toMatch.path
+ ) {
+ found = true;
+ ok(true, "Found cookie " + item.name + " in response");
+ is(item.value.str, toMatch.value, "The value matches.");
+ is(item.expires, toMatch.expires, "The expiry time matches.");
+ is(item.path, toMatch.path, "The path matches.");
+ is(item.host, toMatch.host, "The host matches.");
+ is(item.isSecure, toMatch.isSecure, "The isSecure value matches.");
+ is(item.hostOnly, toMatch.hostOnly, "The hostOnly value matches.");
+ break;
+ }
+ }
+ ok(found, "cookie " + item.name + " should exist in response");
+ }
+
+ ok(!!TESTDATA[host], "Host is present in the list : " + host);
+ if (index == Object.keys(hosts).length - 1) {
+ return;
+ }
+ await testCookiesObjects(++index, hosts, dataByHost);
+};
diff --git a/devtools/server/tests/browser/browser_storage_dynamic_windows.js b/devtools/server/tests/browser/browser_storage_dynamic_windows.js
new file mode 100644
index 0000000000..0417cf0f09
--- /dev/null
+++ b/devtools/server/tests/browser/browser_storage_dynamic_windows.js
@@ -0,0 +1,410 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/server/tests/browser/storage-helpers.js",
+ this
+);
+
+// beforeReload references an object representing the initialized state of the
+// storage actor.
+const beforeReload = {
+ cookies: {
+ "http://test1.example.org": ["c1", "cs2", "c3", "uc1"],
+ "http://sectest1.example.org": ["uc1", "cs2"],
+ },
+ "indexed-db": {
+ "http://test1.example.org": [
+ JSON.stringify(["idb1", "obj1"]),
+ JSON.stringify(["idb1", "obj2"]),
+ JSON.stringify(["idb2", "obj3"]),
+ ],
+ "http://sectest1.example.org": [],
+ },
+ "local-storage": {
+ "http://test1.example.org": ["ls1", "ls2"],
+ "http://sectest1.example.org": ["iframe-u-ls1"],
+ },
+ "session-storage": {
+ "http://test1.example.org": ["ss1"],
+ "http://sectest1.example.org": ["iframe-u-ss1", "iframe-u-ss2"],
+ },
+};
+
+// afterIframeAdded references the items added when an iframe containing storage
+// items is added to the page.
+const afterIframeAdded = {
+ cookies: {
+ "https://sectest1.example.org": [
+ getCookieId("cs2", ".example.org", "/"),
+ getCookieId(
+ "sc1",
+ "sectest1.example.org",
+ "/browser/devtools/server/tests/browser"
+ ),
+ ],
+ "http://sectest1.example.org": [
+ getCookieId(
+ "sc1",
+ "sectest1.example.org",
+ "/browser/devtools/server/tests/browser"
+ ),
+ ],
+ },
+ "indexed-db": {
+ // empty because indexed db creation happens after the page load, so at
+ // the time of window-ready, there was no indexed db present.
+ "https://sectest1.example.org": [],
+ },
+ "local-storage": {
+ "https://sectest1.example.org": ["iframe-s-ls1"],
+ },
+ "session-storage": {
+ "https://sectest1.example.org": ["iframe-s-ss1"],
+ },
+};
+
+// afterIframeRemoved references the items deleted when an iframe containing
+// storage items is removed from the page.
+const afterIframeRemoved = {
+ cookies: {
+ "http://sectest1.example.org": [],
+ },
+ "indexed-db": {
+ "http://sectest1.example.org": [],
+ },
+ "local-storage": {
+ "http://sectest1.example.org": [],
+ },
+ "session-storage": {
+ "http://sectest1.example.org": [],
+ },
+};
+
+add_task(async function () {
+ const { commands } = await openTabAndSetupStorage(
+ MAIN_DOMAIN + "storage-dynamic-windows.html"
+ );
+
+ const { resourceCommand } = commands;
+ const { TYPES } = resourceCommand;
+ const allResources = {};
+ const onAvailable = resources => {
+ for (const resource of resources) {
+ is(
+ resource.targetFront.targetType,
+ commands.targetCommand.TYPES.FRAME,
+ "Each storage resource has a valid 'targetFront' attribute"
+ );
+ // Because we have iframes, we have distinct targets, each spawning their own storage resource
+ if (allResources[resource.resourceType]) {
+ allResources[resource.resourceType].push(resource);
+ } else {
+ allResources[resource.resourceType] = [resource];
+ }
+ }
+ };
+ const parentProcessStorages = [TYPES.COOKIE, TYPES.INDEXED_DB];
+ const contentProcessStorages = [TYPES.LOCAL_STORAGE, TYPES.SESSION_STORAGE];
+ const allStorages = [...parentProcessStorages, ...contentProcessStorages];
+ await resourceCommand.watchResources(allStorages, { onAvailable });
+ is(
+ Object.keys(allStorages).length,
+ allStorages.length,
+ "Got all the storage resources"
+ );
+
+ // Do a copy of all the initial storages as test function may spawn new resources for the same
+ // type and override the initial ones.
+ // We do not call unwatchResources as it would clear its cache and next call
+ // to watchResources with ignoreExistingResources would break and reprocess all resources again.
+ const initialResources = Object.assign({}, allResources);
+
+ testWindowsBeforeReload(initialResources);
+
+ await testAddIframe(commands, initialResources, {
+ contentProcessStorages,
+ parentProcessStorages,
+ allStorages,
+ });
+
+ await testRemoveIframe(commands, initialResources, {
+ contentProcessStorages,
+ parentProcessStorages,
+ allStorages,
+ });
+
+ await clearStorage();
+
+ // Forcing GC/CC to get rid of docshells and windows created by this test.
+ forceCollections();
+ await commands.destroy();
+ forceCollections();
+});
+
+function testWindowsBeforeReload(resources) {
+ for (const storageType in beforeReload) {
+ ok(resources[storageType], `${storageType} storage actor is present`);
+
+ const hosts = {};
+ for (const resource of resources[storageType]) {
+ for (const [hostType, hostValues] of Object.entries(resource.hosts)) {
+ if (!hosts[hostType]) {
+ hosts[hostType] = [];
+ }
+
+ hosts[hostType].push(hostValues);
+ }
+ }
+
+ // If this test is run with chrome debugging enabled we get an extra
+ // key for "chrome". We don't want the test to fail in this case, so
+ // ignore it.
+ if (storageType == "indexedDB") {
+ delete hosts.chrome;
+ }
+
+ is(
+ Object.keys(hosts).length,
+ Object.keys(beforeReload[storageType]).length,
+ `Number of hosts for ${storageType} match`
+ );
+ for (const host in beforeReload[storageType]) {
+ ok(hosts[host], `Host ${host} is present`);
+ }
+ }
+}
+
+/**
+ * Wait for new storage resources to be created of the given types.
+ */
+async function waitForNewResourcesAndUpdates(commands, resourceTypes) {
+ // When fission is off, we don't expect any new resource
+ if (resourceTypes.length === 0) {
+ return { newResources: [], updates: [] };
+ }
+ const { resourceCommand } = commands;
+ let resolve;
+ const promise = new Promise(r => (resolve = r));
+ const allResources = {};
+ const allUpdates = {};
+ const onAvailable = resources => {
+ for (const resource of resources) {
+ if (resource.resourceType in allResources) {
+ ok(false, `Got multiple ${resource.resourceTypes} resources`);
+ }
+ allResources[resource.resourceType] = resource;
+ ok(true, `Got resource for ${resource.resourceType}`);
+
+ // Stop watching for resources when we got them all
+ if (Object.keys(allResources).length == resourceTypes.length) {
+ resourceCommand.unwatchResources(resourceTypes, {
+ onAvailable,
+ });
+ }
+
+ // But also listen for updates on each new resource
+ resource.once("single-store-update").then(update => {
+ ok(true, `Got updates for ${resource.resourceType}`);
+ allUpdates[resource.resourceType] = update;
+
+ // Resolve only once we got all the updates, for all the resources
+ if (Object.keys(allUpdates).length == resourceTypes.length) {
+ resolve({ newResources: allResources, updates: allUpdates });
+ }
+ });
+ }
+ };
+ await resourceCommand.watchResources(resourceTypes, {
+ onAvailable,
+ ignoreExistingResources: true,
+ });
+ return promise;
+}
+
+/**
+ * Wait for single-store-update events on all the given storage resources.
+ */
+function waitForResourceUpdates(resources, resourceTypes) {
+ const allUpdates = {};
+ const promises = [];
+ for (const type of resourceTypes) {
+ // Resolves once any of the many resources for the given storage type updates
+ const promise = Promise.any(
+ resources[type].map(resource => resource.once("single-store-update"))
+ );
+ promise.then(update => {
+ ok(true, `Got updates for ${type}`);
+ allUpdates[type] = update;
+ });
+ promises.push(promise);
+ }
+ return Promise.all(promises).then(() => allUpdates);
+}
+
+async function testAddIframe(
+ commands,
+ resources,
+ { contentProcessStorages, parentProcessStorages, allStorages }
+) {
+ info("Testing if new iframe addition works properly");
+
+ // If Fission or EFT is enabled:
+ // * we get new resources alongside single-store-update events for content process storages
+ // * only single-store-update events for previous resources for parent process storages
+ // Otherwise if fission is disables:
+ // * we get single-store-update events for all previous resources
+ const onResources = waitForNewResourcesAndUpdates(
+ commands,
+ isFissionEnabled() || isEveryFrameTargetEnabled()
+ ? contentProcessStorages
+ : []
+ );
+ // If fission or EFT is enabled, we only get update for parent process storages.
+ // The content process storage resources are notified via brand new resource instances.
+ const storagesWithUpdates =
+ isFissionEnabled() || isEveryFrameTargetEnabled()
+ ? parentProcessStorages
+ : allStorages;
+ const onUpdates = waitForResourceUpdates(resources, storagesWithUpdates);
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [ALT_DOMAIN_SECURED],
+ secured => {
+ const doc = content.document;
+
+ const iframe = doc.createElement("iframe");
+ iframe.src = secured + "storage-secured-iframe.html";
+
+ doc.querySelector("body").appendChild(iframe);
+ }
+ );
+
+ info("Wait for all resources");
+ const { newResources, updates } = await onResources;
+ info("Wait for all updates");
+ const previousResourceUpdates = await onUpdates;
+
+ if (isFissionEnabled() || isEveryFrameTargetEnabled()) {
+ for (const resourceType of contentProcessStorages) {
+ const resource = newResources[resourceType];
+ const expected = afterIframeAdded[resourceType];
+ // The resource only comes with hosts, without any values.
+ // Each host will be an empty array.
+ Assert.deepEqual(
+ Object.keys(resource.hosts),
+ Object.keys(expected),
+ `List of hosts for resource ${resourceType} is correct`
+ );
+ for (const host in resource.hosts) {
+ is(
+ resource.hosts[host].length,
+ 0,
+ "For new resources, each host has no value and is an empty array"
+ );
+ }
+ const update = updates[resourceType];
+ const storageKey = resourceTypeToStorageKey(resourceType);
+ Assert.deepEqual(
+ update.added[storageKey],
+ expected,
+ "We get an update after the resource, with the host values"
+ );
+ }
+ }
+
+ for (const resourceType of storagesWithUpdates) {
+ const expected = afterIframeAdded[resourceType];
+ const update = previousResourceUpdates[resourceType];
+ const storageKey = resourceTypeToStorageKey(resourceType);
+ Assert.deepEqual(
+ update.added[storageKey],
+ expected,
+ `We get an update after the resource ${resourceType}, with the host values`
+ );
+ }
+
+ return newResources;
+}
+
+async function testRemoveIframe(
+ commands,
+ resources,
+ { contentProcessStorages, parentProcessStorages, allStorages }
+) {
+ info("Testing if iframe removal works properly");
+
+ // If fission or EFT is enabled, we only get update for parent process storages.
+ // The content process storage resources are wiped via their related target destruction.
+ const onUpdates = waitForResourceUpdates(
+ resources,
+ isFissionEnabled() || isEveryFrameTargetEnabled()
+ ? parentProcessStorages
+ : allStorages
+ );
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ for (const iframe of content.document.querySelectorAll("iframe")) {
+ if (iframe.src.startsWith("http:")) {
+ iframe.remove();
+ break;
+ }
+ }
+ });
+
+ info("Wait for all updates");
+ const previousResourceUpdates = await onUpdates;
+
+ const storagesWithUpdates =
+ isFissionEnabled() || isEveryFrameTargetEnabled()
+ ? parentProcessStorages
+ : allStorages;
+ for (const resourceType of storagesWithUpdates) {
+ const expected = afterIframeRemoved[resourceType];
+ const update = previousResourceUpdates[resourceType];
+ const storageKey = resourceTypeToStorageKey(resourceType);
+ Assert.deepEqual(
+ update.deleted[storageKey],
+ expected,
+ `We get an update after the resource ${resourceType}, with the host values`
+ );
+ }
+
+ // With Fission or EFT, the iframe target is destroyed,
+ // which ends up destroying the related resources
+ if (isFissionEnabled() || isEveryFrameTargetEnabled()) {
+ const destroyedResourceTypes = [];
+ for (const storageType in resources) {
+ for (const resource of resources[storageType]) {
+ if (resource.isDestroyed()) {
+ destroyedResourceTypes.push(resource.resourceType);
+ }
+ }
+ }
+ Assert.deepEqual(
+ destroyedResourceTypes.sort(),
+ contentProcessStorages.sort(),
+ "Content process storage resources have been destroyed [local and session storages]"
+ );
+ }
+}
+
+/**
+ * single-store-update emits objects using attributes with old "storage key" namings,
+ * which is different from resource type namings.
+ */
+function resourceTypeToStorageKey(resourceType) {
+ if (resourceType == "local-storage") {
+ return "localStorage";
+ }
+ if (resourceType == "session-storage") {
+ return "sessionStorage";
+ }
+ if (resourceType == "indexed-db") {
+ return "indexedDB";
+ }
+ return resourceType;
+}
diff --git a/devtools/server/tests/browser/browser_storage_listings.js b/devtools/server/tests/browser/browser_storage_listings.js
new file mode 100644
index 0000000000..40365ede85
--- /dev/null
+++ b/devtools/server/tests/browser/browser_storage_listings.js
@@ -0,0 +1,743 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/server/tests/browser/storage-helpers.js",
+ this
+);
+
+const l10n = new Localization(["devtools/client/storage.ftl"], true);
+const sessionString = l10n.formatValueSync("storage-expires-session");
+
+const storeMap = {
+ cookies: {
+ "http://test1.example.org": [
+ {
+ name: "c1",
+ value: "foobar",
+ expires: 2000000000000,
+ path: "/browser",
+ host: "test1.example.org",
+ hostOnly: true,
+ isSecure: false,
+ },
+ {
+ name: "cs2",
+ value: "sessionCookie",
+ path: "/",
+ host: ".example.org",
+ expires: 0,
+ hostOnly: false,
+ isSecure: false,
+ },
+ {
+ name: "c3",
+ value: "foobar-2",
+ expires: 2000000001000,
+ path: "/",
+ host: "test1.example.org",
+ hostOnly: true,
+ isSecure: true,
+ },
+ ],
+
+ "http://sectest1.example.org": [
+ {
+ name: "cs2",
+ value: "sessionCookie",
+ path: "/",
+ host: ".example.org",
+ expires: 0,
+ hostOnly: false,
+ isSecure: false,
+ },
+ {
+ name: "sc1",
+ value: "foobar",
+ path: "/browser/devtools/server/tests/browser",
+ host: "sectest1.example.org",
+ expires: 0,
+ hostOnly: true,
+ isSecure: false,
+ },
+ ],
+
+ "https://sectest1.example.org": [
+ {
+ name: "uc1",
+ value: "foobar",
+ host: ".example.org",
+ path: "/",
+ expires: 0,
+ hostOnly: false,
+ isSecure: true,
+ },
+ {
+ name: "cs2",
+ value: "sessionCookie",
+ path: "/",
+ host: ".example.org",
+ expires: 0,
+ hostOnly: false,
+ isSecure: false,
+ },
+ {
+ name: "sc1",
+ value: "foobar",
+ path: "/browser/devtools/server/tests/browser",
+ host: "sectest1.example.org",
+ expires: 0,
+ hostOnly: true,
+ isSecure: false,
+ },
+ ],
+ },
+ "local-storage": {
+ "http://test1.example.org": [
+ {
+ name: "ls1",
+ value: "foobar",
+ },
+ {
+ name: "ls2",
+ value: "foobar-2",
+ },
+ ],
+ "http://sectest1.example.org": [
+ {
+ name: "iframe-u-ls1",
+ value: "foobar",
+ },
+ ],
+ "https://sectest1.example.org": [
+ {
+ name: "iframe-s-ls1",
+ value: "foobar",
+ },
+ ],
+ },
+ "session-storage": {
+ "http://test1.example.org": [
+ {
+ name: "ss1",
+ value: "foobar-3",
+ },
+ ],
+ "http://sectest1.example.org": [
+ {
+ name: "iframe-u-ss1",
+ value: "foobar1",
+ },
+ {
+ name: "iframe-u-ss2",
+ value: "foobar2",
+ },
+ ],
+ "https://sectest1.example.org": [
+ {
+ name: "iframe-s-ss1",
+ value: "foobar-2",
+ },
+ ],
+ },
+};
+
+const IDBValues = {
+ listStoresResponse: {
+ "http://test1.example.org": [
+ ["idb1 (default)", "obj1"],
+ ["idb1 (default)", "obj2"],
+ ["idb2 (default)", "obj3"],
+ ],
+ "http://sectest1.example.org": [],
+ "https://sectest1.example.org": [
+ ["idb-s1 (default)", "obj-s1"],
+ ["idb-s2 (default)", "obj-s2"],
+ ],
+ },
+ dbDetails: {
+ "http://test1.example.org": [
+ {
+ db: "idb1 (default)",
+ origin: "http://test1.example.org",
+ version: 1,
+ objectStores: 2,
+ },
+ {
+ db: "idb2 (default)",
+ origin: "http://test1.example.org",
+ version: 1,
+ objectStores: 1,
+ },
+ ],
+ "http://sectest1.example.org": [],
+ "https://sectest1.example.org": [
+ {
+ db: "idb-s1 (default)",
+ origin: "https://sectest1.example.org",
+ version: 1,
+ objectStores: 1,
+ },
+ {
+ db: "idb-s2 (default)",
+ origin: "https://sectest1.example.org",
+ version: 1,
+ objectStores: 1,
+ },
+ ],
+ },
+ objectStoreDetails: {
+ "http://test1.example.org": {
+ "idb1 (default)": [
+ {
+ objectStore: "obj1",
+ keyPath: "id",
+ autoIncrement: false,
+ indexes: [
+ {
+ name: "name",
+ keyPath: "name",
+ unique: false,
+ multiEntry: false,
+ },
+ {
+ name: "email",
+ keyPath: "email",
+ unique: true,
+ multiEntry: false,
+ },
+ ],
+ },
+ {
+ objectStore: "obj2",
+ keyPath: "id2",
+ autoIncrement: false,
+ indexes: [],
+ },
+ ],
+ "idb2 (default)": [
+ {
+ objectStore: "obj3",
+ keyPath: "id3",
+ autoIncrement: false,
+ indexes: [
+ {
+ name: "name2",
+ keyPath: "name2",
+ unique: true,
+ multiEntry: false,
+ },
+ ],
+ },
+ ],
+ },
+ "http://sectest1.example.org": {},
+ "https://sectest1.example.org": {
+ "idb-s1 (default)": [
+ {
+ objectStore: "obj-s1",
+ keyPath: "id",
+ autoIncrement: false,
+ indexes: [],
+ },
+ ],
+ "idb-s2 (default)": [
+ {
+ objectStore: "obj-s2",
+ keyPath: "id3",
+ autoIncrement: true,
+ indexes: [
+ {
+ name: "name2",
+ keyPath: "name2",
+ unique: true,
+ multiEntry: false,
+ },
+ ],
+ },
+ ],
+ },
+ },
+ entries: {
+ "http://test1.example.org": {
+ "idb1 (default)#obj1": [
+ {
+ name: 1,
+ value: {
+ id: 1,
+ name: "foo",
+ email: "foo@bar.com",
+ },
+ },
+ {
+ name: 2,
+ value: {
+ id: 2,
+ name: "foo2",
+ email: "foo2@bar.com",
+ },
+ },
+ {
+ name: 3,
+ value: {
+ id: 3,
+ name: "foo2",
+ email: "foo3@bar.com",
+ },
+ },
+ ],
+ "idb1 (default)#obj2": [
+ {
+ name: 1,
+ value: {
+ id2: 1,
+ name: "foo",
+ email: "foo@bar.com",
+ extra: "baz",
+ },
+ },
+ ],
+ "idb2 (default)#obj3": [],
+ },
+ "http://sectest1.example.org": {},
+ "https://sectest1.example.org": {
+ "idb-s1 (default)#obj-s1": [
+ {
+ name: 6,
+ value: {
+ id: 6,
+ name: "foo",
+ email: "foo@bar.com",
+ },
+ },
+ {
+ name: 7,
+ value: {
+ id: 7,
+ name: "foo2",
+ email: "foo2@bar.com",
+ },
+ },
+ ],
+ "idb-s2 (default)#obj-s2": [
+ {
+ name: 13,
+ value: {
+ id2: 13,
+ name2: "foo",
+ email: "foo@bar.com",
+ },
+ },
+ ],
+ },
+ },
+};
+
+async function testStores(commands) {
+ const { resourceCommand } = commands;
+ const { TYPES } = resourceCommand;
+ /**
+ * Data is a dictionary whose keys are storage types (their resourceType)
+ * while values are objects with following attributes:
+ * - hosts: dictionary of storage values (values are specific to each storage type)
+ * keyed by host names.
+ * - dataByHost: dictionary of storage objects keyed by host names.
+ * storages objects are returned by StorageActor.getStoreObjects.
+ * For IndexedDB it is different, instead it is still a dictionary
+ * keyed by host names, but each value is yet another sub dictionary with
+ * a special "main" attribute, with global store objects.
+ * Then, there will be one key per idb database, with their store objects
+ * as value.
+ */
+ const data = {};
+ await resourceCommand.watchResources(
+ [
+ TYPES.COOKIE,
+ TYPES.LOCAL_STORAGE,
+ TYPES.SESSION_STORAGE,
+ TYPES.INDEXED_DB,
+ ],
+ {
+ async onAvailable(resources) {
+ for (const resource of resources) {
+ const { resourceType } = resource;
+ if (!data[resourceType]) {
+ data[resourceType] = { hosts: {}, dataByHost: {} };
+ }
+
+ for (const host in resource.hosts) {
+ if (!data[resourceType].hosts[host]) {
+ data[resourceType].hosts[host] = [];
+ }
+ // For indexed DB, we have some values, the database names. Other are empty arrays.
+ const hostValues = resource.hosts[host];
+ data[resourceType].hosts[host].push(...hostValues);
+
+ // For INDEXED_DB, it is slightly more complex, as we may have 3 store per host,
+ if (resourceType == TYPES.INDEXED_DB) {
+ if (!data[resourceType].dataByHost[host]) {
+ data[resourceType].dataByHost[host] = {};
+ }
+ data[resourceType].dataByHost[host].main =
+ await resource.getStoreObjects(host, null, {
+ sessionString,
+ });
+ for (const name of resource.hosts[host]) {
+ const objName = JSON.parse(name).slice(0, 1);
+ data[resourceType].dataByHost[host][objName] =
+ await resource.getStoreObjects(
+ host,
+ [JSON.stringify(objName)],
+ { sessionString }
+ );
+ data[resourceType].dataByHost[host][name] =
+ await resource.getStoreObjects(host, [name], {
+ sessionString,
+ });
+ }
+ } else {
+ data[resourceType].dataByHost[host] =
+ await resource.getStoreObjects(host, null, { sessionString });
+ }
+ }
+ }
+ },
+ }
+ );
+
+ await testCookies(data.cookies);
+ await testLocalStorage(data["local-storage"]);
+ await testSessionStorage(data["session-storage"]);
+ await testIndexedDB(data["indexed-db"]);
+}
+
+function testCookies({ hosts, dataByHost }) {
+ is(
+ Object.keys(hosts).length,
+ 3,
+ "Correct number of host entries for cookies"
+ );
+ return testCookiesObjects(0, hosts, dataByHost);
+}
+
+async function testCookiesObjects(index, hosts, dataByHost) {
+ const host = Object.keys(hosts)[index];
+ ok(!!storeMap.cookies[host], "Host is present in the list : " + host);
+ const data = dataByHost[host];
+ let cookiesLength = 0;
+ for (const secureCookie of storeMap.cookies[host]) {
+ if (secureCookie.isSecure) {
+ ++cookiesLength;
+ }
+ }
+ // Any secure cookies did not get stored in the database.
+ is(
+ data.total,
+ storeMap.cookies[host].length - cookiesLength,
+ "Number of cookies in host " + host + " matches"
+ );
+ for (const item of data.data) {
+ let found = false;
+ for (const toMatch of storeMap.cookies[host]) {
+ if (item.name == toMatch.name) {
+ found = true;
+ ok(true, "Found cookie " + item.name + " in response");
+ is(item.value.str, toMatch.value, "The value matches.");
+ is(item.expires, toMatch.expires, "The expiry time matches.");
+ is(item.path, toMatch.path, "The path matches.");
+ is(item.host, toMatch.host, "The host matches.");
+ is(item.isSecure, toMatch.isSecure, "The isSecure value matches.");
+ is(item.hostOnly, toMatch.hostOnly, "The hostOnly value matches.");
+ break;
+ }
+ }
+ ok(found, "cookie " + item.name + " should exist in response");
+ }
+
+ if (index == Object.keys(hosts).length - 1) {
+ return;
+ }
+ await testCookiesObjects(++index, hosts, dataByHost);
+}
+
+function testLocalStorage({ hosts, dataByHost }) {
+ is(
+ Object.keys(hosts).length,
+ 3,
+ "Correct number of host entries for local storage"
+ );
+ return testLocalStorageObjects(0, hosts, dataByHost);
+}
+
+var testLocalStorageObjects = async function (index, hosts, dataByHost) {
+ const host = Object.keys(hosts)[index];
+ ok(
+ !!storeMap["local-storage"][host],
+ "Host is present in the list : " + host
+ );
+ const data = dataByHost[host];
+ is(
+ data.total,
+ storeMap["local-storage"][host].length,
+ "Number of local storage items in host " + host + " matches"
+ );
+ for (const item of data.data) {
+ let found = false;
+ for (const toMatch of storeMap["local-storage"][host]) {
+ if (item.name == toMatch.name) {
+ found = true;
+ ok(true, "Found local storage item " + item.name + " in response");
+ is(item.value.str, toMatch.value, "The value matches.");
+ break;
+ }
+ }
+ ok(found, "local storage item " + item.name + " should exist in response");
+ }
+
+ if (index == Object.keys(hosts).length - 1) {
+ return;
+ }
+ await testLocalStorageObjects(++index, hosts, dataByHost);
+};
+
+function testSessionStorage({ hosts, dataByHost }) {
+ is(
+ Object.keys(hosts).length,
+ 3,
+ "Correct number of host entries for session storage"
+ );
+ return testSessionStorageObjects(0, hosts, dataByHost);
+}
+
+async function testSessionStorageObjects(index, hosts, dataByHost) {
+ const host = Object.keys(hosts)[index];
+ ok(
+ !!storeMap["session-storage"][host],
+ "Host is present in the list : " + host
+ );
+ const data = dataByHost[host];
+ is(
+ data.total,
+ storeMap["session-storage"][host].length,
+ "Number of session storage items in host " + host + " matches"
+ );
+ for (const item of data.data) {
+ let found = false;
+ for (const toMatch of storeMap["session-storage"][host]) {
+ if (item.name == toMatch.name) {
+ found = true;
+ ok(true, "Found session storage item " + item.name + " in response");
+ is(item.value.str, toMatch.value, "The value matches.");
+ break;
+ }
+ }
+ ok(
+ found,
+ "session storage item " + item.name + " should exist in response"
+ );
+ }
+
+ if (index == Object.keys(hosts).length - 1) {
+ return;
+ }
+ await testSessionStorageObjects(++index, hosts, dataByHost);
+}
+
+async function testIndexedDB({ hosts, dataByHost }) {
+ is(
+ Object.keys(hosts).length,
+ 3,
+ "Correct number of host entries for indexed db"
+ );
+
+ for (const host in hosts) {
+ for (const item of hosts[host]) {
+ const parsedItem = JSON.parse(item);
+ let found = false;
+ for (const toMatch of IDBValues.listStoresResponse[host]) {
+ if (toMatch[0] == parsedItem[0] && toMatch[1] == parsedItem[1]) {
+ found = true;
+ break;
+ }
+ }
+ ok(found, item + " should exist in list stores response");
+ }
+ }
+
+ await testIndexedDBs(0, hosts, dataByHost);
+ await testObjectStores(0, hosts, dataByHost);
+ await testIDBEntries(0, hosts, dataByHost);
+}
+
+async function testIndexedDBs(index, hosts, dataByHost) {
+ const host = Object.keys(hosts)[index];
+ const data = dataByHost[host].main;
+ is(
+ data.total,
+ IDBValues.dbDetails[host].length,
+ "Number of indexed db in host " + host + " matches"
+ );
+ for (const item of data.data) {
+ let found = false;
+ for (const toMatch of IDBValues.dbDetails[host]) {
+ if (item.uniqueKey == toMatch.db) {
+ found = true;
+ ok(true, "Found indexed db " + item.uniqueKey + " in response");
+ is(item.origin, toMatch.origin, "The origin matches.");
+ is(item.version, toMatch.version, "The version matches.");
+ is(
+ item.objectStores,
+ toMatch.objectStores,
+ "The number of object stores matches."
+ );
+ break;
+ }
+ }
+ ok(found, "indexed db " + item.uniqueKey + " should exist in response");
+ }
+
+ ok(!!IDBValues.dbDetails[host], "Host is present in the list : " + host);
+ if (index == Object.keys(hosts).length - 1) {
+ return;
+ }
+ await testIndexedDBs(++index, hosts, dataByHost);
+}
+
+async function testObjectStores(ix, hosts, dataByHost) {
+ const host = Object.keys(hosts)[ix];
+ const matchItems = (data, db) => {
+ is(
+ data.total,
+ IDBValues.objectStoreDetails[host][db].length,
+ "Number of object stores in host " + host + " matches"
+ );
+ for (const item of data.data) {
+ let found = false;
+ for (const toMatch of IDBValues.objectStoreDetails[host][db]) {
+ if (item.objectStore == toMatch.objectStore) {
+ found = true;
+ ok(true, "Found object store " + item.objectStore + " in response");
+ is(item.keyPath, toMatch.keyPath, "The keyPath matches.");
+ is(
+ item.autoIncrement,
+ toMatch.autoIncrement,
+ "The autoIncrement matches."
+ );
+ // We might already have parsed the JSON value, in which case this will no longer be a string
+ item.indexes =
+ typeof item.indexes == "string"
+ ? JSON.parse(item.indexes)
+ : item.indexes;
+ is(
+ item.indexes.length,
+ toMatch.indexes.length,
+ "Number of indexes match"
+ );
+ for (const index of item.indexes) {
+ let indexFound = false;
+ for (const toMatchIndex of toMatch.indexes) {
+ if (toMatchIndex.name == index.name) {
+ indexFound = true;
+ ok(true, "Found index " + index.name);
+ is(
+ index.keyPath,
+ toMatchIndex.keyPath,
+ "The keyPath of index matches."
+ );
+ is(index.unique, toMatchIndex.unique, "The unique matches");
+ is(
+ index.multiEntry,
+ toMatchIndex.multiEntry,
+ "The multiEntry matches"
+ );
+ break;
+ }
+ }
+ ok(indexFound, "Index " + index + " should exist in response");
+ }
+ break;
+ }
+ }
+ ok(found, "indexed db " + item.name + " should exist in response");
+ }
+ };
+
+ ok(
+ !!IDBValues.objectStoreDetails[host],
+ "Host is present in the list : " + host
+ );
+ for (const name of hosts[host]) {
+ const objName = JSON.parse(name).slice(0, 1);
+ matchItems(dataByHost[host][objName], objName[0]);
+ }
+ if (ix == Object.keys(hosts).length - 1) {
+ return;
+ }
+ await testObjectStores(++ix, hosts, dataByHost);
+}
+
+async function testIDBEntries(index, hosts, dataByHost) {
+ const host = Object.keys(hosts)[index];
+ const matchItems = (data, obj) => {
+ is(
+ data.total,
+ IDBValues.entries[host][obj].length,
+ "Number of items in object store " + obj + " matches"
+ );
+ for (const item of data.data) {
+ let found = false;
+ for (const toMatch of IDBValues.entries[host][obj]) {
+ if (item.name == toMatch.name) {
+ found = true;
+ ok(true, "Found indexed db item " + item.name + " in response");
+ const value = JSON.parse(item.value.str);
+ is(
+ Object.keys(value).length,
+ Object.keys(toMatch.value).length,
+ "Number of entries in the value matches"
+ );
+ for (const key in value) {
+ is(
+ value[key],
+ toMatch.value[key],
+ "value of " + key + " value key matches"
+ );
+ }
+ break;
+ }
+ }
+ ok(found, "indexed db item " + item.name + " should exist in response");
+ }
+ };
+
+ ok(!!IDBValues.entries[host], "Host is present in the list : " + host);
+ for (const name of hosts[host]) {
+ const parsed = JSON.parse(name);
+ matchItems(dataByHost[host][name], parsed[0] + "#" + parsed[1]);
+ }
+ if (index == Object.keys(hosts).length - 1) {
+ return;
+ }
+ await testObjectStores(++index, hosts, dataByHost);
+}
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.documentCookies.maxage", 0]],
+ });
+
+ const { commands } = await openTabAndSetupStorage(
+ MAIN_DOMAIN + "storage-listings.html"
+ );
+
+ await testStores(commands);
+
+ await clearStorage();
+
+ // Forcing GC/CC to get rid of docshells and windows created by this test.
+ forceCollections();
+ await commands.destroy();
+ forceCollections();
+});
diff --git a/devtools/server/tests/browser/browser_storage_updates.js b/devtools/server/tests/browser/browser_storage_updates.js
new file mode 100644
index 0000000000..50926538a5
--- /dev/null
+++ b/devtools/server/tests/browser/browser_storage_updates.js
@@ -0,0 +1,343 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Ensure that storage updates are detected and that the correct information is
+// contained inside the storage actors.
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/server/tests/browser/storage-helpers.js",
+ this
+);
+
+const l10n = new Localization(["devtools/client/storage.ftl"], true);
+const sessionString = l10n.formatValueSync("storage-expires-session");
+
+const TESTS = [
+ // index 0
+ {
+ async action(win) {
+ await addCookie("c1", "foobar1");
+ await addCookie("c2", "foobar2");
+ await localStorageSetItem("l1", "foobar1");
+ },
+ snapshot: {
+ cookies: [
+ {
+ name: "c1",
+ value: "foobar1",
+ },
+ {
+ name: "c2",
+ value: "foobar2",
+ },
+ ],
+ "local-storage": [
+ {
+ name: "l1",
+ value: "foobar1",
+ },
+ ],
+ },
+ },
+
+ // index 1
+ {
+ async action() {
+ await addCookie("c1", "new_foobar1");
+ await localStorageSetItem("l2", "foobar2");
+ },
+ snapshot: {
+ cookies: [
+ {
+ name: "c1",
+ value: "new_foobar1",
+ },
+ {
+ name: "c2",
+ value: "foobar2",
+ },
+ ],
+ "local-storage": [
+ {
+ name: "l1",
+ value: "foobar1",
+ },
+ {
+ name: "l2",
+ value: "foobar2",
+ },
+ ],
+ },
+ },
+
+ // index 2
+ {
+ async action() {
+ await removeCookie("c2");
+ await localStorageRemoveItem("l1");
+ await localStorageSetItem("l3", "foobar3");
+ },
+ snapshot: {
+ cookies: [
+ {
+ name: "c1",
+ value: "new_foobar1",
+ },
+ ],
+ "local-storage": [
+ {
+ name: "l2",
+ value: "foobar2",
+ },
+ {
+ name: "l3",
+ value: "foobar3",
+ },
+ ],
+ },
+ },
+
+ // index 3
+ {
+ async action() {
+ await removeCookie("c1");
+ await addCookie("c3", "foobar3");
+ await localStorageRemoveItem("l2");
+ await sessionStorageSetItem("s1", "foobar1");
+ await sessionStorageSetItem("s2", "foobar2");
+ await localStorageSetItem("l3", "new_foobar3");
+ },
+ snapshot: {
+ cookies: [
+ {
+ name: "c3",
+ value: "foobar3",
+ },
+ ],
+ "local-storage": [
+ {
+ name: "l3",
+ value: "new_foobar3",
+ },
+ ],
+ "session-storage": [
+ {
+ name: "s1",
+ value: "foobar1",
+ },
+ {
+ name: "s2",
+ value: "foobar2",
+ },
+ ],
+ },
+ },
+
+ // index 4
+ {
+ async action() {
+ await sessionStorageRemoveItem("s1");
+ },
+ snapshot: {
+ cookies: [
+ {
+ name: "c3",
+ value: "foobar3",
+ },
+ ],
+ "local-storage": [
+ {
+ name: "l3",
+ value: "new_foobar3",
+ },
+ ],
+ "session-storage": [
+ {
+ name: "s2",
+ value: "foobar2",
+ },
+ ],
+ },
+ },
+
+ // index 5
+ {
+ async action() {
+ await clearCookies();
+ },
+ snapshot: {
+ cookies: [],
+ "local-storage": [
+ {
+ name: "l3",
+ value: "new_foobar3",
+ },
+ ],
+ "session-storage": [
+ {
+ name: "s2",
+ value: "foobar2",
+ },
+ ],
+ },
+ },
+
+ // index 6
+ {
+ async action() {
+ await clearLocalAndSessionStores();
+ },
+ snapshot: {
+ cookies: [],
+ "local-storage": [],
+ "session-storage": [],
+ },
+ },
+];
+
+add_task(async function () {
+ const { commands } = await openTabAndSetupStorage(
+ MAIN_DOMAIN + "storage-updates.html"
+ );
+
+ for (let i = 0; i < TESTS.length; i++) {
+ const test = TESTS[i];
+ await runTest(test, commands, i);
+ }
+
+ await commands.destroy();
+});
+
+async function runTest({ action, snapshot }, commands, index) {
+ info("Running test at index " + index);
+ await action();
+ await checkStores(commands, snapshot);
+}
+
+async function checkStores(commands, snapshot) {
+ const { resourceCommand } = commands;
+ const { TYPES } = resourceCommand;
+ const actual = {};
+ await resourceCommand.watchResources(
+ [TYPES.COOKIE, TYPES.LOCAL_STORAGE, TYPES.SESSION_STORAGE],
+ {
+ async onAvailable(resources) {
+ for (const resource of resources) {
+ actual[resource.resourceType] = await resource.getStoreObjects(
+ TEST_DOMAIN,
+ null,
+ {
+ sessionString,
+ }
+ );
+ }
+ },
+ }
+ );
+
+ for (const [type, entries] of Object.entries(snapshot)) {
+ const store = actual[type].data;
+
+ is(
+ store.length,
+ entries.length,
+ `The number of entries in ${type} is correct`
+ );
+
+ for (const entry of entries) {
+ checkStoreValue(entry.name, entry.value, store);
+ }
+ }
+}
+
+function checkStoreValue(name, value, store) {
+ for (const entry of store) {
+ if (entry.name === name) {
+ ok(true, `There is an entry for "${name}"`);
+
+ // entry.value is a longStringActor so we need to read it's value using
+ // entry.value.str.
+ is(entry.value.str, value, `Value for ${name} is correct`);
+ return;
+ }
+ }
+ ok(false, `There is an entry for "${name}"`);
+}
+
+async function addCookie(name, value) {
+ info(`addCookie("${name}", "${value}")`);
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [[name, value]],
+ ([iName, iValue]) => {
+ content.wrappedJSObject.window.addCookie(iName, iValue);
+ }
+ );
+}
+
+async function removeCookie(name) {
+ info(`removeCookie("${name}")`);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [name], iName => {
+ content.wrappedJSObject.window.removeCookie(iName);
+ });
+}
+
+async function localStorageSetItem(name, value) {
+ info(`localStorageSetItem("${name}", "${value}")`);
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [[name, value]],
+ ([iName, iValue]) => {
+ content.window.localStorage.setItem(iName, iValue);
+ }
+ );
+}
+
+async function localStorageRemoveItem(name) {
+ info(`localStorageRemoveItem("${name}")`);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [name], iName => {
+ content.window.localStorage.removeItem(iName);
+ });
+}
+
+async function sessionStorageSetItem(name, value) {
+ info(`sessionStorageSetItem("${name}", "${value}")`);
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [[name, value]],
+ ([iName, iValue]) => {
+ content.window.sessionStorage.setItem(iName, iValue);
+ }
+ );
+}
+
+async function sessionStorageRemoveItem(name) {
+ info(`sessionStorageRemoveItem("${name}")`);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [name], iName => {
+ content.window.sessionStorage.removeItem(iName);
+ });
+}
+
+async function clearCookies() {
+ info(`clearCookies()`);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.wrappedJSObject.window.clearCookies();
+ });
+}
+
+async function clearLocalAndSessionStores() {
+ info(`clearLocalAndSessionStores()`);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.wrappedJSObject.window.clearLocalAndSessionStores();
+ });
+}
diff --git a/devtools/server/tests/browser/browser_style_utils_getFontPreviewData.js b/devtools/server/tests/browser/browser_style_utils_getFontPreviewData.js
new file mode 100644
index 0000000000..7145e90446
--- /dev/null
+++ b/devtools/server/tests/browser/browser_style_utils_getFontPreviewData.js
@@ -0,0 +1,137 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that getFontPreviewData of the style utils generates font previews.
+
+const TEST_URI = "data:text/html,<title>Test getFontPreviewData</title>";
+
+add_task(async function () {
+ await addTab(TEST_URI);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ getFontPreviewData,
+ } = require("resource://devtools/server/actors/utils/style-utils.js");
+
+ const font = Services.appinfo.OS === "WINNT" ? "Arial" : "Liberation Sans";
+ let fontPreviewData = getFontPreviewData(font, content.document);
+ ok(
+ fontPreviewData?.dataURL,
+ "Returned a font preview with a valid dataURL"
+ );
+
+ // Create <img> element and load the generated preview into it
+ // to check whether the image is valid and get its dimensions
+ const image = content.document.createElement("img");
+ let imageLoaded = new Promise(loaded =>
+ image.addEventListener("load", loaded, { once: true })
+ );
+ image.src = fontPreviewData.dataURL;
+ await imageLoaded;
+
+ const { naturalWidth: widthImage1, naturalHeight: heightImage1 } = image;
+
+ Assert.greater(widthImage1, 0, "Preview width is greater than 0");
+ Assert.greater(heightImage1, 0, "Preview height is greater than 0");
+
+ // Create a preview with different text and compare
+ // its dimensions with the first one
+ fontPreviewData = getFontPreviewData(font, content.document, {
+ previewText: "Abcdef",
+ });
+
+ ok(
+ fontPreviewData?.dataURL,
+ "Returned a font preview with a valid dataURL"
+ );
+
+ imageLoaded = new Promise(loaded =>
+ image.addEventListener("load", loaded, { once: true })
+ );
+ image.src = fontPreviewData.dataURL;
+ await imageLoaded;
+
+ const { naturalWidth: widthImage2, naturalHeight: heightImage2 } = image;
+
+ // Check whether the width is greater than with the default parameters
+ // and that the height is the same
+ Assert.greater(
+ widthImage2,
+ widthImage1,
+ "Preview width is greater than with default parameters"
+ );
+ Assert.strictEqual(
+ heightImage2,
+ heightImage1,
+ "Preview height is the same as with default parameters"
+ );
+
+ // Create a preview with smaller font size and compare
+ // its dimensions with the first one
+ fontPreviewData = getFontPreviewData(font, content.document, {
+ previewFontSize: 20,
+ });
+
+ ok(
+ fontPreviewData?.dataURL,
+ "Returned a font preview with a valid dataURL"
+ );
+
+ imageLoaded = new Promise(loaded =>
+ image.addEventListener("load", loaded, { once: true })
+ );
+ image.src = fontPreviewData.dataURL;
+ await imageLoaded;
+
+ const { naturalWidth: widthImage3, naturalHeight: heightImage3 } = image;
+
+ // Check whether the width and height are smaller than with the default parameters
+ Assert.less(
+ widthImage3,
+ widthImage1,
+ "Preview width is smaller than with default parameters"
+ );
+ Assert.less(
+ heightImage3,
+ heightImage1,
+ "Preview height is smaller than with default parameters"
+ );
+
+ // Create a preview with multiple lines and compare
+ // its dimensions with the first one
+ fontPreviewData = getFontPreviewData(font, content.document, {
+ previewText: "Abc\ndef",
+ });
+
+ ok(
+ fontPreviewData?.dataURL,
+ "Returned a font preview with a valid dataURL"
+ );
+
+ imageLoaded = new Promise(loaded =>
+ image.addEventListener("load", loaded, { once: true })
+ );
+ image.src = fontPreviewData.dataURL;
+ await imageLoaded;
+
+ const { naturalWidth: widthImage4, naturalHeight: heightImage4 } = image;
+
+ // Check whether the width is the same as with the default parameters
+ // and that the height is greater
+ Assert.strictEqual(
+ widthImage4,
+ widthImage1,
+ "Preview width is the same as with default parameters"
+ );
+ Assert.greater(
+ heightImage4,
+ heightImage1,
+ "Preview height is greater than with default parameters"
+ );
+ });
+});
diff --git a/devtools/server/tests/browser/browser_styles_getRuleText.js b/devtools/server/tests/browser/browser_styles_getRuleText.js
new file mode 100644
index 0000000000..e775bcbb28
--- /dev/null
+++ b/devtools/server/tests/browser/browser_styles_getRuleText.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that StyleRuleActor.getRuleText returns the contents of the CSS rule.
+
+const CSS_RULE = `#test {
+ background-color: #f06;
+}`;
+
+const CONTENT = `
+ <style type='text/css'>
+ ${CSS_RULE}
+ </style>
+ <div id="test"></div>
+`;
+
+const TEST_URI = `data:text/html;charset=utf-8,${encodeURIComponent(CONTENT)}`;
+
+add_task(async function () {
+ const { inspector, target, walker } = await initInspectorFront(TEST_URI);
+
+ const pageStyle = await inspector.getPageStyle();
+ const element = await walker.querySelector(walker.rootNode, "#test");
+ const entries = await pageStyle.getApplied(element, { inherited: false });
+
+ const rule = entries[1].rule;
+ const text = await rule.getRuleText();
+
+ is(text, CSS_RULE, "CSS rule text content matches");
+
+ await target.destroy();
+});
diff --git a/devtools/server/tests/browser/browser_stylesheets_getTextEmpty.js b/devtools/server/tests/browser/browser_stylesheets_getTextEmpty.js
new file mode 100644
index 0000000000..a8c069e950
--- /dev/null
+++ b/devtools/server/tests/browser/browser_stylesheets_getTextEmpty.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that StyleSheetsActor.getText handles empty text correctly.
+
+const CSS_CONTENT = "body { background-color: #f06; }";
+const TEST_URI = `data:text/html;charset=utf-8,<style>${encodeURIComponent(
+ CSS_CONTENT
+)}</style>`;
+
+add_task(async function () {
+ const tab = await addTab(TEST_URI);
+
+ const commands = await CommandsFactory.forTab(tab);
+ await commands.targetCommand.startListening();
+ const target = commands.targetCommand.targetFront;
+
+ const styleSheetsFront = await target.getFront("stylesheets");
+ ok(styleSheetsFront, "The StyleSheetsFront was created.");
+
+ const sheets = [];
+ await commands.resourceCommand.watchResources(
+ [commands.resourceCommand.TYPES.STYLESHEET],
+ {
+ onAvailable: resources => sheets.push(...resources),
+ }
+ );
+ is(sheets.length, 1, "watchResources returned the correct number of sheets");
+
+ const { resourceId } = sheets[0];
+
+ is(
+ await getStyleSheetText(styleSheetsFront, resourceId),
+ CSS_CONTENT,
+ "The stylesheet has expected initial text"
+ );
+ info("Update stylesheet content via the styleSheetsFront");
+ await styleSheetsFront.update(resourceId, "", false);
+ is(
+ await getStyleSheetText(styleSheetsFront, resourceId),
+ "",
+ "Stylesheet is now empty, as expected"
+ );
+
+ await commands.destroy();
+});
+
+async function getStyleSheetText(styleSheetsFront, resourceId) {
+ const longStringFront = await styleSheetsFront.getText(resourceId);
+ return longStringFront.string();
+}
diff --git a/devtools/server/tests/browser/director-script-target.html b/devtools/server/tests/browser/director-script-target.html
new file mode 100644
index 0000000000..c436a5446c
--- /dev/null
+++ b/devtools/server/tests/browser/director-script-target.html
@@ -0,0 +1,18 @@
+<html>
+ <head>
+ <script>
+ /* exported globalAccessibleVar */
+ "use strict";
+ // change the eval function to ensure the window object
+ // in the debug-script is correctly wrapped
+ // eslint-disable-next-line no-eval
+ window.eval = function() {
+ return "unsecure-eval-called";
+ };
+ var globalAccessibleVar = "global-value";
+ </script>
+ </head>
+ <body>
+ <h1>debug script target</h1>
+ </body>
+</html>
diff --git a/devtools/server/tests/browser/doc_accessibility.html b/devtools/server/tests/browser/doc_accessibility.html
new file mode 100644
index 0000000000..845dd7c562
--- /dev/null
+++ b/devtools/server/tests/browser/doc_accessibility.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+<body>
+ <h1 id="h1">Accessibility Test</h1>
+ <button id="button" aria-describedby="h1" accesskey="b">Accessible Button</button>
+ <div id="slider" role="slider" aria-valuenow="5"
+ aria-valuemin="0" aria-valuemax="7">slider</div>
+ <label id="label" for="control">Label
+ <input id="control" aria-details="details">
+ </label>
+ <div id="details">details</div>
+ <header id="header">header</header>
+ <nav id="nav">nav</nav>
+ <footer id="footer">footer</footer>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/doc_accessibility_audit.html b/devtools/server/tests/browser/doc_accessibility_audit.html
new file mode 100644
index 0000000000..0667e0569e
--- /dev/null
+++ b/devtools/server/tests/browser/doc_accessibility_audit.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+<body style="color: red;">
+ <p id="p1">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
+ <p id="p2">Accessible Paragraph</p>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/doc_accessibility_infobar.html b/devtools/server/tests/browser/doc_accessibility_infobar.html
new file mode 100644
index 0000000000..8f3c66911c
--- /dev/null
+++ b/devtools/server/tests/browser/doc_accessibility_infobar.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+<body>
+ <h1 id="h1">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</h1>
+ <button id="button">Accessible Button</button>
+ <p id="p" style="font-size: 0;">This is a paragraph that has no bounds.</p>
+ <label>Enter text: <input id="input" type="text"></text></label>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/doc_accessibility_keyboard_audit.html b/devtools/server/tests/browser/doc_accessibility_keyboard_audit.html
new file mode 100644
index 0000000000..00c002efe9
--- /dev/null
+++ b/devtools/server/tests/browser/doc_accessibility_keyboard_audit.html
@@ -0,0 +1,150 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ #focusable-1 {
+ outline: none;
+ }
+
+ #focusable-2:focus {
+ outline: none;
+ border: 1px solid black;
+ }
+ </style>
+ </head>
+<body>
+ <div id="button-1" class="Button" tabindex="0">I should really be a button</div>
+ <div id="button-2" class="Button">I should really be a button</div>
+ <div id="input-container"><input id="input-1" type="text" tabindex="-1" /></div>
+ <input id="input-2" type="text" tabindex="-1" disabled />
+ <input id="input-3" type="text" disabled />
+ <input id="input-4" type="text" />
+ <a id="link-1">Though a link, I'm not interactive.</a>
+ <a id="link-2" href="example.com">I'm a proper link.</a>
+ <a id="link-3" href="#">Link 3</a>
+ <a id="link-4" href="">Link 4</a>
+ <a id="link-5" href="https://example.com">Website</a>
+ <button id="button-3">Button with no tabindex</button>
+ <button id="button-4" tabindex="-1">Button with -1 tabindex</button>
+ <button id="button-5" tabindex="0">Button with 0 tabindex</button>
+ <button id="button-6" tabindex="1">Button with 1 tabindex</button>
+ <div id="focusable-1" role="button" tabindex="0">Focusable with no focus style.</div>
+ <div id="focusable-2" role="button" tabindex="0">Focusable with focus style.</div>
+ <div id="focusable-3" role="button" tabindex="0">Focusable with focus style.</div>
+ <div id="mouse-only-1" onclick="console.log('foo');">Button for mouse only</div>
+ <div id="focusable-4" onclick="console.log('foo');" tabindex="0">Button no semantics</div>
+ <div id="button-7" onclick="console.log('foo');" role="button">Semantic button not focusable</div>
+ <div id="button-8" onclick="console.log('foo');" role="button" tabindex="0">Button</div>
+ <img id="img-1" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
+ <img id="img-2" longdesc="https://example.com" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
+ <img id="img-3" longdesc="https://example.com" onclick="console.log('foo');" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
+ <img id="img-4" onclick="console.log('foo');" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
+ <button id="buttonmenu-1" aria-haspopup="true">I have a popup</button>
+ <div role="button" id="buttonmenu-2" aria-haspopup="true">I have a popup</div>
+ <input id="checkbox-1" type="checkbox" name="hello" />
+ <select id="listbox-1" size="2">
+ <option id="lb_orange">orange</option>
+ <option id="lb_apple">apple</option>
+ </select>
+ <select id="combobox-1"></select>
+ <select id="combobox-2"><option>One</option></select>
+ <select id="combobox-3">
+ <option id="cb_orange">orange</option>
+ <option id="cb_apple">apple</option>
+ </select>
+ <div id="editcombobox-1" role="combobox"><span role="option">One</span></div>
+ <span id="editcombobox-2"role="combobox"></span>
+ <span id="editcombobox-3"role="combobox" tabindex="0"></span>
+ <span id="switch-1" role="switch"></span>
+ <span id="switch-2" role="switch" tabindex="0"></span>
+ <div aria-label="Tag" role="combobox" aria-expanded="true" aria-owns="owned_listbox" aria-haspopup="listbox">
+ <input type="text" aria-autocomplete="list" aria-controls="owned_listbox" aria-activedescendant="selected_option">
+ </div>
+ <ul role="listbox" id="owned_listbox">
+ <li role="option">Zebra</li>
+ <li role="option" id="selected_option">Zoom</li>
+ </ul>
+ <label id="label-1">hello<input type="checkbox" name="world" /></label>
+ <label id="label-2" for="checkbox-1">hello</label>
+ <label id="label-3">hello</label>
+ <label id="label-4">hello</label><input type="checkbox" name="world" />
+ <a href="about:mozilla" target="_blank" rel="opener">
+ <img id="img-5" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
+ </a>
+ <a onmousedown="">
+ <img id="img-6" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
+ </a>
+ <a onclick="">
+ <img id="img-7" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
+ </a>
+ <a onmouseup="">
+ <img id="img-8" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
+ </a>
+ <section id="section-1" class="collapsible-section top-sites animation-enabled" aria-expanded="true"></section>
+ <main id="main" tabindex="-1">Main content</main>
+ <div id="not-keyboard-focusable-1" tabindex="-1">Not keyboard fqocusable with no focus style.</div>
+ <div id="grid-1" role="grid" aria-label="Interactive grid"></div>
+ <div id="grid-2" tabindex="0" role="grid" aria-label="Interactive grid"></div>
+ <div id="table-1" role="table" aria-label="Non-interactive ARIA table"></div>
+ <div id="table-2" tabindex="0" role="table" aria-label="Non-interactive ARIA table"></div>
+ <table id="table-3" aria-label="Non-interactive table"></table>
+ <table id="table-4" tabindex="0" aria-label="Non-interactive table"></table>
+ <div id="article-1" role="article"></div>
+ <div id="article-2" role="article" tabindex="0"></div>
+ <div role="grid" aria-label="Interactive grid">
+ <div id="columnheader-1" role="columnheader"></div>
+ <div id="rowheader-1" role="rowheader"></div>
+ <div id="gridcell-1" role="gridcell"></div>
+ <div id="gridcell-2" role="gridcell" tabindex="0"></div>
+ </div>
+ <div role="table" aria-label="Non-interactive table">
+ <div id="columnheader-2" role="columnheader"></div>
+ <div id="rowheader-2" role="rowheader"></div>
+ </div>
+ <table>
+ <tr>
+ <th id="columnheader-3">Animals</th>
+ </tr>
+ <tr>
+ <th id="columnheader-4" tabindex="0">Hippopotamus</th>
+ </tr>
+ <tr>
+ <th id="rowheader-3">Horse</th>
+ <td>Mare</td>
+ </tr>
+ <tr>
+ <th id="rowheader-4" tabindex="0">Chicken</th>
+ <td>Hen</td>
+ </tr>
+ </table>
+ <table role="grid">
+ <tr>
+ <th id="columnheader-5">Animals</th>
+ </tr>
+ <tr>
+ <th id="columnheader-6" tabindex="0">Hippopotamus</th>
+ </tr>
+ <tr>
+ <th id="rowheader-5">Horse</th>
+ <td id="gridcell-3">Mare</td>
+ </tr>
+ <tr>
+ <th id="rowheader-6" tabindex="0">Chicken</th>
+ <td id="gridcell-4" tabindex="0">Hen</td>
+ </tr>
+ </table>
+ <div id="tablist-1" role="tablist"></div>
+ <div id="tablist-2" role="tablist" tabindex="0"></div>
+ <div id="scrollbar-1" role="scrollbar"></div>
+ <div id="scrollbar-2" role="scrollbar" tabindex="0"></div>
+ <div id="separator-1" role="separator"></div>
+ <div id="separator-2" role="separator" tabindex="0"></div>
+ <div id="toolbar-1" role="toolbar"></div>
+ <div id="toolbar-2" role="toolbar" tabindex="0"></div>
+ <div id="menu-1" role="menu"></div>
+ <div id="menu-2" role="menu" tabindex="0"></div>
+ <div id="menubar-1" role="menubar"></div>
+ <div id="menubar-2" role="menubar" tabindex="0"></div>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/doc_accessibility_text_label_audit.html b/devtools/server/tests/browser/doc_accessibility_text_label_audit.html
new file mode 100644
index 0000000000..982cc5c243
--- /dev/null
+++ b/devtools/server/tests/browser/doc_accessibility_text_label_audit.html
@@ -0,0 +1,463 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+<body>
+ <button id="buttonmenu-1" aria-haspopup="true">I have a popup</button>
+ <label>I have a popup<button id="buttonmenu-2" aria-haspopup="true"></button></label>
+ <button id="buttonmenu-3" aria-haspopup="true"></button>
+ <button id="buttonmenu-4" aria-haspopup="true" aria-label="I have a popup"></button>
+ <label for="buttonmenu-5">I have a popup </label><button id="buttonmenu-5" aria-haspopup="true"></button>
+ <label id="buttonmenu-6-label">I have a popup </label><button id="buttonmenu-6" aria-haspopup="true" aria-labelledby="buttonmenu-6-label"></button>
+ <p id="p1">I am a paragraph</p>
+ <p id="p2"></p>
+ <canvas id="canvas-1"></canvas>
+ <canvas id="canvas-2" aria-label="Canvas label"></canvas>
+ <canvas id="canvas-3" aria-labelledby="canvas-3-heading">
+ <h2 id="canvas-3-heading">Shapes</h2>
+ </canvas>
+ <canvas id="canvas-4">
+ <h2>Shapes</h2>
+ </canvas>
+ <input id="checkbox-1" type="checkbox" name="world" />
+ <label>hello</label><input id="checkbox-2" type="checkbox" name="world" />
+ <label>hello<input id="checkbox-3" type="checkbox" name="world" /></label>
+ <label for="checkbox-4">hello</label><input id="checkbox-4" type="checkbox" name="world" />
+ <input id="checkbox-5" type="checkbox" name="world" aria-label="hello" />
+ <label id="checkbox-6-label">hello</label><input id="checkbox-6" type="checkbox" name="world" aria-labelledby="checkbox-6-label" />
+ <div id="checkbox-7" role="checkbox"></div>
+ <div id="checkbox-8" aria-label="hello" role="checkbox"></div>
+ <div id="checkbox-9-label">hello</div><div id="checkbox-9" aria-labelledby="checkbox-9-label" role="checkbox"></div>
+ <div role="menu">
+ <div id="menuitemcheckbox-1" role="menuitemcheckbox">hello</div>
+ <div id="menuitemcheckbox-2" role="menuitemcheckbox"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" /></div>
+ <div id="menuitemcheckbox-3" role="menuitemcheckbox"></div>
+ <div id="menuitemcheckbox-4" role="menuitemcheckbox"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="" /></div>
+ <div id="menuitemcheckbox-5" role="menuitemcheckbox"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="hello" /></div>
+ <div id="menuitemcheckbox-6" role="menuitemcheckbox">&nbsp;</div>
+ </div>
+ <p id="columnheader-7-label">Budget</p>
+ <p id="rowheader-7-label">Toy Story 3</p>
+ <table>
+ <thead>
+ <tr>
+ <th id="columnheader-1" scope="col">Film Title</th>
+ <th id="columnheader-2" scope="col"></th>
+ <th id="columnheader-3" scope="col">&nbsp;</th>
+ <th id="columnheader-4" scope="col" aria-label="Worldwide Gross"></th>
+ <th id="columnheader-5" scope="col" aria-label=""></th>
+ <th id="columnheader-6" scope="col" aria-label=" "></th>
+ <th id="columnheader-7" scope="col" aria-labelledby="columnheader-7-label"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr><th id="rowheader-1" scope="row">Toy Story 3</th></tr>
+ <tr><th id="rowheader-2" scope="row"></th></tr>
+ <tr><th id="rowheader-3" scope="row">&nbsp;</th></tr>
+ <tr><th id="rowheader-4" scope="row" aria-label="Alladin"></th></tr>
+ <tr><th id="rowheader-5" scope="row" aria-label=""></th></tr>
+ <tr><th id="rowheader-6" scope="row" aria-label=" "></th></tr>
+ <tr><th id="rowheader-7" scope="row" aria-labelledby="columnheader-7-label"></th></tr>
+ </tbody>
+ </table>
+ <div role="columnheader" id="columnheader-8">Film Title</div>
+ <div role="columnheader" id="columnheader-9"></div>
+ <div role="columnheader" id="columnheader-10">&nbsp;</div>
+ <div role="columnheader" id="columnheader-11" aria-label="Worldwide Gross"></div>
+ <div role="columnheader" id="columnheader-12" aria-label=""></div>
+ <div role="columnheader" id="columnheader-13" aria-label=" "></div>
+ <div role="columnheader" id="columnheader-14" aria-labelledby="columnheader-7-label"></div>
+ <label for="combobox-1">Choose a pet:</label>
+ <select id="combobox-1">
+ <option id="combobox-option-1" value="">--Please choose an option--</option>
+ <option id="combobox-option-2" value="dog"></option>
+ <option id="combobox-option-3" value="cat">&nbsp;</option>
+ <option id="combobox-option-4" value="" label="--Please choose an option--"></option>
+ <option id="combobox-option-5" value="dog" label=""></option>
+ <option id="combobox-option-6" value="cat" label=" "></option>
+ </select>
+ <select id="combobox-2"></select>
+ <label>Choose a pet:</label><select id="combobox-3"></select>
+ <label>Choose a pet:<select id="combobox-4"></select></label>
+ <select id="combobox-5" aria-label="Choose a pet:"></select>
+ <label id="combobox-6-label">Choose a pet:</label><select id="combobox-6" aria-labelledby="combobox-6-label"></select>
+ <svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="diagram-1"
+ xmlns:xlink="http://www.w3.org/1999/xlink"></svg>
+ <svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="diagram-2" aria-label=""
+ xmlns:xlink="http://www.w3.org/1999/xlink"></svg>
+ <svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="diagram-3" aria-label="empty drawing"
+ xmlns:xlink="http://www.w3.org/1999/xlink"></svg>
+ <div id="diagram-4-label">Empty drawing</div>
+ <svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="diagram-4" aria-labelledby="diagram-4-label"
+ xmlns:xlink="http://www.w3.org/1999/xlink"></svg>
+ <div id="diagram-5-label"></div>
+ <svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="diagram-5" aria-labelledby="diagram-5-label"
+ xmlns:xlink="http://www.w3.org/1999/xlink"></svg>
+ <dialog id="dialog-1" open>
+ <p>Greetings, one and all!</p>
+ </dialog>
+ <dialog id="dialog-2" aria-label="" open>
+ <p>Greetings, one and all!</p>
+ </dialog>
+ <dialog id="dialog-3" aria-label="Greetings" open>
+ <p>Greetings, one and all!</p>
+ </dialog>
+ <dialog id="dialog-4" aria-labelledby="dialog-4-label" open>
+ <p id="dialog-4-label">Greetings, one and all!</p>
+ </dialog>
+ <div role="dialog" id="dialog-5">
+ <p>Greetings, one and all!</p>
+ </div>
+ <div role="dialog" id="dialog-6" aria-label="">
+ <p>Greetings, one and all!</p>
+ </div>
+ <div role="dialog" id="dialog-7" aria-label="Greetings">
+ <p>Greetings, one and all!</p>
+ </div>
+ <div role="dialog" id="dialog-8" aria-labelledby="dialog-8-label">
+ <p id="dialog-8-label">Greetings, one and all!</p>
+ </div>
+ <dialog id="dialog-9" aria-labelledby="dialog-9-label" open>
+ <p id="dialog-9-label"></p>
+ </dialog>
+ <div role="dialog" id="dialog-10" aria-labelledby="dialog-10-label">
+ <p id="dialog-10-label"></p>
+ </div>
+ <div id="editcombobox-1" role="combobox"></div>
+ <div id="editcombobox-2" aria-label="Choose a pet:" role="combobox"></div>
+ <div id="editcombobox-3-label">Choose a pet:</div><div id="editcombobox-3" aria-labelledby="editcombobox-3-label" role="combobox"></div>
+ <label>Customer name: <input id="entry-1"></label>
+ <input id="entry-2">
+ <input id="entry-3" aria-label="Customer name:">
+ <label>Customer name: </label><input id="entry-4">
+ <label for="entry-5">Customer name: </label><input id="entry-5">
+ <label id="entry-6-label">Customer name: </label><input id="entry-6" aria-labelledby="entry-6-label">
+ <div id="entry-7" role="textbox"></div>
+ <div id="entry-8" aria-label="Customer name:" role="textbox"></div>
+ <div id="entry-9-label">Customer name: </div><div id="entry-9" aria-labelledby="entry-9-label" role="textbox"></div>
+ <figure id="figure-1">
+ <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
+ <figcaption>Figure 1: The four layers of awesome.</figcaption>
+ </figure>
+ <figure id="figure-2">
+ <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
+ </figure>
+ <div id="figure-3" role="figure" aria-labelledby="caption-figure-3">
+ <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
+ <p id="caption-figure-3">Figure 1: The caption</p>
+ </div>
+ <div id="figure-4" role="figure" aria-labelledby="caption-figure-4">
+ <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
+ <p id="caption-figure-4"></p>
+ </div>
+ <div id="figure-5" role="figure">
+ <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
+ </div>
+ <img id="img-1" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=">
+ <img id="img-2" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" aria-label="alt text">
+ <p id="img-3-label">Label</p>
+ <img id="img-3" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" aria-labelledby="img-3-label">
+ <img id="img-4" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text">
+ <p id="img-5-label"></p>
+ <img id="img-5" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" aria-labelledby="img-5-label">
+ <div id="img-6" role="img"></div>
+ <div id="img-7" role="img" aria-label="alt text"></div>
+ <p id="img-8-label">Label</p>
+ <div id="img-8" role="img" aria-labelledby="img-8-label"></div>
+ <div id="img-9" role="img" aria-label=""></div>
+ <p id="img-10-label"></p>
+ <div id="img-10" role="img" aria-labelledby="img-10-label"></div>
+ <select>
+ <optgroup id="optgroup-1" label="Group 1">
+ <option>Option 1.1</option>
+ </optgroup>
+ <optgroup id="optgroup-2" label="">
+ <option>Option 2.1</option>
+ </optgroup>
+ <optgroup id="optgroup-3">
+ <option>Option 3.1</option>
+ </optgroup>
+ <optgroup id="optgroup-4" aria-label="Group 4">
+ <option>Option 4.1</option>
+ </optgroup>
+ <optgroup id="optgroup-5" aria-labelledby="optgroup-5-label">
+ <option id="optgroup-5-label">Option 5.1</option>
+ </optgroup>
+ </select>
+ <fieldset id="fieldset-1"><legend>Choose your favorite monster</legend></fieldset>
+ <fieldset id="fieldset-2"><legend></legend></fieldset>
+ <fieldset id="fieldset-3"></fieldset>
+ <fieldset id="fieldset-4" aria-label="Choose your favorite monster"></fieldset>
+ <p id="fieldset-5-label">Choose your favorite monster</p>
+ <fieldset id="fieldset-5" aria-labelledby="fieldset-5-label"></fieldset>
+ <h1 id="heading-1"></h1>
+ <h1 id="heading-2">Heading</h1>
+ <h1 id="heading-3">&nbsp;</h1>
+ <h1 id="heading-4" aria-label="Heading"></h1>
+ <h1 id="heading-5" aria-labelledby="heading-5-label"></h1>
+ <p id="heading-5-label">Heading</p>
+ <h1 id="heading-6" aria-label="Heading">H</h1>
+ <h1 id="heading-7" aria-labelledby="heading-7-label">H</h1>
+ <p id="heading-7-label">Heading</p>
+ <div role="heading" aria-level="1" id="heading-8"></div>
+ <div role="heading" aria-level="1" id="heading-9">Heading</div>
+ <div role="heading" aria-level="1" id="heading-10">&nbsp;</div>
+ <div role="heading" aria-level="1" id="heading-11" aria-label="Heading"></div>
+ <div role="heading" aria-level="1" id="heading-12" aria-labelledby="heading-12-label"></div>
+ <p id="heading-12-label">Heading</p>
+ <div role="heading" aria-level="1" id="heading-13" aria-label="Heading">H</div>
+ <div role="heading" aria-level="1" id="heading-14" aria-labelledby="heading-14-label">H</div>
+ <p id="heading-14-label">Heading</p>
+ <map name="imagemap">
+ <area alt="One" shape="rect" coords="0,0,14,28" href="1.html">
+ <area shape="rect" coords="14,0,28,28" href="2.html">
+ </map>
+ <img id="imagemap-1" usemap="#imagemap" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=">
+ <img id="imagemap-2" usemap="#imagemap" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" aria-label="image map name">
+ <p id="imagemap-3-label">image map name</p>
+ <img id="imagemap-3" usemap="#imagemap" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" aria-labelledby="imagemap-3-label">
+ <img id="imagemap-4" usemap="#imagemap" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="image map name">
+ <p id="imagemap-5-label"></p>
+ <img id="imagemap-5" usemap="#imagemap" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" aria-labelledby="img-5-label">
+ <iframe id="iframe-1" title="IFrame Title" src="https://example.com"></iframe>
+ <iframe id="iframe-2" title="" src="https://example.com"></iframe>
+ <iframe id="iframe-3" src="https://example.com"></iframe>
+ <iframe id="iframe-4" aria-label="Bad Title" src="https://example.com"></iframe>
+ <iframe id="iframe-5" aria-label="Bad Title" title="Good Title" src="https://example.com"></iframe>
+ <object id="object-1" type="image/png" data="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII="></object>
+ <object id="object-2" aria-label="Image object" type="image/png" data="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII="></object>
+ <p id="object-3-label">Image object</p>
+ <object id="object-3" aria-labelledby="object-3-label" type="image/png" data="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII="></object>
+ <object id="object-4" type="text/html" data="https://example.com"></object>
+ <embed id="embed-1" type="image/png" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=">
+ <embed id="embed-2" type="video/webm" src="data:video/webm,xxx">
+ <embed id="embed-3" aria-label="Embedded video" type="video/webm" src="data:video/webm,xxx">
+ <p id="embed-4-label">Embedded video</p>
+ <embed id="embed-4" aria-labelledby="embed-4-label" type="video/webm" src="data:video/webm,xxx">
+ <a id="link-1"></a>
+ <a id="link-2">Hello world</a>
+ <a id="link-3" href></a>
+ <a id="link-4" href>Hello world</a>
+ <a id="link-5" href=""></a>
+ <a id="link-6" href="">Hello world</a>
+ <a id="link-7" href="#"></a>
+ <a id="link-8" href="#">Hello world</a>
+ <a id="link-9" href="https://example.com"></a>
+ <a id="link-10" href="https://example.com">Hello world</a>
+ <a id="link-11" aria-label="Hello world" href="https://example.com"></a>
+ <p id="link-12-label">Hello world</p>
+ <a id="link-12" aria-labelledby="link-12-label" href="https://example.com"></a>
+ <div role="link" id="link-13"></div>
+ <div role="link" id="link-14">Hello world</div>
+ <div role="link" id="link-15" aria-label="Hello world"></div>
+ <p id="link-16-label">Hello world</p>
+ <div role="link" id="link-16" aria-labelledby="link-16-label"></div>
+ <p id="mglyph-3-label">Label</p>
+ <p id="mglyph-6-label"></p>
+ <math>
+ <mi><mglyph id="mglyph-1" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII="/></mi>
+ <mi><mglyph id="mglyph-2" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" aria-label="alt text"/></mi>
+ <mi><mglyph id="mglyph-3" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" aria-labelledby="mglyph-3-label"/></mi>
+ <mi><mglyph id="mglyph-4" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="alt text"/></mi>
+ <mi><mglyph id="mglyph-5" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt=""/></mi>
+ <mi><mglyph id="mglyph-6" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" aria-labelledby="mglyph-6-label"/></mi>
+ </math>
+ <span id="menuitem-1" role="menuitem"></span>
+ <span id="menuitem-2" aria-label="" role="menuitem"></span>
+ <span id="menuitem-3" aria-label="Menu Item" role="menuitem"></span>
+ <p id="menuitem-4-label">Menu Item</p>
+ <span id="menuitem-4" aria-labelledby="menuitem-4-label" role="menuitem"></span>
+ <p id="menuitem-5-label"></p>
+ <span id="menuitem-5" aria-labelledby="menuitem-5-label" role="menuitem"></span>
+ <span id="menuitem-6" role="menuitem">Menu Item</span>
+ <label for="listbox-1">Choose a pet:</label>
+ <select id="listbox-1" size="6">
+ <option id="option-1" value="">--Please choose an option--</option>
+ <option id="option-2" value="dog"></option>
+ <option id="option-3" value="cat">&nbsp;</option>
+ <option id="option-4" value="" label="--Please choose an option--"></option>
+ <option id="option-5" value="dog" label=""></option>
+ <option id="option-6" value="cat" label=" "></option>
+ </select>
+ <select id="listbox-2" size="2"></select>
+ <label>Choose a pet:</label><select id="listbox-3" size="2"></select>
+ <label>Choose a pet:<select id="listbox-4" size="2"></select></label>
+ <select id="listbox-5" aria-label="Choose a pet:" size="2"></select>
+ <label id="listbox-6-label">Choose a pet:</label><select id="listbox-6" aria-labelledby="listbox-6-label" size="2"></select>
+ <div role="listbox">
+ <div role="option" id="option-7">--Please choose an option--</div>
+ <div role="option" id="option-8"></div>
+ <div role="option" id="option-9">&nbsp;</div>
+ <div role="option" id="option-10" aria-label="--Please choose an option--"></div>
+ <div role="option" id="option-11" aria-label=""></div>
+ <div role="option" id="option-12" aria-label=" "></div>
+ <p id="option-13-label">--Please choose an option--</p>
+ <div role="option" id="option-13" aria-labelledby="option-13-label"></div>
+ <p id="option-14-label"></p>
+ <div role="option" id="option-14" aria-labelledby="option-14-label"></div>
+ <p id="option-15-label"> </p>
+ <div role="option" id="option-15" aria-labelledby="option-15-label"></div>
+ </div>
+ <span id="treeitem-1" role="treeitem"></span>
+ <span id="treeitem-2" aria-label="" role="treeitem"></span>
+ <span id="treeitem-3" aria-label="Tree Item" role="treeitem"></span>
+ <p id="treeitem-4-label">Tree Item</p>
+ <span id="treeitem-4" aria-labelledby="treeitem-4-label" role="treeitem"></span>
+ <p id="treeitem-5-label"></p>
+ <span id="treeitem-5" aria-labelledby="treeitem-5-label" role="treeitem"></span>
+ <span id="treeitem-6" role="treeitem">Tree Item</span>
+ <div role="tablist">
+ <span id="tab-1" role="tab"></span>
+ <span id="tab-2" aria-label="" role="tab"></span>
+ <span id="tab-3" aria-label="Tab" role="tab"></span>
+ <p id="tab-4-label">Tab</p>
+ <span id="tab-4" aria-labelledby="tab-4-label" role="tab"></span>
+ <p id="tab-5-label"></p>
+ <span id="tab-5" aria-labelledby="tab-5-label" role="tab"></span>
+ <span id="tab-6" role="tab">Tab</span>
+ </div>
+ <label>Password: <input type="password" id="password-1"></label>
+ <input type="password" id="password-2">
+ <input type="password" id="password-3" aria-label="Password:">
+ <label>Password: </label><input type="password" id="password-4">
+ <label for="password-5">Password: </label><input type="password" id="password-5">
+ <label id="password-6-label">Password: </label><input type="password" id="password-6" aria-labelledby="password-6-label">
+ <label>Progress: <progress id="progress-1"></progress></label>
+ <progress id="progress-2"></progress>
+ <progress id="progress-3" aria-label="Progress:"></progress>
+ <label>Progress: </label><progress id="progress-4"></progress>
+ <label for="progress-5">Progress: </label><progress id="progress-5"></progress>
+ <label id="progress-6-label">Progress: </label><progress id="progress-6" aria-labelledby="progress-6-label"></progress>
+ <label>Progress: <div role="progressbar" id="progress-7"></div></label>
+ <label id="progress-8-label">Progress: <div role="progressbar" id="progress-8" aria-labelledby="progress-8-label"></div></label>
+ <div role="progressbar" id="progress-9"></div>
+ <div role="progressbar" id="progress-10" aria-label="Progress:"></div>
+ <label>Progress: </label><div role="progressbar" id="progress-11"></div>
+ <label for="progress-12">Progress: </label><div role="progressbar" id="progress-12"></div>
+ <label id="progress-13-label">Progress: </label><div role="progressbar" id="progress-13" aria-labelledby="progress-13-label"></div>
+ <button id="button-1">hello</button>
+ <button id="button-2"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" /></button>
+ <button id="button-3"></button>
+ <button id="button-4"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="" /></button>
+ <button id="button-5"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="hello" /></button>
+ <button id="button-6">&nbsp;</button>
+ <label>Button: <button id="button-7"></button></label>
+ <button id="button-8" aria-label="Button:"></button>
+ <label>Button: </label><button id="button-9"></button>
+ <label for="button-10">Button: </label><button id="button-10"></button>
+ <label id="button-11-label">Button: </label><button id="button-11" aria-labelledby="button-11-label"></button>
+ <label>Button: <div role="button" id="button-12"></div></label>
+ <label id="button-13-label">Button: <div role="button" id="button-13" aria-labelledby="button-13-label"></div></label>
+ <div role="button" id="button-14"></div>
+ <div role="button" id="button-15" aria-label="Button:"></div>
+ <label>Button: </label><div role="button" id="button-16"></div>
+ <label for="button-17">Button: </label><div role="button" id="button-17"></div>
+ <label id="button-18-label">Button: </label><div role="button" id="button-18" aria-labelledby="button-18-label"></div>
+ <label>Radio label: <input type="radio" id="radiobutton-1"></label>
+ <input type="radio" id="radiobutton-2">
+ <input type="radio" id="radiobutton-3" aria-label="Radio label:">
+ <label>Radio label: </label><input type="radio" id="radiobutton-4">
+ <label for="radiobutton-5">Radio label: </label><input type="radio" id="radiobutton-5">
+ <label id="radiobutton-6-label">Radio label: </label><input type="radio" id="radiobutton-6" aria-labelledby="radiobutton-6-label">
+ <div id="radiobutton-7" role="radio"></div>
+ <div id="radiobutton-8" aria-label="Radio label:" role="radio"></div>
+ <div id="radiobutton-9-label">Radio label: </div><div id="radiobutton-9" aria-labelledby="radiobutton-9-label" role="radio"></div>
+ <div role="menu">
+ <div id="menuitemradio-1" role="menuitemradio">hello</div>
+ <div id="menuitemradio-2" role="menuitemradio"></div>
+ <div id="menuitemradio-3" role="menuitemradio">&nbsp;</div>
+ </div>
+ <div role="rowheader" id="rowheader-8">Toy Story 3</div>
+ <div role="rowheader" id="rowheader-9"></div>
+ <div role="rowheader" id="rowheader-10">&nbsp;</div>
+ <div role="rowheader" id="rowheader-11" aria-label="Alladin"></div>
+ <div role="rowheader" id="rowheader-12" aria-label=""></div>
+ <div role="rowheader" id="rowheader-13" aria-label=" "></div>
+ <div role="rowheader" id="rowheader-14" aria-labelledby="columnheader-7-label"></div>
+ <label>Slider label: <input type="range" id="slider-1"></label>
+ <input type="range" id="slider-2">
+ <input type="range" id="slider-3" aria-label="Slider label:">
+ <label>Slider label: </label><input type="range" id="slider-4">
+ <label for="slider-5">Slider label: </label><input type="range" id="slider-5">
+ <label id="slider-6-label">Slider label: </label><input type="range" id="slider-6" aria-labelledby="slider-6-label">
+ <div id="slider-7" role="slider"></div>
+ <div id="slider-8" aria-label="Slider label:" role="slider"></div>
+ <div id="slider-9-label">Slider label: </div><div id="slider-9" aria-labelledby="slider-9-label" role="slider"></div>
+ <label>Spin button label: <input type="number" id="spinbutton-1"></label>
+ <input type="number" id="spinbutton-2">
+ <input type="number" id="spinbutton-3" aria-label="Spin button label:">
+ <label>Spin button label: </label><input type="number" id="spinbutton-4">
+ <label for="spinbutton-5">Spin button label: </label><input type="number" id="spinbutton-5">
+ <label id="spinbutton-6-label">Spin button label: </label><input type="number" id="spinbutton-6" aria-labelledby="spinbutton-6-label">
+ <div id="spinbutton-7" role="spinbutton"></div>
+ <div id="spinbutton-8" aria-label="Spin button label:" role="spinbutton"></div>
+ <div id="spinbutton-9-label">Spin button label: </div><div id="spinbutton-9" aria-labelledby="spinbutton-9-label" role="spinbutton"></div>
+ <div id="switch-1" role="switch"></div>
+ <div id="switch-2" aria-label="hello" role="switch"></div>
+ <div id="switch-3-label">hello</div><div id="switch-3" aria-labelledby="switch-3-label" role="switch"></div>
+ <label for="switch-4">hello</label><div id="switch-4" role="switch"></div>
+ <label>hello<div id="switch-5" role="switch"></div></label>
+ <label>Meter label: <meter id="meter-1"></meter></label>
+ <meter id="meter-2"></meter>
+ <meter id="meter-3" aria-label="Meter label:"></meter>
+ <label>Meter label: </label><meter id="meter-4"></meter>
+ <label for="meter-5">Meter label: </label><meter id="meter-5"></meter>
+ <label id="meter-6-label">Meter label: </label><meter id="meter-6" aria-labelledby="meter-6-label"></meter>
+ <div id="meter-7" role="meter"></div>
+ <div id="meter-8" aria-label="Meter label:" role="meter"></div>
+ <div id="meter-9-label">Meter label: </div><div id="meter-9" aria-labelledby="meter-9-label" role="meter"></div>
+ <button aria-pressed="true" id="togglebutton-1" >hello</button>
+ <button aria-pressed="true" id="togglebutton-2"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" /></button>
+ <button aria-pressed="true" id="togglebutton-3"></button>
+ <button aria-pressed="true" id="togglebutton-4"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="" /></button>
+ <button aria-pressed="true" id="togglebutton-5"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt="hello" /></button>
+ <button aria-pressed="true" id="togglebutton-6">&nbsp;</button>
+ <label>Button: <button aria-pressed="true" id="togglebutton-7"></button></label>
+ <button aria-pressed="true" id="togglebutton-8" aria-label="Button:"></button>
+ <label>Button: </label><button aria-pressed="true" id="togglebutton-9"></button>
+ <label for="togglebutton-10">Button: </label><button aria-pressed="true" id="togglebutton-10"></button>
+ <label id="togglebutton-11-label">Button: </label><button aria-pressed="true" id="togglebutton-11" aria-labelledby="togglebutton-11-label"></button>
+ <label>Button: <div role="button" aria-pressed="true" id="togglebutton-12"></div></label>
+ <label id="togglebutton-13-label">Button: <div role="button" aria-pressed="true" id="togglebutton-13" aria-labelledby="togglebutton-13-label"></div></label>
+ <div role="button" aria-pressed="true" id="togglebutton-14"></div>
+ <div role="button" aria-pressed="true" id="togglebutton-15" aria-label="Button:"></div>
+ <label>Button: </label><div role="button" aria-pressed="true" id="togglebutton-16"></div>
+ <label for="togglebutton-17">Button: </label><div role="button" aria-pressed="true" id="togglebutton-17"></div>
+ <label id="togglebutton-18-label">Button: </label><div role="button" aria-pressed="true" id="togglebutton-18" aria-labelledby="togglebutton-18-label"></div>
+ <span id="toolbar-1" role="toolbar" aria-label="Toolbar"></span>
+ <span id="toolbar-2" role="toolbar"></span>
+ <p id="toolbar-3-label"></p>
+ <span id="toolbar-3" role="toolbar" aria-labelledby="toolbar-3-label"></span>
+ <p id="toolbar-4-label">Toolbar</p>
+ <span id="toolbar-4" role="toolbar" aria-labelledby="toolbar-4-label"></span>
+ <svg id="svg-1" role="img" viewbox="0 0 100 10" height="10px">
+ <title id="siteLogoTitle">Site Logo</title>
+ <rect x="0" y="00" width="100" height="10" fill="red"></rect>
+ </svg>
+ <svg id="svg-2" viewbox="0 0 100 10" height="10px">
+ <title id="siteLogoTitle">Site Logo</title>
+ <rect x="0" y="00" width="100" height="10" fill="red"></rect>
+ </svg>
+ <svg id="svg-3" role="img" viewbox="0 0 100 10" height="10px">
+ <rect x="0" y="00" width="100" height="10" fill="red"></rect>
+ </svg>
+ <svg id="svg-4" viewbox="0 0 100 10" height="10px">
+ <rect x="0" y="00" width="100" height="10" fill="red"></rect>
+ </svg>
+ <svg id="svg-5" aria-label="foo" viewbox="0 0 100 10" height="10px">
+ <rect id="svg-6" aria-label="bar" x="0" y="00" width="100" height="10" fill="red"></rect>
+ </svg>
+ <svg id="svg-7" viewbox="0 0 100 10" height="10px">
+ <title id="siteLogoTitle">Site Logo</title>
+ <rect id="svg-8" aria-label="foo" x="0" y="00" width="100" height="10" fill="red"></rect>
+ </svg>
+ <svg id="svg-9" role="img" viewbox="0 0 100 10" height="10px">
+ <title id="siteLogoTitle">Site Logo</title>
+ <rect aria-label="foo" id="svg-10" x="0" y="00" width="100" height="10" fill="red"></rect>
+ </svg>
+ <svg id="svg-11" role="img" viewbox="0 0 100 10" height="10px">
+ <rect aria-label="foo" id="svg-12" x="0" y="00" width="100" height="10" fill="red"></rect>
+ </svg>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/doc_accessibility_text_label_audit_frame.html b/devtools/server/tests/browser/doc_accessibility_text_label_audit_frame.html
new file mode 100644
index 0000000000..34a32abeb2
--- /dev/null
+++ b/devtools/server/tests/browser/doc_accessibility_text_label_audit_frame.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <frameset cols="50%,50%">
+ <frame id="frame-1" src="https://example.com"></frame>
+ <frame id="frame-2" aria-label="Label" src="https://example.com"></frame>
+ </frameset>
+</html>
diff --git a/devtools/server/tests/browser/doc_allocations.html b/devtools/server/tests/browser/doc_allocations.html
new file mode 100644
index 0000000000..a5c9ea6d41
--- /dev/null
+++ b/devtools/server/tests/browser/doc_allocations.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+<script>
+"use strict";
+
+window.allocs = [];
+window.onload = function() {
+ function allocator() {
+ for (let i = 0; i < 1000; i++) {
+ window.allocs.push({});
+ }
+ }
+
+ window.setInterval(allocator, 1);
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/doc_compatibility.html b/devtools/server/tests/browser/doc_compatibility.html
new file mode 100644
index 0000000000..82cee286b5
--- /dev/null
+++ b/devtools/server/tests/browser/doc_compatibility.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<style>
+ div {
+ color: lime;
+ }
+
+ #id-clip {
+ clip: rect(10px, 10px, 10px, 10px);
+ }
+
+ .class-clip {
+ clip: rect(5px, 5px, 5px, 5px);
+ }
+
+ .class-user-select {
+ -moz-user-select: all;
+ }
+
+ .duplicate {
+ clip: rect(10px, 10px, 10px, 10px);
+ clip: rect(5px, 5px, 5px, 5px);
+ clip: rect(2px, 2px, 2px, 2px);
+ }
+</style>
+<div></div>
+<div class="class-user-select"></div>
+<div id="id-clip" class="class-clip class-user-select"></div>
+<div class="duplicate"></div>
diff --git a/devtools/server/tests/browser/doc_force_cc.html b/devtools/server/tests/browser/doc_force_cc.html
new file mode 100644
index 0000000000..22b1eb4071
--- /dev/null
+++ b/devtools/server/tests/browser/doc_force_cc.html
@@ -0,0 +1,32 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Performance tool + cycle collection test page</title>
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ "use strict";
+
+ /* global test */
+ window.test = function() {
+ document.body.expando1 = { cycle: document.body };
+ SpecialPowers.Cu.forceCC();
+
+ document.body.expando2 = { cycle: document.body };
+ SpecialPowers.Cu.forceCC();
+
+ document.body.expando3 = { cycle: document.body };
+ SpecialPowers.Cu.forceCC();
+
+ setTimeout(window.test, 100);
+ };
+ test();
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/server/tests/browser/doc_force_gc.html b/devtools/server/tests/browser/doc_force_gc.html
new file mode 100644
index 0000000000..7dee110501
--- /dev/null
+++ b/devtools/server/tests/browser/doc_force_gc.html
@@ -0,0 +1,31 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Performance tool + garbage collection test page</title>
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ "use strict";
+
+ var x = 1;
+ /* global test */
+ window.test = function() {
+ SpecialPowers.Cu.forceGC();
+ document.body.style.borderTop = x + "px solid red";
+ x = 1 ^ x;
+ // flush pending reflows
+ document.body.innerHeight;
+
+ // Prevent this script from being garbage collected.
+ setTimeout(window.test, 100);
+ };
+ test();
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/server/tests/browser/doc_iframe.html b/devtools/server/tests/browser/doc_iframe.html
new file mode 100644
index 0000000000..445361f7fa
--- /dev/null
+++ b/devtools/server/tests/browser/doc_iframe.html
@@ -0,0 +1,17 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>iframe test page</title>
+ </head>
+
+ <body>
+ <iframe id="better-not-ask" src="data:text/html,<iframe src='data:text/html,foo'></iframe>"></iframe>
+ <!-- This page is loaded on an example.org subdomain, so we switch to .com -->
+ <iframe id="remote-frame" src="http://example.com/browser/devtools/server/tests/browser/doc_iframe_content.html"></iframe>
+ </body>
+
+</html>
diff --git a/devtools/server/tests/browser/doc_iframe2.html b/devtools/server/tests/browser/doc_iframe2.html
new file mode 100644
index 0000000000..2255490f26
--- /dev/null
+++ b/devtools/server/tests/browser/doc_iframe2.html
@@ -0,0 +1,15 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Sub document page</title>
+ </head>
+
+ <body>
+ Iframe document
+ </body>
+
+</html>
diff --git a/devtools/server/tests/browser/doc_iframe_content.html b/devtools/server/tests/browser/doc_iframe_content.html
new file mode 100644
index 0000000000..6f80e4dd6d
--- /dev/null
+++ b/devtools/server/tests/browser/doc_iframe_content.html
@@ -0,0 +1,14 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Frame for browser_resource_list-remote-frames.js</title>
+ </head>
+
+ <body>
+ <div>Remote frame content</div>
+ </body>
+</html>
diff --git a/devtools/server/tests/browser/doc_innerHTML.html b/devtools/server/tests/browser/doc_innerHTML.html
new file mode 100644
index 0000000000..e58b32f51e
--- /dev/null
+++ b/devtools/server/tests/browser/doc_innerHTML.html
@@ -0,0 +1,21 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Performance tool + innerHTML test page</title>
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ "use strict";
+ window.test = function() {
+ document.body.innerHTML = "<h1>LOL</h1>";
+ };
+ setInterval(window.test, 100);
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/server/tests/browser/error-actor.js b/devtools/server/tests/browser/error-actor.js
new file mode 100644
index 0000000000..3872d8ad96
--- /dev/null
+++ b/devtools/server/tests/browser/error-actor.js
@@ -0,0 +1,25 @@
+/* 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");
+
+/**
+ * Test actor designed to check that clients are properly notified of errors when calling
+ * methods on old style actors.
+ */
+class ErrorActor extends Actor {
+ constructor(conn, tab) {
+ super(conn, { typeName: "error", methods: [] });
+ this.tab = tab;
+ this.requestTypes = {
+ error: this.onError,
+ };
+ }
+ onError() {
+ throw new Error("error");
+ }
+}
+
+exports.ErrorActor = ErrorActor;
diff --git a/devtools/server/tests/browser/grid.html b/devtools/server/tests/browser/grid.html
new file mode 100644
index 0000000000..3bd0e1ec26
--- /dev/null
+++ b/devtools/server/tests/browser/grid.html
@@ -0,0 +1,42 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8"/>
+ <title>Grid test page</title>
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ grid-template-columns: [col-1 col-start-1] 100px [col-2] 100px;
+ grid-template-rows: 100px 100px;
+ grid-template-areas: ". header"
+ "sidebar content";
+ }
+ #cell1 {
+ grid-column: 1;
+ grid-row: 1;
+ }
+ #cell2 {
+ grid-column: 2;
+ grid-row: 1;
+ }
+ #cell3 {
+ grid-column: 1;
+ grid-row: 2;
+ }
+ #cell4 {
+ grid-column: 2;
+ grid-row: 2;
+ }
+ </style>
+</head>
+<body>
+ <div id="grid">
+ <div id="cell1">cell1</div>
+ <div id="cell2">cell2</div>
+ <div id="cell3">cell3</div>
+ <div id="cell4">cell4</div>
+ </div>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/head.js b/devtools/server/tests/browser/head.js
new file mode 100644
index 0000000000..aba6d578f2
--- /dev/null
+++ b/devtools/server/tests/browser/head.js
@@ -0,0 +1,337 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
+
+const {
+ DevToolsClient,
+} = require("resource://devtools/client/devtools-client.js");
+const {
+ ActorRegistry,
+} = require("resource://devtools/server/actors/utils/actor-registry.js");
+const {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+
+const PATH = "browser/devtools/server/tests/browser/";
+const TEST_DOMAIN = "http://test1.example.org";
+const MAIN_DOMAIN = `${TEST_DOMAIN}/${PATH}`;
+const ALT_DOMAIN = "http://sectest1.example.org/" + PATH;
+const ALT_DOMAIN_SECURED = "https://sectest1.example.org:443/" + PATH;
+
+// GUID to be used as a separator in compound keys. This must match the same
+// constant in devtools/server/actors/resources/storage/index.js,
+// devtools/client/storage/ui.js and devtools/client/storage/test/head.js
+const SEPARATOR_GUID = "{9d414cc5-8319-0a04-0586-c0a6ae01670a}";
+
+// All tests are asynchronous.
+waitForExplicitFinish();
+
+// does almost the same thing as addTab, but directly returns an object
+async function addTabTarget(url) {
+ info(`Adding a new tab with URL: ${url}`);
+ const tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, url));
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ info(`Tab added a URL ${url} loaded`);
+ return createAndAttachTargetForTab(tab);
+}
+
+async function initAnimationsFrontForUrl(url) {
+ const { inspector, walker, target } = await initInspectorFront(url);
+ const animations = await target.getFront("animations");
+
+ return { inspector, walker, animations, target };
+}
+
+async function initLayoutFrontForUrl(url) {
+ const { inspector, walker, target } = await initInspectorFront(url);
+ const layout = await walker.getLayoutInspector();
+
+ return { inspector, walker, layout, target };
+}
+
+async function initAccessibilityFrontsForUrl(
+ url,
+ { enableByDefault = true } = {}
+) {
+ const { inspector, walker, target } = await initInspectorFront(url);
+ const parentAccessibility = await target.client.mainRoot.getFront(
+ "parentaccessibility"
+ );
+ const accessibility = await target.getFront("accessibility");
+ const a11yWalker = accessibility.accessibleWalkerFront;
+ if (enableByDefault) {
+ await parentAccessibility.enable();
+ }
+
+ return {
+ inspector,
+ walker,
+ accessibility,
+ parentAccessibility,
+ a11yWalker,
+ target,
+ };
+}
+
+function initDevToolsServer() {
+ try {
+ // Sometimes devtools server does not get destroyed correctly by previous
+ // tests.
+ DevToolsServer.destroy();
+ } catch (e) {
+ info(`DevToolsServer destroy error: ${e}\n${e.stack}`);
+ }
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+}
+
+async function initPerfFront() {
+ initDevToolsServer();
+ const client = new DevToolsClient(DevToolsServer.connectPipe());
+ await waitUntilClientConnected(client);
+ const front = await client.mainRoot.getFront("perf");
+ return { front, client };
+}
+
+async function initInspectorFront(url) {
+ const target = await addTabTarget(url);
+ const inspector = await target.getFront("inspector");
+ const walker = inspector.walker;
+
+ return { inspector, walker, target };
+}
+
+/**
+ * Wait until a DevToolsClient is connected.
+ * @param {DevToolsClient} client
+ * @return {Promise} Resolves when connected.
+ */
+function waitUntilClientConnected(client) {
+ return client.once("connected");
+}
+
+/**
+ * Wait for eventName on target.
+ * @param {Object} target An observable object that either supports on/off or
+ * addEventListener/removeEventListener
+ * @param {String} eventName
+ * @param {Boolean} useCapture Optional, for addEventListener/removeEventListener
+ * @return A promise that resolves when the event has been handled
+ */
+function once(target, eventName, useCapture = false) {
+ info("Waiting for event: '" + eventName + "' on " + target + ".");
+
+ return new Promise(resolve => {
+ for (const [add, remove] of [
+ ["addEventListener", "removeEventListener"],
+ ["addListener", "removeListener"],
+ ["on", "off"],
+ ]) {
+ if (add in target && remove in target) {
+ target[add](
+ eventName,
+ function onEvent(...aArgs) {
+ info("Got event: '" + eventName + "' on " + target + ".");
+ target[remove](eventName, onEvent, useCapture);
+ resolve(...aArgs);
+ },
+ useCapture
+ );
+ break;
+ }
+ }
+ });
+}
+
+/**
+ * Forces GC, CC and Shrinking GC to get rid of disconnected docshells and
+ * windows.
+ */
+function forceCollections() {
+ Cu.forceGC();
+ Cu.forceCC();
+ Cu.forceShrinkingGC();
+}
+
+registerCleanupFunction(function tearDown() {
+ Services.cookies.removeAll();
+
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+});
+
+function idleWait(time) {
+ return DevToolsUtils.waitForTime(time);
+}
+
+function busyWait(time) {
+ const start = Date.now();
+ let stack;
+ while (Date.now() - start < time) {
+ stack = Components.stack; // eslint-disable-line no-unused-vars
+ }
+}
+
+/**
+ * Waits until a predicate returns true.
+ *
+ * @param function predicate
+ * Invoked once in a while until it returns true.
+ * @param number interval [optional]
+ * How often the predicate is invoked, in milliseconds.
+ */
+function waitUntil(predicate, interval = 10) {
+ if (predicate()) {
+ return Promise.resolve(true);
+ }
+ return new Promise(resolve => {
+ setTimeout(function () {
+ waitUntil(predicate).then(() => resolve(true));
+ }, interval);
+ });
+}
+
+function waitForMarkerType(
+ front,
+ types,
+ predicate,
+ unpackFun = (name, data) => data.markers,
+ eventName = "timeline-data"
+) {
+ types = [].concat(types);
+ predicate =
+ predicate ||
+ function () {
+ return true;
+ };
+ let filteredMarkers = [];
+
+ return new Promise(resolve => {
+ info("Waiting for markers of type: " + types);
+
+ function handler(name, data) {
+ if (typeof name === "string" && name !== "markers") {
+ return;
+ }
+
+ const markers = unpackFun(name, data);
+ info("Got markers");
+
+ filteredMarkers = filteredMarkers.concat(
+ markers.filter(m => types.includes(m.name))
+ );
+
+ if (
+ types.every(t => filteredMarkers.some(m => m.name === t)) &&
+ predicate(filteredMarkers)
+ ) {
+ front.off(eventName, handler);
+ resolve(filteredMarkers);
+ }
+ }
+ front.on(eventName, handler);
+ });
+}
+
+function getCookieId(name, domain, path) {
+ return `${name}${SEPARATOR_GUID}${domain}${SEPARATOR_GUID}${path}`;
+}
+
+/**
+ * Trigger DOM activity and wait for the corresponding accessibility event.
+ * @param {Object} emitter Devtools event emitter, usually a front.
+ * @param {Sting} name Accessibility event in question.
+ * @param {Function} handler Accessibility event handler function with checks.
+ * @param {Promise} task A promise that resolves when DOM activity is done.
+ */
+async function emitA11yEvent(emitter, name, handler, task) {
+ const promise = emitter.once(name, handler);
+ await task();
+ await promise;
+}
+
+/**
+ * Check that accessibilty front is correct and its attributes are also
+ * up-to-date.
+ * @param {Object} front Accessibility front to be tested.
+ * @param {Object} expected A map of a11y front properties to be verified.
+ * @param {Object} expectedFront Expected accessibility front.
+ */
+function checkA11yFront(front, expected, expectedFront) {
+ ok(front, "The accessibility front is created");
+
+ if (expectedFront) {
+ is(front, expectedFront, "Matching accessibility front");
+ }
+
+ // Clone the front so we could modify some values for comparison.
+ front = Object.assign(front);
+ for (const key in expected) {
+ if (key === "checks") {
+ const { CONTRAST } = front[key];
+ // Contrast values are rounded to two digits after the decimal point.
+ if (CONTRAST && CONTRAST.value) {
+ CONTRAST.value = parseFloat(CONTRAST.value.toFixed(2));
+ }
+ }
+
+ if (["actions", "states", "attributes", "checks"].includes(key)) {
+ SimpleTest.isDeeply(
+ front[key],
+ expected[key],
+ `Accessible Front has correct ${key}`
+ );
+ } else {
+ is(front[key], expected[key], `accessibility front has correct ${key}`);
+ }
+ }
+}
+
+function getA11yInitOrShutdownPromise() {
+ return new Promise(resolve => {
+ const observe = (subject, topic, data) => {
+ Services.obs.removeObserver(observe, "a11y-init-or-shutdown");
+ resolve(data);
+ };
+ Services.obs.addObserver(observe, "a11y-init-or-shutdown");
+ });
+}
+
+/**
+ * Wait for accessibility service to shut down. We consider it shut down when
+ * an "a11y-init-or-shutdown" event is received with a value of "0".
+ */
+async function waitForA11yShutdown(parentAccessibility) {
+ await parentAccessibility.disable();
+ if (!Services.appinfo.accessibilityEnabled) {
+ return;
+ }
+
+ await getA11yInitOrShutdownPromise().then(data =>
+ data === "0" ? Promise.resolve() : Promise.reject()
+ );
+}
+
+/**
+ * Wait for accessibility service to initialize. We consider it initialized when
+ * an "a11y-init-or-shutdown" event is received with a value of "1".
+ */
+async function waitForA11yInit() {
+ if (Services.appinfo.accessibilityEnabled) {
+ return;
+ }
+
+ await getA11yInitOrShutdownPromise().then(data =>
+ data === "1" ? Promise.resolve() : Promise.reject()
+ );
+}
diff --git a/devtools/server/tests/browser/inspector-helpers.js b/devtools/server/tests/browser/inspector-helpers.js
new file mode 100644
index 0000000000..0c05432b98
--- /dev/null
+++ b/devtools/server/tests/browser/inspector-helpers.js
@@ -0,0 +1,161 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* exported assertOwnershipTrees, checkMissing, waitForMutation,
+ isSrcChange, isUnretained, isChildList */
+
+function serverOwnershipTree(walkerArg) {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [[walkerArg.actorID]],
+ function (actorID) {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ DevToolsServer,
+ } = require("resource://devtools/server/devtools-server.js");
+ const {
+ DocumentWalker,
+ } = require("resource://devtools/server/actors/inspector/document-walker.js");
+
+ // Convert actorID to current compartment string otherwise
+ // searchAllConnectionsForActor is confused and won't find the actor.
+ actorID = String(actorID);
+ const serverWalker = DevToolsServer.searchAllConnectionsForActor(actorID);
+
+ function sortOwnershipChildrenContentScript(children) {
+ return children.sort((a, b) => a.name.localeCompare(b.name));
+ }
+
+ function serverOwnershipSubtree(walker, node) {
+ const actor = walker.getNode(node);
+ if (!actor) {
+ return undefined;
+ }
+
+ const children = [];
+ const docwalker = new DocumentWalker(node, content);
+ let child = docwalker.firstChild();
+ while (child) {
+ const item = serverOwnershipSubtree(walker, child);
+ if (item) {
+ children.push(item);
+ }
+ child = docwalker.nextSibling();
+ }
+ return {
+ name: actor.actorID,
+ children: sortOwnershipChildrenContentScript(children),
+ };
+ }
+ return {
+ root: serverOwnershipSubtree(serverWalker, serverWalker.rootDoc),
+ orphaned: [...serverWalker._orphaned].map(o =>
+ serverOwnershipSubtree(serverWalker, o.rawNode)
+ ),
+ retained: [...serverWalker._retainedOrphans].map(o =>
+ serverOwnershipSubtree(serverWalker, o.rawNode)
+ ),
+ };
+ }
+ );
+}
+
+function sortOwnershipChildren(children) {
+ return children.sort((a, b) => a.name.localeCompare(b.name));
+}
+
+function clientOwnershipSubtree(node) {
+ return {
+ name: node.actorID,
+ children: sortOwnershipChildren(
+ node.treeChildren().map(child => clientOwnershipSubtree(child))
+ ),
+ };
+}
+
+function clientOwnershipTree(walker) {
+ return {
+ root: clientOwnershipSubtree(walker.rootNode),
+ orphaned: [...walker._orphaned].map(o => clientOwnershipSubtree(o)),
+ retained: [...walker._retainedOrphans].map(o => clientOwnershipSubtree(o)),
+ };
+}
+
+function ownershipTreeSize(tree) {
+ let size = 1;
+ for (const child of tree.children) {
+ size += ownershipTreeSize(child);
+ }
+ return size;
+}
+
+async function assertOwnershipTrees(walker) {
+ const serverTree = await serverOwnershipTree(walker);
+ const clientTree = clientOwnershipTree(walker);
+ is(
+ JSON.stringify(clientTree, null, " "),
+ JSON.stringify(serverTree, null, " "),
+ "Server and client ownership trees should match."
+ );
+
+ return ownershipTreeSize(clientTree.root);
+}
+
+// Verify that an actorID is inaccessible both from the client library and the server.
+async function checkMissing({ client }, actorID) {
+ const front = client.getFrontByID(actorID);
+ ok(
+ !front,
+ "Front shouldn't be accessible from the client for actorID: " + actorID
+ );
+
+ try {
+ await client.request({
+ to: actorID,
+ type: "request",
+ });
+ ok(false, "The actor wasn't missing as the request worked");
+ } catch (e) {
+ is(
+ e.error,
+ "noSuchActor",
+ "node list actor should no longer be contactable."
+ );
+ }
+}
+
+// Load mutations aren't predictable, so keep accumulating mutations until
+// the one we're looking for shows up.
+function waitForMutation(walker, test, mutations = []) {
+ return new Promise(resolve => {
+ for (const change of mutations) {
+ if (test(change)) {
+ resolve(mutations);
+ }
+ }
+
+ walker.once("mutations", newMutations => {
+ waitForMutation(walker, test, mutations.concat(newMutations)).then(
+ finalMutations => {
+ resolve(finalMutations);
+ }
+ );
+ });
+ });
+}
+
+function isSrcChange(change) {
+ return change.type === "attributes" && change.attributeName === "src";
+}
+
+function isUnretained(change) {
+ return change.type === "unretained";
+}
+
+function isChildList(change) {
+ return change.type === "childList";
+}
diff --git a/devtools/server/tests/browser/inspector-isScrollable-data.html b/devtools/server/tests/browser/inspector-isScrollable-data.html
new file mode 100644
index 0000000000..07caabd894
--- /dev/null
+++ b/devtools/server/tests/browser/inspector-isScrollable-data.html
@@ -0,0 +1,79 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Inspector test of isScrollable</title>
+ <style>
+ /* "e" is our custom tag name for "element" */
+ e {
+ background: lightgray;
+ display: inline-block;
+ margin: 10px;
+ padding: 0;
+ border: 0;
+ width: 100px;
+ height: 100px;
+ overflow: auto;
+ }
+
+ /* "c" is our custom tag name for "child" */
+ c {
+ display: block;
+ background: green;
+ }
+
+ .fixedSize {
+ width: 10px;
+ height: 10px;
+ }
+
+ .target {
+ background: red;
+ }
+ </style>
+</head>
+<body id="body">
+<e id="no_children"></e>
+
+<e id="one_child_no_overflow">
+ <c></c>
+</e>
+
+<e id="margin_left_overflow">
+ <c class="target" style="margin-left:100px">abcd</c>
+</e>
+
+<e id="transform_overflow">
+ <c class="target" style="transform: translate(50px)">abcd</c>
+</e>
+
+<e id="nested_overflow">
+ <c>
+ <c class="target" style="margin-left:100px">abcd</c>
+ </c>
+</e>
+
+<e id="intermediate_overflow">
+ <c class="fixedSize target" style="margin-left:100px">
+ <c></c>
+ </c>
+</e>
+
+<e id="multiple_overflow_at_different_depths">
+ <c class="fixedSize target" style="margin-left:100px">
+ <c></c>
+ </c>
+ <c style="margin-left:100px">
+ <c class="target">abcd</c>
+ </c>
+</e>
+
+<e id="overflow_hidden" style="overflow:hidden">
+ <c class="target" style="margin-left:100px">abcd</c>
+</e>
+
+<e id="scrollbar_none" style="scrollbar-width:none">
+ <c class="target" style="margin-left:100px">abcd</c>
+</e>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/inspector-search-data.html b/devtools/server/tests/browser/inspector-search-data.html
new file mode 100644
index 0000000000..784dcb7c9b
--- /dev/null
+++ b/devtools/server/tests/browser/inspector-search-data.html
@@ -0,0 +1,54 @@
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Inspector Search Test Data</title>
+ <style>
+ #pseudo {
+ display: block;
+ margin: 0;
+ }
+ #pseudo:before {
+ content: "before element";
+ }
+ #pseudo:after {
+ content: "after element";
+ }
+ </style>
+ <script type="text/javascript">
+ "use strict";
+
+ window.onload = function() {
+ window.opener.postMessage("ready", "*");
+ };
+ </script>
+</head>
+</body>
+ <!-- A comment
+ spread across multiple lines -->
+
+ <img width="100" height="100" src="large-image.jpg" />
+
+ <h1 id="pseudo">Heading 1</h1>
+ <p>A p tag with the text 'h1' inside of it.
+ <strong>A strong h1 result</strong>
+ </p>
+
+ <div id="arrows" northwest="↖" northeast="↗" southeast="↘" southwest="↙">
+ Unicode arrows
+ </div>
+
+ <h2>Heading 2</h2>
+ <h2>Heading 2</h2>
+ <h2>Heading 2</h2>
+
+ <h3>Heading 3</h3>
+ <h3>Heading 3</h3>
+ <h3>Heading 3</h3>
+
+ <h4>Heading 4</h4>
+ <h4>Heading 4</h4>
+ <h4>Heading 4</h4>
+
+ <div class="💩" id="💩" 💩="💩"></div>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/inspector-shadow.html b/devtools/server/tests/browser/inspector-shadow.html
new file mode 100644
index 0000000000..eb600548e2
--- /dev/null
+++ b/devtools/server/tests/browser/inspector-shadow.html
@@ -0,0 +1,117 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Inspector (empty page)</title>
+ <script>
+ "use strict";
+
+ window.onload = function() {
+ customElements.define("test-empty", class extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({mode: "open"});
+ }
+ });
+
+ customElements.define("test-empty-closed", class extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({mode: "closed"});
+ }
+ });
+
+ customElements.define("test-children", class extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({mode: "open"});
+ this.shadowRoot.innerHTML = `
+ <h1>One child</h1>
+ <p>A second child</p>`;
+ }
+ });
+
+ customElements.define("test-named-slot", class extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({mode: "open"});
+ this.shadowRoot.innerHTML = `
+ <h1>With slot</h1>
+ <slot name="slot1"></slot>`;
+ }
+ });
+
+ customElements.define("test-slot", class extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({mode: "open"});
+ this.shadowRoot.innerHTML = `
+ <style>
+ slot::before { content: "[SLOT BEFORE]"; color: red; }
+ slot::after { content: "[SLOT AFTER]"; color: blue; }
+ </style>
+ <slot></slot>`;
+ }
+ });
+
+ customElements.define("test-simple-slot", class extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({ mode: "open"});
+ this.shadowRoot.innerHTML = "<slot></slot>";
+ }
+ });
+ };
+ </script>
+ <style>
+ #host-pseudo::before { content: "[HOST BEFORE]"; color: red; }
+ #host-pseudo::after { content: "[HOST AFTER]"; color: blue; }
+ </style>
+</head>
+<body>
+ <test-empty id="empty"></test-empty>
+
+ <hr>
+
+ <test-empty id="one-child">
+ <h1>One child</h1>
+ </test-empty>
+
+ <hr>
+
+ <test-children id="shadow-children"></test-children>
+
+ <hr>
+
+ <test-named-slot id="named-slot">
+ <p class="slotted" slot="slot1">Slotted</p>
+ </test-named-slot>
+
+ <hr>
+
+ <test-slot id="slot-pseudo">
+ <span class="has-before">Slotted</span>
+ </test-slot>
+
+ <hr>
+
+ <test-empty id="host-pseudo"></test-empty>
+
+ <hr>
+
+ <test-empty id="mode-open"></test-empty>
+ <test-empty-closed id="mode-closed"></test-empty-closed>
+
+ <hr>
+
+ <test-simple-slot id="slot-inline-text">
+ Lorem ipsum
+ </test-simple-slot>
+
+ <hr>
+ <video id="video-controls" controls></video>
+ <video id="video-controls-with-children" controls>
+ <div>some content</div>
+ </video>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/inspector-traversal-data.html b/devtools/server/tests/browser/inspector-traversal-data.html
new file mode 100644
index 0000000000..6f025747ec
--- /dev/null
+++ b/devtools/server/tests/browser/inspector-traversal-data.html
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Inspector Traversal Test Data</title>
+ <style type="text/css">
+ #pseudo::before {
+ content: "before";
+ }
+ #pseudo::after {
+ content: "after";
+ }
+ #pseudo-empty::before {
+ content: "before an empty element";
+ }
+ #shadow::before {
+ content: "Testing ::before on a shadow host";
+ }
+ </style>
+ <script type="text/javascript">
+ "use strict";
+
+ window.onload = function() {
+ // Set up a basic shadow DOM
+ const host = document.querySelector("#shadow");
+ const root = host.attachShadow({ mode: "open" });
+
+ const h3 = document.createElement("h3");
+ h3.append("Shadow ");
+
+ const em = document.createElement("em");
+ em.append("DOM");
+
+ const select = document.createElement("select");
+ select.setAttribute("multiple", "");
+ h3.appendChild(em);
+ root.appendChild(h3);
+ root.appendChild(select);
+
+ // Put a copy of the body in an iframe to test frame traversal.
+ const body = document.querySelector("body");
+ const data = "data:text/html,<html>" + body.outerHTML + "<html>";
+ const iframe = document.createElement("iframe");
+ iframe.setAttribute("id", "childFrame");
+ iframe.src = data;
+ body.appendChild(iframe);
+ };
+ </script>
+</head>
+<body style="background-color:white">
+ <h1>Inspector Actor Tests</h1>
+ <span id="longstring">longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong</span>
+ <span id="shortstring">short</span>
+ <span id="empty"></span>
+ <div id="longlist" data-test="exists">
+ <div id="a">a</div>
+ <div id="b">b</div>
+ <div id="c">c</div>
+ <div id="d">d</div>
+ <div id="e">e</div>
+ <div id="f">f</div>
+ <div id="g">g</div>
+ <div id="h">h</div>
+ <div id="i">i</div>
+ <div id="j">j</div>
+ <div id="k">k</div>
+ <div id="l">l</div>
+ <div id="m">m</div>
+ <div id="n">n</div>
+ <div id="o">o</div>
+ <div id="p">p</div>
+ <div id="q">q</div>
+ <div id="r">r</div>
+ <div id="s">s</div>
+ <div id="t">t</div>
+ <div id="u">u</div>
+ <div id="v">v</div>
+ <div id="w">w</div>
+ <div id="x">x</div>
+ <div id="y">y</div>
+ <div id="z">z</div>
+ </div>
+ <div id="longlist-sibling">
+ <div id="longlist-sibling-firstchild"></div>
+ </div>
+ <p id="edit-html"></p>
+
+ <select multiple><option>one</option><option>two</option></select>
+ <div id="pseudo"><span>middle</span></div>
+ <div id="pseudo-empty"></div>
+ <div id="shadow">light dom</div>
+ <object>
+ <div id="1"></div>
+ </object>
+ <div class="node-to-duplicate"></div>
+ <div id="scroll-into-view" style="margin-top: 1000px;">scroll</div>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/storage-cookies-same-name.html b/devtools/server/tests/browser/storage-cookies-same-name.html
new file mode 100644
index 0000000000..235c8a451f
--- /dev/null
+++ b/devtools/server/tests/browser/storage-cookies-same-name.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector cookies with duplicate names</title>
+</head>
+<body onload="createCookies()">
+<script type="application/javascript">
+"use strict";
+// eslint-disable-next-line no-unused-vars
+function createCookies() {
+ document.cookie = "name=value1;path=/;";
+ document.cookie = "name=value2;path=/path2/;";
+ document.cookie = "name=value3;path=/path3/;";
+}
+
+window.removeCookie = function (name) {
+ document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT";
+};
+
+window.clearCookies = function () {
+ const cookies = document.cookie;
+ for (const cookie of cookies.split(";")) {
+ window.removeCookie(cookie.split("=")[0]);
+ }
+};
+</script>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/storage-dynamic-windows.html b/devtools/server/tests/browser/storage-dynamic-windows.html
new file mode 100644
index 0000000000..22df8a255e
--- /dev/null
+++ b/devtools/server/tests/browser/storage-dynamic-windows.html
@@ -0,0 +1,117 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 965872 - Storage inspector actor with cookies, local storage and session storage.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector test for listing hosts and storages</title>
+</head>
+<body>
+<iframe src="http://sectest1.example.org/browser/devtools/server/tests/browser/storage-unsecured-iframe.html"></iframe>
+<script type="application/javascript">
+"use strict";
+const partialHostname = location.hostname.match(/^[^.]+(\..*)$/)[1];
+const cookieExpiresTime1 = 2000000000000;
+const cookieExpiresTime2 = 2000000001000;
+// Setting up some cookies to eat.
+document.cookie = "c1=foobar; expires=" +
+ new Date(cookieExpiresTime1).toGMTString() + "; path=/browser";
+document.cookie = "cs2=sessionCookie; path=/; domain=" + partialHostname;
+document.cookie = "c3=foobar-2; expires=" +
+ new Date(cookieExpiresTime2).toGMTString() + "; path=/";
+// ... and some local storage items ..
+localStorage.setItem("ls1", "foobar");
+localStorage.setItem("ls2", "foobar-2");
+// ... and finally some session storage items too
+sessionStorage.setItem("ss1", "foobar-3");
+
+const idbGenerator = async function () {
+ let request = indexedDB.open("idb1", 1);
+ request.onerror = function() {
+ throw new Error("error opening db connection");
+ };
+ const db = await new Promise(done => {
+ request.onupgradeneeded = event => {
+ const dbResult = event.target.result;
+ const store1 = dbResult.createObjectStore("obj1", { keyPath: "id" });
+ store1.createIndex("name", "name", { unique: false });
+ store1.createIndex("email", "email", { unique: true });
+ dbResult.createObjectStore("obj2", { keyPath: "id2" });
+ store1.transaction.oncomplete = () => {
+ done(dbResult);
+ };
+ };
+ });
+
+ // Prevents AbortError
+ await new Promise(done => {
+ request.onsuccess = done;
+ });
+
+ const transaction = db.transaction(["obj1", "obj2"], "readwrite");
+ const store1 = transaction.objectStore("obj1");
+ const store2 = transaction.objectStore("obj2");
+ store1.add({id: 1, name: "foo", email: "foo@bar.com"});
+ store1.add({id: 2, name: "foo2", email: "foo2@bar.com"});
+ store1.add({id: 3, name: "foo2", email: "foo3@bar.com"});
+ store2.add({
+ id2: 1,
+ name: "foo",
+ email: "foo@bar.com",
+ extra: "baz"
+ });
+ // Prevents AbortError during close()
+ await new Promise(success => {
+ transaction.oncomplete = success;
+ });
+
+ db.close();
+
+ request = indexedDB.open("idb2", 1);
+ const db2 = await new Promise(done => {
+ request.onupgradeneeded = event => {
+ const db2Result = event.target.result;
+ const store3 = db2Result.createObjectStore("obj3", { keyPath: "id3" });
+ store3.createIndex("name2", "name2", { unique: true });
+ store3.transaction.oncomplete = () => {
+ done(db2Result);
+ }
+ };
+ });
+ // Prevents AbortError during close()
+ await new Promise(done => {
+ request.onsuccess = done;
+ });
+ db2.close();
+
+ console.log("added cookies and stuff from main page");
+};
+
+function deleteDB(dbName) {
+ return new Promise(resolve => {
+ dump("removing database " + dbName + " from " + document.location + "\n");
+ indexedDB.deleteDatabase(dbName).onsuccess = resolve;
+ });
+}
+
+window.setup = async function () {
+ await idbGenerator();
+};
+
+window.clear = async function () {
+ document.cookie = "c1=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
+ document.cookie = "c3=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
+ document.cookie = "cs2=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
+
+ localStorage.clear();
+
+ await deleteDB("idb1");
+ await deleteDB("idb2");
+
+ dump("removed cookies, localStorage and indexedDB data from " +
+ document.location + "\n");
+};
+</script>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/storage-helpers.js b/devtools/server/tests/browser/storage-helpers.js
new file mode 100644
index 0000000000..1315c77b31
--- /dev/null
+++ b/devtools/server/tests/browser/storage-helpers.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This file assumes head.js is loaded in the global scope.
+/* import-globals-from head.js */
+
+/* exported openTabAndSetupStorage, clearStorage */
+
+"use strict";
+
+/**
+ * This generator function opens the given url in a new tab, then sets up the
+ * page by waiting for all cookies, indexedDB items etc. to be created.
+ *
+ * @param url {String} The url to be opened in the new tab
+ *
+ * @return {Promise} A promise that resolves after storage inspector is ready
+ */
+async function openTabAndSetupStorage(url) {
+ await addTab(url);
+
+ // Setup the async storages in main window and for all its iframes
+ const browsingContexts =
+ gBrowser.selectedBrowser.browsingContext.getAllBrowsingContextsInSubtree();
+ for (const browsingContext of browsingContexts) {
+ await SpecialPowers.spawn(browsingContext, [], async function () {
+ if (content.wrappedJSObject.setup) {
+ await content.wrappedJSObject.setup();
+ }
+ });
+ }
+
+ // selected tab is set in addTab
+ const commands = await CommandsFactory.forTab(gBrowser.selectedTab);
+ await commands.targetCommand.startListening();
+ const target = commands.targetCommand.targetFront;
+ return { commands, target };
+}
+
+async function clearStorage() {
+ const browsingContexts =
+ gBrowser.selectedBrowser.browsingContext.getAllBrowsingContextsInSubtree();
+ for (const browsingContext of browsingContexts) {
+ await SpecialPowers.spawn(browsingContext, [], async function () {
+ if (content.wrappedJSObject.clear) {
+ await content.wrappedJSObject.clear();
+ }
+ });
+ }
+}
diff --git a/devtools/server/tests/browser/storage-listings.html b/devtools/server/tests/browser/storage-listings.html
new file mode 100644
index 0000000000..98ac182bd0
--- /dev/null
+++ b/devtools/server/tests/browser/storage-listings.html
@@ -0,0 +1,123 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 965872 - Storage inspector actor with cookies, local storage and session storage.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector test for listing hosts and storages</title>
+</head>
+<body>
+<iframe src="http://sectest1.example.org/browser/devtools/server/tests/browser/storage-unsecured-iframe.html"></iframe>
+<iframe src="https://sectest1.example.org:443/browser/devtools/server/tests/browser/storage-secured-iframe.html"></iframe>
+<script type="application/javascript">
+"use strict";
+const partialHostname = location.hostname.match(/^[^.]+(\..*)$/)[1];
+const cookieExpiresTime1 = 2000000000000;
+const cookieExpiresTime2 = 2000000001000;
+// Setting up some cookies to eat.
+document.cookie = "c1=foobar; expires=" +
+ new Date(cookieExpiresTime1).toGMTString() + "; path=/browser";
+document.cookie = "cs2=sessionCookie; path=/; domain=" + partialHostname;
+document.cookie = "c3=foobar-2; secure=true; expires=" +
+ new Date(cookieExpiresTime2).toGMTString() + "; path=/";
+// ... and some local storage items ..
+localStorage.setItem("ls1", "foobar");
+localStorage.setItem("ls2", "foobar-2");
+// ... and finally some session storage items too
+sessionStorage.setItem("ss1", "foobar-3");
+console.log("added cookies and stuff from main page");
+
+const idbGenerator = async function () {
+ let request = indexedDB.open("idb1", 1);
+ request.onerror = function() {
+ throw new Error("error opening db connection");
+ };
+ const db = await new Promise(done => {
+ request.onupgradeneeded = event => {
+ const dbResult = event.target.result;
+ const store1 = dbResult.createObjectStore("obj1", { keyPath: "id" });
+ store1.createIndex("name", "name", { unique: false });
+ store1.createIndex("email", "email", { unique: true });
+ dbResult.createObjectStore("obj2", { keyPath: "id2" });
+ store1.transaction.oncomplete = () => {
+ done(dbResult);
+ };
+ };
+ });
+
+ // Prevents AbortError
+ await new Promise(done => {
+ request.onsuccess = done;
+ });
+
+ const transaction = db.transaction(["obj1", "obj2"], "readwrite");
+ const store1 = transaction.objectStore("obj1");
+ const store2 = transaction.objectStore("obj2");
+ store1.add({id: 1, name: "foo", email: "foo@bar.com"});
+ store1.add({id: 2, name: "foo2", email: "foo2@bar.com"});
+ store1.add({id: 3, name: "foo2", email: "foo3@bar.com"});
+ store2.add({
+ id2: 1,
+ name: "foo",
+ email: "foo@bar.com",
+ extra: "baz"
+ });
+ // Prevents AbortError during close()
+ await new Promise(success => {
+ transaction.oncomplete = success;
+ });
+
+ db.close();
+
+ request = indexedDB.open("idb2", 1);
+ const db2 = await new Promise(done => {
+ request.onupgradeneeded = event => {
+ const db2Result = event.target.result;
+ const store3 = db2Result.createObjectStore("obj3", { keyPath: "id3" });
+ store3.createIndex("name2", "name2", { unique: true });
+ store3.transaction.oncomplete = () => {
+ done(db2Result);
+ }
+ };
+ });
+ // Prevents AbortError during close()
+ await new Promise(done => {
+ request.onsuccess = done;
+ });
+ db2.close();
+
+ dump("added cookies and stuff from main page\n");
+};
+
+function deleteDB(dbName) {
+ return new Promise(resolve => {
+ dump("removing database " + dbName + " from " + document.location + "\n");
+ indexedDB.deleteDatabase(dbName).onsuccess = resolve;
+ });
+}
+
+window.setup = async function () {
+ await idbGenerator();
+};
+
+window.clear = async function () {
+ document.cookie = "c1=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/browser";
+ document.cookie =
+ "c3=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; secure=true";
+ document.cookie =
+ "cs2=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=" +
+ partialHostname;
+
+ localStorage.clear();
+ sessionStorage.clear();
+
+ await deleteDB("idb1");
+ await deleteDB("idb2");
+
+ dump("removed cookies, localStorage, sessionStorage and indexedDB data " +
+ "from " + document.location + "\n");
+};
+</script>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/storage-secured-iframe.html b/devtools/server/tests/browser/storage-secured-iframe.html
new file mode 100644
index 0000000000..c2fe4ed485
--- /dev/null
+++ b/devtools/server/tests/browser/storage-secured-iframe.html
@@ -0,0 +1,94 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Iframe for testing multiple host detetion in storage actor
+-->
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+<script type="application/javascript">
+"use strict";
+document.cookie = "sc1=foobar;";
+localStorage.setItem("iframe-s-ls1", "foobar");
+sessionStorage.setItem("iframe-s-ss1", "foobar-2");
+
+const idbGenerator = async function () {
+ let request = indexedDB.open("idb-s1", 1);
+ request.onerror = function() {
+ throw new Error("error opening db connection");
+ };
+ const db = await new Promise(done => {
+ request.onupgradeneeded = event => {
+ const dbResult = event.target.result;
+ const store1 = dbResult.createObjectStore("obj-s1", { keyPath: "id" });
+ store1.transaction.oncomplete = () => {
+ done(dbResult);
+ };
+ };
+ });
+ await new Promise(done => {
+ request.onsuccess = done;
+ });
+
+ let transaction = db.transaction(["obj-s1"], "readwrite");
+ const store1 = transaction.objectStore("obj-s1");
+ store1.add({id: 6, name: "foo", email: "foo@bar.com"});
+ store1.add({id: 7, name: "foo2", email: "foo2@bar.com"});
+ await new Promise(success => {
+ transaction.oncomplete = success;
+ });
+
+ db.close();
+
+ request = indexedDB.open("idb-s2", 1);
+ const db2 = await new Promise(done => {
+ request.onupgradeneeded = event => {
+ const db2Result = event.target.result;
+ const store3 =
+ db2Result.createObjectStore("obj-s2", { keyPath: "id3", autoIncrement: true });
+ store3.createIndex("name2", "name2", { unique: true });
+ store3.transaction.oncomplete = () => {
+ done(db2Result);
+ };
+ };
+ });
+ await new Promise(done => {
+ request.onsuccess = done;
+ });
+
+ transaction = db2.transaction(["obj-s2"], "readwrite");
+ const store3 = transaction.objectStore("obj-s2");
+ store3.add({id3: 16, name2: "foo", email: "foo@bar.com"});
+ await new Promise(success => {
+ transaction.oncomplete = success;
+ });
+
+ db2.close();
+ dump("added cookies and stuff from secured iframe\n");
+}
+
+function deleteDB(dbName) {
+ return new Promise(resolve => {
+ dump("removing database " + dbName + " from " + document.location + "\n");
+ indexedDB.deleteDatabase(dbName).onsuccess = resolve;
+ });
+}
+
+window.setup = async function () {
+ await idbGenerator();
+};
+
+window.clear = async function () {
+ document.cookie = "sc1=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
+
+ localStorage.clear();
+
+ await deleteDB("idb-s1");
+ await deleteDB("idb-s2");
+
+ console.log("removed cookies and stuff from secured iframe");
+}
+</script>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/storage-unsecured-iframe.html b/devtools/server/tests/browser/storage-unsecured-iframe.html
new file mode 100644
index 0000000000..db70c9c692
--- /dev/null
+++ b/devtools/server/tests/browser/storage-unsecured-iframe.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Iframe for testing multiple host detetion in storage actor
+-->
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+<script>
+"use strict";
+
+document.cookie = "uc1=foobar; domain=.example.org; path=/; secure=true";
+localStorage.setItem("iframe-u-ls1", "foobar");
+sessionStorage.setItem("iframe-u-ss1", "foobar1");
+sessionStorage.setItem("iframe-u-ss2", "foobar2");
+console.log("added cookies and stuff from unsecured iframe");
+
+window.clear = function () {
+ document.cookie = "uc1=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
+ localStorage.clear();
+ sessionStorage.clear();
+ console.log("removed cookies and stuff from unsecured iframe");
+};
+
+</script>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/storage-updates.html b/devtools/server/tests/browser/storage-updates.html
new file mode 100644
index 0000000000..594c28ce0f
--- /dev/null
+++ b/devtools/server/tests/browser/storage-updates.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 965872 - Storage inspector actor with cookies, local storage and session storage.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector blank html for tests</title>
+</head>
+<body>
+<script type="application/javascript">
+"use strict";
+window.addCookie = function(name, value, path, domain, expires, secure) {
+ let cookieString = name + "=" + value + ";";
+ if (path) {
+ cookieString += "path=" + path + ";";
+ }
+ if (domain) {
+ cookieString += "domain=" + domain + ";";
+ }
+ if (expires) {
+ cookieString += "expires=" + expires + ";";
+ }
+ if (secure) {
+ cookieString += "secure=true;";
+ }
+ document.cookie = cookieString;
+};
+
+window.removeCookie = function(name) {
+ document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT";
+};
+
+window.clearLocalAndSessionStores = function() {
+ localStorage.clear();
+ sessionStorage.clear();
+};
+
+window.clearCookies = function() {
+ const cookies = document.cookie;
+ for (const cookie of cookies.split(";")) {
+ window.removeCookie(cookie.split("=")[0]);
+ }
+};
+</script>
+</body>
+</html>
diff --git a/devtools/server/tests/browser/test-errors-actor.js b/devtools/server/tests/browser/test-errors-actor.js
new file mode 100644
index 0000000000..e476324be4
--- /dev/null
+++ b/devtools/server/tests/browser/test-errors-actor.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const protocol = require("resource://devtools/shared/protocol.js");
+
+const testErrorsSpec = protocol.generateActorSpec({
+ typeName: "testErrors",
+
+ methods: {
+ throwsComponentsException: {
+ request: {},
+ response: {},
+ },
+ throwsException: {
+ request: {},
+ response: {},
+ },
+ throwsJSError: {
+ request: {},
+ response: {},
+ },
+ throwsString: {
+ request: {},
+ response: {},
+ },
+ throwsObject: {
+ request: {},
+ response: {},
+ },
+ },
+});
+
+class TestErrorsActor extends protocol.Actor {
+ constructor(conn) {
+ super(conn, testErrorsSpec);
+ }
+
+ throwsComponentsException() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ throwsException() {
+ return this.a.b.c;
+ }
+
+ throwsJSError() {
+ throw new Error("JSError");
+ }
+
+ throwsString() {
+ // eslint-disable-next-line no-throw-literal
+ throw "ErrorString";
+ }
+
+ throwsObject() {
+ // eslint-disable-next-line no-throw-literal
+ throw {
+ error: "foo",
+ };
+ }
+}
+exports.TestErrorsActor = TestErrorsActor;
+
+class TestErrorsFront extends protocol.FrontClassWithSpec(testErrorsSpec) {
+ constructor(client) {
+ super(client);
+ this.formAttributeName = "testErrorsActor";
+ }
+}
+protocol.registerFront(TestErrorsFront);
diff --git a/devtools/server/tests/browser/test-window.xhtml b/devtools/server/tests/browser/test-window.xhtml
new file mode 100644
index 0000000000..33e70e2dee
--- /dev/null
+++ b/devtools/server/tests/browser/test-window.xhtml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xul:window xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Test page">
+</xul:window>
diff --git a/devtools/server/tests/chrome/Debugger.Source.prototype.element-2.js b/devtools/server/tests/chrome/Debugger.Source.prototype.element-2.js
new file mode 100644
index 0000000000..7260431428
--- /dev/null
+++ b/devtools/server/tests/chrome/Debugger.Source.prototype.element-2.js
@@ -0,0 +1,4 @@
+"use strict";
+
+// eslint-disable-next-line no-debugger
+debugger;
diff --git a/devtools/server/tests/chrome/Debugger.Source.prototype.element.html b/devtools/server/tests/chrome/Debugger.Source.prototype.element.html
new file mode 100644
index 0000000000..6959ad970d
--- /dev/null
+++ b/devtools/server/tests/chrome/Debugger.Source.prototype.element.html
@@ -0,0 +1,25 @@
+<head>
+ <!-- Static (not dynamically inserted) inline script. -->
+ <script id='franz'>
+ /* exported franz */
+ "use strict";
+
+ function franz() {
+ // eslint-disable-next-line no-debugger
+ debugger;
+ }
+ </script>
+
+ <!-- Static out-of-line script element. -->
+ <script id='heinrich' src='Debugger.Source.prototype.element.js'></script>
+</head>
+
+<!-- HTML requires some body element onfoo attributes to add handlers to the
+ *window*, not the element --- but Debugger.Source.prototype.element should
+ return the element. Here, that rule should apply to the body's 'onresize'
+ handler. (For the reason for the 'cancelable' check, see the code that
+ sends the event.) -->
+<body onresize='if (event.cancelable) debugger;'>
+ <!-- Ordinary content element with event handler. -->
+ <div id='heidi' onclick='heinrichFun();'>Heidi</div>
+</body>
diff --git a/devtools/server/tests/chrome/Debugger.Source.prototype.element.js b/devtools/server/tests/chrome/Debugger.Source.prototype.element.js
new file mode 100644
index 0000000000..095398ddad
--- /dev/null
+++ b/devtools/server/tests/chrome/Debugger.Source.prototype.element.js
@@ -0,0 +1,7 @@
+/* exported heinrichFun */
+/* global franz */
+"use strict";
+
+function heinrichFun() {
+ franz();
+}
diff --git a/devtools/server/tests/chrome/chrome.toml b/devtools/server/tests/chrome/chrome.toml
new file mode 100644
index 0000000000..b67b1ee971
--- /dev/null
+++ b/devtools/server/tests/chrome/chrome.toml
@@ -0,0 +1,150 @@
+[DEFAULT]
+tags = "devtools"
+skip-if = ["os == 'android'"]
+support-files = [
+ "doc_Debugger.Source.prototype.introductionType.xhtml",
+ "Debugger.Source.prototype.element.js",
+ "Debugger.Source.prototype.element-2.js",
+ "Debugger.Source.prototype.element.html",
+ "hello-actor.js",
+ "iframe1_makeGlobalObjectReference.html",
+ "iframe2_makeGlobalObjectReference.html",
+ "inspector_css-properties.html",
+ "inspector_display-type.html",
+ "inspector_getImageData.html",
+ "inspector_getOffsetParent.html",
+ "inspector-delay-image-response.sjs",
+ "inspector-eyedropper.html",
+ "inspector-helpers.js",
+ "inspector-search-data.html",
+ "inspector-styles-data.css",
+ "inspector-styles-data.html",
+ "inspector-template.html",
+ "inspector-traversal-data.html",
+ "large-image.jpg",
+ "memory-helpers.js",
+ "nonchrome_unsafeDereference.html",
+ "suspendTimeouts_content.html",
+ "suspendTimeouts_content.js",
+ "suspendTimeouts_worker.js",
+ "small-image.gif",
+ "test_suspendTimeouts.js",
+ "webconsole-helpers.js",
+ "inactive-property-helper/*.mjs",
+]
+
+["test_Debugger.Script.prototype.global.html"]
+
+["test_Debugger.Source.prototype.elementAttribute.html"]
+
+["test_Debugger.Source.prototype.introductionScript.html"]
+
+["test_Debugger.Source.prototype.introductionType.html"]
+
+["test_animation-type-longhand.html"]
+
+["test_css-logic-specificity.html"]
+
+["test_css-logic.html"]
+
+["test_css-properties.html"]
+
+["test_device.html"]
+
+["test_executeInGlobal-outerized_this.html"]
+
+["test_highlighter_paused_debugger.html"]
+
+["test_inspector-changeattrs.html"]
+
+["test_inspector-changevalue.html"]
+
+["test_inspector-display-type.html"]
+
+["test_inspector-duplicate-node.html"]
+
+["test_inspector-hide.html"]
+
+["test_inspector-inactive-property-helper.html"]
+
+["test_inspector-mutations-attr.html"]
+
+["test_inspector-mutations-events.html"]
+
+["test_inspector-mutations-value.html"]
+
+["test_inspector-pick-color.html"]
+
+["test_inspector-pseudoclass-lock.html"]
+
+["test_inspector-reload.html"]
+
+["test_inspector-resize.html"]
+
+["test_inspector-resolve-url.html"]
+
+["test_inspector-scroll-into-view.html"]
+
+["test_inspector-search-front.html"]
+
+["test_inspector-template.html"]
+
+["test_inspector_getImageData-wait-for-load.html"]
+
+["test_inspector_getImageData.html"]
+
+["test_inspector_getImageDataFromURL.html"]
+
+["test_inspector_getNodeFromActor.html"]
+
+["test_inspector_getOffsetParent.html"]
+
+["test_makeGlobalObjectReference.html"]
+
+["test_memory.html"]
+
+["test_memory_allocations_02.html"]
+
+["test_memory_allocations_03.html"]
+
+["test_memory_allocations_04.html"]
+
+["test_memory_allocations_05.html"]
+
+["test_memory_allocations_06.html"]
+
+["test_memory_allocations_07.html"]
+
+["test_memory_attach_01.html"]
+
+["test_memory_attach_02.html"]
+
+["test_memory_census.html"]
+
+["test_memory_gc_01.html"]
+
+["test_memory_gc_events.html"]
+
+["test_overflowing-body.html"]
+
+["test_overflowing-children.html"]
+
+["test_preference.html"]
+
+["test_styles-applied.html"]
+
+["test_styles-computed.html"]
+
+["test_styles-layout.html"]
+
+["test_styles-matched.html"]
+
+["test_styles-modify.html"]
+
+["test_styles-svg.html"]
+
+["test_suspendTimeouts.html"]
+
+["test_unsafeDereference.html"]
+
+["test_webconsole-node-grip.html"]
diff --git a/devtools/server/tests/chrome/doc_Debugger.Source.prototype.introductionType.xhtml b/devtools/server/tests/chrome/doc_Debugger.Source.prototype.introductionType.xhtml
new file mode 100644
index 0000000000..b037190c9a
--- /dev/null
+++ b/devtools/server/tests/chrome/doc_Debugger.Source.prototype.introductionType.xhtml
@@ -0,0 +1,7 @@
+<?xml version="1.0"?>
+<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'>
+<script id='xulie'>
+/* eslint-disable strict, no-unused-vars, no-debugger */
+function xulScriptFunc() { debugger; }
+</script>
+</window>
diff --git a/devtools/server/tests/chrome/hello-actor.js b/devtools/server/tests/chrome/hello-actor.js
new file mode 100644
index 0000000000..eabb4a6773
--- /dev/null
+++ b/devtools/server/tests/chrome/hello-actor.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* exported HelloActor */
+"use strict";
+
+const protocol = require("resource://devtools/shared/protocol.js");
+
+const helloSpec = protocol.generateActorSpec({
+ typeName: "helloActor",
+
+ methods: {
+ count: {
+ request: {},
+ response: { count: protocol.RetVal("number") },
+ },
+ },
+});
+
+class HelloActor extends protocol.Actor {
+ constructor(conn) {
+ super(conn, helloSpec);
+ this.counter = 0;
+ }
+
+ count() {
+ return ++this.counter;
+ }
+}
diff --git a/devtools/server/tests/chrome/iframe1_makeGlobalObjectReference.html b/devtools/server/tests/chrome/iframe1_makeGlobalObjectReference.html
new file mode 100644
index 0000000000..bab5a70765
--- /dev/null
+++ b/devtools/server/tests/chrome/iframe1_makeGlobalObjectReference.html
@@ -0,0 +1 @@
+<html>The word 'smorgasbord' spoken by an adorably plump child, symbolizing prosperity</html>
diff --git a/devtools/server/tests/chrome/iframe2_makeGlobalObjectReference.html b/devtools/server/tests/chrome/iframe2_makeGlobalObjectReference.html
new file mode 100644
index 0000000000..b297ca8a2b
--- /dev/null
+++ b/devtools/server/tests/chrome/iframe2_makeGlobalObjectReference.html
@@ -0,0 +1 @@
+<html>Her retrospection, in hindsight, was prescient.</html>
diff --git a/devtools/server/tests/chrome/inactive-property-helper/align-content.mjs b/devtools/server/tests/chrome/inactive-property-helper/align-content.mjs
new file mode 100644
index 0000000000..a871081fad
--- /dev/null
+++ b/devtools/server/tests/chrome/inactive-property-helper/align-content.mjs
@@ -0,0 +1,92 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// InactivePropertyHelper `align-content` test cases.
+
+export default [
+ {
+ info: "align-content is inactive on block elements (until bug 1105571 is fixed)",
+ property: "align-content",
+ tagName: "div",
+ rules: ["div { align-content: center; }"],
+ isActive: false,
+ },
+ {
+ info: "align-content is active on flex containers",
+ property: "align-content",
+ tagName: "div",
+ rules: ["div { align-content: center; display: flex; }"],
+ isActive: true,
+ },
+ {
+ info: "align-content is active on grid containers",
+ property: "align-content",
+ tagName: "div",
+ rules: ["div { align-content: center; display: grid; }"],
+ isActive: true,
+ },
+ {
+ info: "align-content is inactive on flex items",
+ property: "align-content",
+ createTestElement: rootNode => {
+ const container = document.createElement("div");
+ const element = document.createElement("span");
+ container.append(element);
+ rootNode.append(container);
+ return element;
+ },
+ rules: ["div { display: flex; }", "span { align-content: center; }"],
+ ruleIndex: 1,
+ isActive: false,
+ },
+ {
+ info: "align-content is inactive on grid items",
+ property: "align-content",
+ createTestElement: rootNode => {
+ const container = document.createElement("div");
+ const element = document.createElement("span");
+ container.append(element);
+ rootNode.append(container);
+ return element;
+ },
+ rules: ["div { display: grid; }", "span { align-content: center; }"],
+ ruleIndex: 1,
+ isActive: false,
+ },
+ {
+ info: "align-content:baseline is active on flex items",
+ property: "align-content",
+ createTestElement: rootNode => {
+ const container = document.createElement("div");
+ const element = document.createElement("span");
+ container.append(element);
+ rootNode.append(container);
+ return element;
+ },
+ rules: ["div { display: flex; }", "span { align-content: baseline; }"],
+ ruleIndex: 1,
+ isActive: true,
+ },
+ {
+ info: "align-content:baseline is active on grid items",
+ property: "align-content",
+ createTestElement: rootNode => {
+ const container = document.createElement("div");
+ const element = document.createElement("span");
+ container.append(element);
+ rootNode.append(container);
+ return element;
+ },
+ rules: ["div { display: grid; }", "span { align-content: baseline; }"],
+ ruleIndex: 1,
+ isActive: true,
+ },
+ {
+ info: "align-content:baseline is active on table cells",
+ property: "align-content",
+ tagName: "div",
+ rules: ["div { display: table-cell; align-content: baseline; }"],
+ isActive: true,
+ },
+];
diff --git a/devtools/server/tests/chrome/inactive-property-helper/border-image.mjs b/devtools/server/tests/chrome/inactive-property-helper/border-image.mjs
new file mode 100644
index 0000000000..85c57418a4
--- /dev/null
+++ b/devtools/server/tests/chrome/inactive-property-helper/border-image.mjs
@@ -0,0 +1,162 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// InactivePropertyHelper `border-image` test cases.
+export default [
+ {
+ info: "border-image is active on another element then a table element or internal table element where border-collapse is not set to collapse",
+ property: "border-image",
+ tagName: "div",
+ rules: ["div { border-image: linear-gradient(red, yellow) 10; }"],
+ isActive: true,
+ },
+ {
+ info: "border-image is active on another element then a table element or internal table element where border-collapse is set to collapse",
+ property: "border-image",
+ tagName: "div",
+ rules: [
+ "div { border-image: linear-gradient(red, yellow) 10; border-collapse: collapse;}",
+ ],
+ isActive: true,
+ },
+ {
+ info: "border-image is active on a td element with no table parent and the browser is not crashing",
+ property: "border-image",
+ tagName: "td",
+ rules: [
+ "td { border-image: linear-gradient(red, yellow) 10; border-collapse: collapse;}",
+ ],
+ isActive: true,
+ },
+ createTableElementsToTestBorderImage({
+ useDivTagWithDisplayTableStyle: false,
+ borderCollapse: true,
+ borderCollapsePropertyIsInherited: false,
+ isActive: true,
+ }),
+ createTableElementsToTestBorderImage({
+ useDivTagWithDisplayTableStyle: false,
+ borderCollapse: false,
+ borderCollapsePropertyIsInherited: false,
+ isActive: true,
+ }),
+ createTableElementsToTestBorderImage({
+ useDivTagWithDisplayTableStyle: false,
+ borderCollapse: true,
+ borderCollapsePropertyIsInherited: true,
+ isActive: false,
+ }),
+ createTableElementsToTestBorderImage({
+ useDivTagWithDisplayTableStyle: false,
+ borderCollapse: false,
+ borderCollapsePropertyIsInherited: true,
+ isActive: true,
+ }),
+ createTableElementsToTestBorderImage({
+ useDivTagWithDisplayTableStyle: true,
+ borderCollapse: true,
+ borderCollapsePropertyIsInherited: false,
+ isActive: true,
+ }),
+ createTableElementsToTestBorderImage({
+ useDivTagWithDisplayTableStyle: true,
+ borderCollapse: false,
+ borderCollapsePropertyIsInherited: false,
+ isActive: true,
+ }),
+ createTableElementsToTestBorderImage({
+ useDivTagWithDisplayTableStyle: true,
+ borderCollapse: true,
+ borderCollapsePropertyIsInherited: true,
+ isActive: false,
+ }),
+ createTableElementsToTestBorderImage({
+ useDivTagWithDisplayTableStyle: true,
+ borderCollapse: false,
+ borderCollapsePropertyIsInherited: true,
+ isActive: true,
+ }),
+];
+
+/**
+ * @param {Object} testParameters
+ * @param {bool} testParameters.useDivTagWithDisplayTableStyle use generic divs using display property instead of actual table/tr/td tags
+ * @param {bool} testParameters.borderCollapse is `border-collapse` property set to `collapse` ( instead of `separate`)
+ * @param {bool} testParameters.borderCollapsePropertyIsInherited should the border collapse property be inherited from the table parent (instead of directly set on the internal table element)
+ * @param {bool} testParameters.isActive is the border-image property actve on the element
+ * @returns
+ */
+function createTableElementsToTestBorderImage({
+ useDivTagWithDisplayTableStyle,
+ borderCollapse,
+ borderCollapsePropertyIsInherited,
+ isActive,
+}) {
+ return {
+ info: `border-image is ${
+ isActive ? "active" : "inactive"
+ } on an internal table element where border-collapse is${
+ borderCollapse ? "" : " not"
+ } set to collapse${
+ borderCollapsePropertyIsInherited
+ ? " by being inherited from its table parent"
+ : ""
+ } when the table and its internal elements are ${
+ useDivTagWithDisplayTableStyle ? "not " : ""
+ }using semantic tags (table, tr, td, ...)`,
+ property: "border-image",
+ createTestElement: rootNode => {
+ const table = useDivTagWithDisplayTableStyle
+ ? document.createElement("div")
+ : document.createElement("table");
+ if (useDivTagWithDisplayTableStyle) {
+ table.style.display = "table";
+ }
+ if (borderCollapsePropertyIsInherited) {
+ table.style.borderCollapse = `${
+ borderCollapse ? "collapse" : "separate"
+ }`;
+ }
+ rootNode.appendChild(table);
+
+ const tbody = useDivTagWithDisplayTableStyle
+ ? document.createElement("div")
+ : document.createElement("tbody");
+ if (useDivTagWithDisplayTableStyle) {
+ tbody.style.display = "table-row-group";
+ }
+ table.appendChild(tbody);
+
+ const tr = useDivTagWithDisplayTableStyle
+ ? document.createElement("div")
+ : document.createElement("tr");
+ if (useDivTagWithDisplayTableStyle) {
+ tr.style.display = "table-row";
+ }
+ tbody.appendChild(tr);
+
+ const td = useDivTagWithDisplayTableStyle
+ ? document.createElement("div")
+ : document.createElement("td");
+ if (useDivTagWithDisplayTableStyle) {
+ td.style.display = "table-cell";
+ td.classList.add("td");
+ }
+ tr.appendChild(td);
+
+ return td;
+ },
+ rules: [
+ `td, .td {
+ border-image: linear-gradient(red, yellow) 10;
+ ${
+ !borderCollapsePropertyIsInherited
+ ? `border-collapse: ${borderCollapse ? "collapse" : "separate"};`
+ : ""
+ }
+ }`,
+ ],
+ isActive,
+ };
+}
diff --git a/devtools/server/tests/chrome/inactive-property-helper/cue-pseudo-element.mjs b/devtools/server/tests/chrome/inactive-property-helper/cue-pseudo-element.mjs
new file mode 100644
index 0000000000..7a55425632
--- /dev/null
+++ b/devtools/server/tests/chrome/inactive-property-helper/cue-pseudo-element.mjs
@@ -0,0 +1,371 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// InactivePropertyHelper `cue-pseudo-element` test cases.
+
+// "background",
+// "background-attachment",
+// "background-blend-mode",
+// "background-clip",
+// "background-color",
+// "background-image",
+// "background-origin",
+// "background-position",
+// "background-position-x",
+// "background-position-y",
+// "background-repeat",
+// "background-size",
+// "color",
+// "font",
+// "font-family",
+// "font-size",
+// "font-stretch",
+// "font-style",
+// "font-variant",
+// "font-variant-alternates",
+// "font-variant-caps",
+// "font-variant-east-asian",
+// "font-variant-ligatures",
+// "font-variant-numeric",
+// "font-variant-position",
+// "font-weight",
+// "line-height",
+// "opacity",
+// "outline",
+// "outline-color",
+// "outline-offset",
+// "outline-style",
+// "outline-width",
+// "ruby-position",
+// "text-combine-upright",
+// "text-decoration",
+// "text-decoration-color",
+// "text-decoration-line",
+// "text-decoration-style",
+// "text-decoration-thickness",
+// "text-shadow",
+// "visibility",
+// "white-space",
+
+export default [
+ {
+ info: "background is active on ::cue",
+ property: "background",
+ tagName: "video",
+ rules: ["video::cue { background: linear-gradient(white, black); }"],
+ isActive: true,
+ },
+ {
+ info: "background-attachment is active on ::cue",
+ property: "background-attachment",
+ tagName: "video",
+ rules: ["video::cue { background-attachment: fixed; }"],
+ isActive: true,
+ },
+ {
+ info: "background-blend-mode is active on ::cue",
+ property: "background-blend-mode",
+ tagName: "video",
+ rules: ["video::cue { background-blend-mode: difference; }"],
+ isActive: true,
+ },
+ {
+ info: "background-clip is active on ::cue",
+ property: "background-clip",
+ tagName: "video",
+ rules: ["video::cue { background-clip: padding-box; }"],
+ isActive: true,
+ },
+ {
+ info: "background-color is active on ::cue",
+ property: "background-color",
+ tagName: "video",
+ rules: ["video::cue { background-color: red; }"],
+ isActive: true,
+ },
+ {
+ info: "background-image is active on ::cue",
+ property: "background-image",
+ tagName: "video",
+ rules: [
+ "video::cue { background-image: url('https://example.com/image.png'); }",
+ ],
+ isActive: true,
+ },
+ {
+ info: "background-origin is active on ::cue",
+ property: "background-origin",
+ tagName: "video",
+ rules: ["video::cue { background-origin: padding-box; }"],
+ isActive: true,
+ },
+ {
+ info: "background-position is active on ::cue",
+ property: "background-position",
+ tagName: "video",
+ rules: ["video::cue { background-position: 0 0; }"],
+ isActive: true,
+ },
+ {
+ info: "background-position-x is active on ::cue",
+ property: "background-position-x",
+ tagName: "video",
+ rules: ["video::cue { background-position-x: 0; }"],
+ isActive: true,
+ },
+ {
+ info: "background-position-y is active on ::cue",
+ property: "background-position-y",
+ tagName: "video",
+ rules: ["video::cue { background-position-y: 0; }"],
+ isActive: true,
+ },
+ {
+ info: "background-repeat is active on ::cue",
+ property: "background-repeat",
+ tagName: "video",
+ rules: ["video::cue { background-repeat: repeat-y; }"],
+ isActive: true,
+ },
+ {
+ info: "background-size is active on ::cue",
+ property: "background-size",
+ tagName: "video",
+ rules: ["video::cue { background-size: 100% 100%; }"],
+ isActive: true,
+ },
+ {
+ info: "color is active on ::cue",
+ property: "color",
+ tagName: "video",
+ rules: ["video::cue { color: red; }"],
+ isActive: true,
+ },
+ {
+ info: "font is active on ::cue",
+ property: "font",
+ tagName: "video",
+ rules: ["video::cue { font: 1em sans-serif; }"],
+ isActive: true,
+ },
+ {
+ info: "font-family is active on ::cue",
+ property: "font-family",
+ tagName: "video",
+ rules: ["video::cue { font-family: sans-serif; }"],
+ isActive: true,
+ },
+ {
+ info: "font-size is active on ::cue",
+ property: "font-size",
+ tagName: "video",
+ rules: ["video::cue { font-size: 1em; }"],
+ isActive: true,
+ },
+ {
+ info: "font-stretch is active on ::cue",
+ property: "font-stretch",
+ tagName: "video",
+ rules: ["video::cue { font-stretch: ultra-condensed; }"],
+ isActive: true,
+ },
+ {
+ info: "font-style is active on ::cue",
+ property: "font-style",
+ tagName: "video",
+ rules: ["video::cue { font-style: italic; }"],
+ isActive: true,
+ },
+ {
+ info: "font-variant is active on ::cue",
+ property: "font-variant",
+ tagName: "video",
+ rules: ["video::cue { font-variant: small-caps; }"],
+ isActive: true,
+ },
+ {
+ info: "font-variant-alternates is active on ::cue",
+ property: "font-variant-alternates",
+ tagName: "video",
+ rules: ["video::cue { font-variant-alternates: slashed-zero; }"],
+ isActive: true,
+ },
+ {
+ info: "font-variant-caps is active on ::cue",
+ property: "font-variant-caps",
+ tagName: "video",
+ rules: ["video::cue { font-variant-caps: all-small-caps; }"],
+ isActive: true,
+ },
+ {
+ info: "font-variant-east-asian is active on ::cue",
+ property: "font-variant-east-asian",
+ tagName: "video",
+ rules: ["video::cue { font-variant-east-asian: ruby; }"],
+ isActive: true,
+ },
+ {
+ info: "font-variant-ligatures is active on ::cue",
+ property: "font-variant-ligatures",
+ tagName: "video",
+ rules: ["video::cue { font-variant-ligatures: common-ligatures; }"],
+ isActive: true,
+ },
+ {
+ info: "font-variant-numeric is active on ::cue",
+ property: "font-variant-numeric",
+ tagName: "video",
+ rules: ["video::cue { font-variant-numeric: ordinal; }"],
+ isActive: true,
+ },
+ {
+ info: "font-variant-position is active on ::cue",
+ property: "font-variant-position",
+ tagName: "video",
+ rules: ["video::cue { font-variant-position: sub; }"],
+ isActive: true,
+ },
+ {
+ info: "font-weight is active on ::cue",
+ property: "font-weight",
+ tagName: "video",
+ rules: ["video::cue { font-weight: bold; }"],
+ isActive: true,
+ },
+ {
+ info: "line-height is active on ::cue",
+ property: "line-height",
+ tagName: "video",
+ rules: ["video::cue { line-height: 1.2; }"],
+ isActive: true,
+ },
+ {
+ info: "opacity is active on ::cue",
+ property: "opacity",
+ tagName: "video",
+ rules: ["video::cue { opacity: 0.8; }"],
+ isActive: true,
+ },
+ {
+ info: "outline is active on ::cue",
+ property: "outline",
+ tagName: "video",
+ rules: ["video::cue { outline: 1px solid red; }"],
+ isActive: true,
+ },
+ {
+ info: "outline-color is active on ::cue",
+ property: "outline-color",
+ tagName: "video",
+ rules: ["video::cue { outline-color: red; }"],
+ isActive: true,
+ },
+ {
+ info: "outline-offset is active on ::cue",
+ property: "outline-offset",
+ tagName: "video",
+ rules: ["video::cue { outline-offset: 1px; }"],
+ isActive: true,
+ },
+ {
+ info: "outline-style is active on ::cue",
+ property: "outline-style",
+ tagName: "video",
+ rules: ["video::cue { outline-style: solid; }"],
+ isActive: true,
+ },
+ {
+ info: "outline-width is active on ::cue",
+ property: "outline-width",
+ tagName: "video",
+ rules: ["video::cue { outline-width: 1px; }"],
+ isActive: true,
+ },
+ {
+ info: "ruby-position is active on ::cue",
+ property: "ruby-position",
+ tagName: "video",
+ rules: ["video::cue { ruby-position: over; }"],
+ isActive: true,
+ },
+ {
+ info: "text-combine-upright is active on ::cue",
+ property: "text-combine-upright",
+ tagName: "video",
+ rules: ["video::cue { text-combine-upright: all; }"],
+ isActive: true,
+ },
+ {
+ info: "text-decoration is active on ::cue",
+ property: "text-decoration",
+ tagName: "video",
+ rules: ["video::cue { text-decoration: 1px underline red; }"],
+ isActive: true,
+ },
+ {
+ info: "text-decoration-color is active on ::cue",
+ property: "text-decoration-color",
+ tagName: "video",
+ rules: ["video::cue { text-decoration-color: red; }"],
+ isActive: true,
+ },
+ {
+ info: "text-decoration-line is active on ::cue",
+ property: "text-decoration-line",
+ tagName: "video",
+ rules: ["video::cue { text-decoration-line: underline; }"],
+ isActive: true,
+ },
+ {
+ info: "text-decoration-style is active on ::cue",
+ property: "text-decoration-style",
+ tagName: "video",
+ rules: ["video::cue { text-decoration-style: wavy; }"],
+ isActive: true,
+ },
+ {
+ info: "text-decoration-thickness is active on ::cue",
+ property: "text-decoration-thickness",
+ tagName: "video",
+ rules: ["video::cue { text-decoration-thickness: 1px; }"],
+ isActive: true,
+ },
+ {
+ info: "text-shadow is active on ::cue",
+ property: "text-shadow",
+ tagName: "video",
+ rules: ["video::cue { text-shadow: 1px 1px 1px red; }"],
+ isActive: true,
+ },
+ {
+ info: "visibility is active on ::cue",
+ property: "visibility",
+ tagName: "video",
+ rules: ["video::cue { visibility: hidden; }"],
+ isActive: true,
+ },
+ {
+ info: "white-space is active on ::cue",
+ property: "white-space",
+ tagName: "video",
+ rules: ["video::cue { white-space: nowrap; }"],
+ isActive: true,
+ },
+ {
+ info: "border is inactive on ::cue",
+ property: "border",
+ tagName: "video",
+ rules: ["video::cue { border: 1px solid red; }"],
+ isActive: false,
+ expectedMsgId: "inactive-css-cue-pseudo-element-not-supported",
+ },
+ {
+ info: "display is inactive on ::cue",
+ property: "display",
+ tagName: "video",
+ rules: ["video::cue { display: grid; }"],
+ isActive: false,
+ expectedMsgId: "inactive-css-cue-pseudo-element-not-supported",
+ },
+];
diff --git a/devtools/server/tests/chrome/inactive-property-helper/first-letter-pseudo-element.mjs b/devtools/server/tests/chrome/inactive-property-helper/first-letter-pseudo-element.mjs
new file mode 100644
index 0000000000..ebce0d292a
--- /dev/null
+++ b/devtools/server/tests/chrome/inactive-property-helper/first-letter-pseudo-element.mjs
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// InactivePropertyHelper `first-letter-pseudo-element` test cases.
+
+// "content",
+
+export default [
+ {
+ info: "content is inactive on ::first-letter",
+ property: "content",
+ tagName: "div",
+ rules: ["div::first-letter { content: 'invalid'; }"],
+ isActive: false,
+ expectedMsgId: "inactive-css-first-letter-pseudo-element-not-supported",
+ },
+ {
+ info: "color is active on ::first-letter",
+ property: "color",
+ tagName: "div",
+ rules: ["div::first-letter { color: green; }"],
+ isActive: true,
+ },
+ {
+ info: "display is active on ::first-letter",
+ property: "display",
+ tagName: "div",
+ rules: ["div::first-letter { display: grid; }"],
+ isActive: true,
+ },
+];
diff --git a/devtools/server/tests/chrome/inactive-property-helper/first-line-pseudo-element.mjs b/devtools/server/tests/chrome/inactive-property-helper/first-line-pseudo-element.mjs
new file mode 100644
index 0000000000..68948a16bc
--- /dev/null
+++ b/devtools/server/tests/chrome/inactive-property-helper/first-line-pseudo-element.mjs
@@ -0,0 +1,50 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// InactivePropertyHelper `first-line-pseudo-element` test cases.
+
+// "direction",
+// "text-orientation",
+// "writing-mode",
+
+export default [
+ {
+ info: "direction is inactive on ::first-line",
+ property: "direction",
+ tagName: "div",
+ rules: ["div::first-line { direction: rtl; }"],
+ isActive: false,
+ expectedMsgId: "inactive-css-first-line-pseudo-element-not-supported",
+ },
+ {
+ info: "text-orientation is inactive on ::first-line",
+ property: "text-orientation",
+ tagName: "div",
+ rules: ["div::first-line { text-orientation: sideways; }"],
+ isActive: false,
+ expectedMsgId: "inactive-css-first-line-pseudo-element-not-supported",
+ },
+ {
+ info: "writing-mode is inactive on ::first-line",
+ property: "writing-mode",
+ tagName: "div",
+ rules: ["div::first-line { writing-mode: vertical-rl; }"],
+ isActive: false,
+ expectedMsgId: "inactive-css-first-line-pseudo-element-not-supported",
+ },
+ {
+ info: "color is active on ::first-line",
+ property: "color",
+ tagName: "div",
+ rules: ["div::first-line { color: green; }"],
+ isActive: true,
+ },
+ {
+ info: "display is active on ::first-line",
+ property: "display",
+ tagName: "div",
+ rules: ["div::first-line { display: grid; }"],
+ isActive: true,
+ },
+];
diff --git a/devtools/server/tests/chrome/inactive-property-helper/flex-grid-item-properties.mjs b/devtools/server/tests/chrome/inactive-property-helper/flex-grid-item-properties.mjs
new file mode 100644
index 0000000000..79c587798a
--- /dev/null
+++ b/devtools/server/tests/chrome/inactive-property-helper/flex-grid-item-properties.mjs
@@ -0,0 +1,229 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// InactivePropertyHelper `align-self`, `place-self`, and `order` test cases.
+export default [
+ {
+ info: "align-self is inactive on block element",
+ property: "align-self",
+ tagName: "div",
+ rules: ["div { align-self: center; }"],
+ isActive: false,
+ },
+ {
+ info: "align-self is inactive on flex container",
+ property: "align-self",
+ tagName: "div",
+ rules: ["div { align-self: center; display: flex;}"],
+ isActive: false,
+ },
+ {
+ info: "align-self is inactive on inline-flex container",
+ property: "align-self",
+ tagName: "div",
+ rules: ["div { align-self: center; display: inline-flex;}"],
+ isActive: false,
+ },
+ {
+ info: "align-self is inactive on grid container",
+ property: "align-self",
+ tagName: "div",
+ rules: ["div { align-self: center; display: grid;}"],
+ isActive: false,
+ },
+ {
+ info: "align-self is inactive on inline grid container",
+ property: "align-self",
+ tagName: "div",
+ rules: ["div { align-self: center; display: inline-grid;}"],
+ isActive: false,
+ },
+ {
+ info: "align-self is inactive on inline element",
+ property: "align-self",
+ tagName: "span",
+ rules: ["span { align-self: center; }"],
+ isActive: false,
+ },
+ {
+ info: "align-self is active on flex item",
+ property: "align-self",
+ createTestElement: rootNode => {
+ const container = document.createElement("div");
+ const element = document.createElement("span");
+ container.append(element);
+ rootNode.append(container);
+ return element;
+ },
+ rules: [
+ "div { display: flex; align-items: start; }",
+ "span { align-self: center; }",
+ ],
+ ruleIndex: 1,
+ isActive: true,
+ },
+ {
+ info: "align-self is active on grid item",
+ property: "align-self",
+ createTestElement: rootNode => {
+ const container = document.createElement("div");
+ const element = document.createElement("span");
+ container.append(element);
+ rootNode.append(container);
+ return element;
+ },
+ rules: [
+ "div { display: grid; align-items: start; }",
+ "span { align-self: center; }",
+ ],
+ ruleIndex: 1,
+ isActive: true,
+ },
+ {
+ info: "place-self is inactive on block element",
+ property: "place-self",
+ tagName: "div",
+ rules: ["div { place-self: center; }"],
+ isActive: false,
+ },
+ {
+ info: "place-self is inactive on flex container",
+ property: "place-self",
+ tagName: "div",
+ rules: ["div { place-self: center; display: flex;}"],
+ isActive: false,
+ },
+ {
+ info: "place-self is inactive on inline-flex container",
+ property: "place-self",
+ tagName: "div",
+ rules: ["div { place-self: center; display: inline-flex;}"],
+ isActive: false,
+ },
+ {
+ info: "place-self is inactive on grid container",
+ property: "place-self",
+ tagName: "div",
+ rules: ["div { place-self: center; display: grid;}"],
+ isActive: false,
+ },
+ {
+ info: "place-self is inactive on inline grid container",
+ property: "place-self",
+ tagName: "div",
+ rules: ["div { place-self: center; display: inline-grid;}"],
+ isActive: false,
+ },
+ {
+ info: "place-self is inactive on inline element",
+ property: "place-self",
+ tagName: "span",
+ rules: ["span { place-self: center; }"],
+ isActive: false,
+ },
+ {
+ info: "place-self is active on flex item",
+ property: "place-self",
+ createTestElement: rootNode => {
+ const container = document.createElement("div");
+ const element = document.createElement("span");
+ container.append(element);
+ rootNode.append(container);
+ return element;
+ },
+ rules: [
+ "div { display: flex; align-items: start; }",
+ "span { place-self: center; }",
+ ],
+ ruleIndex: 1,
+ isActive: true,
+ },
+ {
+ info: "place-self is active on grid item",
+ property: "place-self",
+ createTestElement: rootNode => {
+ const container = document.createElement("div");
+ const element = document.createElement("span");
+ container.append(element);
+ rootNode.append(container);
+ return element;
+ },
+ rules: [
+ "div { display: grid; align-items: start; }",
+ "span { place-self: center; }",
+ ],
+ ruleIndex: 1,
+ isActive: true,
+ },
+ {
+ info: "order is inactive on block element",
+ property: "order",
+ tagName: "div",
+ rules: ["div { order: 1; }"],
+ isActive: false,
+ },
+ {
+ info: "order is inactive on flex container",
+ property: "order",
+ tagName: "div",
+ rules: ["div { order: 1; display: flex;}"],
+ isActive: false,
+ },
+ {
+ info: "order is inactive on inline-flex container",
+ property: "order",
+ tagName: "div",
+ rules: ["div { order: 1; display: inline-flex;}"],
+ isActive: false,
+ },
+ {
+ info: "order is inactive on grid container",
+ property: "order",
+ tagName: "div",
+ rules: ["div { order: 1; display: grid;}"],
+ isActive: false,
+ },
+ {
+ info: "order is inactive on inline grid container",
+ property: "order",
+ tagName: "div",
+ rules: ["div { order: 1; display: inline-grid;}"],
+ isActive: false,
+ },
+ {
+ info: "order is inactive on inline element",
+ property: "order",
+ tagName: "span",
+ rules: ["span { order: 1; }"],
+ isActive: false,
+ },
+ {
+ info: "order is active on flex item",
+ property: "order",
+ createTestElement: rootNode => {
+ const container = document.createElement("div");
+ const element = document.createElement("span");
+ container.append(element);
+ rootNode.append(container);
+ return element;
+ },
+ rules: ["div { display: flex; }", "span { order: 1; }"],
+ ruleIndex: 1,
+ isActive: true,
+ },
+ {
+ info: "order is active on grid item",
+ property: "order",
+ createTestElement: rootNode => {
+ const container = document.createElement("div");
+ const element = document.createElement("span");
+ container.append(element);
+ rootNode.append(container);
+ return element;
+ },
+ rules: ["div { display: grid; }", "span { order: 1; }"],
+ ruleIndex: 1,
+ isActive: true,
+ },
+];
diff --git a/devtools/server/tests/chrome/inactive-property-helper/float.mjs b/devtools/server/tests/chrome/inactive-property-helper/float.mjs
new file mode 100644
index 0000000000..4c502e3cca
--- /dev/null
+++ b/devtools/server/tests/chrome/inactive-property-helper/float.mjs
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// InactivePropertyHelper `float` test cases.
+export default [
+ {
+ info: "display: inline is inactive on a floated element",
+ property: "display",
+ tagName: "div",
+ rules: ["div { display: inline; float: right; }"],
+ isActive: false,
+ },
+ {
+ info: "display: block is active on a floated element",
+ property: "display",
+ tagName: "div",
+ rules: ["div { display: block; float: right;}"],
+ isActive: true,
+ },
+ {
+ info: "display: inline-grid is inactive on a floated element",
+ property: "display",
+ createTestElement: rootNode => {
+ const container = document.createElement("div");
+ container.classList.add("test");
+ rootNode.append(container);
+ return container;
+ },
+ rules: [
+ "div { float: left; display:block; }",
+ ".test { display: inline-grid ;}",
+ ],
+ isActive: false,
+ },
+ {
+ info: "display: table-footer-group is inactive on a floated element",
+ property: "display",
+ createTestElement: rootNode => {
+ const container = document.createElement("div");
+ container.style.display = "table";
+ const footer = document.createElement("div");
+ footer.classList.add("table-footer");
+ container.append(footer);
+ rootNode.append(container);
+ return footer;
+ },
+ rules: [".table-footer { display: table-footer-group; float: left;}"],
+ isActive: false,
+ },
+ createGridPlacementOnFloatedItemTest("grid-row"),
+ createGridPlacementOnFloatedItemTest("grid-column"),
+ createGridPlacementOnFloatedItemTest("grid-area", "foo"),
+];
+
+function createGridPlacementOnFloatedItemTest(property, value = "2") {
+ return {
+ info: `grid placement property ${property} is active on a floated grid item`,
+ property,
+ createTestElement: rootNode => {
+ const grid = document.createElement("div");
+ grid.style.display = "grid";
+ grid.style.gridTemplateRows = "repeat(5, 1fr)";
+ grid.style.gridTemplateColumns = "repeat(5, 1fr)";
+ grid.style.gridTemplateAreas = "'foo foo foo'";
+ rootNode.appendChild(grid);
+
+ const item = document.createElement("span");
+ grid.appendChild(item);
+
+ return item;
+ },
+ rules: [`span { ${property}: ${value}; float: left; }`],
+ isActive: true,
+ };
+}
diff --git a/devtools/server/tests/chrome/inactive-property-helper/gap.mjs b/devtools/server/tests/chrome/inactive-property-helper/gap.mjs
new file mode 100644
index 0000000000..83befcba0d
--- /dev/null
+++ b/devtools/server/tests/chrome/inactive-property-helper/gap.mjs
@@ -0,0 +1,133 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// InactivePropertyHelper `gap` test cases.
+export default [
+ {
+ info: "column-gap is inactive on non-grid and non-flex container",
+ property: "column-gap",
+ tagName: "div",
+ rules: ["div { column-gap: 10px; display: block; }"],
+ isActive: false,
+ },
+ {
+ info: "column-gap is active on grid container",
+ property: "column-gap",
+ tagName: "div",
+ rules: ["div { column-gap: 10px; display: grid; }"],
+ isActive: true,
+ },
+ {
+ info: "column-gap is active on flex container",
+ property: "column-gap",
+ tagName: "div",
+ rules: ["div { column-gap: 10px; display: flex; }"],
+ isActive: true,
+ },
+ {
+ info: "column-gap is inactive on non-multi-col container",
+ property: "column-gap",
+ tagName: "div",
+ rules: ["div { column-gap: 10px; column-count: auto; }"],
+ isActive: false,
+ },
+ {
+ info: "column-gap is active on multi-column container",
+ property: "column-gap",
+ tagName: "div",
+ rules: ["div { column-gap: 10px; column-count: 2; }"],
+ isActive: true,
+ },
+ {
+ info: "row-gap is inactive on non-grid and non-flex container",
+ property: "row-gap",
+ tagName: "div",
+ rules: ["div { row-gap: 10px; display: block; }"],
+ isActive: false,
+ },
+ {
+ info: "row-gap is active on grid container",
+ property: "row-gap",
+ tagName: "div",
+ rules: ["div { row-gap: 10px; display: grid; }"],
+ isActive: true,
+ },
+ {
+ info: "row-gap is active on flex container",
+ property: "row-gap",
+ tagName: "div",
+ rules: ["div { row-gap: 10px; display: flex; }"],
+ isActive: true,
+ },
+ {
+ info: "gap is inactive on non-grid and non-flex container",
+ property: "gap",
+ tagName: "div",
+ rules: ["div { gap: 10px; display: block; }"],
+ isActive: false,
+ },
+ {
+ info: "gap is active on flex container",
+ property: "gap",
+ tagName: "div",
+ rules: ["div { gap: 10px; display: flex; }"],
+ isActive: true,
+ },
+ {
+ info: "gap is active on grid container",
+ property: "gap",
+ tagName: "div",
+ rules: ["div { gap: 10px; display: grid; }"],
+ isActive: true,
+ },
+ {
+ info: "gap is inactive on non-multi-col container",
+ property: "gap",
+ tagName: "div",
+ rules: ["div { gap: 10px; column-count: auto; }"],
+ isActive: false,
+ },
+ {
+ info: "gap is active on multi-col container",
+ property: "gap",
+ tagName: "div",
+ rules: ["div { gap: 10px; column-count: 2; }"],
+ isActive: true,
+ },
+ {
+ info: "grid-gap is inactive on non-grid and non-flex container",
+ property: "grid-gap",
+ tagName: "div",
+ rules: ["div { grid-gap: 10px; display: block; }"],
+ isActive: false,
+ },
+ {
+ info: "grid-gap is active on flex container",
+ property: "grid-gap",
+ tagName: "div",
+ rules: ["div { grid-gap: 10px; display: flex; }"],
+ isActive: true,
+ },
+ {
+ info: "grid-gap is active on grid container",
+ property: "grid-gap",
+ tagName: "div",
+ rules: ["div { grid-gap: 10px; display: grid; }"],
+ isActive: true,
+ },
+ {
+ info: "grid-gap is inactive on non-multi-col container",
+ property: "grid-gap",
+ tagName: "div",
+ rules: ["div { grid-gap: 10px; column-count: auto; }"],
+ isActive: false,
+ },
+ {
+ info: "grid-gap is active on multi-col container",
+ property: "grid-gap",
+ tagName: "div",
+ rules: ["div { grid-gap: 10px; column-count: 2; }"],
+ isActive: true,
+ },
+];
diff --git a/devtools/server/tests/chrome/inactive-property-helper/grid-container-properties.mjs b/devtools/server/tests/chrome/inactive-property-helper/grid-container-properties.mjs
new file mode 100644
index 0000000000..1fca234733
--- /dev/null
+++ b/devtools/server/tests/chrome/inactive-property-helper/grid-container-properties.mjs
@@ -0,0 +1,43 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// InactivePropertyHelper test cases:
+// `grid-auto-columns`, `grid-auto-flow`, `grid-auto-rows`, `grid-template`,
+// `grid-template-areas`, `grid-template-columns`, `grid-template-rows`,
+// and `justify-items`.
+let tests = [];
+
+for (const { propertyName, propertyValue } of [
+ { propertyName: "grid-auto-columns", propertyValue: "100px" },
+ { propertyName: "grid-auto-flow", propertyValue: "columns" },
+ { propertyName: "grid-auto-rows", propertyValue: "100px" },
+ { propertyName: "grid-template", propertyValue: "auto / auto" },
+ { propertyName: "grid-template-areas", propertyValue: "a b c" },
+ { propertyName: "grid-template-columns", propertyValue: "100px 1fr" },
+ { propertyName: "grid-template-rows", propertyValue: "100px 1fr" },
+ { propertyName: "justify-items", propertyValue: "center" },
+]) {
+ tests = tests.concat(createTestsForProp(propertyName, propertyValue));
+}
+
+function createTestsForProp(propertyName, propertyValue) {
+ return [
+ {
+ info: `${propertyName} is active on a grid container`,
+ property: propertyName,
+ tagName: "div",
+ rules: [`div { display:grid; ${propertyName}: ${propertyValue}; }`],
+ isActive: true,
+ },
+ {
+ info: `${propertyName} is inactive on a non-grid container`,
+ property: propertyName,
+ tagName: "div",
+ rules: [`div { ${propertyName}: ${propertyValue}; }`],
+ isActive: false,
+ },
+ ];
+}
+
+export default tests;
diff --git a/devtools/server/tests/chrome/inactive-property-helper/grid-with-absolute-properties.mjs b/devtools/server/tests/chrome/inactive-property-helper/grid-with-absolute-properties.mjs
new file mode 100644
index 0000000000..fd963e0d3b
--- /dev/null
+++ b/devtools/server/tests/chrome/inactive-property-helper/grid-with-absolute-properties.mjs
@@ -0,0 +1,71 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// InactivePropertyHelper test cases:
+// `grid-area`, `grid-column`, `grid-column-end`, `grid-column-start`,
+// `grid-row`, `grid-row-end`, `grid-row-start`, `justify-self`, `align-self`
+// and `place-self`.
+let tests = [];
+
+for (const { propertyName, propertyValue } of [
+ { propertyName: "grid-area", propertyValue: "2 / 1 / span 2 / span 3" },
+ { propertyName: "grid-column", propertyValue: 2 },
+ { propertyName: "grid-column-end", propertyValue: "span 3" },
+ { propertyName: "grid-column-start", propertyValue: 2 },
+ { propertyName: "grid-row", propertyValue: "1 / span 2" },
+ { propertyName: "grid-row-end", propertyValue: "span 3" },
+ { propertyName: "grid-row-start", propertyValue: 2 },
+ { propertyName: "justify-self", propertyValue: "start" },
+ { propertyName: "align-self", propertyValue: "auto" },
+ { propertyName: "place-self", propertyValue: "auto center" },
+]) {
+ tests = tests.concat(createTestsForProp(propertyName, propertyValue));
+}
+
+function createTestsForProp(propertyName, propertyValue) {
+ return [
+ {
+ info: `${propertyName} is active on a grid item`,
+ property: `${propertyName}`,
+ createTestElement,
+ rules: [
+ `#grid-container { display:grid; grid:auto/100px 100px; }`,
+ `#grid-item { ${propertyName}: ${propertyValue}; }`,
+ ],
+ ruleIndex: 1,
+ isActive: true,
+ },
+ {
+ info: `${propertyName} is active on an absolutely positioned grid item`,
+ property: `${propertyName}`,
+ createTestElement,
+ rules: [
+ `#grid-container { display:grid; grid:auto/100px 100px; position: relative }`,
+ `#grid-item { ${propertyName}: ${propertyValue}; position: absolute; }`,
+ ],
+ ruleIndex: 1,
+ isActive: true,
+ },
+ {
+ info: `${propertyName} is inactive on a non-grid item`,
+ property: `${propertyName}`,
+ tagName: `div`,
+ rules: [`div { ${propertyName}: ${propertyValue}; }`],
+ isActive: false,
+ },
+ ];
+}
+
+function createTestElement(rootNode) {
+ const container = document.createElement("div");
+ container.id = "grid-container";
+ const element = document.createElement("div");
+ element.id = "grid-item";
+ container.append(element);
+ rootNode.append(container);
+
+ return element;
+}
+
+export default tests;
diff --git a/devtools/server/tests/chrome/inactive-property-helper/highlight-pseudo-elements.mjs b/devtools/server/tests/chrome/inactive-property-helper/highlight-pseudo-elements.mjs
new file mode 100644
index 0000000000..bcb5b8763c
--- /dev/null
+++ b/devtools/server/tests/chrome/inactive-property-helper/highlight-pseudo-elements.mjs
@@ -0,0 +1,155 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// InactivePropertyHelper `highlight-pseudo-elements` test cases.
+
+// "background",
+// "background-color",
+// "color",
+// "fill-color",
+// "stroke-color",
+// "stroke-width",
+// "text-decoration",
+// "text-shadow",
+// "text-underline-offset",
+// "text-underline-position",
+
+export default [
+ {
+ info: "width is inactive on ::selection",
+ property: "width",
+ tagName: "span",
+ rules: ["span::selection { width: 10px; }"],
+ isActive: false,
+ // `width` is also inactive on inline element, so make sure we get the warning
+ // because we're using it in a highlight pseudo.
+ expectedMsgId: "inactive-css-highlight-pseudo-elements-not-supported",
+ },
+ {
+ info: "display is inactive on ::highlight",
+ property: "display",
+ tagName: "span",
+ rules: ["span::highlight(result) { display: grid; }"],
+ isActive: false,
+ expectedMsgId: "inactive-css-highlight-pseudo-elements-not-supported",
+ },
+ {
+ // accept background shorthand, even if it might hold inactive values
+ info: "background is active on ::selection",
+ property: "background",
+ tagName: "span",
+ rules: ["span::selection { background: red; }"],
+ isActive: true,
+ },
+ {
+ info: "border-color is inactive on ::selection",
+ property: "border-color",
+ tagName: "span",
+ rules: ["span::selection { border-color: red; }"],
+ isActive: false,
+ // `width` is also inactive on inline element, so make sure we get the warning
+ // because we're using it in a highlight pseudo.
+ expectedMsgId: "inactive-css-highlight-pseudo-elements-not-supported",
+ },
+ {
+ info: "background-color is active on ::selection",
+ property: "background-color",
+ tagName: "span",
+ rules: ["span::selection { background-color: red; }"],
+ isActive: true,
+ },
+ {
+ info: "color is active on ::selection",
+ property: "color",
+ tagName: "span",
+ rules: ["span::selection { color: red; }"],
+ isActive: true,
+ },
+ {
+ info: "text-decoration is active on ::selection",
+ property: "text-decoration",
+ tagName: "span",
+ rules: [
+ "span::selection { text-decoration: double overline #FF3028 4px; }",
+ ],
+ isActive: true,
+ },
+ {
+ info: "text-decoration-color is active on ::selection",
+ property: "text-decoration-color",
+ tagName: "span",
+ rules: ["span::selection { text-decoration-color: #FF3028; }"],
+ isActive: true,
+ },
+ {
+ info: "text-decoration-line is active on ::selection",
+ property: "text-decoration-line",
+ tagName: "span",
+ rules: ["span::selection { text-decoration-line: overline; }"],
+ isActive: true,
+ },
+ {
+ info: "text-decoration-style is active on ::selection",
+ property: "text-decoration-style",
+ tagName: "span",
+ rules: ["span::selection { text-decoration-style: double; }"],
+ isActive: true,
+ },
+ {
+ info: "text-decoration-thickness is active on ::selection",
+ property: "text-decoration-thickness",
+ tagName: "span",
+ rules: ["span::selection { text-decoration-thickness: 4px; }"],
+ isActive: true,
+ },
+ {
+ info: "text-shadow is active on ::selection",
+ property: "text-shadow",
+ tagName: "span",
+ rules: ["span::selection { text-shadow: text-shadow: #FC0 1px 0 10px; }"],
+ isActive: true,
+ },
+ {
+ info: "text-underline-offset is active on ::selection",
+ property: "text-underline-offset",
+ tagName: "span",
+ rules: ["span::selection { text-underline-offset: 10px; }"],
+ isActive: true,
+ },
+ {
+ info: "text-underline-position is active on ::selection",
+ property: "text-underline-position",
+ tagName: "span",
+ rules: ["span::selection { text-underline-position: under; }"],
+ isActive: true,
+ },
+ {
+ info: "-webkit-text-fill-color is active on ::selection",
+ property: "-webkit-text-fill-color",
+ tagName: "span",
+ rules: ["span::selection { -webkit-text-fill-color: red; }"],
+ isActive: true,
+ },
+ {
+ info: "-webkit-text-stroke-color is active on ::selection",
+ property: "-webkit-text-stroke-color",
+ tagName: "span",
+ rules: ["span::selection { -webkit-text-stroke-color: red; }"],
+ isActive: true,
+ },
+ {
+ info: "-webkit-text-stroke-width is active on ::selection",
+ property: "-webkit-text-stroke-width",
+ tagName: "span",
+ rules: ["span::selection { -webkit-text-stroke-width: 4px; }"],
+ isActive: true,
+ },
+ {
+ info: "-webkit-text-stroke is active on ::selection",
+ property: "-webkit-text-stroke",
+ tagName: "span",
+ rules: ["span::selection { -webkit-text-stroke: 4px navy; }"],
+ isActive: true,
+ },
+];
diff --git a/devtools/server/tests/chrome/inactive-property-helper/margin-padding.mjs b/devtools/server/tests/chrome/inactive-property-helper/margin-padding.mjs
new file mode 100644
index 0000000000..7c1d348512
--- /dev/null
+++ b/devtools/server/tests/chrome/inactive-property-helper/margin-padding.mjs
@@ -0,0 +1,260 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// InactivePropertyHelper `align-content` test cases.
+
+export default [
+ {
+ info: "margin is active on block containers",
+ property: "margin",
+ tagName: "div",
+ rules: ["div { margin: 10px; }"],
+ isActive: true,
+ },
+ {
+ info: "margin is active on flex containers",
+ property: "margin",
+ tagName: "div",
+ rules: ["div { display: flex; margin: 10px; }"],
+ isActive: true,
+ },
+ {
+ info: "margin is active on grid containers",
+ property: "margin",
+ tagName: "div",
+ rules: ["div { display: grid; margin: 10px; }"],
+ isActive: true,
+ },
+ {
+ info: "margin is active on tables",
+ property: "margin",
+ tagName: "div",
+ rules: ["div { display: table; margin: 10px; }"],
+ isActive: true,
+ },
+ {
+ info: "margin is active on inline tables",
+ property: "margin",
+ tagName: "div",
+ rules: ["div { display: inline-table; margin: 10px; }"],
+ isActive: true,
+ },
+ {
+ info: "margin is active on table captions",
+ property: "margin",
+ tagName: "div",
+ rules: ["div { display: table-caption; margin: 10px; }"],
+ isActive: true,
+ },
+ {
+ info: "margin is inactive on table cells",
+ property: "margin",
+ tagName: "div",
+ rules: ["div { display: table-cell; margin: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "margin-block is inactive on table cells",
+ property: "margin-block",
+ tagName: "div",
+ rules: ["div { display: table-cell; margin-block: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "margin-block-start is inactive on table cells",
+ property: "margin-block-start",
+ tagName: "div",
+ rules: ["div { display: table-cell; margin-block-start: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "margin-block-end is inactive on table cells",
+ property: "margin-block-end",
+ tagName: "div",
+ rules: ["div { display: table-cell; margin-block-end: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "margin-block is inactive on table cells",
+ property: "margin-block",
+ tagName: "div",
+ rules: ["div { display: table-cell; margin-block: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "margin-bottom is inactive on table rows",
+ property: "margin-bottom",
+ tagName: "div",
+ rules: ["div { display: table-row; margin-bottom: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "margin-inline-start is inactive on table rows",
+ property: "margin-inline-start",
+ tagName: "div",
+ rules: ["div { display: table-row; margin-inline-start: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "margin-inline-end is inactive on table rows",
+ property: "margin-inline-end",
+ tagName: "div",
+ rules: ["div { display: table-row; margin-inline-end: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "margin-inline is inactive on table rows",
+ property: "margin-inline",
+ tagName: "div",
+ rules: ["div { display: table-row; margin-inline: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "margin-left is inactive on table columns",
+ property: "margin-left",
+ tagName: "div",
+ rules: ["div { display: table-column; margin-left: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "margin-right is inactive on table row groups",
+ property: "margin-right",
+ tagName: "div",
+ rules: ["div { display: table-row-group; margin-right: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "margin-top is inactive on table column groups",
+ property: "margin-top",
+ tagName: "div",
+ rules: ["div { display: table-column-group; margin-top: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "padding is active on block containers",
+ property: "padding",
+ tagName: "div",
+ rules: ["div { padding: 10px; }"],
+ isActive: true,
+ },
+ {
+ info: "padding is active on flex containers",
+ property: "padding",
+ tagName: "div",
+ rules: ["div { display: flex; padding: 10px; }"],
+ isActive: true,
+ },
+ {
+ info: "padding is active on grid containers",
+ property: "padding",
+ tagName: "div",
+ rules: ["div { display: grid; padding: 10px; }"],
+ isActive: true,
+ },
+ {
+ info: "padding is active on tables",
+ property: "padding",
+ tagName: "div",
+ rules: ["div { display: table; padding: 10px; }"],
+ isActive: true,
+ },
+ {
+ info: "padding is active on inline tables",
+ property: "padding",
+ tagName: "div",
+ rules: ["div { display: inline-table; padding: 10px; }"],
+ isActive: true,
+ },
+ {
+ info: "padding is active on table captions",
+ property: "padding",
+ tagName: "div",
+ rules: ["div { display: table-caption; padding: 10px; }"],
+ isActive: true,
+ },
+ {
+ info: "padding is active on table cells",
+ property: "padding",
+ tagName: "div",
+ rules: ["div { display: table-cell; padding: 10px; }"],
+ isActive: true,
+ },
+ {
+ info: "padding-block is active on table cells",
+ property: "padding-block",
+ tagName: "div",
+ rules: ["div { display: table-cell; padding-block: 10px; }"],
+ isActive: true,
+ },
+ {
+ info: "padding-block-start is active on table cells",
+ property: "padding-block-start",
+ tagName: "div",
+ rules: ["div { display: table-cell; padding-block-start: 10px; }"],
+ isActive: true,
+ },
+ {
+ info: "padding-block-end is active on table cells",
+ property: "padding-block-end",
+ tagName: "div",
+ rules: ["div { display: table-cell; padding-block-end: 10px; }"],
+ isActive: true,
+ },
+ {
+ info: "padding-block is active on table cells",
+ property: "padding-block",
+ tagName: "div",
+ rules: ["div { display: table-cell; padding-block: 10px; }"],
+ isActive: true,
+ },
+ {
+ info: "padding-bottom is inactive on table rows",
+ property: "padding-bottom",
+ tagName: "div",
+ rules: ["div { display: table-row; padding-bottom: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "padding-inline-start is inactive on table rows",
+ property: "padding-inline-start",
+ tagName: "div",
+ rules: ["div { display: table-row; padding-inline-start: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "padding-inline-end is inactive on table rows",
+ property: "padding-inline-end",
+ tagName: "div",
+ rules: ["div { display: table-row; padding-inline-end: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "padding-inline is inactive on table rows",
+ property: "padding-inline",
+ tagName: "div",
+ rules: ["div { display: table-row; padding-inline: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "padding-left is inactive on table columns",
+ property: "padding-left",
+ tagName: "div",
+ rules: ["div { display: table-column; padding-left: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "padding-right is inactive on table row groups",
+ property: "padding-right",
+ tagName: "div",
+ rules: ["div { display: table-row-group; padding-right: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "padding-top is inactive on table column groups",
+ property: "padding-top",
+ tagName: "div",
+ rules: ["div { display: table-column-group; padding-top: 10px; }"],
+ isActive: false,
+ },
+];
diff --git a/devtools/server/tests/chrome/inactive-property-helper/max-min-width-height.mjs b/devtools/server/tests/chrome/inactive-property-helper/max-min-width-height.mjs
new file mode 100644
index 0000000000..4bb5623f6e
--- /dev/null
+++ b/devtools/server/tests/chrome/inactive-property-helper/max-min-width-height.mjs
@@ -0,0 +1,366 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// InactivePropertyHelper `width`, `min-width`, `max-width`, `height`, `min-height`,
+// `max-height` test cases.
+export default [
+ {
+ info: "width is inactive on a non-replaced inline element",
+ property: "width",
+ tagName: "span",
+ rules: ["span { width: 500px; }"],
+ isActive: false,
+ },
+ {
+ info: "min-width is inactive on a non-replaced inline element",
+ property: "min-width",
+ tagName: "span",
+ rules: ["span { min-width: 500px; }"],
+ isActive: false,
+ },
+ {
+ info: "max-width is inactive on a non-replaced inline element",
+ property: "max-width",
+ tagName: "span",
+ rules: ["span { max-width: 500px; }"],
+ isActive: false,
+ },
+ {
+ info: "width is inactive on an tr element",
+ property: "width",
+ tagName: "tr",
+ rules: ["tr { width: 500px; }"],
+ isActive: false,
+ },
+ {
+ info: "min-width is inactive on an tr element",
+ property: "min-width",
+ tagName: "tr",
+ rules: ["tr { min-width: 500px; }"],
+ isActive: false,
+ },
+ {
+ info: "max-width is inactive on an tr element",
+ property: "max-width",
+ tagName: "tr",
+ rules: ["tr { max-width: 500px; }"],
+ isActive: false,
+ },
+ {
+ info: "width is inactive on an thead element",
+ property: "width",
+ tagName: "thead",
+ rules: ["thead { width: 500px; }"],
+ isActive: false,
+ },
+ {
+ info: "min-width is inactive on an thead element",
+ property: "min-width",
+ tagName: "thead",
+ rules: ["thead { min-width: 500px; }"],
+ isActive: false,
+ },
+ {
+ info: "max-width is inactive on an thead element",
+ property: "max-width",
+ tagName: "thead",
+ rules: ["thead { max-width: 500px; }"],
+ isActive: false,
+ },
+ {
+ info: "width is inactive on an tfoot element",
+ property: "width",
+ tagName: "tfoot",
+ rules: ["tfoot { width: 500px; }"],
+ isActive: false,
+ },
+ {
+ info: "min-width is inactive on an tfoot element",
+ property: "min-width",
+ tagName: "tfoot",
+ rules: ["tfoot { min-width: 500px; }"],
+ isActive: false,
+ },
+ {
+ info: "max-width is inactive on an tfoot element",
+ property: "max-width",
+ tagName: "tfoot",
+ rules: ["tfoot { max-width: 500px; }"],
+ isActive: false,
+ },
+ {
+ info: "width is active on a replaced inline element",
+ property: "width",
+ tagName: "img",
+ rules: ["img { width: 500px; }"],
+ isActive: true,
+ },
+ {
+ info: "width is active on an inline input element",
+ property: "width",
+ tagName: "input",
+ rules: ["input { display: inline; width: 500px; }"],
+ isActive: true,
+ },
+ {
+ info: "width is active on an inline select element",
+ property: "width",
+ tagName: "select",
+ rules: ["select { display: inline; width: 500px; }"],
+ isActive: true,
+ },
+ {
+ info: "width is active on a textarea element",
+ property: "width",
+ tagName: "textarea",
+ rules: ["textarea { width: 500px; }"],
+ isActive: true,
+ },
+ {
+ info: "min-width is active on a replaced inline element",
+ property: "min-width",
+ tagName: "img",
+ rules: ["img { min-width: 500px; }"],
+ isActive: true,
+ },
+ {
+ info: "max-width is active on a replaced inline element",
+ property: "max-width",
+ tagName: "img",
+ rules: ["img { max-width: 500px; }"],
+ isActive: true,
+ },
+ {
+ info: "width is active on a block element",
+ property: "width",
+ tagName: "div",
+ rules: ["div { width: 500px; }"],
+ isActive: true,
+ },
+ {
+ info: "min-width is active on a block element",
+ property: "min-width",
+ tagName: "div",
+ rules: ["div { min-width: 500px; }"],
+ isActive: true,
+ },
+ {
+ info: "max-width is active on a block element",
+ property: "max-width",
+ tagName: "div",
+ rules: ["div { max-width: 500px; }"],
+ isActive: true,
+ },
+ {
+ info: "height is inactive on a non-replaced inline element",
+ property: "height",
+ tagName: "span",
+ rules: ["span { height: 500px; }"],
+ isActive: false,
+ },
+ {
+ info: "min-height is inactive on a non-replaced inline element",
+ property: "min-height",
+ tagName: "span",
+ rules: ["span { min-height: 500px; }"],
+ isActive: false,
+ },
+ {
+ info: "max-height is inactive on a non-replaced inline element",
+ property: "max-height",
+ tagName: "span",
+ rules: ["span { max-height: 500px; }"],
+ isActive: false,
+ },
+ {
+ info: "height is inactive on colgroup element",
+ property: "height",
+ tagName: "colgroup",
+ rules: ["colgroup { height: 500px; }"],
+ isActive: false,
+ },
+ {
+ info: "min-height is inactive on colgroup element",
+ property: "min-height",
+ tagName: "colgroup",
+ rules: ["colgroup { min-height: 500px; }"],
+ isActive: false,
+ },
+ {
+ info: "max-height is inactive on colgroup element",
+ property: "max-height",
+ tagName: "colgroup",
+ rules: ["colgroup { max-height: 500px; }"],
+ isActive: false,
+ },
+ {
+ info: "height is inactive on col element",
+ property: "height",
+ tagName: "col",
+ rules: ["col { height: 500px; }"],
+ isActive: false,
+ },
+ {
+ info: "min-height is inactive on col element",
+ property: "min-height",
+ tagName: "col",
+ rules: ["col { min-height: 500px; }"],
+ isActive: false,
+ },
+ {
+ info: "max-height is inactive on col element",
+ property: "max-height",
+ tagName: "col",
+ rules: ["col { max-height: 500px; }"],
+ isActive: false,
+ },
+ {
+ info: "height is active on a replaced inline element",
+ property: "height",
+ tagName: "img",
+ rules: ["img { height: 500px; }"],
+ isActive: true,
+ },
+ {
+ info: "height is active on an inline input element",
+ property: "height",
+ tagName: "input",
+ rules: ["input { display: inline; height: 500px; }"],
+ isActive: true,
+ },
+ {
+ info: "height is active on an inline select element",
+ property: "height",
+ tagName: "select",
+ rules: ["select { display: inline; height: 500px; }"],
+ isActive: true,
+ },
+ {
+ info: "height is active on a textarea element",
+ property: "height",
+ tagName: "textarea",
+ rules: ["textarea { height: 500px; }"],
+ isActive: true,
+ },
+ {
+ info: "min-height is active on a replaced inline element",
+ property: "min-height",
+ tagName: "img",
+ rules: ["img { min-height: 500px; }"],
+ isActive: true,
+ },
+ {
+ info: "max-height is active on a replaced inline element",
+ property: "max-height",
+ tagName: "img",
+ rules: ["img { max-height: 500px; }"],
+ isActive: true,
+ },
+ {
+ info: "height is active on a block element",
+ property: "height",
+ tagName: "div",
+ rules: ["div { height: 500px; }"],
+ isActive: true,
+ },
+ {
+ info: "min-height is active on a block element",
+ property: "min-height",
+ tagName: "div",
+ rules: ["div { min-height: 500px; }"],
+ isActive: true,
+ },
+ {
+ info: "max-height is active on a block element",
+ property: "max-height",
+ tagName: "div",
+ rules: ["div { max-height: 500px; }"],
+ isActive: true,
+ },
+ {
+ info: "height is active on an svg <rect> element.",
+ property: "height",
+ createTestElement: main => {
+ main.innerHTML = `
+ <svg width=100 height=100>
+ <rect width=100 fill=green></rect>
+ </svg>
+ `;
+ return main.querySelector("rect");
+ },
+ rules: ["rect { height: 100px; }"],
+ isActive: true,
+ },
+ createTableElementTestCase("width", false, "table-row"),
+ createTableElementTestCase("width", false, "table-row-group"),
+ createTableElementTestCase("width", true, "table-column"),
+ createTableElementTestCase("width", true, "table-column-group"),
+ createTableElementTestCase("height", false, "table-column"),
+ createTableElementTestCase("height", false, "table-column-group"),
+ createTableElementTestCase("height", true, "table-row"),
+ createTableElementTestCase("height", true, "table-row-group"),
+ createVerticalTableElementTestCase("width", true, "table-row"),
+ createVerticalTableElementTestCase("width", true, "table-row-group"),
+ createVerticalTableElementTestCase("width", false, "table-column"),
+ createVerticalTableElementTestCase("width", false, "table-column-group"),
+ createVerticalTableElementTestCase("height", true, "table-column"),
+ createVerticalTableElementTestCase("height", true, "table-column-group"),
+ createVerticalTableElementTestCase("height", false, "table-row"),
+ createVerticalTableElementTestCase("height", false, "table-row-group"),
+ {
+ info: "width's inactivity status for a row takes the table's writing mode into account",
+ property: "width",
+ createTestElement: rootNode => {
+ const table = document.createElement("table");
+ table.style.writingMode = "vertical-lr";
+ rootNode.appendChild(table);
+
+ const tbody = document.createElement("tbody");
+ table.appendChild(tbody);
+
+ const tr = document.createElement("tr");
+ tbody.appendChild(tr);
+
+ const td = document.createElement("td");
+ tr.appendChild(td);
+
+ return tr;
+ },
+ rules: ["tr { writing-mode: horizontal-tb; width: 360px; }"],
+ isActive: true,
+ },
+];
+
+function createTableElementTestCase(property, isActive, displayType) {
+ return {
+ info: `${property} is ${
+ isActive ? "active" : "inactive"
+ } on a ${displayType}`,
+ property,
+ tagName: "div",
+ rules: [`div { display: ${displayType}; ${property}: 100px; }`],
+ isActive,
+ };
+}
+
+function createVerticalTableElementTestCase(property, isActive, displayType) {
+ return {
+ info: `${property} is ${
+ isActive ? "active" : "inactive"
+ } on a vertical ${displayType}`,
+ property,
+ createTestElement: rootNode => {
+ const container = document.createElement("div");
+ container.style.writingMode = "vertical-lr";
+ rootNode.append(container);
+
+ const element = document.createElement("span");
+ container.append(element);
+
+ return element;
+ },
+ rules: [`span { display: ${displayType}; ${property}: 100px; }`],
+ isActive,
+ };
+}
diff --git a/devtools/server/tests/chrome/inactive-property-helper/multicol-container-properties.mjs b/devtools/server/tests/chrome/inactive-property-helper/multicol-container-properties.mjs
new file mode 100644
index 0000000000..6bc4e9dd13
--- /dev/null
+++ b/devtools/server/tests/chrome/inactive-property-helper/multicol-container-properties.mjs
@@ -0,0 +1,39 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// InactivePropertyHelper test cases:
+// `column-fill`, `column-rule`, `column-rule-color`, `column-rule-style`,
+// and `column-rule-width`.
+let tests = [];
+
+for (const { propertyName, propertyValue } of [
+ { propertyName: "column-fill", propertyValue: "auto" },
+ { propertyName: "column-rule", propertyValue: "1px solid black" },
+ { propertyName: "column-rule-color", propertyValue: "black" },
+ { propertyName: "column-rule-style", propertyValue: "solid" },
+ { propertyName: "column-rule-width", propertyValue: "1px" },
+]) {
+ tests = tests.concat(createTestsForProp(propertyName, propertyValue));
+}
+
+function createTestsForProp(propertyName, propertyValue) {
+ return [
+ {
+ info: `${propertyName} is active on a multi-column container`,
+ property: propertyName,
+ tagName: "div",
+ rules: [`div { columns:2; ${propertyName}: ${propertyValue}; }`],
+ isActive: true,
+ },
+ {
+ info: `${propertyName} is inactive on a non-multi-column container`,
+ property: propertyName,
+ tagName: "div",
+ rules: [`div { ${propertyName}: ${propertyValue}; }`],
+ isActive: false,
+ },
+ ];
+}
+
+export default tests;
diff --git a/devtools/server/tests/chrome/inactive-property-helper/place-items-content.mjs b/devtools/server/tests/chrome/inactive-property-helper/place-items-content.mjs
new file mode 100644
index 0000000000..f554a785a7
--- /dev/null
+++ b/devtools/server/tests/chrome/inactive-property-helper/place-items-content.mjs
@@ -0,0 +1,159 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// InactivePropertyHelper `place-items` and `place-content` test cases.
+export default [
+ {
+ info: "place-items is inactive on block element",
+ property: "place-items",
+ tagName: "div",
+ rules: ["div { place-items: center; }"],
+ isActive: false,
+ },
+ {
+ info: "place-items is inactive on inline element",
+ property: "place-items",
+ tagName: "span",
+ rules: ["span { place-items: center; }"],
+ isActive: false,
+ },
+ {
+ info: "place-items is inactive on flex item",
+ property: "place-items",
+ createTestElement: rootNode => {
+ const container = document.createElement("div");
+ const element = document.createElement("span");
+ container.append(element);
+ rootNode.append(container);
+ return element;
+ },
+ rules: [
+ "div { display: flex; align-items: start; }",
+ "span { place-items: center; }",
+ ],
+ ruleIndex: 1,
+ isActive: false,
+ },
+ {
+ info: "place-items is inactive on grid item",
+ property: "place-items",
+ createTestElement: rootNode => {
+ const container = document.createElement("div");
+ const element = document.createElement("span");
+ container.append(element);
+ rootNode.append(container);
+ return element;
+ },
+ rules: [
+ "div { display: grid; align-items: start; }",
+ "span { place-items: center; }",
+ ],
+ ruleIndex: 1,
+ isActive: false,
+ },
+ {
+ info: "place-items is active on flex container",
+ property: "place-items",
+ tagName: "div",
+ rules: ["div { place-items: center; display: flex;}"],
+ isActive: true,
+ },
+ {
+ info: "place-items is active on inline-flex container",
+ property: "place-items",
+ tagName: "div",
+ rules: ["div { place-items: center; display: inline-flex;}"],
+ isActive: true,
+ },
+ {
+ info: "place-items is active on grid container",
+ property: "place-items",
+ tagName: "div",
+ rules: ["div { place-items: center; display: grid;}"],
+ isActive: true,
+ },
+ {
+ info: "place-items is active on inline grid container",
+ property: "place-items",
+ tagName: "div",
+ rules: ["div { place-items: center; display: inline-grid;}"],
+ isActive: true,
+ },
+ {
+ info: "place-content is inactive on block element",
+ property: "place-content",
+ tagName: "div",
+ rules: ["div { place-content: center; }"],
+ isActive: false,
+ },
+ {
+ info: "place-content is inactive on inline element",
+ property: "place-content",
+ tagName: "span",
+ rules: ["span { place-content: center; }"],
+ isActive: false,
+ },
+ {
+ info: "place-content is inactive on flex item",
+ property: "place-content",
+ createTestElement: rootNode => {
+ const container = document.createElement("div");
+ const element = document.createElement("span");
+ container.append(element);
+ rootNode.append(container);
+ return element;
+ },
+ rules: [
+ "div { display: flex; align-items: start; }",
+ "span { place-content: center; }",
+ ],
+ ruleIndex: 1,
+ isActive: false,
+ },
+ {
+ info: "place-content is inactive on grid item",
+ property: "place-content",
+ createTestElement: rootNode => {
+ const container = document.createElement("div");
+ const element = document.createElement("span");
+ container.append(element);
+ rootNode.append(container);
+ return element;
+ },
+ rules: [
+ "div { display: grid; align-items: start; }",
+ "span { place-content: center; }",
+ ],
+ ruleIndex: 1,
+ isActive: false,
+ },
+ {
+ info: "place-content is active on flex container",
+ property: "place-content",
+ tagName: "div",
+ rules: ["div { place-content: center; display: flex;}"],
+ isActive: true,
+ },
+ {
+ info: "place-content is active on inline-flex container",
+ property: "place-content",
+ tagName: "div",
+ rules: ["div { place-content: center; display: inline-flex;}"],
+ isActive: true,
+ },
+ {
+ info: "place-content is active on grid container",
+ property: "place-content",
+ tagName: "div",
+ rules: ["div { place-content: center; display: grid;}"],
+ isActive: true,
+ },
+ {
+ info: "place-content is active on inline grid container",
+ property: "place-content",
+ tagName: "div",
+ rules: ["div { place-content: center; display: inline-grid;}"],
+ isActive: true,
+ },
+];
diff --git a/devtools/server/tests/chrome/inactive-property-helper/placeholder-pseudo-element.mjs b/devtools/server/tests/chrome/inactive-property-helper/placeholder-pseudo-element.mjs
new file mode 100644
index 0000000000..6c9a81472b
--- /dev/null
+++ b/devtools/server/tests/chrome/inactive-property-helper/placeholder-pseudo-element.mjs
@@ -0,0 +1,122 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// InactivePropertyHelper `placeholder-pseudo-element` test cases.
+
+//"baseline-source",
+//"direction",
+//"dominant-baseline",
+//"line-height",
+//"text-orientation",
+//"vertical-align",
+//"writing-mode",
+//"alignment-baseline",
+//"baseline-shift",
+//"initial-letter",
+//"text-box-trim",
+
+export default [
+ {
+ info: "baseline-source is inactive on ::placeholder",
+ property: "baseline-source",
+ tagName: "input",
+ rules: ["input::placeholder { baseline-source: first; }"],
+ isActive: false,
+ expectedMsgId: "inactive-css-placeholder-pseudo-element-not-supported",
+ },
+ {
+ info: "direction is inactive on ::placeholder",
+ property: "direction",
+ tagName: "input",
+ rules: ["input::placeholder { direction: rtl; }"],
+ isActive: false,
+ expectedMsgId: "inactive-css-placeholder-pseudo-element-not-supported",
+ },
+ {
+ info: "dominant-baseline is inactive on ::placeholder",
+ property: "dominant-baseline",
+ tagName: "input",
+ rules: ["input::placeholder { dominant-baseline: central; }"],
+ isActive: false,
+ expectedMsgId: "inactive-css-placeholder-pseudo-element-not-supported",
+ },
+ {
+ info: "line-height is inactive on ::placeholder",
+ property: "line-height",
+ tagName: "input",
+ rules: ["input::placeholder { line-height: 2em; }"],
+ isActive: false,
+ expectedMsgId: "inactive-css-placeholder-pseudo-element-not-supported",
+ },
+ {
+ info: "text-orientation is inactive on ::placeholder",
+ property: "text-orientation",
+ tagName: "input",
+ rules: ["input::placeholder { text-orientation: sideways; }"],
+ isActive: false,
+ expectedMsgId: "inactive-css-placeholder-pseudo-element-not-supported",
+ },
+ {
+ info: "vertical-align is inactive on ::placeholder",
+ property: "vertical-align",
+ tagName: "input",
+ rules: ["input::placeholder { vertical-align: super; }"],
+ isActive: false,
+ expectedMsgId: "inactive-css-placeholder-pseudo-element-not-supported",
+ },
+ {
+ info: "writing-mode is inactive on ::placeholder",
+ property: "writing-mode",
+ tagName: "input",
+ rules: ["input::placeholder { writing-mode: vertical-rl; }"],
+ isActive: false,
+ expectedMsgId: "inactive-css-placeholder-pseudo-element-not-supported",
+ },
+ {
+ info: "alignment-baseline is inactive on ::placeholder",
+ property: "alignment-baseline",
+ tagName: "input",
+ rules: ["input::placeholder { alignment-baseline: central; }"],
+ isActive: false,
+ expectedMsgId: "inactive-css-placeholder-pseudo-element-not-supported",
+ },
+ {
+ info: "baseline-shift is inactive on ::placeholder",
+ property: "baseline-shift",
+ tagName: "input",
+ rules: ["input::placeholder { baseline-shift: super; }"],
+ isActive: false,
+ expectedMsgId: "inactive-css-placeholder-pseudo-element-not-supported",
+ },
+ {
+ info: "initial-letter is inactive on ::placeholder",
+ property: "initial-letter",
+ tagName: "input",
+ rules: ["input::placeholder { initial-letter: 2em; }"],
+ isActive: false,
+ expectedMsgId: "inactive-css-placeholder-pseudo-element-not-supported",
+ },
+ {
+ info: "text-box-trim is inactive on ::placeholder",
+ property: "text-box-trim",
+ tagName: "input",
+ rules: ["input::placeholder { text-box-trim: both; }"],
+ isActive: false,
+ expectedMsgId: "inactive-css-placeholder-pseudo-element-not-supported",
+ },
+ {
+ info: "color is active on ::placeholder",
+ property: "color",
+ tagName: "input",
+ rules: ["input::placeholder { color: green; }"],
+ isActive: true,
+ },
+ {
+ info: "display is active on ::placeholder",
+ property: "display",
+ tagName: "input",
+ rules: ["input::placeholder { display: grid; }"],
+ isActive: true,
+ },
+];
diff --git a/devtools/server/tests/chrome/inactive-property-helper/positioned.mjs b/devtools/server/tests/chrome/inactive-property-helper/positioned.mjs
new file mode 100644
index 0000000000..0386c278c5
--- /dev/null
+++ b/devtools/server/tests/chrome/inactive-property-helper/positioned.mjs
@@ -0,0 +1,82 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// InactivePropertyHelper positioned elements test cases.
+
+// These are the properties we care about, those that are inactive when the element isn't
+// positioned.
+const PROPERTIES = [
+ { property: "z-index", value: "2" },
+ { property: "top", value: "20px" },
+ { property: "right", value: "20px" },
+ { property: "bottom", value: "20px" },
+ { property: "left", value: "20px" },
+];
+
+// These are all of the different position values and whether the above properties are
+// active or not for each.
+const POSITIONS = [
+ { position: "unset", isActive: false },
+ { position: "initial", isActive: false },
+ { position: "inherit", isActive: false },
+ { position: "static", isActive: false },
+ { position: "absolute", isActive: true },
+ { position: "relative", isActive: true },
+ { position: "fixed", isActive: true },
+ { position: "sticky", isActive: true },
+];
+
+function makeTestCase(property, value, position, isActive) {
+ return {
+ info: `${property} is ${
+ isActive ? "" : "in"
+ }active when position is ${position}`,
+ property,
+ tagName: "div",
+ rules: [`div { ${property}: ${value}; position: ${position}; }`],
+ isActive,
+ };
+}
+
+// Make the test cases for all the combinations of PROPERTIES and POSITIONS
+const mainTests = [];
+
+for (const { property, value } of PROPERTIES) {
+ for (const { position, isActive } of POSITIONS) {
+ mainTests.push(makeTestCase(property, value, position, isActive));
+ }
+}
+
+// Add a few test cases to check that z-index actually works inside grids and flexboxes.
+mainTests.push({
+ info: "z-index is active even on unpositioned elements if they are grid items",
+ property: "z-index",
+ createTestElement: rootNode => {
+ const container = document.createElement("div");
+ const element = document.createElement("span");
+ container.append(element);
+ rootNode.append(container);
+ return element;
+ },
+ rules: ["div { display: grid; }", "span { z-index: 3; }"],
+ ruleIndex: 1,
+ isActive: true,
+});
+
+mainTests.push({
+ info: "z-index is active even on unpositioned elements if they are flex items",
+ property: "z-index",
+ createTestElement: rootNode => {
+ const container = document.createElement("div");
+ const element = document.createElement("span");
+ container.append(element);
+ rootNode.append(container);
+ return element;
+ },
+ rules: ["div { display: flex; }", "span { z-index: 3; }"],
+ ruleIndex: 1,
+ isActive: true,
+});
+
+export default mainTests;
diff --git a/devtools/server/tests/chrome/inactive-property-helper/scroll-padding.mjs b/devtools/server/tests/chrome/inactive-property-helper/scroll-padding.mjs
new file mode 100644
index 0000000000..acb2899be2
--- /dev/null
+++ b/devtools/server/tests/chrome/inactive-property-helper/scroll-padding.mjs
@@ -0,0 +1,159 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// InactivePropertyHelper `scroll-padding-*` test cases.
+
+export default [
+ {
+ info: "scroll-padding is active on element with auto-overflow",
+ property: "scroll-padding",
+ tagName: "div",
+ rules: ["div { overflow: auto; scroll-padding: 10px; }"],
+ isActive: true,
+ },
+ {
+ info: "scroll-padding is active on element with scrollable overflow",
+ property: "scroll-padding",
+ tagName: "div",
+ rules: ["div { overflow: scroll; scroll-padding: 10px; }"],
+ isActive: true,
+ },
+ {
+ info: "scroll-padding is active on element with hidden overflow",
+ property: "scroll-padding",
+ tagName: "div",
+ rules: ["div { overflow: hidden; scroll-padding: 10px; }"],
+ isActive: true,
+ },
+ {
+ info: "scroll-padding is inactive on element with visible overflow",
+ property: "scroll-padding",
+ tagName: "div",
+ rules: ["div { scroll-padding: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "scroll-padding is inactive on element with clipped overflow",
+ property: "scroll-padding",
+ tagName: "div",
+ rules: ["div { overflow: clip; scroll-padding: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "scroll-padding is inactive on element with horizontally clipped overflow",
+ property: "scroll-padding",
+ tagName: "div",
+ rules: ["div { overflow-x: clip; scroll-padding: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "scroll-padding is inactive on element with vertically clipped overflow",
+ property: "scroll-padding",
+ tagName: "div",
+ rules: ["div { overflow-y: clip; scroll-padding: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "scroll-padding-top is inactive on element with visible overflow",
+ property: "scroll-padding-top",
+ tagName: "div",
+ rules: ["div { scroll-padding-top: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "scroll-padding-top is inactive on element with horizontally clipped overflow",
+ property: "scroll-padding-top",
+ tagName: "div",
+ rules: ["div { overflow-x: clip; scroll-padding-top: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "scroll-padding-top is inactive on element with vertically clipped overflow",
+ property: "scroll-padding-top",
+ tagName: "div",
+ rules: ["div { overflow-y: clip; scroll-padding-top: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "scroll-padding-top is active on element with horizontally clipped but vertical auto-overflow (as 'clip' is computed to 'hidden')",
+ property: "scroll-padding-top",
+ tagName: "div",
+ rules: [
+ "div { overflow-x: clip; overflow-y: auto; scroll-padding-top: 10px; }",
+ ],
+ isActive: true,
+ },
+ {
+ info: "scroll-padding-top is active on element with vertically clipped but horizontal auto-overflow (as 'clip' is computed to 'hidden')",
+ property: "scroll-padding-top",
+ tagName: "div",
+ rules: [
+ "div { overflow-x: auto; overflow-y: clip; scroll-padding-top: 10px; }",
+ ],
+ isActive: true,
+ },
+ {
+ info: "scroll-padding-right is inactive on element with visible overflow",
+ property: "scroll-padding-right",
+ tagName: "div",
+ rules: ["div { scroll-padding-right: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "scroll-padding-bottom is inactive on element with visible overflow",
+ property: "scroll-padding-bottom",
+ tagName: "div",
+ rules: ["div { scroll-padding-bottom: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "scroll-padding-left is inactive on element with visible overflow",
+ property: "scroll-padding-left",
+ tagName: "div",
+ rules: ["div { scroll-padding-left: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "scroll-padding-block is inactive on element with visible overflow",
+ property: "scroll-padding-block",
+ tagName: "div",
+ rules: ["div { scroll-padding-block: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "scroll-padding-block-end is inactive on element with visible overflow",
+ property: "scroll-padding-block-end",
+ tagName: "div",
+ rules: ["div { scroll-padding-block-end: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "scroll-padding-block-start is inactive on element with visible overflow",
+ property: "scroll-padding-block-start",
+ tagName: "div",
+ rules: ["div { scroll-padding-block-start: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "scroll-padding-inline is inactive on element with visible overflow",
+ property: "scroll-padding-inline",
+ tagName: "div",
+ rules: ["div { scroll-padding-inline: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "scroll-padding-inline-end is inactive on element with visible overflow",
+ property: "scroll-padding-inline-end",
+ tagName: "div",
+ rules: ["div { scroll-padding-inline-end: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "scroll-padding-inline-start is inactive on element with visible overflow",
+ property: "scroll-padding-inline-start",
+ tagName: "div",
+ rules: ["div { scroll-padding-inline-start: 10px; }"],
+ isActive: false,
+ },
+];
diff --git a/devtools/server/tests/chrome/inactive-property-helper/table-cell.mjs b/devtools/server/tests/chrome/inactive-property-helper/table-cell.mjs
new file mode 100644
index 0000000000..bda1f27015
--- /dev/null
+++ b/devtools/server/tests/chrome/inactive-property-helper/table-cell.mjs
@@ -0,0 +1,21 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// InactivePropertyHelper `empty-cells` test cases.
+export default [
+ {
+ info: "empty-cells is inactive on block element",
+ property: "empty-cells",
+ tagName: "div",
+ rules: ["div { empty-cells: hide; }"],
+ isActive: false,
+ },
+ {
+ info: "empty-cells is active on table cell element",
+ property: "empty-cells",
+ tagName: "div",
+ rules: ["div { display: table-cell; empty-cells: hide; }"],
+ isActive: true,
+ },
+];
diff --git a/devtools/server/tests/chrome/inactive-property-helper/table.mjs b/devtools/server/tests/chrome/inactive-property-helper/table.mjs
new file mode 100644
index 0000000000..596698522c
--- /dev/null
+++ b/devtools/server/tests/chrome/inactive-property-helper/table.mjs
@@ -0,0 +1,28 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// InactivePropertyHelper `table-layout` test cases.
+export default [
+ {
+ info: "table-layout is inactive on block element",
+ property: "table-layout",
+ tagName: "div",
+ rules: ["div { table-layout: fixed; }"],
+ isActive: false,
+ },
+ {
+ info: "table-layout is active on table element",
+ property: "table-layout",
+ tagName: "div",
+ rules: ["div { display: table; table-layout: fixed; }"],
+ isActive: true,
+ },
+ {
+ info: "table-layout is active on inline table element",
+ property: "table-layout",
+ tagName: "div",
+ rules: ["div { display: inline-table; table-layout: fixed; }"],
+ isActive: true,
+ },
+];
diff --git a/devtools/server/tests/chrome/inactive-property-helper/text-overflow.mjs b/devtools/server/tests/chrome/inactive-property-helper/text-overflow.mjs
new file mode 100644
index 0000000000..ada2211b3a
--- /dev/null
+++ b/devtools/server/tests/chrome/inactive-property-helper/text-overflow.mjs
@@ -0,0 +1,92 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// InactivePropertyHelper `text-overflow` test cases.
+export default [
+ {
+ info: "text-overflow is inactive when overflow is not set",
+ property: "text-overflow",
+ tagName: "div",
+ rules: ["div { text-overflow: ellipsis; }"],
+ isActive: false,
+ },
+ {
+ info: "text-overflow is active when overflow is set to hidden",
+ property: "text-overflow",
+ tagName: "div",
+ rules: ["div { text-overflow: ellipsis; overflow: hidden; }"],
+ isActive: true,
+ },
+ {
+ info: "text-overflow is active when overflow is set to auto",
+ property: "text-overflow",
+ tagName: "div",
+ rules: ["div { text-overflow: ellipsis; overflow: auto; }"],
+ isActive: true,
+ },
+ {
+ info: "text-overflow is active when overflow is set to scroll",
+ property: "text-overflow",
+ tagName: "div",
+ rules: ["div { text-overflow: ellipsis; overflow: scroll; }"],
+ isActive: true,
+ },
+ {
+ info: "text-overflow is inactive when overflow is set to visible",
+ property: "text-overflow",
+ tagName: "div",
+ rules: ["div { text-overflow: ellipsis; overflow: visible; }"],
+ isActive: false,
+ },
+ {
+ info: "text-overflow is active when overflow-x is set to hidden on horizontal writing mode",
+ property: "text-overflow",
+ tagName: "div",
+ rules: [
+ "div { writing-mode: lr; text-overflow: ellipsis; overflow-x: hidden; }",
+ ],
+ isActive: true,
+ },
+ {
+ info: "text-overflow is inactive when overflow-x is set to visible on horizontal writing mode",
+ property: "text-overflow",
+ tagName: "div",
+ rules: [
+ "div { writing-mode: lr; text-overflow: ellipsis; overflow-x: visible; }",
+ ],
+ isActive: false,
+ },
+ {
+ info: "text-overflow is active when overflow-y is set to hidden on vertical writing mode",
+ property: "text-overflow",
+ tagName: "div",
+ rules: [
+ "div { writing-mode: vertical-lr; text-overflow: ellipsis; overflow-y: hidden; }",
+ ],
+ isActive: true,
+ },
+ {
+ info: "text-overflow is inactive when overflow-y is set to visible on vertical writing mode",
+ property: "text-overflow",
+ tagName: "div",
+ rules: [
+ "div { writing-mode: vertical-lr; text-overflow: ellipsis; overflow-y: visible; }",
+ ],
+ isActive: false,
+ },
+ {
+ info: "as soon as overflow:hidden is set, text-overflow is active whatever the box type",
+ property: "text-overflow",
+ tagName: "span",
+ rules: ["span { text-overflow: ellipsis; overflow: hidden; }"],
+ isActive: true,
+ },
+ {
+ info: "as soon as overflow:hidden is set, text-overflow is active whatever the box type",
+ property: "text-overflow",
+ tagName: "legend",
+ rules: ["legend { text-overflow: ellipsis; overflow: hidden; }"],
+ isActive: true,
+ },
+];
diff --git a/devtools/server/tests/chrome/inactive-property-helper/text-wrap.mjs b/devtools/server/tests/chrome/inactive-property-helper/text-wrap.mjs
new file mode 100644
index 0000000000..58751aa764
--- /dev/null
+++ b/devtools/server/tests/chrome/inactive-property-helper/text-wrap.mjs
@@ -0,0 +1,86 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// InactivePropertyHelper `text-wrap: balance` test cases.
+const LOREM_IPSUM = `
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+ Donec a diam lectus. Sed sit amet ipsum mauris.
+ Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit.
+`;
+
+export default [
+ {
+ info: "text-wrap: balance; is inactive when line number exceeds threshold",
+ property: "text-wrap",
+ createTestElement: rootNode => {
+ const element = document.createElement("div");
+ element.textContent = LOREM_IPSUM;
+ rootNode.append(element);
+ return element;
+ },
+ tagName: "div",
+ rules: ["div { text-wrap: balance; width: 100px; }"],
+ isActive: false,
+ },
+ {
+ info: "text-wrap: balance; is active when line number is below threshold",
+ property: "text-wrap",
+ createTestElement: rootNode => {
+ const element = document.createElement("div");
+ element.textContent = LOREM_IPSUM;
+ rootNode.append(element);
+ return element;
+ },
+ tagName: "div",
+ rules: ["div { text-wrap: balance; width: 300px; }"],
+ isActive: true,
+ },
+ {
+ info: "text-wrap: balance is inactive when element is fragmented",
+ property: "text-wrap",
+ createTestElement: rootNode => {
+ const element = document.createElement("div");
+ element.textContent = `
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+ Donec a diam lectus. Sed sit amet ipsum mauris.
+ Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit.
+ `;
+ rootNode.append(element);
+ return element;
+ },
+ tagName: "div",
+ rules: ["div { text-wrap: balance; column-count: 2; }"],
+ isActive: false,
+ },
+ {
+ info: "text-wrap: balance; does not throw if element is not a block",
+ property: "text-wrap",
+ createTestElement: rootNode => {
+ const element = document.createElement("div");
+ element.textContent = LOREM_IPSUM;
+ rootNode.append(element);
+ return element;
+ },
+ tagName: "div",
+ rules: ["div { text-wrap: balance; display: inline; }"],
+ isActive: true,
+ },
+ {
+ info: "text-wrap: initial; is active",
+ property: "text-wrap",
+ createTestElement: rootNode => {
+ const element = document.createElement("div");
+ element.textContent = `
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+ Donec a diam lectus. Sed sit amet ipsum mauris.
+ Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit.
+ `;
+ rootNode.append(element);
+ return element;
+ },
+ tagName: "div",
+ rules: ["div { text-wrap: initial; width: 100px; }"],
+ isActive: true,
+ },
+];
diff --git a/devtools/server/tests/chrome/inactive-property-helper/vertical-align.mjs b/devtools/server/tests/chrome/inactive-property-helper/vertical-align.mjs
new file mode 100644
index 0000000000..e9873d4865
--- /dev/null
+++ b/devtools/server/tests/chrome/inactive-property-helper/vertical-align.mjs
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// InactivePropertyHelper `vertical-align` test cases.
+export default [
+ {
+ info: "vertical-align is inactive on a block element",
+ property: "vertical-align",
+ tagName: "div",
+ rules: ["div { vertical-align: top; }"],
+ isActive: false,
+ },
+ {
+ info: "vertical-align is inactive on a span with display block",
+ property: "vertical-align",
+ tagName: "span",
+ rules: ["span { vertical-align: top; display: block;}"],
+ isActive: false,
+ },
+ {
+ info: "vertical-align is active on a div with display inline-block",
+ property: "vertical-align",
+ tagName: "div",
+ rules: ["div { vertical-align: top; display: inline-block;}"],
+ isActive: true,
+ },
+ {
+ info: "vertical-align is active on a table-cell",
+ property: "vertical-align",
+ tagName: "div",
+ rules: ["div { vertical-align: top; display: table-cell;}"],
+ isActive: true,
+ },
+ {
+ info: "vertical-align is active on a block element ::first-letter",
+ property: "vertical-align",
+ tagName: "div",
+ rules: ["div::first-letter { vertical-align: top; }"],
+ isActive: true,
+ },
+ {
+ info: "vertical-align is active on a block element ::first-line",
+ property: "vertical-align",
+ tagName: "div",
+ rules: ["div::first-line { vertical-align: top; }"],
+ isActive: true,
+ },
+ {
+ info: "vertical-align is active on an inline element",
+ property: "vertical-align",
+ tagName: "span",
+ rules: ["span { vertical-align: top; }"],
+ isActive: true,
+ },
+];
diff --git a/devtools/server/tests/chrome/inactive-property-helper/width-height-ruby.mjs b/devtools/server/tests/chrome/inactive-property-helper/width-height-ruby.mjs
new file mode 100644
index 0000000000..0dda222e0b
--- /dev/null
+++ b/devtools/server/tests/chrome/inactive-property-helper/width-height-ruby.mjs
@@ -0,0 +1,147 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// InactivePropertyHelper `width & height on ruby element` test cases.
+export default [
+ {
+ info: "width is inactive on ruby element",
+ property: "width",
+ tagName: "ruby",
+ rules: ["ruby { width: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "min-width is inactive on ruby element",
+ property: "min-width",
+ tagName: "ruby",
+ rules: ["ruby { min-width: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "max-width is inactive on ruby element",
+ property: "max-width",
+ tagName: "ruby",
+ rules: ["ruby { max-width: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "height is inactive on ruby element",
+ property: "height",
+ tagName: "ruby",
+ rules: ["ruby { height: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "min-height is inactive on ruby element",
+ property: "min-height",
+ tagName: "ruby",
+ rules: ["ruby { min-height: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "max-height is inactive on ruby element",
+ property: "max-height",
+ tagName: "ruby",
+ rules: ["ruby { max-height: 10px; }"],
+ isActive: false,
+ },
+ {
+ info: "width is active on div element",
+ property: "width",
+ tagName: "div",
+ rules: ["div { width: 10px; }"],
+ isActive: true,
+ },
+ {
+ info: "min-width is active on div element",
+ property: "min-width",
+ tagName: "div",
+ rules: ["div { min-width: 10px; }"],
+ isActive: true,
+ },
+ {
+ info: "max-width is active on div element",
+ property: "max-width",
+ tagName: "div",
+ rules: ["div { max-width: 10px; }"],
+ isActive: true,
+ },
+ {
+ info: "height is active on div element",
+ property: "height",
+ tagName: "div",
+ rules: ["div { height: 10px; }"],
+ isActive: true,
+ },
+ {
+ info: "min-height is active on div element",
+ property: "min-height",
+ tagName: "div",
+ rules: ["div { min-height: 10px; }"],
+ isActive: true,
+ },
+ {
+ info: "max-height is active on div element",
+ property: "max-height",
+ tagName: "div",
+ rules: ["div { max-height: 10px; }"],
+ isActive: true,
+ },
+ {
+ info: "width is inactive on div element with display ruby",
+ property: "width",
+ tagName: "div",
+ rules: ["div { width: 10px; display: ruby;}"],
+ isActive: false,
+ },
+ {
+ info: "height is inactive on div element with display ruby",
+ property: "height",
+ tagName: "div",
+ rules: ["div { height: 10px; display: ruby;}"],
+ isActive: false,
+ },
+ {
+ info: "width is active on ruby element with display block",
+ property: "width",
+ tagName: "ruby",
+ rules: ["ruby { width: 10px; display: block;}"],
+ isActive: true,
+ },
+ {
+ info: "height is active on ruby element with display block",
+ property: "height",
+ tagName: "ruby",
+ rules: ["ruby { height: 10px; display: block;}"],
+ isActive: true,
+ },
+ {
+ info: "width is inactive on ruby-text element",
+ property: "width",
+ tagName: "rt",
+ rules: ["rt { width: 10px;}"],
+ isActive: false,
+ },
+ {
+ info: "height is inactive on ruby-text element",
+ property: "height",
+ tagName: "rt",
+ rules: ["rt { height: 10px;}"],
+ isActive: false,
+ },
+ {
+ info: "width is inactive on div elements with display ruby-text",
+ property: "width",
+ tagName: "div",
+ rules: ["div { width: 10px; display: ruby-text;}"],
+ isActive: false,
+ },
+ {
+ info: "height is inactive on div elements with display ruby-text",
+ property: "height",
+ tagName: "div",
+ rules: ["div { height: 10px; display: ruby-text;}"],
+ isActive: false,
+ },
+];
diff --git a/devtools/server/tests/chrome/inspector-delay-image-response.sjs b/devtools/server/tests/chrome/inspector-delay-image-response.sjs
new file mode 100644
index 0000000000..633d7e3aa6
--- /dev/null
+++ b/devtools/server/tests/chrome/inspector-delay-image-response.sjs
@@ -0,0 +1,46 @@
+/**
+ * Adapted from https://searchfox.org/mozilla-central/source/layout/reftests/backgrounds/delay-image-response.sjs
+ */
+"use strict";
+
+// A 1x1 PNG image.
+// Source: https://commons.wikimedia.org/wiki/File:1x1.png (Public Domain)
+const IMAGE = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" +
+ "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII="
+);
+
+// To avoid GC.
+let timer = null;
+
+function handleRequest(request, response) {
+ const query = {};
+ request.queryString.split("&").forEach(function (val) {
+ const [name, value] = val.split("=");
+ query[name] = unescape(value);
+ });
+
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "image/png", false);
+
+ // If there is no delay, we write the image and leave.
+ if (!("delay" in query)) {
+ response.write(IMAGE);
+ return;
+ }
+
+ // If there is a delay, we create a timer which, when it fires, will write
+ // image and leave.
+ response.processAsync();
+ const nsITimer = Ci.nsITimer;
+
+ timer = Cc["@mozilla.org/timer;1"].createInstance(nsITimer);
+ timer.initWithCallback(
+ function () {
+ response.write(IMAGE);
+ response.finish();
+ },
+ query.delay,
+ nsITimer.TYPE_ONE_SHOT
+ );
+}
diff --git a/devtools/server/tests/chrome/inspector-eyedropper.html b/devtools/server/tests/chrome/inspector-eyedropper.html
new file mode 100644
index 0000000000..f5bd3a1cb8
--- /dev/null
+++ b/devtools/server/tests/chrome/inspector-eyedropper.html
@@ -0,0 +1,20 @@
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Inspector Eyedropper tests</title>
+ <style>
+ html {
+ background: black;
+ }
+ </style>
+ <script type="text/javascript">
+ "use strict";
+
+ window.onload = function() {
+ window.opener.postMessage("ready", "*");
+ };
+ </script>
+</head>
+</body>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/inspector-helpers.js b/devtools/server/tests/chrome/inspector-helpers.js
new file mode 100644
index 0000000000..0b7edd8035
--- /dev/null
+++ b/devtools/server/tests/chrome/inspector-helpers.js
@@ -0,0 +1,133 @@
+/* exported attachURL, promiseDone,
+ promiseOnce,
+ addTest, addAsyncTest,
+ runNextTest, _documentWalker */
+"use strict";
+
+const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+const {
+ CommandsFactory,
+} = require("resource://devtools/shared/commands/commands-factory.js");
+const {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+const { BrowserTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/BrowserTestUtils.sys.mjs"
+);
+const {
+ DocumentWalker: _documentWalker,
+} = require("resource://devtools/server/actors/inspector/document-walker.js");
+
+// Always log packets when running tests.
+Services.prefs.setBoolPref("devtools.debugger.log", true);
+SimpleTest.registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("devtools.debugger.log");
+});
+
+if (!DevToolsServer.initialized) {
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+ SimpleTest.registerCleanupFunction(function () {
+ DevToolsServer.destroy();
+ });
+}
+
+var gAttachCleanups = [];
+
+SimpleTest.registerCleanupFunction(function () {
+ for (const cleanup of gAttachCleanups) {
+ cleanup();
+ }
+});
+
+/**
+ * Open a tab, load the url, wait for it to signal its readiness,
+ * connect to this tab via DevTools protocol and return.
+ *
+ * Returns an object with a few helpful attributes:
+ * - commands {Object}: The commands object defined by modules from devtools/shared/commands
+ * - target {TargetFront}: The current top-level target front.
+ * - doc {HtmlDocument}: the tab's document that got opened
+ */
+async function attachURL(url) {
+ // Get the current browser window
+ const gBrowser =
+ Services.wm.getMostRecentWindow("navigator:browser").gBrowser;
+
+ // open the url in a new tab, save a reference to the new inner window global object
+ // and wait for it to load. The tests rely on this window object to send a "ready"
+ // event to its opener (the test page). This window reference is used within
+ // the test tab, to reference the webpage being tested against, which is in another
+ // tab.
+ const windowOpened = BrowserTestUtils.waitForNewTab(gBrowser, url);
+ const win = window.open(url, "_blank");
+ await windowOpened;
+
+ const commands = await CommandsFactory.forTab(gBrowser.selectedTab);
+ await commands.targetCommand.startListening();
+
+ const cleanup = async function () {
+ await commands.destroy();
+ if (win) {
+ win.close();
+ }
+ };
+
+ gAttachCleanups.push(cleanup);
+ return {
+ commands,
+ target: commands.targetCommand.targetFront,
+ doc: win.document,
+ };
+}
+
+function promiseOnce(target, event) {
+ return new Promise(resolve => {
+ target.on(event, (...args) => {
+ if (args.length === 1) {
+ resolve(args[0]);
+ } else {
+ resolve(args);
+ }
+ });
+ });
+}
+
+function promiseDone(currentPromise) {
+ currentPromise.catch(err => {
+ ok(false, "Promise failed: " + err);
+ if (err.stack) {
+ dump(err.stack);
+ }
+ SimpleTest.finish();
+ });
+}
+
+var _tests = [];
+function addTest(test) {
+ _tests.push(test);
+}
+
+function addAsyncTest(generator) {
+ _tests.push(() => generator().catch(ok.bind(null, false)));
+}
+
+function runNextTest() {
+ if (!_tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+ const fn = _tests.shift();
+ try {
+ fn();
+ } catch (ex) {
+ info(
+ "Test function " +
+ (fn.name ? "'" + fn.name + "' " : "") +
+ "threw an exception: " +
+ ex
+ );
+ }
+}
diff --git a/devtools/server/tests/chrome/inspector-search-data.html b/devtools/server/tests/chrome/inspector-search-data.html
new file mode 100644
index 0000000000..784dcb7c9b
--- /dev/null
+++ b/devtools/server/tests/chrome/inspector-search-data.html
@@ -0,0 +1,54 @@
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Inspector Search Test Data</title>
+ <style>
+ #pseudo {
+ display: block;
+ margin: 0;
+ }
+ #pseudo:before {
+ content: "before element";
+ }
+ #pseudo:after {
+ content: "after element";
+ }
+ </style>
+ <script type="text/javascript">
+ "use strict";
+
+ window.onload = function() {
+ window.opener.postMessage("ready", "*");
+ };
+ </script>
+</head>
+</body>
+ <!-- A comment
+ spread across multiple lines -->
+
+ <img width="100" height="100" src="large-image.jpg" />
+
+ <h1 id="pseudo">Heading 1</h1>
+ <p>A p tag with the text 'h1' inside of it.
+ <strong>A strong h1 result</strong>
+ </p>
+
+ <div id="arrows" northwest="↖" northeast="↗" southeast="↘" southwest="↙">
+ Unicode arrows
+ </div>
+
+ <h2>Heading 2</h2>
+ <h2>Heading 2</h2>
+ <h2>Heading 2</h2>
+
+ <h3>Heading 3</h3>
+ <h3>Heading 3</h3>
+ <h3>Heading 3</h3>
+
+ <h4>Heading 4</h4>
+ <h4>Heading 4</h4>
+ <h4>Heading 4</h4>
+
+ <div class="💩" id="💩" 💩="💩"></div>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/inspector-styles-data.css b/devtools/server/tests/chrome/inspector-styles-data.css
new file mode 100644
index 0000000000..5c3652f522
--- /dev/null
+++ b/devtools/server/tests/chrome/inspector-styles-data.css
@@ -0,0 +1,3 @@
+.external-rule {
+ cursor: crosshair;
+}
diff --git a/devtools/server/tests/chrome/inspector-styles-data.html b/devtools/server/tests/chrome/inspector-styles-data.html
new file mode 100644
index 0000000000..334b268bfd
--- /dev/null
+++ b/devtools/server/tests/chrome/inspector-styles-data.html
@@ -0,0 +1,85 @@
+<html>
+<script>
+ "use strict";
+
+ window.onload = () => {
+ window.opener.postMessage("ready", "*");
+ };
+</script>
+<style>
+ .inheritable-rule {
+ font-size: 15px;
+ }
+ /* Has to be on one line, is such for test */
+ .column-rule { font-size: 20px; } .column-rule { font-size: 25px; }
+ .uninheritable-rule {
+ background-color: #f06;
+ }
+ @media screen {
+ #mediaqueried {
+ background-color: #f06;
+ }
+ }
+ #svgcontent rect {
+ fill: rgb(1,2,3);
+ }
+
+ #layout-element,
+ #layout-auto-margin-element {
+ width: 50px;
+ height: 50px;
+ padding: 3px 5px 7px 5px;
+ border: 5px solid red;
+ margin: 10px 20px 30px 0;
+ box-sizing: border-box;
+ position: absolute;
+ z-index: 2;
+ }
+
+ #layout-auto-margin-element {
+ margin: 10px auto;
+ }
+</style>
+<link type="text/css" rel="stylesheet" href="inspector-styles-data.css"></link>
+<body>
+ <h1>Style Actor Tests</h1>
+ <!-- Inheritance checks -->
+ <div id="inheritable-rule-uninheritable-style" class="inheritable-rule" style="background-color: purple">
+ <div id="inheritable-rule-inheritable-style" class="inheritable-rule" style="color: blue">
+ <div id="uninheritable-rule-uninheritable-style" class="uninheritable-rule" style="background-color: green">
+ <div id="uninheritable-rule-inheritable-style" class="uninheritable-rule" style="color: red">
+ <div id="test-node">
+ Here is the test node.
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- Computed checks -->
+ <div id="computed-parent" class="external-rule inheritable-rule uninheritable-rule" style="color: red;">
+ <div id="computed-test-node" class="external-rule">
+ Here is the test node.
+ </div>
+ </div>
+
+ <!-- Matched checks -->
+ <div id="matched-parent" class="external-rule inheritable-rule column-rule uninheritable-rule" style="color: red;">
+ <div id="matched-test-node" style="font-size: 10px" class="external-rule">
+ Here is the test node.
+ </div>
+ </div>
+
+ <div id="mediaqueried">
+ Screen mediaqueried.
+ </div>
+
+ <div id="svgcontent">
+ <svg><rect></rect></svg>
+ </div>
+
+ <div id="layout-element">I can has layout</div>
+ <div id="layout-auto-margin-element">I can has layout too</div>
+
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/inspector-template.html b/devtools/server/tests/chrome/inspector-template.html
new file mode 100644
index 0000000000..13c9d5c7d3
--- /dev/null
+++ b/devtools/server/tests/chrome/inspector-template.html
@@ -0,0 +1,17 @@
+<html>
+<body>
+ <template>
+ <p>template content</p>
+ </template>
+ <div></div>
+ <script>
+ "use strict";
+
+ const template = document.querySelector("template");
+ const clone = document.importNode(template.content, true);
+ document.querySelector("div").appendChild(clone);
+
+ window.opener.postMessage("ready", "*");
+ </script>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/inspector-traversal-data.html b/devtools/server/tests/chrome/inspector-traversal-data.html
new file mode 100644
index 0000000000..e294796467
--- /dev/null
+++ b/devtools/server/tests/chrome/inspector-traversal-data.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Inspector Traversal Test Data</title>
+ <style type="text/css">
+ #pseudo::before {
+ content: "before";
+ }
+ #pseudo::after {
+ content: "after";
+ }
+ #pseudo-empty::before {
+ content: "before an empty element";
+ }
+ #shadow::before {
+ content: "Testing ::before on a shadow host";
+ }
+ </style>
+ <script type="text/javascript">
+ "use strict";
+
+ window.onload = function() {
+ // Set up a basic shadow DOM
+ const host = document.querySelector("#shadow");
+ if (host.attachShadow) {
+ const root = host.attachShadow({ mode: "open" });
+
+ const h3 = document.createElement("h3");
+ h3.append("Shadow ");
+
+ const em = document.createElement("em");
+ em.append("DOM");
+
+ const select = document.createElement("select");
+ select.setAttribute("multiple", "");
+ h3.appendChild(em);
+ root.appendChild(h3);
+ root.appendChild(select);
+ }
+
+ // Put a copy of the body in an iframe to test frame traversal.
+ const body = document.querySelector("body");
+ const data = "data:text/html,<html>" + body.outerHTML + "<html>";
+ const iframe = document.createElement("iframe");
+ iframe.setAttribute("id", "childFrame");
+ iframe.onload = function() {
+ window.opener.postMessage("ready", "*");
+ };
+ iframe.src = data;
+ body.appendChild(iframe);
+ };
+ </script>
+</head>
+<body style="background-color:white">
+ <h1>Inspector Actor Tests</h1>
+ <span id="longstring">longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong</span>
+ <span id="shortstring">short</span>
+ <span id="empty"></span>
+ <div id="longlist" data-test="exists">
+ <div id="a">a</div>
+ <div id="b">b</div>
+ <div id="c">c</div>
+ <div id="d">d</div>
+ <div id="e">e</div>
+ <div id="f">f</div>
+ <div id="g">g</div>
+ <div id="h">h</div>
+ <div id="i">i</div>
+ <div id="j">j</div>
+ <div id="k">k</div>
+ <div id="l">l</div>
+ <div id="m">m</div>
+ <div id="n">n</div>
+ <div id="o">o</div>
+ <div id="p">p</div>
+ <div id="q">q</div>
+ <div id="r">r</div>
+ <div id="s">s</div>
+ <div id="t">t</div>
+ <div id="u">u</div>
+ <div id="v">v</div>
+ <div id="w">w</div>
+ <div id="x">x</div>
+ <div id="y">y</div>
+ <div id="z">z</div>
+ </div>
+ <div id="longlist-sibling">
+ <div id="longlist-sibling-firstchild"></div>
+ </div>
+ <p id="edit-html"></p>
+
+ <select multiple><option>one</option><option>two</option></select>
+ <div id="pseudo"><span>middle</span></div>
+ <div id="pseudo-empty"></div>
+ <div id="shadow">light dom</div>
+ <object>
+ <div id="1"></div>
+ </object>
+ <div class="node-to-duplicate"></div>
+ <div id="scroll-into-view" style="margin-top: 1000px;">scroll</div>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/inspector_css-properties.html b/devtools/server/tests/chrome/inspector_css-properties.html
new file mode 100644
index 0000000000..8cc6368cd1
--- /dev/null
+++ b/devtools/server/tests/chrome/inspector_css-properties.html
@@ -0,0 +1,12 @@
+<html>
+<head>
+<body>
+ <script type="text/javascript">
+ "use strict";
+
+ window.onload = function() {
+ window.opener.postMessage("ready", "*");
+ };
+ </script>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/inspector_display-type.html b/devtools/server/tests/chrome/inspector_display-type.html
new file mode 100644
index 0000000000..7bd0da6709
--- /dev/null
+++ b/devtools/server/tests/chrome/inspector_display-type.html
@@ -0,0 +1,17 @@
+<html>
+<head>
+<body>
+ <div id="inline-block" style="display: inline-block">
+ HELLO WORLD
+ </div>
+ <div id="grid" style="display: grid"></div>
+ <div id="block" style="position: fixed"></div>
+ <script>
+ "use strict";
+
+ window.onload = () => {
+ window.opener.postMessage("ready", "*");
+ };
+ </script>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/inspector_getImageData.html b/devtools/server/tests/chrome/inspector_getImageData.html
new file mode 100644
index 0000000000..754798df44
--- /dev/null
+++ b/devtools/server/tests/chrome/inspector_getImageData.html
@@ -0,0 +1,23 @@
+<html>
+<head>
+<body>
+ <img class="custom">
+ <img class="big-horizontal" src="large-image.jpg" style="width:500px;">
+ <canvas class="big-vertical" style="width:500px;"></canvas>
+ <img class="small" src="small-image.gif">
+ <img class="data" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=">
+ <script>
+ "use strict";
+
+ window.onload = () => {
+ const canvas = document.querySelector("canvas"), ctx = canvas.getContext("2d");
+ canvas.width = 1000;
+ canvas.height = 2000;
+ ctx.fillStyle = "red";
+ ctx.fillRect(0, 0, 1000, 2000);
+
+ window.opener.postMessage("ready", "*");
+ };
+ </script>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/inspector_getOffsetParent.html b/devtools/server/tests/chrome/inspector_getOffsetParent.html
new file mode 100644
index 0000000000..72aac5f70b
--- /dev/null
+++ b/devtools/server/tests/chrome/inspector_getOffsetParent.html
@@ -0,0 +1,18 @@
+<html>
+<head>
+<body>
+ <div id="relative_parent" style="position: relative">
+ <div id="absolute_child" style="position: absolute"></div>
+ </div>
+ <div id="static"></div>
+ <div id="no_parent" style="position: absolute"></div>
+ <div id="fixed" style="position: fixed"></div>
+ <script>
+ "use strict";
+
+ window.onload = () => {
+ window.opener.postMessage("ready", "*");
+ };
+ </script>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/large-image.jpg b/devtools/server/tests/chrome/large-image.jpg
new file mode 100644
index 0000000000..bda383e594
--- /dev/null
+++ b/devtools/server/tests/chrome/large-image.jpg
Binary files differ
diff --git a/devtools/server/tests/chrome/memory-helpers.js b/devtools/server/tests/chrome/memory-helpers.js
new file mode 100644
index 0000000000..e4db689134
--- /dev/null
+++ b/devtools/server/tests/chrome/memory-helpers.js
@@ -0,0 +1,72 @@
+/* exported Task, startServerAndGetSelectedTabMemory, destroyServerAndFinish,
+ waitForTime, waitUntil */
+"use strict";
+
+const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+const {
+ CommandsFactory,
+} = require("resource://devtools/shared/commands/commands-factory.js");
+
+// Always log packets when running tests.
+Services.prefs.setBoolPref("devtools.debugger.log", true);
+var gReduceTimePrecision = Services.prefs.getBoolPref(
+ "privacy.reduceTimerPrecision"
+);
+Services.prefs.setBoolPref("privacy.reduceTimerPrecision", false);
+SimpleTest.registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("devtools.debugger.log");
+ Services.prefs.setBoolPref(
+ "privacy.reduceTimerPrecision",
+ gReduceTimePrecision
+ );
+});
+
+async function getTargetForSelectedTab() {
+ const browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ const commands = await CommandsFactory.forTab(
+ browserWindow.gBrowser.selectedTab
+ );
+ await commands.targetCommand.startListening();
+ const isEveryFrameTargetEnabled = Services.prefs.getBoolPref(
+ "devtools.every-frame-target.enabled",
+ false
+ );
+ if (!isEveryFrameTargetEnabled) {
+ return commands.targetCommand.targetFront;
+ }
+
+ // If EFT is enabled, we need to retrieve the target of the test document
+ const targets = await commands.targetCommand.getAllTargets([
+ commands.targetCommand.TYPES.FRAME,
+ ]);
+
+ return targets.find(t => t.url !== "chrome://mochikit/content/harness.xhtml");
+}
+
+async function startServerAndGetSelectedTabMemory() {
+ const target = await getTargetForSelectedTab();
+ const memory = await target.getFront("memory");
+ return { memory, target };
+}
+
+async function destroyServerAndFinish(target) {
+ await target.destroy();
+ SimpleTest.finish();
+}
+
+function waitForTime(ms) {
+ return new Promise((resolve, reject) => {
+ setTimeout(resolve, ms);
+ });
+}
+
+function waitUntil(predicate) {
+ if (predicate()) {
+ return Promise.resolve(true);
+ }
+ return new Promise(resolve =>
+ setTimeout(() => waitUntil(predicate).then(() => resolve(true)), 10)
+ );
+}
diff --git a/devtools/server/tests/chrome/nonchrome_unsafeDereference.html b/devtools/server/tests/chrome/nonchrome_unsafeDereference.html
new file mode 100644
index 0000000000..15e9fd9160
--- /dev/null
+++ b/devtools/server/tests/chrome/nonchrome_unsafeDereference.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+<script>
+"use strict";
+
+var xhr = new XMLHttpRequest();
+xhr.timeout = 1742;
+xhr.expando = "Expando!";
+</script>
+</html>
diff --git a/devtools/server/tests/chrome/small-image.gif b/devtools/server/tests/chrome/small-image.gif
new file mode 100644
index 0000000000..e702427a53
--- /dev/null
+++ b/devtools/server/tests/chrome/small-image.gif
Binary files differ
diff --git a/devtools/server/tests/chrome/suspendTimeouts_content.html b/devtools/server/tests/chrome/suspendTimeouts_content.html
new file mode 100644
index 0000000000..f3969fc10c
--- /dev/null
+++ b/devtools/server/tests/chrome/suspendTimeouts_content.html
@@ -0,0 +1 @@
+<script src='suspendTimeouts_content.js'></script>
diff --git a/devtools/server/tests/chrome/suspendTimeouts_content.js b/devtools/server/tests/chrome/suspendTimeouts_content.js
new file mode 100644
index 0000000000..cb41653cff
--- /dev/null
+++ b/devtools/server/tests/chrome/suspendTimeouts_content.js
@@ -0,0 +1,75 @@
+"use strict";
+
+// To make it easier to follow, this code is arranged so that the functions are
+// arranged in the order they are called.
+
+const worker = new Worker("suspendTimeouts_worker.js");
+worker.onerror = error => {
+ const message = `error from worker: ${error.filename}:${error.lineno}: ${error.message}`;
+ throw new Error(message);
+};
+
+// Create a message channel. Send one end to the worker, and return the other to
+// the mochitest.
+/* exported create_channel */
+function create_channel() {
+ const { port1, port2 } = new MessageChannel();
+ info(`sending port to worker`);
+ worker.postMessage({ mochitestPort: port1 }, [port1]);
+ return port2;
+}
+
+// Provoke the worker into sending us a message, and then refuse to receive said
+// message, causing it to be delayed for later delivery.
+//
+// The worker will also post a message to the MessagePort we sent it earlier.
+// That message should not be delayed, as it is handled by the mochitest window,
+// not the content window. Its receipt signals that the test can assume that the
+// runnable for step 2) is in the main thread's event queue, so the test can
+// prepare for step 3).
+/* exported start_worker */
+function start_worker() {
+ worker.onmessage = handle_echo;
+
+ // This should prevent worker.onmessage from being called, until
+ // resumeTimeouts is called.
+ //
+ // This function is provided by test_suspendTimeouts.js.
+ // eslint-disable-next-line no-undef
+ suspendTimeouts();
+
+ // The worker should echo this message back to us and to the mochitest.
+ worker.postMessage("HALLOOOOOO"); // suitable message for echoing
+ info(`posted message to worker`);
+}
+
+var resumeTimeouts_has_returned = false;
+
+// Resume timeouts. After this call, the worker's message should not be
+// delivered to our onmessage handler until control returns to the event loop.
+/* exported resume_timeouts */
+function resume_timeouts() {
+ // This function is provided by test_suspendTimeouts.js.
+ // eslint-disable-next-line no-undef
+ resumeTimeouts(); // onmessage handlers should not run from this call.
+
+ resumeTimeouts_has_returned = true;
+
+ // When this JavaScript invocation returns to the main thread's event loop,
+ // only then should onmessage handlers be invoked.
+}
+
+// The buggy code calls this handler from the resumeTimeouts call, before the
+// main thread returns to the event loop. The correct code calls this only once
+// the JavaScript invocation that called resumeTimeouts has run to completion.
+function handle_echo({ data }) {
+ ok(
+ resumeTimeouts_has_returned,
+ "worker message delivered from main event loop"
+ );
+
+ // Finish the mochitest.
+ // This function is set and defined by test_suspendTimeouts.js
+ // eslint-disable-next-line no-undef
+ finish();
+}
diff --git a/devtools/server/tests/chrome/suspendTimeouts_worker.js b/devtools/server/tests/chrome/suspendTimeouts_worker.js
new file mode 100644
index 0000000000..e008f7d0d3
--- /dev/null
+++ b/devtools/server/tests/chrome/suspendTimeouts_worker.js
@@ -0,0 +1,12 @@
+"use strict";
+
+// Once content sends us a port connected to the mochitest, we simply echo every
+// message we receive back to content and the mochitest.
+onmessage = ({ data: { mochitestPort } }) => {
+ onmessage = ({ data }) => {
+ // Send a message to both content and the mochitest, which the main thread's
+ // event loop will attempt to deliver as step 2).
+ postMessage(`worker echo to content: ${data}`);
+ mochitestPort.postMessage(`worker echo to port: ${data}`);
+ };
+};
diff --git a/devtools/server/tests/chrome/test_Debugger.Script.prototype.global.html b/devtools/server/tests/chrome/test_Debugger.Script.prototype.global.html
new file mode 100644
index 0000000000..d403d6b4a3
--- /dev/null
+++ b/devtools/server/tests/chrome/test_Debugger.Script.prototype.global.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=958646
+
+Debugger.Script.prototype.global should return innerize globals, not WindowProxies.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Debugger.Script.prototype.global should return inner windows</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script>
+"use strict";
+
+const {addDebuggerToGlobal} = ChromeUtils.importESModule("resource://gre/modules/jsdebugger.sys.mjs");
+addDebuggerToGlobal(globalThis);
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ const iframe = document.createElement("iframe");
+ iframe.src = "data:text/html,<script>function glorp() { }<\/script>";
+ iframe.onload = firstOnLoadHandler;
+ document.body.appendChild(iframe);
+
+ function firstOnLoadHandler() {
+ const dbg = new Debugger();
+ const iframeDO = dbg.addDebuggee(iframe.contentWindow);
+
+ // For sanity: check that the debuggee global is the inner window,
+ // and that the outer window gets a distinct D.O.
+ const iframeWindowProxyDO = iframeDO.makeDebuggeeValue(iframe.contentWindow);
+ ok(iframeDO !== iframeWindowProxyDO);
+
+ // The real test: Debugger.Script.prototype.global returns inner windows.
+ ok(iframeDO.getOwnPropertyDescriptor("glorp").value.script.global === iframeDO);
+
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_Debugger.Source.prototype.elementAttribute.html b/devtools/server/tests/chrome/test_Debugger.Source.prototype.elementAttribute.html
new file mode 100644
index 0000000000..cb9c2bbcdc
--- /dev/null
+++ b/devtools/server/tests/chrome/test_Debugger.Source.prototype.elementAttribute.html
@@ -0,0 +1,159 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=941876
+
+Debugger.Source.prototype.element and .elementAttributeName should report the DOM
+element to which code is attached (if any), and how.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Debugger.Source.prototype.element should return owning element</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script>
+"use strict";
+
+const {addSandboxedDebuggerToGlobal} = ChromeUtils.importESModule("resource://gre/modules/jsdebugger.sys.mjs");
+addSandboxedDebuggerToGlobal(globalThis);
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ let log = "";
+ let doc, dieter, ulrich, isolde, albrecht;
+ let dbg, iframeDO;
+
+ // Create an iframe to debug.
+ // We can't use a data: URL here, because we want to test script elements
+ // that refer to the JavaScript via 'src' attributes, and data: documents
+ // can't refer to those. So we use a separate HTML document.
+ const iframe = document.createElement("iframe");
+ iframe.src = "Debugger.Source.prototype.element.html";
+ iframe.onload = onLoadHandler;
+ document.body.appendChild(iframe);
+
+ function onLoadHandler() {
+ log += "l";
+
+ // Now that the iframe's window has been created, we can add
+ // it as a debuggee.
+ dbg = new Debugger();
+ dbg.onDebuggerStatement = franzDebuggerHandler;
+ iframeDO = dbg.addDebuggee(iframe.contentWindow);
+ iframeDO.makeDebuggeeValue.bind(iframeDO);
+
+ // Send a click event to heidi.
+ doc = iframe.contentWindow.document;
+ doc.getElementById("heidi").dispatchEvent(new Event("click"));
+ }
+
+ function franzDebuggerHandler(frame) {
+ log += "f";
+
+ // The top stack frame should be franz, belonging to the script element.
+ ok(frame.callee.displayName === "franz", "top frame is franz");
+ ok(frame.script.source.elementAttributeName === undefined,
+ "top frame source doesn't belong to an attribute");
+
+ // The second stack frame should belong to heinrich.
+ ok(frame.older.script.source.elementAttributeName === undefined,
+ "second frame source doesn't belong to an attribute");
+
+ // The next stack frame should belong to heidi's onclick handler.
+ ok(frame.older.older.script.source.elementAttributeName === "onclick",
+ "third frame source belongs to 'onclick' attribute");
+
+ // Try a dynamically inserted inline script element.
+ ulrich = doc.createElement("script");
+ ulrich.text = "debugger;";
+ dbg.onDebuggerStatement = ulrichDebuggerHandler;
+ doc.body.appendChild(ulrich);
+ }
+
+ function ulrichDebuggerHandler(frame) {
+ log += "u";
+
+ // The top frame should be ulrich's text.
+ ok(frame.script.source.elementAttributeName === undefined,
+ "top frame is not on an attribute of ulrich");
+
+ // Try a dynamically inserted out-of-line script element.
+ isolde = doc.createElement("script");
+ isolde.setAttribute("src", "Debugger.Source.prototype.element-2.js");
+ isolde.setAttribute("id", "idolde, my dear");
+ dbg.onDebuggerStatement = isoldeDebuggerHandler;
+ doc.body.appendChild(isolde);
+ }
+
+ function isoldeDebuggerHandler(frame) {
+ log += "i";
+
+ ok(frame.script.source.elementAttributeName === undefined,
+ "top frame source is not an attribute of isolde");
+ info("frame.script.source.elementAttributeName is: " +
+ uneval(frame.script.source.elementAttributeName));
+
+ // Try a dynamically created div element with a handler.
+ dieter = doc.createElement("div");
+ dieter.setAttribute("id", "dieter");
+ dieter.setAttribute("ondrag", "debugger;");
+ dbg.onDebuggerStatement = dieterDebuggerHandler;
+ dieter.dispatchEvent(new Event("drag"));
+ }
+
+ function dieterDebuggerHandler(frame) {
+ log += "d";
+
+ // The top frame should belong to dieter's ondrag handler.
+ ok(frame.script.source.elementAttributeName === "ondrag",
+ "second event's handler is on dieter's 'ondrag' element");
+
+ // Try sending an 'onresize' event to the window.
+ //
+ // Note that we only want Debugger to see the events we send, not any
+ // genuine resize events accidentally generated by the test harness (see bug
+ // 1162067). So we mark our events as cancelable; that seems to be the only
+ // bit chrome can fiddle on an Event that content code will see and that
+ // won't affect propagation. Then, the content event only runs its
+ // 'debugger' statement when the event is cancelable. It's a kludge.
+ dbg.onDebuggerStatement = resizeDebuggerHandler;
+ iframe.contentWindow.dispatchEvent(new Event("resize", { cancelable: true }));
+ }
+
+ function resizeDebuggerHandler(frame) {
+ log += "e";
+
+ // The top frame should belong to the body's 'onresize' handler, even
+ // though we sent the message to the window and it was handled.
+ ok(frame.script.source.elementAttributeName === "onresize",
+ "onresize event handler is on body element's 'onresize' attribute");
+
+ // In SVG, the event and the attribute that holds that event's handler
+ // have different names. Debugger.Source.prototype.elementAttributeName
+ // should report (as one might infer) the attribute name, not the event
+ // name.
+ albrecht = doc.createElementNS("http://www.w3.org/2000/svg", "svg");
+ albrecht.setAttribute("onload", "debugger;");
+ dbg.onDebuggerStatement = SVGLoadHandler;
+ albrecht.dispatchEvent(new Event("SVGLoad"));
+ }
+
+ function SVGLoadHandler(frame) {
+ log += "s";
+
+ // The top frame's source should be on albrecht's 'onload' attribute.
+ ok(frame.script.source.elementAttributeName === "onload",
+ "SVGLoad event handler is on albrecht's 'onload' attribute");
+
+ ok(log === "lfuides", "all tests actually ran");
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_Debugger.Source.prototype.introductionScript.html b/devtools/server/tests/chrome/test_Debugger.Source.prototype.introductionScript.html
new file mode 100644
index 0000000000..09c23b5253
--- /dev/null
+++ b/devtools/server/tests/chrome/test_Debugger.Source.prototype.introductionScript.html
@@ -0,0 +1,96 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=969786
+
+Debugger.Source.prototype.introductionScript and .introductionOffset should
+behave when 'eval' is called with no scripted frames active at all.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Debugger.Source.prototype.introductionScript with no caller</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script>
+"use strict";
+
+const {addDebuggerToGlobal} = ChromeUtils.importESModule("resource://gre/modules/jsdebugger.sys.mjs");
+addDebuggerToGlobal(globalThis);
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ let dbg, iframeDO, doc;
+
+ // Create an iframe to debug.
+ const iframe = document.createElement("iframe");
+ iframe.src = "data:text/html,<div>Hi!</div>";
+ iframe.onload = onLoadHandler;
+ document.body.appendChild(iframe);
+
+ function onLoadHandler() {
+ // Now that the iframe's window has been created, we can add
+ // it as a debuggee.
+ dbg = new Debugger();
+ iframeDO = dbg.addDebuggee(iframe.contentWindow);
+
+ doc = iframe.contentWindow.document;
+ const script = doc.createElement("script");
+ script.text = "setTimeout(eval.bind(null, 'debugger;'), 0);";
+ dbg.onDebuggerStatement = timerHandler;
+ doc.body.appendChild(script);
+ }
+
+ function timerHandler(frame) {
+ // The top stack frame's source should have an undefined
+ // introduction script and introduction offset.
+ const source = frame.script.source;
+ ok(source.introductionScript === undefined,
+ "setTimeout eval introductionScript is undefined");
+ ok(source.introductionOffset === undefined,
+ "setTimeout eval introductionOffset is undefined");
+
+ // Check that the above isn't just some quirk of iframes, or the
+ // browser milieu destroying information: an eval script should indeed
+ // have proper introduction information.
+ const script2 = doc.createElement("script");
+ script2.text = "eval('debugger;');";
+ iframeDO.makeDebuggeeValue(script2);
+
+ dbg.onDebuggerStatement = evalHandler;
+ doc.body.appendChild(script2);
+ }
+
+ function evalHandler(frame) {
+ // The top stack frame's source should be introduced by the script that
+ // called eval.
+ const source = frame.script.source;
+ const frame2 = frame.older;
+ const frame3 = frame2.older;
+
+ ok(source.introductionType === "eval",
+ "top frame's source was introduced by 'eval'");
+ ok(source.introductionScript === frame2.script,
+ "eval frame's introduction script is the older frame's script");
+ ok(source.introductionOffset === frame2.offset,
+ "eval frame's introduction offset is current offset in older frame");
+
+ // The frame that called eval, in turn, was introduced at the call that
+ // inserted the script element into the document.
+ ok(frame2.script.source.introductionType === "injectedScript",
+ "older frame has no introduction type");
+ ok(frame2.script.source.introductionScript === frame3.script,
+ "older frame has introduction script");
+ ok(frame2.script.source.introductionOffset === frame3.offset,
+ "older frame has introduction offset");
+
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_Debugger.Source.prototype.introductionType.html b/devtools/server/tests/chrome/test_Debugger.Source.prototype.introductionType.html
new file mode 100644
index 0000000000..1057d4b94f
--- /dev/null
+++ b/devtools/server/tests/chrome/test_Debugger.Source.prototype.introductionType.html
@@ -0,0 +1,159 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=935203
+
+Debugger.Source.prototype.introductionType should return 'eventHandler' for
+JavaScrip appearing in an inline event handler attribute.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Debugger.Source.prototype.introductionType should identify event handlers</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="inspector-helpers.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script>
+"use strict";
+
+const {addSandboxedDebuggerToGlobal} = ChromeUtils.importESModule("resource://gre/modules/jsdebugger.sys.mjs");
+addSandboxedDebuggerToGlobal(globalThis);
+
+let dbg;
+let iframeDO, doc;
+let Tootles;
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ runNextTest();
+};
+
+addTest(function setup() {
+ // Create an iframe to debug.
+ const iframe = document.createElement("iframe");
+ iframe.srcdoc = "<div id='Tootles' onclick='debugger;'>I'm a DIV!</div>" +
+ "<script id='Auddie'>function auddie() { debugger; }<\/script>";
+ iframe.onload = onLoadHandler;
+ document.body.appendChild(iframe);
+
+ function onLoadHandler() {
+ // Now that the iframe's window has been created, we can add
+ // it as a debuggee.
+ dbg = new Debugger();
+ iframeDO = dbg.addDebuggee(iframe.contentWindow);
+ doc = iframe.contentWindow.document;
+ Tootles = doc.getElementById("Tootles");
+ iframeDO.makeDebuggeeValue(Tootles);
+
+ runNextTest();
+ }
+});
+
+// Check the introduction type of in-markup event handler code.
+// Send a click event to Tootles, whose handler has a 'debugger' statement,
+// and check that script's introduction type.
+addTest(function ClickOnTootles() {
+ dbg.onDebuggerStatement = TootlesClickDebugger;
+ Tootles.dispatchEvent(new Event("click"));
+
+ function TootlesClickDebugger(frame) {
+ // some sanity checks
+ is(frame.script.source.elementAttributeName, "onclick",
+ "top frame source belongs to 'onclick' attribute");
+
+ // And, the actual point of this test:
+ is(frame.script.source.introductionType, "eventHandler",
+ "top frame source's introductionType is 'eventHandler'");
+
+ runNextTest();
+ }
+});
+
+// Check the introduction type of dynamically added event handler code.
+// Add a drag event handler to Tootles as a string, and then send
+// Tootles a drag event.
+addTest(function DragTootles() {
+ dbg.onDebuggerStatement = TootlesDragDebugger;
+ Tootles.setAttribute("ondrag", "debugger;");
+ Tootles.dispatchEvent(new Event("drag"));
+
+ function TootlesDragDebugger(frame) {
+ // sanity checks
+ is(frame.script.source.elementAttributeName, "ondrag",
+ "top frame source belongs to 'ondrag' attribute");
+
+ // And, the actual point of this test:
+ is(frame.script.source.introductionType, "eventHandler",
+ "top frame source's introductionType is 'eventHandler'");
+
+ runNextTest();
+ }
+});
+
+// Check the introduction type of an in-markup script element.
+addTest(function checkAuddie() {
+ const fnDO = iframeDO.getOwnPropertyDescriptor("auddie").value;
+ iframeDO.makeDebuggeeValue(doc.getElementById("Auddie"));
+
+ is(fnDO.class, "Function",
+ "Script element 'Auddie' defined function 'auddie'.");
+ is(fnDO.script.source.elementAttributeName, undefined,
+ "Function auddie's script doesn't belong to any attribute of 'Auddie'");
+ is(fnDO.script.source.introductionType, "inlineScript",
+ "Function auddie's script's source was introduced by a script element");
+
+ runNextTest();
+});
+
+// Check the introduction type of a dynamically inserted script element.
+addTest(function InsertRover() {
+ dbg.onDebuggerStatement = RoverDebugger;
+ const rover = doc.createElement("script");
+ rover.text = "debugger;";
+ doc.body.appendChild(rover);
+ iframeDO.makeDebuggeeValue(rover);
+
+ function RoverDebugger(frame) {
+ // sanity checks
+ ok(frame.script.source.elementAttributeName === undefined,
+ "Rover script doesn't belong to an attribute of Rover");
+
+ // Check the introduction type.
+ ok(frame.script.source.introductionType === "injectedScript",
+ "Rover script's introduction type is 'injectedScript'");
+
+ runNextTest();
+ }
+});
+
+// Creates a chrome document with a XUL script element, and check its introduction type.
+addTest(function XULDocumentScript() {
+ const frame = document.createElement("iframe");
+ frame.src = "doc_Debugger.Source.prototype.introductionType.xhtml";
+ frame.onload = docLoaded;
+ info("Appending iframe containing a document with a XUL script tag");
+ document.body.appendChild(frame);
+
+ function docLoaded() {
+ info("Loaded chrome document");
+ const xulFrameDO = dbg.addDebuggee(frame.contentWindow);
+ const xulFnDO = xulFrameDO.getOwnPropertyDescriptor("xulScriptFunc").value;
+ is(typeof xulFnDO, "object", "XUL script element defined 'xulScriptFunc'");
+ is(xulFnDO.class, "Function",
+ "XUL global 'xulScriptFunc' is indeed a function");
+
+ // A XUL script elements' code gets shared amongst all
+ // instantiations of the document, so there's no specific DOM element
+ // we can attribute the code to.
+
+ is(xulFnDO.script.source.introductionType, "inlineScript",
+ "xulScriptFunc's introduction type is 'inlineScript'");
+ runNextTest();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_animation-type-longhand.html b/devtools/server/tests/chrome/test_animation-type-longhand.html
new file mode 100644
index 0000000000..97f5b1e469
--- /dev/null
+++ b/devtools/server/tests/chrome/test_animation-type-longhand.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>Test animation-type-longhand</title>
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+<body>
+<script>
+ "use strict";
+
+ // This test checks the content of animation type for longhands table that
+ // * every longhand property is included
+ // * nothing else is included
+ // * no property is mapped to more than one animation type
+ window.onload = function() {
+ const {require} = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs");
+ const { ANIMATION_TYPE_FOR_LONGHANDS } =
+ require("devtools/server/actors/animation-type-longhand");
+ const InspectorUtils = SpecialPowers.InspectorUtils;
+
+ const all_longhands = InspectorUtils.getCSSPropertyNames({
+ includeShorthands: false,
+ includeExperimentals: true,
+ });
+
+ const unseen_longhands = new Set(all_longhands);
+ const seen_longhands = new Set();
+ for (const [, names] of ANIMATION_TYPE_FOR_LONGHANDS) {
+ for (const name of names) {
+ ok(!seen_longhands.has(name),
+ `${name} should have only one animation type`);
+ ok(unseen_longhands.has(name),
+ `${name} is an unseen longhand property`);
+ unseen_longhands.delete(name);
+ seen_longhands.add(name);
+ }
+ }
+ is(unseen_longhands.size, 0,
+ "All longhands should be mapped to some animation type: " + [...unseen_longhands].join(", "));
+
+ SimpleTest.finish();
+ };
+</script>
+</body>
diff --git a/devtools/server/tests/chrome/test_css-logic-specificity.html b/devtools/server/tests/chrome/test_css-logic-specificity.html
new file mode 100644
index 0000000000..b5d5c76c0c
--- /dev/null
+++ b/devtools/server/tests/chrome/test_css-logic-specificity.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that css-logic calculates CSS specificity properly
+-->
+<meta charset="utf-8">
+<title>Test css-logic specificity</title>
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+<body style="background:blue;">
+<script>
+ "use strict";
+
+ window.onload = function() {
+ const {require} = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs");
+ const {CssLogic, CssSelector} = require("devtools/server/actors/inspector/css-logic");
+
+ const TEST_DATA = [
+ {text: "*", expected: 0},
+ {text: "LI", expected: 1},
+ {text: "UL LI", expected: 2},
+ {text: "UL OL + LI", expected: 3},
+ {text: "H1 + [REL=\"up\"]", expected: 1025},
+ {text: "UL OL LI.red", expected: 1027},
+ {text: "LI.red.level", expected: 2049},
+ {text: ".red .level", expected: 2048},
+ {text: "#x34y", expected: 1048576},
+ {text: "#s12:not(FOO)", expected: 1048577},
+ {text: "body#home div#warning p.message", expected: 2098179},
+ {text: "* body#home div#warning p.message", expected: 2098179},
+ {text: "#footer :not(nav) li", expected: 1048578},
+ {text: "bar:nth-child(n)", expected: 1025},
+ {text: "li::marker", expected: 2},
+ {text: "a:hover", expected: 1025},
+ ];
+
+ function createDocument() {
+ let text = TEST_DATA.map(i=>i.text).join(",");
+ text = '<style>' + text + " {color:red;}</style>";
+ document.body.innerHTML = text;
+ }
+
+ function getExpectedSpecificity(selectorText) {
+ return TEST_DATA.filter(i => i.text === selectorText)[0].expected;
+ }
+
+ SimpleTest.waitForExplicitFinish();
+
+ createDocument();
+ const cssLogic = new CssLogic();
+
+ cssLogic.highlight(document.body);
+
+ // There could be more stylesheets due to e.g, accessiblecaret, so find the
+ // right one.
+ info(`Sheets: ${cssLogic.sheets.map(s => s.href).join(", ")}`);
+
+ const cssSheet = cssLogic.sheets.find(s => s.href == location.href);
+ const cssRule = cssSheet.domSheet.cssRules[0];
+ const selectors = CssLogic.getSelectors(cssRule);
+
+ is(selectors.length, TEST_DATA.length, "Should be the right rule");
+
+ info("Iterating over the test selectors: " + selectors.join(", "));
+ for (let i = 0; i < selectors.length; i++) {
+ const selectorText = selectors[i];
+ info("Testing selector " + selectorText);
+
+ const selector = new CssSelector(cssRule, selectorText, i);
+ const expected = getExpectedSpecificity(selectorText);
+ const specificity = selector.cssRule.selectorSpecificityAt(selector.selectorIndex);
+ is(specificity, expected,
+ 'Selector "' + selectorText + '" has a specificity of ' + expected);
+ }
+
+ info("Testing specificity of element.style");
+ const colorProp = cssLogic.getPropertyInfo("background");
+ is(colorProp.matchedSelectors[0].specificity, 0x40000000,
+ "Element styles have specificity of 0x40000000 (1073741824).");
+
+ SimpleTest.finish();
+ };
+ </script>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_css-logic.html b/devtools/server/tests/chrome/test_css-logic.html
new file mode 100644
index 0000000000..6378f5a9e7
--- /dev/null
+++ b/devtools/server/tests/chrome/test_css-logic.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug </title>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript" src="inspector-helpers.js"></script>
+ <script type="application/javascript">
+"use strict";
+
+const {CssLogic} = require("devtools/server/actors/inspector/css-logic");
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ runNextTest();
+};
+
+addTest(function getComputedStyle() {
+ const node = document.querySelector("#computed-style");
+ is(CssLogic.getComputedStyle(node).getPropertyValue("width"),
+ "50px", "Computed style on a normal node works (width)");
+ is(CssLogic.getComputedStyle(node).getPropertyValue("height"),
+ "10px", "Computed style on a normal node works (height)");
+
+ const firstChild = new _documentWalker(node, window).firstChild();
+ is(CssLogic.getComputedStyle(firstChild).getPropertyValue("content"),
+ "\"before\"", "Computed style on a ::before node works (content)");
+ const lastChild = new _documentWalker(node, window).lastChild();
+ is(CssLogic.getComputedStyle(lastChild).getPropertyValue("content"),
+ "\"after\"", "Computed style on a ::after node works (content)");
+
+ runNextTest();
+});
+
+addTest(function getBindingElementAndPseudo() {
+ const node = document.querySelector("#computed-style");
+ let {bindingElement, pseudo} = CssLogic.getBindingElementAndPseudo(node);
+
+ is(bindingElement, node,
+ "Binding element is the node itself for a normal node");
+ ok(!pseudo, "Pseudo is null for a normal node");
+
+ const firstChild = new _documentWalker(node, window).firstChild();
+ ({ bindingElement, pseudo } = CssLogic.getBindingElementAndPseudo(firstChild));
+ is(bindingElement, node,
+ "Binding element is the parent for a pseudo node");
+ is(pseudo, "::before", "Pseudo is correct for a ::before node");
+
+ const lastChild = new _documentWalker(node, window).lastChild();
+ ({ bindingElement, pseudo } = CssLogic.getBindingElementAndPseudo(lastChild));
+ is(bindingElement, node,
+ "Binding element is the parent for a pseudo node");
+ is(pseudo, "::after", "Pseudo is correct for a ::after node");
+
+ runNextTest();
+});
+
+ </script>
+</head>
+<body>
+ <style type="text/css">
+ #computed-style { width: 50px; height: 10px; }
+ #computed-style::before { content: "before"; }
+ #computed-style::after { content: "after"; }
+ </style>
+ <div id="computed-style"></div>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_css-properties.html b/devtools/server/tests/chrome/test_css-properties.html
new file mode 100644
index 0000000000..3cbc3a4aa7
--- /dev/null
+++ b/devtools/server/tests/chrome/test_css-properties.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1265798 - Replace inIDOMUtils.cssPropertyIsShorthand
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test CSS Properties Actor</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript" src="inspector-helpers.js"></script>
+ <script type="application/javascript">
+"use strict";
+
+window.onload = function() {
+ function toSortedString(array) {
+ return JSON.stringify(array.sort());
+ }
+
+ const runCssPropertiesTests = async function(url) {
+ info(`Opening tab with CssPropertiesActor support.`);
+ // Open a new tab. The only property we are interested in is `target`.
+ const { target } = await attachURL(url);
+ const { cssProperties } = await target.getFront("cssProperties");
+
+ ok(cssProperties.isKnown("border"),
+ "The `border` shorthand property is known.");
+ ok(cssProperties.isKnown("display"),
+ "The `display` property is known.");
+ ok(!cssProperties.isKnown("foobar"),
+ "A fake property is not known.");
+ ok(cssProperties.isKnown("--foobar"),
+ "A CSS variable properly evaluates.");
+ ok(cssProperties.isKnown("--foob\\{ar"),
+ "A CSS variable with escaped character properly evaluates.");
+ ok(cssProperties.isKnown("--fübar"),
+ "A CSS variable unicode properly evaluates.");
+ ok(!cssProperties.isKnown("--foo bar"),
+ "A CSS variable with spaces fails");
+
+ is(toSortedString(cssProperties.getValues("margin")),
+ toSortedString(["auto", "inherit", "initial", "unset", "revert", "revert-layer"]),
+ "Can get values for the CSS margin.");
+ is(cssProperties.getValues("foobar").length, 0,
+ "Unknown values return an empty array.");
+
+ const bgColorValues = cssProperties.getValues("background-color");
+ ok(bgColorValues.includes("blanchedalmond"),
+ "A property with color values includes blanchedalmond.");
+ ok(bgColorValues.includes("papayawhip"),
+ "A property with color values includes papayawhip.");
+ ok(bgColorValues.includes("rgb"),
+ "A property with color values includes non-colors.");
+ };
+
+ addAsyncTest(async function setup() {
+ const url = document.getElementById("cssProperties").href;
+ await runCssPropertiesTests(url);
+
+ runNextTest();
+ });
+
+ SimpleTest.waitForExplicitFinish();
+ runNextTest();
+};
+ </script>
+</head>
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1265798">Mozilla Bug 1265798</a>
+ <a id="cssProperties" target="_blank" href="inspector_css-properties.html">Test Document</a>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_device.html b/devtools/server/tests/chrome/test_device.html
new file mode 100644
index 0000000000..117e50b5ca
--- /dev/null
+++ b/devtools/server/tests/chrome/test_device.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 895360 - [app manager] Device meta data actor
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Mozilla Bug</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script>
+"use strict";
+
+window.onload = function() {
+ const {require} = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs");
+ const {DevToolsClient} = require("devtools/client/devtools-client");
+ const {DevToolsServer} = require("devtools/server/devtools-server");
+
+ SimpleTest.waitForExplicitFinish();
+
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+
+ const client = new DevToolsClient(DevToolsServer.connectPipe());
+ client.connect().then(function onConnect() {
+ return client.mainRoot.getFront("device");
+ }).then(function(d) {
+ let desc;
+ const appInfo = Services.appinfo;
+ const utils = window.windowUtils;
+
+ const localDesc = {
+ appid: appInfo.ID,
+ vendor: appInfo.vendor,
+ name: appInfo.name,
+ version: appInfo.version,
+ appbuildid: appInfo.appBuildID,
+ platformbuildid: appInfo.platformBuildID,
+ platformversion: appInfo.platformVersion,
+ geckobuildid: appInfo.platformBuildID,
+ geckoversion: appInfo.platformVersion,
+ useragent: window.navigator.userAgent,
+ locale: Services.locale.appLocaleAsBCP47,
+ os: appInfo.OS,
+ processor: appInfo.XPCOMABI.split("-")[0],
+ compiler: appInfo.XPCOMABI.split("-")[1],
+ dpi: utils.displayDPI,
+ width: window.screen.width,
+ height: window.screen.height,
+ };
+
+ function checkValues() {
+ for (const key in localDesc) {
+ is(desc[key], localDesc[key], "valid field (" + key + ")");
+ }
+
+ const currProfD = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ const profileDir = currProfD.path;
+ ok(profileDir.includes(!!desc.profile.length && desc.profile),
+ "valid profile name");
+
+ client.close().then(() => {
+ DevToolsServer.destroy();
+ SimpleTest.finish();
+ });
+ }
+
+ d.getDescription().then(function(v) {
+ desc = v;
+ }).then(checkValues);
+ });
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_executeInGlobal-outerized_this.html b/devtools/server/tests/chrome/test_executeInGlobal-outerized_this.html
new file mode 100644
index 0000000000..6a846596b2
--- /dev/null
+++ b/devtools/server/tests/chrome/test_executeInGlobal-outerized_this.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=837060
+
+When we use Debugger.Object.prototype.executeInGlobal, the 'this' value seen
+by the evaluated code should be the WindowProxy, not the inner window
+object.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Mozilla Bug 837060</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script>
+"use strict";
+
+const {addDebuggerToGlobal} = ChromeUtils.importESModule("resource://gre/modules/jsdebugger.sys.mjs");
+addDebuggerToGlobal(globalThis);
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ const iframe = document.createElement("iframe");
+ iframe.src = "data:text/html,<script>var me = 'page 1';<\/script>";
+ iframe.onload = firstOnLoadHandler;
+ document.body.appendChild(iframe);
+
+ function firstOnLoadHandler() {
+ const dbg = new Debugger();
+ const page1DO = dbg.addDebuggee(iframe.contentWindow);
+ iframe.src = "data:text/html,<script>var me = 'page 2';<\/script>";
+ iframe.onload = function() {
+ const page2DO = dbg.addDebuggee(iframe.contentWindow);
+ ok(page1DO !== page2DO, "the two pages' globals get distinct D.O's");
+ ok(page1DO.unsafeDereference() === page2DO.unsafeDereference(),
+ "unwrapping page1DO and page2DO outerizes both, yielding the same outer window");
+
+ is(page1DO.executeInGlobal("me").return,
+ "page 1", "page1DO continues to refer to original page");
+ is(page2DO.executeInGlobal("me").return, "page 2",
+ "page2DO refers to current page");
+
+ is(page1DO.executeInGlobal("this === window").return, true,
+ "page 1: Debugger.Object.prototype.executeInGlobal should outerize 'this'");
+ is(page1DO.executeInGlobalWithBindings("this === window", {x: 2}).return, true,
+ "page 1: Debugger.Object.prototype.executeInGlobal should outerize 'this'");
+
+ is(page2DO.executeInGlobal("this === window").return, true,
+ "page 2: Debugger.Object.prototype.executeInGlobal should outerize 'this'");
+ is(page2DO.executeInGlobalWithBindings("this === window", {x: 2}).return, true,
+ "page 2: Debugger.Object.prototype.executeInGlobal should outerize 'this'");
+
+ // Debugger doesn't let one use outer windows as globals. You have to innerize.
+ const outerDO = page1DO.makeDebuggeeValue(page1DO.unsafeDereference());
+ ok(outerDO !== page1DO,
+ "outer window gets its own D.O, distinct from page 1's global");
+ ok(outerDO !== page2DO,
+ "outer window gets its own D.O, distinct from page 2's global");
+ SimpleTest.doesThrow(() => outerDO.executeInGlobal("me"),
+ "outer window D.Os can't be used as globals");
+
+ SimpleTest.finish();
+ };
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_highlighter_paused_debugger.html b/devtools/server/tests/chrome/test_highlighter_paused_debugger.html
new file mode 100644
index 0000000000..82dc939dd6
--- /dev/null
+++ b/devtools/server/tests/chrome/test_highlighter_paused_debugger.html
@@ -0,0 +1,88 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the PausedDebuggerOverlay highlighter.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>PausedDebuggerOverlay test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script>
+"use strict";
+
+window.onload = async function() {
+ SimpleTest.waitForExplicitFinish();
+
+ const {require} = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs");
+ require("devtools/server/actors/inspector/inspector");
+ const {HighlighterEnvironment} = require("devtools/server/actors/highlighters");
+ const {PausedDebuggerOverlay} = require("devtools/server/actors/highlighters/paused-debugger");
+
+ const env = new HighlighterEnvironment();
+ env.initFromWindow(window);
+
+ const highlighter = new PausedDebuggerOverlay(env);
+ await highlighter.isReady;
+ const anonymousContent = highlighter.markup.content;
+
+ const id = elementID => `${highlighter.ID_CLASS_PREFIX}${elementID}`;
+
+ function isHidden(elementID) {
+ const attr = anonymousContent.root.getElementById(id(elementID)).getAttribute("hidden");
+ return typeof attr === "string" && attr == "true";
+ }
+
+ function getReason() {
+ return anonymousContent.root.getElementById(id("reason")).textContent;
+ }
+
+ function isOverlayShown() {
+ const attr = anonymousContent.root.getElementById(id("root")).getAttribute("overlay");
+ return typeof attr === "string" && attr == "true";
+ }
+
+ info("Test that the various elements with IDs exist");
+ ok(highlighter.getElement("root"), "The root wrapper element exists");
+ ok(highlighter.getElement("toolbar"), "The toolbar element exists");
+ ok(highlighter.getElement("reason"), "The reason label element exists");
+
+ info("Test that the highlighter is hidden by default");
+ ok(isHidden("root"), "The highlighter is hidden");
+
+ info("Show the highlighter with overlay and toolbar");
+ let didShow = highlighter.show("breakpoint");
+ ok(didShow, "Calling show returned true");
+ ok(!isHidden("root"), "The highlighter is shown");
+ ok(isOverlayShown(), "The overlay is shown");
+ is(
+ getReason(),
+ "Paused on breakpoint",
+ "The reason displayed in the toolbar is correct"
+ );
+
+ info("Call show again with another reason");
+ didShow = highlighter.show("debuggerStatement");
+ ok(didShow, "Calling show returned true too");
+ ok(!isHidden("root"), "The highlighter is still shown");
+ is(getReason(), "Paused on debugger statement",
+ "The reason displayed in the toolbar is correct again");
+ ok(isOverlayShown(), "The overlay is still shown too");
+
+ info("Call show again but with no reason");
+ highlighter.show();
+ ok(isOverlayShown(), "The overlay is shown however");
+
+ info("Hide the highlighter");
+ highlighter.hide();
+ ok(isHidden("root"), "The highlighter is now hidden");
+
+ SimpleTest.finish();
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_inspector-changeattrs.html b/devtools/server/tests/chrome/test_inspector-changeattrs.html
new file mode 100644
index 0000000000..94c4c3dc1b
--- /dev/null
+++ b/devtools/server/tests/chrome/test_inspector-changeattrs.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug </title>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript" src="inspector-helpers.js"></script>
+ <script type="application/javascript">
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ runNextTest();
+};
+
+let gInspectee = null;
+let gWalker = null;
+
+addTest(async function setup() {
+ const url = document.getElementById("inspectorContent").href;
+ const { target, doc } = await attachURL(url);
+ gInspectee = doc;
+ const inspector = await target.getFront("inspector");
+ gWalker = inspector.walker;
+ runNextTest();
+});
+
+addTest(function testChangeAttrs() {
+ const attrNode = gInspectee.querySelector("#a");
+ let attrFront;
+ promiseDone(gWalker.querySelector(gWalker.rootNode, "#a").then(front => {
+ attrFront = front;
+ dump("attrFront is: " + attrFront + "\n");
+ // Add a few attributes.
+ const list = attrFront.startModifyingAttributes();
+ list.setAttribute("data-newattr", "newvalue");
+ list.setAttribute("data-newattr2", "newvalue");
+ return list.apply();
+ }).then(() => {
+ // We're only going to test that the change hit the document.
+ // There are other tests that make sure changes are propagated
+ // to the client.
+ is(attrNode.getAttribute("data-newattr"), "newvalue",
+ "Node should have the first new attribute");
+ is(attrNode.getAttribute("data-newattr2"), "newvalue",
+ "Node should have the second new attribute.");
+ }).then(() => {
+ // Change an attribute.
+ const list = attrFront.startModifyingAttributes();
+ list.setAttribute("data-newattr", "changedvalue");
+ return list.apply();
+ }).then(() => {
+ is(attrNode.getAttribute("data-newattr"), "changedvalue",
+ "Node should have the changed first value.");
+ is(attrNode.getAttribute("data-newattr2"), "newvalue",
+ "Second value should remain unchanged.");
+ }).then(() => {
+ const list = attrFront.startModifyingAttributes();
+ list.removeAttribute("data-newattr2");
+ return list.apply();
+ }).then(() => {
+ is(attrNode.getAttribute("data-newattr"), "changedvalue",
+ "Node should have the changed first value.");
+ ok(!attrNode.hasAttribute("data-newattr2"), "Second value should be removed.");
+ }).then(runNextTest));
+});
+
+addTest(function cleanup() {
+ gWalker = null;
+ gInspectee = null;
+ runNextTest();
+});
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a>
+<a id="inspectorContent" target="_blank" href="inspector-traversal-data.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_inspector-changevalue.html b/devtools/server/tests/chrome/test_inspector-changevalue.html
new file mode 100644
index 0000000000..f5aee52881
--- /dev/null
+++ b/devtools/server/tests/chrome/test_inspector-changevalue.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug </title>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript" src="inspector-helpers.js"></script>
+ <script type="application/javascript">
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ runNextTest();
+};
+
+let gInspectee = null;
+let gWalker = null;
+
+addTest(async function setup() {
+ const url = document.getElementById("inspectorContent").href;
+ const { target, doc } = await attachURL(url);
+ gInspectee = doc;
+ const inspector = await target.getFront("inspector");
+ gWalker = inspector.walker;
+ runNextTest();
+});
+
+addTest(function testChangeValue() {
+ const contentNode = gInspectee.querySelector("#a").firstChild;
+ let nodeFront;
+ promiseDone(gWalker.querySelector(gWalker.rootNode, "#a").then(front => {
+ // Get the text child
+ return gWalker.children(front, { maxNodes: 1 });
+ }).then(children => {
+ nodeFront = children.nodes[0];
+ is(nodeFront.nodeType, Node.TEXT_NODE);
+ return nodeFront.setNodeValue("newvalue");
+ }).then(() => {
+ // We're only going to test that the change hit the document.
+ // There are other tests that make sure changes are propagated
+ // to the client.
+ is(contentNode.nodeValue, "newvalue", "Node should have a new value.");
+ }).then(runNextTest));
+});
+
+addTest(function cleanup() {
+ gWalker = null;
+ gInspectee = null;
+ runNextTest();
+});
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a>
+<a id="inspectorContent" target="_blank" href="inspector-traversal-data.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_inspector-display-type.html b/devtools/server/tests/chrome/test_inspector-display-type.html
new file mode 100644
index 0000000000..a8bbedc22a
--- /dev/null
+++ b/devtools/server/tests/chrome/test_inspector-display-type.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1431900
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1431900</title>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript" src="inspector-helpers.js"></script>
+ <script type="application/javascript">
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ runNextTest();
+};
+
+var gWalker;
+
+addTest(async function setup() {
+ const url = document.getElementById("inspectorContent").href;
+ const { target } = await attachURL(url);
+ const inspector = await target.getFront("inspector");
+ gWalker = inspector.walker;
+ runNextTest();
+});
+
+addAsyncTest(async function testInlineBlockDisplayType() {
+ info("Test getting the display type of an inline block element.");
+ const node = await gWalker.querySelector(gWalker.rootNode, "#inline-block");
+ const displayType = node.displayType;
+ is(displayType, "inline-block", "The node has a display type of 'inline-block'.");
+ runNextTest();
+});
+
+addAsyncTest(async function testInlineTextChildDisplayType() {
+ info("Test getting the display type of an inline text child.");
+ const node = await gWalker.querySelector(gWalker.rootNode, "#inline-block");
+ const children = await gWalker.children(node);
+ const inlineTextChild = children.nodes[0];
+ const displayType = inlineTextChild.displayType;
+ ok(!displayType, "No display type for inline text child.");
+ runNextTest();
+});
+
+addAsyncTest(async function testGridDisplayType() {
+ info("Test getting the display type of an grid container.");
+ const node = await gWalker.querySelector(gWalker.rootNode, "#grid");
+ const displayType = node.displayType;
+ is(displayType, "grid", "The node has a display type of 'grid'.");
+ runNextTest();
+});
+
+addAsyncTest(async function testBlockDisplayType() {
+ info("Test getting the display type of a block element.");
+ const node = await gWalker.querySelector(gWalker.rootNode, "#block");
+ const displayType = await node.displayType;
+ is(displayType, "block", "The node has a display type of 'block'.");
+ runNextTest();
+});
+
+addTest(function() {
+ gWalker = null;
+ runNextTest();
+});
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1431900">Mozilla Bug 1431900</a>
+<a id="inspectorContent" target="_blank" href="inspector_display-type.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_inspector-duplicate-node.html b/devtools/server/tests/chrome/test_inspector-duplicate-node.html
new file mode 100644
index 0000000000..205e11629e
--- /dev/null
+++ b/devtools/server/tests/chrome/test_inspector-duplicate-node.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1208864
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1208864</title>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript" src="inspector-helpers.js"></script>
+ <script type="application/javascript">
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ runNextTest();
+};
+
+let gWalker = null;
+
+addTest(async function setup() {
+ const url = document.getElementById("inspectorContent").href;
+ const { target } = await attachURL(url);
+ const inspector = await target.getFront("inspector");
+ gWalker = inspector.walker;
+ runNextTest();
+});
+
+addTest(async function testDuplicateNode() {
+ const className = ".node-to-duplicate";
+ let matches = await gWalker.querySelectorAll(gWalker.rootNode, className);
+ is(matches.length, 1, "There should initially be one node to duplicate.");
+
+ const nodeFront = await gWalker.querySelector(gWalker.rootNode, className);
+ await gWalker.duplicateNode(nodeFront);
+
+ matches = await gWalker.querySelectorAll(gWalker.rootNode, className);
+ is(matches.length, 2, "The node should now be duplicated.");
+
+ runNextTest();
+});
+
+addTest(function cleanup() {
+ gWalker = null;
+ runNextTest();
+});
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1208864">Mozilla Bug 1208864</a>
+<a id="inspectorContent" target="_blank" href="inspector-traversal-data.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_inspector-hide.html b/devtools/server/tests/chrome/test_inspector-hide.html
new file mode 100644
index 0000000000..e699400ee0
--- /dev/null
+++ b/devtools/server/tests/chrome/test_inspector-hide.html
@@ -0,0 +1,71 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug </title>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript" src="inspector-helpers.js"></script>
+ <script type="application/javascript">
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ runNextTest();
+};
+
+let gWalker = null;
+let gInspectee = null;
+
+addTest(async function setup() {
+ const url = document.getElementById("inspectorContent").href;
+ const { target, doc } = await attachURL(url);
+ const inspector = await target.getFront("inspector");
+ gInspectee = doc;
+ gWalker = inspector.walker;
+ runNextTest();
+});
+
+addTest(function testRearrange() {
+ let listFront = null;
+ const listNode = gInspectee.querySelector("#longlist");
+
+ promiseDone(gWalker.querySelector(gWalker.rootNode, "#longlist").then(front => {
+ listFront = front;
+ }).then(() => {
+ const computed = gInspectee.defaultView.getComputedStyle(listNode);
+ is(computed.visibility, "visible", "Node should be visible to start with");
+ return gWalker.hideNode(listFront);
+ }).then(response => {
+ const computed = gInspectee.defaultView.getComputedStyle(listNode);
+ is(computed.visibility, "hidden", "Node should be hidden");
+ return gWalker.unhideNode(listFront);
+ }).then(() => {
+ const computed = gInspectee.defaultView.getComputedStyle(listNode);
+ is(computed.visibility, "visible", "Node should be visible again.");
+ }).then(runNextTest));
+});
+
+addTest(function cleanup() {
+ gWalker = null;
+ gInspectee = null;
+ runNextTest();
+});
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a>
+<a id="inspectorContent" target="_blank" href="inspector-traversal-data.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_inspector-inactive-property-helper.html b/devtools/server/tests/chrome/test_inspector-inactive-property-helper.html
new file mode 100644
index 0000000000..86c783c035
--- /dev/null
+++ b/devtools/server/tests/chrome/test_inspector-inactive-property-helper.html
@@ -0,0 +1,124 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Test for InactivePropertyHelper</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+"use strict";
+SimpleTest.waitForExplicitFinish();
+
+(async function() {
+ const { require } = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs");
+ const { isPropertyUsed } = require("devtools/server/actors/utils/inactive-property-helper");
+
+ const INACTIVE_CSS_PREF = "devtools.inspector.inactive.css.enabled";
+ const CUSTOM_HIGHLIGHT_API = "dom.customHighlightAPI.enabled";
+ const TEXT_WRAP_BALANCE = "layout.css.text-wrap-balance.enabled";
+
+ Services.prefs.setBoolPref(INACTIVE_CSS_PREF, true);
+ Services.prefs.setBoolPref(CUSTOM_HIGHLIGHT_API, true);
+ Services.prefs.setBoolPref(TEXT_WRAP_BALANCE, true);
+
+ SimpleTest.registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(INACTIVE_CSS_PREF);
+ Services.prefs.clearUserPref(CUSTOM_HIGHLIGHT_API);
+ Services.prefs.clearUserPref(TEXT_WRAP_BALANCE);
+ });
+
+ const FOLDER = "./inactive-property-helper";
+
+ // Each file should `export default` an array of objects, representing each test case.
+ // A single test case is an object of the following shape:
+ // - {String} info: a summary of the test case
+ // - {String} property: the CSS property that should be tested
+ // - {String|undefined} tagName: the tagName of the element we're going to test.
+ // Optional only if there's a createTestElement property.
+ // - {Function|undefined} createTestElement: A function that takes a node as a parameter
+ // where elements used for the test case will
+ // be appended. The function should return the
+ // element that will be passed to
+ // isPropertyUsed.
+ // Optional only if there's a tagName property
+ // - {Array<String>} rules: An array of the rules that will be applied on the element.
+ // This can't be empty as isPropertyUsed need a rule.
+ // - {Integer|undefined} ruleIndex: If there are multiples rules in `rules`, the index
+ // of the one that should be tested in isPropertyUsed.
+ // - {Boolean} isActive: should the property be active (isPropertyUsed `used` result).
+ const testFiles = [
+ "align-content.mjs",
+ "border-image.mjs",
+ "cue-pseudo-element.mjs",
+ "first-letter-pseudo-element.mjs",
+ "first-line-pseudo-element.mjs",
+ "flex-grid-item-properties.mjs",
+ "float.mjs",
+ "gap.mjs",
+ "grid-container-properties.mjs",
+ "grid-with-absolute-properties.mjs",
+ "multicol-container-properties.mjs",
+ "highlight-pseudo-elements.mjs",
+ "margin-padding.mjs",
+ "max-min-width-height.mjs",
+ "place-items-content.mjs",
+ "placeholder-pseudo-element.mjs",
+ "positioned.mjs",
+ "scroll-padding.mjs",
+ "vertical-align.mjs",
+ "table.mjs",
+ "table-cell.mjs",
+ "text-overflow.mjs",
+ "text-wrap.mjs",
+ "width-height-ruby.mjs",
+ ].map(file => `${FOLDER}/${file}`);
+
+ // Import all the test cases
+ const tests =
+ (await Promise.all(testFiles.map(f => import(f).then(data => data.default)))).flat();
+
+ for (const {
+ info: summary,
+ property,
+ tagName,
+ createTestElement,
+ rules,
+ ruleIndex,
+ isActive,
+ expectedMsgId,
+ } of tests) {
+ // Create an element which will contain the test elements.
+ const main = document.createElement("main");
+ document.firstElementChild.appendChild(main);
+
+ // Apply the CSS rules to the document.
+ const style = document.createElement("style");
+ main.append(style);
+ for (const dataRule of rules) {
+ style.sheet.insertRule(dataRule);
+ }
+ const rule = style.sheet.cssRules[ruleIndex || 0];
+
+ // Create the test elements
+ let el;
+ if (createTestElement) {
+ el = createTestElement(main);
+ } else {
+ el = document.createElement(tagName);
+ main.append(el);
+ }
+
+ const { used, msgId } = isPropertyUsed(el, getComputedStyle(el), rule, property);
+ ok(used === isActive, summary);
+ if (expectedMsgId) {
+ is(msgId, expectedMsgId, `${summary} - returned expected msgId`);
+ }
+
+ main.remove();
+ }
+ SimpleTest.finish();
+})();
+ </script>
+ </head>
+ <body></body>
+</html>
diff --git a/devtools/server/tests/chrome/test_inspector-mutations-attr.html b/devtools/server/tests/chrome/test_inspector-mutations-attr.html
new file mode 100644
index 0000000000..9430db65bd
--- /dev/null
+++ b/devtools/server/tests/chrome/test_inspector-mutations-attr.html
@@ -0,0 +1,169 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug </title>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript" src="inspector-helpers.js"></script>
+ <script type="application/javascript">
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ runNextTest();
+};
+
+let gInspectee = null;
+let gWalker = null;
+let attrNode;
+let attrFront;
+
+addTest(async function setup() {
+ const url = document.getElementById("inspectorContent").href;
+ const { target, doc } = await attachURL(url);
+ const inspector = await target.getFront("inspector");
+ gInspectee = doc;
+ gWalker = inspector.walker;
+ runNextTest();
+});
+
+addTest(setupAttrTest);
+addTest(testAddAttribute);
+addTest(testChangeAttribute);
+addTest(testRemoveAttribute);
+addTest(testQueuedMutations);
+addTest(setupFrameAttrTest);
+addTest(testAddAttribute);
+addTest(testChangeAttribute);
+addTest(testRemoveAttribute);
+addTest(testQueuedMutations);
+
+function setupAttrTest() {
+ attrNode = gInspectee.querySelector("#a");
+ promiseDone(gWalker.querySelector(gWalker.rootNode, "#a").then(node => {
+ attrFront = node;
+ }).then(runNextTest));
+}
+
+function setupFrameAttrTest() {
+ const frame = gInspectee.querySelector("#childFrame");
+ attrNode = frame.contentDocument.querySelector("#a");
+
+ promiseDone(gWalker.querySelector(gWalker.rootNode, "#childFrame").then(childFrame => {
+ return childFrame.walkerFront.children(childFrame);
+ }).then(children => {
+ const nodes = children.nodes;
+ is(nodes.length, 1, "There should be only one child of the iframe");
+ const [iframeNode] = nodes;
+ is(iframeNode.nodeType, Node.DOCUMENT_NODE, "iframe child should be a document node");
+ return iframeNode.walkerFront.querySelector(iframeNode, "#a");
+ }).then(node => {
+ attrFront = node;
+ }).then(runNextTest));
+}
+
+function testAddAttribute() {
+ attrNode.setAttribute("data-newattr", "newvalue");
+ attrNode.setAttribute("data-newattr2", "newvalue");
+ attrFront.walkerFront.once("mutations", () => {
+ is(attrFront.attributes.length, 3, "Should have id and two new attributes.");
+ is(attrFront.getAttribute("data-newattr"), "newvalue",
+ "Node front should have the first new attribute");
+ is(attrFront.getAttribute("data-newattr2"), "newvalue",
+ "Node front should have the second new attribute.");
+ runNextTest();
+ });
+}
+
+function testChangeAttribute() {
+ attrNode.setAttribute("data-newattr", "changedvalue1");
+ attrNode.setAttribute("data-newattr", "changedvalue2");
+ attrNode.setAttribute("data-newattr", "changedvalue3");
+ attrFront.walkerFront.once("mutations", mutations => {
+ is(mutations.length, 1,
+ "Only one mutation is sent for multiple queued attribute changes");
+ is(attrFront.attributes.length, 3, "Should have id and two new attributes.");
+ is(attrFront.getAttribute("data-newattr"), "changedvalue3",
+ "Node front should have the changed first value");
+ is(attrFront.getAttribute("data-newattr2"), "newvalue",
+ "Second value should remain unchanged.");
+ runNextTest();
+ });
+}
+
+function testRemoveAttribute() {
+ attrNode.removeAttribute("data-newattr2");
+ attrFront.walkerFront.once("mutations", () => {
+ is(attrFront.attributes.length, 2, "Should have id and one remaining attribute.");
+ is(attrFront.getAttribute("data-newattr"), "changedvalue3",
+ "Node front should still have the first value");
+ ok(!attrFront.hasAttribute("data-newattr2"), "Second value should be removed.");
+ runNextTest();
+ });
+}
+
+function testQueuedMutations() {
+ // All modifications to each attribute should be queued in one mutation event.
+
+ attrNode.removeAttribute("data-newattr");
+ attrNode.setAttribute("data-newattr", "1");
+ attrNode.removeAttribute("data-newattr");
+ attrNode.setAttribute("data-newattr", "2");
+ attrNode.removeAttribute("data-newattr");
+
+ for (let i = 0; i <= 1000; i++) {
+ attrNode.setAttribute("data-newattr2", i);
+ }
+
+ attrNode.removeAttribute("data-newattr3");
+ attrNode.setAttribute("data-newattr3", "1");
+ attrNode.removeAttribute("data-newattr3");
+ attrNode.setAttribute("data-newattr3", "2");
+ attrNode.removeAttribute("data-newattr3");
+ attrNode.setAttribute("data-newattr3", "3");
+
+ // This shouldn't be added in the attribute set, since it's a new
+ // attribute that's been added and removed.
+ attrNode.setAttribute("data-newattr4", "4");
+ attrNode.removeAttribute("data-newattr4");
+
+ attrFront.walkerFront.once("mutations", mutations => {
+ is(mutations.length, 4,
+ "Only one mutation each is sent for multiple queued attribute changes");
+ is(attrFront.attributes.length, 3,
+ "Should have id, data-newattr2, and data-newattr3.");
+
+ is(attrFront.getAttribute("data-newattr2"), "1000",
+ "Node front should still have the correct value");
+ is(attrFront.getAttribute("data-newattr3"), "3",
+ "Node front should still have the correct value");
+ ok(!attrFront.hasAttribute("data-newattr"), "Attribute value should be removed.");
+ ok(!attrFront.hasAttribute("data-newattr4"), "Attribute value should be removed.");
+
+ runNextTest();
+ });
+}
+
+addTest(function cleanup() {
+ gInspectee = null;
+ gWalker = null;
+ runNextTest();
+});
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a>
+<a id="inspectorContent" target="_blank" href="inspector-traversal-data.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_inspector-mutations-events.html b/devtools/server/tests/chrome/test_inspector-mutations-events.html
new file mode 100644
index 0000000000..b48952c4d9
--- /dev/null
+++ b/devtools/server/tests/chrome/test_inspector-mutations-events.html
@@ -0,0 +1,187 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1157469
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1157469</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript" src="inspector-helpers.js"></script>
+ <script type="application/javascript">
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ const prevPrefValue = Services.prefs.getBoolPref("devtools.chrome.enabled");
+ Services.prefs.setBoolPref("devtools.chrome.enabled", true);
+
+ let inspectee = null;
+ let inspector = null;
+ let walker = null;
+ const eventListener1 = function() {};
+ const eventListener2 = function() {};
+ let eventNode1;
+ let eventNode2;
+ let eventFront1;
+ let eventFront2;
+
+ addAsyncTest(async function setup() {
+ info("Setting up inspector and walker actors.");
+ const url = document.getElementById("inspectorContent").href;
+ const { target, doc } = await attachURL(url);
+ inspectee = doc;
+ inspector = await target.getFront("inspector");
+ walker = inspector.walker;
+
+ runNextTest();
+ });
+
+ addAsyncTest(async function setupEventTest() {
+ eventNode1 = inspectee.querySelector("#a");
+ eventNode2 = inspectee.querySelector("#b");
+
+ eventFront1 = await walker.querySelector(walker.rootNode, "#a");
+ eventFront2 = await walker.querySelector(walker.rootNode, "#b");
+
+ runNextTest();
+ });
+
+ addAsyncTest(async function testChangeEventListenerOnSingleNode() {
+ checkNodesHaveNoEventListener();
+
+ info("add event listener on a single node");
+ eventNode1.addEventListener("click", eventListener1);
+
+ let mutations = await waitForMutations();
+ is(mutations.length, 1, "one mutation expected");
+ is(mutations[0].target, eventFront1, "mutation targets eventFront1");
+ is(mutations[0].type, "events", "mutation type is events");
+ is(mutations[0].hasEventListeners, true,
+ "mutation target should have event listeners");
+ is(eventFront1.hasEventListeners, true, "eventFront1 should have event listeners");
+
+ info("remove event listener on a single node");
+ eventNode1.removeEventListener("click", eventListener1);
+
+ mutations = await waitForMutations();
+ is(mutations.length, 1, "one mutation expected");
+ is(mutations[0].target, eventFront1, "mutation targets eventFront1");
+ is(mutations[0].type, "events", "mutation type is events");
+ is(mutations[0].hasEventListeners, false,
+ "mutation target should have no event listeners");
+ is(eventFront1.hasEventListeners, false,
+ "eventFront1 should have no event listeners");
+
+ info("perform several event listener changes on a single node");
+ eventNode1.addEventListener("click", eventListener1);
+ eventNode1.addEventListener("click", eventListener2);
+ eventNode1.removeEventListener("click", eventListener1);
+ eventNode1.removeEventListener("click", eventListener2);
+
+ mutations = await waitForMutations();
+ is(mutations.length, 1, "one mutation expected");
+ is(mutations[0].target, eventFront1, "mutation targets eventFront1");
+ is(mutations[0].type, "events", "mutation type is events");
+ is(mutations[0].hasEventListeners, false,
+ "no event listener expected on mutation target");
+ is(eventFront1.hasEventListeners, false, "no event listener expected on node");
+
+ runNextTest();
+ });
+
+ addAsyncTest(async function testChangeEventsOnSeveralNodes() {
+ checkNodesHaveNoEventListener();
+
+ info("add event listeners on both nodes");
+ eventNode1.addEventListener("click", eventListener1);
+ eventNode2.addEventListener("click", eventListener2);
+
+ let mutations = await waitForMutations();
+ is(mutations.length, 2, "two mutations expected, one for each modified node");
+ // first mutation
+ is(mutations[0].target, eventFront1, "first mutation targets eventFront1");
+ is(mutations[0].type, "events", "mutation type is events");
+ is(mutations[0].hasEventListeners, true,
+ "mutation target should have event listeners");
+ is(eventFront1.hasEventListeners, true, "eventFront1 should have event listeners");
+ // second mutation
+ is(mutations[1].target, eventFront2, "second mutation targets eventFront2");
+ is(mutations[1].type, "events", "mutation type is events");
+ is(mutations[1].hasEventListeners, true,
+ "mutation target should have event listeners");
+ is(eventFront2.hasEventListeners, true, "eventFront1 should have event listeners");
+
+ info("remove event listeners on both nodes");
+ eventNode1.removeEventListener("click", eventListener1);
+ eventNode2.removeEventListener("click", eventListener2);
+
+ mutations = await waitForMutations();
+ is(mutations.length, 2, "one mutation registered for event listener change");
+ // first mutation
+ is(mutations[0].target, eventFront1, "first mutation targets eventFront1");
+ is(mutations[0].type, "events", "mutation type is events");
+ is(mutations[0].hasEventListeners, false,
+ "mutation target should have no event listeners");
+ is(eventFront1.hasEventListeners, false,
+ "eventFront2 should have no event listeners");
+ // second mutation
+ is(mutations[1].target, eventFront2, "second mutation targets eventFront2");
+ is(mutations[1].type, "events", "mutation type is events");
+ is(mutations[1].hasEventListeners, false,
+ "mutation target should have no event listeners");
+ is(eventFront2.hasEventListeners, false,
+ "eventFront2 should have no event listeners");
+
+ runNextTest();
+ });
+
+ addAsyncTest(async function testRemoveMissingEvent() {
+ checkNodesHaveNoEventListener();
+
+ info("try to remove an event listener not previously added");
+ eventNode1.removeEventListener("click", eventListener1);
+
+ info("set any attribute on the node to trigger a mutation");
+ eventNode1.setAttribute("data-attr", "somevalue");
+
+ const mutations = await waitForMutations();
+ is(mutations.length, 1, "expect only one mutation");
+ isnot(mutations.type, "events", "mutation type should not be events");
+
+ Services.prefs.setBoolPref("devtools.chrome.enabled", prevPrefValue);
+ runNextTest();
+ });
+
+ function checkNodesHaveNoEventListener() {
+ is(eventFront1.hasEventListeners, false,
+ "eventFront1 hasEventListeners should be false");
+ is(eventFront2.hasEventListeners, false,
+ "eventFront2 hasEventListeners should be false");
+ }
+
+ function waitForMutations() {
+ return new Promise(resolve => {
+ walker.once("mutations", mutations => {
+ resolve(mutations);
+ });
+ });
+ }
+
+ runNextTest();
+};
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1157469">Mozilla Bug 1157469</a>
+<a id="inspectorContent" target="_blank" href="inspector-traversal-data.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_inspector-mutations-value.html b/devtools/server/tests/chrome/test_inspector-mutations-value.html
new file mode 100644
index 0000000000..14e93b9d1c
--- /dev/null
+++ b/devtools/server/tests/chrome/test_inspector-mutations-value.html
@@ -0,0 +1,163 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug </title>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript" src="inspector-helpers.js"></script>
+ <script type="application/javascript">
+"use strict";
+
+const WalkerActor = require("devtools/server/actors/inspector/walker");
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ runNextTest();
+};
+
+const testSummaryLength = 10;
+WalkerActor.setValueSummaryLength(testSummaryLength);
+SimpleTest.registerCleanupFunction(function() {
+ WalkerActor.setValueSummaryLength(WalkerActor.DEFAULT_VALUE_SUMMARY_LENGTH);
+});
+
+let gInspectee = null;
+let gWalker = null;
+let valueNode;
+var valueFront;
+var longStringFront;
+var longString = "stringstringstringstringstringstringstringstringstringstringstring";
+var shortString = "str";
+var shortString2 = "str2";
+
+addTest(async function setup() {
+ const url = document.getElementById("inspectorContent").href;
+ const { target, doc } = await attachURL(url);
+ const inspector = await target.getFront("inspector");
+ gInspectee = doc;
+ gWalker = inspector.walker;
+ runNextTest();
+});
+
+addTest(setupValueTest);
+addTest(testKeepLongValue);
+addTest(testSetShortValue);
+addTest(testKeepShortValue);
+addTest(testSetLongValue);
+addTest(setupFrameValueTest);
+addTest(testKeepLongValue);
+addTest(testSetShortValue);
+addTest(testKeepShortValue);
+addTest(testSetLongValue);
+
+function setupValueTest() {
+ valueNode = gInspectee.querySelector("#longstring").firstChild;
+ promiseDone(gWalker.querySelector(gWalker.rootNode, "#longstring").then(node => {
+ longStringFront = node;
+ return gWalker.children(node);
+ }).then(children => {
+ valueFront = children.nodes[0];
+ }).then(runNextTest));
+}
+
+function setupFrameValueTest() {
+ const frame = gInspectee.querySelector("#childFrame");
+ valueNode = frame.contentDocument.querySelector("#longstring").firstChild;
+
+ promiseDone(gWalker.querySelector(gWalker.rootNode, "#childFrame").then(childFrame => {
+ return gWalker.children(childFrame);
+ }).then(children => {
+ const nodes = children.nodes;
+ is(nodes.length, 1, "There should be only one child of the iframe");
+ const [node] =nodes;
+ is(node.nodeType, Node.DOCUMENT_NODE, "iframe child should be a document node");
+ return node.walkerFront.querySelector(node, "#longstring");
+ }).then(node => {
+ longStringFront = node;
+ return longStringFront.walkerFront.children(node);
+ }).then(children => {
+ valueFront = children.nodes[0];
+ }).then(runNextTest));
+}
+
+function checkNodeFrontValue(front, expectedValue) {
+ return front.getNodeValue().then(longstring => {
+ return longstring.string();
+ }).then(str => {
+ is(str, expectedValue, "Node value is as expected");
+ });
+}
+
+function testKeepLongValue() {
+ // After first setup we should have a long string in the node
+ ok(!longStringFront.inlineTextChild, "Text node is too long to be inlined.");
+
+ valueNode.nodeValue = longString;
+ valueFront.walkerFront.once("mutations", (changes) => {
+ ok(!longStringFront.inlineTextChild, "Text node is too long to be inlined.");
+ ok(!changes.some(change => change.type === "inlineTextChild"),
+ "No inline text child mutation was fired.");
+ checkNodeFrontValue(valueFront, longString).then(runNextTest);
+ });
+}
+
+function testSetShortValue() {
+ ok(!longStringFront.inlineTextChild, "Text node is too long to be inlined.");
+
+ valueNode.nodeValue = shortString;
+ valueFront.walkerFront.once("mutations", (changes) => {
+ ok(!!longStringFront.inlineTextChild, "Text node is short enough to be inlined.");
+ ok(changes.some(change => change.type === "inlineTextChild"),
+ "An inlineTextChild mutation was fired.");
+ checkNodeFrontValue(valueFront, shortString).then(runNextTest);
+ });
+}
+
+function testKeepShortValue() {
+ ok(!!longStringFront.inlineTextChild, "Text node is short enough to be inlined.");
+
+ valueNode.nodeValue = shortString2;
+ valueFront.walkerFront.once("mutations", (changes) => {
+ ok(!!longStringFront.inlineTextChild, "Text node is short enough to be inlined.");
+ ok(!changes.some(change => change.type === "inlineTextChild"),
+ "No inline text child mutation was fired.");
+ checkNodeFrontValue(valueFront, shortString2).then(runNextTest);
+ });
+}
+
+function testSetLongValue() {
+ ok(!!longStringFront.inlineTextChild, "Text node is short enough to be inlined.");
+
+ valueNode.nodeValue = longString;
+ valueFront.walkerFront.once("mutations", (changes) => {
+ ok(!longStringFront.inlineTextChild, "Text node is too long to be inlined.");
+ ok(changes.some(change => change.type === "inlineTextChild"),
+ "An inlineTextChild mutation was fired.");
+ checkNodeFrontValue(valueFront, longString).then(runNextTest);
+ });
+}
+
+addTest(function cleanup() {
+ gInspectee = null;
+ gWalker = null;
+ runNextTest();
+});
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a>
+<a id="inspectorContent" target="_blank" href="inspector-traversal-data.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_inspector-pick-color.html b/devtools/server/tests/chrome/test_inspector-pick-color.html
new file mode 100644
index 0000000000..74aa3c50ce
--- /dev/null
+++ b/devtools/server/tests/chrome/test_inspector-pick-color.html
@@ -0,0 +1,94 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that the inspector actor has the pickColorFromPage and cancelPickColorFromPage
+methods and that when a color is picked the color-picked event is emitted and that when
+the eyedropper is dimissed, the color-pick-canceled event is emitted.
+https://bugzilla.mozilla.org/show_bug.cgi?id=1262439
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1262439</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript" src="inspector-helpers.js"></script>
+ <script type="application/javascript">
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ let win = null;
+ let inspector = null;
+
+ addAsyncTest(async function() {
+ info("Setting up inspector actor");
+
+ const url = document.getElementById("inspectorContent").href;
+ const { target, doc } = await attachURL(url);
+ inspector = await target.getFront("inspector");
+ win = doc.defaultView;
+ runNextTest();
+ });
+
+ addAsyncTest(async function() {
+ info("Start picking a color from the page");
+ await inspector.pickColorFromPage();
+
+ info("Click in the page and make sure a color-picked event is received");
+ const onColorPicked = waitForEvent("color-picked");
+ win.document.body.click();
+ const color = await onColorPicked;
+
+ is(color, "#000000", "The color-picked event was received with the right color");
+
+ runNextTest();
+ });
+
+ addAsyncTest(async function() {
+ info("Start picking a color from the page");
+ await inspector.pickColorFromPage();
+
+ info("Use the escape key to dismiss the eyedropper");
+ const onPickCanceled = waitForEvent("color-pick-canceled");
+
+ const keyboardEvent = new win.KeyboardEvent("keydown", {
+ bubbles: true,
+ cancelable: true,
+ view: win,
+ keyCode: 27
+ });
+ win.document.dispatchEvent(keyboardEvent);
+
+ await onPickCanceled;
+ ok(true, "The color-pick-canceled event was received");
+
+ runNextTest();
+ });
+
+ addAsyncTest(async function() {
+ info("Start picking a color from the page");
+ await inspector.pickColorFromPage();
+
+ info("And cancel the color picking");
+ await inspector.cancelPickColorFromPage();
+
+ runNextTest();
+ });
+
+ function waitForEvent(name) {
+ return new Promise(resolve => inspector.once(name, resolve));
+ }
+
+ runNextTest();
+};
+ </script>
+</head>
+<body>
+<a id="inspectorContent" target="_blank" href="inspector-eyedropper.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_inspector-pseudoclass-lock.html b/devtools/server/tests/chrome/test_inspector-pseudoclass-lock.html
new file mode 100644
index 0000000000..949066255d
--- /dev/null
+++ b/devtools/server/tests/chrome/test_inspector-pseudoclass-lock.html
@@ -0,0 +1,185 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug </title>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript" src="inspector-helpers.js"></script>
+ <script type="application/javascript">
+"use strict";
+
+const { PSEUDO_CLASSES } = require("devtools/shared/css/constants");
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ runNextTest();
+};
+
+let gInspectee = null;
+let gWalker = null;
+
+async function setup(callback) {
+ const url = document.getElementById("inspectorContent").href;
+ const { target, doc } = await attachURL(url);
+ gInspectee = doc;
+ const inspector = await target.getFront("inspector");
+ ok(inspector.walker, "getWalker() should return an actor.");
+ gWalker = inspector.walker;
+ runNextTest();
+}
+
+function teardown() {
+ gWalker = null;
+ gInspectee = null;
+}
+
+function checkChange(change, expectation) {
+ is(change.type, "pseudoClassLock", "Expect a pseudoclass lock change.");
+ const target = change.target;
+ if (expectation.id) {
+ is(target.id, expectation.id, "Expect a change on node id " + expectation.id);
+ }
+ if (expectation.nodeName) {
+ is(target.nodeName, expectation.nodeName,
+ "Expect a change on node name " + expectation.nodeName);
+ }
+
+ is(target.pseudoClassLocks.length, expectation.pseudos.length,
+ "Expect " + expectation.pseudos.length + " pseudoclass locks.");
+ for (let i = 0; i < expectation.pseudos.length; i++) {
+ const pseudo = expectation.pseudos[i];
+ const enabled = expectation.enabled === undefined ? true : expectation.enabled[i];
+ ok(target.hasPseudoClassLock(pseudo), "Expect lock: " + pseudo);
+ const rawNode = target.rawNode();
+ ok(InspectorUtils.hasPseudoClassLock(rawNode, pseudo),
+ "Expect lock in dom: " + pseudo);
+
+ is(rawNode.matches(pseudo), enabled,
+ `Target should match pseudoclass, '${pseudo}', if enabled (with .matches())`);
+ }
+
+ for (const pseudo of PSEUDO_CLASSES) {
+ if (!expectation.pseudos.some(expected => pseudo === expected)) {
+ ok(!target.hasPseudoClassLock(pseudo), "Don't expect lock: " + pseudo);
+ ok(!InspectorUtils.hasPseudoClassLock(target.rawNode(), pseudo),
+ "Don't expect lock in dom: " + pseudo);
+ }
+ }
+}
+
+function checkMutations(mutations, expectations) {
+ is(mutations.length, expectations.length, "Should get the right number of mutations.");
+ for (let i = 0; i < mutations.length; i++) {
+ checkChange(mutations[i], expectations[i]);
+ }
+}
+
+addTest(function testPseudoClassLock() {
+ let contentNode;
+ let nodeFront;
+ setup(() => {
+ contentNode = gInspectee.querySelector("#b");
+ return promiseDone(gWalker.querySelector(gWalker.rootNode, "#b").then(front => {
+ nodeFront = front;
+ // Lock the pseudoclass alone, no parents.
+ gWalker.addPseudoClassLock(nodeFront, ":active");
+ // Expect a single pseudoClassLock mutation.
+ return promiseOnce(gWalker, "mutations");
+ }).then(mutations => {
+ is(mutations.length, 1, "Should get one mutation");
+ is(mutations[0].target, nodeFront, "Should be the node we tried to apply to");
+ checkChange(mutations[0], {
+ id: "b",
+ nodeName: "DIV",
+ pseudos: [":active"],
+ });
+ }).then(() => {
+ // Now add :hover, this time with parents.
+ gWalker.addPseudoClassLock(nodeFront, ":hover", {parents: true});
+ return promiseOnce(gWalker, "mutations");
+ }).then(mutations => {
+ const expectedMutations = [{
+ id: "b",
+ nodeName: "DIV",
+ pseudos: [":hover", ":active"],
+ },
+ {
+ id: "longlist",
+ nodeName: "DIV",
+ pseudos: [":hover"],
+ },
+ {
+ nodeName: "BODY",
+ pseudos: [":hover"],
+ },
+ {
+ nodeName: "HTML",
+ pseudos: [":hover"],
+ }];
+ checkMutations(mutations, expectedMutations);
+ }).then(() => {
+ // Now remove the :hover on all parents
+ gWalker.removePseudoClassLock(nodeFront, ":hover", {parents: true});
+ return promiseOnce(gWalker, "mutations");
+ }).then(mutations => {
+ const expectedMutations = [{
+ id: "b",
+ nodeName: "DIV",
+ // Should still have :active on the original node.
+ pseudos: [":active"],
+ },
+ {
+ id: "longlist",
+ nodeName: "DIV",
+ pseudos: [],
+ },
+ {
+ nodeName: "BODY",
+ pseudos: [],
+ },
+ {
+ nodeName: "HTML",
+ pseudos: [],
+ }];
+ checkMutations(mutations, expectedMutations);
+ }).then(() => {
+ gWalker.addPseudoClassLock(nodeFront, ":hover", {enabled: false});
+ return promiseOnce(gWalker, "mutations");
+ }).then(mutations => {
+ is(mutations.length, 1, "Should get one mutation");
+ is(mutations[0].target, nodeFront, "Should be the node we tried to apply to");
+ checkChange(mutations[0], {
+ id: "b",
+ nodeName: "DIV",
+ pseudos: [":hover", ":active"],
+ enabled: [false, true],
+ });
+ }).then(() => {
+ // Now shut down the walker and make sure that clears up the remaining lock.
+ return gWalker.release();
+ }).then(() => {
+ ok(!InspectorUtils.hasPseudoClassLock(contentNode, ":active"),
+ "Pseudoclass should have been removed during destruction.");
+ teardown();
+ }).then(runNextTest));
+ });
+});
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a>
+<a id="inspectorContent" target="_blank" href="inspector-traversal-data.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_inspector-reload.html b/devtools/server/tests/chrome/test_inspector-reload.html
new file mode 100644
index 0000000000..09bd31cf75
--- /dev/null
+++ b/devtools/server/tests/chrome/test_inspector-reload.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug </title>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript" src="inspector-helpers.js"></script>
+ <script type="application/javascript">
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ runNextTest();
+};
+
+let gInspectee = null;
+let gWalker = null;
+let gResourceCommand = null;
+let gCommands = null;
+
+addTest(async function setup() {
+ const url = document.getElementById("inspectorContent").href;
+ const { commands, doc } = await attachURL(url);
+ const target = commands.targetCommand.targetFront;
+ const inspector = await target.getFront("inspector");
+ gInspectee = doc;
+ const walker = inspector.walker;
+ gWalker = await inspector.getWalker();
+ gResourceCommand = commands.resourceCommand;
+ gCommands = commands;
+
+ ok(walker === gWalker, "getWalker() twice should return the same walker.");
+ runNextTest();
+});
+
+addTest(async function testReload() {
+ const oldRootID = gWalker.rootNode.actorID;
+
+ info("Start watching for root nodes and wait for the initial root node");
+ let rootNodeResolve;
+ let rootNodePromise = new Promise(r => (rootNodeResolve = r));
+ const onAvailable = rootNodeFront => rootNodeResolve(rootNodeFront);
+ await gResourceCommand.watchResources([gResourceCommand.TYPES.ROOT_NODE], {
+ onAvailable,
+ });
+ await rootNodePromise;
+
+ info("Retrieve the node front for the selector `#a`");
+ const nodeFront = await gWalker.querySelector(gWalker.rootNode, "#a");
+ ok(nodeFront.actorID, "Node front has a valid actor ID");
+
+ info("Reload the page and wait for the newRoot mutation");
+ rootNodePromise = new Promise(r => (rootNodeResolve = r));
+
+ gInspectee.defaultView.location.reload();
+ await rootNodePromise;
+ gWalker = (await gCommands.targetCommand.targetFront.getFront("inspector")).walker;
+
+ info("Retrieve the (new) node front for the selector `#a`");
+ const newNodeFront = await gWalker.querySelector(gWalker.rootNode, "#a");
+ ok(newNodeFront.actorID, "Got a new actor ID");
+ ok(gWalker.rootNode.actorID != oldRootID, "Root node should have changed.");
+
+ runNextTest();
+});
+
+addTest(function cleanup() {
+ gWalker = null;
+ gInspectee = null;
+ gResourceCommand = null;
+ runNextTest();
+});
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a>
+<a id="inspectorContent" target="_blank" href="inspector-traversal-data.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_inspector-resize.html b/devtools/server/tests/chrome/test_inspector-resize.html
new file mode 100644
index 0000000000..e0cf9abade
--- /dev/null
+++ b/devtools/server/tests/chrome/test_inspector-resize.html
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that the inspector actor emits "resize" events when the page is resized.
+https://bugzilla.mozilla.org/show_bug.cgi?id=1222409
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1222409</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript" src="inspector-helpers.js"></script>
+ <script type="application/javascript">
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ let win = null;
+ let inspector = null;
+
+ addAsyncTest(async function setup() {
+ info("Setting up inspector and walker actors.");
+
+ const url = document.getElementById("inspectorContent").href;
+
+ const { target, doc } = await attachURL(url);
+ inspector = await target.getFront("inspector");
+ win = doc.defaultView;
+ runNextTest();
+ });
+
+ addAsyncTest(async function() {
+ const walker = inspector.walker;
+
+ // We can't receive events from the walker if we haven't first executed a
+ // method on the actor to initialize it.
+ await walker.querySelector(walker.rootNode, "img");
+
+ const {outerWidth, outerHeight} = win;
+ // eslint-disable-next-line new-cap
+ const onResize = new Promise(resolve => {
+ walker.once("resize", () => {
+ resolve();
+ });
+ });
+ win.resizeTo(800, 600);
+ await onResize;
+
+ ok(true, "The resize event was emitted");
+ win.resizeTo(outerWidth, outerHeight);
+
+ runNextTest();
+ });
+
+ runNextTest();
+};
+ </script>
+</head>
+<body>
+<a id="inspectorContent" target="_blank" href="inspector-search-data.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_inspector-resolve-url.html b/devtools/server/tests/chrome/test_inspector-resolve-url.html
new file mode 100644
index 0000000000..ddf68f56ed
--- /dev/null
+++ b/devtools/server/tests/chrome/test_inspector-resolve-url.html
@@ -0,0 +1,87 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=921102
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 921102</title>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript" src="inspector-helpers.js"></script>
+ <script type="application/javascript">
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ runNextTest();
+};
+
+let gInspector;
+let gDoc;
+
+addTest(async function() {
+ const url = document.getElementById("inspectorContent").href;
+ const { target, doc } = await attachURL(url);
+ gInspector = await target.getFront("inspector");
+ gDoc = doc;
+ runNextTest();
+});
+
+addTest(function() {
+ info("Resolve a relative URL without providing a context node");
+ gInspector.resolveRelativeURL("test.png?id=4#wow").then(url => {
+ is(url, "chrome://mochitests/content/chrome/devtools/server/tests/" +
+ "chrome/test.png?id=4#wow");
+ runNextTest();
+ });
+});
+
+addTest(function() {
+ info("Resolve an absolute URL without providing a context node");
+ gInspector.resolveRelativeURL("chrome://mochitests/content/chrome/" +
+ "devtools/server/").then(url => {
+ is(url, "chrome://mochitests/content/chrome/devtools/server/");
+ runNextTest();
+ });
+});
+
+addTest(function() {
+ info("Resolve a relative URL providing a context node");
+ const node = gDoc.querySelector(".big-horizontal");
+ gInspector.resolveRelativeURL("test.png?id=4#wow", node).then(url => {
+ is(url, "chrome://mochitests/content/chrome/devtools/server/tests/" +
+ "chrome/test.png?id=4#wow");
+ runNextTest();
+ });
+});
+
+addTest(function() {
+ info("Resolve an absolute URL providing a context node");
+ const node = gDoc.querySelector(".big-horizontal");
+ gInspector.resolveRelativeURL("chrome://mochitests/content/chrome/" +
+ "devtools/server/", node).then(url => {
+ is(url, "chrome://mochitests/content/chrome/devtools/server/");
+ runNextTest();
+ });
+});
+
+addTest(function() {
+ gInspector = null;
+ gDoc = null;
+ runNextTest();
+});
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=921102">Mozilla Bug 921102</a>
+<a id="inspectorContent" target="_blank" href="inspector_getImageData.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_inspector-scroll-into-view.html b/devtools/server/tests/chrome/test_inspector-scroll-into-view.html
new file mode 100644
index 0000000000..a107f9ba4a
--- /dev/null
+++ b/devtools/server/tests/chrome/test_inspector-scroll-into-view.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=901250
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 901250</title>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript" src="inspector-helpers.js"></script>
+ <script type="application/javascript">
+"use strict";
+
+window.onload = async function() {
+ SimpleTest.waitForExplicitFinish();
+
+ const url = document.getElementById("inspectorContent").href;
+ const { target, doc } = await attachURL(url);
+ const inspector = await target.getFront("inspector");
+ const walker = inspector.walker;
+
+ const id = "#scroll-into-view";
+ let rect = doc.querySelector(id).getBoundingClientRect();
+ const nodeFront = await walker.querySelector(walker.rootNode, id);
+ let inViewport = rect.x >= 0 &&
+ rect.y >= 0 &&
+ rect.y <= doc.defaultView.innerHeight &&
+ rect.x <= doc.defaultView.innerWidth;
+
+ ok(!inViewport, "Element is not in viewport initially");
+
+ await nodeFront.scrollIntoView();
+
+ await new Promise(res => SimpleTest.executeSoon(res));
+
+ rect = doc.querySelector(id).getBoundingClientRect();
+ inViewport = rect.x >= 0 &&
+ rect.y >= 0 &&
+ rect.y <= doc.defaultView.innerHeight &&
+ rect.x <= doc.defaultView.innerWidth;
+ ok(inViewport, "Element is in viewport after calling nodeFront.scrollIntoView");
+
+ await target.destroy();
+ SimpleTest.finish();
+};
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=901250">Mozilla Bug 901250</a>
+<a id="inspectorContent" target="_blank" href="inspector-traversal-data.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_inspector-search-front.html b/devtools/server/tests/chrome/test_inspector-search-front.html
new file mode 100644
index 0000000000..a78700e8e6
--- /dev/null
+++ b/devtools/server/tests/chrome/test_inspector-search-front.html
@@ -0,0 +1,163 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=835896
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 835896</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript" src="inspector-helpers.js"></script>
+ <script type="application/javascript">
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ let walkerFront = null;
+ let inspectorCommand = null;
+
+ // WalkerFront and Inspector Command specific tests. These aren't to exercise search
+ // edge cases so much as to test the state the Front maintains between
+ // searches.
+
+ addAsyncTest(async function setup() {
+ info("Setting up inspector and walker actors.");
+
+ const url = document.getElementById("inspectorContent").href;
+
+ const { commands } = await attachURL(url);
+ const target = commands.targetCommand.targetFront;
+ const inspector = await target.getFront("inspector");
+
+ walkerFront = inspector.walker;
+ inspectorCommand = commands.inspectorCommand;
+
+ runNextTest();
+ });
+
+ addAsyncTest(async function testWalkerFrontDefaults() {
+ info("Testing search API using WalkerFront and Inspector Command.");
+ const nodes = await walkerFront.querySelectorAll(walkerFront.rootNode, "h2");
+ const fronts = await nodes.items();
+
+ const commandResult = await inspectorCommand.findNextNode("");
+ ok(!commandResult, "Null result on front when searching for ''");
+
+ let results = await inspectorCommand.findNextNode("h2");
+ isDeeply(results, {
+ node: fronts[0],
+ resultsIndex: 0,
+ resultsLength: 3,
+ }, "Default options work");
+
+ results = await inspectorCommand.findNextNode("h2", { });
+ isDeeply(results, {
+ node: fronts[1],
+ resultsIndex: 1,
+ resultsLength: 3,
+ }, "Search works with empty options");
+
+ // Clear search data to remove result state on the front
+ await inspectorCommand.findNextNode("");
+ runNextTest();
+ });
+
+ addAsyncTest(async function testMultipleSearches() {
+ info("Testing search API using WalkerFront and Inspector Command (reverse=false)");
+ const nodes = await walkerFront.querySelectorAll(walkerFront.rootNode, "h2");
+ const fronts = await nodes.items();
+
+ let results = await inspectorCommand.findNextNode("h2");
+ isDeeply(results, {
+ node: fronts[0],
+ resultsIndex: 0,
+ resultsLength: 3,
+ }, "Search works with multiple results (reverse=false)");
+
+ results = await inspectorCommand.findNextNode("h2");
+ isDeeply(results, {
+ node: fronts[1],
+ resultsIndex: 1,
+ resultsLength: 3,
+ }, "Search works with multiple results (reverse=false)");
+
+ results = await inspectorCommand.findNextNode("h2");
+ isDeeply(results, {
+ node: fronts[2],
+ resultsIndex: 2,
+ resultsLength: 3,
+ }, "Search works with multiple results (reverse=false)");
+
+ results = await inspectorCommand.findNextNode("h2");
+ isDeeply(results, {
+ node: fronts[0],
+ resultsIndex: 0,
+ resultsLength: 3,
+ }, "Search works with multiple results (reverse=false)");
+
+ // Clear search data to remove result state on the front
+ await inspectorCommand.findNextNode("");
+ runNextTest();
+ });
+
+ addAsyncTest(async function testMultipleSearchesReverse() {
+ info("Testing search API using WalkerFront and Inspector Command (reverse=true)");
+ const nodes = await walkerFront.querySelectorAll(walkerFront.rootNode, "h2");
+ const fronts = await nodes.items();
+
+ let results = await inspectorCommand.findNextNode("h2", {reverse: true});
+ isDeeply(results, {
+ node: fronts[2],
+ resultsIndex: 2,
+ resultsLength: 3,
+ }, "Search works with multiple results (reverse=true)");
+
+ results = await inspectorCommand.findNextNode("h2", {reverse: true});
+ isDeeply(results, {
+ node: fronts[1],
+ resultsIndex: 1,
+ resultsLength: 3,
+ }, "Search works with multiple results (reverse=true)");
+
+ results = await inspectorCommand.findNextNode("h2", {reverse: true});
+ isDeeply(results, {
+ node: fronts[0],
+ resultsIndex: 0,
+ resultsLength: 3,
+ }, "Search works with multiple results (reverse=true)");
+
+ results = await inspectorCommand.findNextNode("h2", {reverse: true});
+ isDeeply(results, {
+ node: fronts[2],
+ resultsIndex: 2,
+ resultsLength: 3,
+ }, "Search works with multiple results (reverse=true)");
+
+ results = await inspectorCommand.findNextNode("h2", {reverse: false});
+ isDeeply(results, {
+ node: fronts[0],
+ resultsIndex: 0,
+ resultsLength: 3,
+ }, "Search works with multiple results (reverse=false)");
+
+ // Clear search data to remove result state on the command
+ await inspectorCommand.findNextNode("");
+ runNextTest();
+ });
+
+ runNextTest();
+};
+ </script>
+</head>
+<body>
+<a id="inspectorContent" target="_blank" href="inspector-search-data.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_inspector-template.html b/devtools/server/tests/chrome/test_inspector-template.html
new file mode 100644
index 0000000000..6fbc7742c6
--- /dev/null
+++ b/devtools/server/tests/chrome/test_inspector-template.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1078374
+Display template tag content in inspector.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug </title>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript" src="inspector-helpers.js"></script>
+ <script type="application/javascript">
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ let gWalker = null;
+
+ addAsyncTest(async function setup() {
+ const url = document.getElementById("inspectorContent").href;
+
+ const { target } = await attachURL(url);
+ const inspector = await target.getFront("inspector");
+ gWalker = inspector.walker;
+
+ runNextTest();
+ });
+
+ addAsyncTest(async function testWalker() {
+ const nodeFront = await gWalker.querySelector(gWalker.rootNode, "template");
+
+ let children = await gWalker.children(nodeFront);
+ is(children.nodes.length, 1, "Found one child under the template element");
+
+ const docFragment = children.nodes[0];
+ is(docFragment.nodeName, "#document-fragment",
+ "First child under <template> is a document-fragment");
+
+ children = await gWalker.children(docFragment);
+ is(children.nodes.length, 1, "Found one child under the template element");
+
+ const p = children.nodes[0];
+ is(p.nodeName, "P",
+ "First child under the document-fragment is a p element");
+
+ runNextTest();
+ });
+
+ runNextTest();
+};
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a>
+<a id="inspectorContent" target="_blank" href="inspector-template.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_inspector_getImageData-wait-for-load.html b/devtools/server/tests/chrome/test_inspector_getImageData-wait-for-load.html
new file mode 100644
index 0000000000..129116b913
--- /dev/null
+++ b/devtools/server/tests/chrome/test_inspector_getImageData-wait-for-load.html
@@ -0,0 +1,133 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Tests for InspectorActor.getImageData() in following cases:
+ * Image takes too long to load (the method rejects after a timeout).
+ * Image is loading when the method is called and the load finishes before
+ timeout.
+ * Image fails to load.
+
+https://bugzilla.mozilla.org/show_bug.cgi?id=1192536
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1192536</title>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript" src="inspector-helpers.js"></script>
+ <script type="application/javascript">
+"use strict";
+
+const PATH = "https://example.com/chrome/devtools/server/tests/chrome/";
+const BASE_IMAGE = PATH + "inspector-delay-image-response.sjs";
+const DELAYED_IMAGE = BASE_IMAGE + "?delay=300";
+const TIMEOUT_IMAGE = BASE_IMAGE + "?delay=50000";
+const NONEXISTENT_IMAGE = PATH + "this-does-not-exist.png";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ runNextTest();
+};
+
+function pushPref(preferenceName, value) {
+ return new Promise(resolve => {
+ const options = {"set": [[preferenceName, value]]};
+ SpecialPowers.pushPrefEnv(options, resolve);
+ });
+}
+
+let gImg = null;
+let gNodeFront = null;
+let gWalker = null;
+
+addTest(async function setup() {
+ const url = document.getElementById("inspectorContent").href;
+ const { target, doc } = await attachURL(url);
+ const inspector = await target.getFront("inspector");
+ gWalker = inspector.walker;
+ gNodeFront = await gWalker.querySelector(gWalker.rootNode, "img.custom");
+ gImg = doc.querySelector("img.custom");
+ ok(gNodeFront, "Got the image NodeFront.");
+ ok(gImg, "Got the image Node.");
+ runNextTest();
+});
+
+addTest(async function testTimeout() {
+ info("Testing that the method aborts if the image takes too long to load.");
+
+ // imageToImageData() only times out when flags.testing is not set.
+ await pushPref("devtools.testing", false);
+
+ gImg.src = TIMEOUT_IMAGE;
+
+ info("Calling getImageData().");
+ ensureRejects(gNodeFront.getImageData(), "Timeout image").then(runNextTest);
+});
+
+addTest(async function testNonExistentImage() {
+ info("Testing that non-existent image causes a rejection.");
+
+ // This test shouldn't hit the timeout.
+ await pushPref("devtools.testing", true);
+
+ gImg.src = NONEXISTENT_IMAGE;
+
+ info("Calling getImageData().");
+ ensureRejects(gNodeFront.getImageData(), "Non-existent image").then(runNextTest);
+});
+
+addTest(async function testDelayedImage() {
+ info("Testing that the method waits for an image to load.");
+
+ // This test shouldn't hit the timeout.
+ await pushPref("devtools.testing", true);
+
+ gImg.src = DELAYED_IMAGE;
+
+ info("Calling getImageData().");
+ checkImageData(gNodeFront.getImageData()).then(runNextTest);
+});
+
+addTest(function cleanup() {
+ gImg = null;
+ gNodeFront = null;
+ gWalker = null;
+ runNextTest();
+});
+
+/**
+ * Asserts that the given promise rejects.
+ */
+function ensureRejects(promise, desc) {
+ return promise.then(() => {
+ ok(false, desc + ": promise resolved unexpectedly.");
+ }, () => {
+ ok(true, desc + ": promise rejected as expected.");
+ });
+}
+
+/**
+ * Waits for the call to getImageData() the resolve and checks that the image
+ * size is reported correctly.
+ */
+function checkImageData(promise, { width, height } = { width: 1, height: 1 }) {
+ return promise.then(({ size }) => {
+ is(size.naturalWidth, width, "The width is correct.");
+ is(size.naturalHeight, height, "The height is correct.");
+ });
+}
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1192536">Mozilla Bug 1192536</a>
+<a id="inspectorContent" target="_blank" href="inspector_getImageData.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_inspector_getImageData.html b/devtools/server/tests/chrome/test_inspector_getImageData.html
new file mode 100644
index 0000000000..d95b0e5fd3
--- /dev/null
+++ b/devtools/server/tests/chrome/test_inspector_getImageData.html
@@ -0,0 +1,142 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=932937
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 932937</title>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript" src="inspector-helpers.js"></script>
+ <script type="application/javascript">
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ runNextTest();
+};
+
+let gWalker = null;
+
+addTest(async function setup() {
+ const url = document.getElementById("inspectorContent").href;
+ const { target } = await attachURL(url);
+ const inspector = await target.getFront("inspector");
+ gWalker = inspector.walker;
+ runNextTest();
+});
+
+addTest(async function testLargeImage() {
+ // Select the image node from the test page
+ const img = await gWalker.querySelector(gWalker.rootNode, ".big-horizontal");
+ ok(img, "Image node found in the test page");
+ ok(img.getImageData, "Image node has the getImageData function");
+ const imageData = await img.getImageData(100);
+ ok(imageData.data, "Image data actor was sent back");
+ ok(imageData.size, "Image size info was sent back too");
+ is(imageData.size.naturalWidth, 5333, "Natural width of the image correct");
+ is(imageData.size.naturalHeight, 3000, "Natural width of the image correct");
+ ok(imageData.size.resized, "Image was resized");
+ const str = await imageData.data.string();
+ ok(str, "We have an image data string!");
+ testResizing(imageData, str);
+});
+
+addTest(async function testLargeCanvas() {
+ // Select the canvas node from the test page
+ const canvas = await gWalker.querySelector(gWalker.rootNode, ".big-vertical");
+ ok(canvas, "Image node found in the test page");
+ ok(canvas.getImageData, "Image node has the getImageData function");
+ const imageData = await canvas.getImageData(350);
+ ok(imageData.data, "Image data actor was sent back");
+ ok(imageData.size, "Image size info was sent back too");
+ is(imageData.size.naturalWidth, 1000, "Natural width of the image correct");
+ is(imageData.size.naturalHeight, 2000, "Natural width of the image correct");
+ ok(imageData.size.resized, "Image was resized");
+ const str = await imageData.data.string();
+ ok(str, "We have an image data string!");
+ testResizing(imageData, str);
+});
+
+addTest(async function testSmallImage() {
+ // Select the small image node from the test page
+ const img = await gWalker.querySelector(gWalker.rootNode, ".small");
+ ok(img, "Image node found in the test page");
+ ok(img.getImageData, "Image node has the getImageData function");
+ const imageData = await img.getImageData();
+ ok(imageData.data, "Image data actor was sent back");
+ ok(imageData.size, "Image size info was sent back too");
+ is(imageData.size.naturalWidth, 245, "Natural width of the image correct");
+ is(imageData.size.naturalHeight, 240, "Natural width of the image correct");
+ ok(!imageData.size.resized, "Image was NOT resized");
+ const str = await imageData.data.string();
+ ok(str, "We have an image data string!");
+ testResizing(imageData, str);
+});
+
+addTest(async function testDataImage() {
+ // Select the data image node from the test page
+ const img = await gWalker.querySelector(gWalker.rootNode, ".data");
+ ok(img, "Image node found in the test page");
+ ok(img.getImageData, "Image node has the getImageData function");
+ const imageData = await img.getImageData(14);
+ ok(imageData.data, "Image data actor was sent back");
+ ok(imageData.size, "Image size info was sent back too");
+ is(imageData.size.naturalWidth, 28, "Natural width of the image correct");
+ is(imageData.size.naturalHeight, 28, "Natural width of the image correct");
+ ok(imageData.size.resized, "Image was resized");
+ const str = await imageData.data.string();
+ ok(str, "We have an image data string!");
+ testResizing(imageData, str);
+});
+
+addTest(async function testNonImgOrCanvasElements() {
+ const body = await gWalker.querySelector(gWalker.rootNode, "body");
+ await ensureRejects(body.getImageData(), "Invalid element");
+ runNextTest();
+});
+
+addTest(function cleanup() {
+ gWalker = null;
+ runNextTest();
+});
+
+/**
+ * Checks if the server told the truth about resizing the image
+ */
+function testResizing(imageData, str) {
+ const img = document.createElement("img");
+ img.addEventListener("load", () => {
+ const resized = !(img.naturalWidth == imageData.size.naturalWidth &&
+ img.naturalHeight == imageData.size.naturalHeight);
+ is(imageData.size.resized, resized, "Server told the truth about resizing");
+ runNextTest();
+ });
+ img.src = str;
+}
+
+/**
+ * Asserts that the given promise rejects.
+ */
+function ensureRejects(promise, desc) {
+ return promise.then(() => {
+ ok(false, desc + ": promise resolved unexpectedly.");
+ }, () => {
+ ok(true, desc + ": promise rejected as expected.");
+ });
+}
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=932937">Mozilla Bug 932937</a>
+<a id="inspectorContent" target="_blank" href="inspector_getImageData.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_inspector_getImageDataFromURL.html b/devtools/server/tests/chrome/test_inspector_getImageDataFromURL.html
new file mode 100644
index 0000000000..451c49dcc3
--- /dev/null
+++ b/devtools/server/tests/chrome/test_inspector_getImageDataFromURL.html
@@ -0,0 +1,116 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Tests for InspectorActor.getImageDataFromURL() in following cases:
+ * Normal case, image loads after a small delay.
+ * Image takes too long to load (the method rejects after a timeout).
+ * Image fails to load.
+
+https://bugzilla.mozilla.org/show_bug.cgi?id=1192536
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1192536</title>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript" src="inspector-helpers.js"></script>
+ <script type="application/javascript">
+"use strict";
+
+const PATH = "https://example.com/chrome/devtools/server/tests/chrome/";
+const BASE_IMAGE = PATH + "inspector-delay-image-response.sjs";
+const DELAYED_IMAGE = BASE_IMAGE + "?delay=300";
+const TIMEOUT_IMAGE = BASE_IMAGE + "?delay=50000";
+const NONEXISTENT_IMAGE = PATH + "this-does-not-exist.png";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ runNextTest();
+};
+
+function pushPref(preferenceName, value) {
+ return new Promise(resolve => {
+ const options = {"set": [[preferenceName, value]]};
+ SpecialPowers.pushPrefEnv(options, resolve);
+ });
+}
+
+let gInspector = null;
+
+addTest(async function setup() {
+ const url = document.getElementById("inspectorContent").href;
+ const { target } = await attachURL(url);
+ gInspector = await target.getFront("inspector");
+ runNextTest();
+});
+
+addTest(async function testTimeout() {
+ info("Testing that the method aborts if the image takes too long to load.");
+
+ // imageToImageData() only times out when flags.testing is not set.
+ await pushPref("devtools.testing", false);
+
+ ensureRejects(gInspector.getImageDataFromURL(TIMEOUT_IMAGE),
+ "Image that loads for too long").then(runNextTest);
+});
+
+addTest(async function testNonExistentImage() {
+ info("Testing that non-existent image causes a rejection.");
+
+ // This test shouldn't hit the timeout.
+ await pushPref("devtools.testing", true);
+
+ ensureRejects(gInspector.getImageDataFromURL(NONEXISTENT_IMAGE),
+ "Non-existent image").then(runNextTest);
+});
+
+addTest(async function testNormalImage() {
+ info("Testing that the method waits for an image to load.");
+
+ // This test shouldn't hit the timeout.
+ await pushPref("devtools.testing", true);
+
+ checkImageData(gInspector.getImageDataFromURL(DELAYED_IMAGE)).then(runNextTest);
+});
+
+addTest(function cleanup() {
+ gInspector = null;
+ runNextTest();
+});
+
+/**
+ * Asserts that the given promise rejects.
+ */
+function ensureRejects(promise, desc) {
+ return promise.then(() => {
+ ok(false, desc + ": promise resolved unexpectedly.");
+ }, () => {
+ ok(true, desc + ": promise rejected as expected.");
+ });
+}
+
+/**
+ * Waits for the call to getImageData() the resolve and checks that the image
+ * size is reported correctly.
+ */
+function checkImageData(promise, { width, height } = { width: 1, height: 1 }) {
+ return promise.then(({ size }) => {
+ is(size.naturalWidth, width, "The width is correct.");
+ is(size.naturalHeight, height, "The height is correct.");
+ });
+}
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1192536">Mozilla Bug 1192536</a>
+<a id="inspectorContent" target="_blank" href="inspector_getImageData.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_inspector_getNodeFromActor.html b/devtools/server/tests/chrome/test_inspector_getNodeFromActor.html
new file mode 100644
index 0000000000..c3c5d32af9
--- /dev/null
+++ b/devtools/server/tests/chrome/test_inspector_getNodeFromActor.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1155653
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1155653</title>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript" src="inspector-helpers.js"></script>
+ <script type="application/javascript">
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ runNextTest();
+};
+
+let gWalker;
+
+addTest(async function() {
+ const url = document.getElementById("inspectorContent").href;
+ const { target } = await attachURL(url);
+ const inspector = await target.getFront("inspector");
+ gWalker = inspector.walker;
+ runNextTest();
+});
+
+addTest(function() {
+ info("Try to get a NodeFront from an invalid actorID");
+ gWalker.getNodeFromActor("invalid", ["node"]).then(node => {
+ ok(!node, "The node returned is null");
+ runNextTest();
+ });
+});
+
+addTest(function() {
+ info("Try to get a NodeFront from a valid actorID but invalid path");
+ gWalker.getNodeFromActor(gWalker.actorID, ["invalid", "path"]).then(node => {
+ ok(!node, "The node returned is null");
+ runNextTest();
+ });
+});
+
+addTest(function() {
+ info("Try to get a NodeFront from a valid actorID and valid path");
+ gWalker.getNodeFromActor(gWalker.actorID, ["rootDoc"]).then(rootDocNode => {
+ ok(rootDocNode, "A node was returned");
+ is(rootDocNode, gWalker.rootNode, "The right node was returned");
+ runNextTest();
+ });
+});
+
+addTest(function() {
+ info("Try to get a NodeFront from a valid actorID and valid complex path");
+ gWalker.getNodeFromActor(gWalker.actorID,
+ ["targetActor", "window", "document", "body"]).then(bodyNode => {
+ ok(bodyNode, "A node was returned");
+ gWalker.querySelector(gWalker.rootNode, "body").then(node => {
+ is(bodyNode, node, "The body node was returned");
+ runNextTest();
+ });
+ });
+});
+
+addTest(function() {
+ gWalker = null;
+ runNextTest();
+});
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1155653">Mozilla Bug 1155653</a>
+<a id="inspectorContent" target="_blank" href="inspector_getImageData.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_inspector_getOffsetParent.html b/devtools/server/tests/chrome/test_inspector_getOffsetParent.html
new file mode 100644
index 0000000000..09da7d55d1
--- /dev/null
+++ b/devtools/server/tests/chrome/test_inspector_getOffsetParent.html
@@ -0,0 +1,129 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1345119
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1345119</title>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript" src="inspector-helpers.js"></script>
+ <script type="application/javascript">
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ runNextTest();
+};
+
+var gWalker;
+var gHTMLNode;
+var gBodyNode;
+
+addTest(async function setup() {
+ const url = document.getElementById("inspectorContent").href;
+ const { target } = await attachURL(url);
+ const inspector = await target.getFront("inspector");
+ gWalker = inspector.walker;
+ gBodyNode = await gWalker.querySelector(gWalker.rootNode, "body");
+ gHTMLNode = await gWalker.querySelector(gWalker.rootNode, "html");
+ runNextTest();
+});
+
+addTest(function() {
+ info("Try to get the offset parent for a dead node (null)");
+ gWalker.getOffsetParent(null).then(offsetParent => {
+ ok(!offsetParent, "No offset parent found");
+ runNextTest();
+ });
+});
+
+addTest(function() {
+ info("Try to get the offset parent for a node that is absolutely positioned inside a " +
+ "relative node");
+ gWalker.querySelector(gWalker.rootNode, "#absolute_child").then(node => {
+ return gWalker.getOffsetParent(node);
+ }).then(offsetParent => {
+ ok(offsetParent, "The node has an offset parent");
+ gWalker.querySelector(gWalker.rootNode, "#relative_parent").then(parent => {
+ ok(offsetParent === parent, "The offset parent is the correct node");
+ runNextTest();
+ });
+ });
+});
+
+addTest(function() {
+ info("Try to get the offset parent for a node that is absolutely positioned outside a" +
+ " relative node");
+ gWalker.querySelector(gWalker.rootNode, "#no_parent").then(node => {
+ return gWalker.getOffsetParent(node);
+ }).then(offsetParent => {
+ ok(offsetParent === gBodyNode || offsetParent === gHTMLNode,
+ "The node's offset parent is the body or html node");
+ runNextTest();
+ });
+});
+
+addTest(function() {
+ info("Try to get the offset parent for a relatively positioned node");
+ gWalker.querySelector(gWalker.rootNode, "#relative_parent").then(node => {
+ return gWalker.getOffsetParent(node);
+ }).then(offsetParent => {
+ ok(offsetParent === gBodyNode || offsetParent === gHTMLNode,
+ "The node's offset parent is the body or html node");
+ runNextTest();
+ });
+});
+
+addTest(function() {
+ info("Try to get the offset parent for a statically positioned node");
+ gWalker.querySelector(gWalker.rootNode, "#static").then(node => {
+ return gWalker.getOffsetParent(node);
+ }).then(offsetParent => {
+ ok(offsetParent === gBodyNode || offsetParent === gHTMLNode,
+ "The node's offset parent is the body or html node");
+ runNextTest();
+ });
+});
+
+addTest(function() {
+ info("Try to get the offset parent for a fixed positioned node");
+ gWalker.querySelector(gWalker.rootNode, "#fixed").then(node => {
+ return gWalker.getOffsetParent(node);
+ }).then(offsetParent => {
+ ok(offsetParent === gBodyNode || offsetParent === gHTMLNode,
+ "The node's offset parent is the body or html node");
+ runNextTest();
+ });
+});
+
+addTest(function() {
+ info("Try to get the offset parent for the body");
+ gWalker.querySelector(gWalker.rootNode, "body").then(node => {
+ return gWalker.getOffsetParent(node);
+ }).then(offsetParent => {
+ ok(!offsetParent, "The body has no offset parent");
+ runNextTest();
+ });
+});
+
+addTest(function() {
+ gWalker = null;
+ gBodyNode = null;
+ runNextTest();
+});
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1345119">Mozilla Bug 1345119</a>
+<a id="inspectorContent" target="_blank" href="inspector_getOffsetParent.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_makeGlobalObjectReference.html b/devtools/server/tests/chrome/test_makeGlobalObjectReference.html
new file mode 100644
index 0000000000..d800798427
--- /dev/null
+++ b/devtools/server/tests/chrome/test_makeGlobalObjectReference.html
@@ -0,0 +1,96 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=914405
+
+Debugger.prototype.makeGlobalObjectReference should dereference WindowProxy
+(outer window) objects.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Mozilla Bug 914405</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script>
+"use strict";
+
+const {addSandboxedDebuggerToGlobal} = ChromeUtils.importESModule("resource://gre/modules/jsdebugger.sys.mjs");
+addSandboxedDebuggerToGlobal(globalThis);
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ // Load one of our iframes over http to force it in a different compartment
+ // from the current window and the other iframe.
+ const iframe = document.createElement("iframe");
+ const baseURL = "http://mochi.test:8888/chrome/devtools/server/tests/chrome/";
+ iframe.src = baseURL + "iframe1_makeGlobalObjectReference.html";
+ iframe.onload = iframeOnLoad;
+ document.body.appendChild(iframe);
+
+ function iframeOnLoad() {
+ const dbg = new Debugger();
+
+ // 'o' for 'outer window'
+ const g1o = iframe.contentWindow;
+ ok(!dbg.hasDebuggee(g1o), "iframe is not initially a debuggee");
+
+ // Like addDebuggee, makeGlobalObjectReference innerizes.
+ // 'i' stands for 'inner window'.
+ // 'DO' stands for 'Debugger.Object'.
+ const g1iDO = dbg.makeGlobalObjectReference(g1o);
+ ok(!dbg.hasDebuggee(g1o),
+ "makeGlobalObjectReference does not add g1 as debuggee, designated via outer");
+ ok(!dbg.hasDebuggee(g1iDO),
+ "makeGlobalObjectReference does not add g1 as debuggee, designated via D.O ");
+
+ // Wrapping an object automatically outerizes it, so dereferencing an
+ // inner object D.O gets you an outer object.
+ // ('===' does distinguish inner and outer objects.)
+ // (That's a capital '=', if you must know.)
+ ok(g1iDO.unsafeDereference() === g1o, "g1iDO has the right referent");
+
+ // However, Debugger.Objects do distinguish inner and outer windows.
+ const g1oDO = g1iDO.makeDebuggeeValue(g1o);
+ ok(g1iDO !== g1oDO, "makeDebuggeeValue doesn't innerize");
+ ok(g1iDO.unsafeDereference() === g1oDO.unsafeDereference(),
+ "unsafeDereference() outerizes," +
+ " so inner and outer window D.Os both dereference to outer");
+
+ ok(dbg.addDebuggee(g1o) === g1iDO, "addDebuggee returns the inner window's D.O");
+ ok(dbg.hasDebuggee(g1o), "addDebuggee adds the correct global");
+ ok(dbg.hasDebuggee(g1iDO),
+ "hasDebuggee can take a D.O referring to the inner window");
+ ok(dbg.hasDebuggee(g1oDO),
+ "hasDebuggee can take a D.O referring to the outer window");
+
+ const iframe2 = document.createElement("iframe");
+ iframe2.src = "iframe2_makeGlobalObjectReference.html";
+ iframe2.onload = iframe2OnLoad;
+ document.body.appendChild(iframe2);
+
+ function iframe2OnLoad() {
+ // makeGlobalObjectReference dereferences CCWs.
+ const g2o = iframe2.contentWindow;
+ g2o.g1o = g1o;
+
+ const g2iDO = dbg.addDebuggee(g2o);
+ const g2g1oDO = g2iDO.getOwnPropertyDescriptor("g1o").value;
+ ok(g2g1oDO !== g1oDO, "g2's cross-compartment wrapper for g1o gets its own D.O");
+ ok(g2g1oDO.unwrap() === g1oDO,
+ "unwrapping g2's cross-compartment wrapper for g1o gets the right D.O");
+ ok(dbg.makeGlobalObjectReference(g2g1oDO) === g1iDO,
+ "makeGlobalObjectReference unwraps cross-compartment wrappers, and innerizes");
+
+ SimpleTest.finish();
+ }
+ }
+};
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_memory.html b/devtools/server/tests/chrome/test_memory.html
new file mode 100644
index 0000000000..79ba29c913
--- /dev/null
+++ b/devtools/server/tests/chrome/test_memory.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 923275 - Add a memory monitor widget to the developer toolbar
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Memory monitoring actor test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="memory-helpers.js" type="application/javascript"></script>
+<script>
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ (async function() {
+ const { memory, target } = await startServerAndGetSelectedTabMemory();
+ const measurement = await memory.measure();
+ ok(measurement.total > 0, "total memory is valid");
+ ok(measurement.domSize > 0, "domSize is valid");
+ ok(measurement.styleSize > 0, "styleSize is valid");
+ ok(measurement.jsObjectsSize > 0, "jsObjectsSize is valid");
+ ok(measurement.jsStringsSize > 0, "jsStringsSize is valid");
+ ok(measurement.jsOtherSize > 0, "jsOtherSize is valid");
+ ok(measurement.otherSize > 0, "otherSize is valid");
+ ok(measurement.jsMilliseconds, "jsMilliseconds is valid");
+ ok(measurement.nonJSMilliseconds, "nonJSMilliseconds is valid");
+ await destroyServerAndFinish(target);
+ })();
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_memory_allocations_02.html b/devtools/server/tests/chrome/test_memory_allocations_02.html
new file mode 100644
index 0000000000..632903bc04
--- /dev/null
+++ b/devtools/server/tests/chrome/test_memory_allocations_02.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1132764 - Test controlling the maximum allocations log length over the RDP.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Memory monitoring actor test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="memory-helpers.js" type="application/javascript"></script>
+<script>
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ (async function() {
+ const { memory, target } = await startServerAndGetSelectedTabMemory();
+ await memory.attach();
+
+ const allocs = [];
+ let eventsFired = 0;
+ let intervalId = null;
+ function onAlloc() {
+ eventsFired++;
+ }
+ function startAllocating() {
+ intervalId = setInterval(() => {
+ for (let i = 100000; --i;) {
+ allocs.push({});
+ }
+ }, 1);
+ }
+ function stopAllocating() {
+ clearInterval(intervalId);
+ }
+
+ memory.on("allocations", onAlloc);
+
+ await memory.startRecordingAllocations({
+ drainAllocationsTimeout: 10,
+ });
+
+ await waitUntil(() => eventsFired > 5);
+ ok(eventsFired > 5,
+ "Some allocation events fired without allocating much via auto drain");
+ await memory.stopRecordingAllocations();
+
+ // Set a really high auto drain timer so we can test if
+ // it fires on GC
+ eventsFired = 0;
+ const startTime = performance.now();
+ const drainTimer = 1000000;
+ await memory.startRecordingAllocations({
+ drainAllocationsTimeout: drainTimer,
+ });
+
+ startAllocating();
+ await waitUntil(() => {
+ Cu.forceGC();
+ return eventsFired > 1;
+ });
+ stopAllocating();
+ ok(performance.now() - drainTimer < startTime,
+ "Allocation events fired on GC before timer");
+ await memory.stopRecordingAllocations();
+
+ memory.off("allocations", onAlloc);
+ await memory.detach();
+ destroyServerAndFinish(target);
+ })();
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_memory_allocations_03.html b/devtools/server/tests/chrome/test_memory_allocations_03.html
new file mode 100644
index 0000000000..ca6a1ec1b4
--- /dev/null
+++ b/devtools/server/tests/chrome/test_memory_allocations_03.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1067491 - Test that frames keep the same index while we are recording.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Memory monitoring actor test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="memory-helpers.js" type="application/javascript"></script>
+<script>
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ (async function() {
+ const { memory, target } = await startServerAndGetSelectedTabMemory();
+ await memory.attach();
+
+ await memory.startRecordingAllocations();
+
+ // Allocate twice with the exact same stack (hence setTimeout rather than
+ // allocating directly in the generator), but with getAllocations() calls in
+ // between.
+
+ const allocs = [];
+ function allocator() {
+ allocs.push({});
+ }
+
+ setTimeout(allocator, 1);
+ await waitForTime(2);
+ const first = await memory.getAllocations();
+
+ setTimeout(allocator, 1);
+ await waitForTime(2);
+ const second = await memory.getAllocations();
+
+ await memory.stopRecordingAllocations();
+
+ // Assert that each frame in the first response has the same index in the
+ // second response. This isn't commutative, so we don't check that all
+ // of the second response's frames are the same in the first response,
+ // because there might be new allocations that happen after the first query
+ // but before the second.
+
+ function assertSameFrame(a, b) {
+ info(" First frame = " + JSON.stringify(a, null, 4));
+ info(" Second frame = " + JSON.stringify(b, null, 4));
+
+ is(!!a, !!b);
+ if (!a || !b) {
+ return;
+ }
+
+ is(a.source, b.source);
+ is(a.line, b.line);
+ is(a.column, b.column);
+ is(a.functionDisplayName, b.functionDisplayName);
+ is(a.parent, b.parent);
+ }
+
+ for (let i = 0; i < first.frames.length; i++) {
+ info("Checking frames at index " + i + ":");
+ assertSameFrame(first.frames[i], second.frames[i]);
+ }
+
+ await memory.detach();
+ destroyServerAndFinish(target);
+ })();
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_memory_allocations_04.html b/devtools/server/tests/chrome/test_memory_allocations_04.html
new file mode 100644
index 0000000000..8bb64c591c
--- /dev/null
+++ b/devtools/server/tests/chrome/test_memory_allocations_04.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1068171 - Test controlling the memory actor's allocation sampling probability.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Memory monitoring actor test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="memory-helpers.js" type="application/javascript"></script>
+<script>
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ (async function() {
+ const { memory, target } = await startServerAndGetSelectedTabMemory();
+ await memory.attach();
+
+ const allocs = [];
+ function allocator() {
+ for (let i = 0; i < 100; i++) {
+ allocs.push({});
+ }
+ }
+
+ const testProbability = async function(p, expected) {
+ info("probability = " + p);
+ await memory.startRecordingAllocations({
+ probability: p,
+ });
+ allocator();
+ const response = await memory.getAllocations();
+ await memory.stopRecordingAllocations();
+ return response.allocations.length;
+ };
+
+ is((await testProbability(0.0)), 0,
+ "With probability = 0.0, we shouldn't get any allocations.");
+
+ ok((await testProbability(1.0)) >= 100,
+ "With probability = 1.0, we should get all 100 allocations (plus "
+ + "whatever allocations the actor and SpiderMonkey make).");
+
+ // We don't test any other probabilities because the test would be
+ // non-deterministic. We don't have a way to control the PRNG like we do in
+ // jit-tests
+ // (js/src/jit-test/tests/debug/Memory-allocationsSamplingProbability-*.js).
+
+ await memory.detach();
+ destroyServerAndFinish(target);
+ })();
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_memory_allocations_05.html b/devtools/server/tests/chrome/test_memory_allocations_05.html
new file mode 100644
index 0000000000..590b3358e4
--- /dev/null
+++ b/devtools/server/tests/chrome/test_memory_allocations_05.html
@@ -0,0 +1,93 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1068144 - Test getting the timestamps for allocations.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Memory monitoring actor test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="memory-helpers.js" type="application/javascript"></script>
+<script>
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ (async function() {
+ const { memory, target } = await startServerAndGetSelectedTabMemory();
+ await memory.attach();
+
+ const allocs = [];
+ function allocator() {
+ allocs.push({});
+ }
+
+ // Using setTimeout results in wildly varying delays that make it hard to
+ // test our timestamps and results in intermittent failures. Instead, we
+ // actually spin an empty loop for a whole millisecond.
+ function actuallyWaitOneWholeMillisecond() {
+ const start = window.performance.now();
+ // eslint-disable-next-line curly
+ while (window.performance.now() - start < 1.000);
+ }
+
+ await memory.startRecordingAllocations();
+
+ allocator();
+ actuallyWaitOneWholeMillisecond();
+ allocator();
+ actuallyWaitOneWholeMillisecond();
+ allocator();
+
+ const response = await memory.getAllocations();
+ await memory.stopRecordingAllocations();
+
+ ok(response.allocationsTimestamps, "The response should have timestamps.");
+ is(response.allocationsTimestamps.length, response.allocations.length,
+ "There should be a timestamp for every allocation.");
+
+ const allocatorIndices = response.allocations
+ .map(function(a, idx) {
+ const frame = response.frames[a];
+ if (frame && frame.functionDisplayName === "allocator") {
+ return idx;
+ }
+ return null;
+ })
+ .filter(function(idx) {
+ return idx !== null;
+ });
+
+ is(allocatorIndices.length, 3,
+ "Should have our 3 allocations from the `allocator` timeouts.");
+
+ let lastTimestamp;
+ for (let i = 0; i < 3; i++) {
+ const timestamp = response.allocationsTimestamps[allocatorIndices[i]];
+ info("timestamp", timestamp);
+ ok(timestamp, "We should have a timestamp for the `allocator` allocation.");
+
+ if (lastTimestamp) {
+ const delta = timestamp - lastTimestamp;
+ info("delta since last timestamp", delta);
+ // ms
+ ok(delta >= 1,
+ "The timestamp should be about 1 ms after the last timestamp.");
+ }
+
+ lastTimestamp = timestamp;
+ }
+
+ await memory.detach();
+ destroyServerAndFinish(target);
+ })();
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_memory_allocations_06.html b/devtools/server/tests/chrome/test_memory_allocations_06.html
new file mode 100644
index 0000000000..d6223dd062
--- /dev/null
+++ b/devtools/server/tests/chrome/test_memory_allocations_06.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1132764 - Test controlling the maximum allocations log length over the RDP.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Memory monitoring actor test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="memory-helpers.js" type="application/javascript"></script>
+<script>
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ (async function() {
+ const { memory, target } = await startServerAndGetSelectedTabMemory();
+ await memory.attach();
+
+ const allocs = [];
+ function allocator() {
+ allocs.push({});
+ }
+
+ await memory.startRecordingAllocations({
+ maxLogLength: 1,
+ });
+
+ allocator();
+ allocator();
+ allocator();
+
+ const response = await memory.getAllocations();
+ await memory.stopRecordingAllocations();
+
+ is(response.allocations.length, 1,
+ "There should only be one entry in the allocations log.");
+
+ await memory.detach();
+ destroyServerAndFinish(target);
+ })();
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_memory_allocations_07.html b/devtools/server/tests/chrome/test_memory_allocations_07.html
new file mode 100644
index 0000000000..ce5ba4d2ad
--- /dev/null
+++ b/devtools/server/tests/chrome/test_memory_allocations_07.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1192335 - Test getting the byte sizes for allocations.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Memory monitoring actor test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="memory-helpers.js" type="application/javascript"></script>
+<script>
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ (async function() {
+ const { memory, target } = await startServerAndGetSelectedTabMemory();
+ await memory.attach();
+
+ const allocs = [];
+ function allocator() {
+ allocs.push({});
+ }
+
+ await memory.startRecordingAllocations();
+
+ allocator();
+ allocator();
+ allocator();
+
+ const response = await memory.getAllocations();
+ await memory.stopRecordingAllocations();
+
+ ok(response.allocationSizes, "The response should have bytesizes.");
+ is(response.allocationSizes.length, response.allocations.length,
+ "There should be a bytesize for every allocation.");
+ ok(response.allocationSizes.length >= 3,
+ "There are atleast 3 allocations.");
+ ok(response.allocationSizes.every(isPositiveNumber),
+ "every bytesize is a positive number");
+
+ await memory.detach();
+ destroyServerAndFinish(target);
+ })();
+};
+
+function isPositiveNumber(n) {
+ return typeof n === "number" && n > 0;
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_memory_attach_01.html b/devtools/server/tests/chrome/test_memory_attach_01.html
new file mode 100644
index 0000000000..89f1818292
--- /dev/null
+++ b/devtools/server/tests/chrome/test_memory_attach_01.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 960671 - Test attaching and detaching from a memory actor.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Memory monitoring actor test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="memory-helpers.js" type="application/javascript"></script>
+<script>
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ (async function() {
+ const { memory, target } = await startServerAndGetSelectedTabMemory();
+ await memory.attach();
+ ok(true, "Shouldn't have gotten an error attaching.");
+ await memory.detach();
+ ok(true, "Shouldn't have gotten an error detaching.");
+ destroyServerAndFinish(target);
+ })();
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_memory_attach_02.html b/devtools/server/tests/chrome/test_memory_attach_02.html
new file mode 100644
index 0000000000..89e23f5ed6
--- /dev/null
+++ b/devtools/server/tests/chrome/test_memory_attach_02.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 960671 - Test attaching and detaching while in the wrong state.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Memory monitoring actor test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="memory-helpers.js" type="application/javascript"></script>
+<script>
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ (async function() {
+ const { memory, target } = await startServerAndGetSelectedTabMemory();
+
+ let e = null;
+ try {
+ await memory.detach();
+ } catch (ee) {
+ e = ee;
+ }
+ ok(e, "Should have hit the wrongState error");
+
+ await memory.attach();
+
+ await memory.attach();
+ ok(true, "We can call attach many times, the duplicates will be ignored");
+
+ await memory.detach();
+ destroyServerAndFinish(target);
+ })();
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_memory_census.html b/devtools/server/tests/chrome/test_memory_census.html
new file mode 100644
index 0000000000..3c351740d3
--- /dev/null
+++ b/devtools/server/tests/chrome/test_memory_census.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1067491 - Test taking a census over the RDP.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Memory monitoring actor test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="memory-helpers.js" type="application/javascript"></script>
+<script>
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ (async function() {
+ const { memory, target } = await startServerAndGetSelectedTabMemory();
+ await memory.attach();
+
+ const census = await memory.takeCensus();
+ is(typeof census, "object");
+
+ await memory.detach();
+ destroyServerAndFinish(target);
+ })();
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_memory_gc_01.html b/devtools/server/tests/chrome/test_memory_gc_01.html
new file mode 100644
index 0000000000..8b2f049602
--- /dev/null
+++ b/devtools/server/tests/chrome/test_memory_gc_01.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1067491 - Test forcing a gc.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Memory monitoring actor test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="memory-helpers.js" type="application/javascript"></script>
+<script>
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ (async function() {
+ const { memory, target } = await startServerAndGetSelectedTabMemory();
+
+ let beforeGC, afterGC;
+
+ do {
+ let objects = [];
+ for (let i = 0; i < 1000; i++) {
+ const o = {};
+ o[Math.random()] = 1;
+ objects.push(o);
+ }
+ objects = null;
+
+ beforeGC = (await memory.measure()).total;
+
+ await memory.forceGarbageCollection();
+
+ afterGC = (await memory.measure()).total;
+ } while (beforeGC < afterGC);
+
+ ok(true, "The amount of memory after GC should eventually decrease");
+
+ destroyServerAndFinish(target);
+ })();
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_memory_gc_events.html b/devtools/server/tests/chrome/test_memory_gc_events.html
new file mode 100644
index 0000000000..5db1607c91
--- /dev/null
+++ b/devtools/server/tests/chrome/test_memory_gc_events.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1137527 - Test receiving GC events from the memory actor.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Memory monitoring actor test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="memory-helpers.js" type="application/javascript"></script>
+<script>
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ const EventEmitter = require("devtools/shared/event-emitter");
+
+ (async function() {
+ const { memory, target } = await startServerAndGetSelectedTabMemory();
+ await memory.attach();
+
+ const gotGcEvent = new Promise(resolve => {
+ EventEmitter.on(memory, "garbage-collection", gcData => {
+ ok(gcData, "Got GC data");
+ resolve();
+ });
+ });
+
+ memory.forceGarbageCollection();
+ await gotGcEvent;
+
+ await memory.detach();
+ destroyServerAndFinish(target);
+ })();
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_overflowing-body.html b/devtools/server/tests/chrome/test_overflowing-body.html
new file mode 100644
index 0000000000..1fe52e0011
--- /dev/null
+++ b/devtools/server/tests/chrome/test_overflowing-body.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test InspectorUtils.GetOverflowingChildrenOfElement applied to the body element
+-->
+<head>
+<meta charset="utf-8">
+<title>Test InspectorUtils.GetOverflowingChildrenOfElement on the body element</title>
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+<style>
+body {
+ overflow: auto;
+ margin: 0;
+}
+.tallBox {
+ overflow: auto;
+ background: lavender;
+ width: 200px;
+ height: 110vh;
+}
+</style>
+<script>
+'use strict';
+
+SimpleTest.waitForExplicitFinish();
+const InspectorUtils = SpecialPowers.InspectorUtils;
+
+function runTests() {
+ const body = document.documentElement;
+ const overflowing_children = InspectorUtils.getOverflowingChildrenOfElement(body);
+
+ is(overflowing_children.length, 1, `body has the expected number of children.`);
+
+ SimpleTest.finish();
+};
+window.onload = runTests;
+</script>
+</head>
+<body>
+<div class="tallBox"><div>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_overflowing-children.html b/devtools/server/tests/chrome/test_overflowing-children.html
new file mode 100644
index 0000000000..8ba81bec3d
--- /dev/null
+++ b/devtools/server/tests/chrome/test_overflowing-children.html
@@ -0,0 +1,131 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test InspectorUtils.GetOverflowingChildrenOfElement in various cases
+-->
+<head>
+<meta charset="utf-8">
+<title>Test InspectorUtils.GetOverflowingChildrenOfElement</title>
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+<style>
+/* "e" is our custom tag name for "element" */
+e {
+ background: lightgray;
+ display: inline-block;
+ margin: 10px;
+ padding: 0;
+ border: 0;
+ width: 100px;
+ height: 100px;
+ overflow: auto;
+}
+
+/* "c" is our custom tag name for "child" */
+c {
+ display: block;
+ background: green;
+}
+
+.fixedSize {
+ width: 10px;
+ height: 10px;
+}
+
+.target {
+ background: red;
+}
+</style>
+
+<script>
+'use strict';
+
+SimpleTest.waitForExplicitFinish();
+const InspectorUtils = SpecialPowers.InspectorUtils;
+
+const CASES = [
+ {id: "no_children", expected: 0},
+ {id: "one_child_no_overflow", expected: 0},
+ {id: "margin_left_overflow", expected: 1},
+ {id: "transform_overflow", expected: 1},
+ {id: "nested_overflow", expected: 1},
+ {id: "intermediate_overflow", expected: 1},
+ {id: "multiple_overflow_at_different_depths", expected: 2},
+];
+
+function runTests() {
+ // Assign each child element to an inner id so each of them can be identified for testing.
+ Array.from(document.getElementsByTagName('c')).forEach((e, i) => {e.id = `inner${i}`});
+
+ for (const {id, expected} of CASES) {
+ info(`Checking element id ${id}.`);
+
+ const element = document.getElementById(id);
+ if (!element) {
+ ok(false, `Expected to find element with id ${id}.`);
+ continue;
+ }
+ const overflowing_children = InspectorUtils.getOverflowingChildrenOfElement(element);
+
+ is(overflowing_children.length, expected, `${id} has the expected number of children.`);
+
+ // Check that each child has the "target" class. Otherwise, we're getting the
+ // wrong children. We don't check each child with a test function, because we
+ // don't want to needlessly inflate the number of test functions in the log.
+ // But if we find a child that *doesn't* have the class "target", we report
+ // that as a test failure.
+ for (const child of overflowing_children) {
+ // child is a Node, but not necessarily an Element. We want to get the containing
+ // Element so that we can use its classList, tagName, and id properties.
+ let e = child;
+ if (child.nodeType !== Node.ELEMENT_NODE) {
+ e = child.parentElement;
+ }
+ if (!e.classList.contains("target")) {
+ ok(false, `${id} is reporting this unexpected child as a target: ${e.tagName} id=${e.id}`);
+ }
+ }
+ }
+
+ SimpleTest.finish();
+};
+window.onload = runTests;
+</script>
+</head>
+<body onload="runTests()">
+
+<e id="no_children"></e>
+
+<e id="one_child_no_overflow">
+ <c></c>
+</e>
+
+<e id="margin_left_overflow">
+ <c class="target" style="margin-left:100px">abcd</c>
+</e>
+
+<e id="transform_overflow">
+ <c class="target" style="transform: translate(50px)">abcd</c>
+</e>
+
+<e id="nested_overflow">
+ <c>
+ <c class="target" style="margin-left:100px">abcd</c>
+ </c>
+</e>
+
+<e id="intermediate_overflow">
+ <c class="fixedSize target" style="margin-left:100px">
+ <c></c>
+ </c>
+</e>
+
+<e id="multiple_overflow_at_different_depths">
+ <c class="fixedSize target" style="margin-left:100px">
+ <c></c>
+ </c>
+ <c style="margin-left:100px">
+ <c class="target">abcd</c>
+ </c>
+</e>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_preference.html b/devtools/server/tests/chrome/test_preference.html
new file mode 100644
index 0000000000..b4d23a24aa
--- /dev/null
+++ b/devtools/server/tests/chrome/test_preference.html
@@ -0,0 +1,128 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 943251 - Test preferences actor
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test Preference Actor</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script>
+"use strict";
+
+function runTests() {
+ const {require} = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs");
+ const {DevToolsClient} = require("devtools/client/devtools-client");
+ const {DevToolsServer} = require("devtools/server/devtools-server");
+
+ SimpleTest.waitForExplicitFinish();
+
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+
+ const client = new DevToolsClient(DevToolsServer.connectPipe());
+ client.connect().then(function onConnect() {
+ return client.mainRoot.getFront("preference");
+ }).then(function(p) {
+ const prefs = {};
+
+ const localPref = {
+ boolPref: true,
+ intPref: 0x1234,
+ charPref: "Hello World",
+ };
+
+ function checkValues() {
+ is(prefs.boolPref, localPref.boolPref, "read/write bool pref");
+ is(prefs.intPref, localPref.intPref, "read/write int pref");
+ is(prefs.charPref, localPref.charPref, "read/write string pref");
+
+ ["test.all.bool", "test.all.int", "test.all.string"].forEach(function(key) {
+ let expectedValue;
+ switch (Services.prefs.getPrefType(key)) {
+ case Ci.nsIPrefBranch.PREF_STRING:
+ expectedValue = Services.prefs.getCharPref(key);
+ break;
+ case Ci.nsIPrefBranch.PREF_INT:
+ expectedValue = Services.prefs.getIntPref(key);
+ break;
+ case Ci.nsIPrefBranch.PREF_BOOL:
+ expectedValue = Services.prefs.getBoolPref(key);
+ break;
+ default:
+ ok(false, "unexpected pref type (" + key + ")");
+ break;
+ }
+
+ is(prefs.allPrefs[key].value, expectedValue,
+ "valid preference value (" + key + ")");
+ is(prefs.allPrefs[key].hasUserValue, Services.prefs.prefHasUserValue(key),
+ "valid hasUserValue (" + key + ")");
+ });
+
+ ["test.bool", "test.int", "test.string"].forEach(function(key) {
+ ok(!prefs.allPrefs.hasOwnProperty(key), "expect no pref (" + key + ")");
+ is(Services.prefs.getPrefType(key), Ci.nsIPrefBranch.PREF_INVALID,
+ "pref (" + key + ") is clear");
+ });
+
+ client.close().then(() => {
+ DevToolsServer.destroy();
+ SimpleTest.finish();
+ });
+ }
+
+ function checkUndefined() {
+ let next = p.getCharPref("test.undefined");
+ next = next.then(
+ () => ok(false, "getCharPref should've thrown for an undefined preference"),
+ (ex) => {
+ const messageRe = new RegExp(
+ "Protocol error \\(Error\\): preference is not of the right type: " +
+ `test.undefined from: ${p.actorID} ` +
+ "\\(resource://devtools/server/actors/preference.js:\\d+:\\d+\\)"
+ );
+ ok(messageRe.test(ex.message), "Error message matches the expected format");
+ }
+ );
+ return next;
+ }
+
+ function updatePrefsProperty(key) {
+ return function(value) {
+ prefs[key] = value;
+ };
+ }
+
+ p.getAllPrefs().then(updatePrefsProperty("allPrefs"))
+ .then(() => p.setBoolPref("test.bool", localPref.boolPref))
+ .then(() => p.setIntPref("test.int", localPref.intPref))
+ .then(() => p.setCharPref("test.string", localPref.charPref))
+ .then(() => p.getBoolPref("test.bool")).then(updatePrefsProperty("boolPref"))
+ .then(() => p.getIntPref("test.int")).then(updatePrefsProperty("intPref"))
+ .then(() => p.getCharPref("test.string")).then(updatePrefsProperty("charPref"))
+ .then(() => p.clearUserPref("test.bool"))
+ .then(() => p.clearUserPref("test.int"))
+ .then(() => p.clearUserPref("test.string"))
+ .then(() => checkUndefined())
+ .then(checkValues);
+ });
+}
+
+window.onload = function() {
+ SpecialPowers.pushPrefEnv({
+ "set": [
+ ["test.all.bool", true],
+ ["test.all.int", 0x4321],
+ ["test.all.string", "allizom"],
+ ],
+ }, runTests);
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_styles-applied.html b/devtools/server/tests/chrome/test_styles-applied.html
new file mode 100644
index 0000000000..0910e9d7bc
--- /dev/null
+++ b/devtools/server/tests/chrome/test_styles-applied.html
@@ -0,0 +1,155 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug </title>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript" src="inspector-helpers.js"></script>
+ <script type="application/javascript">
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ runNextTest();
+};
+
+let gWalker = null;
+let gStyles = null;
+
+addTest(async function setup() {
+ const url = document.getElementById("inspectorContent").href;
+ const { commands, target } = await attachURL(url);
+
+ // We need an active resource command before initializing the inspector front.
+ const resourceCommand = commands.resourceCommand;
+ // We listen to any random resource, we only need to trigger the resource command
+ // onTargetAvailable callback so the `resourceCommand` attribute is set on the target front.
+ await resourceCommand.watchResources([resourceCommand.TYPES.DOCUMENT_EVENT], { onAvailable: () => {} });
+
+ const inspector = await target.getFront("inspector");
+ gWalker = inspector.walker;
+ gStyles = await inspector.getPageStyle();
+
+ runNextTest();
+});
+
+addTest(async function inheritedUserStyles() {
+ const node = await gWalker.querySelector(gWalker.rootNode, "#test-node")
+ const applied = await gStyles.getApplied(node, { inherited: true, filter: "user" });
+
+ ok(!applied[0].inherited, "Entry 0 should be uninherited");
+ is(applied[0].rule.type, 100, "Entry 0 should be an element style");
+ ok(!!applied[0].rule.href, "Element styles should have a URL");
+ is(applied[0].rule.cssText, "", "Entry 0 should be an empty style");
+
+ is(applied[1].inherited.id, "uninheritable-rule-inheritable-style",
+ "Entry 1 should be inherited from the parent");
+ is(applied[1].rule.type, 100, "Entry 1 should be an element style");
+ is(applied[1].rule.cssText, "color: red;",
+ "Entry 1 should have the expected cssText");
+
+ is(applied[2].inherited.id, "inheritable-rule-inheritable-style",
+ "Entry 2 should be inherited from the parent's parent");
+ is(applied[2].rule.type, 100, "Entry 2 should be an element style");
+ is(applied[2].rule.cssText, "color: blue;",
+ "Entry 2 should have the expected cssText");
+
+ is(applied[3].inherited.id, "inheritable-rule-inheritable-style",
+ "Entry 3 should be inherited from the parent's parent");
+ is(applied[3].rule.type, 1, "Entry 3 should be a rule style");
+ is(applied[3].rule.cssText, "font-size: 15px;",
+ "Entry 3 should have the expected cssText");
+ ok(!applied[3].matchedDesugaredSelectors,
+ "Shouldn't get matchedDesugaredSelectors with this request.");
+
+ is(applied[4].inherited.id, "inheritable-rule-uninheritable-style",
+ "Entry 4 should be inherited from the parent's parent");
+ is(applied[4].rule.type, 1, "Entry 4 should be an rule style");
+ is(applied[4].rule.cssText, "font-size: 15px;",
+ "Entry 4 should have the expected cssText");
+ ok(!applied[4].matchedDesugaredSelectors, "Shouldn't get matchedDesugaredSelectors with this request.");
+
+ is(applied.length, 5, "Should have 5 rules.");
+
+ runNextTest();
+});
+
+addTest(async function inheritedSystemStyles() {
+ const node = await gWalker.querySelector(gWalker.rootNode, "#test-node");
+ const applied = await gStyles.getApplied(node, { inherited: true, filter: "ua" });
+ // If our system stylesheets are prone to churn, this might be a fragile
+ // test. If you're here because of that I apologize, file a bug
+ // and we can find a different way to test.
+
+ ok(!applied[1].inherited, "Entry 1 should not be inherited");
+ ok(applied[1].rule.parentStyleSheet.system, "Entry 1 should be a system style");
+ is(applied[1].rule.type, 1, "Entry 1 should be a rule style");
+ is(applied.length, 9, "Should have the expected number of rules.");
+
+ runNextTest();
+});
+
+addTest(async function noInheritedStyles() {
+ const node = await gWalker.querySelector(gWalker.rootNode, "#test-node")
+ const applied = await gStyles.getApplied(node, { inherited: false, filter: "user" });
+ ok(!applied[0].inherited, "Entry 0 should be uninherited");
+ is(applied[0].rule.type, 100, "Entry 0 should be an element style");
+ is(applied[0].rule.cssText, "", "Entry 0 should be an empty style");
+ is(applied.length, 1, "Should have 1 rule.");
+
+ runNextTest();
+});
+
+addTest(async function matchedSelectors() {
+ const node = await gWalker.querySelector(gWalker.rootNode, "#test-node");
+ const applied = await gStyles.getApplied(node, {
+ inherited: true, filter: "user", matchedSelectors: true,
+ });
+ is(applied[3].matchedDesugaredSelectors[0], ".inheritable-rule",
+ "Entry 3 should have a matched selector");
+ is(applied[4].matchedDesugaredSelectors[0], ".inheritable-rule",
+ "Entry 4 should have a matched selector");
+
+ runNextTest();
+});
+
+addTest(async function testMediaQuery() {
+ const node = await gWalker.querySelector(gWalker.rootNode, "#mediaqueried")
+ const applied = await gStyles.getApplied(node, {
+ inherited: false,
+ filter: "user",
+ matchedSelectors: true,
+ });
+
+ const ruleWithMedia = applied[1].rule;
+ is(ruleWithMedia.type, 1, "Entry 1 is a rule style");
+ is(ruleWithMedia.ancestorData[0].type, "media", "Entry 1's rule ancestor data holds the media rule data...");
+ is(ruleWithMedia.ancestorData[0].value, "screen", "...with the expected value");
+
+ runNextTest();
+});
+
+addTest(function cleanup() {
+ gStyles = null;
+ gWalker = null;
+ runNextTest();
+});
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a>
+<a id="inspectorContent" target="_blank" href="inspector-styles-data.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_styles-computed.html b/devtools/server/tests/chrome/test_styles-computed.html
new file mode 100644
index 0000000000..9aa962108a
--- /dev/null
+++ b/devtools/server/tests/chrome/test_styles-computed.html
@@ -0,0 +1,130 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug </title>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript" src="inspector-helpers.js"></script>
+ <script type="application/javascript">
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ runNextTest();
+};
+
+let gWalker = null;
+let gStyles = null;
+
+addTest(async function setup() {
+ const url = document.getElementById("inspectorContent").href;
+ const { target } = await attachURL(url);
+ const inspector = await target.getFront("inspector");
+ gWalker = inspector.walker;
+ gStyles = await inspector.getPageStyle();
+ runNextTest();
+});
+
+addTest(function testComputed() {
+ promiseDone(
+ gWalker.querySelector(gWalker.rootNode, "#computed-test-node").then(node => {
+ return gStyles.getComputed(node, {});
+ }).then(computed => {
+ // Test a smattering of properties that include some system-defined
+ // props, some props that were defined in this node's stylesheet,
+ // and some default props.
+ is(computed["white-space-collapse"].value, "collapse", "Default value should appear");
+ is(computed.display.value, "block", "System stylesheet item should appear");
+ is(computed.cursor.value, "crosshair", "Included stylesheet rule should appear");
+ is(computed.color.value, "rgb(255, 0, 0)",
+ "Inherited style attribute should appear");
+ is(computed["font-size"].value, "15px", "Inherited inline rule should appear");
+
+ // We didn't request markMatched, so these shouldn't be set
+ ok(!computed.cursor.matched, "Didn't ask for matched, shouldn't get it");
+ ok(!computed.color.matched, "Didn't ask for matched, shouldn't get it");
+ ok(!computed["font-size"].matched, "Didn't ask for matched, shouldn't get it");
+ }).then(runNextTest)
+ );
+});
+
+addTest(function testComputedUserMatched() {
+ promiseDone(
+ gWalker.querySelector(gWalker.rootNode, "#computed-test-node").then(node => {
+ return gStyles.getComputed(node, { filter: "user", markMatched: true });
+ }).then(computed => {
+ ok(!computed["white-space-collapse"].matched, "Default style shouldn't match");
+ ok(!computed.display.matched, "Only user styles should match");
+ ok(computed.cursor.matched, "Asked for matched, should get it");
+ ok(computed.color.matched, "Asked for matched, should get it");
+ ok(computed["font-size"].matched, "Asked for matched, should get it");
+ }).then(runNextTest)
+ );
+});
+
+addTest(function testComputedSystemMatched() {
+ promiseDone(
+ gWalker.querySelector(gWalker.rootNode, "#computed-test-node").then(node => {
+ return gStyles.getComputed(node, { filter: "ua", markMatched: true });
+ }).then(computed => {
+ ok(!computed["white-space-collapse"].matched, "Default style shouldn't match");
+ ok(computed.display.matched, "System stylesheets should match");
+ ok(computed.cursor.matched, "Asked for matched, should get it");
+ ok(computed.color.matched, "Asked for matched, should get it");
+ ok(computed["font-size"].matched, "Asked for matched, should get it");
+ }).then(runNextTest)
+ );
+});
+
+addTest(function testComputedUserOnlyMatched() {
+ promiseDone(
+ gWalker.querySelector(gWalker.rootNode, "#computed-test-node").then(node => {
+ return gStyles.getComputed(node, { filter: "user", onlyMatched: true });
+ }).then(computed => {
+ ok(!("white-space-collapse" in computed), "Default style shouldn't exist");
+ ok(!("display" in computed), "System stylesheets shouldn't exist");
+ ok(("cursor" in computed), "User items should exist.");
+ ok(("color" in computed), "User items should exist.");
+ ok(("font-size" in computed), "User items should exist.");
+ }).then(runNextTest)
+ );
+});
+
+addTest(function testComputedSystemOnlyMatched() {
+ promiseDone(
+ gWalker.querySelector(gWalker.rootNode, "#computed-test-node").then(node => {
+ return gStyles.getComputed(node, { filter: "ua", onlyMatched: true });
+ }).then(computed => {
+ ok(!("white-space-collapse" in computed), "Default style shouldn't exist");
+ ok(("display" in computed), "System stylesheets should exist");
+ ok(("cursor" in computed), "User items should exist.");
+ ok(("color" in computed), "User items should exist.");
+ ok(("font-size" in computed), "User items should exist.");
+ }).then(runNextTest)
+ );
+});
+
+addTest(function cleanup() {
+ gStyles = null;
+ gWalker = null;
+ runNextTest();
+});
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a>
+<a id="inspectorContent" target="_blank" href="inspector-styles-data.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_styles-layout.html b/devtools/server/tests/chrome/test_styles-layout.html
new file mode 100644
index 0000000000..f0441edd13
--- /dev/null
+++ b/devtools/server/tests/chrome/test_styles-layout.html
@@ -0,0 +1,110 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Test for Bug 1175040 - PageStyleActor.getLayout</title>
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+<script type="application/javascript" src="inspector-helpers.js"></script>
+<script type="application/javascript">
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ runNextTest();
+};
+
+let gWalker = null;
+let gStyles = null;
+
+addTest(async function() {
+ const url = document.getElementById("inspectorContent").href;
+ const { target } = await attachURL(url);
+ const inspector = await target.getFront("inspector");
+ gWalker = inspector.walker;
+ gStyles = await inspector.getPageStyle();
+ runNextTest();
+});
+
+addTest(function() {
+ ok(gStyles.getLayout, "The PageStyleActor has a getLayout method");
+ runNextTest();
+});
+
+addAsyncTest(async function() {
+ const node = await gWalker.querySelector(gWalker.rootNode, "#layout-element");
+ const layout = await gStyles.getLayout(node, {});
+
+ const properties = ["width", "height",
+ "margin-top", "margin-right", "margin-bottom",
+ "margin-left", "padding-top", "padding-right",
+ "padding-bottom", "padding-left", "border-top-width",
+ "border-right-width", "border-bottom-width",
+ "border-left-width", "z-index", "box-sizing", "display",
+ "position"];
+ for (const prop of properties) {
+ ok((prop in layout), "The layout object returned has " + prop);
+ }
+
+ runNextTest();
+});
+
+addAsyncTest(async function() {
+ const node = await gWalker.querySelector(gWalker.rootNode, "#layout-element");
+ const layout = await gStyles.getLayout(node, {});
+
+ const expected = {
+ "box-sizing": "border-box",
+ "position": "absolute",
+ "z-index": "2",
+ "display": "block",
+ "width": 50,
+ "height": 50,
+ "margin-top": "10px",
+ "margin-right": "20px",
+ "margin-bottom": "30px",
+ "margin-left": "0px",
+ };
+
+ for (const name in expected) {
+ is(layout[name], expected[name], "The " + name + " property is correct");
+ }
+
+ runNextTest();
+});
+
+addAsyncTest(async function() {
+ const node = await gWalker.querySelector(gWalker.rootNode,
+ "#layout-auto-margin-element");
+
+ let layout = await gStyles.getLayout(node, {});
+ ok(!("autoMargins" in layout),
+ "By default, getLayout doesn't return auto margins");
+
+ layout = await gStyles.getLayout(node, {autoMargins: true});
+ ok(("autoMargins" in layout),
+ "getLayout does return auto margins when asked to");
+ is(layout.autoMargins.left, "auto", "The left margin is auto");
+ is(layout.autoMargins.right, "auto", "The right margin is auto");
+ ok(!layout.autoMargins.bottom, "The bottom margin is not auto");
+ ok(!layout.autoMargins.top, "The top margin is not auto");
+
+ runNextTest();
+});
+
+addTest(function() {
+ gStyles = gWalker = null;
+ runNextTest();
+});
+
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1175040">Mozilla Bug 1175040</a>
+<a id="inspectorContent" target="_blank" href="inspector-styles-data.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_styles-matched.html b/devtools/server/tests/chrome/test_styles-matched.html
new file mode 100644
index 0000000000..42e1ec7885
--- /dev/null
+++ b/devtools/server/tests/chrome/test_styles-matched.html
@@ -0,0 +1,110 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug </title>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript" src="inspector-helpers.js"></script>
+ <script type="application/javascript">
+"use strict";
+
+const CssLogic = require("devtools/shared/inspector/css-logic");
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ runNextTest();
+};
+
+let gWalker = null;
+let gStyles = null;
+let gInspectee = null;
+
+addTest(async function setup() {
+ const url = document.getElementById("inspectorContent").href;
+ const { commands, target, doc } = await attachURL(url);
+ gInspectee = doc;
+
+ // We need an active resource command before initializing the inspector front.
+ const resourceCommand = commands.resourceCommand;
+ // We listen to any random resource, we only need to trigger the resource command
+ // onTargetAvailable callback so the `resourceCommand` attribute is set on the target front.
+ await resourceCommand.watchResources([resourceCommand.TYPES.DOCUMENT_EVENT], { onAvailable: () => {} });
+
+ const inspector = await target.getFront("inspector");
+ gWalker = inspector.walker;
+ gStyles = await inspector.getPageStyle();
+ runNextTest();
+});
+
+addTest(function testMatchedStyles() {
+ promiseDone(gWalker.querySelector(gWalker.rootNode, "#matched-test-node").then(node => {
+ return gStyles.getMatchedSelectors(node, "font-size", {});
+ }).then(matched => {
+ is(matched[0].sourceText, "this.style", "First match comes from the element style");
+ is(matched[0].selector, "@element.style", "Element style has a special selector");
+ is(matched[0].value, "10px", "First match has the expected value");
+ is(matched[0].status, CssLogic.STATUS.BEST, "First match is the best match");
+ is(matched[0].rule.type, 100, "First match is an element style");
+ is(matched[0].rule.href, gInspectee.defaultView.location.href,
+ "Node style comes from this document");
+
+ is(matched[1].sourceText, ".column-rule",
+ "Second match comes from a rule");
+ is(matched[1].selector, ".column-rule",
+ "Second match comes from highest line number");
+ is(matched[1].value, "25px", "Second match comes from highest column");
+ is(matched[1].status, CssLogic.STATUS.PARENT_MATCH,
+ "Second match is from the parent");
+ is(matched[1].rule.parentStyleSheet.href, null,
+ "Inline stylesheet shouldn't have an href");
+ is(matched[1].rule.parentStyleSheet.nodeHref, gInspectee.defaultView.location.href,
+ "Inline stylesheet's nodeHref should match the current document");
+ ok(!matched[1].rule.parentStyleSheet.system,
+ "Inline stylesheet shouldn't be a system stylesheet.");
+
+ // matched[2] is only there to test matched[1]; do not need to test
+
+ is(matched[3].value, "15px", "Third match has the expected value");
+ }).then(runNextTest));
+});
+
+addTest(function testSystemStyles() {
+ let testNode = null;
+
+ promiseDone(gWalker.querySelector(gWalker.rootNode, "#matched-test-node").then(node => {
+ testNode = node;
+ return gStyles.getMatchedSelectors(testNode, "display", { filter: "user" });
+ }).then(matched => {
+ is(matched.length, 0, "No user selectors apply to this rule.");
+ return gStyles.getMatchedSelectors(testNode, "display", { filter: "ua" });
+ }).then(matched => {
+ is(matched[0].selector, "div", "Should match system div selector");
+ is(matched[0].value, "block");
+ }).then(runNextTest));
+});
+
+addTest(function cleanup() {
+ gStyles = null;
+ gWalker = null;
+ gInspectee = null;
+ runNextTest();
+});
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a>
+<a id="inspectorContent" target="_blank" href="inspector-styles-data.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_styles-modify.html b/devtools/server/tests/chrome/test_styles-modify.html
new file mode 100644
index 0000000000..e615ec4425
--- /dev/null
+++ b/devtools/server/tests/chrome/test_styles-modify.html
@@ -0,0 +1,110 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug </title>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript" src="inspector-helpers.js"></script>
+ <script type="application/javascript">
+"use strict";
+
+const {isCssPropertyKnown} = require("devtools/server/actors/css-properties");
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ runNextTest();
+};
+
+var gWalker = null;
+var gStyles = null;
+var gInspectee = null;
+
+addAsyncTest(async function setup() {
+ const url = document.getElementById("inspectorContent").href;
+
+ const { target, doc } = await attachURL(url);
+ const inspector = await target.getFront("inspector");
+ gInspectee = doc;
+
+ gWalker = inspector.walker;
+ gStyles = await inspector.getPageStyle();
+
+ runNextTest();
+});
+
+addAsyncTest(async function modifyProperties() {
+ const localNode = gInspectee.querySelector("#inheritable-rule-inheritable-style");
+
+ const node = await gWalker.querySelector(gWalker.rootNode,
+ "#inheritable-rule-inheritable-style");
+
+ const applied = await gStyles.getApplied(node,
+ { inherited: false, filter: "user" });
+
+ const elementStyle = applied[0].rule;
+ is(elementStyle.cssText, localNode.style.cssText, "Got expected css text");
+
+ // Change an existing property...
+ await setProperty(elementStyle, 0, "color", "black");
+ // Create a new property
+ await setProperty(elementStyle, 1, "background-color", "green");
+
+ // Create a new property and then change it immediately.
+ await setProperty(elementStyle, 2, "border", "1px solid black");
+ await setProperty(elementStyle, 2, "border", "2px solid black");
+
+ is(elementStyle.cssText,
+ "color: black; background-color: green; border: 2px solid black;",
+ "Should have expected cssText");
+ is(elementStyle.cssText, localNode.style.cssText,
+ "Local node and style front match.");
+
+ // Remove all the properties
+ await removeProperty(elementStyle, 0, "color");
+ await removeProperty(elementStyle, 0, "background-color");
+ await removeProperty(elementStyle, 0, "border");
+
+ is(elementStyle.cssText, "", "Should have expected cssText");
+ is(elementStyle.cssText, localNode.style.cssText,
+ "Local node and style front match.");
+
+ runNextTest();
+});
+
+async function setProperty(rule, index, name, value) {
+ const changes = rule.startModifyingProperties(isCssPropertyKnown);
+ changes.setProperty(index, name, value);
+ await changes.apply();
+}
+
+async function removeProperty(rule, index, name) {
+ const changes = rule.startModifyingProperties(isCssPropertyKnown);
+ changes.removeProperty(index, name);
+ await changes.apply();
+}
+
+addTest(function cleanup() {
+ gStyles = null;
+ gWalker = null;
+ gInspectee = null;
+ runNextTest();
+});
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a>
+<a id="inspectorContent" target="_blank" href="inspector-styles-data.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_styles-svg.html b/devtools/server/tests/chrome/test_styles-svg.html
new file mode 100644
index 0000000000..b03bc868b7
--- /dev/null
+++ b/devtools/server/tests/chrome/test_styles-svg.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=921191
+Bug 921191 - allow inspection/editing of SVG elements' CSS properties
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug </title>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript" src="inspector-helpers.js"></script>
+ <script type="application/javascript">
+"use strict";
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ runNextTest();
+};
+
+let gWalker = null;
+let gStyles = null;
+
+addTest(async function setup() {
+ const url = document.getElementById("inspectorContent").href;
+ const { target } = await attachURL(url);
+ const inspector = await target.getFront("inspector");
+ gWalker = inspector.walker;
+ gStyles = await inspector.getPageStyle();
+ runNextTest();
+});
+
+addTest(function inheritedUserStyles() {
+ promiseDone(gWalker.querySelector(gWalker.rootNode, "#svgcontent rect").then(node => {
+ return gStyles.getApplied(node, { inherited: true, filter: "user" });
+ }).then(applied => {
+ is(applied.length, 2, "Should have 2 rules");
+ is(applied[1].rule.cssText, "fill: rgb(1, 2, 3);", "cssText is right");
+ }).then(runNextTest));
+});
+
+addTest(function cleanup() {
+ gStyles = null;
+ gWalker = null;
+ runNextTest();
+});
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=921191">Mozilla Bug 921191</a>
+<a id="inspectorContent" target="_blank" href="inspector-styles-data.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_suspendTimeouts.html b/devtools/server/tests/chrome/test_suspendTimeouts.html
new file mode 100644
index 0000000000..65a168986f
--- /dev/null
+++ b/devtools/server/tests/chrome/test_suspendTimeouts.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1426467
+
+When we use windowUtils.resumeTimeouts to resume timeouts in a window, that call
+should not immediately dispatch `onmessage` handlers for messages from workers.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Mozilla Bug 1426467</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src='test_suspendTimeouts.js'></script>
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_suspendTimeouts.js b/devtools/server/tests/chrome/test_suspendTimeouts.js
new file mode 100644
index 0000000000..614ac60cdb
--- /dev/null
+++ b/devtools/server/tests/chrome/test_suspendTimeouts.js
@@ -0,0 +1,139 @@
+"use strict";
+
+// The debugger uses nsIDOMWindowUtils::suspendTimeouts and ...::resumeTimeouts
+// to ensure that content event handlers do not run while a JavaScript
+// invocation is stepping or paused at a breakpoint. If a worker thread sends
+// messages to the content while the content is paused, those messages must not
+// run until the JavaScript invocation interrupted by the debugger has completed.
+//
+// Bug 1426467 is that calling nsIDOMWindowUtils::resumeTimeouts actually
+// delivers deferred messages itself, calling the content's 'onmessage' handler.
+// But the debugger calls suspend/resume around each individual interruption of
+// the debuggee -- each step, say -- meaning that hitting the "step into" button
+// causes you to step from the debuggee directly into an onmessage handler,
+// since the onmessage handler is the next function call the debugger sees.
+//
+// In other words, delivering deferred messages from resumeTimeouts, as it is
+// used by the debugger, breaks the run-to-completion rule. They must not be
+// delivered until after the JavaScript invocation at hand is complete. That's
+// what this test checks.
+//
+// For this test to detect the bug, the following steps must take place in
+// order:
+//
+// 1) The content page must call suspendTimeouts.
+// 2) A runnable conveying a message from the worker thread must attempt to
+// deliver the message, see that the content page has suspended such things,
+// and hold the message for later delivery.
+// 3) The content page must call resumeTimeouts.
+//
+// In a correct implementation, the message from the worker thread is delivered
+// only after the main thread returns to the event loop after calling
+// resumeTimeouts in step 3). In the buggy implementation, the onmessage handler
+// is called directly from the call to resumeTimeouts, so that the onmessage
+// handlers run in the midst of whatever JavaScript invocation resumed timeouts
+// (say, stepping in the debugger), in violation of the run-to-completion rule.
+//
+// In this specific bug, the handlers are called from resumeTimeouts, but
+// really, running them any time before that invocation returns to the main
+// event loop would be a bug.
+//
+// Posting the message and calling resumeTimeouts take place in different
+// threads, but if 2) and 3) don't occur in that order, the worker's message
+// will never be delayed and the test will pass spuriously. But the worker
+// can't communicate with the content page directly, to let it know that it
+// should proceed with step 3): the purpose of suspendTimeouts is to pause
+// all such communication.
+//
+// So instead, the content page creates a MessageChannel, and passes one
+// MessagePort to the worker and the other to this mochitest (which has its
+// own window, separate from the one calling suspendTimeouts). The worker
+// notifies the mochitest when it has posted the message, and then the
+// mochitest calls into the content to carry out step 3).
+
+// To help you follow all the callbacks and event handlers, this code pulls out
+// event handler functions so that control flows from top to bottom.
+
+window.onload = function () {
+ // This mochitest is not complete until we call SimpleTest.finish. Don't just
+ // exit as soon as we return to the main event loop.
+ SimpleTest.waitForExplicitFinish();
+
+ const iframe = document.createElement("iframe");
+ iframe.src =
+ "http://mochi.test:8888/chrome/devtools/server/tests/chrome/suspendTimeouts_content.html";
+ iframe.onload = iframe_onload_handler;
+ document.body.appendChild(iframe);
+
+ function iframe_onload_handler() {
+ const content = iframe.contentWindow.wrappedJSObject;
+
+ const windowUtils = iframe.contentWindow.windowUtils;
+
+ // Hand over the suspend and resume functions to the content page, along
+ // with some testing utilities.
+ content.suspendTimeouts = function () {
+ SimpleTest.info("test_suspendTimeouts", "calling suspendTimeouts");
+ windowUtils.suspendTimeouts();
+ };
+ content.resumeTimeouts = function () {
+ windowUtils.resumeTimeouts();
+ SimpleTest.info("test_suspendTimeouts", "resumeTimeouts called");
+ };
+ content.info = function (message) {
+ SimpleTest.info("suspendTimeouts_content.js", message);
+ };
+ content.ok = SimpleTest.ok;
+ content.finish = finish;
+
+ SimpleTest.info(
+ "Disappointed with National Tautology Day? Well, it is what it is."
+ );
+
+ // Once the worker has sent a message to its parent (which should get delayed),
+ // it sends us a message directly on this channel.
+ const workerPort = content.create_channel();
+ workerPort.onmessage = handle_worker_echo;
+
+ // Have content send the worker a message that it should echo back to both
+ // content and us. The echo to content should get delayed; the echo to us
+ // should cause our handle_worker_echo to be called.
+ content.start_worker();
+
+ function handle_worker_echo({ data }) {
+ info(`mochitest received message from worker: ${data}`);
+
+ // As it turns out, it's not correct to assume that, if the worker posts a
+ // message to its parent via the global `postMessage` function, and then
+ // posts a message to the mochitest via the MessagePort, those two
+ // messages will be delivered in the order they were sent.
+ //
+ // - Messages sent via the worker's global's postMessage go through two
+ // ThrottledEventQueues (one in the worker, and one on the parent), and
+ // eventually find their way into the thread's primary event queue,
+ // which is a PrioritizedEventQueue.
+ //
+ // - Messages sent via a MessageChannel whose ports are owned by different
+ // threads are passed as IPDL messages.
+ //
+ // There's basically no reliable way to ensure that delivery to content
+ // has been attempted and the runnable deferred; there are too many
+ // variables affecting the order in which things are processed. Delaying
+ // for a second is the best I could think of.
+ //
+ // Fortunately, this tactic failing can only cause spurious test passes
+ // (the runnable never gets deferred, so things work by accident), not
+ // spurious failures. Without some kind of trustworthy notification that
+ // the runnable has been deferred, perhaps via some special white-box
+ // testing API, we can't do better.
+ setTimeout(() => {
+ content.resume_timeouts();
+ }, 1000);
+ }
+
+ function finish(message) {
+ SimpleTest.info("suspendTimeouts_content.js", "called finish");
+ SimpleTest.finish();
+ }
+ }
+};
diff --git a/devtools/server/tests/chrome/test_unsafeDereference.html b/devtools/server/tests/chrome/test_unsafeDereference.html
new file mode 100644
index 0000000000..eca1e7d43e
--- /dev/null
+++ b/devtools/server/tests/chrome/test_unsafeDereference.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=837723
+
+When we use Debugger.Object.prototype.unsafeDereference to get a non-D.O
+reference to a content object in chrome, that reference should be via an
+xray wrapper.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Mozilla Bug 837723</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script>
+"use strict";
+
+const {addDebuggerToGlobal} = ChromeUtils.importESModule("resource://gre/modules/jsdebugger.sys.mjs");
+addDebuggerToGlobal(globalThis);
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ const iframe = document.createElement("iframe");
+ iframe.src = "http://mochi.test:8888/chrome/devtools/server/tests/chrome/nonchrome_unsafeDereference.html";
+
+ iframe.onload = function() {
+ const dbg = new Debugger();
+ const contentDO = dbg.addDebuggee(iframe.contentWindow);
+ const xhrDesc = contentDO.getOwnPropertyDescriptor("xhr");
+
+ isnot(xhrDesc, undefined, "xhr should be visible as property of content global");
+ isnot(xhrDesc.value, undefined, "xhr should have a value");
+
+ const xhr = xhrDesc.value.unsafeDereference();
+
+ is(typeof xhr, "object", "we should be able to deference xhr's value's D.O");
+ is(xhr.timeout, 1742, "chrome should see the xhr's 'timeout' property");
+ is(xhr.expando, undefined, "chrome should not see the xhr's 'expando' property");
+
+ SimpleTest.finish();
+ };
+
+ document.body.appendChild(iframe);
+};
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/test_webconsole-node-grip.html b/devtools/server/tests/chrome/test_webconsole-node-grip.html
new file mode 100644
index 0000000000..0c54f65964
--- /dev/null
+++ b/devtools/server/tests/chrome/test_webconsole-node-grip.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>DOMNode Object actor test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript" src="webconsole-helpers.js"></script>
+ <script>
+"use strict";
+
+const TEST_URL = "data:text/html,<html><body>Hello</body></html>";
+
+window.onload = async function() {
+ SimpleTest.waitForExplicitFinish();
+
+ try {
+ const commands = await addTabAndCreateCommands(TEST_URL);
+ await testNotInTreeElementNode(commands);
+ await testInTreeElementNode(commands);
+ await testNotInTreeTextNode(commands);
+ await testInTreeTextNode(commands);
+ } catch (e) {
+ ok(false, `Error thrown: ${e.message}`);
+ }
+ SimpleTest.finish();
+};
+
+async function testNotInTreeElementNode(commands) {
+ info("Testing isConnected property on a ElementNode not in the DOM tree");
+ const {result} = await commands.scriptCommand.execute("document.createElement(\"div\")");
+ is(result.getGrip().preview.isConnected, false,
+ "isConnected is false since we only created the element");
+}
+
+async function testInTreeElementNode(commands) {
+ info("Testing isConnected property on a ElementNode in the DOM tree");
+ const {result} = await commands.scriptCommand.execute("document.body");
+ is(result.getGrip().preview.isConnected, true,
+ "isConnected is true as expected, since the element was retrieved from the DOM tree");
+}
+
+async function testNotInTreeTextNode(commands) {
+ info("Testing isConnected property on a TextNode not in the DOM tree");
+ const {result} = await commands.scriptCommand.execute("document.createTextNode(\"Hello\")");
+ is(result.getGrip().preview.isConnected, false,
+ "isConnected is false since we only created the element");
+}
+
+async function testInTreeTextNode(commands) {
+ info("Testing isConnected property on a TextNode in the DOM tree");
+ const {result} = await commands.scriptCommand.execute("document.body.firstChild");
+ is(result.getGrip().preview.isConnected, true,
+ "isConnected is true as expected, since the element was retrieved from the DOM tree");
+}
+
+ </script>
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+</body>
+</html>
diff --git a/devtools/server/tests/chrome/webconsole-helpers.js b/devtools/server/tests/chrome/webconsole-helpers.js
new file mode 100644
index 0000000000..8be8554e35
--- /dev/null
+++ b/devtools/server/tests/chrome/webconsole-helpers.js
@@ -0,0 +1,54 @@
+/* exported addTabAndCreateCommands */
+"use strict";
+
+const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+const {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+const {
+ CommandsFactory,
+} = require("resource://devtools/shared/commands/commands-factory.js");
+
+// Always log packets when running tests.
+Services.prefs.setBoolPref("devtools.debugger.log", true);
+SimpleTest.registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("devtools.debugger.log");
+});
+
+if (!DevToolsServer.initialized) {
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+ SimpleTest.registerCleanupFunction(function () {
+ DevToolsServer.destroy();
+ });
+}
+
+/**
+ * Open a tab, load the url, find the tab with the devtools server,
+ * and attach the console to it.
+ *
+ * @param {string} url : url to navigate to
+ * @return {Promise} Promise resolving when commands are initialized
+ * The Promise resolves with the commands.
+ */
+async function addTabAndCreateCommands(url) {
+ const tab = await addTab(url);
+ const commands = await CommandsFactory.forTab(tab);
+ await commands.targetCommand.startListening();
+ return commands;
+}
+
+/**
+ * Naive implementaion of addTab working from a mochitest-chrome test.
+ */
+async function addTab(url) {
+ const { gBrowser } = Services.wm.getMostRecentWindow("navigator:browser");
+ const { BrowserTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/BrowserTestUtils.sys.mjs"
+ );
+ const tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, url));
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ return tab;
+}
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");