From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001
From: Daniel Baumann
Date: Fri, 19 Apr 2024 02:47:55 +0200
Subject: Adding upstream version 124.0.1.
Signed-off-by: Daniel Baumann
---
.../shared/test/addons/test-addon-1/manifest.json | 10 +
.../shared/test/addons/test-addon-2/manifest.json | 10 +
devtools/client/shared/test/browser.toml | 323 +++
.../shared/test/browser_autocomplete_popup.js | 121 +
.../browser_autocomplete_popup_consecutive-show.js | 57 +
.../test/browser_autocomplete_popup_input.js | 251 +++
.../shared/test/browser_browserloader_mocks.js | 162 ++
devtools/client/shared/test/browser_css_angle.js | 204 ++
devtools/client/shared/test/browser_css_color.js | 106 +
.../client/shared/test/browser_cubic-bezier-01.js | 38 +
.../client/shared/test/browser_cubic-bezier-02.js | 206 ++
.../client/shared/test/browser_cubic-bezier-03.js | 70 +
.../client/shared/test/browser_cubic-bezier-04.js | 59 +
.../client/shared/test/browser_cubic-bezier-05.js | 69 +
.../client/shared/test/browser_cubic-bezier-06.js | 95 +
.../client/shared/test/browser_cubic-bezier-07.js | 69 +
.../client/shared/test/browser_dbg_globalactor.js | 71 +
.../client/shared/test/browser_dbg_listaddons.js | 137 ++
.../client/shared/test/browser_dbg_listtabs-01.js | 84 +
.../client/shared/test/browser_dbg_listtabs-02.js | 248 +++
.../client/shared/test/browser_dbg_listworkers.js | 75 +
.../shared/test/browser_dbg_multiple-windows.js | 122 +
.../test/browser_dbg_target-scoped-actor-01.js | 43 +
.../test/browser_dbg_target-scoped-actor-02.js | 58 +
devtools/client/shared/test/browser_devices.js | 76 +
.../client/shared/test/browser_filter-editor-01.js | 150 ++
.../client/shared/test/browser_filter-editor-02.js | 114 +
.../client/shared/test/browser_filter-editor-03.js | 84 +
.../client/shared/test/browser_filter-editor-04.js | 106 +
.../client/shared/test/browser_filter-editor-05.js | 166 ++
.../client/shared/test/browser_filter-editor-06.js | 77 +
.../client/shared/test/browser_filter-editor-07.js | 32 +
.../client/shared/test/browser_filter-editor-08.js | 103 +
.../client/shared/test/browser_filter-editor-09.js | 155 ++
.../client/shared/test/browser_filter-editor-10.js | 100 +
.../shared/test/browser_filter-presets-01.js | 117 +
.../shared/test/browser_filter-presets-02.js | 47 +
.../shared/test/browser_filter-presets-03.js | 42 +
.../client/shared/test/browser_html_tooltip-01.js | 78 +
.../client/shared/test/browser_html_tooltip-02.js | 227 ++
.../client/shared/test/browser_html_tooltip-03.js | 96 +
.../client/shared/test/browser_html_tooltip-04.js | 100 +
.../client/shared/test/browser_html_tooltip-05.js | 101 +
.../shared/test/browser_html_tooltip_arrow-01.js | 86 +
.../shared/test/browser_html_tooltip_arrow-02.js | 83 +
.../test/browser_html_tooltip_consecutive-show.js | 70 +
.../test/browser_html_tooltip_doorhanger-01.js | 79 +
.../test/browser_html_tooltip_doorhanger-02.js | 76 +
.../test/browser_html_tooltip_height-auto.js | 108 +
.../shared/test/browser_html_tooltip_hover.js | 65 +
.../shared/test/browser_html_tooltip_offset.js | 97 +
.../shared/test/browser_html_tooltip_resize.js | 97 +
.../client/shared/test/browser_html_tooltip_rtl.js | 226 ++
.../test/browser_html_tooltip_screen_edge.js | 74 +
.../test/browser_html_tooltip_variable-height.js | 77 +
.../shared/test/browser_html_tooltip_width-auto.js | 53 +
.../test/browser_html_tooltip_xul-wrapper.js | 79 +
.../shared/test/browser_html_tooltip_zoom.js | 74 +
.../shared/test/browser_inplace-editor-01.js | 202 ++
.../shared/test/browser_inplace-editor-02.js | 80 +
...browser_inplace-editor_autoclose_parentheses.js | 77 +
.../test/browser_inplace-editor_autocomplete_01.js | 79 +
.../test/browser_inplace-editor_autocomplete_02.js | 78 +
...ser_inplace-editor_autocomplete_css_variable.js | 104 +
.../browser_inplace-editor_autocomplete_offset.js | 115 +
.../browser_inplace-editor_focus_closest_editor.js | 180 ++
.../shared/test/browser_inplace-editor_maxwidth.js | 138 ++
.../test/browser_inplace-editor_stop_on_key.js | 219 ++
.../client/shared/test/browser_key_shortcuts.js | 468 ++++
devtools/client/shared/test/browser_keycodes.js | 12 +
.../client/shared/test/browser_layoutHelpers.js | 131 ++
.../test/browser_layoutHelpers_getBoxQuads1.js | 353 +++
.../test/browser_layoutHelpers_getBoxQuads2.js | 185 ++
devtools/client/shared/test/browser_link.js | 40 +
devtools/client/shared/test/browser_num-l10n.js | 70 +
.../client/shared/test/browser_outputparser.js | 856 +++++++
devtools/client/shared/test/browser_prefs-01.js | 53 +
devtools/client/shared/test/browser_prefs-02.js | 67 +
devtools/client/shared/test/browser_require_raw.js | 23 +
devtools/client/shared/test/browser_spectrum.js | 518 +++++
.../shared/test/browser_tableWidget_basic.js | 448 ++++
.../browser_tableWidget_keyboard_interaction.js | 202 ++
.../test/browser_tableWidget_mouse_interaction.js | 359 +++
.../test/browser_telemetry_button_eyedropper.js | 39 +
.../test/browser_telemetry_button_responsive.js | 108 +
.../client/shared/test/browser_telemetry_misc.js | 51 +
.../shared/test/browser_telemetry_sidebar.js | 233 ++
.../shared/test/browser_telemetry_toolbox.js | 34 +
.../browser_telemetry_toolboxtabs_inspector.js | 39 +
.../browser_telemetry_toolboxtabs_jsdebugger.js | 39 +
.../browser_telemetry_toolboxtabs_jsprofiler.js | 39 +
.../browser_telemetry_toolboxtabs_netmonitor.js | 39 +
.../test/browser_telemetry_toolboxtabs_options.js | 34 +
.../test/browser_telemetry_toolboxtabs_storage.js | 34 +
.../browser_telemetry_toolboxtabs_styleeditor.js | 39 +
.../browser_telemetry_toolboxtabs_webconsole.js | 39 +
devtools/client/shared/test/browser_theme.js | 145 ++
.../client/shared/test/browser_theme_switching.js | 58 +
.../client/shared/test/browser_treeWidget_basic.js | 391 ++++
.../browser_treeWidget_keyboard_interaction.js | 291 +++
.../test/browser_treeWidget_mouse_interaction.js | 185 ++
.../code_WorkerTargetActor.attachThread-worker.js | 18 +
.../client/shared/test/code_listworkers-worker1.js | 3 +
.../client/shared/test/code_listworkers-worker2.js | 3 +
.../doc_WorkerTargetActor.attachThread-tab.html | 8 +
.../client/shared/test/doc_cubic-bezier-01.html | 1 +
.../client/shared/test/doc_cubic-bezier-02.html | 3 +
devtools/client/shared/test/doc_empty-tab-01.html | 14 +
devtools/client/shared/test/doc_empty-tab-02.html | 14 +
.../client/shared/test/doc_event-listeners-01.html | 45 +
.../client/shared/test/doc_event-listeners-03.html | 65 +
.../client/shared/test/doc_filter-editor-01.html | 1 +
.../client/shared/test/doc_html_tooltip-02.xhtml | 15 +
.../client/shared/test/doc_html_tooltip-03.xhtml | 19 +
.../client/shared/test/doc_html_tooltip-04.xhtml | 15 +
.../client/shared/test/doc_html_tooltip-05.xhtml | 12 +
devtools/client/shared/test/doc_html_tooltip.xhtml | 12 +
.../shared/test/doc_html_tooltip_arrow-01.xhtml | 90 +
.../shared/test/doc_html_tooltip_arrow-02.xhtml | 65 +
.../test/doc_html_tooltip_doorhanger-01.xhtml | 73 +
.../test/doc_html_tooltip_doorhanger-02.xhtml | 34 +
.../shared/test/doc_html_tooltip_hover.xhtml | 13 +
.../client/shared/test/doc_html_tooltip_rtl.xhtml | 14 +
.../doc_inplace-editor_autocomplete_offset.xhtml | 7 +
devtools/client/shared/test/doc_layoutHelpers.html | 31 +
.../test/doc_layoutHelpers_getBoxQuads1.html | 65 +
.../test/doc_layoutHelpers_getBoxQuads2-a.html | 20 +
.../doc_layoutHelpers_getBoxQuads2-b-and-d.html | 29 +
.../doc_layoutHelpers_getBoxQuads2-c-and-e.html | 27 +
.../client/shared/test/doc_listworkers-tab.html | 8 +
.../shared/test/doc_native-event-handler.html | 25 +
.../shared/test/doc_script-switching-01.html | 18 +
.../shared/test/doc_script-switching-02.html | 18 +
devtools/client/shared/test/doc_spectrum.html | 2 +
.../client/shared/test/doc_tableWidget_basic.html | 7 +
.../doc_tableWidget_keyboard_interaction.xhtml | 8 +
.../test/doc_tableWidget_mouse_interaction.xhtml | 7 +
.../client/shared/test/doc_templater_basic.html | 12 +
devtools/client/shared/test/dummy.html | 1 +
devtools/client/shared/test/head.js | 211 ++
devtools/client/shared/test/helper_color_data.js | 1499 +++++++++++++
devtools/client/shared/test/helper_html_tooltip.js | 116 +
.../client/shared/test/helper_inplace_editor.js | 164 ++
.../client/shared/test/highlighter-test-actor.js | 939 ++++++++
devtools/client/shared/test/leakhunt.js | 173 ++
devtools/client/shared/test/shared-head.js | 2324 ++++++++++++++++++++
.../client/shared/test/telemetry-test-helpers.js | 273 +++
devtools/client/shared/test/test-mocked-module.js | 11 +
devtools/client/shared/test/testactors.js | 27 +
devtools/client/shared/test/xpcshell/.eslintrc.js | 6 +
devtools/client/shared/test/xpcshell/head.js | 10 +
.../test_VariablesView_getString_promise.js | 81 +
.../client/shared/test/xpcshell/test_WeakMapMap.js | 69 +
.../shared/test/xpcshell/test_advanceValidate.js | 33 +
.../test/xpcshell/test_attribute-parsing-01.js | 77 +
.../test/xpcshell/test_attribute-parsing-02.js | 148 ++
.../shared/test/xpcshell/test_bezierCanvas.js | 122 +
.../client/shared/test/xpcshell/test_classnames.js | 53 +
.../client/shared/test/xpcshell/test_cssAngle.js | 32 +
.../shared/test/xpcshell/test_cssColor-01.js | 75 +
.../shared/test/xpcshell/test_cssColor-02.js | 50 +
.../test/xpcshell/test_cssColor-8-digit-hex.js | 20 +
.../shared/test/xpcshell/test_cssColorDatabase.js | 17 +
.../shared/test/xpcshell/test_cubicBezier.js | 152 ++
devtools/client/shared/test/xpcshell/test_curl.js | 397 ++++
.../shared/test/xpcshell/test_escapeCSSComment.js | 41 +
.../shared/test/xpcshell/test_hasCSSVariable.js | 60 +
.../shared/test/xpcshell/test_linearEasing.js | 217 ++
.../shared/test/xpcshell/test_parseDeclarations.js | 1641 ++++++++++++++
.../test_parsePseudoClassesAndAttributes.js | 202 ++
.../shared/test/xpcshell/test_parseSingleValue.js | 106 +
.../test/xpcshell/test_rewriteDeclarations.js | 816 +++++++
.../shared/test/xpcshell/test_source-utils.js | 249 +++
.../shared/test/xpcshell/test_suggestion-picker.js | 147 ++
.../client/shared/test/xpcshell/test_undoStack.js | 88 +
.../shared/test/xpcshell/test_unicode-url.js | 258 +++
devtools/client/shared/test/xpcshell/xpcshell.toml | 57 +
177 files changed, 24680 insertions(+)
create mode 100644 devtools/client/shared/test/addons/test-addon-1/manifest.json
create mode 100644 devtools/client/shared/test/addons/test-addon-2/manifest.json
create mode 100644 devtools/client/shared/test/browser.toml
create mode 100644 devtools/client/shared/test/browser_autocomplete_popup.js
create mode 100644 devtools/client/shared/test/browser_autocomplete_popup_consecutive-show.js
create mode 100644 devtools/client/shared/test/browser_autocomplete_popup_input.js
create mode 100644 devtools/client/shared/test/browser_browserloader_mocks.js
create mode 100644 devtools/client/shared/test/browser_css_angle.js
create mode 100644 devtools/client/shared/test/browser_css_color.js
create mode 100644 devtools/client/shared/test/browser_cubic-bezier-01.js
create mode 100644 devtools/client/shared/test/browser_cubic-bezier-02.js
create mode 100644 devtools/client/shared/test/browser_cubic-bezier-03.js
create mode 100644 devtools/client/shared/test/browser_cubic-bezier-04.js
create mode 100644 devtools/client/shared/test/browser_cubic-bezier-05.js
create mode 100644 devtools/client/shared/test/browser_cubic-bezier-06.js
create mode 100644 devtools/client/shared/test/browser_cubic-bezier-07.js
create mode 100644 devtools/client/shared/test/browser_dbg_globalactor.js
create mode 100644 devtools/client/shared/test/browser_dbg_listaddons.js
create mode 100644 devtools/client/shared/test/browser_dbg_listtabs-01.js
create mode 100644 devtools/client/shared/test/browser_dbg_listtabs-02.js
create mode 100644 devtools/client/shared/test/browser_dbg_listworkers.js
create mode 100644 devtools/client/shared/test/browser_dbg_multiple-windows.js
create mode 100644 devtools/client/shared/test/browser_dbg_target-scoped-actor-01.js
create mode 100644 devtools/client/shared/test/browser_dbg_target-scoped-actor-02.js
create mode 100644 devtools/client/shared/test/browser_devices.js
create mode 100644 devtools/client/shared/test/browser_filter-editor-01.js
create mode 100644 devtools/client/shared/test/browser_filter-editor-02.js
create mode 100644 devtools/client/shared/test/browser_filter-editor-03.js
create mode 100644 devtools/client/shared/test/browser_filter-editor-04.js
create mode 100644 devtools/client/shared/test/browser_filter-editor-05.js
create mode 100644 devtools/client/shared/test/browser_filter-editor-06.js
create mode 100644 devtools/client/shared/test/browser_filter-editor-07.js
create mode 100644 devtools/client/shared/test/browser_filter-editor-08.js
create mode 100644 devtools/client/shared/test/browser_filter-editor-09.js
create mode 100644 devtools/client/shared/test/browser_filter-editor-10.js
create mode 100644 devtools/client/shared/test/browser_filter-presets-01.js
create mode 100644 devtools/client/shared/test/browser_filter-presets-02.js
create mode 100644 devtools/client/shared/test/browser_filter-presets-03.js
create mode 100644 devtools/client/shared/test/browser_html_tooltip-01.js
create mode 100644 devtools/client/shared/test/browser_html_tooltip-02.js
create mode 100644 devtools/client/shared/test/browser_html_tooltip-03.js
create mode 100644 devtools/client/shared/test/browser_html_tooltip-04.js
create mode 100644 devtools/client/shared/test/browser_html_tooltip-05.js
create mode 100644 devtools/client/shared/test/browser_html_tooltip_arrow-01.js
create mode 100644 devtools/client/shared/test/browser_html_tooltip_arrow-02.js
create mode 100644 devtools/client/shared/test/browser_html_tooltip_consecutive-show.js
create mode 100644 devtools/client/shared/test/browser_html_tooltip_doorhanger-01.js
create mode 100644 devtools/client/shared/test/browser_html_tooltip_doorhanger-02.js
create mode 100644 devtools/client/shared/test/browser_html_tooltip_height-auto.js
create mode 100644 devtools/client/shared/test/browser_html_tooltip_hover.js
create mode 100644 devtools/client/shared/test/browser_html_tooltip_offset.js
create mode 100644 devtools/client/shared/test/browser_html_tooltip_resize.js
create mode 100644 devtools/client/shared/test/browser_html_tooltip_rtl.js
create mode 100644 devtools/client/shared/test/browser_html_tooltip_screen_edge.js
create mode 100644 devtools/client/shared/test/browser_html_tooltip_variable-height.js
create mode 100644 devtools/client/shared/test/browser_html_tooltip_width-auto.js
create mode 100644 devtools/client/shared/test/browser_html_tooltip_xul-wrapper.js
create mode 100644 devtools/client/shared/test/browser_html_tooltip_zoom.js
create mode 100644 devtools/client/shared/test/browser_inplace-editor-01.js
create mode 100644 devtools/client/shared/test/browser_inplace-editor-02.js
create mode 100644 devtools/client/shared/test/browser_inplace-editor_autoclose_parentheses.js
create mode 100644 devtools/client/shared/test/browser_inplace-editor_autocomplete_01.js
create mode 100644 devtools/client/shared/test/browser_inplace-editor_autocomplete_02.js
create mode 100644 devtools/client/shared/test/browser_inplace-editor_autocomplete_css_variable.js
create mode 100644 devtools/client/shared/test/browser_inplace-editor_autocomplete_offset.js
create mode 100644 devtools/client/shared/test/browser_inplace-editor_focus_closest_editor.js
create mode 100644 devtools/client/shared/test/browser_inplace-editor_maxwidth.js
create mode 100644 devtools/client/shared/test/browser_inplace-editor_stop_on_key.js
create mode 100644 devtools/client/shared/test/browser_key_shortcuts.js
create mode 100644 devtools/client/shared/test/browser_keycodes.js
create mode 100644 devtools/client/shared/test/browser_layoutHelpers.js
create mode 100644 devtools/client/shared/test/browser_layoutHelpers_getBoxQuads1.js
create mode 100644 devtools/client/shared/test/browser_layoutHelpers_getBoxQuads2.js
create mode 100644 devtools/client/shared/test/browser_link.js
create mode 100644 devtools/client/shared/test/browser_num-l10n.js
create mode 100644 devtools/client/shared/test/browser_outputparser.js
create mode 100644 devtools/client/shared/test/browser_prefs-01.js
create mode 100644 devtools/client/shared/test/browser_prefs-02.js
create mode 100644 devtools/client/shared/test/browser_require_raw.js
create mode 100644 devtools/client/shared/test/browser_spectrum.js
create mode 100644 devtools/client/shared/test/browser_tableWidget_basic.js
create mode 100644 devtools/client/shared/test/browser_tableWidget_keyboard_interaction.js
create mode 100644 devtools/client/shared/test/browser_tableWidget_mouse_interaction.js
create mode 100644 devtools/client/shared/test/browser_telemetry_button_eyedropper.js
create mode 100644 devtools/client/shared/test/browser_telemetry_button_responsive.js
create mode 100644 devtools/client/shared/test/browser_telemetry_misc.js
create mode 100644 devtools/client/shared/test/browser_telemetry_sidebar.js
create mode 100644 devtools/client/shared/test/browser_telemetry_toolbox.js
create mode 100644 devtools/client/shared/test/browser_telemetry_toolboxtabs_inspector.js
create mode 100644 devtools/client/shared/test/browser_telemetry_toolboxtabs_jsdebugger.js
create mode 100644 devtools/client/shared/test/browser_telemetry_toolboxtabs_jsprofiler.js
create mode 100644 devtools/client/shared/test/browser_telemetry_toolboxtabs_netmonitor.js
create mode 100644 devtools/client/shared/test/browser_telemetry_toolboxtabs_options.js
create mode 100644 devtools/client/shared/test/browser_telemetry_toolboxtabs_storage.js
create mode 100644 devtools/client/shared/test/browser_telemetry_toolboxtabs_styleeditor.js
create mode 100644 devtools/client/shared/test/browser_telemetry_toolboxtabs_webconsole.js
create mode 100644 devtools/client/shared/test/browser_theme.js
create mode 100644 devtools/client/shared/test/browser_theme_switching.js
create mode 100644 devtools/client/shared/test/browser_treeWidget_basic.js
create mode 100644 devtools/client/shared/test/browser_treeWidget_keyboard_interaction.js
create mode 100644 devtools/client/shared/test/browser_treeWidget_mouse_interaction.js
create mode 100644 devtools/client/shared/test/code_WorkerTargetActor.attachThread-worker.js
create mode 100644 devtools/client/shared/test/code_listworkers-worker1.js
create mode 100644 devtools/client/shared/test/code_listworkers-worker2.js
create mode 100644 devtools/client/shared/test/doc_WorkerTargetActor.attachThread-tab.html
create mode 100644 devtools/client/shared/test/doc_cubic-bezier-01.html
create mode 100644 devtools/client/shared/test/doc_cubic-bezier-02.html
create mode 100644 devtools/client/shared/test/doc_empty-tab-01.html
create mode 100644 devtools/client/shared/test/doc_empty-tab-02.html
create mode 100644 devtools/client/shared/test/doc_event-listeners-01.html
create mode 100644 devtools/client/shared/test/doc_event-listeners-03.html
create mode 100644 devtools/client/shared/test/doc_filter-editor-01.html
create mode 100644 devtools/client/shared/test/doc_html_tooltip-02.xhtml
create mode 100644 devtools/client/shared/test/doc_html_tooltip-03.xhtml
create mode 100644 devtools/client/shared/test/doc_html_tooltip-04.xhtml
create mode 100644 devtools/client/shared/test/doc_html_tooltip-05.xhtml
create mode 100644 devtools/client/shared/test/doc_html_tooltip.xhtml
create mode 100644 devtools/client/shared/test/doc_html_tooltip_arrow-01.xhtml
create mode 100644 devtools/client/shared/test/doc_html_tooltip_arrow-02.xhtml
create mode 100644 devtools/client/shared/test/doc_html_tooltip_doorhanger-01.xhtml
create mode 100644 devtools/client/shared/test/doc_html_tooltip_doorhanger-02.xhtml
create mode 100644 devtools/client/shared/test/doc_html_tooltip_hover.xhtml
create mode 100644 devtools/client/shared/test/doc_html_tooltip_rtl.xhtml
create mode 100644 devtools/client/shared/test/doc_inplace-editor_autocomplete_offset.xhtml
create mode 100644 devtools/client/shared/test/doc_layoutHelpers.html
create mode 100644 devtools/client/shared/test/doc_layoutHelpers_getBoxQuads1.html
create mode 100644 devtools/client/shared/test/doc_layoutHelpers_getBoxQuads2-a.html
create mode 100644 devtools/client/shared/test/doc_layoutHelpers_getBoxQuads2-b-and-d.html
create mode 100644 devtools/client/shared/test/doc_layoutHelpers_getBoxQuads2-c-and-e.html
create mode 100644 devtools/client/shared/test/doc_listworkers-tab.html
create mode 100644 devtools/client/shared/test/doc_native-event-handler.html
create mode 100644 devtools/client/shared/test/doc_script-switching-01.html
create mode 100644 devtools/client/shared/test/doc_script-switching-02.html
create mode 100644 devtools/client/shared/test/doc_spectrum.html
create mode 100644 devtools/client/shared/test/doc_tableWidget_basic.html
create mode 100644 devtools/client/shared/test/doc_tableWidget_keyboard_interaction.xhtml
create mode 100644 devtools/client/shared/test/doc_tableWidget_mouse_interaction.xhtml
create mode 100644 devtools/client/shared/test/doc_templater_basic.html
create mode 100644 devtools/client/shared/test/dummy.html
create mode 100644 devtools/client/shared/test/head.js
create mode 100644 devtools/client/shared/test/helper_color_data.js
create mode 100644 devtools/client/shared/test/helper_html_tooltip.js
create mode 100644 devtools/client/shared/test/helper_inplace_editor.js
create mode 100644 devtools/client/shared/test/highlighter-test-actor.js
create mode 100644 devtools/client/shared/test/leakhunt.js
create mode 100644 devtools/client/shared/test/shared-head.js
create mode 100644 devtools/client/shared/test/telemetry-test-helpers.js
create mode 100644 devtools/client/shared/test/test-mocked-module.js
create mode 100644 devtools/client/shared/test/testactors.js
create mode 100644 devtools/client/shared/test/xpcshell/.eslintrc.js
create mode 100644 devtools/client/shared/test/xpcshell/head.js
create mode 100644 devtools/client/shared/test/xpcshell/test_VariablesView_getString_promise.js
create mode 100644 devtools/client/shared/test/xpcshell/test_WeakMapMap.js
create mode 100644 devtools/client/shared/test/xpcshell/test_advanceValidate.js
create mode 100644 devtools/client/shared/test/xpcshell/test_attribute-parsing-01.js
create mode 100644 devtools/client/shared/test/xpcshell/test_attribute-parsing-02.js
create mode 100644 devtools/client/shared/test/xpcshell/test_bezierCanvas.js
create mode 100644 devtools/client/shared/test/xpcshell/test_classnames.js
create mode 100644 devtools/client/shared/test/xpcshell/test_cssAngle.js
create mode 100644 devtools/client/shared/test/xpcshell/test_cssColor-01.js
create mode 100644 devtools/client/shared/test/xpcshell/test_cssColor-02.js
create mode 100644 devtools/client/shared/test/xpcshell/test_cssColor-8-digit-hex.js
create mode 100644 devtools/client/shared/test/xpcshell/test_cssColorDatabase.js
create mode 100644 devtools/client/shared/test/xpcshell/test_cubicBezier.js
create mode 100644 devtools/client/shared/test/xpcshell/test_curl.js
create mode 100644 devtools/client/shared/test/xpcshell/test_escapeCSSComment.js
create mode 100644 devtools/client/shared/test/xpcshell/test_hasCSSVariable.js
create mode 100644 devtools/client/shared/test/xpcshell/test_linearEasing.js
create mode 100644 devtools/client/shared/test/xpcshell/test_parseDeclarations.js
create mode 100644 devtools/client/shared/test/xpcshell/test_parsePseudoClassesAndAttributes.js
create mode 100644 devtools/client/shared/test/xpcshell/test_parseSingleValue.js
create mode 100644 devtools/client/shared/test/xpcshell/test_rewriteDeclarations.js
create mode 100644 devtools/client/shared/test/xpcshell/test_source-utils.js
create mode 100644 devtools/client/shared/test/xpcshell/test_suggestion-picker.js
create mode 100644 devtools/client/shared/test/xpcshell/test_undoStack.js
create mode 100644 devtools/client/shared/test/xpcshell/test_unicode-url.js
create mode 100644 devtools/client/shared/test/xpcshell/xpcshell.toml
(limited to 'devtools/client/shared/test')
diff --git a/devtools/client/shared/test/addons/test-addon-1/manifest.json b/devtools/client/shared/test/addons/test-addon-1/manifest.json
new file mode 100644
index 0000000000..2d6ec55940
--- /dev/null
+++ b/devtools/client/shared/test/addons/test-addon-1/manifest.json
@@ -0,0 +1,10 @@
+{
+ "manifest_version": 2,
+ "name": "test-addon-1",
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "test-addon-1@mozilla.org"
+ }
+ }
+}
diff --git a/devtools/client/shared/test/addons/test-addon-2/manifest.json b/devtools/client/shared/test/addons/test-addon-2/manifest.json
new file mode 100644
index 0000000000..7d5e2d54ae
--- /dev/null
+++ b/devtools/client/shared/test/addons/test-addon-2/manifest.json
@@ -0,0 +1,10 @@
+{
+ "manifest_version": 2,
+ "name": "test-addon-2",
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "test-addon-2@mozilla.org"
+ }
+ }
+}
diff --git a/devtools/client/shared/test/browser.toml b/devtools/client/shared/test/browser.toml
new file mode 100644
index 0000000000..4c55c7fee9
--- /dev/null
+++ b/devtools/client/shared/test/browser.toml
@@ -0,0 +1,323 @@
+[DEFAULT]
+tags = "devtools"
+subsuite = "devtools"
+support-files = [
+ "addons/*",
+ "code_listworkers-worker1.js",
+ "code_listworkers-worker2.js",
+ "code_WorkerTargetActor.attachThread-worker.js",
+ "doc_cubic-bezier-01.html",
+ "doc_cubic-bezier-02.html",
+ "doc_empty-tab-01.html",
+ "doc_empty-tab-02.html",
+ "doc_filter-editor-01.html",
+ "doc_html_tooltip-02.xhtml",
+ "doc_html_tooltip-03.xhtml",
+ "doc_html_tooltip-04.xhtml",
+ "doc_html_tooltip-05.xhtml",
+ "doc_html_tooltip.xhtml",
+ "doc_html_tooltip_arrow-01.xhtml",
+ "doc_html_tooltip_arrow-02.xhtml",
+ "doc_html_tooltip_doorhanger-01.xhtml",
+ "doc_html_tooltip_doorhanger-02.xhtml",
+ "doc_html_tooltip_hover.xhtml",
+ "doc_html_tooltip_rtl.xhtml",
+ "doc_inplace-editor_autocomplete_offset.xhtml",
+ "doc_layoutHelpers_getBoxQuads1.html",
+ "doc_layoutHelpers_getBoxQuads2-a.html",
+ "doc_layoutHelpers_getBoxQuads2-b-and-d.html",
+ "doc_layoutHelpers_getBoxQuads2-c-and-e.html",
+ "doc_layoutHelpers.html",
+ "doc_listworkers-tab.html",
+ "doc_native-event-handler.html",
+ "doc_script-switching-01.html",
+ "doc_script-switching-02.html",
+ "doc_spectrum.html",
+ "doc_tableWidget_basic.html",
+ "doc_tableWidget_keyboard_interaction.xhtml",
+ "doc_tableWidget_mouse_interaction.xhtml",
+ "doc_templater_basic.html",
+ "doc_WorkerTargetActor.attachThread-tab.html",
+ "dummy.html",
+ "head.js",
+ "helper_color_data.js",
+ "helper_html_tooltip.js",
+ "helper_inplace_editor.js",
+ "highlighter-test-actor.js",
+ "leakhunt.js",
+ "shared-head.js",
+ "telemetry-test-helpers.js",
+ "test-mocked-module.js",
+ "testactors.js",
+ "!/devtools/client/debugger/test/mochitest/shared-head.js",
+ "!/gfx/layers/apz/test/mochitest/apz_test_utils.js",
+]
+
+["browser_autocomplete_popup.js"]
+
+["browser_autocomplete_popup_consecutive-show.js"]
+
+["browser_autocomplete_popup_input.js"]
+
+["browser_browserloader_mocks.js"]
+
+["browser_css_angle.js"]
+
+["browser_css_color.js"]
+
+["browser_cubic-bezier-01.js"]
+
+["browser_cubic-bezier-02.js"]
+skip-if = [
+ "apple_catalina", # Bug 1713158
+ "os == 'linux' && !asan && !debug && !swgl && !ccov", # Bug 1721159
+]
+
+["browser_cubic-bezier-03.js"]
+
+["browser_cubic-bezier-04.js"]
+
+["browser_cubic-bezier-05.js"]
+
+["browser_cubic-bezier-06.js"]
+skip-if = [
+ "apple_catalina", # Bug 1713158
+ "os == 'linux' && !asan && !debug && !swgl && !ccov", # Bug 1721159
+]
+
+["browser_cubic-bezier-07.js"]
+tags = "addons"
+
+["browser_dbg_listaddons.js"]
+skip-if = ["debug"]
+tags = "addons"
+
+["browser_dbg_listtabs-01.js"]
+
+["browser_dbg_listtabs-02.js"]
+skip-if = ["true"] # Never worked for remote frames, needs a mock DevToolsServerConnection
+
+["browser_dbg_listworkers.js"]
+
+["browser_dbg_multiple-windows.js"]
+
+["browser_dbg_target-scoped-actor-01.js"]
+
+["browser_dbg_target-scoped-actor-02.js"]
+
+["browser_devices.js"]
+skip-if = ["verify"]
+
+["browser_filter-editor-01.js"]
+
+["browser_filter-editor-02.js"]
+
+["browser_filter-editor-03.js"]
+
+["browser_filter-editor-04.js"]
+
+["browser_filter-editor-05.js"]
+
+["browser_filter-editor-06.js"]
+
+["browser_filter-editor-07.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_filter-editor-08.js"]
+
+["browser_filter-editor-09.js"]
+
+["browser_filter-editor-10.js"]
+
+["browser_filter-presets-01.js"]
+
+["browser_filter-presets-02.js"]
+
+["browser_filter-presets-03.js"]
+
+["browser_html_tooltip-01.js"]
+
+["browser_html_tooltip-02.js"]
+skip-if = [
+ "apple_catalina", # Bug 1713158
+ "os == 'linux' && !asan && !debug && !swgl && !ccov", # Bug 1721159
+ "a11y_checks && debug", # Bugs 1849028 and 1858041 for causing intermittent test results
+]
+
+["browser_html_tooltip-03.js"]
+skip-if = [
+ "apple_catalina", # Bug 1713158
+ "os == 'linux' && !asan && !debug && !swgl && !ccov", # Bug 1721159
+]
+
+["browser_html_tooltip-04.js"]
+skip-if = ["os == 'linux' && !asan && !debug && !swgl && !ccov"] # Bug 1721159
+
+["browser_html_tooltip-05.js"]
+
+["browser_html_tooltip_arrow-01.js"]
+skip-if = [
+ "apple_catalina", # Bug 1713158
+ "os == 'linux' && !asan && !debug && !swgl && !ccov", # Bug 1721159
+]
+
+["browser_html_tooltip_arrow-02.js"]
+skip-if = [
+ "apple_catalina", # Bug 1713158
+ "os == 'linux' && !asan && !debug && !swgl && !ccov", # Bug 1721159
+]
+
+["browser_html_tooltip_consecutive-show.js"]
+
+["browser_html_tooltip_doorhanger-01.js"]
+
+["browser_html_tooltip_doorhanger-02.js"]
+
+["browser_html_tooltip_height-auto.js"]
+
+["browser_html_tooltip_hover.js"]
+
+["browser_html_tooltip_offset.js"]
+skip-if = [
+ "apple_catalina", # Bug 1713158
+ "os == 'linux' && !asan && !debug && !swgl && !ccov", # Bug 1721159
+]
+
+["browser_html_tooltip_resize.js"]
+
+["browser_html_tooltip_rtl.js"]
+
+["browser_html_tooltip_screen_edge.js"]
+
+["browser_html_tooltip_variable-height.js"]
+skip-if = [
+ "apple_catalina", # Bug 1713158
+ "os == 'linux' && !asan && !debug && !swgl && !ccov", # Bug 1721159
+]
+
+["browser_html_tooltip_width-auto.js"]
+skip-if = [
+ "apple_catalina", # Bug 1713158
+ "os == 'linux' && !asan && !debug && !swgl && !ccov", # Bug 1721159
+]
+
+["browser_html_tooltip_xul-wrapper.js"]
+
+["browser_html_tooltip_zoom.js"]
+
+["browser_inplace-editor-01.js"]
+
+["browser_inplace-editor-02.js"]
+skip-if = [
+ "apple_catalina", # Bug 1713158
+ "os == 'linux' && !asan && !debug && !swgl && !ccov", # Bug 1721159
+]
+
+["browser_inplace-editor_autoclose_parentheses.js"]
+
+["browser_inplace-editor_autocomplete_01.js"]
+skip-if = [
+ "apple_catalina", # Bug 1713158
+ "os == 'linux' && !asan && !debug && !swgl && !ccov", # Bug 1721159
+]
+
+["browser_inplace-editor_autocomplete_02.js"]
+
+["browser_inplace-editor_autocomplete_css_variable.js"]
+skip-if = [
+ "apple_catalina", # Bug 1713158
+ "os == 'linux' && !asan && !debug && !swgl && !ccov", # Bug 1721159
+]
+
+["browser_inplace-editor_autocomplete_offset.js"]
+skip-if = [
+ "apple_catalina", # Bug 1713158
+ "os == 'linux' && !asan && !debug && !swgl && !ccov", # Bug 1721159
+]
+
+["browser_inplace-editor_focus_closest_editor.js"]
+
+["browser_inplace-editor_maxwidth.js"]
+skip-if = [
+ "apple_catalina", # Bug 1713158
+ "os == 'linux' && !asan && !debug && !swgl && !ccov", # Bug 1721159
+]
+
+["browser_inplace-editor_stop_on_key.js"]
+
+["browser_key_shortcuts.js"]
+
+["browser_keycodes.js"]
+
+["browser_layoutHelpers.js"]
+
+["browser_layoutHelpers_getBoxQuads1.js"]
+skip-if = [
+ "verify",
+]
+
+["browser_layoutHelpers_getBoxQuads2.js"]
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+]
+
+["browser_link.js"]
+
+["browser_num-l10n.js"]
+
+["browser_outputparser.js"]
+
+["browser_prefs-01.js"]
+
+["browser_prefs-02.js"]
+
+["browser_require_raw.js"]
+
+["browser_spectrum.js"]
+
+["browser_tableWidget_basic.js"]
+
+["browser_tableWidget_keyboard_interaction.js"]
+
+["browser_tableWidget_mouse_interaction.js"]
+skip-if = ["(os == 'linux' && os_version == '18.04' && bits == 64) && (!debug && !asan)"] #Bug 1118592
+
+["browser_telemetry_button_eyedropper.js"]
+
+["browser_telemetry_button_responsive.js"]
+skip-if = ["os == 'win'"] # Win: bug 1404197
+
+["browser_telemetry_misc.js"]
+
+["browser_telemetry_sidebar.js"]
+
+["browser_telemetry_toolbox.js"]
+
+["browser_telemetry_toolboxtabs_inspector.js"]
+
+["browser_telemetry_toolboxtabs_jsdebugger.js"]
+
+["browser_telemetry_toolboxtabs_jsprofiler.js"]
+
+["browser_telemetry_toolboxtabs_netmonitor.js"]
+
+["browser_telemetry_toolboxtabs_options.js"]
+
+["browser_telemetry_toolboxtabs_storage.js"]
+
+["browser_telemetry_toolboxtabs_styleeditor.js"]
+
+["browser_telemetry_toolboxtabs_webconsole.js"]
+
+["browser_theme.js"]
+
+["browser_theme_switching.js"]
+
+["browser_treeWidget_basic.js"]
+
+["browser_treeWidget_keyboard_interaction.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_treeWidget_mouse_interaction.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
diff --git a/devtools/client/shared/test/browser_autocomplete_popup.js b/devtools/client/shared/test/browser_autocomplete_popup.js
new file mode 100644
index 0000000000..d9d057ea1c
--- /dev/null
+++ b/devtools/client/shared/test/browser_autocomplete_popup.js
@@ -0,0 +1,121 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ const AutocompletePopup = require("resource://devtools/client/shared/autocomplete-popup.js");
+
+ info("Create an autocompletion popup");
+ const { doc } = await createHost();
+ const input = doc.createElement("input");
+ doc.body.appendChild(input);
+
+ const autocompleteOptions = {
+ position: "top",
+ autoSelect: true,
+ };
+ const popup = new AutocompletePopup(doc, autocompleteOptions);
+ input.focus();
+
+ const items = [
+ { label: "item0", value: "value0" },
+ { label: "item1", value: "value1" },
+ { label: "item2", value: "value2" },
+ ];
+
+ ok(!popup.isOpen, "popup is not open");
+ ok(!input.hasAttribute("aria-activedescendant"), "no aria-activedescendant");
+
+ const onPopupOpen = popup.once("popup-opened");
+ popup.openPopup(input);
+ await onPopupOpen;
+
+ ok(popup.isOpen, "popup is open");
+ is(popup.itemCount, 0, "no items");
+ ok(!input.hasAttribute("aria-activedescendant"), "no aria-activedescendant");
+
+ popup.setItems(items);
+
+ is(popup.itemCount, items.length, "items added");
+ is(
+ JSON.stringify(popup.getItems()),
+ JSON.stringify(items),
+ "getItems returns back the same items"
+ );
+ is(popup.selectedIndex, 0, "Index of the first item from top is selected.");
+ is(popup.selectedItem, items[0], "First item from top is selected");
+ // Make sure the list containing the active descendant doesn't get rebuilt
+ // when the selected item changes.
+ const listClone = getListFromActiveDescendant(popup, input);
+ checkActiveDescendant(popup, input, listClone);
+
+ popup.selectItemAtIndex(1);
+
+ is(popup.selectedIndex, 1, "index 1 is selected");
+ is(popup.selectedItem, items[1], "item1 is selected");
+ checkActiveDescendant(popup, input, listClone);
+
+ popup.selectedItem = items[2];
+
+ is(popup.selectedIndex, 2, "index 2 is selected");
+ is(popup.selectedItem, items[2], "item2 is selected");
+ checkActiveDescendant(popup, input, listClone);
+
+ is(popup.selectPreviousItem(), items[1], "selectPreviousItem() works");
+
+ is(popup.selectedIndex, 1, "index 1 is selected");
+ is(popup.selectedItem, items[1], "item1 is selected");
+ checkActiveDescendant(popup, input, listClone);
+
+ is(popup.selectNextItem(), items[2], "selectNextItem() works");
+
+ is(popup.selectedIndex, 2, "index 2 is selected");
+ is(popup.selectedItem, items[2], "item2 is selected");
+ checkActiveDescendant(popup, input, listClone);
+
+ ok(popup.selectNextItem(), "selectNextItem() works");
+
+ is(popup.selectedIndex, 0, "index 0 is selected");
+ is(popup.selectedItem, items[0], "item0 is selected");
+ checkActiveDescendant(popup, input, listClone);
+
+ popup.clearItems();
+ is(popup.itemCount, 0, "items cleared");
+ ok(!input.hasAttribute("aria-activedescendant"), "no aria-activedescendant");
+
+ const onPopupClose = popup.once("popup-closed");
+ popup.hidePopup();
+ await onPopupClose;
+});
+
+function stripNS(text) {
+ return text.replace(RegExp(' xmlns="http://www.w3.org/1999/xhtml"', "g"), "");
+}
+
+function getListFromActiveDescendant(popup, input) {
+ const activeElement = input.ownerDocument.activeElement;
+ const descendantId = activeElement.getAttribute("aria-activedescendant");
+ const cloneItem = input.ownerDocument.querySelector("#" + descendantId);
+ return cloneItem.parentNode;
+}
+
+function checkActiveDescendant(popup, input, list) {
+ const activeElement = input.ownerDocument.activeElement;
+ const descendantId = activeElement.getAttribute("aria-activedescendant");
+ const popupItem = popup._tooltip.panel.querySelector("#" + descendantId);
+ const cloneItem = input.ownerDocument.querySelector("#" + descendantId);
+
+ ok(popupItem, "Active descendant is found in the popup list");
+ ok(cloneItem, "Active descendant is found in the list clone");
+ is(
+ cloneItem.parentNode,
+ list,
+ "Active descendant is a child of the expected list"
+ );
+ is(
+ stripNS(popupItem.outerHTML),
+ cloneItem.outerHTML,
+ "Cloned item has the same HTML as the original element"
+ );
+}
diff --git a/devtools/client/shared/test/browser_autocomplete_popup_consecutive-show.js b/devtools/client/shared/test/browser_autocomplete_popup_consecutive-show.js
new file mode 100644
index 0000000000..1af2df3a1f
--- /dev/null
+++ b/devtools/client/shared/test/browser_autocomplete_popup_consecutive-show.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that calling `showPopup` multiple time does not lead to invalid state.
+
+add_task(async function () {
+ const AutocompletePopup = require("resource://devtools/client/shared/autocomplete-popup.js");
+
+ info("Create an autocompletion popup");
+ const { doc } = await createHost();
+ const input = doc.createElement("input");
+ doc.body.appendChild(input);
+
+ const autocompleteOptions = {
+ position: "top",
+ autoSelect: true,
+ useXulWrapper: true,
+ };
+ const popup = new AutocompletePopup(doc, autocompleteOptions);
+ const items = [{ label: "a" }, { label: "b" }, { label: "c" }];
+ popup.setItems(items);
+
+ input.focus();
+
+ let onAllEventsReceived = waitForNEvents(popup, "popup-opened", 3);
+ // Note that the lack of `await` on those function calls are wanted.
+ popup.openPopup(input, 0, 0, 0);
+ popup.openPopup(input, 0, 0, 1);
+ popup.openPopup(input, 0, 0, 2);
+ await onAllEventsReceived;
+
+ ok(popup.isOpen, "popup is open");
+ is(
+ popup.selectedIndex,
+ 2,
+ "Selected index matches the one that was set last when calling openPopup"
+ );
+
+ onAllEventsReceived = waitForNEvents(popup, "popup-opened", 2);
+ // Note that the lack of `await` on those function calls are wanted.
+ popup.openPopup(input, 0, 0, 1);
+ popup.openPopup(input);
+ await onAllEventsReceived;
+
+ ok(popup.isOpen, "popup is open");
+ is(
+ popup.selectedIndex,
+ 0,
+ "First item is selected, as last call to openPopup did not specify an index and autoSelect is true"
+ );
+
+ const onPopupClose = popup.once("popup-closed");
+ popup.hidePopup();
+ await onPopupClose;
+});
diff --git a/devtools/client/shared/test/browser_autocomplete_popup_input.js b/devtools/client/shared/test/browser_autocomplete_popup_input.js
new file mode 100644
index 0000000000..a7d04f1c9c
--- /dev/null
+++ b/devtools/client/shared/test/browser_autocomplete_popup_input.js
@@ -0,0 +1,251 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ // Prevent the URL Bar to steal the focus.
+ const preventUrlBarFocus = e => {
+ e.preventDefault();
+ };
+ window.gURLBar.addEventListener("beforefocus", preventUrlBarFocus);
+ registerCleanupFunction(() => {
+ window.gURLBar.removeEventListener("beforefocus", preventUrlBarFocus);
+ });
+
+ const AutocompletePopup = require("resource://devtools/client/shared/autocomplete-popup.js");
+
+ info("Create an autocompletion popup and an input that will be bound to it");
+ const { doc } = await createHost();
+
+ const input = doc.createElement("input");
+ doc.body.append(input, doc.createElement("input"));
+
+ const onSelectCalled = [];
+ const onClickCalled = [];
+ const popup = new AutocompletePopup(doc, {
+ input,
+ position: "top",
+ autoSelect: true,
+ onSelect: item => onSelectCalled.push(item),
+ onClick: (e, item) => onClickCalled.push(item),
+ });
+
+ input.focus();
+ ok(hasFocus(input), "input has focus");
+
+ info(
+ "Check that Tab moves the focus out of the input when the popup isn't opened"
+ );
+ EventUtils.synthesizeKey("KEY_Tab");
+ is(onClickCalled.length, 0, "onClick wasn't called");
+ is(hasFocus(input), false, "input does not have the focus anymore");
+ info("Set the focus back to the input and open the popup");
+ input.focus();
+ await new Promise(res => setTimeout(res, 0));
+ ok(hasFocus(input), "input is focused");
+
+ await populateAndOpenPopup(popup);
+
+ const checkSelectedItem = (expected, info) =>
+ checkPopupSelectedItem(popup, input, expected, info);
+
+ checkSelectedItem(popupItems[0], "First item from top is selected");
+ is(
+ onSelectCalled[0].label,
+ popupItems[0].label,
+ "onSelect was called with expected param"
+ );
+
+ info("Check that arrow down/up navigates into the list");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ checkSelectedItem(popupItems[1], "item-1 is selected");
+ is(
+ onSelectCalled[1].label,
+ popupItems[1].label,
+ "onSelect was called with expected param"
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ checkSelectedItem(popupItems[2], "item-2 is selected");
+ is(
+ onSelectCalled[2].label,
+ popupItems[2].label,
+ "onSelect was called with expected param"
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ checkSelectedItem(popupItems[0], "item-0 is selected");
+ is(
+ onSelectCalled[3].label,
+ popupItems[0].label,
+ "onSelect was called with expected param"
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ checkSelectedItem(popupItems[2], "item-2 is selected");
+ is(
+ onSelectCalled[4].label,
+ popupItems[2].label,
+ "onSelect was called with expected param"
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ checkSelectedItem(popupItems[1], "item-2 is selected");
+ is(
+ onSelectCalled[5].label,
+ popupItems[1].label,
+ "onSelect was called with expected param"
+ );
+
+ info("Check that Escape closes the popup");
+ let onPopupClosed = popup.once("popup-closed");
+ EventUtils.synthesizeKey("KEY_Escape");
+ await onPopupClosed;
+ ok(true, "popup was closed with Escape key");
+ ok(hasFocus(input), "input still has the focus");
+ is(onClickCalled.length, 0, "onClick wasn't called");
+
+ info("Fill the input");
+ const value = "item";
+ EventUtils.sendString(value);
+ is(input.value, value, "input has the expected value");
+ is(
+ input.selectionStart,
+ value.length,
+ "input cursor is at expected position"
+ );
+ info("Open the popup again");
+ await populateAndOpenPopup(popup);
+
+ info("Check that Arrow Left + Shift does not close the popup");
+ const timeoutRes = "TIMED_OUT";
+ const onRaceEnded = Promise.race([
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(res => setTimeout(() => res(timeoutRes), 500)),
+ popup.once("popup-closed"),
+ ]);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true });
+ const raceResult = await onRaceEnded;
+ is(raceResult, timeoutRes, "popup wasn't closed");
+ ok(popup.isOpen, "popup is still open");
+ is(input.selectionEnd - input.selectionStart, 1, "text was selected");
+ ok(hasFocus(input), "input still has the focus");
+
+ info("Check that Arrow Left closes the popup");
+ onPopupClosed = popup.once("popup-closed");
+ EventUtils.synthesizeKey("KEY_ArrowLeft");
+ await onPopupClosed;
+ is(
+ input.selectionStart,
+ value.length - 1,
+ "input cursor was moved one char back"
+ );
+ is(input.selectionEnd, input.selectionStart, "selection was removed");
+ is(onClickCalled.length, 0, "onClick wasn't called");
+ ok(hasFocus(input), "input still has the focus");
+
+ info("Open the popup again");
+ await populateAndOpenPopup(popup);
+
+ info("Check that Arrow Right + Shift does not trigger onClick");
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true });
+ is(onClickCalled.length, 0, "onClick wasn't called");
+ is(input.selectionEnd - input.selectionStart, 1, "input text was selected");
+ ok(hasFocus(input), "input still has the focus");
+
+ info("Check that Arrow Right triggers onClick");
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ is(onClickCalled.length, 1, "onClick was called");
+ is(
+ onClickCalled[0],
+ popupItems[0],
+ "onClick was called with the selected item"
+ );
+ ok(hasFocus(input), "input still has the focus");
+
+ info("Check that Enter triggers onClick");
+ EventUtils.synthesizeKey("KEY_Enter");
+ is(onClickCalled.length, 2, "onClick was called");
+ is(
+ onClickCalled[1],
+ popupItems[0],
+ "onClick was called with the selected item"
+ );
+ ok(hasFocus(input), "input still has the focus");
+
+ info("Check that Tab triggers onClick");
+ EventUtils.synthesizeKey("KEY_Tab");
+ is(onClickCalled.length, 3, "onClick was called");
+ is(
+ onClickCalled[2],
+ popupItems[0],
+ "onClick was called with the selected item"
+ );
+ ok(hasFocus(input), "input still has the focus");
+
+ info(
+ "Check that Shift+Tab does not trigger onClick and move the focus out of the input"
+ );
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ is(onClickCalled.length, 3, "onClick wasn't called");
+ is(hasFocus(input), false, "input does not have the focus anymore");
+
+ const onPopupClose = popup.once("popup-closed");
+ popup.hidePopup();
+ await onPopupClose;
+});
+
+const popupItems = [
+ { label: "item-0", value: "value-0" },
+ { label: "item-1", value: "value-1" },
+ { label: "item-2", value: "value-2" },
+];
+
+async function populateAndOpenPopup(popup) {
+ popup.setItems(popupItems);
+ await popup.openPopup();
+}
+
+/**
+ * Returns true if the give node is currently focused.
+ */
+function hasFocus(node) {
+ return (
+ node.ownerDocument.activeElement == node && node.ownerDocument.hasFocus()
+ );
+}
+
+/**
+ * Check that the selected item in the popup is the expected one. Also check that the
+ * active descendant is properly set and that the popup has the focus.
+ *
+ * @param {AutocompletePopup} popup
+ * @param {HTMLInput} input
+ * @param {Object} expectedSelectedItem
+ * @param {String} info
+ */
+function checkPopupSelectedItem(popup, input, expectedSelectedItem, info) {
+ is(popup.selectedItem.label, expectedSelectedItem.label, info);
+ checkActiveDescendant(popup, input);
+ ok(hasFocus(input), "input still has the focus");
+}
+
+function checkActiveDescendant(popup, input) {
+ const activeElement = input.ownerDocument.activeElement;
+ const descendantId = activeElement.getAttribute("aria-activedescendant");
+ const popupItem = popup._tooltip.panel.querySelector(`#${descendantId}`);
+ const cloneItem = input.ownerDocument.querySelector(`#${descendantId}`);
+
+ ok(popupItem, "Active descendant is found in the popup list");
+ ok(cloneItem, "Active descendant is found in the list clone");
+ is(
+ stripNS(popupItem.outerHTML),
+ cloneItem.outerHTML,
+ "Cloned item has the same HTML as the original element"
+ );
+}
+
+function stripNS(text) {
+ return text.replace(RegExp(' xmlns="http://www.w3.org/1999/xhtml"', "g"), "");
+}
diff --git a/devtools/client/shared/test/browser_browserloader_mocks.js b/devtools/client/shared/test/browser_browserloader_mocks.js
new file mode 100644
index 0000000000..6cc38259f3
--- /dev/null
+++ b/devtools/client/shared/test/browser_browserloader_mocks.js
@@ -0,0 +1,162 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { BrowserLoader } = ChromeUtils.import(
+ "resource://devtools/shared/loader/browser-loader.js"
+);
+
+const {
+ getMockedModule,
+ setMockedModule,
+ removeMockedModule,
+} = require("resource://devtools/shared/loader/browser-loader-mocks.js");
+const { require: browserRequire } = BrowserLoader({
+ baseURI: "resource://devtools/client/shared/",
+ window,
+});
+
+// Check that modules can be mocked in the browser loader.
+// Test with a custom test module under the chrome:// scheme.
+function testWithChromeScheme() {
+ // Full chrome URI for the test module.
+ const CHROME_URI = CHROME_URL_ROOT + "test-mocked-module.js";
+ const ORIGINAL_VALUE = "Original value";
+ const MOCKED_VALUE_1 = "Mocked value 1";
+ const MOCKED_VALUE_2 = "Mocked value 2";
+
+ const m1 = browserRequire(CHROME_URI);
+ ok(m1, "Regular module can be required");
+ is(m1.methodToMock(), ORIGINAL_VALUE, "Method returns the expected value");
+ is(m1.someProperty, "someProperty", "Property has the expected value");
+
+ info("Create a simple mocked version of the test module");
+ const mockedModule = {
+ methodToMock: () => MOCKED_VALUE_1,
+ someProperty: "somePropertyMocked",
+ };
+ setMockedModule(mockedModule, CHROME_URI);
+ ok(!!getMockedModule(CHROME_URI), "Has an entry for the chrome URI.");
+
+ const m2 = browserRequire(CHROME_URI);
+ ok(m2, "Mocked module can be required via chrome URI");
+ is(
+ m2.methodToMock(),
+ MOCKED_VALUE_1,
+ "Mocked method returns the expected value"
+ );
+ is(
+ m2.someProperty,
+ "somePropertyMocked",
+ "Mocked property has the expected value"
+ );
+
+ const { methodToMock: requiredMethod } = browserRequire(CHROME_URI);
+ Assert.strictEqual(
+ requiredMethod(),
+ MOCKED_VALUE_1,
+ "Mocked method returns the expected value when imported with destructuring"
+ );
+
+ info("Update the mocked method to return a different value");
+ mockedModule.methodToMock = () => MOCKED_VALUE_2;
+ is(
+ requiredMethod(),
+ MOCKED_VALUE_2,
+ "Mocked method returns the updated value when imported with destructuring"
+ );
+
+ info("Remove the mock for the test module");
+ removeMockedModule(CHROME_URI);
+ ok(!getMockedModule(CHROME_URI), "Has no entry for the chrome URI.");
+
+ const m3 = browserRequire(CHROME_URI);
+ ok(m3, "Regular module can be required after removing the mock");
+ is(
+ m3.methodToMock(),
+ ORIGINAL_VALUE,
+ "Method on module returns the expected value"
+ );
+}
+
+// Similar tests as in testWithChromeScheme, but this time with a devtools module
+// available under the resource:// scheme.
+function testWithRegularDevtoolsModule() {
+ // Testing with devtools/shared/path because it is a simple module, that can be imported
+ // with destructuring. Any other module would do.
+ const DEVTOOLS_MODULE_PATH = "devtools/shared/path";
+ const DEVTOOLS_MODULE_URI = "resource://devtools/shared/path.js";
+
+ const m1 = browserRequire(DEVTOOLS_MODULE_PATH);
+ is(
+ m1.joinURI("https://a", "b"),
+ "https://a/b",
+ "Original module was required"
+ );
+
+ info(
+ "Set a mock for a sub-part of the path, which should not match require calls"
+ );
+ setMockedModule({ joinURI: () => "WRONG_PATH" }, "shared/path");
+
+ ok(
+ !getMockedModule(DEVTOOLS_MODULE_URI),
+ "Has no mock entry for the full URI"
+ );
+ const m2 = browserRequire(DEVTOOLS_MODULE_PATH);
+ is(
+ m2.joinURI("https://a", "b"),
+ "https://a/b",
+ "Original module is still required"
+ );
+
+ info(
+ "Set a mock for the complete path, which should now match require calls"
+ );
+ const mockedModule = {
+ joinURI: () => "MOCKED VALUE",
+ };
+ setMockedModule(mockedModule, DEVTOOLS_MODULE_PATH);
+ ok(
+ !!getMockedModule(DEVTOOLS_MODULE_URI),
+ "Has a mock entry for the full URI."
+ );
+
+ const m3 = browserRequire(DEVTOOLS_MODULE_PATH);
+ is(
+ m3.joinURI("https://a", "b"),
+ "MOCKED VALUE",
+ "The mocked module has been returned"
+ );
+
+ info(
+ "Check that the mocked methods can be updated after a destructuring import"
+ );
+ const { joinURI } = browserRequire(DEVTOOLS_MODULE_PATH);
+ mockedModule.joinURI = () => "UPDATED VALUE";
+ is(
+ joinURI("https://a", "b"),
+ "UPDATED VALUE",
+ "Mocked method was correctly updated"
+ );
+
+ removeMockedModule(DEVTOOLS_MODULE_PATH);
+ ok(
+ !getMockedModule(DEVTOOLS_MODULE_URI),
+ "Has no mock entry for the full URI"
+ );
+ const m4 = browserRequire(DEVTOOLS_MODULE_PATH);
+ is(
+ m4.joinURI("https://a", "b"),
+ "https://a/b",
+ "Original module can be required again"
+ );
+}
+
+function test() {
+ testWithChromeScheme();
+ testWithRegularDevtoolsModule();
+ delete window.getBrowserLoaderForWindow;
+ finish();
+}
diff --git a/devtools/client/shared/test/browser_css_angle.js b/devtools/client/shared/test/browser_css_angle.js
new file mode 100644
index 0000000000..a29d3e4f0e
--- /dev/null
+++ b/devtools/client/shared/test/browser_css_angle.js
@@ -0,0 +1,204 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from head.js */
+"use strict";
+
+var { angleUtils } = require("resource://devtools/client/shared/css-angle.js");
+
+add_task(async function () {
+ await addTab("about:blank");
+ const { host } = await createHost("bottom");
+
+ info("Starting the test");
+ testAngleUtils();
+ testAngleValidity();
+
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function testAngleUtils() {
+ const data = getTestData();
+
+ for (const { authored, deg, rad, grad, turn } of data) {
+ const angle = new angleUtils.CssAngle(authored);
+
+ // Check all values.
+ info("Checking values for " + authored);
+ is(angle.deg, deg, "color.deg === deg");
+ is(angle.rad, rad, "color.rad === rad");
+ is(angle.grad, grad, "color.grad === grad");
+ is(angle.turn, turn, "color.turn === turn");
+
+ testToString(angle, deg, rad, grad, turn);
+ }
+}
+
+function testAngleValidity() {
+ const data = getAngleValidityData();
+
+ for (const { angle, result } of data) {
+ const testAngle = new angleUtils.CssAngle(angle);
+ const validString = testAngle.valid ? " a valid" : "an invalid";
+
+ is(
+ testAngle.valid,
+ result,
+ `Testing that "${angle}" is ${validString} angle`
+ );
+ }
+}
+
+function testToString(angle, deg, rad, grad, turn) {
+ const { ANGLEUNIT } = angleUtils.CssAngle.prototype;
+ angle.angleUnit = ANGLEUNIT.deg;
+ is(angle.toString(), deg, "toString() with deg type");
+
+ angle.angleUnit = ANGLEUNIT.rad;
+ is(angle.toString(), rad, "toString() with rad type");
+
+ angle.angleUnit = ANGLEUNIT.grad;
+ is(angle.toString(), grad, "toString() with grad type");
+
+ angle.angleUnit = ANGLEUNIT.turn;
+ is(angle.toString(), turn, "toString() with turn type");
+}
+
+function getAngleValidityData() {
+ return [
+ {
+ angle: "0.2turn",
+ result: true,
+ },
+ {
+ angle: "-0.2turn",
+ result: true,
+ },
+ {
+ angle: "-.2turn",
+ result: true,
+ },
+ {
+ angle: "1e02turn",
+ result: true,
+ },
+ {
+ angle: "-2e2turn",
+ result: true,
+ },
+ {
+ angle: ".2turn",
+ result: true,
+ },
+ {
+ angle: "0.2aaturn",
+ result: false,
+ },
+ {
+ angle: "2dega",
+ result: false,
+ },
+ {
+ angle: "0.deg",
+ result: false,
+ },
+ {
+ angle: ".deg",
+ result: false,
+ },
+ {
+ angle: "..2turn",
+ result: false,
+ },
+ ];
+}
+
+function getTestData() {
+ return [
+ {
+ authored: "0deg",
+ deg: "0deg",
+ rad: "0rad",
+ grad: "0grad",
+ turn: "0turn",
+ },
+ {
+ authored: "180deg",
+ deg: "180deg",
+ rad: `${Math.round(Math.PI * 10000) / 10000}rad`,
+ grad: "200grad",
+ turn: "0.5turn",
+ },
+ {
+ authored: "180DEG",
+ deg: "180DEG",
+ rad: `${Math.round(Math.PI * 10000) / 10000}RAD`,
+ grad: "200GRAD",
+ turn: "0.5TURN",
+ },
+ {
+ authored: `-${Math.PI}rad`,
+ deg: "-180deg",
+ rad: `-${Math.PI}rad`,
+ grad: "-200grad",
+ turn: "-0.5turn",
+ },
+ {
+ authored: `-${Math.PI}RAD`,
+ deg: "-180DEG",
+ rad: `-${Math.PI}RAD`,
+ grad: "-200GRAD",
+ turn: "-0.5TURN",
+ },
+ {
+ authored: "100grad",
+ deg: "90deg",
+ rad: `${Math.round((Math.PI / 2) * 10000) / 10000}rad`,
+ grad: "100grad",
+ turn: "0.25turn",
+ },
+ {
+ authored: "100GRAD",
+ deg: "90DEG",
+ rad: `${Math.round((Math.PI / 2) * 10000) / 10000}RAD`,
+ grad: "100GRAD",
+ turn: "0.25TURN",
+ },
+ {
+ authored: "-1turn",
+ deg: "-360deg",
+ rad: `${(-1 * Math.round(Math.PI * 2 * 10000)) / 10000}rad`,
+ grad: "-400grad",
+ turn: "-1turn",
+ },
+ {
+ authored: "-10TURN",
+ deg: "-3600DEG",
+ rad: `${(-1 * Math.round(Math.PI * 2 * 10 * 10000)) / 10000}RAD`,
+ grad: "-4000GRAD",
+ turn: "-10TURN",
+ },
+ {
+ authored: "inherit",
+ deg: "inherit",
+ rad: "inherit",
+ grad: "inherit",
+ turn: "inherit",
+ },
+ {
+ authored: "initial",
+ deg: "initial",
+ rad: "initial",
+ grad: "initial",
+ turn: "initial",
+ },
+ {
+ authored: "unset",
+ deg: "unset",
+ rad: "unset",
+ grad: "unset",
+ turn: "unset",
+ },
+ ];
+}
diff --git a/devtools/client/shared/test/browser_css_color.js b/devtools/client/shared/test/browser_css_color.js
new file mode 100644
index 0000000000..d66656ac86
--- /dev/null
+++ b/devtools/client/shared/test/browser_css_color.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var { colorUtils } = require("resource://devtools/shared/css/color.js");
+/* global getFixtureColorData */
+loadHelperScript("helper_color_data.js");
+
+add_task(async function () {
+ await addTab("about:blank");
+ const { host, doc } = await createHost("bottom");
+
+ info("Creating a test canvas element to test colors");
+ const canvas = createTestCanvas(doc);
+ info("Starting the test");
+ testColorUtils(canvas);
+
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function createTestCanvas(doc) {
+ const canvas = doc.createElement("canvas");
+ canvas.width = canvas.height = 10;
+ doc.body.appendChild(canvas);
+ return canvas;
+}
+
+function testColorUtils(canvas) {
+ const data = getFixtureColorData();
+
+ for (const { authored, name, hex, hsl, rgb } of data) {
+ const color = new colorUtils.CssColor(authored);
+
+ // Check all values.
+ info("Checking values for " + authored);
+ is(color.name, name, "color.name === name");
+ is(color.hex, hex, "color.hex === hex");
+ is(color.hsl, hsl, "color.hsl === hsl");
+ is(color.rgb, rgb, "color.rgb === rgb");
+
+ testToString(color, name, hex, hsl, rgb);
+ testColorMatch(name, hex, hsl, rgb, color.rgba, canvas);
+ }
+}
+
+function testToString(color, name, hex, hsl, rgb) {
+ const { COLORUNIT } = colorUtils.CssColor;
+ is(color.toString(COLORUNIT.name), name, "toString() with authored type");
+ is(color.toString(COLORUNIT.hex), hex, "toString() with hex type");
+ is(color.toString(COLORUNIT.hsl), hsl, "toString() with hsl type");
+ is(color.toString(COLORUNIT.rgb), rgb, "toString() with rgb type");
+}
+
+function testColorMatch(name, hex, hsl, rgb, rgba, canvas) {
+ let target;
+ const ctx = canvas.getContext("2d");
+
+ const clearCanvas = function () {
+ canvas.width = 1;
+ };
+ const setColor = function (color) {
+ ctx.fillStyle = color;
+ ctx.fillRect(0, 0, 1, 1);
+ };
+ const setTargetColor = function () {
+ clearCanvas();
+ // All colors have rgba so we can use this to compare against.
+ setColor(rgba);
+ const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
+ target = { r, g, b, a };
+ };
+ const test = function (color, type) {
+ // hsla -> rgba -> hsla produces inaccurate results so we
+ // need some tolerence here.
+ const tolerance = 3;
+ clearCanvas();
+
+ setColor(color);
+ const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
+
+ const rgbFail =
+ Math.abs(r - target.r) > tolerance ||
+ Math.abs(g - target.g) > tolerance ||
+ Math.abs(b - target.b) > tolerance;
+ ok(!rgbFail, "color " + rgba + " matches target. Type: " + type);
+ if (rgbFail) {
+ info(
+ `target: ${JSON.stringify(
+ target
+ )}, color: [r: ${r}, g: ${g}, b: ${b}, a: ${a}]`
+ );
+ }
+
+ const alphaFail = a !== target.a;
+ ok(!alphaFail, "color " + rgba + " alpha value matches target.");
+ };
+
+ setTargetColor();
+
+ test(name, "name");
+ test(hex, "hex");
+ test(hsl, "hsl");
+ test(rgb, "rgb");
+}
diff --git a/devtools/client/shared/test/browser_cubic-bezier-01.js b/devtools/client/shared/test/browser_cubic-bezier-01.js
new file mode 100644
index 0000000000..b57e2de25b
--- /dev/null
+++ b/devtools/client/shared/test/browser_cubic-bezier-01.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the CubicBezierWidget generates content in a given parent node
+
+const {
+ CubicBezierWidget,
+} = require("resource://devtools/client/shared/widgets/CubicBezierWidget.js");
+
+const TEST_URI = CHROME_URL_ROOT + "doc_cubic-bezier-01.html";
+
+add_task(async function () {
+ const { host, doc } = await createHost("bottom", TEST_URI);
+
+ info("Checking that the graph markup is created in the parent");
+ const container = doc.querySelector("#cubic-bezier-container");
+ const w = new CubicBezierWidget(container);
+
+ ok(container.querySelector(".display-wrap"), "The display has been added");
+
+ ok(
+ container.querySelector(".coordinate-plane"),
+ "The coordinate plane has been added"
+ );
+ const buttons = container.querySelectorAll("button");
+ is(buttons.length, 2, "The 2 control points have been added");
+ is(buttons[0].className, "control-point");
+ is(buttons[1].className, "control-point");
+ ok(container.querySelector("canvas"), "The curve canvas has been added");
+
+ info("Destroying the widget");
+ w.destroy();
+ is(container.children.length, 0, "All nodes have been removed");
+
+ host.destroy();
+});
diff --git a/devtools/client/shared/test/browser_cubic-bezier-02.js b/devtools/client/shared/test/browser_cubic-bezier-02.js
new file mode 100644
index 0000000000..3ce0af2f99
--- /dev/null
+++ b/devtools/client/shared/test/browser_cubic-bezier-02.js
@@ -0,0 +1,206 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the CubicBezierWidget events
+
+const {
+ CubicBezierWidget,
+} = require("resource://devtools/client/shared/widgets/CubicBezierWidget.js");
+const {
+ PREDEFINED,
+} = require("resource://devtools/client/shared/widgets/CubicBezierPresets.js");
+
+// In this test we have to use a slightly more complete HTML tree, with
+// in order to remove its margin and prevent shifted positions
+const TEST_URI = CHROME_URL_ROOT + "doc_cubic-bezier-02.html";
+
+add_task(async function () {
+ const { host, win, doc } = await createHost("bottom", TEST_URI);
+
+ // Required or widget will be clipped inside of 'bottom'
+ // host by -14. Setting `fixed` zeroes this which is needed for
+ // calculating offsets. Occurs in test env only.
+ doc.body.setAttribute("style", "position: fixed; margin: 0;");
+
+ const container = doc.querySelector("#cubic-bezier-container");
+ const w = new CubicBezierWidget(container, PREDEFINED.linear);
+
+ const rect = w.curve.getBoundingClientRect();
+ rect.graphTop = rect.height * w.bezierCanvas.padding[0];
+ rect.graphBottom = rect.height - rect.graphTop;
+ rect.graphHeight = rect.graphBottom - rect.graphTop;
+
+ await pointsCanBeDragged(w, win, doc, rect);
+ await curveCanBeClicked(w, win, doc, rect);
+ await pointsCanBeMovedWithKeyboard(w, win, doc, rect);
+
+ w.destroy();
+ host.destroy();
+});
+
+async function pointsCanBeDragged(widget, win, doc, offsets) {
+ info("Checking that the control points can be dragged with the mouse");
+
+ info("Listening for the update event");
+ let onUpdated = widget.once("updated");
+
+ info("Generating a mousedown/move/up on P1");
+ widget._onPointMouseDown({ target: widget.p1 });
+ doc.onmousemove({ pageX: offsets.left, pageY: offsets.graphTop });
+ doc.onmouseup();
+
+ let bezier = await onUpdated;
+ ok(true, "The widget fired the updated event");
+ ok(bezier, "The updated event contains a bezier argument");
+ is(bezier.P1[0], 0, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 1, "The new P1 progress coordinate is correct");
+
+ info("Listening for the update event");
+ onUpdated = widget.once("updated");
+
+ info("Generating a mousedown/move/up on P2");
+ widget._onPointMouseDown({ target: widget.p2 });
+ doc.onmousemove({ pageX: offsets.right, pageY: offsets.graphBottom });
+ doc.onmouseup();
+
+ bezier = await onUpdated;
+ is(bezier.P2[0], 1, "The new P2 time coordinate is correct");
+ is(bezier.P2[1], 0, "The new P2 progress coordinate is correct");
+}
+
+async function curveCanBeClicked(widget, win, doc, offsets) {
+ info("Checking that clicking on the curve moves the closest control point");
+
+ info("Listening for the update event");
+ let onUpdated = widget.once("updated");
+
+ info("Click close to P1");
+ let x = offsets.left + offsets.width / 4.0;
+ let y = offsets.graphTop + offsets.graphHeight / 4.0;
+ widget._onCurveClick({ pageX: x, pageY: y });
+
+ let bezier = await onUpdated;
+ ok(true, "The widget fired the updated event");
+ is(bezier.P1[0], 0.25, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
+ is(bezier.P2[0], 1, "P2 time coordinate remained unchanged");
+ is(bezier.P2[1], 0, "P2 progress coordinate remained unchanged");
+
+ info("Listening for the update event");
+ onUpdated = widget.once("updated");
+
+ info("Click close to P2");
+ x = offsets.right - offsets.width / 4;
+ y = offsets.graphBottom - offsets.graphHeight / 4;
+ widget._onCurveClick({ pageX: x, pageY: y });
+
+ bezier = await onUpdated;
+ is(bezier.P2[0], 0.75, "The new P2 time coordinate is correct");
+ is(bezier.P2[1], 0.25, "The new P2 progress coordinate is correct");
+ is(bezier.P1[0], 0.25, "P1 time coordinate remained unchanged");
+ is(bezier.P1[1], 0.75, "P1 progress coordinate remained unchanged");
+}
+
+async function pointsCanBeMovedWithKeyboard(widget, win, doc, offsets) {
+ info("Checking that points respond to keyboard events");
+
+ const singleStep = 3;
+ const shiftStep = 30;
+
+ info("Moving P1 to the left");
+ let newOffset = parseInt(widget.p1.style.left, 10) - singleStep;
+ let x = widget.bezierCanvas.offsetsToCoordinates({
+ style: { left: newOffset },
+ })[0];
+
+ let onUpdated = widget.once("updated");
+ widget._onPointKeyDown(getKeyEvent(widget.p1, 37));
+ let bezier = await onUpdated;
+
+ is(bezier.P1[0], x, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
+
+ info("Moving P1 to the left, fast");
+ newOffset = parseInt(widget.p1.style.left, 10) - shiftStep;
+ x = widget.bezierCanvas.offsetsToCoordinates({
+ style: { left: newOffset },
+ })[0];
+
+ onUpdated = widget.once("updated");
+ widget._onPointKeyDown(getKeyEvent(widget.p1, 37, true));
+ bezier = await onUpdated;
+ is(bezier.P1[0], x, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
+
+ info("Moving P1 to the right, fast");
+ newOffset = parseInt(widget.p1.style.left, 10) + shiftStep;
+ x = widget.bezierCanvas.offsetsToCoordinates({
+ style: { left: newOffset },
+ })[0];
+
+ onUpdated = widget.once("updated");
+ widget._onPointKeyDown(getKeyEvent(widget.p1, 39, true));
+ bezier = await onUpdated;
+ is(bezier.P1[0], x, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
+
+ info("Moving P1 to the bottom");
+ newOffset = parseInt(widget.p1.style.top, 10) + singleStep;
+ let y = widget.bezierCanvas.offsetsToCoordinates({
+ style: { top: newOffset },
+ })[1];
+
+ onUpdated = widget.once("updated");
+ widget._onPointKeyDown(getKeyEvent(widget.p1, 40));
+ bezier = await onUpdated;
+ is(bezier.P1[0], x, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], y, "The new P1 progress coordinate is correct");
+
+ info("Moving P1 to the bottom, fast");
+ newOffset = parseInt(widget.p1.style.top, 10) + shiftStep;
+ y = widget.bezierCanvas.offsetsToCoordinates({
+ style: { top: newOffset },
+ })[1];
+
+ onUpdated = widget.once("updated");
+ widget._onPointKeyDown(getKeyEvent(widget.p1, 40, true));
+ bezier = await onUpdated;
+ is(bezier.P1[0], x, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], y, "The new P1 progress coordinate is correct");
+
+ info("Moving P1 to the top, fast");
+ newOffset = parseInt(widget.p1.style.top, 10) - shiftStep;
+ y = widget.bezierCanvas.offsetsToCoordinates({
+ style: { top: newOffset },
+ })[1];
+
+ onUpdated = widget.once("updated");
+ widget._onPointKeyDown(getKeyEvent(widget.p1, 38, true));
+ bezier = await onUpdated;
+ is(bezier.P1[0], x, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], y, "The new P1 progress coordinate is correct");
+
+ info("Checking that keyboard events also work with P2");
+ info("Moving P2 to the left");
+ newOffset = parseInt(widget.p2.style.left, 10) - singleStep;
+ x = widget.bezierCanvas.offsetsToCoordinates({
+ style: { left: newOffset },
+ })[0];
+
+ onUpdated = widget.once("updated");
+ widget._onPointKeyDown(getKeyEvent(widget.p2, 37));
+ bezier = await onUpdated;
+ is(bezier.P2[0], x, "The new P2 time coordinate is correct");
+ is(bezier.P2[1], 0.25, "The new P2 progress coordinate is correct");
+}
+
+function getKeyEvent(target, keyCode, shift = false) {
+ return {
+ target,
+ keyCode,
+ shiftKey: shift,
+ preventDefault: () => {},
+ };
+}
diff --git a/devtools/client/shared/test/browser_cubic-bezier-03.js b/devtools/client/shared/test/browser_cubic-bezier-03.js
new file mode 100644
index 0000000000..2c722bbf41
--- /dev/null
+++ b/devtools/client/shared/test/browser_cubic-bezier-03.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that coordinates can be changed programatically in the CubicBezierWidget
+
+const {
+ CubicBezierWidget,
+} = require("resource://devtools/client/shared/widgets/CubicBezierWidget.js");
+const {
+ PREDEFINED,
+} = require("resource://devtools/client/shared/widgets/CubicBezierPresets.js");
+
+const TEST_URI = CHROME_URL_ROOT + "doc_cubic-bezier-01.html";
+
+add_task(async function () {
+ const { host, doc } = await createHost("bottom", TEST_URI);
+
+ const container = doc.querySelector("#cubic-bezier-container");
+ const w = new CubicBezierWidget(container, PREDEFINED.linear);
+
+ await coordinatesCanBeChangedByProvidingAnArray(w);
+ await coordinatesCanBeChangedByProvidingAValue(w);
+
+ w.destroy();
+ host.destroy();
+});
+
+async function coordinatesCanBeChangedByProvidingAnArray(widget) {
+ info("Listening for the update event");
+ const onUpdated = widget.once("updated");
+
+ info("Setting new coordinates");
+ widget.coordinates = [0, 1, 1, 0];
+
+ const bezier = await onUpdated;
+ ok(true, "The updated event was fired as a result of setting coordinates");
+
+ is(bezier.P1[0], 0, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 1, "The new P1 progress coordinate is correct");
+ is(bezier.P2[0], 1, "The new P2 time coordinate is correct");
+ is(bezier.P2[1], 0, "The new P2 progress coordinate is correct");
+}
+
+async function coordinatesCanBeChangedByProvidingAValue(widget) {
+ info("Listening for the update event");
+ let onUpdated = widget.once("updated");
+
+ info("Setting linear css value");
+ widget.cssCubicBezierValue = "linear";
+ let bezier = await onUpdated;
+ ok(true, "The updated event was fired as a result of setting cssValue");
+
+ is(bezier.P1[0], 0, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 0, "The new P1 progress coordinate is correct");
+ is(bezier.P2[0], 1, "The new P2 time coordinate is correct");
+ is(bezier.P2[1], 1, "The new P2 progress coordinate is correct");
+
+ info("Setting a custom cubic-bezier css value");
+ onUpdated = widget.once("updated");
+ widget.cssCubicBezierValue = "cubic-bezier(.25,-0.5, 1, 1.25)";
+ bezier = await onUpdated;
+ ok(true, "The updated event was fired as a result of setting cssValue");
+
+ is(bezier.P1[0], 0.25, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], -0.5, "The new P1 progress coordinate is correct");
+ is(bezier.P2[0], 1, "The new P2 time coordinate is correct");
+ is(bezier.P2[1], 1.25, "The new P2 progress coordinate is correct");
+}
diff --git a/devtools/client/shared/test/browser_cubic-bezier-04.js b/devtools/client/shared/test/browser_cubic-bezier-04.js
new file mode 100644
index 0000000000..1fff1821ff
--- /dev/null
+++ b/devtools/client/shared/test/browser_cubic-bezier-04.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the CubicBezierPresetWidget generates markup.
+
+const {
+ CubicBezierPresetWidget,
+} = require("resource://devtools/client/shared/widgets/CubicBezierWidget.js");
+const {
+ PRESETS,
+} = require("resource://devtools/client/shared/widgets/CubicBezierPresets.js");
+
+const TEST_URI = CHROME_URL_ROOT + "doc_cubic-bezier-01.html";
+
+add_task(async function () {
+ const { host, doc } = await createHost("bottom", TEST_URI);
+
+ const container = doc.querySelector("#cubic-bezier-container");
+ const w = new CubicBezierPresetWidget(container);
+
+ info("Checking that the presets are created in the parent");
+ ok(container.querySelector(".preset-pane"), "The preset pane has been added");
+
+ ok(
+ container.querySelector("#preset-categories"),
+ "The preset categories have been added"
+ );
+ const categories = container.querySelectorAll(".category");
+ is(
+ categories.length,
+ Object.keys(PRESETS).length,
+ "The preset categories have been added"
+ );
+ Object.keys(PRESETS).forEach(category => {
+ ok(container.querySelector("#" + category), `${category} has been added`);
+ ok(
+ container.querySelector("#preset-category-" + category),
+ `The preset list for ${category} has been added.`
+ );
+ });
+
+ info("Checking that each of the presets and its preview have been added");
+ Object.keys(PRESETS).forEach(category => {
+ Object.keys(PRESETS[category]).forEach(presetLabel => {
+ const preset = container.querySelector("#" + presetLabel);
+ ok(preset, `${presetLabel} has been added`);
+ ok(
+ preset.querySelector("canvas"),
+ `${presetLabel}'s canvas preview has been added`
+ );
+ ok(preset.querySelector("p"), `${presetLabel}'s label has been added`);
+ });
+ });
+
+ w.destroy();
+ host.destroy();
+});
diff --git a/devtools/client/shared/test/browser_cubic-bezier-05.js b/devtools/client/shared/test/browser_cubic-bezier-05.js
new file mode 100644
index 0000000000..2e6659c07d
--- /dev/null
+++ b/devtools/client/shared/test/browser_cubic-bezier-05.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the CubicBezierPresetWidget cycles menus
+
+const {
+ CubicBezierPresetWidget,
+} = require("resource://devtools/client/shared/widgets/CubicBezierWidget.js");
+const {
+ PREDEFINED,
+ PRESETS,
+ DEFAULT_PRESET_CATEGORY,
+} = require("resource://devtools/client/shared/widgets/CubicBezierPresets.js");
+
+const TEST_URI = CHROME_URL_ROOT + "doc_cubic-bezier-01.html";
+
+add_task(async function () {
+ const { host, doc } = await createHost("bottom", TEST_URI);
+
+ const container = doc.querySelector("#cubic-bezier-container");
+ const w = new CubicBezierPresetWidget(container);
+
+ info("Checking that preset is selected if coordinates are known");
+
+ w.refreshMenu([0, 0, 0, 0]);
+ is(
+ w.activeCategory,
+ container.querySelector(`#${DEFAULT_PRESET_CATEGORY}`),
+ "The default category is selected"
+ );
+ is(w._activePreset, null, "There is no selected category");
+
+ w.refreshMenu(PREDEFINED.linear);
+ is(
+ w.activeCategory,
+ container.querySelector("#ease-in-out"),
+ "The ease-in-out category is active"
+ );
+ is(
+ w._activePreset,
+ container.querySelector("#ease-in-out-linear"),
+ "The ease-in-out-linear preset is active"
+ );
+
+ w.refreshMenu(PRESETS["ease-out"]["ease-out-sine"]);
+ is(
+ w.activeCategory,
+ container.querySelector("#ease-out"),
+ "The ease-out category is active"
+ );
+ is(
+ w._activePreset,
+ container.querySelector("#ease-out-sine"),
+ "The ease-out-sine preset is active"
+ );
+
+ w.refreshMenu([0, 0, 0, 0]);
+ is(
+ w.activeCategory,
+ container.querySelector("#ease-out"),
+ "The ease-out category is still active"
+ );
+ is(w._activePreset, null, "No preset is active");
+
+ w.destroy();
+ host.destroy();
+});
diff --git a/devtools/client/shared/test/browser_cubic-bezier-06.js b/devtools/client/shared/test/browser_cubic-bezier-06.js
new file mode 100644
index 0000000000..9cb00e8bf7
--- /dev/null
+++ b/devtools/client/shared/test/browser_cubic-bezier-06.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the integration between CubicBezierWidget and CubicBezierPresets
+
+const {
+ CubicBezierWidget,
+} = require("resource://devtools/client/shared/widgets/CubicBezierWidget.js");
+const {
+ PRESETS,
+} = require("resource://devtools/client/shared/widgets/CubicBezierPresets.js");
+
+const TEST_URI = CHROME_URL_ROOT + "doc_cubic-bezier-01.html";
+
+add_task(async function () {
+ const { host, win, doc } = await createHost("bottom", TEST_URI);
+
+ const container = doc.querySelector("#cubic-bezier-container");
+ const w = new CubicBezierWidget(
+ container,
+ PRESETS["ease-in"]["ease-in-sine"]
+ );
+ w.presets.refreshMenu(PRESETS["ease-in"]["ease-in-sine"]);
+
+ const rect = w.curve.getBoundingClientRect();
+ rect.graphTop = rect.height * w.bezierCanvas.padding[0];
+
+ await adjustingBezierUpdatesPreset(w, win, doc, rect);
+ await selectingPresetUpdatesBezier(w, win, doc, rect);
+
+ w.destroy();
+ host.destroy();
+});
+
+function adjustingBezierUpdatesPreset(widget, win, doc, rect) {
+ info("Checking that changing the bezier refreshes the preset menu");
+
+ is(
+ widget.presets.activeCategory,
+ doc.querySelector("#ease-in"),
+ "The selected category is ease-in"
+ );
+
+ is(
+ widget.presets._activePreset,
+ doc.querySelector("#ease-in-sine"),
+ "The selected preset is ease-in-sine"
+ );
+
+ info("Generating custom bezier curve by dragging");
+ widget._onPointMouseDown({ target: widget.p1 });
+ doc.onmousemove({ pageX: rect.left, pageY: rect.graphTop });
+ doc.onmouseup();
+
+ is(
+ widget.presets.activeCategory,
+ doc.querySelector("#ease-in"),
+ "The selected category is still ease-in"
+ );
+
+ is(widget.presets._activePreset, null, "There is no active preset");
+}
+
+async function selectingPresetUpdatesBezier(widget, win, doc, rect) {
+ info("Checking that selecting a preset updates bezier curve");
+
+ info("Listening for the new coordinates event");
+ const onNewCoordinates = widget.presets.once("new-coordinates");
+ const onUpdated = widget.once("updated");
+
+ info("Click a preset");
+ const preset = doc.querySelector("#ease-in-sine");
+ widget.presets._onPresetClick({ currentTarget: preset });
+
+ await onNewCoordinates;
+ ok(true, "The preset widget fired the new-coordinates event");
+
+ const bezier = await onUpdated;
+ ok(true, "The bezier canvas fired the updated event");
+
+ is(
+ bezier.P1[0],
+ preset.coordinates[0],
+ "The new P1 time coordinate is correct"
+ );
+ is(
+ bezier.P1[1],
+ preset.coordinates[1],
+ "The new P1 progress coordinate is correct"
+ );
+ is(bezier.P2[0], preset.coordinates[2], "P2 time coordinate is correct ");
+ is(bezier.P2[1], preset.coordinates[3], "P2 progress coordinate is correct");
+}
diff --git a/devtools/client/shared/test/browser_cubic-bezier-07.js b/devtools/client/shared/test/browser_cubic-bezier-07.js
new file mode 100644
index 0000000000..2c7d81b2e3
--- /dev/null
+++ b/devtools/client/shared/test/browser_cubic-bezier-07.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that changing the cubic-bezier curve in the widget does change the dot animation
+// preview too.
+
+const {
+ CubicBezierWidget,
+} = require("resource://devtools/client/shared/widgets/CubicBezierWidget.js");
+const {
+ PREDEFINED,
+} = require("resource://devtools/client/shared/widgets/CubicBezierPresets.js");
+
+const TEST_URI = CHROME_URL_ROOT + "doc_cubic-bezier-01.html";
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("ui.prefersReducedMotion");
+});
+
+add_task(async function () {
+ const { host, doc } = await createHost("bottom", TEST_URI);
+ // Unset "prefers reduced motion", otherwise the dot animation preview won't be created.
+ // See Bug 1637842
+ // https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion
+ await pushPref("ui.prefersReducedMotion", 0);
+
+ const container = doc.querySelector("#cubic-bezier-container");
+ const w = new CubicBezierWidget(container, PREDEFINED.linear);
+
+ await previewDotReactsToChanges(w, [0.6, -0.28, 0.74, 0.05]);
+ await previewDotReactsToChanges(w, [0.9, 0.03, 0.69, 0.22]);
+ await previewDotReactsToChanges(w, [0.68, -0.55, 0.27, 1.55]);
+ await previewDotReactsToChanges(w, PREDEFINED.ease, "ease");
+ await previewDotReactsToChanges(w, PREDEFINED["ease-in-out"], "ease-in-out");
+
+ w.destroy();
+ host.destroy();
+});
+
+async function previewDotReactsToChanges(widget, coords, expectedEasing) {
+ const onUpdated = widget.once("updated");
+ widget.coordinates = coords;
+ await onUpdated;
+
+ const animatedDot = widget.timingPreview.dot;
+ const animations = animatedDot.getAnimations();
+
+ if (!expectedEasing) {
+ expectedEasing = `cubic-bezier(${coords[0]}, ${coords[1]}, ${coords[2]}, ${coords[3]})`;
+ }
+
+ is(animations.length, 1, "The dot is animated");
+
+ const goingToRight = animations[0].effect.getKeyframes()[2];
+ is(
+ goingToRight.easing,
+ expectedEasing,
+ `The easing when going to the right was set correctly to ${coords}`
+ );
+
+ const goingToLeft = animations[0].effect.getKeyframes()[6];
+ is(
+ goingToLeft.easing,
+ expectedEasing,
+ `The easing when going to the left was set correctly to ${coords}`
+ );
+}
diff --git a/devtools/client/shared/test/browser_dbg_globalactor.js b/devtools/client/shared/test/browser_dbg_globalactor.js
new file mode 100644
index 0000000000..71cda04b47
--- /dev/null
+++ b/devtools/client/shared/test/browser_dbg_globalactor.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Check extension-added global actor API.
+ */
+
+"use strict";
+
+var {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+var {
+ ActorRegistry,
+} = require("resource://devtools/server/actors/utils/actor-registry.js");
+var {
+ DevToolsClient,
+} = require("resource://devtools/client/devtools-client.js");
+
+const ACTORS_URL = EXAMPLE_URL + "testactors.js";
+
+add_task(async function () {
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+
+ ActorRegistry.registerModule(ACTORS_URL, {
+ prefix: "testOne",
+ constructor: "TestActor1",
+ type: { global: true },
+ });
+
+ const transport = DevToolsServer.connectPipe();
+ const client = new DevToolsClient(transport);
+ const [type] = await client.connect();
+ is(type, "browser", "Root actor should identify itself as a browser.");
+
+ let response = await client.mainRoot.rootForm;
+ const globalActor = response.testOneActor;
+ ok(globalActor, "Found the test global actor.");
+ ok(
+ globalActor.includes("testOne"),
+ "testGlobalActor1's typeName should be used."
+ );
+
+ response = await client.request({ to: globalActor, type: "ping" });
+ is(response.pong, "pong", "Actor should respond to requests.");
+
+ // Send another ping to see if the same actor is used.
+ response = await client.request({ to: globalActor, type: "ping" });
+ is(response.pong, "pong", "Actor should respond to requests.");
+
+ // Make sure that lazily-created actors are created only once.
+ let count = 0;
+ for (const connID of Object.getOwnPropertyNames(
+ DevToolsServer._connections
+ )) {
+ const conn = DevToolsServer._connections[connID];
+ const computedPrefix = conn._prefix + "testOne";
+ for (const pool of conn._extraPools) {
+ for (const actor of pool.poolChildren()) {
+ if (actor.actorID.startsWith(computedPrefix)) {
+ count++;
+ }
+ }
+ }
+ }
+
+ is(count, 1, "Only one actor exists in all pools. One global actor.");
+
+ await client.close();
+});
diff --git a/devtools/client/shared/test/browser_dbg_listaddons.js b/devtools/client/shared/test/browser_dbg_listaddons.js
new file mode 100644
index 0000000000..d6d302db34
--- /dev/null
+++ b/devtools/client/shared/test/browser_dbg_listaddons.js
@@ -0,0 +1,137 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+var {
+ DevToolsClient,
+} = require("resource://devtools/client/devtools-client.js");
+
+/**
+ * Make sure the listAddons request works as specified.
+ */
+const ADDON1_ID = "test-addon-1@mozilla.org";
+const ADDON1_PATH = "addons/test-addon-1/";
+const ADDON2_ID = "test-addon-2@mozilla.org";
+const ADDON2_PATH = "addons/test-addon-2/";
+
+add_task(async function () {
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+
+ const transport = DevToolsServer.connectPipe();
+ const client = new DevToolsClient(transport);
+
+ const [type] = await client.connect();
+ is(type, "browser", "Root actor should identify itself as a browser.");
+
+ let addonListChangedEvents = 0;
+ client.mainRoot.on("addonListChanged", () => addonListChangedEvents++);
+ const addons = await client.mainRoot.getFront("addons");
+
+ const addon1 = await addTemporaryAddon({
+ addons,
+ path: ADDON1_PATH,
+ openDevTools: false,
+ });
+ const addonFront1 = await client.mainRoot.getAddon({ id: ADDON1_ID });
+ is(addonListChangedEvents, 0, "Should not receive addonListChanged yet.");
+ ok(addonFront1, "Should find an addon actor for addon1.");
+
+ const addon2 = await addTemporaryAddon({
+ addons,
+ path: ADDON2_PATH,
+ openDevTools: true,
+ });
+ const front1AfterAddingAddon2 = await client.mainRoot.getAddon({
+ id: ADDON1_ID,
+ });
+ const addonFront2 = await client.mainRoot.getAddon({ id: ADDON2_ID });
+
+ is(
+ addonListChangedEvents,
+ 1,
+ "Should have received an addonListChanged event."
+ );
+ ok(addonFront2, "Should find an addon actor for addon2.");
+ is(
+ addonFront1,
+ front1AfterAddingAddon2,
+ "Front for addon1 should be the same"
+ );
+
+ await removeAddon(addon1);
+ const front1AfterRemove = await client.mainRoot.getAddon({ id: ADDON1_ID });
+ is(
+ addonListChangedEvents,
+ 2,
+ "Should have received an addonListChanged event."
+ );
+ ok(!front1AfterRemove, "Should no longer get a front for addon1");
+
+ await removeAddon(addon2);
+ const front2AfterRemove = await client.mainRoot.getAddon({ id: ADDON2_ID });
+ is(
+ addonListChangedEvents,
+ 3,
+ "Should have received an addonListChanged event."
+ );
+ ok(!front2AfterRemove, "Should no longer get a front for addon1");
+
+ // Check behavior when openDevTools is not passed:
+ const addon2again = await addTemporaryAddon({
+ addons,
+ path: ADDON2_PATH,
+ // openDevTools: null,
+ });
+ const addonFront2again = await client.mainRoot.getAddon({ id: ADDON2_ID });
+ ok(addonFront2again, "Should find an addon actor for addon2.");
+ is(addonListChangedEvents, 4, "Should have seen addonListChanged.");
+ await removeAddon(addon2again);
+ is(addonListChangedEvents, 5, "Should have seen addonListChanged.");
+
+ await client.close();
+});
+
+async function addTemporaryAddon({ addons, path, openDevTools }) {
+ const addonFilePath = getTestFilePath(path);
+ info("Installing addon: " + addonFilePath);
+
+ const onToolboxReady = gDevTools.once("toolbox-ready");
+ const { id } = await addons.installTemporaryAddon(
+ addonFilePath,
+ openDevTools
+ );
+
+ if (openDevTools) {
+ info("Wait for toolbox to be opened");
+ const toolbox = await onToolboxReady;
+ ok(true, "Toolbox was opened when openDevTools option was true");
+ info("Destroying this toolbox");
+ await toolbox.destroy();
+ }
+
+ return AddonManager.getAddonByID(id);
+}
+
+function removeAddon(addon) {
+ return new Promise(resolve => {
+ info("Removing addon.");
+
+ const listener = {
+ onUninstalled(uninstalledAddon) {
+ if (uninstalledAddon != addon) {
+ return;
+ }
+ AddonManager.removeAddonListener(listener);
+ resolve();
+ },
+ };
+
+ AddonManager.addAddonListener(listener);
+ addon.uninstall();
+ });
+}
diff --git a/devtools/client/shared/test/browser_dbg_listtabs-01.js b/devtools/client/shared/test/browser_dbg_listtabs-01.js
new file mode 100644
index 0000000000..e804a6b91c
--- /dev/null
+++ b/devtools/client/shared/test/browser_dbg_listtabs-01.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Make sure the listTabs request works as specified.
+ */
+
+var {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+var {
+ DevToolsClient,
+} = require("resource://devtools/client/devtools-client.js");
+
+const TAB1_URL = EXAMPLE_URL + "doc_empty-tab-01.html";
+const TAB2_URL = EXAMPLE_URL + "doc_empty-tab-02.html";
+
+add_task(async function test() {
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+
+ const transport = DevToolsServer.connectPipe();
+ const client = new DevToolsClient(transport);
+ const [aType] = await client.connect();
+ is(aType, "browser", "Root actor should identify itself as a browser.");
+
+ const firstTab = await testFirstTab(client);
+ const secondTab = await testSecondTab(client, firstTab.front);
+ await testRemoveTab(client, firstTab.tab);
+ await testAttachRemovedTab(secondTab.tab, secondTab.front);
+ await client.close();
+});
+
+async function testFirstTab(client) {
+ const tab = await addTab(TAB1_URL);
+
+ const front = await getDescriptorActorForUrl(client, TAB1_URL);
+ ok(front, "Should find a target actor for the first tab.");
+ return { tab, front };
+}
+
+async function testSecondTab(client, firstTabFront) {
+ const tab = await addTab(TAB2_URL);
+
+ const firstFront = await getDescriptorActorForUrl(client, TAB1_URL);
+ const secondFront = await getDescriptorActorForUrl(client, TAB2_URL);
+ is(firstFront, firstTabFront, "First tab's actor shouldn't have changed.");
+ ok(secondFront, "Should find a target actor for the second tab.");
+ return { tab, front: secondFront };
+}
+
+async function testRemoveTab(client, firstTab) {
+ await removeTab(firstTab);
+ const front = await getDescriptorActorForUrl(client, TAB1_URL);
+ ok(!front, "Shouldn't find a target actor for the first tab anymore.");
+}
+
+async function testAttachRemovedTab(secondTab, secondTabFront) {
+ await removeTab(secondTab);
+
+ const { actorID } = secondTabFront;
+ try {
+ await secondTabFront.getFavicon({});
+ ok(
+ false,
+ "any request made to the descriptor for a closed tab should have failed"
+ );
+ } catch (error) {
+ ok(
+ error.message.includes(
+ `Connection closed, pending request to ${actorID}, type getFavicon failed`
+ ),
+ "Actor is gone since the tab was removed."
+ );
+ }
+}
+
+async function getDescriptorActorForUrl(client, url) {
+ const tabDescriptors = await client.mainRoot.listTabs();
+ const tabDescriptor = tabDescriptors.find(front => front.url == url);
+ return tabDescriptor;
+}
diff --git a/devtools/client/shared/test/browser_dbg_listtabs-02.js b/devtools/client/shared/test/browser_dbg_listtabs-02.js
new file mode 100644
index 0000000000..a23981a93b
--- /dev/null
+++ b/devtools/client/shared/test/browser_dbg_listtabs-02.js
@@ -0,0 +1,248 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Make sure the root actor's live tab list implementation works as specified.
+ */
+
+var {
+ BrowserTabList,
+} = require("resource://devtools/server/actors/webbrowser.js");
+var {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+
+var gTestPage =
+ "data:text/html;charset=utf-8," +
+ encodeURIComponent(
+ "JS Debugger BrowserTabList test pageYo."
+ );
+
+// The tablist object whose behavior we observe.
+var gTabList;
+var gFirstActor, gActorA;
+var gTabA, gTabB, gTabC;
+var gNewWindow;
+
+// Stock onListChanged handler.
+var onListChangedCount = 0;
+function onListChangedHandler() {
+ onListChangedCount++;
+}
+
+function test() {
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+
+ gTabList = new BrowserTabList("fake DevToolsServerConnection");
+ gTabList._testing = true;
+ gTabList.onListChanged = onListChangedHandler;
+
+ checkSingleTab()
+ .then(addTabA)
+ .then(testTabA)
+ .then(addTabB)
+ .then(testTabB)
+ .then(removeTabA)
+ .then(testTabClosed)
+ .then(addTabC)
+ .then(testTabC)
+ .then(removeTabC)
+ .then(testNewWindow)
+ .then(removeNewWindow)
+ .then(testWindowClosed)
+ .then(removeTabB)
+ .then(checkSingleTab)
+ .then(finishUp);
+}
+
+function checkSingleTab() {
+ return gTabList.getList().then(targetActors => {
+ is(targetActors.length, 1, "initial tab list: contains initial tab");
+ gFirstActor = targetActors[0];
+ is(
+ gFirstActor.url,
+ "about:blank",
+ "initial tab list: initial tab URL is 'about:blank'"
+ );
+ is(
+ gFirstActor.title,
+ "New Tab",
+ "initial tab list: initial tab title is 'New Tab'"
+ );
+ });
+}
+
+function addTabA() {
+ return addTab(gTestPage).then(tab => {
+ gTabA = tab;
+ });
+}
+
+function testTabA() {
+ is(onListChangedCount, 1, "onListChanged handler call count");
+
+ return gTabList.getList().then(targetActors => {
+ targetActors = new Set(targetActors);
+ is(targetActors.size, 2, "gTabA opened: two tabs in list");
+ ok(targetActors.has(gFirstActor), "gTabA opened: initial tab present");
+
+ info("actors: " + [...targetActors].map(a => a.url));
+ gActorA = [...targetActors].filter(a => a !== gFirstActor)[0];
+ ok(gActorA.url.match(/^data:text\/html;/), "gTabA opened: new tab URL");
+ is(
+ gActorA.title,
+ "JS Debugger BrowserTabList test page",
+ "gTabA opened: new tab title"
+ );
+ });
+}
+
+function addTabB() {
+ return addTab(gTestPage).then(tab => {
+ gTabB = tab;
+ });
+}
+
+function testTabB() {
+ is(onListChangedCount, 2, "onListChanged handler call count");
+
+ return gTabList.getList().then(targetActors => {
+ targetActors = new Set(targetActors);
+ is(targetActors.size, 3, "gTabB opened: three tabs in list");
+ });
+}
+
+function removeTabA() {
+ return new Promise(resolve => {
+ once(gBrowser.tabContainer, "TabClose").then(event => {
+ ok(!event.detail.adoptedBy, "This was a normal tab close");
+
+ // Let the actor's TabClose handler finish first.
+ executeSoon(resolve);
+ }, false);
+
+ removeTab(gTabA);
+ });
+}
+
+function testTabClosed() {
+ is(onListChangedCount, 3, "onListChanged handler call count");
+
+ gTabList.getList().then(targetActors => {
+ targetActors = new Set(targetActors);
+ is(targetActors.size, 2, "gTabA closed: two tabs in list");
+ ok(targetActors.has(gFirstActor), "gTabA closed: initial tab present");
+
+ info("actors: " + [...targetActors].map(a => a.url));
+ gActorA = [...targetActors].filter(a => a !== gFirstActor)[0];
+ ok(gActorA.url.match(/^data:text\/html;/), "gTabA closed: new tab URL");
+ is(
+ gActorA.title,
+ "JS Debugger BrowserTabList test page",
+ "gTabA closed: new tab title"
+ );
+ });
+}
+
+function addTabC() {
+ return addTab(gTestPage).then(tab => {
+ gTabC = tab;
+ });
+}
+
+function testTabC() {
+ is(onListChangedCount, 4, "onListChanged handler call count");
+
+ gTabList.getList().then(targetActors => {
+ targetActors = new Set(targetActors);
+ is(targetActors.size, 3, "gTabC opened: three tabs in list");
+ });
+}
+
+function removeTabC() {
+ return new Promise(resolve => {
+ once(gBrowser.tabContainer, "TabClose").then(event => {
+ ok(event.detail.adoptedBy, "This was a tab closed by moving");
+
+ // Let the actor's TabClose handler finish first.
+ executeSoon(resolve);
+ }, false);
+
+ gNewWindow = gBrowser.replaceTabWithWindow(gTabC);
+ });
+}
+
+function testNewWindow() {
+ is(onListChangedCount, 5, "onListChanged handler call count");
+
+ return gTabList.getList().then(targetActors => {
+ targetActors = new Set(targetActors);
+ is(targetActors.size, 3, "gTabC closed: three tabs in list");
+ ok(targetActors.has(gFirstActor), "gTabC closed: initial tab present");
+
+ info("actors: " + [...targetActors].map(a => a.url));
+ gActorA = [...targetActors].filter(a => a !== gFirstActor)[0];
+ ok(gActorA.url.match(/^data:text\/html;/), "gTabC closed: new tab URL");
+ is(
+ gActorA.title,
+ "JS Debugger BrowserTabList test page",
+ "gTabC closed: new tab title"
+ );
+ });
+}
+
+function removeNewWindow() {
+ return new Promise(resolve => {
+ once(gNewWindow, "unload").then(event => {
+ ok(!event.detail, "This was a normal window close");
+
+ // Let the actor's TabClose handler finish first.
+ executeSoon(resolve);
+ }, false);
+
+ gNewWindow.close();
+ });
+}
+
+function testWindowClosed() {
+ is(onListChangedCount, 6, "onListChanged handler call count");
+
+ return gTabList.getList().then(targetActors => {
+ targetActors = new Set(targetActors);
+ is(targetActors.size, 2, "gNewWindow closed: two tabs in list");
+ ok(targetActors.has(gFirstActor), "gNewWindow closed: initial tab present");
+
+ info("actors: " + [...targetActors].map(a => a.url));
+ gActorA = [...targetActors].filter(a => a !== gFirstActor)[0];
+ ok(
+ gActorA.url.match(/^data:text\/html;/),
+ "gNewWindow closed: new tab URL"
+ );
+ is(
+ gActorA.title,
+ "JS Debugger BrowserTabList test page",
+ "gNewWindow closed: new tab title"
+ );
+ });
+}
+
+function removeTabB() {
+ return new Promise(resolve => {
+ once(gBrowser.tabContainer, "TabClose").then(event => {
+ ok(!event.detail.adoptedBy, "This was a normal tab close");
+
+ // Let the actor's TabClose handler finish first.
+ executeSoon(resolve);
+ }, false);
+
+ removeTab(gTabB);
+ });
+}
+
+function finishUp() {
+ gTabList = gFirstActor = gActorA = gTabA = gTabB = gTabC = gNewWindow = null;
+ finish();
+}
diff --git a/devtools/client/shared/test/browser_dbg_listworkers.js b/devtools/client/shared/test/browser_dbg_listworkers.js
new file mode 100644
index 0000000000..fcdcf8e5dd
--- /dev/null
+++ b/devtools/client/shared/test/browser_dbg_listworkers.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");
+});
+
+var TAB_URL = EXAMPLE_URL + "doc_listworkers-tab.html";
+var WORKER1_URL = "code_listworkers-worker1.js";
+var WORKER2_URL = "code_listworkers-worker2.js";
+
+add_task(async function test() {
+ const tab = await addTab(TAB_URL);
+ const target = await createAndAttachTargetForTab(tab);
+
+ let { workers } = await listWorkers(target);
+ is(workers.length, 0);
+
+ let onWorkerListChanged = waitForWorkerListChanged(target);
+ await SpecialPowers.spawn(tab.linkedBrowser, [WORKER1_URL], workerUrl => {
+ content.worker1 = new content.Worker(workerUrl);
+ });
+ await onWorkerListChanged;
+
+ ({ workers } = await listWorkers(target));
+ is(workers.length, 1);
+ is(workers[0].url, WORKER1_URL);
+
+ onWorkerListChanged = waitForWorkerListChanged(target);
+ await SpecialPowers.spawn(tab.linkedBrowser, [WORKER2_URL], workerUrl => {
+ content.worker2 = new content.Worker(workerUrl);
+ });
+ await onWorkerListChanged;
+
+ ({ workers } = await listWorkers(target));
+ is(workers.length, 2);
+ is(workers[0].url, WORKER1_URL);
+ is(workers[1].url, WORKER2_URL);
+
+ onWorkerListChanged = waitForWorkerListChanged(target);
+ await SpecialPowers.spawn(tab.linkedBrowser, [WORKER2_URL], workerUrl => {
+ content.worker1.terminate();
+ });
+ await onWorkerListChanged;
+
+ ({ workers } = await listWorkers(target));
+ is(workers.length, 1);
+ is(workers[0].url, WORKER2_URL);
+
+ onWorkerListChanged = waitForWorkerListChanged(target);
+ await SpecialPowers.spawn(tab.linkedBrowser, [WORKER2_URL], workerUrl => {
+ content.worker2.terminate();
+ });
+ await onWorkerListChanged;
+
+ ({ workers } = await listWorkers(target));
+ is(workers.length, 0);
+
+ await target.destroy();
+ finish();
+});
+
+function listWorkers(targetFront) {
+ info("Listing workers.");
+ return targetFront.listWorkers();
+}
+
+function waitForWorkerListChanged(targetFront) {
+ info("Waiting for worker list to change.");
+ return targetFront.once("workerListChanged");
+}
diff --git a/devtools/client/shared/test/browser_dbg_multiple-windows.js b/devtools/client/shared/test/browser_dbg_multiple-windows.js
new file mode 100644
index 0000000000..2e2013479c
--- /dev/null
+++ b/devtools/client/shared/test/browser_dbg_multiple-windows.js
@@ -0,0 +1,122 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Make sure that the debugger attaches to the right tab when multiple windows
+ * are open.
+ */
+
+var {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+var {
+ DevToolsClient,
+} = require("resource://devtools/client/devtools-client.js");
+
+const TAB1_URL = "data:text/html;charset=utf-8,first-tab";
+const TAB2_URL = "data:text/html;charset=utf-8,second-tab";
+
+add_task(async function () {
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+
+ const transport = DevToolsServer.connectPipe();
+ const client = new DevToolsClient(transport);
+ const [type] = await client.connect();
+ is(type, "browser", "Root actor should identify itself as a browser.");
+
+ const tab = await addTab(TAB1_URL);
+ await testFirstTab(client, tab);
+ const win = await addWindow(TAB2_URL);
+ await testNewWindow(client, win);
+ testFocusFirst(client);
+ await testRemoveTab(client, win, tab);
+ await client.close();
+});
+
+async function testFirstTab(client, tab) {
+ ok(!!tab, "Second tab created.");
+
+ const tabs = await client.mainRoot.listTabs();
+ const targetFront = tabs.find(grip => grip.url == TAB1_URL);
+ ok(targetFront, "Should find a target actor for the first tab.");
+
+ ok(!tabs[0].selected, "The previously opened tab isn't selected.");
+ ok(tabs[1].selected, "The first tab is selected.");
+}
+
+async function testNewWindow(client, win) {
+ ok(!!win, "Second window created.");
+
+ win.focus();
+
+ const topWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ is(topWindow, win, "The second window is on top.");
+
+ if (Services.focus.activeWindow != win) {
+ await new Promise(resolve => {
+ win.addEventListener(
+ "activate",
+ function onActivate(event) {
+ if (event.target != win) {
+ return;
+ }
+ win.removeEventListener("activate", onActivate, true);
+ resolve();
+ },
+ true
+ );
+ });
+ }
+
+ const tabs = await client.mainRoot.listTabs();
+ ok(!tabs[0].selected, "The previously opened tab isn't selected.");
+ ok(!tabs[1].selected, "The first tab isn't selected.");
+ ok(tabs[2].selected, "The second tab is selected.");
+}
+
+async function testFocusFirst(client) {
+ const tab = window.gBrowser.selectedTab;
+ await ContentTask.spawn(tab.linkedBrowser, null, async function () {
+ const onFocus = new Promise(resolve => {
+ content.addEventListener("focus", resolve, { once: true });
+ });
+ await onFocus;
+ });
+
+ const tabs = await client.mainRoot.listTabs();
+ ok(!tabs[0].selected, "The previously opened tab isn't selected.");
+ ok(!tabs[1].selected, "The first tab is selected after focusing on i.");
+ ok(tabs[2].selected, "The second tab isn't selected.");
+}
+
+async function testRemoveTab(client, win, tab) {
+ win.close();
+
+ // give it time to close
+ await new Promise(resolve => executeSoon(resolve));
+ await continue_remove_tab(client, tab);
+}
+
+async function continue_remove_tab(client, tab) {
+ removeTab(tab);
+
+ const tabs = await client.mainRoot.listTabs();
+
+ // Verify that tabs are no longer included in listTabs.
+ const foundTab1 = tabs.some(grip => grip.url == TAB1_URL);
+ const foundTab2 = tabs.some(grip => grip.url == TAB2_URL);
+ ok(!foundTab1, "Tab1 should be gone.");
+ ok(!foundTab2, "Tab2 should be gone.");
+
+ ok(tabs[0].selected, "The previously opened tab is selected.");
+}
+
+async function addWindow(url) {
+ info("Adding window: " + url);
+ const onNewWindow = BrowserTestUtils.waitForNewWindow({ url });
+ window.open(url, "_blank", "noopener");
+ return onNewWindow;
+}
diff --git a/devtools/client/shared/test/browser_dbg_target-scoped-actor-01.js b/devtools/client/shared/test/browser_dbg_target-scoped-actor-01.js
new file mode 100644
index 0000000000..004c7bbc9d
--- /dev/null
+++ b/devtools/client/shared/test/browser_dbg_target-scoped-actor-01.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check target-scoped actor lifetimes.
+ */
+
+const ACTORS_URL = EXAMPLE_URL + "testactors.js";
+const TAB_URL = TEST_URI_ROOT + "doc_empty-tab-01.html";
+
+add_task(async function test() {
+ const tab = await addTab(TAB_URL);
+
+ await registerActorInContentProcess(ACTORS_URL, {
+ prefix: "testOne",
+ constructor: "TestActor1",
+ type: { target: true },
+ });
+
+ const target = await createAndAttachTargetForTab(tab);
+ const { client } = target;
+ const form = target.targetForm;
+
+ await testTargetScopedActor(client, form);
+ await removeTab(gBrowser.selectedTab);
+ await target.destroy();
+});
+
+async function testTargetScopedActor(client, form) {
+ ok(form.testOneActor, "Found the test target-scoped actor.");
+ ok(
+ form.testOneActor.includes("testOne"),
+ "testOneActor's typeName should be used."
+ );
+
+ const response = await client.request({
+ to: form.testOneActor,
+ type: "ping",
+ });
+ is(response.pong, "pong", "Actor should respond to requests.");
+}
diff --git a/devtools/client/shared/test/browser_dbg_target-scoped-actor-02.js b/devtools/client/shared/test/browser_dbg_target-scoped-actor-02.js
new file mode 100644
index 0000000000..c87eda05e0
--- /dev/null
+++ b/devtools/client/shared/test/browser_dbg_target-scoped-actor-02.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check target-scoped actor lifetimes.
+ */
+
+const ACTORS_URL = EXAMPLE_URL + "testactors.js";
+const TAB_URL = TEST_URI_ROOT + "doc_empty-tab-01.html";
+
+add_task(async function () {
+ const tab = await addTab(TAB_URL);
+
+ await registerActorInContentProcess(ACTORS_URL, {
+ prefix: "testOne",
+ constructor: "TestActor1",
+ type: { target: true },
+ });
+
+ const target = await createAndAttachTargetForTab(tab);
+ const { client } = target;
+ const form = target.targetForm;
+
+ await testTargetScopedActor(client, form);
+ await closeTab(client, form);
+ await target.destroy();
+});
+
+async function testTargetScopedActor(client, form) {
+ ok(form.testOneActor, "Found the test target-scoped actor.");
+ ok(
+ form.testOneActor.includes("testOne"),
+ "testOneActor's typeName should be used."
+ );
+
+ const response = await client.request({
+ to: form.testOneActor,
+ type: "ping",
+ });
+ is(response.pong, "pong", "Actor should respond to requests.");
+}
+
+async function closeTab(client, form) {
+ // We need to start listening for the rejection before removing the tab
+ /* eslint-disable-next-line mozilla/rejects-requires-await*/
+ const onReject = Assert.rejects(
+ client.request({ to: form.testOneActor, type: "ping" }),
+ err =>
+ err.message ===
+ `'ping' active request packet to '${form.testOneActor}' ` +
+ `can't be sent as the connection just closed.`,
+ "testOneActor went away."
+ );
+ await removeTab(gBrowser.selectedTab);
+ await onReject;
+}
diff --git a/devtools/client/shared/test/browser_devices.js b/devtools/client/shared/test/browser_devices.js
new file mode 100644
index 0000000000..35a68d2714
--- /dev/null
+++ b/devtools/client/shared/test/browser_devices.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ getDevices,
+ getDeviceString,
+ addDevice,
+} = require("resource://devtools/client/shared/devices.js");
+
+add_task(async function () {
+ let devices = await getDevices();
+
+ let types = [...devices.keys()];
+ ok(!!types.length, `Found ${types.length} device types.`);
+
+ for (const type of types) {
+ const string = getDeviceString(type);
+ ok(
+ typeof string === "string" &&
+ !!string.length &&
+ string != `device.${type}`,
+ `Able to localize "${type}": "${string}"`
+ );
+
+ ok(
+ !!devices.get(type).length,
+ `Found ${devices.get(type).length} ${type} devices`
+ );
+ }
+
+ const type1 = types[0];
+ const type1DeviceCount = devices.get(type1).length;
+
+ const device1 = {
+ name: "SquarePhone",
+ width: 320,
+ height: 320,
+ pixelRatio: 2,
+ userAgent: "Mozilla/5.0 (Mobile; rv:42.0)",
+ touch: true,
+ firefoxOS: true,
+ };
+ addDevice(device1, types[0]);
+ devices = await getDevices();
+
+ is(
+ devices.get(type1).length,
+ type1DeviceCount + 1,
+ `Added new device of type "${type1}".`
+ );
+ ok(
+ devices.get(type1).find(d => d.name === device1.name),
+ "Found the new device."
+ );
+
+ const type2 = "appliances";
+ const device2 = {
+ name: "Mr Freezer",
+ width: 800,
+ height: 600,
+ pixelRatio: 5,
+ userAgent: "Mozilla/5.0 (Appliance; rv:42.0)",
+ touch: true,
+ firefoxOS: true,
+ };
+
+ const typeCount = types.length;
+ addDevice(device2, type2);
+ devices = await getDevices();
+ types = [...devices.keys()];
+
+ is(types.length, typeCount + 1, `Added device type "${type2}".`);
+ is(devices.get(type2).length, 1, `Added new "${type2}" device`);
+});
diff --git a/devtools/client/shared/test/browser_filter-editor-01.js b/devtools/client/shared/test/browser_filter-editor-01.js
new file mode 100644
index 0000000000..557a02857c
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-01.js
@@ -0,0 +1,150 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the Filter Editor Widget parses filter values correctly (setCssValue)
+
+const {
+ CSSFilterEditorWidget,
+} = require("resource://devtools/client/shared/widgets/FilterWidget.js");
+
+const TEST_URI = CHROME_URL_ROOT + "doc_filter-editor-01.html";
+const { getCSSLexer } = require("resource://devtools/shared/css/lexer.js");
+
+// Verify that the given string consists of a valid CSS URL token.
+// Return true on success, false on error.
+function verifyURL(string) {
+ const lexer = getCSSLexer(string);
+
+ const token = lexer.nextToken();
+ if (!token || token.tokenType !== "url") {
+ return false;
+ }
+
+ return lexer.nextToken() === null;
+}
+
+add_task(async function () {
+ const { doc } = await createHost("bottom", TEST_URI);
+
+ const container = doc.querySelector("#filter-container");
+ const widget = new CSSFilterEditorWidget(container, "none");
+
+ info("Test parsing of a valid CSS Filter value");
+ widget.setCssValue("blur(2px) contrast(200%)");
+ is(
+ widget.getCssValue(),
+ "blur(2px) contrast(200%)",
+ "setCssValue should work for computed values"
+ );
+
+ info("Test parsing of space-filled value");
+ widget.setCssValue("blur( 2px ) contrast( 2 )");
+ is(
+ widget.getCssValue(),
+ "blur(2px) contrast(200%)",
+ "setCssValue should work for spaced values"
+ );
+
+ info("Test parsing of string-typed values");
+ widget.setCssValue(
+ "drop-shadow( 2px 1px 5px black) url( example.svg#filter )"
+ );
+
+ is(
+ widget.getCssValue(),
+ "drop-shadow(2px 1px 5px black) url(example.svg#filter)",
+ "setCssValue should work for string-typed values"
+ );
+
+ info("Test parsing of mixed-case function names");
+ widget.setCssValue("BLUR(2px) Contrast(200%) Drop-Shadow(2px 1px 5px Black)");
+ is(
+ widget.getCssValue(),
+ "BLUR(2px) Contrast(200%) Drop-Shadow(2px 1px 5px Black)",
+ "setCssValue should work for mixed-case function names"
+ );
+
+ info("Test parsing of invalid filter value");
+ widget.setCssValue("totallyinvalid");
+ is(
+ widget.getCssValue(),
+ "none",
+ "setCssValue should turn completely invalid value to 'none'"
+ );
+
+ info("Test parsing of invalid function argument");
+ widget.setCssValue("blur('hello')");
+ is(
+ widget.getCssValue(),
+ "blur(0px)",
+ "setCssValue should replace invalid function argument with default"
+ );
+
+ info("Test parsing of invalid function argument #2");
+ widget.setCssValue("drop-shadow(whatever)");
+ is(
+ widget.getCssValue(),
+ "drop-shadow()",
+ "setCssValue should replace invalid drop-shadow argument with empty string"
+ );
+
+ info("Test parsing of mixed invalid argument");
+ widget.setCssValue("contrast(5%) whatever invert('xxx')");
+ is(
+ widget.getCssValue(),
+ "contrast(5%) invert(0%)",
+ "setCssValue should handle multiple errors"
+ );
+
+ info("Test parsing of 'unset'");
+ widget.setCssValue("unset");
+ is(widget.getCssValue(), "unset", "setCssValue should handle 'unset'");
+ info("Test parsing of 'initial'");
+ widget.setCssValue("initial");
+ is(widget.getCssValue(), "initial", "setCssValue should handle 'initial'");
+ info("Test parsing of 'inherit'");
+ widget.setCssValue("inherit");
+ is(widget.getCssValue(), "inherit", "setCssValue should handle 'inherit'");
+
+ info("Test parsing of quoted URL");
+ widget.setCssValue("url('invalid ) when ) unquoted')");
+ is(
+ widget.getCssValue(),
+ "url('invalid ) when ) unquoted')",
+ "setCssValue should re-quote single-quoted URL contents"
+ );
+ widget.setCssValue('url("invalid ) when ) unquoted")');
+ is(
+ widget.getCssValue(),
+ 'url("invalid ) when ) unquoted")',
+ "setCssValue should re-quote double-quoted URL contents"
+ );
+ widget.setCssValue("url(ordinary)");
+ is(
+ widget.getCssValue(),
+ "url(ordinary)",
+ "setCssValue should not quote ordinary unquoted URL contents"
+ );
+
+ const quotedurl =
+ "url(invalid\\ \\)\\ {\\\twhen\\ }\\ ;\\ \\\\unquoted\\'\\\")";
+ ok(verifyURL(quotedurl), "weird URL is valid");
+ widget.setCssValue(quotedurl);
+ is(
+ widget.getCssValue(),
+ quotedurl,
+ "setCssValue should re-quote weird unquoted URL contents"
+ );
+
+ const dataurl =
+ "url(data:image/svg+xml;utf8,#blur)';
+ ok(verifyURL(dataurl), "data URL is valid");
+ widget.setCssValue(dataurl);
+ is(widget.getCssValue(), dataurl, "setCssValue should not mangle data urls");
+
+ widget.destroy();
+});
diff --git a/devtools/client/shared/test/browser_filter-editor-02.js b/devtools/client/shared/test/browser_filter-editor-02.js
new file mode 100644
index 0000000000..2a69f5265f
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-02.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the Filter Editor Widget renders filters correctly
+
+const {
+ CSSFilterEditorWidget,
+} = require("resource://devtools/client/shared/widgets/FilterWidget.js");
+
+const STRINGS_URI = "devtools/client/locales/filterwidget.properties";
+const L10N = new LocalizationHelper(STRINGS_URI);
+
+const TEST_URI = CHROME_URL_ROOT + "doc_filter-editor-01.html";
+
+add_task(async function () {
+ const { doc } = await createHost("bottom", TEST_URI);
+
+ const TEST_DATA = [
+ {
+ cssValue:
+ "blur(2px) contrast(200%) hue-rotate(20.2deg) drop-shadow(5px 5px black)",
+ expected: [
+ {
+ label: "blur",
+ value: "2",
+ unit: "px",
+ },
+ {
+ label: "contrast",
+ value: "200",
+ unit: "%",
+ },
+ {
+ label: "hue-rotate",
+ value: "20.2",
+ unit: "deg",
+ },
+ {
+ label: "drop-shadow",
+ value: "5px 5px black",
+ unit: null,
+ },
+ ],
+ },
+ {
+ cssValue: "hue-rotate(420.2deg)",
+ expected: [
+ {
+ label: "hue-rotate",
+ value: "420.2",
+ unit: "deg",
+ },
+ ],
+ },
+ {
+ cssValue: "url(example.svg)",
+ expected: [
+ {
+ label: "url",
+ value: "example.svg",
+ unit: null,
+ },
+ ],
+ },
+ {
+ cssValue: "none",
+ expected: [],
+ },
+ ];
+
+ const container = doc.querySelector("#filter-container");
+ const widget = new CSSFilterEditorWidget(container, "none");
+
+ info("Test rendering of different types");
+
+ for (const { cssValue, expected } of TEST_DATA) {
+ widget.setCssValue(cssValue);
+
+ if (cssValue === "none") {
+ const text = container.querySelector("#filters").textContent;
+ Assert.greater(
+ text.indexOf(L10N.getStr("emptyFilterList")),
+ -1,
+ "Contains |emptyFilterList| string when given value 'none'"
+ );
+ Assert.greater(
+ text.indexOf(L10N.getStr("addUsingList")),
+ -1,
+ "Contains |addUsingList| string when given value 'none'"
+ );
+ continue;
+ }
+ const filters = container.querySelectorAll(".filter");
+ testRenderedFilters(filters, expected);
+ }
+ widget.destroy();
+});
+
+function testRenderedFilters(filters, expected) {
+ for (const [index, filter] of [...filters].entries()) {
+ const [name, value] = filter.children,
+ label = name.children[1],
+ [input, unit] = value.children;
+
+ const eq = expected[index];
+ is(label.textContent, eq.label, "Label should match");
+ is(input.value, eq.value, "Values should match");
+ if (eq.unit) {
+ is(unit.textContent, eq.unit, "Unit should match");
+ }
+ }
+}
diff --git a/devtools/client/shared/test/browser_filter-editor-03.js b/devtools/client/shared/test/browser_filter-editor-03.js
new file mode 100644
index 0000000000..4c66134ea9
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-03.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the Filter Editor Widget add, removeAt, updateAt, getValueAt methods
+
+const {
+ CSSFilterEditorWidget,
+} = require("resource://devtools/client/shared/widgets/FilterWidget.js");
+const GRAYSCALE_MAX = 100;
+const INVERT_MIN = 0;
+
+const TEST_URI = CHROME_URL_ROOT + "doc_filter-editor-01.html";
+
+add_task(async function () {
+ const { doc } = await createHost("bottom", TEST_URI);
+
+ const container = doc.querySelector("#filter-container");
+ const widget = new CSSFilterEditorWidget(container, "none");
+
+ info("Test add method");
+ const blur = widget.add("blur", "10.2px");
+ is(widget.getCssValue(), "blur(10.2px)", "Should add filters");
+
+ const url = widget.add("url", "test.svg");
+ is(
+ widget.getCssValue(),
+ "blur(10.2px) url(test.svg)",
+ "Should add filters in order"
+ );
+
+ info("Test updateValueAt method");
+ widget.updateValueAt(url, "test2.svg");
+ widget.updateValueAt(blur, 5);
+ is(
+ widget.getCssValue(),
+ "blur(5px) url(test2.svg)",
+ "Should update values correctly"
+ );
+
+ info("Test getValueAt method");
+ is(widget.getValueAt(blur), "5px", "Should return value + unit");
+ is(
+ widget.getValueAt(url),
+ "test2.svg",
+ "Should return value for string-type filters"
+ );
+
+ info("Test removeAt method");
+ widget.removeAt(url);
+ is(widget.getCssValue(), "blur(5px)", "Should remove the specified filter");
+
+ info("Test add method applying filter range to value");
+ const grayscale = widget.add("grayscale", GRAYSCALE_MAX + 1);
+ is(
+ widget.getValueAt(grayscale),
+ `${GRAYSCALE_MAX}%`,
+ "Shouldn't allow values higher than max"
+ );
+
+ const invert = widget.add("invert", INVERT_MIN - 1);
+ is(
+ widget.getValueAt(invert),
+ `${INVERT_MIN}%`,
+ "Shouldn't allow values less than INVERT_MIN"
+ );
+
+ info("Test updateValueAt method applying filter range to value");
+ widget.updateValueAt(grayscale, GRAYSCALE_MAX + 1);
+ is(
+ widget.getValueAt(grayscale),
+ `${GRAYSCALE_MAX}%`,
+ "Shouldn't allow values higher than max"
+ );
+
+ widget.updateValueAt(invert, INVERT_MIN - 1);
+ is(
+ widget.getValueAt(invert),
+ `${INVERT_MIN}%`,
+ "Shouldn't allow values less than INVERT_MIN"
+ );
+ widget.destroy();
+});
diff --git a/devtools/client/shared/test/browser_filter-editor-04.js b/devtools/client/shared/test/browser_filter-editor-04.js
new file mode 100644
index 0000000000..fdc966fa7f
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-04.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the Filter Editor Widget's drag-drop re-ordering
+
+const {
+ CSSFilterEditorWidget,
+} = require("resource://devtools/client/shared/widgets/FilterWidget.js");
+const LIST_ITEM_HEIGHT = 32;
+
+const TEST_URI = CHROME_URL_ROOT + "doc_filter-editor-01.html";
+
+add_task(async function () {
+ const { doc } = await createHost("bottom", TEST_URI);
+
+ const container = doc.querySelector("#filter-container");
+ const initialValue = "blur(2px) contrast(200%) brightness(200%)";
+ const widget = new CSSFilterEditorWidget(container, initialValue);
+
+ const filters = widget.el.querySelector("#filters");
+ function first() {
+ return filters.children[0];
+ }
+ function mid() {
+ return filters.children[1];
+ }
+ function last() {
+ return filters.children[2];
+ }
+
+ info("Test re-ordering neighbour filters");
+ widget._mouseDown({
+ target: first().querySelector("i"),
+ pageY: 0,
+ });
+ widget._mouseMove({ pageY: LIST_ITEM_HEIGHT });
+
+ // Element re-ordering should be instant
+ is(
+ mid().querySelector("label").textContent,
+ "blur",
+ "Should reorder elements correctly"
+ );
+
+ widget._mouseUp();
+
+ is(
+ widget.getCssValue(),
+ "contrast(200%) blur(2px) brightness(200%)",
+ "Should reorder filters objects correctly"
+ );
+
+ info("Test re-ordering first and last filters");
+ widget._mouseDown({
+ target: first().querySelector("i"),
+ pageY: 0,
+ });
+ widget._mouseMove({ pageY: LIST_ITEM_HEIGHT * 2 });
+
+ // Element re-ordering should be instant
+ is(
+ last().querySelector("label").textContent,
+ "contrast",
+ "Should reorder elements correctly"
+ );
+ widget._mouseUp();
+
+ is(
+ widget.getCssValue(),
+ "brightness(200%) blur(2px) contrast(200%)",
+ "Should reorder filters objects correctly"
+ );
+
+ info("Test dragging first element out of list");
+ const boundaries = filters.getBoundingClientRect();
+
+ widget._mouseDown({
+ target: first().querySelector("i"),
+ pageY: 0,
+ });
+ widget._mouseMove({ pageY: -LIST_ITEM_HEIGHT * 5 });
+ Assert.greaterOrEqual(
+ first().getBoundingClientRect().top,
+ boundaries.top,
+ "First filter should not move outside filter list"
+ );
+
+ widget._mouseUp();
+
+ info("Test dragging last element out of list");
+ widget._mouseDown({
+ target: last().querySelector("i"),
+ pageY: 0,
+ });
+ widget._mouseMove({ pageY: -LIST_ITEM_HEIGHT * 5 });
+ Assert.lessOrEqual(
+ last().getBoundingClientRect().bottom,
+ boundaries.bottom,
+ "Last filter should not move outside filter list"
+ );
+
+ widget._mouseUp();
+ widget.destroy();
+});
diff --git a/devtools/client/shared/test/browser_filter-editor-05.js b/devtools/client/shared/test/browser_filter-editor-05.js
new file mode 100644
index 0000000000..37c53ff6f4
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-05.js
@@ -0,0 +1,166 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+// Tests the Filter Editor Widget's label-dragging
+
+const {
+ CSSFilterEditorWidget,
+} = require("resource://devtools/client/shared/widgets/FilterWidget.js");
+
+const FAST_VALUE_MULTIPLIER = 10;
+const SLOW_VALUE_MULTIPLIER = 0.1;
+const DEFAULT_VALUE_MULTIPLIER = 1;
+
+const GRAYSCALE_MAX = 100,
+ GRAYSCALE_MIN = 0;
+
+const TEST_URI = CHROME_URL_ROOT + "doc_filter-editor-01.html";
+
+add_task(async function () {
+ const { doc } = await createHost("bottom", TEST_URI);
+
+ const container = doc.querySelector("#filter-container");
+ const widget = new CSSFilterEditorWidget(
+ container,
+ "grayscale(0%) url(test.svg)"
+ );
+
+ const filters = widget.el.querySelector("#filters");
+ const grayscale = filters.children[0];
+ const url = filters.children[1];
+
+ info("Test label-dragging on number-type filters without modifiers");
+ widget._mouseDown({
+ target: grayscale.querySelector("label"),
+ pageX: 0,
+ altKey: false,
+ shiftKey: false,
+ });
+
+ widget._mouseMove({
+ pageX: 12,
+ altKey: false,
+ shiftKey: false,
+ });
+ let expected = DEFAULT_VALUE_MULTIPLIER * 12;
+ is(
+ widget.getValueAt(0),
+ `${expected}%`,
+ "Should update value correctly without modifiers"
+ );
+
+ info("Test label-dragging on number-type filters with alt");
+ widget._mouseMove({
+ // 20 - 12 = 8
+ pageX: 20,
+ altKey: true,
+ shiftKey: false,
+ });
+
+ expected = expected + SLOW_VALUE_MULTIPLIER * 8;
+ is(
+ widget.getValueAt(0),
+ `${expected}%`,
+ "Should update value correctly with alt key"
+ );
+
+ info("Test label-dragging on number-type filters with shift");
+ widget._mouseMove({
+ // 25 - 20 = 5
+ pageX: 25,
+ altKey: false,
+ shiftKey: true,
+ });
+
+ expected = expected + FAST_VALUE_MULTIPLIER * 5;
+ is(
+ widget.getValueAt(0),
+ `${expected}%`,
+ "Should update value correctly with shift key"
+ );
+
+ info("Test releasing mouse and dragging again");
+
+ widget._mouseUp();
+
+ widget._mouseDown({
+ target: grayscale.querySelector("label"),
+ pageX: 0,
+ altKey: false,
+ shiftKey: false,
+ });
+
+ widget._mouseMove({
+ pageX: 5,
+ altKey: false,
+ shiftKey: false,
+ });
+
+ expected = expected + DEFAULT_VALUE_MULTIPLIER * 5;
+ is(
+ widget.getValueAt(0),
+ `${expected}%`,
+ "Should reset multiplier to default"
+ );
+
+ info("Test value ranges");
+
+ widget._mouseMove({
+ // 30 - 25 = 5
+ pageX: 30,
+ altKey: false,
+ shiftKey: true,
+ });
+
+ expected = GRAYSCALE_MAX;
+ is(
+ widget.getValueAt(0),
+ `${expected}%`,
+ "Shouldn't allow values higher than max"
+ );
+
+ widget._mouseMove({
+ pageX: -11,
+ altKey: false,
+ shiftKey: true,
+ });
+
+ expected = GRAYSCALE_MIN;
+ is(
+ widget.getValueAt(0),
+ `${expected}%`,
+ "Shouldn't allow values less than min"
+ );
+
+ widget._mouseUp();
+
+ info("Test label-dragging on string-type filters");
+ widget._mouseDown({
+ target: url.querySelector("label"),
+ pageX: 0,
+ altKey: false,
+ shiftKey: false,
+ });
+
+ ok(
+ !widget.isDraggingLabel,
+ "Label-dragging should not work for string-type filters"
+ );
+
+ widget._mouseMove({
+ pageX: -11,
+ altKey: false,
+ shiftKey: true,
+ });
+
+ is(
+ widget.getValueAt(1),
+ "test.svg",
+ "Label-dragging on string-type filters shouldn't affect their value"
+ );
+ widget.destroy();
+});
diff --git a/devtools/client/shared/test/browser_filter-editor-06.js b/devtools/client/shared/test/browser_filter-editor-06.js
new file mode 100644
index 0000000000..0f5f5abd4c
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-06.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the Filter Editor Widget's add button
+
+const {
+ CSSFilterEditorWidget,
+} = require("resource://devtools/client/shared/widgets/FilterWidget.js");
+
+const STRINGS_URI = "devtools/client/locales/filterwidget.properties";
+const L10N = new LocalizationHelper(STRINGS_URI);
+
+const TEST_URI = CHROME_URL_ROOT + "doc_filter-editor-01.html";
+
+add_task(async function () {
+ const { doc } = await createHost("bottom", TEST_URI);
+
+ const container = doc.querySelector("#filter-container");
+ const widget = new CSSFilterEditorWidget(container, "none");
+
+ const select = widget.el.querySelector("select"),
+ add = widget.el.querySelector("#add-filter");
+
+ const TEST_DATA = [
+ {
+ name: "blur",
+ unit: "px",
+ type: "length",
+ },
+ {
+ name: "contrast",
+ unit: "%",
+ type: "percentage",
+ },
+ {
+ name: "hue-rotate",
+ unit: "deg",
+ type: "angle",
+ },
+ {
+ name: "drop-shadow",
+ placeholder: L10N.getStr("dropShadowPlaceholder"),
+ type: "string",
+ },
+ {
+ name: "url",
+ placeholder: "example.svg#c1",
+ type: "string",
+ },
+ ];
+
+ info("Test adding new filters with different units");
+
+ for (const [index, filter] of TEST_DATA.entries()) {
+ select.value = filter.name;
+ add.click();
+
+ if (filter.unit) {
+ is(
+ widget.getValueAt(index),
+ `0${filter.unit}`,
+ `Should add ${filter.unit} to ${filter.type} filters`
+ );
+ } else if (filter.placeholder) {
+ const i = index + 1;
+ const input = widget.el.querySelector(`.filter:nth-child(${i}) input`);
+ is(
+ input.placeholder,
+ filter.placeholder,
+ "Should set the appropriate placeholder for string-type filters"
+ );
+ }
+ }
+ widget.destroy();
+});
diff --git a/devtools/client/shared/test/browser_filter-editor-07.js b/devtools/client/shared/test/browser_filter-editor-07.js
new file mode 100644
index 0000000000..354d46addc
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-07.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the Filter Editor Widget's remove button
+
+const {
+ CSSFilterEditorWidget,
+} = require("resource://devtools/client/shared/widgets/FilterWidget.js");
+
+const TEST_URI = CHROME_URL_ROOT + "doc_filter-editor-01.html";
+
+add_task(async function () {
+ const { doc } = await createHost("bottom", TEST_URI);
+
+ const container = doc.querySelector("#filter-container");
+ const widget = new CSSFilterEditorWidget(
+ container,
+ "blur(2px) contrast(200%)"
+ );
+
+ info("Test removing filters with remove button");
+ widget.el.querySelector(".filter button").click();
+
+ is(
+ widget.getCssValue(),
+ "contrast(200%)",
+ "Should remove the clicked filter"
+ );
+ widget.destroy();
+});
diff --git a/devtools/client/shared/test/browser_filter-editor-08.js b/devtools/client/shared/test/browser_filter-editor-08.js
new file mode 100644
index 0000000000..8759a230a0
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-08.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the Filter Editor Widget inputs increase/decrease value using
+// arrow keys, applying multiplier using alt/shift on number-type filters
+
+const {
+ CSSFilterEditorWidget,
+} = require("resource://devtools/client/shared/widgets/FilterWidget.js");
+
+const FAST_VALUE_MULTIPLIER = 10;
+const SLOW_VALUE_MULTIPLIER = 0.1;
+const DEFAULT_VALUE_MULTIPLIER = 1;
+
+const TEST_URI = CHROME_URL_ROOT + "doc_filter-editor-01.html";
+
+add_task(async function () {
+ const { doc } = await createHost("bottom", TEST_URI);
+
+ const container = doc.querySelector("#filter-container");
+ const initialValue = "blur(2px)";
+ const widget = new CSSFilterEditorWidget(container, initialValue);
+
+ let value = 2;
+
+ triggerKey = triggerKey.bind(widget);
+
+ info("Test simple arrow keys");
+ triggerKey(40);
+
+ value -= DEFAULT_VALUE_MULTIPLIER;
+ is(
+ widget.getValueAt(0),
+ `${value}px`,
+ "Should decrease value using down arrow"
+ );
+
+ triggerKey(38);
+
+ value += DEFAULT_VALUE_MULTIPLIER;
+ is(
+ widget.getValueAt(0),
+ `${value}px`,
+ "Should decrease value using down arrow"
+ );
+
+ info("Test shift key multiplier");
+ triggerKey(38, "shiftKey");
+
+ value += FAST_VALUE_MULTIPLIER;
+ is(
+ widget.getValueAt(0),
+ `${value}px`,
+ "Should increase value by fast multiplier using up arrow"
+ );
+
+ triggerKey(40, "shiftKey");
+
+ value -= FAST_VALUE_MULTIPLIER;
+ is(
+ widget.getValueAt(0),
+ `${value}px`,
+ "Should decrease value by fast multiplier using down arrow"
+ );
+
+ info("Test alt key multiplier");
+ triggerKey(38, "altKey");
+
+ value += SLOW_VALUE_MULTIPLIER;
+ is(
+ widget.getValueAt(0),
+ `${value}px`,
+ "Should increase value by slow multiplier using up arrow"
+ );
+
+ triggerKey(40, "altKey");
+
+ value -= SLOW_VALUE_MULTIPLIER;
+ is(
+ widget.getValueAt(0),
+ `${value}px`,
+ "Should decrease value by slow multiplier using down arrow"
+ );
+
+ widget.destroy();
+ triggerKey = null;
+});
+
+// Triggers the specified keyCode and modifier key on
+// first filter's input
+function triggerKey(key, modifier) {
+ const filter = this.el.querySelector("#filters").children[0];
+ const input = filter.querySelector("input");
+
+ this._keyDown({
+ target: input,
+ keyCode: key,
+ [modifier]: true,
+ preventDefault() {},
+ });
+}
diff --git a/devtools/client/shared/test/browser_filter-editor-09.js b/devtools/client/shared/test/browser_filter-editor-09.js
new file mode 100644
index 0000000000..e22898cfd5
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-09.js
@@ -0,0 +1,155 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the Filter Editor Widget inputs increase/decrease value when cursor is
+// on a number using arrow keys, applying multiplier using alt/shift on strings
+
+const {
+ CSSFilterEditorWidget,
+} = require("resource://devtools/client/shared/widgets/FilterWidget.js");
+
+const FAST_VALUE_MULTIPLIER = 10;
+const SLOW_VALUE_MULTIPLIER = 0.1;
+const DEFAULT_VALUE_MULTIPLIER = 1;
+
+const TEST_URI = CHROME_URL_ROOT + "doc_filter-editor-01.html";
+
+add_task(async function () {
+ const { doc } = await createHost("bottom", TEST_URI);
+
+ const container = doc.querySelector("#filter-container");
+ const initialValue = "drop-shadow(rgb(0, 0, 0) 1px 1px 0px)";
+ const widget = new CSSFilterEditorWidget(container, initialValue);
+ widget.el.querySelector("#filters input").setSelectionRange(13, 13);
+
+ let value = 1;
+
+ triggerKey = triggerKey.bind(widget);
+
+ info("Test simple arrow keys");
+ triggerKey(40);
+
+ value -= DEFAULT_VALUE_MULTIPLIER;
+ is(
+ widget.getValueAt(0),
+ val(value),
+ "Should decrease value using down arrow"
+ );
+
+ triggerKey(38);
+
+ value += DEFAULT_VALUE_MULTIPLIER;
+ is(
+ widget.getValueAt(0),
+ val(value),
+ "Should decrease value using down arrow"
+ );
+
+ info("Test shift key multiplier");
+ triggerKey(38, "shiftKey");
+
+ value += FAST_VALUE_MULTIPLIER;
+ is(
+ widget.getValueAt(0),
+ val(value),
+ "Should increase value by fast multiplier using up arrow"
+ );
+
+ triggerKey(40, "shiftKey");
+
+ value -= FAST_VALUE_MULTIPLIER;
+ is(
+ widget.getValueAt(0),
+ val(value),
+ "Should decrease value by fast multiplier using down arrow"
+ );
+
+ info("Test alt key multiplier");
+ triggerKey(38, "altKey");
+
+ value += SLOW_VALUE_MULTIPLIER;
+ is(
+ widget.getValueAt(0),
+ val(value),
+ "Should increase value by slow multiplier using up arrow"
+ );
+
+ triggerKey(40, "altKey");
+
+ value -= SLOW_VALUE_MULTIPLIER;
+ is(
+ widget.getValueAt(0),
+ val(value),
+ "Should decrease value by slow multiplier using down arrow"
+ );
+
+ triggerKey(40, "shiftKey");
+
+ value -= FAST_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), val(value), "Should decrease to negative");
+
+ triggerKey(40);
+
+ value -= DEFAULT_VALUE_MULTIPLIER;
+ is(
+ widget.getValueAt(0),
+ val(value),
+ "Should decrease negative numbers correctly"
+ );
+
+ triggerKey(38);
+
+ value += DEFAULT_VALUE_MULTIPLIER;
+ is(
+ widget.getValueAt(0),
+ val(value),
+ "Should increase negative values correctly"
+ );
+
+ triggerKey(40, "altKey");
+ triggerKey(40, "altKey");
+
+ value -= SLOW_VALUE_MULTIPLIER * 2;
+ is(
+ widget.getValueAt(0),
+ val(value),
+ "Should decrease float numbers correctly"
+ );
+
+ triggerKey(38, "altKey");
+
+ value += SLOW_VALUE_MULTIPLIER;
+ is(
+ widget.getValueAt(0),
+ val(value),
+ "Should increase float numbers correctly"
+ );
+
+ widget.destroy();
+ triggerKey = null;
+});
+
+// Triggers the specified keyCode and modifier key on
+// first filter's input
+function triggerKey(key, modifier) {
+ const filter = this.el.querySelector("#filters").children[0];
+ const input = filter.querySelector("input");
+
+ this._keyDown({
+ target: input,
+ keyCode: key,
+ [modifier]: true,
+ preventDefault() {},
+ });
+}
+
+function val(value) {
+ let v = value.toFixed(1);
+
+ if (v.indexOf(".0") > -1) {
+ v = v.slice(0, -2);
+ }
+ return `rgb(0, 0, 0) ${v}px 1px 0px`;
+}
diff --git a/devtools/client/shared/test/browser_filter-editor-10.js b/devtools/client/shared/test/browser_filter-editor-10.js
new file mode 100644
index 0000000000..248ee0c506
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-10.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the Filter Editor Widget inputs increase/decrease value when cursor is
+// on a number using arrow keys if cursor is behind/mid/after the number strings
+
+const {
+ CSSFilterEditorWidget,
+} = require("resource://devtools/client/shared/widgets/FilterWidget.js");
+
+const DEFAULT_VALUE_MULTIPLIER = 1;
+
+const TEST_URI = CHROME_URL_ROOT + "doc_filter-editor-01.html";
+
+add_task(async function () {
+ const { doc } = await createHost("bottom", TEST_URI);
+
+ const container = doc.querySelector("#filter-container");
+ const initialValue = "drop-shadow(rgb(0, 0, 0) 10px 1px 0px)";
+ const widget = new CSSFilterEditorWidget(container, initialValue);
+ const input = widget.el.querySelector("#filters input");
+
+ let value = 10;
+
+ triggerKey = triggerKey.bind(widget);
+
+ info("Test increment/decrement of string-type numbers without selection");
+
+ input.setSelectionRange(14, 14);
+ triggerKey(40);
+
+ value -= DEFAULT_VALUE_MULTIPLIER;
+ is(
+ widget.getValueAt(0),
+ val(value),
+ "Should work with cursor in the middle of number"
+ );
+
+ input.setSelectionRange(13, 13);
+ triggerKey(38);
+
+ value += DEFAULT_VALUE_MULTIPLIER;
+ is(
+ widget.getValueAt(0),
+ val(value),
+ "Should work with cursor before the number"
+ );
+
+ input.setSelectionRange(15, 15);
+ triggerKey(40);
+
+ value -= DEFAULT_VALUE_MULTIPLIER;
+ is(
+ widget.getValueAt(0),
+ val(value),
+ "Should work with cursor after the number"
+ );
+
+ info("Test increment/decrement of string-type numbers with a selection");
+
+ input.setSelectionRange(13, 15);
+ triggerKey(38);
+ input.setSelectionRange(13, 18);
+ triggerKey(38);
+
+ value += DEFAULT_VALUE_MULTIPLIER * 2;
+ is(
+ widget.getValueAt(0),
+ val(value),
+ "Should work if a there is a selection, starting with the number"
+ );
+
+ widget.destroy();
+ triggerKey = null;
+});
+
+// Triggers the specified keyCode and modifier key on
+// first filter's input
+function triggerKey(key, modifier) {
+ const filter = this.el.querySelector("#filters").children[0];
+ const input = filter.querySelector("input");
+
+ this._keyDown({
+ target: input,
+ keyCode: key,
+ [modifier]: true,
+ preventDefault() {},
+ });
+}
+
+function val(value) {
+ let v = value.toFixed(1);
+
+ if (v.indexOf(".0") > -1) {
+ v = v.slice(0, -2);
+ }
+ return `rgb(0, 0, 0) ${v}px 1px 0px`;
+}
diff --git a/devtools/client/shared/test/browser_filter-presets-01.js b/devtools/client/shared/test/browser_filter-presets-01.js
new file mode 100644
index 0000000000..3a6d4a93d5
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-presets-01.js
@@ -0,0 +1,117 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests saving presets
+
+const {
+ CSSFilterEditorWidget,
+} = require("resource://devtools/client/shared/widgets/FilterWidget.js");
+
+const TEST_URI = CHROME_URL_ROOT + "doc_filter-editor-01.html";
+
+add_task(async function () {
+ const { doc } = await createHost("bottom", TEST_URI);
+
+ const container = doc.querySelector("#filter-container");
+ const widget = new CSSFilterEditorWidget(container, "none");
+ // First render
+ await widget.once("render");
+
+ const VALUE = "blur(2px) contrast(150%)";
+ const NAME = "Test";
+
+ await showFilterPopupPresetsAndCreatePreset(widget, NAME, VALUE);
+
+ const preset = widget.el.querySelector(".preset");
+ is(
+ preset.querySelector("label").textContent,
+ NAME,
+ "Should show preset name correctly"
+ );
+ is(
+ preset.querySelector("span").textContent,
+ VALUE,
+ "Should show preset value preview correctly"
+ );
+
+ let list = await widget.getPresets();
+ const input = widget.el.querySelector(".presets-list .footer input");
+ let data = list[0];
+
+ is(data.name, NAME, "Should add the preset to asyncStorage - name property");
+ is(
+ data.value,
+ VALUE,
+ "Should add the preset to asyncStorage - name property"
+ );
+
+ info("Test overriding preset by using the same name");
+
+ const VALUE_2 = "saturate(50%) brightness(10%)";
+
+ widget.setCssValue(VALUE_2);
+
+ await savePreset(widget);
+
+ is(
+ widget.el.querySelectorAll(".preset").length,
+ 1,
+ "Should override the preset with the same name - render"
+ );
+
+ list = await widget.getPresets();
+ data = list[0];
+
+ is(
+ list.length,
+ 1,
+ "Should override the preset with the same name - asyncStorage"
+ );
+
+ is(
+ data.name,
+ NAME,
+ "Should override the preset with the same name - prop name"
+ );
+ is(
+ data.value,
+ VALUE_2,
+ "Should override the preset with the same name - prop value"
+ );
+
+ await widget.setPresets([]);
+
+ info("Test saving a preset without name");
+ input.value = "";
+
+ await savePreset(widget, "preset-save-error");
+
+ list = await widget.getPresets();
+ is(list.length, 0, "Should not add a preset without name");
+
+ info("Test saving a preset without filters");
+
+ input.value = NAME;
+ widget.setCssValue("none");
+
+ await savePreset(widget, "preset-save-error");
+
+ list = await widget.getPresets();
+ is(list.length, 0, "Should not add a preset without filters (value: none)");
+});
+
+/**
+ * Call savePreset on widget and wait for the specified event to emit
+ * @param {CSSFilterWidget} widget
+ * @param {string} expectEvent="render" The event to listen on
+ * @return {Promise}
+ */
+function savePreset(widget, expectEvent = "render") {
+ const onEvent = widget.once(expectEvent);
+ widget._savePreset({
+ preventDefault: () => {},
+ });
+ return onEvent;
+}
diff --git a/devtools/client/shared/test/browser_filter-presets-02.js b/devtools/client/shared/test/browser_filter-presets-02.js
new file mode 100644
index 0000000000..ef9cb62bbe
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-presets-02.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests loading presets
+
+const {
+ CSSFilterEditorWidget,
+} = require("resource://devtools/client/shared/widgets/FilterWidget.js");
+
+const TEST_URI = CHROME_URL_ROOT + "doc_filter-editor-01.html";
+
+add_task(async function () {
+ const { doc } = await createHost("bottom", TEST_URI);
+
+ const container = doc.querySelector("#filter-container");
+ const widget = new CSSFilterEditorWidget(container, "none");
+ // First render
+ await widget.once("render");
+
+ const VALUE = "blur(2px) contrast(150%)";
+ const NAME = "Test";
+
+ await showFilterPopupPresetsAndCreatePreset(widget, NAME, VALUE);
+
+ let onRender = widget.once("render");
+ // reset value
+ widget.setCssValue("saturate(100%) brightness(150%)");
+ await onRender;
+
+ const preset = widget.el.querySelector(".preset");
+
+ onRender = widget.once("render");
+ widget._presetClick({
+ target: preset,
+ });
+
+ await onRender;
+
+ is(widget.getCssValue(), VALUE, "Should set widget's value correctly");
+ is(
+ widget.el.querySelector(".presets-list .footer input").value,
+ NAME,
+ "Should set input's value to name"
+ );
+});
diff --git a/devtools/client/shared/test/browser_filter-presets-03.js b/devtools/client/shared/test/browser_filter-presets-03.js
new file mode 100644
index 0000000000..2c679c3a1b
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-presets-03.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests deleting presets
+
+const {
+ CSSFilterEditorWidget,
+} = require("resource://devtools/client/shared/widgets/FilterWidget.js");
+
+const TEST_URI = CHROME_URL_ROOT + "doc_filter-editor-01.html";
+
+add_task(async function () {
+ const { doc } = await createHost("bottom", TEST_URI);
+
+ const container = doc.querySelector("#filter-container");
+ const widget = new CSSFilterEditorWidget(container, "none");
+ // First render
+ await widget.once("render");
+
+ const NAME = "Test";
+ const VALUE = "blur(2px) contrast(150%)";
+
+ await showFilterPopupPresetsAndCreatePreset(widget, NAME, VALUE);
+
+ const removeButton = widget.el.querySelector(".preset .remove-button");
+ const onRender = widget.once("render");
+ widget._presetClick({
+ target: removeButton,
+ });
+
+ await onRender;
+ is(
+ widget.el.querySelector(".preset"),
+ null,
+ "Should re-render after removing preset"
+ );
+
+ const list = await widget.getPresets();
+ is(list.length, 0, "Should remove presets from asyncStorage");
+});
diff --git a/devtools/client/shared/test/browser_html_tooltip-01.js b/devtools/client/shared/test/browser_html_tooltip-01.js
new file mode 100644
index 0000000000..401d9d1c61
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip-01.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip show & hide methods.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = CHROME_URL_ROOT + "doc_html_tooltip.xhtml";
+
+const {
+ HTMLTooltip,
+} = require("resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js");
+loadHelperScript("helper_html_tooltip.js");
+
+let useXulWrapper;
+
+function getTooltipContent(doc) {
+ const div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "50px";
+ div.style.boxSizing = "border-box";
+ div.textContent = "tooltip";
+ return div;
+}
+
+add_task(async function () {
+ const { doc } = await createHost("bottom", TEST_URI);
+
+ info("Run tests for a Tooltip without using a XUL panel");
+ useXulWrapper = false;
+ await runTests(doc);
+
+ info("Run tests for a Tooltip with a XUL panel");
+ useXulWrapper = true;
+ await runTests(doc);
+});
+
+async function runTests(doc) {
+ const tooltip = new HTMLTooltip(doc, { useXulWrapper });
+
+ info("Set tooltip content");
+ tooltip.panel.appendChild(getTooltipContent(doc));
+ tooltip.setContentSize({ width: 100, height: 50 });
+
+ is(tooltip.isVisible(), false, "Tooltip is not visible");
+
+ info("Show the tooltip and check the expected events are fired.");
+
+ let shown = 0;
+ tooltip.on("shown", () => shown++);
+
+ const onShown = tooltip.once("shown");
+ tooltip.show(doc.getElementById("box1"));
+ await onShown;
+ is(shown, 1, "Event shown was fired once");
+
+ await waitForReflow(tooltip);
+ is(tooltip.isVisible(), true, "Tooltip is visible");
+
+ info("Hide the tooltip and check the expected events are fired.");
+
+ let hidden = 0;
+ tooltip.on("hidden", () => hidden++);
+
+ const onPopupHidden = tooltip.once("hidden");
+ tooltip.hide();
+
+ await onPopupHidden;
+ is(hidden, 1, "Event hidden was fired once");
+
+ await waitForReflow(tooltip);
+ is(tooltip.isVisible(), false, "Tooltip is not visible");
+
+ tooltip.destroy();
+}
diff --git a/devtools/client/shared/test/browser_html_tooltip-02.js b/devtools/client/shared/test/browser_html_tooltip-02.js
new file mode 100644
index 0000000000..4a622a7e7a
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip-02.js
@@ -0,0 +1,227 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+"use strict";
+
+/**
+ * Test the HTMLTooltip is closed when clicking outside of its container.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = CHROME_URL_ROOT + "doc_html_tooltip-02.xhtml";
+const PROMISE_TIMEOUT = 3000;
+
+const {
+ HTMLTooltip,
+} = require("resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js");
+loadHelperScript("helper_html_tooltip.js");
+
+let useXulWrapper;
+
+add_task(async function () {
+ await addTab("about:blank");
+ const { doc } = await createHost("bottom", TEST_URI);
+
+ info("Run tests for a Tooltip without using a XUL panel");
+ useXulWrapper = false;
+ await runTests(doc);
+
+ info("Run tests for a Tooltip with a XUL panel");
+ useXulWrapper = true;
+ await runTests(doc);
+});
+
+async function runTests(doc) {
+ await testClickInTooltipContent(doc);
+ await testClickInTooltipIcon(doc);
+ await testConsumeOutsideClicksFalse(doc);
+ await testConsumeOutsideClicksTrue(doc);
+ await testConsumeWithRightClick(doc);
+ await testClickInOuterIframe(doc);
+ await testClickInInnerIframe(doc);
+}
+
+async function testClickInTooltipContent(doc) {
+ info("Test a tooltip is not closed when clicking inside itself");
+
+ const tooltip = new HTMLTooltip(doc, { useXulWrapper });
+ tooltip.panel.appendChild(getTooltipContent(doc));
+ tooltip.setContentSize({ width: 100, height: 50 });
+ await showTooltip(tooltip, doc.getElementById("box1"));
+
+ const onTooltipContainerClick = once(tooltip.container, "click");
+ EventUtils.synthesizeMouseAtCenter(tooltip.container, {}, doc.defaultView);
+ await onTooltipContainerClick;
+ is(tooltip.isVisible(), true, "Tooltip is still visible");
+
+ tooltip.destroy();
+}
+
+async function testClickInTooltipIcon(doc) {
+ info("Test a tooltip is not closed when clicking it's icon");
+
+ const tooltip = new HTMLTooltip(doc, { useXulWrapper, noAutoHide: true });
+ tooltip.panel.appendChild(getTooltipContent(doc));
+ tooltip.setContentSize({ width: 100, height: 50 });
+
+ const box1 = doc.getElementById("box1");
+ await showTooltip(tooltip, box1);
+
+ const onHidden = once(tooltip, "hidden");
+ box1.click();
+
+ // Hiding the tooltip is async so we need to wait for "hidden" to be emitted
+ // timing out after 3 seconds. If hidden is emitted we need to fail,
+ // otherwise the test passes.
+ const shown = await Promise.race([
+ onHidden,
+ wait(PROMISE_TIMEOUT).then(() => true),
+ ]);
+
+ ok(shown, "Tooltip is still visible");
+
+ tooltip.destroy();
+}
+
+async function testConsumeOutsideClicksFalse(doc) {
+ info("Test closing a tooltip via click with consumeOutsideClicks: false");
+ const box4 = doc.getElementById("box4");
+
+ const tooltip = new HTMLTooltip(doc, {
+ consumeOutsideClicks: false,
+ useXulWrapper,
+ });
+ tooltip.panel.appendChild(getTooltipContent(doc));
+ tooltip.setContentSize({ width: 100, height: 50 });
+ await showTooltip(tooltip, doc.getElementById("box1"));
+
+ const onBox4Clicked = once(box4, "click");
+ const onHidden = once(tooltip, "hidden");
+ box4.click();
+ await onHidden;
+ await onBox4Clicked;
+
+ is(tooltip.isVisible(), false, "Tooltip is hidden");
+
+ tooltip.destroy();
+}
+
+async function testConsumeOutsideClicksTrue(doc) {
+ info("Test closing a tooltip via click with consumeOutsideClicks: true");
+ const box4 = doc.getElementById("box4");
+
+ // Count clicks on box4
+ let box4clicks = 0;
+ box4.addEventListener("click", () => box4clicks++);
+
+ const tooltip = new HTMLTooltip(doc, {
+ consumeOutsideClicks: true,
+ useXulWrapper,
+ });
+ tooltip.panel.appendChild(getTooltipContent(doc));
+ tooltip.setContentSize({ width: 100, height: 50 });
+ await showTooltip(tooltip, doc.getElementById("box1"));
+
+ const onHidden = once(tooltip, "hidden");
+ box4.click();
+ await onHidden;
+
+ is(box4clicks, 0, "box4 catched no click event");
+ is(tooltip.isVisible(), false, "Tooltip is hidden");
+
+ tooltip.destroy();
+}
+
+async function testConsumeWithRightClick(doc) {
+ info(
+ "Test closing a tooltip with a right-click, with consumeOutsideClicks: true"
+ );
+ const box4 = doc.getElementById("box4");
+
+ const tooltip = new HTMLTooltip(doc, {
+ consumeOutsideClicks: true,
+ useXulWrapper,
+ });
+ tooltip.panel.appendChild(getTooltipContent(doc));
+ tooltip.setContentSize({ width: 100, height: 50 });
+ await showTooltip(tooltip, doc.getElementById("box1"));
+
+ // Only left-click events should be consumed, so we expect to catch a click when using
+ // {button: 2}, which simulates a right-click.
+ info(
+ "Right click on box4, expect tooltip to be hidden, event should not be consumed"
+ );
+ const onBox4Clicked = once(box4, "click");
+ const onHidden = once(tooltip, "hidden");
+ EventUtils.synthesizeMouseAtCenter(box4, { button: 2 }, doc.defaultView);
+ await onHidden;
+ await onBox4Clicked;
+
+ is(tooltip.isVisible(), false, "Tooltip is hidden");
+
+ tooltip.destroy();
+}
+
+async function testClickInOuterIframe(doc) {
+ info("Test clicking an iframe outside of the tooltip closes the tooltip");
+ const frame = doc.getElementById("frame");
+
+ const tooltip = new HTMLTooltip(doc, { useXulWrapper });
+ tooltip.panel.appendChild(getTooltipContent(doc));
+ tooltip.setContentSize({ width: 100, height: 50 });
+ await showTooltip(tooltip, doc.getElementById("box1"));
+
+ const onHidden = once(tooltip, "hidden");
+ frame.click();
+ await onHidden;
+
+ is(tooltip.isVisible(), false, "Tooltip is hidden");
+ tooltip.destroy();
+}
+
+async function testClickInInnerIframe(doc) {
+ info(
+ "Test clicking an iframe inside the tooltip content does not close the tooltip"
+ );
+
+ const tooltip = new HTMLTooltip(doc, {
+ consumeOutsideClicks: false,
+ useXulWrapper,
+ });
+
+ const iframe = doc.createElementNS(HTML_NS, "iframe");
+ iframe.style.width = "100px";
+ iframe.style.height = "50px";
+
+ tooltip.panel.appendChild(iframe);
+
+ tooltip.setContentSize({ width: 100, height: 50 });
+ await showTooltip(tooltip, doc.getElementById("box1"));
+
+ iframe.srcdoc = "";
+ await new Promise(r => {
+ const frameLoad = () => {
+ r();
+ };
+ DOMHelpers.onceDOMReady(iframe.contentWindow, frameLoad);
+ });
+
+ await waitUntil(() => iframe.contentWindow.document.getElementById("test"));
+
+ const target = iframe.contentWindow.document.getElementById("test");
+ const onTooltipClick = once(target, "click");
+ target.click();
+ await onTooltipClick;
+
+ is(tooltip.isVisible(), true, "Tooltip is still visible");
+
+ tooltip.destroy();
+}
+
+function getTooltipContent(doc) {
+ const div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "50px";
+ div.style.boxSizing = "border-box";
+ div.textContent = "tooltip";
+ return div;
+}
diff --git a/devtools/client/shared/test/browser_html_tooltip-03.js b/devtools/client/shared/test/browser_html_tooltip-03.js
new file mode 100644
index 0000000000..15be3e3382
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip-03.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * This is the sanity test for the HTMLTooltip focus
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = CHROME_URL_ROOT + "doc_html_tooltip-03.xhtml";
+
+const {
+ HTMLTooltip,
+} = require("resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js");
+loadHelperScript("helper_html_tooltip.js");
+
+let useXulWrapper;
+
+add_task(async function () {
+ await addTab("about:blank");
+ const { doc } = await createHost("bottom", TEST_URI);
+
+ info("Run tests for a Tooltip without using a XUL panel");
+ useXulWrapper = false;
+ await runTests(doc);
+
+ info("Run tests for a Tooltip with a XUL panel");
+ useXulWrapper = true;
+ await runTests(doc);
+});
+
+async function runTests(doc) {
+ await focusNode(doc, "#box4-input");
+ ok(doc.activeElement.closest("#box4-input"), "Focus is in the #box4-input");
+
+ info("Test a tooltip will not take focus");
+ const tooltip = await createTooltip(doc);
+
+ await showTooltip(tooltip, doc.getElementById("box1"));
+ ok(
+ doc.activeElement.closest("#box4-input"),
+ "Focus is still in the #box4-input"
+ );
+
+ await hideTooltip(tooltip);
+ await blurNode(doc, "#box4-input");
+
+ tooltip.destroy();
+}
+
+/**
+ * Fpcus the node corresponding to the provided selector in the provided document. Returns
+ * a promise that will resolve when receiving the focus event on the node.
+ */
+function focusNode(doc, selector) {
+ const node = doc.querySelector(selector);
+ const onFocus = once(node, "focus");
+ node.focus();
+ return onFocus;
+}
+
+/**
+ * Blur the node corresponding to the provided selector in the provided document. Returns
+ * a promise that will resolve when receiving the blur event on the node.
+ */
+function blurNode(doc, selector) {
+ const node = doc.querySelector(selector);
+ const onBlur = once(node, "blur");
+ node.blur();
+ return onBlur;
+}
+
+/**
+ * Create an HTMLTooltip instance.
+ *
+ * @param {Document} doc
+ * Document in which the tooltip should be created
+ * @return {Promise} promise that will resolve the HTMLTooltip instance created when the
+ * tooltip content will be ready.
+ */
+function createTooltip(doc) {
+ const tooltip = new HTMLTooltip(doc, { useXulWrapper });
+ const div = doc.createElementNS(HTML_NS, "div");
+ div.classList.add("tooltip-content");
+ div.style.height = "50px";
+
+ const input = doc.createElementNS(HTML_NS, "input");
+ input.setAttribute("type", "text");
+ div.appendChild(input);
+
+ tooltip.panel.appendChild(div);
+ tooltip.setContentSize({ width: 150, height: 50 });
+ return tooltip;
+}
diff --git a/devtools/client/shared/test/browser_html_tooltip-04.js b/devtools/client/shared/test/browser_html_tooltip-04.js
new file mode 100644
index 0000000000..9fe854bee4
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip-04.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip positioning for a small tooltip element (should aways
+ * find a way to fit).
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = CHROME_URL_ROOT + "doc_html_tooltip-04.xhtml";
+
+const {
+ HTMLTooltip,
+} = require("resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js");
+loadHelperScript("helper_html_tooltip.js");
+
+const TOOLTIP_HEIGHT = 30;
+const TOOLTIP_WIDTH = 100;
+
+add_task(async function () {
+ // Force the toolbox to be 400px high;
+ await pushPref("devtools.toolbox.footer.height", 400);
+
+ await addTab("about:blank");
+ const { doc } = await createHost("bottom", TEST_URI);
+
+ info("Create HTML tooltip");
+ const tooltip = new HTMLTooltip(doc, { useXulWrapper: false });
+ const div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "100%";
+ tooltip.panel.appendChild(div);
+ tooltip.setContentSize({ width: TOOLTIP_WIDTH, height: TOOLTIP_HEIGHT });
+
+ const box1 = doc.getElementById("box1");
+ const box2 = doc.getElementById("box2");
+ const box3 = doc.getElementById("box3");
+ const box4 = doc.getElementById("box4");
+ const height = TOOLTIP_HEIGHT,
+ width = TOOLTIP_WIDTH;
+
+ // box1: Can only fit below box1
+ info("Display the tooltip on box1.");
+ await showTooltip(tooltip, box1);
+ let expectedTooltipGeometry = { position: "bottom", height, width };
+ checkTooltipGeometry(tooltip, box1, expectedTooltipGeometry);
+ await hideTooltip(tooltip);
+
+ info("Try to display the tooltip on top of box1.");
+ await showTooltip(tooltip, box1, { position: "top" });
+ expectedTooltipGeometry = { position: "bottom", height, width };
+ checkTooltipGeometry(tooltip, box1, expectedTooltipGeometry);
+ await hideTooltip(tooltip);
+
+ // box2: Can fit above or below, will default to bottom, more height
+ // available.
+ info("Try to display the tooltip on box2.");
+ await showTooltip(tooltip, box2);
+ expectedTooltipGeometry = { position: "bottom", height, width };
+ checkTooltipGeometry(tooltip, box2, expectedTooltipGeometry);
+ await hideTooltip(tooltip);
+
+ info("Try to display the tooltip on top of box2.");
+ await showTooltip(tooltip, box2, { position: "top" });
+ expectedTooltipGeometry = { position: "top", height, width };
+ checkTooltipGeometry(tooltip, box2, expectedTooltipGeometry);
+ await hideTooltip(tooltip);
+
+ // box3: Can fit above or below, will default to top, more height available.
+ info("Try to display the tooltip on box3.");
+ await showTooltip(tooltip, box3);
+ expectedTooltipGeometry = { position: "top", height, width };
+ checkTooltipGeometry(tooltip, box3, expectedTooltipGeometry);
+ await hideTooltip(tooltip);
+
+ info("Try to display the tooltip on bottom of box3.");
+ await showTooltip(tooltip, box3, { position: "bottom" });
+ expectedTooltipGeometry = { position: "bottom", height, width };
+ checkTooltipGeometry(tooltip, box3, expectedTooltipGeometry);
+ await hideTooltip(tooltip);
+
+ // box4: Can only fit above box4
+ info("Display the tooltip on box4.");
+ await showTooltip(tooltip, box4);
+ expectedTooltipGeometry = { position: "top", height, width };
+ checkTooltipGeometry(tooltip, box4, expectedTooltipGeometry);
+ await hideTooltip(tooltip);
+
+ info("Try to display the tooltip on bottom of box4.");
+ await showTooltip(tooltip, box4, { position: "bottom" });
+ expectedTooltipGeometry = { position: "top", height, width };
+ checkTooltipGeometry(tooltip, box4, expectedTooltipGeometry);
+ await hideTooltip(tooltip);
+
+ is(tooltip.isVisible(), false, "Tooltip is not visible");
+
+ tooltip.destroy();
+});
diff --git a/devtools/client/shared/test/browser_html_tooltip-05.js b/devtools/client/shared/test/browser_html_tooltip-05.js
new file mode 100644
index 0000000000..7217039cb4
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip-05.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip positioning for a huge tooltip element (can not fit in
+ * the viewport).
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = CHROME_URL_ROOT + "doc_html_tooltip-05.xhtml";
+
+const {
+ HTMLTooltip,
+} = require("resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js");
+loadHelperScript("helper_html_tooltip.js");
+
+const TOOLTIP_HEIGHT = 200;
+const TOOLTIP_WIDTH = 200;
+
+add_task(async function () {
+ // Force the toolbox to be 200px high;
+ await pushPref("devtools.toolbox.footer.height", 200);
+ await addTab("about:blank");
+ const { doc } = await createHost("bottom", TEST_URI);
+
+ info("Create HTML tooltip");
+ const tooltip = new HTMLTooltip(doc, { useXulWrapper: false });
+ const div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "100%";
+ tooltip.panel.appendChild(div);
+ tooltip.setContentSize({ width: TOOLTIP_WIDTH, height: TOOLTIP_HEIGHT });
+
+ const box1 = doc.getElementById("box1");
+ const box2 = doc.getElementById("box2");
+ const box3 = doc.getElementById("box3");
+ const box4 = doc.getElementById("box4");
+ const width = TOOLTIP_WIDTH;
+
+ // box1: Can not fit above or below box1, default to bottom with a reduced
+ // height of 150px.
+ info("Display the tooltip on box1.");
+ await showTooltip(tooltip, box1);
+ let expectedTooltipGeometry = { position: "bottom", height: 150, width };
+ checkTooltipGeometry(tooltip, box1, expectedTooltipGeometry);
+ await hideTooltip(tooltip);
+
+ info("Try to display the tooltip on top of box1.");
+ await showTooltip(tooltip, box1, { position: "top" });
+ expectedTooltipGeometry = { position: "bottom", height: 150, width };
+ checkTooltipGeometry(tooltip, box1, expectedTooltipGeometry);
+ await hideTooltip(tooltip);
+
+ // box2: Can not fit above or below box2, default to bottom with a reduced
+ // height of 100px.
+ info("Try to display the tooltip on box2.");
+ await showTooltip(tooltip, box2);
+ expectedTooltipGeometry = { position: "bottom", height: 100, width };
+ checkTooltipGeometry(tooltip, box2, expectedTooltipGeometry);
+ await hideTooltip(tooltip);
+
+ info("Try to display the tooltip on top of box2.");
+ await showTooltip(tooltip, box2, { position: "top" });
+ expectedTooltipGeometry = { position: "bottom", height: 100, width };
+ checkTooltipGeometry(tooltip, box2, expectedTooltipGeometry);
+ await hideTooltip(tooltip);
+
+ // box3: Can not fit above or below box3, default to top with a reduced height
+ // of 100px.
+ info("Try to display the tooltip on box3.");
+ await showTooltip(tooltip, box3);
+ expectedTooltipGeometry = { position: "top", height: 100, width };
+ checkTooltipGeometry(tooltip, box3, expectedTooltipGeometry);
+ await hideTooltip(tooltip);
+
+ info("Try to display the tooltip on bottom of box3.");
+ await showTooltip(tooltip, box3, { position: "bottom" });
+ expectedTooltipGeometry = { position: "top", height: 100, width };
+ checkTooltipGeometry(tooltip, box3, expectedTooltipGeometry);
+ await hideTooltip(tooltip);
+
+ // box4: Can not fit above or below box4, default to top with a reduced height
+ // of 150px.
+ info("Display the tooltip on box4.");
+ await showTooltip(tooltip, box4);
+ expectedTooltipGeometry = { position: "top", height: 150, width };
+ checkTooltipGeometry(tooltip, box4, expectedTooltipGeometry);
+ await hideTooltip(tooltip);
+
+ info("Try to display the tooltip on bottom of box4.");
+ await showTooltip(tooltip, box4, { position: "bottom" });
+ expectedTooltipGeometry = { position: "top", height: 150, width };
+ checkTooltipGeometry(tooltip, box4, expectedTooltipGeometry);
+ await hideTooltip(tooltip);
+
+ is(tooltip.isVisible(), false, "Tooltip is not visible");
+
+ tooltip.destroy();
+});
diff --git a/devtools/client/shared/test/browser_html_tooltip_arrow-01.js b/devtools/client/shared/test/browser_html_tooltip_arrow-01.js
new file mode 100644
index 0000000000..222d43b696
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_arrow-01.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip "arrow" type on small anchors. The arrow should remain
+ * aligned with the anchors as much as possible
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = CHROME_URL_ROOT + "doc_html_tooltip_arrow-01.xhtml";
+
+const {
+ HTMLTooltip,
+} = require("resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js");
+loadHelperScript("helper_html_tooltip.js");
+
+let useXulWrapper;
+
+add_task(async function () {
+ // Force the toolbox to be 200px high;
+ await pushPref("devtools.toolbox.footer.height", 200);
+
+ await addTab("about:blank");
+ const { doc } = await createHost("bottom", TEST_URI);
+
+ info("Run tests for a Tooltip without using a XUL panel");
+ useXulWrapper = false;
+ await runTests(doc);
+
+ info("Run tests for a Tooltip with a XUL panel");
+ useXulWrapper = true;
+ await runTests(doc);
+});
+
+async function runTests(doc) {
+ info("Create HTML tooltip");
+ const tooltip = new HTMLTooltip(doc, { type: "arrow", useXulWrapper });
+ const div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "35px";
+ tooltip.panel.appendChild(div);
+ tooltip.setContentSize({ width: 200, height: 35 });
+
+ const { right: docRight } = doc.documentElement.getBoundingClientRect();
+
+ const elements = [...doc.querySelectorAll(".anchor")];
+ for (const el of elements) {
+ info("Display the tooltip on an anchor.");
+ await showTooltip(tooltip, el);
+
+ const arrow = tooltip.arrow;
+ ok(arrow, "Tooltip has an arrow");
+
+ // Get the geometry of the anchor, the tooltip panel & arrow.
+ const arrowBounds = arrow.getBoxQuads({ relativeTo: doc })[0].getBounds();
+ const panelBounds = tooltip.panel
+ .getBoxQuads({ relativeTo: doc })[0]
+ .getBounds();
+ const anchorBounds = el.getBoxQuads({ relativeTo: doc })[0].getBounds();
+
+ const intersects =
+ arrowBounds.left <= anchorBounds.right &&
+ arrowBounds.right >= anchorBounds.left;
+ const isBlockedByViewport =
+ arrowBounds.left == 0 || arrowBounds.right == docRight;
+ ok(
+ intersects || isBlockedByViewport,
+ "Tooltip arrow is aligned with the anchor, or stuck on viewport's edge."
+ );
+
+ const isInPanel =
+ arrowBounds.left >= panelBounds.left &&
+ arrowBounds.right <= panelBounds.right;
+
+ ok(
+ isInPanel,
+ "The tooltip arrow remains inside the tooltip panel horizontally"
+ );
+
+ await hideTooltip(tooltip);
+ }
+
+ tooltip.destroy();
+}
diff --git a/devtools/client/shared/test/browser_html_tooltip_arrow-02.js b/devtools/client/shared/test/browser_html_tooltip_arrow-02.js
new file mode 100644
index 0000000000..07222a6a49
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_arrow-02.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip "arrow" type on wide anchors. The arrow should remain
+ * aligned with the anchors as much as possible
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = CHROME_URL_ROOT + "doc_html_tooltip_arrow-02.xhtml";
+
+const {
+ HTMLTooltip,
+} = require("resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js");
+loadHelperScript("helper_html_tooltip.js");
+
+let useXulWrapper;
+
+add_task(async function () {
+ // Force the toolbox to be 200px high;
+ await pushPref("devtools.toolbox.footer.height", 200);
+
+ const { doc } = await createHost("bottom", TEST_URI);
+
+ info("Run tests for a Tooltip without using a XUL panel");
+ useXulWrapper = false;
+ await runTests(doc);
+
+ info("Run tests for a Tooltip with a XUL panel");
+ useXulWrapper = true;
+ await runTests(doc);
+});
+
+async function runTests(doc) {
+ info("Create HTML tooltip");
+ const tooltip = new HTMLTooltip(doc, { type: "arrow", useXulWrapper });
+ const div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "35px";
+ tooltip.panel.appendChild(div);
+ tooltip.setContentSize({ width: 200, height: 35 });
+
+ const { right: docRight } = doc.documentElement.getBoundingClientRect();
+
+ const elements = [...doc.querySelectorAll(".anchor")];
+ for (const el of elements) {
+ info("Display the tooltip on an anchor.");
+ await showTooltip(tooltip, el);
+
+ const arrow = tooltip.arrow;
+ ok(arrow, "Tooltip has an arrow");
+
+ // Get the geometry of the anchor, the tooltip panel & arrow.
+ const arrowBounds = arrow.getBoxQuads({ relativeTo: doc })[0].getBounds();
+ const panelBounds = tooltip.panel
+ .getBoxQuads({ relativeTo: doc })[0]
+ .getBounds();
+ const anchorBounds = el.getBoxQuads({ relativeTo: doc })[0].getBounds();
+
+ const intersects =
+ arrowBounds.left <= anchorBounds.right &&
+ arrowBounds.right >= anchorBounds.left;
+ const isBlockedByViewport =
+ arrowBounds.left == 0 || arrowBounds.right == docRight;
+ ok(
+ intersects || isBlockedByViewport,
+ "Tooltip arrow is aligned with the anchor, or stuck on viewport's edge."
+ );
+
+ const isInPanel =
+ arrowBounds.left >= panelBounds.left &&
+ arrowBounds.right <= panelBounds.right;
+ ok(
+ isInPanel,
+ "The tooltip arrow remains inside the tooltip panel horizontally"
+ );
+ await hideTooltip(tooltip);
+ }
+
+ tooltip.destroy();
+}
diff --git a/devtools/client/shared/test/browser_html_tooltip_consecutive-show.js b/devtools/client/shared/test/browser_html_tooltip_consecutive-show.js
new file mode 100644
index 0000000000..d61d57caff
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_consecutive-show.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip show can be called several times. It should move according to the
+ * new anchor/options and should not leak event listeners.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = CHROME_URL_ROOT + "doc_html_tooltip.xhtml";
+
+const {
+ HTMLTooltip,
+} = require("resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js");
+loadHelperScript("helper_html_tooltip.js");
+
+function getTooltipContent(doc) {
+ const div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "50px";
+ div.textContent = "tooltip";
+ return div;
+}
+
+add_task(async function () {
+ const { doc } = await createHost("bottom", TEST_URI);
+
+ // Creating a host is not correctly waiting when DevTools run in content frame
+ // See Bug 1571421.
+ await wait(1000);
+
+ const box1 = doc.getElementById("box1");
+ const box2 = doc.getElementById("box2");
+ const box3 = doc.getElementById("box3");
+ const box4 = doc.getElementById("box4");
+
+ const width = 100,
+ height = 50;
+
+ const tooltip = new HTMLTooltip(doc, { useXulWrapper: false });
+ tooltip.panel.appendChild(getTooltipContent(doc));
+ tooltip.setContentSize({ width, height });
+
+ info(
+ "Show the tooltip on each of the 4 hbox, without calling hide in between"
+ );
+
+ info("Show tooltip on box1");
+ tooltip.show(box1);
+ checkTooltipGeometry(tooltip, box1, { position: "bottom", width, height });
+
+ info("Show tooltip on box2");
+ tooltip.show(box2);
+ checkTooltipGeometry(tooltip, box2, { position: "bottom", width, height });
+
+ info("Show tooltip on box3");
+ tooltip.show(box3);
+ checkTooltipGeometry(tooltip, box3, { position: "top", width, height });
+
+ info("Show tooltip on box4");
+ tooltip.show(box4);
+ checkTooltipGeometry(tooltip, box4, { position: "top", width, height });
+
+ info("Hide tooltip before leaving test");
+ await hideTooltip(tooltip);
+
+ tooltip.destroy();
+});
diff --git a/devtools/client/shared/test/browser_html_tooltip_doorhanger-01.js b/devtools/client/shared/test/browser_html_tooltip_doorhanger-01.js
new file mode 100644
index 0000000000..7e2d9e5657
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_doorhanger-01.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip "doorhanger" type's hang direction. It should hang
+ * along the flow of text e.g. in RTL mode it should hang left and in LTR mode
+ * it should hang right.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = CHROME_URL_ROOT + "doc_html_tooltip_doorhanger-01.xhtml";
+
+const {
+ HTMLTooltip,
+} = require("resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js");
+loadHelperScript("helper_html_tooltip.js");
+
+let useXulWrapper;
+
+add_task(async function () {
+ // Force the toolbox to be 200px high;
+ await pushPref("devtools.toolbox.footer.height", 200);
+
+ await addTab("about:blank");
+ const { doc } = await createHost("bottom", TEST_URI);
+
+ info("Run tests for a Tooltip without using a XUL panel");
+ useXulWrapper = false;
+ await runTests(doc);
+});
+
+async function runTests(doc) {
+ info("Create HTML tooltip");
+ const tooltip = new HTMLTooltip(doc, { type: "doorhanger", useXulWrapper });
+ const div = doc.createElementNS(HTML_NS, "div");
+ div.style.width = "200px";
+ div.style.height = "35px";
+ tooltip.panel.appendChild(div);
+
+ const anchors = [...doc.querySelectorAll(".anchor")];
+ for (const anchor of anchors) {
+ const hangDirection = anchor.getAttribute("data-hang");
+
+ info("Display the tooltip on an anchor.");
+ await showTooltip(tooltip, anchor);
+
+ const arrow = tooltip.arrow;
+ ok(arrow, "Tooltip has an arrow");
+
+ // Get the geometry of the the tooltip panel & arrow.
+ const panelBounds = tooltip.panel
+ .getBoxQuads({ relativeTo: doc })[0]
+ .getBounds();
+ const arrowBounds = arrow.getBoxQuads({ relativeTo: doc })[0].getBounds();
+ const panelBoundsCentre = (panelBounds.left + panelBounds.right) / 2;
+ const arrowCentre = (arrowBounds.left + arrowBounds.right) / 2;
+
+ if (hangDirection === "left") {
+ Assert.greater(
+ arrowCentre,
+ panelBoundsCentre,
+ `tooltip hangs to the left for ${anchor.id}`
+ );
+ } else {
+ Assert.less(
+ arrowCentre,
+ panelBoundsCentre,
+ `tooltip hangs to the right for ${anchor.id}`
+ );
+ }
+
+ await hideTooltip(tooltip);
+ }
+
+ tooltip.destroy();
+}
diff --git a/devtools/client/shared/test/browser_html_tooltip_doorhanger-02.js b/devtools/client/shared/test/browser_html_tooltip_doorhanger-02.js
new file mode 100644
index 0000000000..b1564f698c
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_doorhanger-02.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip "doorhanger" type's arrow tip is precisely centered on
+ * the anchor when the anchor is small.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = CHROME_URL_ROOT + "doc_html_tooltip_doorhanger-02.xhtml";
+
+const {
+ HTMLTooltip,
+} = require("resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js");
+loadHelperScript("helper_html_tooltip.js");
+
+let useXulWrapper;
+
+add_task(async function () {
+ // Force the toolbox to be 200px high;
+ await pushPref("devtools.toolbox.footer.height", 200);
+
+ await addTab("about:blank");
+ const { doc } = await createHost("bottom", TEST_URI);
+
+ info("Run tests for a Tooltip without using a XUL panel");
+ useXulWrapper = false;
+ await runTests(doc);
+
+ info("Run tests for a Tooltip with a XUL panel");
+ useXulWrapper = true;
+ await runTests(doc);
+});
+
+async function runTests(doc) {
+ info("Create HTML tooltip");
+ const tooltip = new HTMLTooltip(doc, { type: "doorhanger", useXulWrapper });
+ const div = doc.createElementNS(HTML_NS, "div");
+ div.style.width = "200px";
+ div.style.height = "35px";
+ tooltip.panel.appendChild(div);
+
+ const elements = [...doc.querySelectorAll(".anchor")];
+ for (const el of elements) {
+ info("Display the tooltip on an anchor.");
+ await showTooltip(tooltip, el);
+
+ const arrow = tooltip.arrow;
+ ok(arrow, "Tooltip has an arrow");
+
+ // Get the geometry of the anchor and arrow.
+ const anchorBounds = el.getBoxQuads({ relativeTo: doc })[0].getBounds();
+ const arrowBounds = arrow.getBoxQuads({ relativeTo: doc })[0].getBounds();
+
+ // Compare the centers
+ const center = bounds => bounds.left + bounds.width / 2;
+ const delta = Math.abs(center(anchorBounds) - center(arrowBounds));
+ const describeBounds = bounds =>
+ `${bounds.left}<--[${center(bounds)}]-->${bounds.right}`;
+ const params =
+ `anchor: ${describeBounds(anchorBounds)}, ` +
+ `arrow: ${describeBounds(arrowBounds)}`;
+ Assert.lessOrEqual(
+ delta,
+ 1,
+ `Arrow center is roughly aligned with anchor center (${params})`
+ );
+
+ await hideTooltip(tooltip);
+ }
+
+ tooltip.destroy();
+}
diff --git a/devtools/client/shared/test/browser_html_tooltip_height-auto.js b/devtools/client/shared/test/browser_html_tooltip_height-auto.js
new file mode 100644
index 0000000000..7a42183f80
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_height-auto.js
@@ -0,0 +1,108 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip content can automatically calculate its height based on
+ * content.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = CHROME_URL_ROOT + "doc_html_tooltip.xhtml";
+
+const {
+ HTMLTooltip,
+} = require("resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js");
+loadHelperScript("helper_html_tooltip.js");
+
+let useXulWrapper;
+
+add_task(async function () {
+ await addTab("about:blank");
+ const { doc } = await createHost("bottom", TEST_URI);
+
+ info("Run tests for a Tooltip without using a XUL panel");
+ useXulWrapper = false;
+ await runTests(doc);
+
+ info("Run tests for a Tooltip with a XUL panel");
+ useXulWrapper = true;
+ await runTests(doc);
+});
+
+async function runTests(doc) {
+ const tooltip = new HTMLTooltip(doc, { useXulWrapper });
+ info("Create tooltip content height to 150px");
+ const tooltipContent = doc.createElementNS(HTML_NS, "div");
+ tooltipContent.style.cssText =
+ "width: 300px; height: 150px; background: red;";
+
+ info("Set tooltip content using width:auto and height:auto");
+ tooltip.panel.appendChild(tooltipContent);
+
+ info("Show the tooltip and check the tooltip container dimensions.");
+ await showTooltip(tooltip, doc.getElementById("box1"));
+
+ let panelRect = tooltip.container.getBoundingClientRect();
+ is(panelRect.width, 300, "Tooltip container has the expected width.");
+ is(panelRect.height, 150, "Tooltip container has the expected height.");
+
+ await hideTooltip(tooltip);
+
+ info("Set tooltip content using fixed width and height:auto");
+ tooltipContent.style.cssText = "width: auto; height: 160px; background: red;";
+ tooltip.setContentSize({ width: 400 });
+
+ info("Show the tooltip and check the tooltip container height.");
+ await showTooltip(tooltip, doc.getElementById("box1"));
+
+ panelRect = tooltip.container.getBoundingClientRect();
+ is(panelRect.height, 160, "Tooltip container has the expected height.");
+
+ await hideTooltip(tooltip);
+
+ info("Update the height and show the tooltip again");
+ tooltipContent.style.cssText = "width: auto; height: 165px; background: red;";
+
+ await showTooltip(tooltip, doc.getElementById("box1"));
+
+ panelRect = tooltip.container.getBoundingClientRect();
+ is(
+ panelRect.height,
+ 165,
+ "Tooltip container has the expected updated height."
+ );
+
+ await hideTooltip(tooltip);
+
+ info(
+ "Check that refreshing the tooltip when it overflows does keep scroll position"
+ );
+ // Set the tooltip panel to overflow. Some consumers of the HTMLTooltip are doing that
+ // via CSS (e.g. the iframe dropdown, the context selector, …).
+ tooltip.panel.style.overflowY = "auto";
+ tooltipContent.style.cssText =
+ "width: auto; height: 3000px; background: tomato;";
+ await showTooltip(tooltip, doc.getElementById("box1"));
+
+ Assert.greater(
+ tooltip.panel.scrollHeight,
+ tooltip.panel.clientHeight,
+ "Tooltip overflows"
+ );
+
+ const scrollPosition = 500;
+ tooltip.panel.scrollTop = scrollPosition;
+
+ await showTooltip(tooltip, doc.getElementById("box1"));
+ is(
+ tooltip.panel.scrollTop,
+ scrollPosition,
+ "scroll position was kept during the update"
+ );
+ await hideTooltip(tooltip);
+
+ tooltip.destroy();
+}
diff --git a/devtools/client/shared/test/browser_html_tooltip_hover.js b/devtools/client/shared/test/browser_html_tooltip_hover.js
new file mode 100644
index 0000000000..c7d055ba35
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_hover.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the TooltipToggle helper class for HTMLTooltip
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = CHROME_URL_ROOT + "doc_html_tooltip_hover.xhtml";
+
+const {
+ HTMLTooltip,
+} = require("resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js");
+loadHelperScript("helper_html_tooltip.js");
+
+add_task(async function () {
+ const { doc } = await createHost("bottom", TEST_URI);
+ // Wait for full page load before synthesizing events on the page.
+ await waitUntil(() => doc.readyState === "complete");
+
+ const width = 100,
+ height = 50;
+ const tooltipContent = doc.createElementNS(HTML_NS, "div");
+ tooltipContent.textContent = "tooltip";
+ const tooltip = new HTMLTooltip(doc, { useXulWrapper: false });
+ tooltip.panel.appendChild(tooltipContent);
+ tooltip.setContentSize({ width, height });
+
+ const container = doc.getElementById("container");
+ tooltip.startTogglingOnHover(container, () => true);
+
+ info("Hover on each of the 4 boxes, expect the tooltip to appear");
+ async function showAndCheck(boxId, position) {
+ info(`Show tooltip on ${boxId}`);
+ const box = doc.getElementById(boxId);
+ const shown = tooltip.once("shown");
+ EventUtils.synthesizeMouseAtCenter(
+ box,
+ { type: "mousemove" },
+ doc.defaultView
+ );
+ await shown;
+ checkTooltipGeometry(tooltip, box, { position, width, height });
+ }
+
+ await showAndCheck("box1", "bottom");
+ await showAndCheck("box2", "bottom");
+ await showAndCheck("box3", "top");
+ await showAndCheck("box4", "top");
+
+ info("Move out of the container");
+ const hidden = tooltip.once("hidden");
+ EventUtils.synthesizeMouseAtCenter(
+ container,
+ { type: "mousemove" },
+ doc.defaultView
+ );
+ await hidden;
+
+ info("Destroy the tooltip and finish");
+ tooltip.destroy();
+});
diff --git a/devtools/client/shared/test/browser_html_tooltip_offset.js b/devtools/client/shared/test/browser_html_tooltip_offset.js
new file mode 100644
index 0000000000..d7d7e1200e
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_offset.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+"use strict";
+
+/**
+ * Test the HTMLTooltip can be displayed with vertical and horizontal offsets.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = CHROME_URL_ROOT + "doc_html_tooltip.xhtml";
+
+const {
+ HTMLTooltip,
+} = require("resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js");
+loadHelperScript("helper_html_tooltip.js");
+
+add_task(async function () {
+ // Force the toolbox to be 200px high;
+ await pushPref("devtools.toolbox.footer.height", 200);
+
+ const { doc } = await createHost("bottom", TEST_URI);
+
+ info("Test a tooltip is not closed when clicking inside itself");
+
+ const box1 = doc.getElementById("box1");
+ const box2 = doc.getElementById("box2");
+ const box3 = doc.getElementById("box3");
+ const box4 = doc.getElementById("box4");
+
+ const tooltip = new HTMLTooltip(doc, { useXulWrapper: false });
+
+ const div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "100px";
+ div.style.boxSizing = "border-box";
+ div.textContent = "tooltip";
+ tooltip.panel.appendChild(div);
+ tooltip.setContentSize({ width: 50, height: 100 });
+
+ info("Display the tooltip on box1.");
+ await showTooltip(tooltip, box1, { x: 5, y: 10 });
+
+ let panelRect = tooltip.container.getBoundingClientRect();
+ let anchorRect = box1.getBoundingClientRect();
+
+ // Tooltip will be displayed below box1
+ is(panelRect.top, anchorRect.bottom + 10, "Tooltip top has 10px offset");
+ is(panelRect.left, anchorRect.left + 5, "Tooltip left has 5px offset");
+ is(panelRect.height, 100, "Tooltip height is at 100px as expected");
+
+ info("Display the tooltip on box2.");
+ await showTooltip(tooltip, box2, { x: 5, y: 10 });
+
+ panelRect = tooltip.container.getBoundingClientRect();
+ anchorRect = box2.getBoundingClientRect();
+
+ // Tooltip will be displayed below box2, but can't be fully displayed because of the
+ // offset
+ is(panelRect.top, anchorRect.bottom + 10, "Tooltip top has 10px offset");
+ is(panelRect.left, anchorRect.left + 5, "Tooltip left has 5px offset");
+ is(panelRect.height, 90, "Tooltip height is only 90px");
+
+ info("Display the tooltip on box3.");
+ await showTooltip(tooltip, box3, { x: 5, y: 10 });
+
+ panelRect = tooltip.container.getBoundingClientRect();
+ anchorRect = box3.getBoundingClientRect();
+
+ // Tooltip will be displayed above box3, but can't be fully displayed because of the
+ // offset
+ is(
+ panelRect.bottom,
+ anchorRect.top - 10,
+ "Tooltip bottom is 10px above anchor"
+ );
+ is(panelRect.left, anchorRect.left + 5, "Tooltip left has 5px offset");
+ is(panelRect.height, 90, "Tooltip height is only 90px");
+
+ info("Display the tooltip on box4.");
+ await showTooltip(tooltip, box4, { x: 5, y: 10 });
+
+ panelRect = tooltip.container.getBoundingClientRect();
+ anchorRect = box4.getBoundingClientRect();
+
+ // Tooltip will be displayed above box4
+ is(
+ panelRect.bottom,
+ anchorRect.top - 10,
+ "Tooltip bottom is 10px above anchor"
+ );
+ is(panelRect.left, anchorRect.left + 5, "Tooltip left has 5px offset");
+ is(panelRect.height, 100, "Tooltip height is at 100px as expected");
+
+ await hideTooltip(tooltip);
+
+ tooltip.destroy();
+});
diff --git a/devtools/client/shared/test/browser_html_tooltip_resize.js b/devtools/client/shared/test/browser_html_tooltip_resize.js
new file mode 100644
index 0000000000..145d2582ca
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_resize.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+"use strict";
+
+/**
+ * Test the HTMLTooltip can be resized.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = CHROME_URL_ROOT + "doc_html_tooltip.xhtml";
+
+const {
+ HTMLTooltip,
+} = require("resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js");
+loadHelperScript("helper_html_tooltip.js");
+
+const TOOLBOX_WIDTH = 500;
+
+add_task(async function () {
+ await pushPref("devtools.toolbox.sidebar.width", TOOLBOX_WIDTH);
+
+ // Open the host on the right so that the doorhangers hang right.
+ const { doc } = await createHost("right", TEST_URI);
+
+ info("Test resizing of a tooltip");
+
+ const tooltip = new HTMLTooltip(doc, {
+ useXulWrapper: true,
+ type: "doorhanger",
+ });
+ const div = doc.createElementNS(HTML_NS, "div");
+ div.textContent = "tooltip";
+ div.style.cssText = "width: 100px; height: 40px";
+ tooltip.panel.appendChild(div);
+
+ const box1 = doc.getElementById("box1");
+
+ await showTooltip(tooltip, box1, { position: "top" });
+
+ // Get the original position of the panel and arrow.
+ const originalPanelBounds = tooltip.panel
+ .getBoxQuads({ relativeTo: doc })[0]
+ .getBounds();
+ const originalArrowBounds = tooltip.arrow
+ .getBoxQuads({ relativeTo: doc })[0]
+ .getBounds();
+
+ // Resize the content
+ div.style.cssText = "width: 200px; height: 30px";
+ tooltip.show(box1, { position: "top" });
+
+ // The panel should have moved 100px to the left and 10px down
+ const updatedPanelBounds = tooltip.panel
+ .getBoxQuads({ relativeTo: doc })[0]
+ .getBounds();
+
+ const panelXMovement =
+ `panel right: ${originalPanelBounds.right} -> ` + updatedPanelBounds.right;
+ Assert.strictEqual(
+ Math.round(updatedPanelBounds.right - originalPanelBounds.right),
+ 100,
+ `Panel should have moved 100px to the right (actual: ${panelXMovement})`
+ );
+
+ const panelYMovement =
+ `panel top: ${originalPanelBounds.top} -> ` + updatedPanelBounds.top;
+ Assert.strictEqual(
+ Math.round(updatedPanelBounds.top - originalPanelBounds.top),
+ 10,
+ `Panel should have moved 10px down (actual: ${panelYMovement})`
+ );
+
+ // The arrow should be in the same position
+ const updatedArrowBounds = tooltip.arrow
+ .getBoxQuads({ relativeTo: doc })[0]
+ .getBounds();
+
+ const arrowXMovement =
+ `arrow left: ${originalArrowBounds.left} -> ` + updatedArrowBounds.left;
+ Assert.strictEqual(
+ Math.round(updatedArrowBounds.left - originalArrowBounds.left),
+ 0,
+ `Arrow should not have moved (actual: ${arrowXMovement})`
+ );
+
+ const arrowYMovement =
+ `arrow top: ${originalArrowBounds.top} -> ` + updatedArrowBounds.top;
+ Assert.strictEqual(
+ Math.round(updatedArrowBounds.top - originalArrowBounds.top),
+ 0,
+ `Arrow should not have moved (actual: ${arrowYMovement})`
+ );
+
+ await hideTooltip(tooltip);
+ tooltip.destroy();
+});
diff --git a/devtools/client/shared/test/browser_html_tooltip_rtl.js b/devtools/client/shared/test/browser_html_tooltip_rtl.js
new file mode 100644
index 0000000000..11fe3932f3
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_rtl.js
@@ -0,0 +1,226 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+"use strict";
+
+/**
+ * Test the HTMLTooltip anchor alignment changes with the anchor direction.
+ * - should be aligned to the right of RTL anchors
+ * - should be aligned to the left of LTR anchors
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = CHROME_URL_ROOT + "doc_html_tooltip_rtl.xhtml";
+
+const {
+ HTMLTooltip,
+} = require("resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js");
+loadHelperScript("helper_html_tooltip.js");
+
+const TOOLBOX_WIDTH = 500;
+const TOOLTIP_WIDTH = 150;
+const TOOLTIP_HEIGHT = 30;
+
+add_task(async function () {
+ await pushPref("devtools.toolbox.sidebar.width", TOOLBOX_WIDTH);
+
+ const { doc } = await createHost("right", TEST_URI);
+
+ info("Test the positioning of tooltips in RTL and LTR directions");
+
+ const tooltip = new HTMLTooltip(doc, { useXulWrapper: false });
+ const div = doc.createElementNS(HTML_NS, "div");
+ div.textContent = "tooltip";
+ div.style.cssText = "box-sizing: border-box; border: 1px solid black";
+ tooltip.panel.appendChild(div);
+ tooltip.setContentSize({ width: TOOLTIP_WIDTH, height: TOOLTIP_HEIGHT });
+
+ await testRtlAnchors(doc, tooltip);
+ await testLtrAnchors(doc, tooltip);
+ await hideTooltip(tooltip);
+
+ tooltip.destroy();
+
+ await testRtlArrow(doc);
+});
+
+async function testRtlAnchors(doc, tooltip) {
+ /*
+ * The layout of the test page is as follows:
+ * _______________________________
+ * | toolbox |
+ * | _____ _____ _____ _____ |
+ * || | | | | | | ||
+ * || box1| | box2| | box3| | box4||
+ * ||_____| |_____| |_____| |_____||
+ * |_______________________________|
+ *
+ * - box1 is aligned with the left edge of the toolbox
+ * - box2 is displayed right after box1
+ * - total toolbox width is 500px so each box is 125px wide
+ */
+
+ const box1 = doc.getElementById("box1");
+ const box2 = doc.getElementById("box2");
+
+ info("Display the tooltip on box1.");
+ await showTooltip(tooltip, box1, { position: "bottom" });
+
+ let panelRect = tooltip.container.getBoundingClientRect();
+ let anchorRect = box1.getBoundingClientRect();
+
+ // box1 uses RTL direction, so the tooltip should be aligned with the right edge of the
+ // anchor, but it is shifted to the right to fit in the toolbox.
+ is(panelRect.left, 0, "Tooltip is aligned with left edge of the toolbox");
+ is(
+ panelRect.top,
+ anchorRect.bottom,
+ "Tooltip aligned with the anchor bottom edge"
+ );
+ is(
+ panelRect.height,
+ TOOLTIP_HEIGHT,
+ "Tooltip height is at 100px as expected"
+ );
+
+ info("Display the tooltip on box2.");
+ await showTooltip(tooltip, box2, { position: "bottom" });
+
+ panelRect = tooltip.container.getBoundingClientRect();
+ anchorRect = box2.getBoundingClientRect();
+
+ // box2 uses RTL direction, so the tooltip is aligned with the right edge of the anchor
+ is(
+ Math.round(panelRect.right),
+ Math.round(anchorRect.right),
+ "Tooltip is aligned with right edge of anchor"
+ );
+ is(
+ panelRect.top,
+ anchorRect.bottom,
+ "Tooltip aligned with the anchor bottom edge"
+ );
+ is(
+ panelRect.height,
+ TOOLTIP_HEIGHT,
+ "Tooltip height is at 100px as expected"
+ );
+}
+
+async function testLtrAnchors(doc, tooltip) {
+ /*
+ * The layout of the test page is as follows:
+ * _______________________________
+ * | toolbox |
+ * | _____ _____ _____ _____ |
+ * || | | | | | | ||
+ * || box1| | box2| | box3| | box4||
+ * ||_____| |_____| |_____| |_____||
+ * |_______________________________|
+ *
+ * - box3 is is displayed right after box2
+ * - box4 is aligned with the right edge of the toolbox
+ * - total toolbox width is 500px so each box is 125px wide
+ */
+
+ const box3 = doc.getElementById("box3");
+ const box4 = doc.getElementById("box4");
+
+ info("Display the tooltip on box3.");
+ await showTooltip(tooltip, box3, { position: "bottom" });
+
+ let panelRect = tooltip.container.getBoundingClientRect();
+ let anchorRect = box3.getBoundingClientRect();
+
+ // box3 uses LTR direction, so the tooltip is aligned with the left edge of the anchor.
+ is(
+ Math.round(panelRect.left),
+ Math.round(anchorRect.left),
+ "Tooltip is aligned with left edge of anchor"
+ );
+ is(
+ panelRect.top,
+ anchorRect.bottom,
+ "Tooltip aligned with the anchor bottom edge"
+ );
+ is(
+ panelRect.height,
+ TOOLTIP_HEIGHT,
+ "Tooltip height is at 100px as expected"
+ );
+
+ info("Display the tooltip on box4.");
+ await showTooltip(tooltip, box4, { position: "bottom" });
+
+ panelRect = tooltip.container.getBoundingClientRect();
+ anchorRect = box4.getBoundingClientRect();
+
+ // box4 uses LTR direction, so the tooltip should be aligned with the left edge of the
+ // anchor, but it is shifted to the left to fit in the toolbox.
+ is(
+ panelRect.right,
+ TOOLBOX_WIDTH,
+ "Tooltip is aligned with right edge of toolbox"
+ );
+ is(
+ panelRect.top,
+ anchorRect.bottom,
+ "Tooltip aligned with the anchor bottom edge"
+ );
+ is(
+ panelRect.height,
+ TOOLTIP_HEIGHT,
+ "Tooltip height is at 100px as expected"
+ );
+}
+
+async function testRtlArrow(doc) {
+ // Set up the arrow-style tooltip
+ const arrowTooltip = new HTMLTooltip(doc, {
+ type: "arrow",
+ useXulWrapper: false,
+ });
+ const div = doc.createElementNS(HTML_NS, "div");
+ div.textContent = "tooltip";
+ div.style.cssText = "box-sizing: border-box; border: 1px solid black";
+ arrowTooltip.panel.appendChild(div);
+ arrowTooltip.setContentSize({
+ width: TOOLTIP_WIDTH,
+ height: TOOLTIP_HEIGHT,
+ });
+
+ // box2 uses RTL direction and is far enough from the edge that the arrow
+ // should not be squashed in the wrong direction.
+ const box2 = doc.getElementById("box2");
+
+ info("Display the arrow tooltip on box2.");
+ await showTooltip(arrowTooltip, box2, { position: "top" });
+
+ const arrow = arrowTooltip.arrow;
+ ok(arrow, "Tooltip has an arrow");
+
+ const panelRect = arrowTooltip.container.getBoundingClientRect();
+ const arrowRect = arrow.getBoundingClientRect();
+
+ // The arrow should be offset from the right edge, but still closer to the
+ // right edge than the left edge.
+ Assert.less(
+ arrowRect.right,
+ panelRect.right,
+ "Right edge of the arrow " +
+ `(${arrowRect.right}) is less than the right edge of the panel ` +
+ `(${panelRect.right})`
+ );
+ const rightMargin = panelRect.right - arrowRect.right;
+ const leftMargin = arrowRect.left - panelRect.right;
+ Assert.greater(
+ rightMargin,
+ leftMargin,
+ "Arrow should be closer to the right side of " +
+ ` the panel (margin: ${rightMargin}) than the left side ` +
+ ` (margin: ${leftMargin})`
+ );
+
+ await hideTooltip(arrowTooltip);
+ arrowTooltip.destroy();
+}
diff --git a/devtools/client/shared/test/browser_html_tooltip_screen_edge.js b/devtools/client/shared/test/browser_html_tooltip_screen_edge.js
new file mode 100644
index 0000000000..e27c1e808b
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_screen_edge.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip "doorhanger" when the anchor is near the edge of the
+ * screen and uses a XUL wrapper. The XUL panel cannot be displayed off screen
+ * at all so this verifies that the calculated position of the tooltip always
+ * ensure that the whole tooltip is rendered on the screen
+ *
+ * See Bug 1590408
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = CHROME_URL_ROOT + "doc_html_tooltip_doorhanger-01.xhtml";
+
+const {
+ HTMLTooltip,
+} = require("resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js");
+loadHelperScript("helper_html_tooltip.js");
+
+add_task(async function () {
+ // Force the toolbox to be 200px high;
+ await pushPref("devtools.toolbox.footer.height", 200);
+
+ const { win, doc } = await createHost("bottom", TEST_URI);
+
+ const originalTop = win.screenTop;
+ const originalLeft = win.screenLeft;
+ const screenWidth = win.screen.width;
+ await moveWindowTo(win, screenWidth - win.outerWidth, originalTop);
+
+ registerCleanupFunction(async () => {
+ info(`Restore original window position. ${originalLeft}, ${originalTop}`);
+ await moveWindowTo(win, originalLeft, originalTop);
+ });
+
+ info("Create a doorhanger HTML tooltip with XULPanel");
+ const tooltip = new HTMLTooltip(doc, {
+ type: "doorhanger",
+ useXulWrapper: true,
+ });
+ const div = doc.createElementNS(HTML_NS, "div");
+ div.style.width = "200px";
+ div.style.height = "35px";
+ tooltip.panel.appendChild(div);
+
+ const anchor = doc.querySelector("#anchor5");
+
+ info("Display the tooltip on an anchor.");
+ await showTooltip(tooltip, anchor);
+
+ const arrow = tooltip.arrow;
+ ok(arrow, "Tooltip has an arrow");
+
+ const panelBounds = tooltip.panel
+ .getBoxQuads({ relativeTo: doc })[0]
+ .getBounds();
+
+ const anchorBounds = anchor.getBoxQuads({ relativeTo: doc })[0].getBounds();
+ ok(
+ anchorBounds.left < panelBounds.right &&
+ panelBounds.left < anchorBounds.right,
+ `The tooltip panel is over (ie intersects) the anchor horizontally: ` +
+ `${anchorBounds.left} < ${panelBounds.right} and ` +
+ `${panelBounds.left} < ${anchorBounds.right}`
+ );
+
+ await hideTooltip(tooltip);
+
+ tooltip.destroy();
+});
diff --git a/devtools/client/shared/test/browser_html_tooltip_variable-height.js b/devtools/client/shared/test/browser_html_tooltip_variable-height.js
new file mode 100644
index 0000000000..69f6d17aed
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_variable-height.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip content can have a variable height.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = CHROME_URL_ROOT + "doc_html_tooltip.xhtml";
+
+const CONTAINER_HEIGHT = 300;
+const CONTAINER_WIDTH = 200;
+const TOOLTIP_HEIGHT = 50;
+
+const {
+ HTMLTooltip,
+} = require("resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js");
+loadHelperScript("helper_html_tooltip.js");
+
+add_task(async function () {
+ // Force the toolbox to be 400px tall => 50px for each box.
+ await pushPref("devtools.toolbox.footer.height", 400);
+
+ await addTab("about:blank");
+ const { doc } = await createHost("bottom", TEST_URI);
+
+ const tooltip = new HTMLTooltip(doc, { useXulWrapper: false });
+ info("Set tooltip content 50px tall, but request a container 200px tall");
+ const tooltipContent = doc.createElementNS(HTML_NS, "div");
+ tooltipContent.style.cssText =
+ "height: " + TOOLTIP_HEIGHT + "px; background: red;";
+ tooltip.panel.appendChild(tooltipContent);
+ tooltip.setContentSize({ width: CONTAINER_WIDTH, height: Infinity });
+
+ info("Show the tooltip and check the container and panel height.");
+ await showTooltip(tooltip, doc.getElementById("box1"));
+
+ const containerRect = tooltip.container.getBoundingClientRect();
+ const panelRect = tooltip.panel.getBoundingClientRect();
+ is(
+ containerRect.height,
+ CONTAINER_HEIGHT,
+ "Tooltip container has the expected height."
+ );
+ is(
+ panelRect.height,
+ TOOLTIP_HEIGHT,
+ "Tooltip panel has the expected height."
+ );
+
+ info("Click below the tooltip panel but in the tooltip filler element.");
+ let onHidden = once(tooltip, "hidden");
+ EventUtils.synthesizeMouse(tooltip.container, 100, 100, {}, doc.defaultView);
+ await onHidden;
+
+ info("Show the tooltip one more time, and increase the content height");
+ await showTooltip(tooltip, doc.getElementById("box1"));
+ tooltipContent.style.height = 2 * CONTAINER_HEIGHT + "px";
+
+ info(
+ "Click at the same coordinates as earlier, this time it should hit the tooltip."
+ );
+ const onPanelClick = once(tooltip.panel, "click");
+ EventUtils.synthesizeMouse(tooltip.container, 100, 100, {}, doc.defaultView);
+ await onPanelClick;
+ is(tooltip.isVisible(), true, "Tooltip is still visible");
+
+ info("Click above the tooltip container, the tooltip should be closed.");
+ onHidden = once(tooltip, "hidden");
+ EventUtils.synthesizeMouse(tooltip.container, 100, -10, {}, doc.defaultView);
+ await onHidden;
+
+ tooltip.destroy();
+});
diff --git a/devtools/client/shared/test/browser_html_tooltip_width-auto.js b/devtools/client/shared/test/browser_html_tooltip_width-auto.js
new file mode 100644
index 0000000000..8aaf969d36
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_width-auto.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip content can automatically calculate its width based on content.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = CHROME_URL_ROOT + "doc_html_tooltip.xhtml";
+
+const {
+ HTMLTooltip,
+} = require("resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js");
+loadHelperScript("helper_html_tooltip.js");
+
+let useXulWrapper;
+
+add_task(async function () {
+ await addTab("about:blank");
+ const { doc } = await createHost("bottom", TEST_URI);
+
+ info("Run tests for a Tooltip without using a XUL panel");
+ useXulWrapper = false;
+ await runTests(doc);
+
+ info("Run tests for a Tooltip with a XUL panel");
+ useXulWrapper = true;
+ await runTests(doc);
+});
+
+async function runTests(doc) {
+ const tooltip = new HTMLTooltip(doc, { useXulWrapper });
+ info("Create tooltip content width to 150px");
+ const tooltipContent = doc.createElementNS(HTML_NS, "div");
+ tooltipContent.style.cssText = "height: 100%; width: 150px; background: red;";
+
+ info("Set tooltip content using width:auto");
+ tooltip.panel.appendChild(tooltipContent);
+ tooltip.setContentSize({ width: "auto", height: 50 });
+
+ info("Show the tooltip and check the tooltip panel width.");
+ await showTooltip(tooltip, doc.getElementById("box1"));
+
+ const containerRect = tooltip.container.getBoundingClientRect();
+ is(containerRect.width, 150, "Tooltip container has the expected width.");
+
+ await hideTooltip(tooltip);
+
+ tooltip.destroy();
+}
diff --git a/devtools/client/shared/test/browser_html_tooltip_xul-wrapper.js b/devtools/client/shared/test/browser_html_tooltip_xul-wrapper.js
new file mode 100644
index 0000000000..1eae311d5a
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_xul-wrapper.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip can overflow out of the toolbox when using a XUL panel wrapper.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = CHROME_URL_ROOT + "doc_html_tooltip-05.xhtml";
+
+const {
+ HTMLTooltip,
+} = require("resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js");
+loadHelperScript("helper_html_tooltip.js");
+
+// The test toolbox will be 200px tall, the anchors are 50px tall, therefore, the maximum
+// tooltip height that could fit in the toolbox is 150px. Setting 160px, the tooltip will
+// either have to overflow or to be resized.
+const TOOLTIP_HEIGHT = 160;
+const TOOLTIP_WIDTH = 200;
+
+add_task(async function () {
+ // Force the toolbox to be 200px high;
+ await pushPref("devtools.toolbox.footer.height", 200);
+
+ const { win, doc } = await createHost("bottom", TEST_URI);
+
+ info("Resize and move the window to have space below.");
+ const originalWidth = win.outerWidth;
+ const originalHeight = win.outerHeight;
+ win.resizeBy(-100, -200);
+
+ const originalTop = win.screenTop;
+ const originalLeft = win.screenLeft;
+ await moveWindowTo(win, 100, 100);
+
+ registerCleanupFunction(async () => {
+ info("Restore original window dimensions and position.");
+ win.resizeTo(originalWidth, originalHeight);
+ await moveWindowTo(win, originalLeft, originalTop);
+ });
+
+ info("Create HTML tooltip");
+ const tooltip = new HTMLTooltip(doc, { useXulWrapper: true });
+ const div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "200px";
+ div.style.background = "red";
+ tooltip.panel.appendChild(div);
+ tooltip.setContentSize({ width: TOOLTIP_WIDTH, height: TOOLTIP_HEIGHT });
+
+ const box1 = doc.getElementById("box1");
+
+ // Above box1: check that the tooltip can overflow onto the content page.
+ info("Display the tooltip above box1.");
+ await showTooltip(tooltip, box1, { position: "top" });
+ checkTooltip(tooltip, "top", TOOLTIP_HEIGHT);
+ await hideTooltip(tooltip);
+
+ // Below box1: check that the tooltip can overflow out of the browser window.
+ info("Display the tooltip below box1.");
+ await showTooltip(tooltip, box1, { position: "bottom" });
+ checkTooltip(tooltip, "bottom", TOOLTIP_HEIGHT);
+ await hideTooltip(tooltip);
+
+ is(tooltip.isVisible(), false, "Tooltip is not visible");
+
+ tooltip.destroy();
+});
+
+function checkTooltip(tooltip, position, height) {
+ is(tooltip.position, position, "Actual tooltip position is " + position);
+ const rect = tooltip.container.getBoundingClientRect();
+ is(rect.height, height, "Actual tooltip height is " + height);
+ // Testing the actual left/top offsets is not relevant here as it is handled by the XUL
+ // panel.
+}
diff --git a/devtools/client/shared/test/browser_html_tooltip_zoom.js b/devtools/client/shared/test/browser_html_tooltip_zoom.js
new file mode 100644
index 0000000000..f30aa586d3
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_zoom.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip is displayed correct position if content is zoomed in.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = CHROME_URL_ROOT + "doc_html_tooltip.xhtml";
+
+const {
+ HTMLTooltip,
+} = require("resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js");
+
+function getTooltipContent(doc) {
+ const div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "50px";
+ div.style.boxSizing = "border-box";
+ div.style.backgroundColor = "red";
+ div.textContent = "tooltip";
+ return div;
+}
+
+add_task(async function () {
+ const { host, doc } = await createHost("window", TEST_URI);
+
+ // Creating a window host is not correctly waiting when DevTools run in content frame
+ // See Bug 1571421.
+ await wait(1000);
+
+ const zoom = 1.5;
+ await pushPref("devtools.toolbox.zoomValue", zoom.toString(10));
+
+ // Change this xul zoom to the x1.5 since this test doesn't use the toolbox preferences.
+ host.frame.docShell.browsingContext.fullZoom = zoom;
+ const tooltip = new HTMLTooltip(doc, { useXulWrapper: true });
+
+ info("Set tooltip content");
+ tooltip.panel.appendChild(getTooltipContent(doc));
+ tooltip.setContentSize({ width: 100, height: 50 });
+
+ is(tooltip.isVisible(), false, "Tooltip is not visible");
+
+ info("Show the tooltip and check the expected events are fired.");
+ const onShown = tooltip.once("shown");
+ tooltip.show(doc.getElementById("box1"));
+ await onShown;
+
+ const menuRect = doc
+ .querySelector(".tooltip-xul-wrapper > .tooltip-container")
+ .getBoxQuads({ relativeTo: doc })[0]
+ .getBounds();
+ const anchorRect = doc
+ .getElementById("box1")
+ .getBoxQuads({ relativeTo: doc })[0]
+ .getBounds();
+ const xDelta = Math.abs(menuRect.left - anchorRect.left);
+ const yDelta = Math.abs(menuRect.top - anchorRect.bottom);
+
+ Assert.less(xDelta, 1, "xDelta: " + xDelta + ".");
+ Assert.less(yDelta, 1, "yDelta: " + yDelta + ".");
+
+ info("Hide the tooltip and check the expected events are fired.");
+
+ const onPopupHidden = tooltip.once("hidden");
+ tooltip.hide();
+ await onPopupHidden;
+
+ tooltip.destroy();
+ await host.destroy();
+});
diff --git a/devtools/client/shared/test/browser_inplace-editor-01.js b/devtools/client/shared/test/browser_inplace-editor-01.js
new file mode 100644
index 0000000000..b919fca946
--- /dev/null
+++ b/devtools/client/shared/test/browser_inplace-editor-01.js
@@ -0,0 +1,202 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_inplace_editor.js */
+
+"use strict";
+
+loadHelperScript("helper_inplace_editor.js");
+
+// Test the inplace-editor behavior.
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8,inline editor tests");
+ const { host, doc } = await createHost();
+
+ await testMultipleInitialization(doc);
+ await testReturnCommit(doc);
+ await testBlurCommit(doc);
+ await testAdvanceCharCommit(doc);
+ await testAdvanceCharsFunction(doc);
+ await testEscapeCancel(doc);
+ await testInputAriaLabel(doc);
+
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function testMultipleInitialization(doc) {
+ doc.body.innerHTML = "";
+ const options = {};
+ const span = (options.element = createSpan(doc));
+
+ info("Creating multiple inplace-editor fields");
+ editableField(options);
+ editableField(options);
+
+ info("Clicking on the inplace-editor field to turn to edit mode");
+ span.click();
+
+ is(span.style.display, "none", "The original is hidden");
+ is(doc.querySelectorAll("input").length, 1, "Only one ");
+ is(
+ doc.querySelectorAll("span").length,
+ 2,
+ "Correct number of elements"
+ );
+ is(
+ doc.querySelectorAll("span.autosizer").length,
+ 1,
+ "There is an autosizer element"
+ );
+}
+
+function testReturnCommit(doc) {
+ info("Testing that pressing return commits the new value");
+ return new Promise(resolve => {
+ createInplaceEditorAndClick(
+ {
+ initial: "explicit initial",
+ start(editor) {
+ is(
+ editor.input.value,
+ "explicit initial",
+ "Explicit initial value should be used."
+ );
+ editor.input.value = "Test Value";
+ EventUtils.sendKey("return");
+ },
+ done: onDone("Test Value", true, resolve),
+ },
+ doc
+ );
+ });
+}
+
+function testBlurCommit(doc) {
+ info("Testing that bluring the field commits the new value");
+ return new Promise(resolve => {
+ createInplaceEditorAndClick(
+ {
+ start(editor) {
+ is(editor.input.value, "Edit Me!", "textContent of the span used.");
+ editor.input.value = "Test Value";
+ editor.input.blur();
+ },
+ done: onDone("Test Value", true, resolve),
+ },
+ doc,
+ "Edit Me!"
+ );
+ });
+}
+
+function testAdvanceCharCommit(doc) {
+ info("Testing that configured advanceChars commit the new value");
+ return new Promise(resolve => {
+ createInplaceEditorAndClick(
+ {
+ advanceChars: ":",
+ start(editor) {
+ EventUtils.sendString("Test:");
+ },
+ done: onDone("Test", true, resolve),
+ },
+ doc
+ );
+ });
+}
+
+function testAdvanceCharsFunction(doc) {
+ info("Testing advanceChars as a function");
+ return new Promise(resolve => {
+ let firstTime = true;
+
+ createInplaceEditorAndClick(
+ {
+ initial: "",
+ advanceChars(charCode, text, insertionPoint) {
+ if (charCode !== KeyboardEvent.DOM_VK_COLON) {
+ return false;
+ }
+ if (firstTime) {
+ firstTime = false;
+ return false;
+ }
+
+ // Just to make sure we check it somehow.
+ return !!text.length;
+ },
+ start(editor) {
+ for (const ch of ":Test:") {
+ EventUtils.sendChar(ch);
+ }
+ },
+ done: onDone(":Test", true, resolve),
+ },
+ doc
+ );
+ });
+}
+
+function testEscapeCancel(doc) {
+ info("Testing that escape cancels the new value");
+ return new Promise(resolve => {
+ createInplaceEditorAndClick(
+ {
+ initial: "initial text",
+ start(editor) {
+ editor.input.value = "Test Value";
+ EventUtils.sendKey("escape");
+ },
+ done: onDone("initial text", false, resolve),
+ },
+ doc
+ );
+ });
+}
+
+function testInputAriaLabel(doc) {
+ info("Testing that inputAriaLabel works as expected");
+ doc.body.innerHTML = "";
+
+ let element = createSpan(doc);
+ editableField({
+ element,
+ inputAriaLabel: "TEST_ARIA_LABEL",
+ });
+
+ info("Clicking on the inplace-editor field to turn to edit mode");
+ element.click();
+ let input = doc.querySelector("input");
+ is(
+ input.getAttribute("aria-label"),
+ "TEST_ARIA_LABEL",
+ "Input has expected aria-label"
+ );
+
+ info("Testing that inputAriaLabelledBy works as expected");
+ doc.body.innerHTML = "";
+ element = createSpan(doc);
+ editableField({
+ element,
+ inputAriaLabelledBy: "TEST_ARIA_LABELLED_BY",
+ });
+
+ info("Clicking on the inplace-editor field to turn to edit mode");
+ element.click();
+ input = doc.querySelector("input");
+ is(
+ input.getAttribute("aria-labelledby"),
+ "TEST_ARIA_LABELLED_BY",
+ "Input has expected aria-labelledby"
+ );
+}
+
+function onDone(value, isCommit, resolve) {
+ return function (actualValue, actualCommit) {
+ info("Inplace-editor's done callback executed, checking its state");
+ is(actualValue, value, "The value is correct");
+ is(actualCommit, isCommit, "The commit boolean is correct");
+ resolve();
+ };
+}
diff --git a/devtools/client/shared/test/browser_inplace-editor-02.js b/devtools/client/shared/test/browser_inplace-editor-02.js
new file mode 100644
index 0000000000..9ecb0bca02
--- /dev/null
+++ b/devtools/client/shared/test/browser_inplace-editor-02.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_inplace_editor.js */
+
+"use strict";
+
+loadHelperScript("helper_inplace_editor.js");
+
+// Test that the trimOutput option for the inplace editor works correctly.
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8,inline editor tests");
+ const { host, doc } = await createHost();
+
+ await testNonTrimmed(doc);
+ await testTrimmed(doc);
+
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function testNonTrimmed(doc) {
+ info("Testing the trimOutput=false option");
+ return new Promise(resolve => {
+ const initial = "\nMultiple\nLines\n";
+ const changed = " \nMultiple\nLines\n with more whitespace ";
+ createInplaceEditorAndClick(
+ {
+ trimOutput: false,
+ multiline: true,
+ initial,
+ start(editor) {
+ is(
+ editor.input.value,
+ initial,
+ "Explicit initial value should be used."
+ );
+ editor.input.value = changed;
+ EventUtils.sendKey("return");
+ },
+ done: onDone(changed, true, resolve),
+ },
+ doc
+ );
+ });
+}
+
+function testTrimmed(doc) {
+ info("Testing the trimOutput=true option (default value)");
+ return new Promise(resolve => {
+ const initial = "\nMultiple\nLines\n";
+ const changed = " \nMultiple\nLines\n with more whitespace ";
+ createInplaceEditorAndClick(
+ {
+ initial,
+ multiline: true,
+ start(editor) {
+ is(
+ editor.input.value,
+ initial,
+ "Explicit initial value should be used."
+ );
+ editor.input.value = changed;
+ EventUtils.sendKey("return");
+ },
+ done: onDone(changed.trim(), true, resolve),
+ },
+ doc
+ );
+ });
+}
+
+function onDone(value, isCommit, resolve) {
+ return function (actualValue, actualCommit) {
+ info("Inplace-editor's done callback executed, checking its state");
+ is(actualValue, value, "The value is correct");
+ is(actualCommit, isCommit, "The commit boolean is correct");
+ resolve();
+ };
+}
diff --git a/devtools/client/shared/test/browser_inplace-editor_autoclose_parentheses.js b/devtools/client/shared/test/browser_inplace-editor_autoclose_parentheses.js
new file mode 100644
index 0000000000..79f4a2d14a
--- /dev/null
+++ b/devtools/client/shared/test/browser_inplace-editor_autoclose_parentheses.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_inplace_editor.js */
+
+"use strict";
+
+const AutocompletePopup = require("resource://devtools/client/shared/autocomplete-popup.js");
+const {
+ InplaceEditor,
+} = require("resource://devtools/client/shared/inplace-editor.js");
+loadHelperScript("helper_inplace_editor.js");
+
+// Test the inplace-editor closes parentheses automatically.
+
+// format :
+// [
+// what key to press,
+// expected input box value after keypress,
+// selected suggestion index (-1 if popup is hidden),
+// number of suggestions in the popup (0 if popup is hidden),
+// ]
+const testData = [
+ ["u", "u", -1, 0],
+ ["r", "ur", -1, 0],
+ ["l", "url", -1, 0],
+ ["(", "url()", -1, 0],
+ ["v", "url(v)", -1, 0],
+ ["a", "url(va)", -1, 0],
+ ["r", "url(var)", -1, 0],
+ ["(", "url(var())", -1, 0],
+ ["-", "url(var(-))", -1, 0],
+ ["-", "url(var(--))", -1, 0],
+ ["a", "url(var(--a))", -1, 0],
+ [")", "url(var(--a))", -1, 0],
+ [")", "url(var(--a))", -1, 0],
+];
+
+add_task(async function () {
+ await addTab(
+ "data:text/html;charset=utf-8," + "inplace editor parentheses autoclose"
+ );
+ const { host, doc } = await createHost();
+
+ const popup = new AutocompletePopup(doc, { autoSelect: true });
+ await new Promise(resolve => {
+ createInplaceEditorAndClick(
+ {
+ start: runPropertyAutocompletionTest,
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE,
+ property: {
+ name: "background-image",
+ },
+ cssProperties: {
+ // No need to test autocompletion here, return an empty array.
+ getNames: () => [],
+ getValues: () => [],
+ },
+ cssVariables: new Map(),
+ done: resolve,
+ popup,
+ },
+ doc
+ );
+ });
+
+ popup.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+const runPropertyAutocompletionTest = async function (editor) {
+ info("Starting to test for css property completion");
+ for (const data of testData) {
+ await testCompletion(data, editor);
+ }
+ EventUtils.synthesizeKey("VK_RETURN", {}, editor.input.defaultView);
+};
diff --git a/devtools/client/shared/test/browser_inplace-editor_autocomplete_01.js b/devtools/client/shared/test/browser_inplace-editor_autocomplete_01.js
new file mode 100644
index 0000000000..1ed10778c3
--- /dev/null
+++ b/devtools/client/shared/test/browser_inplace-editor_autocomplete_01.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_inplace_editor.js */
+
+"use strict";
+
+const AutocompletePopup = require("resource://devtools/client/shared/autocomplete-popup.js");
+const {
+ InplaceEditor,
+} = require("resource://devtools/client/shared/inplace-editor.js");
+loadHelperScript("helper_inplace_editor.js");
+
+// Test the inplace-editor autocomplete popup for CSS properties suggestions.
+// Using a mocked list of CSS properties to avoid test failures linked to
+// engine changes (new property, removed property, ...).
+
+// format :
+// [
+// what key to press,
+// expected input box value after keypress,
+// selected suggestion index (-1 if popup is hidden),
+// number of suggestions in the popup (0 if popup is hidden),
+// ]
+const testData = [
+ ["b", "border", 1, 3],
+ ["VK_DOWN", "box-sizing", 2, 3],
+ ["VK_DOWN", "background", 0, 3],
+ ["VK_DOWN", "border", 1, 3],
+ ["VK_BACK_SPACE", "b", -1, 0],
+ ["VK_BACK_SPACE", "", -1, 0],
+ ["VK_DOWN", "background", 0, 6],
+ ["VK_LEFT", "background", -1, 0],
+];
+
+const mockValues = {
+ background: [],
+ border: [],
+ "box-sizing": [],
+ color: [],
+ display: [],
+ visibility: [],
+};
+
+add_task(async function () {
+ await addTab(
+ "data:text/html;charset=utf-8," + "inplace editor CSS property autocomplete"
+ );
+ const { host, doc } = await createHost();
+
+ const popup = new AutocompletePopup(doc, { autoSelect: true });
+ await new Promise(resolve => {
+ createInplaceEditorAndClick(
+ {
+ start: runPropertyAutocompletionTest,
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY,
+ done: resolve,
+ popup,
+ cssProperties: {
+ getNames: () => Object.keys(mockValues),
+ getValues: propertyName => mockValues[propertyName] || [],
+ },
+ },
+ doc
+ );
+ });
+
+ popup.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+const runPropertyAutocompletionTest = async function (editor) {
+ info("Starting to test for css property completion");
+ for (const data of testData) {
+ await testCompletion(data, editor);
+ }
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, editor.input.defaultView);
+};
diff --git a/devtools/client/shared/test/browser_inplace-editor_autocomplete_02.js b/devtools/client/shared/test/browser_inplace-editor_autocomplete_02.js
new file mode 100644
index 0000000000..44fbffc207
--- /dev/null
+++ b/devtools/client/shared/test/browser_inplace-editor_autocomplete_02.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_inplace_editor.js */
+
+"use strict";
+
+const AutocompletePopup = require("resource://devtools/client/shared/autocomplete-popup.js");
+const {
+ InplaceEditor,
+} = require("resource://devtools/client/shared/inplace-editor.js");
+loadHelperScript("helper_inplace_editor.js");
+
+// Test the inplace-editor autocomplete popup for CSS values suggestions.
+// Using a mocked list of CSS properties to avoid test failures linked to
+// engine changes (new property, removed property, ...).
+
+// format :
+// [
+// what key to press,
+// expected input box value after keypress,
+// selected suggestion index (-1 if popup is hidden),
+// number of suggestions in the popup (0 if popup is hidden),
+// ]
+const testData = [
+ ["b", "block", -1, 0],
+ ["VK_BACK_SPACE", "b", -1, 0],
+ ["VK_BACK_SPACE", "", -1, 0],
+ ["i", "inline", 0, 2],
+ ["VK_DOWN", "inline-block", 1, 2],
+ ["VK_DOWN", "inline", 0, 2],
+ ["VK_LEFT", "inline", -1, 0],
+];
+
+const mockValues = {
+ display: ["block", "flex", "inline", "inline-block", "none"],
+};
+
+add_task(async function () {
+ await addTab(
+ "data:text/html;charset=utf-8," + "inplace editor CSS value autocomplete"
+ );
+ const { host, win, doc } = await createHost();
+
+ const xulDocument = win.top.document;
+ const popup = new AutocompletePopup(xulDocument, { autoSelect: true });
+
+ await new Promise(resolve => {
+ createInplaceEditorAndClick(
+ {
+ start: runAutocompletionTest,
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE,
+ property: {
+ name: "display",
+ },
+ cssProperties: {
+ getNames: () => Object.keys(mockValues),
+ getValues: propertyName => mockValues[propertyName] || [],
+ },
+ done: resolve,
+ popup,
+ },
+ doc
+ );
+ });
+
+ popup.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+const runAutocompletionTest = async function (editor) {
+ info("Starting to test for css property completion");
+ for (const data of testData) {
+ await testCompletion(data, editor);
+ }
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, editor.input.defaultView);
+};
diff --git a/devtools/client/shared/test/browser_inplace-editor_autocomplete_css_variable.js b/devtools/client/shared/test/browser_inplace-editor_autocomplete_css_variable.js
new file mode 100644
index 0000000000..8a27778670
--- /dev/null
+++ b/devtools/client/shared/test/browser_inplace-editor_autocomplete_css_variable.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_inplace_editor.js */
+
+"use strict";
+
+const AutocompletePopup = require("resource://devtools/client/shared/autocomplete-popup.js");
+const {
+ InplaceEditor,
+} = require("resource://devtools/client/shared/inplace-editor.js");
+loadHelperScript("helper_inplace_editor.js");
+
+// Test the inplace-editor autocomplete popup for variable suggestions.
+// Using a mocked list of CSS variables to avoid test failures linked to
+// engine changes (new property, removed property, ...).
+// Also using a mocked list of CSS properties to avoid autocompletion when
+// typing in "var"
+
+// Used for representing the expectation of a visible color swatch
+const COLORSWATCH = true;
+// format :
+// [
+// what key to press,
+// expected input box value after keypress,
+// selected suggestion index (-1 if popup is hidden),
+// number of suggestions in the popup (0 if popup is hidden),
+// expected post label corresponding with the input box value,
+// boolean representing if there should be a colour swatch visible,
+// ]
+const testData = [
+ ["v", "v", -1, 0, null, !COLORSWATCH],
+ ["a", "va", -1, 0, null, !COLORSWATCH],
+ ["r", "var", -1, 0, null, !COLORSWATCH],
+ ["(", "var()", -1, 0, null, !COLORSWATCH],
+ ["-", "var(--abc)", 0, 9, "inherit", !COLORSWATCH],
+ ["VK_BACK_SPACE", "var(-)", -1, 0, null, !COLORSWATCH],
+ ["-", "var(--abc)", 0, 9, "inherit", !COLORSWATCH],
+ ["VK_DOWN", "var(--def)", 1, 9, "transparent", !COLORSWATCH],
+ ["VK_DOWN", "var(--ghi)", 2, 9, "#00FF00", COLORSWATCH],
+ ["VK_DOWN", "var(--jkl)", 3, 9, "rgb(255, 0, 0)", COLORSWATCH],
+ ["VK_DOWN", "var(--mno)", 4, 9, "hsl(120, 60%, 70%)", COLORSWATCH],
+ ["VK_DOWN", "var(--pqr)", 5, 9, "BlueViolet", COLORSWATCH],
+ ["VK_DOWN", "var(--stu)", 6, 9, "15px", !COLORSWATCH],
+ ["VK_DOWN", "var(--vwx)", 7, 9, "rgba(255, 0, 0, 0.4)", COLORSWATCH],
+ ["VK_DOWN", "var(--yz)", 8, 9, "hsla(120, 60%, 70%, 0.3)", COLORSWATCH],
+ ["VK_DOWN", "var(--abc)", 0, 9, "inherit", !COLORSWATCH],
+ ["VK_DOWN", "var(--def)", 1, 9, "transparent", !COLORSWATCH],
+ ["VK_DOWN", "var(--ghi)", 2, 9, "#00FF00", COLORSWATCH],
+ ["VK_LEFT", "var(--ghi)", -1, 0, null, !COLORSWATCH],
+];
+
+const CSS_VARIABLES = [
+ ["--abc", "inherit"],
+ ["--def", "transparent"],
+ ["--ghi", "#00FF00"],
+ ["--jkl", "rgb(255, 0, 0)"],
+ ["--mno", "hsl(120, 60%, 70%)"],
+ ["--pqr", "BlueViolet"],
+ ["--stu", "15px"],
+ ["--vwx", "rgba(255, 0, 0, 0.4)"],
+ ["--yz", "hsla(120, 60%, 70%, 0.3)"],
+];
+
+add_task(async function () {
+ await addTab(
+ "data:text/html;charset=utf-8,inplace editor CSS variable autocomplete"
+ );
+ const { host, doc } = await createHost();
+
+ const popup = new AutocompletePopup(doc, { autoSelect: true });
+
+ await new Promise(resolve => {
+ createInplaceEditorAndClick(
+ {
+ start: runAutocompletionTest,
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE,
+ property: {
+ name: "color",
+ },
+ cssProperties: {
+ getNames: () => [],
+ getValues: () => [],
+ },
+ cssVariables: new Map(CSS_VARIABLES),
+ done: resolve,
+ popup,
+ },
+ doc
+ );
+ });
+
+ popup.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+const runAutocompletionTest = async function (editor) {
+ info("Starting to test for css variable completion");
+ for (const data of testData) {
+ await testCompletion(data, editor);
+ }
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, editor.input.defaultView);
+};
diff --git a/devtools/client/shared/test/browser_inplace-editor_autocomplete_offset.js b/devtools/client/shared/test/browser_inplace-editor_autocomplete_offset.js
new file mode 100644
index 0000000000..7272ad7091
--- /dev/null
+++ b/devtools/client/shared/test/browser_inplace-editor_autocomplete_offset.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_inplace_editor.js */
+
+"use strict";
+
+const AutocompletePopup = require("resource://devtools/client/shared/autocomplete-popup.js");
+const {
+ InplaceEditor,
+} = require("resource://devtools/client/shared/inplace-editor.js");
+loadHelperScript("helper_inplace_editor.js");
+
+const TEST_URI =
+ CHROME_URL_ROOT + "doc_inplace-editor_autocomplete_offset.xhtml";
+
+// Test the inplace-editor autocomplete popup is aligned with the completed query.
+// Which means when completing "style=display:flex; color:" the popup will aim to be
+// aligned with the ":" next to "color".
+
+// format :
+// [
+// what key to press,
+// expected input box value after keypress,
+// selected suggestion index (-1 if popup is hidden),
+// number of suggestions in the popup (0 if popup is hidden),
+// ]
+// or
+// ["checkPopupOffset"]
+// to measure and test the autocomplete popup left offset.
+const testData = [
+ ["VK_RIGHT", "style=", -1, 0],
+ ["d", "style=display", 1, 2],
+ ["checkPopupOffset"],
+ ["VK_RIGHT", "style=display", -1, 0],
+ [":", "style=display:block", 0, 3],
+ ["checkPopupOffset"],
+ ["f", "style=display:flex", -1, 0],
+ ["VK_RIGHT", "style=display:flex", -1, 0],
+ [";", "style=display:flex;", -1, 0],
+ ["c", "style=display:flex;color", 1, 2],
+ ["checkPopupOffset"],
+ ["VK_RIGHT", "style=display:flex;color", -1, 0],
+ [":", "style=display:flex;color:blue", 0, 2],
+ ["checkPopupOffset"],
+];
+
+const mockValues = {
+ clear: [],
+ color: ["blue", "red"],
+ direction: [],
+ display: ["block", "flex", "none"],
+};
+
+add_task(async function () {
+ await addTab(
+ "data:text/html;charset=utf-8,inplace editor CSS value autocomplete"
+ );
+ const { host, doc } = await createHost("bottom", TEST_URI);
+
+ const popup = new AutocompletePopup(doc, { autoSelect: true });
+
+ info("Create a CSS_MIXED type autocomplete");
+ await new Promise(resolve => {
+ createInplaceEditorAndClick(
+ {
+ initial: "style=",
+ start: runAutocompletionTest,
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED,
+ done: resolve,
+ popup,
+ cssProperties: {
+ getNames: () => Object.keys(mockValues),
+ getValues: propertyName => mockValues[propertyName] || [],
+ },
+ },
+ doc
+ );
+ });
+
+ popup.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+const runAutocompletionTest = async function (editor) {
+ info("Starting autocomplete test for inplace-editor popup offset");
+ let previousOffset = -1;
+ for (const data of testData) {
+ if (data[0] === "checkPopupOffset") {
+ info("Check the popup offset has been modified");
+ // We are not testing hard coded offset values here, which could be fragile. We only
+ // want to ensure the popup tries to match the position of the query in the editor
+ // input.
+ const offset = getPopupOffset(editor);
+ Assert.greater(
+ offset,
+ previousOffset,
+ "New popup offset is greater than the previous one"
+ );
+ previousOffset = offset;
+ } else {
+ await testCompletion(data, editor);
+ }
+ }
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, editor.input.defaultView);
+};
+
+/**
+ * Get the autocomplete panel left offset, relative to the provided input's left offset.
+ */
+function getPopupOffset({ popup, input }) {
+ const popupQuads = popup._panel.getBoxQuads({ relativeTo: input });
+ return popupQuads[0].getBounds().left;
+}
diff --git a/devtools/client/shared/test/browser_inplace-editor_focus_closest_editor.js b/devtools/client/shared/test/browser_inplace-editor_focus_closest_editor.js
new file mode 100644
index 0000000000..5d721410ac
--- /dev/null
+++ b/devtools/client/shared/test/browser_inplace-editor_focus_closest_editor.js
@@ -0,0 +1,180 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_inplace_editor.js */
+
+"use strict";
+
+loadHelperScript("helper_inplace_editor.js");
+
+// Test the inplace-editor behavior with focusEditableFieldAfterApply
+// and focusEditableFieldContainerSelector options
+
+add_task(async function () {
+ await addTab(
+ "data:text/html;charset=utf-8,inline editor focusEditableFieldAfterApply"
+ );
+ const { host, doc } = await createHost();
+
+ testFocusNavigationWithMultipleEditor(doc);
+ testFocusNavigationWithNonMatchingFocusEditableFieldContainerSelector(doc);
+ testMissingFocusEditableFieldContainerSelector(doc);
+
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function testFocusNavigationWithMultipleEditor(doc) {
+ // For some reason
+ SPAN 3
+ BUTTON 3
+
+
+
+
+ `;
+
+ const span1 = doc.getElementById("span-1");
+ const span2 = doc.getElementById("span-2");
+ const span3 = doc.getElementById("span-3");
+
+ info("Create 3 editable fields for the 3 spans inside the main element");
+ const options = {
+ focusEditableFieldAfterApply: true,
+ focusEditableFieldContainerSelector: "main",
+ };
+ editableField({
+ element: span1,
+ ...options,
+ });
+ editableField({
+ element: span2,
+ ...options,
+ });
+ editableField({
+ element: span3,
+ ...options,
+ });
+
+ span1.click();
+
+ is(
+ doc.activeElement.inplaceEditor.elt.id,
+ "span-1",
+ "Visible editable field is the one associated with span-1"
+ );
+ assertFocusedElementInplaceEditorInput(doc);
+
+ EventUtils.sendKey("Tab");
+
+ is(
+ doc.activeElement.inplaceEditor.elt.id,
+ "span-2",
+ "Using Tab moved focus to span-2 editable field"
+ );
+ assertFocusedElementInplaceEditorInput(doc);
+
+ EventUtils.sendKey("Tab");
+
+ is(
+ doc.activeElement.inplaceEditor.elt.id,
+ "span-3",
+ "Using Tab moved focus to span-3 editable field"
+ );
+ assertFocusedElementInplaceEditorInput(doc);
+
+ EventUtils.sendKey("Tab");
+
+ is(
+ doc.activeElement.id,
+ "sidebar-button",
+ "Using Tab moved focus outside of "
+ );
+}
+
+function testFocusNavigationWithNonMatchingFocusEditableFieldContainerSelector(
+ doc
+) {
+ // For some reason