summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/test
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/test')
-rw-r--r--devtools/client/shared/test/.eslintrc.js9
-rw-r--r--devtools/client/shared/test/browser.ini188
-rw-r--r--devtools/client/shared/test/browser_css_angle.js176
-rw-r--r--devtools/client/shared/test/browser_css_color.js137
-rw-r--r--devtools/client/shared/test/browser_cubic-bezier-01.js38
-rw-r--r--devtools/client/shared/test/browser_cubic-bezier-02.js200
-rw-r--r--devtools/client/shared/test/browser_cubic-bezier-03.js68
-rw-r--r--devtools/client/shared/test/browser_cubic-bezier-04.js50
-rw-r--r--devtools/client/shared/test/browser_cubic-bezier-05.js48
-rw-r--r--devtools/client/shared/test/browser_cubic-bezier-06.js79
-rw-r--r--devtools/client/shared/test/browser_devices.js57
-rw-r--r--devtools/client/shared/test/browser_devices.json23
-rw-r--r--devtools/client/shared/test/browser_filter-editor-01.js114
-rw-r--r--devtools/client/shared/test/browser_filter-editor-02.js107
-rw-r--r--devtools/client/shared/test/browser_filter-editor-03.js65
-rw-r--r--devtools/client/shared/test/browser_filter-editor-04.js87
-rw-r--r--devtools/client/shared/test/browser_filter-editor-05.js148
-rw-r--r--devtools/client/shared/test/browser_filter-editor-06.js71
-rw-r--r--devtools/client/shared/test/browser_filter-editor-07.js27
-rw-r--r--devtools/client/shared/test/browser_filter-editor-08.js84
-rw-r--r--devtools/client/shared/test/browser_filter-editor-09.js125
-rw-r--r--devtools/client/shared/test/browser_filter-editor-10.js87
-rw-r--r--devtools/client/shared/test/browser_filter-presets-01.js99
-rw-r--r--devtools/client/shared/test/browser_filter-presets-02.js45
-rw-r--r--devtools/client/shared/test/browser_filter-presets-03.js40
-rw-r--r--devtools/client/shared/test/browser_flame-graph-01.js61
-rw-r--r--devtools/client/shared/test/browser_flame-graph-02.js44
-rw-r--r--devtools/client/shared/test/browser_flame-graph-03a.js138
-rw-r--r--devtools/client/shared/test/browser_flame-graph-03b.js92
-rw-r--r--devtools/client/shared/test/browser_flame-graph-03c.js155
-rw-r--r--devtools/client/shared/test/browser_flame-graph-04.js90
-rw-r--r--devtools/client/shared/test/browser_flame-graph-05.js113
-rw-r--r--devtools/client/shared/test/browser_flame-graph-utils-01.js256
-rw-r--r--devtools/client/shared/test/browser_flame-graph-utils-02.js130
-rw-r--r--devtools/client/shared/test/browser_flame-graph-utils-03.js136
-rw-r--r--devtools/client/shared/test/browser_flame-graph-utils-04.js188
-rw-r--r--devtools/client/shared/test/browser_flame-graph-utils-05.js48
-rw-r--r--devtools/client/shared/test/browser_flame-graph-utils-06.js117
-rw-r--r--devtools/client/shared/test/browser_flame-graph-utils-hash.js24
-rw-r--r--devtools/client/shared/test/browser_graphs-01.js70
-rw-r--r--devtools/client/shared/test/browser_graphs-02.js107
-rw-r--r--devtools/client/shared/test/browser_graphs-03.js111
-rw-r--r--devtools/client/shared/test/browser_graphs-04.js69
-rw-r--r--devtools/client/shared/test/browser_graphs-05.js154
-rw-r--r--devtools/client/shared/test/browser_graphs-06.js112
-rw-r--r--devtools/client/shared/test/browser_graphs-07a.js232
-rw-r--r--devtools/client/shared/test/browser_graphs-07b.js88
-rw-r--r--devtools/client/shared/test/browser_graphs-07c.js139
-rw-r--r--devtools/client/shared/test/browser_graphs-07d.js71
-rw-r--r--devtools/client/shared/test/browser_graphs-07e.js127
-rw-r--r--devtools/client/shared/test/browser_graphs-08.js88
-rw-r--r--devtools/client/shared/test/browser_graphs-09a.js104
-rw-r--r--devtools/client/shared/test/browser_graphs-09b.js82
-rw-r--r--devtools/client/shared/test/browser_graphs-09c.js38
-rw-r--r--devtools/client/shared/test/browser_graphs-09d.js39
-rw-r--r--devtools/client/shared/test/browser_graphs-09e.js84
-rw-r--r--devtools/client/shared/test/browser_graphs-09f.js53
-rw-r--r--devtools/client/shared/test/browser_graphs-10a.js162
-rw-r--r--devtools/client/shared/test/browser_graphs-10b.js71
-rw-r--r--devtools/client/shared/test/browser_graphs-10c.js109
-rw-r--r--devtools/client/shared/test/browser_graphs-11a.js60
-rw-r--r--devtools/client/shared/test/browser_graphs-11b.js133
-rw-r--r--devtools/client/shared/test/browser_graphs-12.js157
-rw-r--r--devtools/client/shared/test/browser_graphs-13.js44
-rw-r--r--devtools/client/shared/test/browser_graphs-14.js111
-rw-r--r--devtools/client/shared/test/browser_graphs-15.js49
-rw-r--r--devtools/client/shared/test/browser_graphs-16.js45
-rw-r--r--devtools/client/shared/test/browser_html_tooltip-01.js88
-rw-r--r--devtools/client/shared/test/browser_html_tooltip-02.js174
-rw-r--r--devtools/client/shared/test/browser_html_tooltip-03.js155
-rw-r--r--devtools/client/shared/test/browser_html_tooltip-04.js110
-rw-r--r--devtools/client/shared/test/browser_html_tooltip-05.js109
-rw-r--r--devtools/client/shared/test/browser_html_tooltip_arrow-01.js108
-rw-r--r--devtools/client/shared/test/browser_html_tooltip_arrow-02.js100
-rw-r--r--devtools/client/shared/test/browser_html_tooltip_consecutive-show.js71
-rw-r--r--devtools/client/shared/test/browser_html_tooltip_hover.js65
-rw-r--r--devtools/client/shared/test/browser_html_tooltip_offset.js99
-rw-r--r--devtools/client/shared/test/browser_html_tooltip_rtl.js140
-rw-r--r--devtools/client/shared/test/browser_html_tooltip_variable-height.js75
-rw-r--r--devtools/client/shared/test/browser_html_tooltip_width-auto.js61
-rw-r--r--devtools/client/shared/test/browser_html_tooltip_xul-wrapper.js80
-rw-r--r--devtools/client/shared/test/browser_inplace-editor-01.js150
-rw-r--r--devtools/client/shared/test/browser_inplace-editor-02.js71
-rw-r--r--devtools/client/shared/test/browser_inplace-editor_autocomplete_01.js75
-rw-r--r--devtools/client/shared/test/browser_inplace-editor_autocomplete_02.js80
-rw-r--r--devtools/client/shared/test/browser_inplace-editor_autocomplete_offset.js119
-rw-r--r--devtools/client/shared/test/browser_inplace-editor_maxwidth.js114
-rw-r--r--devtools/client/shared/test/browser_key_shortcuts.js425
-rw-r--r--devtools/client/shared/test/browser_keycodes.js12
-rw-r--r--devtools/client/shared/test/browser_layoutHelpers-getBoxQuads.html65
-rw-r--r--devtools/client/shared/test/browser_layoutHelpers-getBoxQuads.js219
-rw-r--r--devtools/client/shared/test/browser_layoutHelpers.html24
-rw-r--r--devtools/client/shared/test/browser_layoutHelpers.js93
-rw-r--r--devtools/client/shared/test/browser_mdn-docs-01.js168
-rw-r--r--devtools/client/shared/test/browser_mdn-docs-02.js128
-rw-r--r--devtools/client/shared/test/browser_mdn-docs-03.js277
-rw-r--r--devtools/client/shared/test/browser_num-l10n.js27
-rw-r--r--devtools/client/shared/test/browser_options-view-01.js110
-rw-r--r--devtools/client/shared/test/browser_outputparser.js292
-rw-r--r--devtools/client/shared/test/browser_poller.js136
-rw-r--r--devtools/client/shared/test/browser_prefs-01.js44
-rw-r--r--devtools/client/shared/test/browser_prefs-02.js45
-rw-r--r--devtools/client/shared/test/browser_require_raw.js20
-rw-r--r--devtools/client/shared/test/browser_spectrum.js114
-rw-r--r--devtools/client/shared/test/browser_tableWidget_basic.js390
-rw-r--r--devtools/client/shared/test/browser_tableWidget_keyboard_interaction.js194
-rw-r--r--devtools/client/shared/test/browser_tableWidget_mouse_interaction.js317
-rw-r--r--devtools/client/shared/test/browser_telemetry_button_eyedropper.js52
-rw-r--r--devtools/client/shared/test/browser_telemetry_button_paintflashing.js89
-rw-r--r--devtools/client/shared/test/browser_telemetry_button_responsive.js95
-rw-r--r--devtools/client/shared/test/browser_telemetry_button_scratchpad.js127
-rw-r--r--devtools/client/shared/test/browser_telemetry_sidebar.js84
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolbox.js22
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_canvasdebugger.js29
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_inspector.js22
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_jsdebugger.js22
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_jsprofiler.js22
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_netmonitor.js23
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_options.js22
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_shadereditor.js37
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_storage.js28
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_styleeditor.js23
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_webaudioeditor.js29
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_webconsole.js22
-rw-r--r--devtools/client/shared/test/browser_templater_basic.html13
-rw-r--r--devtools/client/shared/test/browser_templater_basic.js286
-rw-r--r--devtools/client/shared/test/browser_theme.js98
-rw-r--r--devtools/client/shared/test/browser_theme_switching.js53
-rw-r--r--devtools/client/shared/test/browser_toolbar_basic.html40
-rw-r--r--devtools/client/shared/test/browser_toolbar_basic.js60
-rw-r--r--devtools/client/shared/test/browser_toolbar_tooltip.js111
-rw-r--r--devtools/client/shared/test/browser_toolbar_webconsole_errors_count.html33
-rw-r--r--devtools/client/shared/test/browser_toolbar_webconsole_errors_count.js256
-rw-r--r--devtools/client/shared/test/browser_treeWidget_basic.js267
-rw-r--r--devtools/client/shared/test/browser_treeWidget_keyboard_interaction.js228
-rw-r--r--devtools/client/shared/test/browser_treeWidget_mouse_interaction.js135
-rw-r--r--devtools/client/shared/test/doc_options-view.xul26
-rw-r--r--devtools/client/shared/test/head.js346
-rw-r--r--devtools/client/shared/test/helper_color_data.js175
-rw-r--r--devtools/client/shared/test/helper_html_tooltip.js96
-rw-r--r--devtools/client/shared/test/helper_inplace_editor.js115
-rw-r--r--devtools/client/shared/test/html-mdn-css-basic-testing.html21
-rw-r--r--devtools/client/shared/test/html-mdn-css-no-summary-or-syntax.html12
-rw-r--r--devtools/client/shared/test/html-mdn-css-no-summary.html21
-rw-r--r--devtools/client/shared/test/html-mdn-css-no-syntax.html17
-rw-r--r--devtools/client/shared/test/html-mdn-css-syntax-old-style.html23
-rw-r--r--devtools/client/shared/test/leakhunt.js165
-rw-r--r--devtools/client/shared/test/test-actor-registry.js97
-rw-r--r--devtools/client/shared/test/test-actor.js1138
-rw-r--r--devtools/client/shared/test/unit/.eslintrc.js6
-rw-r--r--devtools/client/shared/test/unit/test_VariablesView_filtering-without-controller.js36
-rw-r--r--devtools/client/shared/test/unit/test_VariablesView_getString_promise.js76
-rw-r--r--devtools/client/shared/test/unit/test_advanceValidate.js31
-rw-r--r--devtools/client/shared/test/unit/test_attribute-parsing-01.js73
-rw-r--r--devtools/client/shared/test/unit/test_attribute-parsing-02.js134
-rw-r--r--devtools/client/shared/test/unit/test_bezierCanvas.js117
-rw-r--r--devtools/client/shared/test/unit/test_cssAngle.js33
-rw-r--r--devtools/client/shared/test/unit/test_cssColor-01.js76
-rw-r--r--devtools/client/shared/test/unit/test_cssColor-02.js45
-rw-r--r--devtools/client/shared/test/unit/test_cssColor-03.js61
-rw-r--r--devtools/client/shared/test/unit/test_cssColorDatabase.js63
-rw-r--r--devtools/client/shared/test/unit/test_cubicBezier.js146
-rw-r--r--devtools/client/shared/test/unit/test_escapeCSSComment.js40
-rw-r--r--devtools/client/shared/test/unit/test_parseDeclarations.js439
-rw-r--r--devtools/client/shared/test/unit/test_parsePseudoClassesAndAttributes.js213
-rw-r--r--devtools/client/shared/test/unit/test_parseSingleValue.js93
-rw-r--r--devtools/client/shared/test/unit/test_rewriteDeclarations.js529
-rw-r--r--devtools/client/shared/test/unit/test_source-utils.js181
-rw-r--r--devtools/client/shared/test/unit/test_suggestion-picker.js149
-rw-r--r--devtools/client/shared/test/unit/test_undoStack.js98
-rw-r--r--devtools/client/shared/test/unit/xpcshell.ini30
171 files changed, 18866 insertions, 0 deletions
diff --git a/devtools/client/shared/test/.eslintrc.js b/devtools/client/shared/test/.eslintrc.js
new file mode 100644
index 000000000..ed80d6d12
--- /dev/null
+++ b/devtools/client/shared/test/.eslintrc.js
@@ -0,0 +1,9 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js",
+ "globals": {
+ "DeveloperToolbar": true
+ }
+};
diff --git a/devtools/client/shared/test/browser.ini b/devtools/client/shared/test/browser.ini
new file mode 100644
index 000000000..ad3f52fbd
--- /dev/null
+++ b/devtools/client/shared/test/browser.ini
@@ -0,0 +1,188 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ browser_layoutHelpers.html
+ browser_layoutHelpers-getBoxQuads.html
+ browser_templater_basic.html
+ browser_toolbar_basic.html
+ browser_toolbar_webconsole_errors_count.html
+ browser_devices.json
+ doc_options-view.xul
+ head.js
+ helper_color_data.js
+ helper_html_tooltip.js
+ helper_inplace_editor.js
+ html-mdn-css-basic-testing.html
+ html-mdn-css-no-summary.html
+ html-mdn-css-no-summary-or-syntax.html
+ html-mdn-css-no-syntax.html
+ html-mdn-css-syntax-old-style.html
+ leakhunt.js
+ test-actor.js
+ test-actor-registry.js
+ !/devtools/client/framework/test/shared-head.js
+
+[browser_css_angle.js]
+[browser_css_color.js]
+[browser_cubic-bezier-01.js]
+[browser_cubic-bezier-02.js]
+[browser_cubic-bezier-03.js]
+[browser_cubic-bezier-04.js]
+[browser_cubic-bezier-05.js]
+[browser_cubic-bezier-06.js]
+[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]
+[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_flame-graph-01.js]
+[browser_flame-graph-02.js]
+[browser_flame-graph-03a.js]
+[browser_flame-graph-03b.js]
+[browser_flame-graph-03c.js]
+[browser_flame-graph-04.js]
+[browser_flame-graph-05.js]
+[browser_flame-graph-utils-01.js]
+[browser_flame-graph-utils-02.js]
+[browser_flame-graph-utils-03.js]
+[browser_flame-graph-utils-04.js]
+[browser_flame-graph-utils-05.js]
+[browser_flame-graph-utils-06.js]
+[browser_flame-graph-utils-hash.js]
+[browser_graphs-01.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-02.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-03.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-04.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-05.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-06.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-07a.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-07b.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-07c.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-07d.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-07e.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-08.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-09a.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-09b.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-09c.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-09d.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-09e.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-09f.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-10a.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-10b.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-10c.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-11a.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-11b.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-12.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-13.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-14.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-15.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-16.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_html_tooltip-01.js]
+[browser_html_tooltip-02.js]
+[browser_html_tooltip-03.js]
+[browser_html_tooltip-04.js]
+[browser_html_tooltip-05.js]
+[browser_html_tooltip_arrow-01.js]
+[browser_html_tooltip_arrow-02.js]
+[browser_html_tooltip_consecutive-show.js]
+[browser_html_tooltip_hover.js]
+[browser_html_tooltip_offset.js]
+[browser_html_tooltip_rtl.js]
+[browser_html_tooltip_variable-height.js]
+[browser_html_tooltip_width-auto.js]
+[browser_html_tooltip_xul-wrapper.js]
+[browser_inplace-editor-01.js]
+[browser_inplace-editor-02.js]
+[browser_inplace-editor_autocomplete_01.js]
+[browser_inplace-editor_autocomplete_02.js]
+[browser_inplace-editor_autocomplete_offset.js]
+[browser_inplace-editor_maxwidth.js]
+[browser_keycodes.js]
+[browser_key_shortcuts.js]
+[browser_layoutHelpers.js]
+skip-if = e10s # Layouthelpers test should not run in a content page.
+[browser_layoutHelpers-getBoxQuads.js]
+skip-if = e10s # Layouthelpers test should not run in a content page.
+[browser_mdn-docs-01.js]
+[browser_mdn-docs-02.js]
+[browser_mdn-docs-03.js]
+[browser_num-l10n.js]
+[browser_options-view-01.js]
+[browser_outputparser.js]
+skip-if = e10s # Test intermittently fails with e10s. Bug 1124162.
+[browser_poller.js]
+[browser_prefs-01.js]
+[browser_prefs-02.js]
+[browser_require_raw.js]
+[browser_spectrum.js]
+[browser_theme.js]
+[browser_tableWidget_basic.js]
+[browser_tableWidget_keyboard_interaction.js]
+[browser_tableWidget_mouse_interaction.js]
+[browser_telemetry_button_eyedropper.js]
+[browser_telemetry_button_paintflashing.js]
+skip-if = e10s # Bug 937167 - e10s paintflashing
+[browser_telemetry_button_responsive.js]
+skip-if = e10s # Bug 1067145 - e10s responsiveview
+[browser_telemetry_button_scratchpad.js]
+[browser_telemetry_sidebar.js]
+[browser_telemetry_toolbox.js]
+[browser_telemetry_toolboxtabs_canvasdebugger.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_shadereditor.js]
+[browser_telemetry_toolboxtabs_storage.js]
+[browser_telemetry_toolboxtabs_styleeditor.js]
+[browser_telemetry_toolboxtabs_webaudioeditor.js]
+[browser_telemetry_toolboxtabs_webconsole.js]
+[browser_templater_basic.js]
+[browser_toolbar_basic.js]
+skip-if = (e10s && debug) # Bug 1253035
+[browser_toolbar_tooltip.js]
+[browser_toolbar_webconsole_errors_count.js]
+skip-if = e10s # The developertoolbar error count isn't correct with e10s
+[browser_treeWidget_basic.js]
+[browser_treeWidget_keyboard_interaction.js]
+[browser_treeWidget_mouse_interaction.js]
+[browser_devices.js]
+[browser_theme_switching.js]
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 000000000..903be44d8
--- /dev/null
+++ b/devtools/client/shared/test/browser_css_angle.js
@@ -0,0 +1,176 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from head.js */
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,browser_css_angle.js";
+var {angleUtils} = require("devtools/client/shared/css-angle");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ let [host] = yield createHost("bottom", TEST_URI);
+
+ info("Starting the test");
+ testAngleUtils();
+ testAngleValidity();
+
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function testAngleUtils() {
+ let data = getTestData();
+
+ for (let {authored, deg, rad, grad, turn} of data) {
+ let 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() {
+ let data = getAngleValidityData();
+
+ for (let {angle, result} of data) {
+ let testAngle = new angleUtils.CssAngle(angle);
+ let validString = testAngle.valid ? " a valid" : "an invalid";
+
+ is(testAngle.valid, result,
+ `Testing that "${angle}" is ${validString} angle`);
+ }
+}
+
+function testToString(angle, deg, rad, grad, turn) {
+ angle.angleUnit = angleUtils.CssAngle.ANGLEUNIT.deg;
+ is(angle.toString(), deg, "toString() with deg type");
+
+ angle.angleUnit = angleUtils.CssAngle.ANGLEUNIT.rad;
+ is(angle.toString(), rad, "toString() with rad type");
+
+ angle.angleUnit = angleUtils.CssAngle.ANGLEUNIT.grad;
+ is(angle.toString(), grad, "toString() with grad type");
+
+ angle.angleUnit = angleUtils.CssAngle.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 000000000..c0846c362
--- /dev/null
+++ b/devtools/client/shared/test/browser_css_color.js
@@ -0,0 +1,137 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,browser_css_color.js";
+var {colorUtils} = require("devtools/shared/css/color");
+/* global getFixtureColorData */
+loadHelperScript("helper_color_data.js");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ let [host,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Creating a test canvas element to test colors");
+ let canvas = createTestCanvas(doc);
+ info("Starting the test");
+ testColorUtils(canvas);
+
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function createTestCanvas(doc) {
+ let canvas = doc.createElement("canvas");
+ canvas.width = canvas.height = 10;
+ doc.body.appendChild(canvas);
+ return canvas;
+}
+
+function testColorUtils(canvas) {
+ let data = getFixtureColorData();
+
+ for (let {authored, name, hex, hsl, rgb} of data) {
+ let 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);
+ }
+
+ testSetAlpha();
+}
+
+function testToString(color, name, hex, hsl, rgb) {
+ color.colorUnit = colorUtils.CssColor.COLORUNIT.name;
+ is(color.toString(), name, "toString() with authored type");
+
+ color.colorUnit = colorUtils.CssColor.COLORUNIT.hex;
+ is(color.toString(), hex, "toString() with hex type");
+
+ color.colorUnit = colorUtils.CssColor.COLORUNIT.hsl;
+ is(color.toString(), hsl, "toString() with hsl type");
+
+ color.colorUnit = colorUtils.CssColor.COLORUNIT.rgb;
+ is(color.toString(), rgb, "toString() with rgb type");
+}
+
+function testColorMatch(name, hex, hsl, rgb, rgba, canvas) {
+ let target;
+ let ctx = canvas.getContext("2d");
+
+ let clearCanvas = function () {
+ canvas.width = 1;
+ };
+ let setColor = function (color) {
+ ctx.fillStyle = color;
+ ctx.fillRect(0, 0, 1, 1);
+ };
+ let setTargetColor = function () {
+ clearCanvas();
+ // All colors have rgba so we can use this to compare against.
+ setColor(rgba);
+ let [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
+ target = {r: r, g: g, b: b, a: a};
+ };
+ let test = function (color, type) {
+ // hsla -> rgba -> hsla produces inaccurate results so we
+ // need some tolerence here.
+ let tolerance = 3;
+ clearCanvas();
+
+ setColor(color);
+ let [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
+
+ let 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: ${target.toSource()}, color: [r: ${r}, g: ${g}, b: ${b}, a: ${a}]`);
+ }
+
+ let 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");
+}
+
+function testSetAlpha() {
+ let values = [
+ ["longhex", "#ff0000", 0.5, "rgba(255, 0, 0, 0.5)"],
+ ["hex", "#f0f", 0.2, "rgba(255, 0, 255, 0.2)"],
+ ["rgba", "rgba(120, 34, 23, 1)", 0.25, "rgba(120, 34, 23, 0.25)"],
+ ["rgb", "rgb(120, 34, 23)", 0.25, "rgba(120, 34, 23, 0.25)"],
+ ["hsl", "hsl(208, 100%, 97%)", 0.75, "rgba(240, 248, 255, 0.75)"],
+ ["hsla", "hsla(208, 100%, 97%, 1)", 0.75, "rgba(240, 248, 255, 0.75)"],
+ ["alphahex", "#f08f", 0.6, "rgba(255, 0, 136, 0.6)"],
+ ["longalphahex", "#00ff80ff", 0.2, "rgba(0, 255, 128, 0.2)"]
+ ];
+ values.forEach(([type, value, alpha, expected]) => {
+ is(colorUtils.setAlpha(value, alpha), expected,
+ "correctly sets alpha value for " + type);
+ });
+
+ try {
+ colorUtils.setAlpha("rgb(24, 25%, 45, 1)", 1);
+ ok(false, "Should fail when passing in an invalid color.");
+ } catch (e) {
+ ok(true, "Fails when setAlpha receives an invalid color.");
+ }
+
+ is(colorUtils.setAlpha("#fff"), "rgba(255, 255, 255, 1)",
+ "sets alpha to 1 if invalid.");
+}
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 000000000..4c32590b2
--- /dev/null
+++ b/devtools/client/shared/test/browser_cubic-bezier-01.js
@@ -0,0 +1,38 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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("devtools/client/shared/widgets/CubicBezierWidget");
+
+const TEST_URI = `data:text/html,<div id="cubic-bezier-container" />`;
+
+add_task(function* () {
+ let [host,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Checking that the graph markup is created in the parent");
+ let container = doc.querySelector("#cubic-bezier-container");
+ let 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");
+ let 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 000000000..f5e21e4d4
--- /dev/null
+++ b/devtools/client/shared/test/browser_cubic-bezier-02.js
@@ -0,0 +1,200 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the CubicBezierWidget events
+
+const {CubicBezierWidget} =
+ require("devtools/client/shared/widgets/CubicBezierWidget");
+const {PREDEFINED} = require("devtools/client/shared/widgets/CubicBezierPresets");
+
+// In this test we have to use a slightly more complete HTML tree, with <body>
+// in order to remove its margin and prevent shifted positions
+const TEST_URI = `data:text/html,
+ <html><body>
+ <div id="cubic-bezier-container"/>
+ </body></html>`;
+
+add_task(function* () {
+ let [host, win, doc] = yield 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;");
+
+ let container = doc.querySelector("#cubic-bezier-container");
+ let w = new CubicBezierWidget(container, PREDEFINED.linear);
+
+ let rect = w.curve.getBoundingClientRect();
+ rect.graphTop = rect.height * w.bezierCanvas.padding[0];
+ rect.graphBottom = rect.height - rect.graphTop;
+ rect.graphHeight = rect.graphBottom - rect.graphTop;
+
+ yield pointsCanBeDragged(w, win, doc, rect);
+ yield curveCanBeClicked(w, win, doc, rect);
+ yield pointsCanBeMovedWithKeyboard(w, win, doc, rect);
+
+ w.destroy();
+ host.destroy();
+});
+
+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 = yield 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 = yield 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");
+}
+
+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 = yield 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 = yield 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");
+}
+
+function* pointsCanBeMovedWithKeyboard(widget, win, doc, offsets) {
+ info("Checking that points respond to keyboard events");
+
+ let singleStep = 3;
+ let 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 = yield 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 = yield 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 = yield 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 = yield 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 = yield 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 = yield 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 = yield 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: target,
+ keyCode: 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 000000000..274ed81ef
--- /dev/null
+++ b/devtools/client/shared/test/browser_cubic-bezier-03.js
@@ -0,0 +1,68 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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("devtools/client/shared/widgets/CubicBezierWidget");
+const {PREDEFINED} = require("devtools/client/shared/widgets/CubicBezierPresets");
+
+const TEST_URI = `data:text/html,<div id="cubic-bezier-container" />`;
+
+add_task(function* () {
+ let [host,, doc] = yield createHost("bottom", TEST_URI);
+
+ let container = doc.querySelector("#cubic-bezier-container");
+ let w = new CubicBezierWidget(container, PREDEFINED.linear);
+
+ yield coordinatesCanBeChangedByProvidingAnArray(w);
+ yield coordinatesCanBeChangedByProvidingAValue(w);
+
+ w.destroy();
+ host.destroy();
+});
+
+function* coordinatesCanBeChangedByProvidingAnArray(widget) {
+ info("Listening for the update event");
+ let onUpdated = widget.once("updated");
+
+ info("Setting new coordinates");
+ widget.coordinates = [0, 1, 1, 0];
+
+ let bezier = yield 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");
+}
+
+function* coordinatesCanBeChangedByProvidingAValue(widget) {
+ info("Listening for the update event");
+ let onUpdated = widget.once("updated");
+
+ info("Setting linear css value");
+ widget.cssCubicBezierValue = "linear";
+ let bezier = yield 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 = yield onUpdated;
+ ok(true, "The updated event was fired as a result of setting cssValue");
+
+ is(bezier.P1[0], .25, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], -.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 000000000..102428035
--- /dev/null
+++ b/devtools/client/shared/test/browser_cubic-bezier-04.js
@@ -0,0 +1,50 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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("devtools/client/shared/widgets/CubicBezierWidget");
+const {PRESETS} = require("devtools/client/shared/widgets/CubicBezierPresets");
+
+const TEST_URI = `data:text/html,<div id="cubic-bezier-container" />`;
+
+add_task(function* () {
+ let [host,, doc] = yield createHost("bottom", TEST_URI);
+
+ let container = doc.querySelector("#cubic-bezier-container");
+ let 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");
+ let 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 => {
+ let 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 000000000..b9cdab294
--- /dev/null
+++ b/devtools/client/shared/test/browser_cubic-bezier-05.js
@@ -0,0 +1,48 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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("devtools/client/shared/widgets/CubicBezierWidget");
+const {PREDEFINED, PRESETS, DEFAULT_PRESET_CATEGORY} =
+ require("devtools/client/shared/widgets/CubicBezierPresets");
+
+const TEST_URI = `data:text/html,<div id="cubic-bezier-container" />`;
+
+add_task(function* () {
+ let [host,, doc] = yield createHost("bottom", TEST_URI);
+
+ let container = doc.querySelector("#cubic-bezier-container");
+ let 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 000000000..150949929
--- /dev/null
+++ b/devtools/client/shared/test/browser_cubic-bezier-06.js
@@ -0,0 +1,79 @@
+
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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("devtools/client/shared/widgets/CubicBezierWidget");
+const {PRESETS} = require("devtools/client/shared/widgets/CubicBezierPresets");
+
+const TEST_URI = `data:text/html,<div id="cubic-bezier-container" />`;
+
+add_task(function* () {
+ let [host, win, doc] = yield createHost("bottom", TEST_URI);
+
+ let container = doc.querySelector("#cubic-bezier-container");
+ let w = new CubicBezierWidget(container,
+ PRESETS["ease-in"]["ease-in-sine"]);
+ w.presets.refreshMenu(PRESETS["ease-in"]["ease-in-sine"]);
+
+ let rect = w.curve.getBoundingClientRect();
+ rect.graphTop = rect.height * w.bezierCanvas.padding[0];
+
+ yield adjustingBezierUpdatesPreset(w, win, doc, rect);
+ yield 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");
+}
+
+function* selectingPresetUpdatesBezier(widget, win, doc, rect) {
+ info("Checking that selecting a preset updates bezier curve");
+
+ info("Listening for the new coordinates event");
+ let onNewCoordinates = widget.presets.once("new-coordinates");
+ let onUpdated = widget.once("updated");
+
+ info("Click a preset");
+ let preset = doc.querySelector("#ease-in-sine");
+ widget.presets._onPresetClick({currentTarget: preset});
+
+ yield onNewCoordinates;
+ ok(true, "The preset widget fired the new-coordinates event");
+
+ let bezier = yield 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_devices.js b/devtools/client/shared/test/browser_devices.js
new file mode 100644
index 000000000..0bf52fe8e
--- /dev/null
+++ b/devtools/client/shared/test/browser_devices.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ getDevices,
+ getDeviceString,
+ addDevice
+} = require("devtools/client/shared/devices");
+
+add_task(function* () {
+ Services.prefs.setCharPref("devtools.devices.url",
+ TEST_URI_ROOT + "browser_devices.json");
+
+ let devices = yield getDevices();
+
+ is(devices.TYPES.length, 1, "Found 1 device type.");
+
+ let type1 = devices.TYPES[0];
+
+ is(devices[type1].length, 2, "Found 2 devices of type #1.");
+
+ let string = getDeviceString(type1);
+ ok(typeof string === "string" && string.length > 0, "Able to localize type #1.");
+
+ let device1 = {
+ name: "SquarePhone",
+ width: 320,
+ height: 320,
+ pixelRatio: 2,
+ userAgent: "Mozilla/5.0 (Mobile; rv:42.0)",
+ touch: true,
+ firefoxOS: true
+ };
+ addDevice(device1, type1);
+ devices = yield getDevices();
+
+ is(devices[type1].length, 3, "Added new device of type #1.");
+ ok(devices[type1].filter(d => d.name === device1.name), "Found the new device.");
+
+ let type2 = "appliances";
+ let device2 = {
+ name: "Mr Freezer",
+ width: 800,
+ height: 600,
+ pixelRatio: 5,
+ userAgent: "Mozilla/5.0 (Appliance; rv:42.0)",
+ touch: true,
+ firefoxOS: true
+ };
+ addDevice(device2, type2);
+ devices = yield getDevices();
+
+ is(devices.TYPES.length, 2, "Added device type #2.");
+ is(devices[type2].length, 1, "Added new device of type #2.");
+});
diff --git a/devtools/client/shared/test/browser_devices.json b/devtools/client/shared/test/browser_devices.json
new file mode 100644
index 000000000..cc7722a7f
--- /dev/null
+++ b/devtools/client/shared/test/browser_devices.json
@@ -0,0 +1,23 @@
+{
+ "TYPES": [ "phones" ],
+ "phones": [
+ {
+ "name": "Small Phone",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ "touch": true,
+ "firefoxOS": true
+ },
+ {
+ "name": "Big Phone",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 3,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ "touch": true,
+ "firefoxOS": true
+ }
+ ]
+}
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 000000000..1a5beb454
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-01.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 parses filter values correctly (setCssValue)
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const DOMUtils =
+ Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+// Verify that the given string consists of a valid CSS URL token.
+// Return true on success, false on error.
+function verifyURL(string) {
+ let lexer = DOMUtils.getCSSLexer(string);
+
+ let token = lexer.nextToken();
+ if (!token || token.tokenType !== "url") {
+ return false;
+ }
+
+ return lexer.nextToken() === null;
+}
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ let widget = new CSSFilterEditorWidget(container, "none", cssIsValid);
+
+ 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");
+
+ let 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");
+
+ let dataurl = "url(data:image/svg+xml;utf8,<svg\\ " +
+ "xmlns=\\\"http://www.w3.org/2000/svg\\\"><filter\\ id=\\\"blur\\\">" +
+ "<feGaussianBlur\\ stdDeviation=\\\"3\\\"/></filter></svg>#blur)";
+ ok(verifyURL(dataurl), "data URL is valid");
+ widget.setCssValue(dataurl);
+ is(widget.getCssValue(), dataurl, "setCssValue should not mangle data urls");
+});
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 000000000..7c0ec270a
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-02.js
@@ -0,0 +1,107 @@
+/* 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("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const STRINGS_URI = "devtools/client/locales/filterwidget.properties";
+const L10N = new LocalizationHelper(STRINGS_URI);
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ 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");
+ let widget = new CSSFilterEditorWidget(container, "none", cssIsValid);
+
+ info("Test rendering of different types");
+
+ for (let {cssValue, expected} of TEST_DATA) {
+ widget.setCssValue(cssValue);
+
+ if (cssValue === "none") {
+ const text = container.querySelector("#filters").textContent;
+ ok(text.indexOf(L10N.getStr("emptyFilterList")) > -1,
+ "Contains |emptyFilterList| string when given value 'none'");
+ ok(text.indexOf(L10N.getStr("addUsingList")) > -1,
+ "Contains |addUsingList| string when given value 'none'");
+ continue;
+ }
+ const filters = container.querySelectorAll(".filter");
+ testRenderedFilters(filters, expected);
+ }
+});
+
+function testRenderedFilters(filters, expected) {
+ for (let [index, filter] of [...filters].entries()) {
+ let [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 000000000..67d36b6b2
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-03.js
@@ -0,0 +1,65 @@
+/* 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("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+const GRAYSCALE_MAX = 100;
+const INVERT_MIN = 0;
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ let widget = new CSSFilterEditorWidget(container, "none", cssIsValid);
+
+ 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");
+});
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 000000000..c1c8f4380
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-04.js
@@ -0,0 +1,87 @@
+/* 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("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+const LIST_ITEM_HEIGHT = 32;
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ const initialValue = "blur(2px) contrast(200%) brightness(200%)";
+ let widget = new CSSFilterEditorWidget(container, initialValue, cssIsValid);
+
+ 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 });
+ ok(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 });
+ ok(last().getBoundingClientRect().bottom <= boundaries.bottom,
+ "Last filter should not move outside filter list");
+
+ widget._mouseUp();
+});
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 000000000..a18429542
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-05.js
@@ -0,0 +1,148 @@
+/* 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("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+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 = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ let widget = new CSSFilterEditorWidget(
+ container, "grayscale(0%) url(test.svg)", cssIsValid
+ );
+
+ 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");
+});
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 000000000..1e1a6c914
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-06.js
@@ -0,0 +1,71 @@
+/* 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("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const STRINGS_URI = "devtools/client/locales/filterwidget.properties";
+const L10N = new LocalizationHelper(STRINGS_URI);
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ let widget = new CSSFilterEditorWidget(container, "none", cssIsValid);
+
+ 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 (let [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) {
+ let 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");
+ }
+ }
+});
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 000000000..af2975d5c
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-07.js
@@ -0,0 +1,27 @@
+/* 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("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ let widget = new CSSFilterEditorWidget(
+ container, "blur(2px) contrast(200%)", cssIsValid
+ );
+
+ info("Test removing filters with remove button");
+ widget.el.querySelector(".filter button").click();
+
+ is(widget.getCssValue(), "contrast(200%)",
+ "Should remove the clicked filter");
+});
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 000000000..c30dbf299
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-08.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 inputs increase/decrease value using
+// arrow keys, applying multiplier using alt/shift on number-type filters
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const FAST_VALUE_MULTIPLIER = 10;
+const SLOW_VALUE_MULTIPLIER = 0.1;
+const DEFAULT_VALUE_MULTIPLIER = 1;
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ const initialValue = "blur(2px)";
+ let widget = new CSSFilterEditorWidget(container, initialValue, cssIsValid);
+
+ 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");
+
+ 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 () {}
+ });
+}
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 000000000..1a358425e
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-09.js
@@ -0,0 +1,125 @@
+/* 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("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const FAST_VALUE_MULTIPLIER = 10;
+const SLOW_VALUE_MULTIPLIER = 0.1;
+const DEFAULT_VALUE_MULTIPLIER = 1;
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ const initialValue = "drop-shadow(rgb(0, 0, 0) 1px 1px 0px)";
+ let widget = new CSSFilterEditorWidget(container, initialValue, cssIsValid);
+ 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");
+
+ 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 () {}
+ });
+}
+
+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 000000000..b73c53a83
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-10.js
@@ -0,0 +1,87 @@
+/* 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("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const DEFAULT_VALUE_MULTIPLIER = 1;
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ const initialValue = "drop-shadow(rgb(0, 0, 0) 10px 1px 0px)";
+ let widget = new CSSFilterEditorWidget(container, initialValue, cssIsValid);
+ 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");
+
+ 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 () {}
+ });
+}
+
+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 000000000..859f5f63e
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-presets-01.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests saving presets
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ let widget = new CSSFilterEditorWidget(container, "none", cssIsValid);
+ // First render
+ yield widget.once("render");
+
+ const VALUE = "blur(2px) contrast(150%)";
+ const NAME = "Test";
+
+ yield showFilterPopupPresetsAndCreatePreset(widget, NAME, VALUE);
+
+ let 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 = yield widget.getPresets();
+ let 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);
+
+ yield savePreset(widget);
+
+ is(widget.el.querySelectorAll(".preset").length, 1,
+ "Should override the preset with the same name - render");
+
+ list = yield 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");
+
+ yield widget.setPresets([]);
+
+ info("Test saving a preset without name");
+ input.value = "";
+
+ yield savePreset(widget, "preset-save-error");
+
+ list = yield 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");
+
+ yield savePreset(widget, "preset-save-error");
+
+ list = yield 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") {
+ let 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 000000000..5e700ea94
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-presets-02.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests loading presets
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ let widget = new CSSFilterEditorWidget(container, "none", cssIsValid);
+ // First render
+ yield widget.once("render");
+
+ const VALUE = "blur(2px) contrast(150%)";
+ const NAME = "Test";
+
+ yield showFilterPopupPresetsAndCreatePreset(widget, NAME, VALUE);
+
+ let onRender = widget.once("render");
+ // reset value
+ widget.setCssValue("saturate(100%) brightness(150%)");
+ yield onRender;
+
+ let preset = widget.el.querySelector(".preset");
+
+ onRender = widget.once("render");
+ widget._presetClick({
+ target: preset
+ });
+
+ yield 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 000000000..a61bf35db
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-presets-03.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests deleting presets
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ let widget = new CSSFilterEditorWidget(container, "none", cssIsValid);
+ // First render
+ yield widget.once("render");
+
+ const NAME = "Test";
+ const VALUE = "blur(2px) contrast(150%)";
+
+ yield showFilterPopupPresetsAndCreatePreset(widget, NAME, VALUE);
+
+ let removeButton = widget.el.querySelector(".preset .remove-button");
+ let onRender = widget.once("render");
+ widget._presetClick({
+ target: removeButton
+ });
+
+ yield onRender;
+ is(widget.el.querySelector(".preset"), null,
+ "Should re-render after removing preset");
+
+ let list = yield widget.getPresets();
+ is(list.length, 0,
+ "Should remove presets from asyncStorage");
+});
diff --git a/devtools/client/shared/test/browser_flame-graph-01.js b/devtools/client/shared/test/browser_flame-graph-01.js
new file mode 100644
index 000000000..a32fb9fd3
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-01.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that flame graph widget works properly.
+
+const {FlameGraph} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new FlameGraph(doc.body);
+
+ let readyEventEmitted;
+ graph.once("ready", () => {
+ readyEventEmitted = true;
+ });
+
+ yield graph.ready();
+ ok(readyEventEmitted, "The 'ready' event should have been emitted");
+
+ testGraph(host, graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(host, graph) {
+ ok(graph._container.classList.contains("flame-graph-widget-container"),
+ "The correct graph container was created.");
+ ok(graph._canvas.classList.contains("flame-graph-widget-canvas"),
+ "The correct graph container was created.");
+
+ let bounds = host.frame.getBoundingClientRect();
+
+ is(graph.width, bounds.width * window.devicePixelRatio,
+ "The graph has the correct width.");
+ is(graph.height, bounds.height * window.devicePixelRatio,
+ "The graph has the correct height.");
+
+ ok(graph._selection.start === null,
+ "The graph's selection start value is initially null.");
+ ok(graph._selection.end === null,
+ "The graph's selection end value is initially null.");
+
+ ok(graph._selectionDragger.origin === null,
+ "The graph's dragger origin value is initially null.");
+ ok(graph._selectionDragger.anchor.start === null,
+ "The graph's dragger anchor start value is initially null.");
+ ok(graph._selectionDragger.anchor.end === null,
+ "The graph's dragger anchor end value is initially null.");
+}
diff --git a/devtools/client/shared/test/browser_flame-graph-02.js b/devtools/client/shared/test/browser_flame-graph-02.js
new file mode 100644
index 000000000..e15c3efe0
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-02.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that flame graph widgets may have a fixed width or height.
+
+const {FlameGraph} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new FlameGraph(doc.body);
+ graph.fixedWidth = 200;
+ graph.fixedHeight = 100;
+
+ yield graph.ready();
+ testGraph(host, graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(host, graph) {
+ let bounds = host.frame.getBoundingClientRect();
+
+ isnot(graph.width, bounds.width * window.devicePixelRatio,
+ "The graph should not span all the parent node's width.");
+ isnot(graph.height, bounds.height * window.devicePixelRatio,
+ "The graph should not span all the parent node's height.");
+
+ is(graph.width, graph.fixedWidth * window.devicePixelRatio,
+ "The graph has the correct width.");
+ is(graph.height, graph.fixedHeight * window.devicePixelRatio,
+ "The graph has the correct height.");
+}
diff --git a/devtools/client/shared/test/browser_flame-graph-03a.js b/devtools/client/shared/test/browser_flame-graph-03a.js
new file mode 100644
index 000000000..10fcf9457
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-03a.js
@@ -0,0 +1,138 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that selections in the flame graph widget work properly.
+
+const TEST_DATA = [
+ {
+ color: "#f00",
+ blocks: [
+ { x: 0, y: 0, width: 50, height: 20, text: "FOO" },
+ { x: 50, y: 0, width: 100, height: 20, text: "BAR" }
+ ]
+ },
+ {
+ color: "#00f",
+ blocks: [
+ { x: 0, y: 30, width: 30, height: 20, text: "BAZ" }
+ ]
+ }
+];
+const TEST_BOUNDS = { startTime: 0, endTime: 150 };
+const TEST_WIDTH = 200;
+const TEST_HEIGHT = 100;
+
+const {FlameGraph} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new FlameGraph(doc.body, 1);
+ graph.fixedWidth = TEST_WIDTH;
+ graph.fixedHeight = TEST_HEIGHT;
+ graph.horizontalPanThreshold = 0;
+ graph.verticalPanThreshold = 0;
+
+ yield graph.ready();
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.setData({ data: TEST_DATA, bounds: TEST_BOUNDS });
+
+ is(graph.getViewRange().startTime, 0,
+ "The selection start boundary is correct (1).");
+ is(graph.getViewRange().endTime, 150,
+ "The selection end boundary is correct (1).");
+
+ scroll(graph, 200, HORIZONTAL_AXIS, 10);
+ is(graph.getViewRange().startTime | 0, 75,
+ "The selection start boundary is correct (2).");
+ is(graph.getViewRange().endTime | 0, 150,
+ "The selection end boundary is correct (2).");
+
+ scroll(graph, -200, HORIZONTAL_AXIS, 10);
+ is(graph.getViewRange().startTime | 0, 37,
+ "The selection start boundary is correct (3).");
+ is(graph.getViewRange().endTime | 0, 112,
+ "The selection end boundary is correct (3).");
+
+ scroll(graph, 200, VERTICAL_AXIS, TEST_WIDTH / 2);
+ is(graph.getViewRange().startTime | 0, 34,
+ "The selection start boundary is correct (4).");
+ is(graph.getViewRange().endTime | 0, 115,
+ "The selection end boundary is correct (4).");
+
+ scroll(graph, -200, VERTICAL_AXIS, TEST_WIDTH / 2);
+ is(graph.getViewRange().startTime | 0, 37,
+ "The selection start boundary is correct (5).");
+ is(graph.getViewRange().endTime | 0, 112,
+ "The selection end boundary is correct (5).");
+
+ dragStart(graph, TEST_WIDTH / 2);
+ is(graph.getViewRange().startTime | 0, 37,
+ "The selection start boundary is correct (6).");
+ is(graph.getViewRange().endTime | 0, 112,
+ "The selection end boundary is correct (6).");
+
+ hover(graph, TEST_WIDTH / 2 - 10);
+ is(graph.getViewRange().startTime | 0, 41,
+ "The selection start boundary is correct (7).");
+ is(graph.getViewRange().endTime | 0, 116,
+ "The selection end boundary is correct (7).");
+
+ dragStop(graph, 10);
+ is(graph.getViewRange().startTime | 0, 71,
+ "The selection start boundary is correct (8).");
+ is(graph.getViewRange().endTime | 0, 145,
+ "The selection end boundary is correct (8).");
+}
+
+// EventUtils just doesn't work!
+
+function hover(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+}
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
+
+var HORIZONTAL_AXIS = 1;
+var VERTICAL_AXIS = 2;
+
+function scroll(graph, wheel, axis, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseWheel({ testX: x, testY: y, axis, detail: wheel,
+ HORIZONTAL_AXIS,
+ VERTICAL_AXIS
+ });
+}
diff --git a/devtools/client/shared/test/browser_flame-graph-03b.js b/devtools/client/shared/test/browser_flame-graph-03b.js
new file mode 100644
index 000000000..936eb831e
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-03b.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that selections in the flame graph widget work properly on HiDPI.
+
+const TEST_DATA = [
+ {
+ color: "#f00",
+ blocks: [
+ { x: 0, y: 0, width: 50, height: 20, text: "FOO" },
+ { x: 50, y: 0, width: 100, height: 20, text: "BAR" }
+ ]
+ },
+ {
+ color: "#00f",
+ blocks: [
+ { x: 0, y: 30, width: 30, height: 20, text: "BAZ" }
+ ]
+ }
+];
+const TEST_BOUNDS = { startTime: 0, endTime: 150 };
+const TEST_WIDTH = 200;
+const TEST_HEIGHT = 100;
+const TEST_DPI_DENSITIY = 2;
+
+var {FlameGraph} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new FlameGraph(doc.body, TEST_DPI_DENSITIY);
+ graph.fixedWidth = TEST_WIDTH;
+ graph.fixedHeight = TEST_HEIGHT;
+
+ yield graph.ready();
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.setData({ data: TEST_DATA, bounds: TEST_BOUNDS });
+
+ is(graph.getViewRange().startTime, 0,
+ "The selection start boundary is correct on HiDPI (1).");
+ is(graph.getViewRange().endTime, 150,
+ "The selection end boundary is correct on HiDPI (1).");
+
+ is(graph.getOuterBounds().startTime, 0,
+ "The bounds start boundary is correct on HiDPI (1).");
+ is(graph.getOuterBounds().endTime, 150,
+ "The bounds end boundary is correct on HiDPI (1).");
+
+ scroll(graph, 10000, HORIZONTAL_AXIS, 1);
+
+ is(Math.round(graph.getViewRange().startTime), 150,
+ "The selection start boundary is correct on HiDPI (2).");
+ is(Math.round(graph.getViewRange().endTime), 150,
+ "The selection end boundary is correct on HiDPI (2).");
+
+ is(graph.getOuterBounds().startTime, 0,
+ "The bounds start boundary is correct on HiDPI (2).");
+ is(graph.getOuterBounds().endTime, 150,
+ "The bounds end boundary is correct on HiDPI (2).");
+}
+
+// EventUtils just doesn't work!
+
+var HORIZONTAL_AXIS = 1;
+var VERTICAL_AXIS = 2;
+
+function scroll(graph, wheel, axis, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseWheel({ testX: x, testY: y, axis, detail: wheel,
+ HORIZONTAL_AXIS,
+ VERTICAL_AXIS
+ });
+}
diff --git a/devtools/client/shared/test/browser_flame-graph-03c.js b/devtools/client/shared/test/browser_flame-graph-03c.js
new file mode 100644
index 000000000..3a6bf80ae
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-03c.js
@@ -0,0 +1,155 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that vertical panning in the flame graph widget works properly.
+
+const TEST_DATA = [
+ {
+ color: "#f00",
+ blocks: [
+ { x: 0, y: 0, width: 50, height: 20, text: "FOO" },
+ { x: 50, y: 0, width: 100, height: 20, text: "BAR" }
+ ]
+ },
+ {
+ color: "#00f",
+ blocks: [
+ { x: 0, y: 30, width: 30, height: 20, text: "BAZ" }
+ ]
+ }
+];
+const TEST_BOUNDS = { startTime: 0, endTime: 150 };
+const TEST_WIDTH = 200;
+const TEST_HEIGHT = 100;
+const TEST_DPI_DENSITIY = 2;
+
+const {FlameGraph} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new FlameGraph(doc.body, TEST_DPI_DENSITIY);
+ graph.fixedWidth = TEST_WIDTH;
+ graph.fixedHeight = TEST_HEIGHT;
+
+ yield graph.ready();
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.setData({ data: TEST_DATA, bounds: TEST_BOUNDS });
+
+ // Drag up vertically only.
+
+ dragStart(graph, TEST_WIDTH / 2, TEST_HEIGHT / 2);
+ is(graph.getViewRange().startTime | 0, 0,
+ "The selection start boundary is correct (1).");
+ is(graph.getViewRange().endTime | 0, 150,
+ "The selection end boundary is correct (1).");
+ is(graph.getViewRange().verticalOffset | 0, 0,
+ "The vertical offset is correct (1).");
+
+ hover(graph, TEST_WIDTH / 2, TEST_HEIGHT / 2 - 50);
+ is(graph.getViewRange().startTime | 0, 0,
+ "The selection start boundary is correct (2).");
+ is(graph.getViewRange().endTime | 0, 150,
+ "The selection end boundary is correct (2).");
+ is(graph.getViewRange().verticalOffset | 0, 17,
+ "The vertical offset is correct (2).");
+
+ dragStop(graph, TEST_WIDTH / 2, TEST_HEIGHT / 2 - 100);
+ is(graph.getViewRange().startTime | 0, 0,
+ "The selection start boundary is correct (3).");
+ is(graph.getViewRange().endTime | 0, 150,
+ "The selection end boundary is correct (3).");
+ is(graph.getViewRange().verticalOffset | 0, 42,
+ "The vertical offset is correct (3).");
+
+ // Drag down strongly vertically and slightly horizontally.
+
+ dragStart(graph, TEST_WIDTH / 2, TEST_HEIGHT / 2);
+ is(graph.getViewRange().startTime | 0, 0,
+ "The selection start boundary is correct (4).");
+ is(graph.getViewRange().endTime | 0, 150,
+ "The selection end boundary is correct (4).");
+ is(graph.getViewRange().verticalOffset | 0, 42,
+ "The vertical offset is correct (4).");
+
+ hover(graph, TEST_WIDTH / 2, TEST_HEIGHT / 2 + 50);
+ is(graph.getViewRange().startTime | 0, 0,
+ "The selection start boundary is correct (5).");
+ is(graph.getViewRange().endTime | 0, 150,
+ "The selection end boundary is correct (5).");
+ is(graph.getViewRange().verticalOffset | 0, 25,
+ "The vertical offset is correct (5).");
+
+ dragStop(graph, TEST_WIDTH / 2 + 100, TEST_HEIGHT / 2 + 500);
+ is(graph.getViewRange().startTime | 0, 0,
+ "The selection start boundary is correct (6).");
+ is(graph.getViewRange().endTime | 0, 150,
+ "The selection end boundary is correct (6).");
+ is(graph.getViewRange().verticalOffset | 0, 0,
+ "The vertical offset is correct (6).");
+
+ // Drag up slightly vertically and strongly horizontally.
+
+ dragStart(graph, TEST_WIDTH / 2, TEST_HEIGHT / 2);
+ is(graph.getViewRange().startTime | 0, 0,
+ "The selection start boundary is correct (7).");
+ is(graph.getViewRange().endTime | 0, 150,
+ "The selection end boundary is correct (7).");
+ is(graph.getViewRange().verticalOffset | 0, 0,
+ "The vertical offset is correct (7).");
+
+ hover(graph, TEST_WIDTH / 2 + 50, TEST_HEIGHT / 2);
+ is(graph.getViewRange().startTime | 0, 0,
+ "The selection start boundary is correct (8).");
+ is(graph.getViewRange().endTime | 0, 116,
+ "The selection end boundary is correct (8).");
+ is(graph.getViewRange().verticalOffset | 0, 0,
+ "The vertical offset is correct (8).");
+
+ dragStop(graph, TEST_WIDTH / 2 + 500, TEST_HEIGHT / 2 + 100);
+ is(graph.getViewRange().startTime | 0, 0,
+ "The selection start boundary is correct (9).");
+ is(graph.getViewRange().endTime | 0, 0,
+ "The selection end boundary is correct (9).");
+ is(graph.getViewRange().verticalOffset | 0, 0,
+ "The vertical offset is correct (9).");
+}
+
+// EventUtils just doesn't work!
+
+function hover(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+}
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
diff --git a/devtools/client/shared/test/browser_flame-graph-04.js b/devtools/client/shared/test/browser_flame-graph-04.js
new file mode 100644
index 000000000..5bcc112ec
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-04.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that text metrics in the flame graph widget work properly.
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const {ELLIPSIS} = require("devtools/shared/l10n");
+const {FlameGraph} = require("devtools/client/shared/widgets/FlameGraph");
+const {FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE} = require("devtools/client/shared/widgets/FlameGraph");
+const {FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new FlameGraph(doc.body, 1);
+ yield graph.ready();
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ is(graph._averageCharWidth, getAverageCharWidth(),
+ "The average char width was calculated correctly.");
+ is(graph._overflowCharWidth, getCharWidth(ELLIPSIS),
+ "The ellipsis char width was calculated correctly.");
+
+ let text = "This text is maybe overflowing";
+ let text1000px = graph._getFittedText(text, 1000);
+ let text50px = graph._getFittedText(text, 50);
+ let text10px = graph._getFittedText(text, 10);
+ let text1px = graph._getFittedText(text, 1);
+
+ is(graph._getTextWidthApprox(text), getAverageCharWidth() * text.length,
+ "The approximate width was calculated correctly.");
+
+ info("Text at 1000px width: " + text1000px);
+ info("Text at 50px width : " + text50px);
+ info("Text at 10px width : " + text10px);
+ info("Text at 1px width : " + text1px);
+
+ is(text1000px, text,
+ "The fitted text for 1000px width is correct.");
+
+ isnot(text50px, text,
+ "The fitted text for 50px width is correct (1).");
+
+ ok(text50px.includes(ELLIPSIS),
+ "The fitted text for 50px width is correct (2).");
+
+ is(graph._getFittedText(text, FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE + 1), ELLIPSIS,
+ "The fitted text for text font size width is correct.");
+
+ is(graph._getFittedText(text, 1), "",
+ "The fitted text for 1px width is correct.");
+}
+
+function getAverageCharWidth() {
+ let letterWidthsSum = 0;
+
+ let start = " ".charCodeAt(0);
+ let end = "z".charCodeAt(0) + 1;
+
+ for (let i = start; i < end; i++) {
+ let char = String.fromCharCode(i);
+ letterWidthsSum += getCharWidth(char);
+ }
+
+ return letterWidthsSum / (end - start);
+}
+
+function getCharWidth(char) {
+ let canvas = document.createElementNS(HTML_NS, "canvas");
+ let ctx = canvas.getContext("2d");
+
+ let fontSize = FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE;
+ let fontFamily = FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY;
+ ctx.font = fontSize + "px " + fontFamily;
+
+ return ctx.measureText(char).width;
+}
diff --git a/devtools/client/shared/test/browser_flame-graph-05.js b/devtools/client/shared/test/browser_flame-graph-05.js
new file mode 100644
index 000000000..1b30489dc
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-05.js
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that flame graph widget has proper keyboard support.
+
+const TEST_DATA = [
+ {
+ color: "#f00",
+ blocks: [
+ { x: 0, y: 0, width: 50, height: 20, text: "FOO" },
+ { x: 50, y: 0, width: 100, height: 20, text: "BAR" }
+ ]
+ },
+ {
+ color: "#00f",
+ blocks: [
+ { x: 0, y: 30, width: 30, height: 20, text: "BAZ" }
+ ]
+ }
+];
+const TEST_BOUNDS = { startTime: 0, endTime: 150 };
+const TEST_DPI_DENSITIY = 2;
+
+const KEY_CODE_UP = 38;
+const KEY_CODE_LEFT = 37;
+const KEY_CODE_RIGHT = 39;
+
+var {FlameGraph} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new FlameGraph(doc.body, TEST_DPI_DENSITIY);
+ yield graph.ready();
+
+ yield testGraph(host, graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(host, graph) {
+ graph.setData({ data: TEST_DATA, bounds: TEST_BOUNDS });
+
+ is(graph._selection.start, 0,
+ "The graph's selection start value is initially correct.");
+ is(graph._selection.end, TEST_BOUNDS.endTime * TEST_DPI_DENSITIY,
+ "The graph's selection end value is initially correct.");
+
+ yield pressKeyForTime(graph, KEY_CODE_LEFT, 1000);
+
+ is(graph._selection.start, 0,
+ "The graph's selection start value is correct after pressing LEFT.");
+ ok(graph._selection.end < TEST_BOUNDS.endTime * TEST_DPI_DENSITIY,
+ "The graph's selection end value is correct after pressing LEFT.");
+
+ graph._selection.start = 0;
+ graph._selection.end = TEST_BOUNDS.endTime * TEST_DPI_DENSITIY;
+ info("Graph selection was reset (1).");
+
+ yield pressKeyForTime(graph, KEY_CODE_RIGHT, 1000);
+
+ ok(graph._selection.start > 0,
+ "The graph's selection start value is correct after pressing RIGHT.");
+ is(graph._selection.end, TEST_BOUNDS.endTime * TEST_DPI_DENSITIY,
+ "The graph's selection end value is correct after pressing RIGHT.");
+
+ graph._selection.start = 0;
+ graph._selection.end = TEST_BOUNDS.endTime * TEST_DPI_DENSITIY;
+ info("Graph selection was reset (2).");
+
+ yield pressKeyForTime(graph, KEY_CODE_UP, 1000);
+
+ ok(graph._selection.start > 0,
+ "The graph's selection start value is correct after pressing UP.");
+ ok(graph._selection.end < TEST_BOUNDS.endTime * TEST_DPI_DENSITIY,
+ "The graph's selection end value is correct after pressing UP.");
+
+ let distanceLeft = graph._selection.start;
+ let distanceRight = TEST_BOUNDS.endTime * TEST_DPI_DENSITIY - graph._selection.end;
+
+ ok(Math.abs(distanceRight - distanceLeft) < 0.1,
+ "The graph zoomed correctly towards the center point.");
+}
+
+function pressKeyForTime(graph, keyCode, ms) {
+ graph._onKeyDown({
+ keyCode,
+ preventDefault: () => {},
+ stopPropagation: () => {},
+ });
+
+ return new Promise(resolve => {
+ setTimeout(() => {
+ graph._onKeyUp({
+ keyCode,
+ preventDefault: () => {},
+ stopPropagation: () => {},
+ });
+ resolve();
+ }, ms);
+ });
+}
diff --git a/devtools/client/shared/test/browser_flame-graph-utils-01.js b/devtools/client/shared/test/browser_flame-graph-utils-01.js
new file mode 100644
index 000000000..6871e234c
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-utils-01.js
@@ -0,0 +1,256 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that text metrics and data conversion from profiler samples
+// widget work properly in the flame graph.
+
+const {FlameGraphUtils} = require("devtools/client/shared/widgets/FlameGraph");
+const {PALLETTE_SIZE} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let out = FlameGraphUtils.createFlameGraphDataFromThread(TEST_DATA);
+
+ ok(out, "Some data was outputted properly");
+ is(out.length, PALLETTE_SIZE, "The outputted length is correct.");
+
+ info("Got flame graph data:\n" + out.toSource() + "\n");
+
+ for (let i = 0; i < out.length; i++) {
+ let found = out[i];
+ let expected = EXPECTED_OUTPUT[i];
+
+ is(found.blocks.length, expected.blocks.length,
+ "The correct number of blocks were found in this bucket.");
+
+ for (let j = 0; j < found.blocks.length; j++) {
+ is(found.blocks[j].x, expected.blocks[j].x,
+ "The expected block X position is correct for this frame.");
+ is(found.blocks[j].y, expected.blocks[j].y,
+ "The expected block Y position is correct for this frame.");
+ is(found.blocks[j].width, expected.blocks[j].width,
+ "The expected block width is correct for this frame.");
+ is(found.blocks[j].height, expected.blocks[j].height,
+ "The expected block height is correct for this frame.");
+ is(found.blocks[j].text, expected.blocks[j].text,
+ "The expected block text is correct for this frame.");
+ }
+ }
+}
+
+var TEST_DATA = synthesizeProfileForTest([{
+ frames: [{
+ location: "M"
+ }, {
+ location: "N",
+ }, {
+ location: "P"
+ }],
+ time: 50,
+}, {
+ frames: [{
+ location: "A"
+ }, {
+ location: "B",
+ }, {
+ location: "C"
+ }],
+ time: 100,
+}, {
+ frames: [{
+ location: "A"
+ }, {
+ location: "B",
+ }, {
+ location: "D"
+ }],
+ time: 210,
+}, {
+ frames: [{
+ location: "A"
+ }, {
+ location: "E",
+ }, {
+ location: "F"
+ }],
+ time: 330,
+}, {
+ frames: [{
+ location: "A"
+ }, {
+ location: "B",
+ }, {
+ location: "C"
+ }],
+ time: 460,
+}, {
+ frames: [{
+ location: "X"
+ }, {
+ location: "Y",
+ }, {
+ location: "Z"
+ }],
+ time: 500
+}]);
+
+var EXPECTED_OUTPUT = [{
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 50,
+ frameKey: "A",
+ x: 50,
+ y: 0,
+ width: 410,
+ height: 15,
+ text: "A"
+ }]
+}, {
+ blocks: [{
+ startTime: 50,
+ frameKey: "B",
+ x: 50,
+ y: 15,
+ width: 160,
+ height: 15,
+ text: "B"
+ }, {
+ startTime: 330,
+ frameKey: "B",
+ x: 330,
+ y: 15,
+ width: 130,
+ height: 15,
+ text: "B"
+ }]
+}, {
+ blocks: [{
+ startTime: 50,
+ frameKey: "C",
+ x: 50,
+ y: 30,
+ width: 50,
+ height: 15,
+ text: "C"
+ }, {
+ startTime: 330,
+ frameKey: "C",
+ x: 330,
+ y: 30,
+ width: 130,
+ height: 15,
+ text: "C"
+ }]
+}, {
+ blocks: [{
+ startTime: 100,
+ frameKey: "D",
+ x: 100,
+ y: 30,
+ width: 110,
+ height: 15,
+ text: "D"
+ }, {
+ startTime: 460,
+ frameKey: "X",
+ x: 460,
+ y: 0,
+ width: 40,
+ height: 15,
+ text: "X"
+ }]
+}, {
+ blocks: [{
+ startTime: 210,
+ frameKey: "E",
+ x: 210,
+ y: 15,
+ width: 120,
+ height: 15,
+ text: "E"
+ }, {
+ startTime: 460,
+ frameKey: "Y",
+ x: 460,
+ y: 15,
+ width: 40,
+ height: 15,
+ text: "Y"
+ }]
+}, {
+ blocks: [{
+ startTime: 210,
+ frameKey: "F",
+ x: 210,
+ y: 30,
+ width: 120,
+ height: 15,
+ text: "F"
+ }, {
+ startTime: 460,
+ frameKey: "Z",
+ x: 460,
+ y: 30,
+ width: 40,
+ height: 15,
+ text: "Z"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "M",
+ x: 0,
+ y: 0,
+ width: 50,
+ height: 15,
+ text: "M"
+ }]
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "N",
+ x: 0,
+ y: 15,
+ width: 50,
+ height: 15,
+ text: "N"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "P",
+ x: 0,
+ y: 30,
+ width: 50,
+ height: 15,
+ text: "P"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}];
diff --git a/devtools/client/shared/test/browser_flame-graph-utils-02.js b/devtools/client/shared/test/browser_flame-graph-utils-02.js
new file mode 100644
index 000000000..15e9d1933
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-utils-02.js
@@ -0,0 +1,130 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests consecutive duplicate frames are removed from the flame graph data.
+
+const {FlameGraphUtils} = require("devtools/client/shared/widgets/FlameGraph");
+const {PALLETTE_SIZE} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let out = FlameGraphUtils.createFlameGraphDataFromThread(TEST_DATA, {
+ flattenRecursion: true
+ });
+
+ ok(out, "Some data was outputted properly");
+ is(out.length, PALLETTE_SIZE, "The outputted length is correct.");
+
+ info("Got flame graph data:\n" + out.toSource() + "\n");
+
+ for (let i = 0; i < out.length; i++) {
+ let found = out[i];
+ let expected = EXPECTED_OUTPUT[i];
+
+ is(found.blocks.length, expected.blocks.length,
+ "The correct number of blocks were found in this bucket.");
+
+ for (let j = 0; j < found.blocks.length; j++) {
+ is(found.blocks[j].x, expected.blocks[j].x,
+ "The expected block X position is correct for this frame.");
+ is(found.blocks[j].y, expected.blocks[j].y,
+ "The expected block Y position is correct for this frame.");
+ is(found.blocks[j].width, expected.blocks[j].width,
+ "The expected block width is correct for this frame.");
+ is(found.blocks[j].height, expected.blocks[j].height,
+ "The expected block height is correct for this frame.");
+ is(found.blocks[j].text, expected.blocks[j].text,
+ "The expected block text is correct for this frame.");
+ }
+ }
+}
+
+var TEST_DATA = synthesizeProfileForTest([{
+ frames: [{
+ location: "A"
+ }, {
+ location: "A"
+ }, {
+ location: "A"
+ }, {
+ location: "B",
+ }, {
+ location: "B",
+ }, {
+ location: "C"
+ }],
+ time: 50,
+}]);
+
+var EXPECTED_OUTPUT = [{
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "A",
+ x: 0,
+ y: 0,
+ width: 50,
+ height: 15,
+ text: "A"
+ }]
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "B",
+ x: 0,
+ y: 15,
+ width: 50,
+ height: 15,
+ text: "B"
+ }]
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "C",
+ x: 0,
+ y: 30,
+ width: 50,
+ height: 15,
+ text: "C"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}];
diff --git a/devtools/client/shared/test/browser_flame-graph-utils-03.js b/devtools/client/shared/test/browser_flame-graph-utils-03.js
new file mode 100644
index 000000000..0f28c0afc
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-utils-03.js
@@ -0,0 +1,136 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests if platform frames are removed from the flame graph data.
+
+const {FlameGraphUtils} = require("devtools/client/shared/widgets/FlameGraph");
+const {PALLETTE_SIZE} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let out = FlameGraphUtils.createFlameGraphDataFromThread(TEST_DATA, {
+ contentOnly: true
+ });
+
+ ok(out, "Some data was outputted properly");
+ is(out.length, PALLETTE_SIZE, "The outputted length is correct.");
+
+ info("Got flame graph data:\n" + out.toSource() + "\n");
+
+ for (let i = 0; i < out.length; i++) {
+ let found = out[i];
+ let expected = EXPECTED_OUTPUT[i];
+
+ is(found.blocks.length, expected.blocks.length,
+ "The correct number of blocks were found in this bucket.");
+
+ for (let j = 0; j < found.blocks.length; j++) {
+ is(found.blocks[j].x, expected.blocks[j].x,
+ "The expected block X position is correct for this frame.");
+ is(found.blocks[j].y, expected.blocks[j].y,
+ "The expected block Y position is correct for this frame.");
+ is(found.blocks[j].width, expected.blocks[j].width,
+ "The expected block width is correct for this frame.");
+ is(found.blocks[j].height, expected.blocks[j].height,
+ "The expected block height is correct for this frame.");
+ is(found.blocks[j].text, expected.blocks[j].text,
+ "The expected block text is correct for this frame.");
+ }
+ }
+}
+
+var TEST_DATA = synthesizeProfileForTest([{
+ frames: [{
+ location: "http://A"
+ }, {
+ location: "https://B"
+ }, {
+ location: "file://C",
+ }, {
+ location: "chrome://D"
+ }, {
+ location: "resource://E"
+ }],
+ time: 50,
+}]);
+
+var EXPECTED_OUTPUT = [{
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "http://A",
+ x: 0,
+ y: 0,
+ width: 50,
+ height: 15,
+ text: "http://A"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "Gecko",
+ x: 0,
+ y: 45,
+ width: 50,
+ height: 15,
+ text: "Gecko"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "https://B",
+ x: 0,
+ y: 15,
+ width: 50,
+ height: 15,
+ text: "https://B"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "file://C",
+ x: 0,
+ y: 30,
+ width: 50,
+ height: 15,
+ text: "file://C"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}];
diff --git a/devtools/client/shared/test/browser_flame-graph-utils-04.js b/devtools/client/shared/test/browser_flame-graph-utils-04.js
new file mode 100644
index 000000000..1bf6c1f59
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-utils-04.js
@@ -0,0 +1,188 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests if (idle) nodes are added when necessary in the flame graph data.
+
+const {FlameGraphUtils} = require("devtools/client/shared/widgets/FlameGraph");
+const {PALLETTE_SIZE} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let out = FlameGraphUtils.createFlameGraphDataFromThread(TEST_DATA, {
+ flattenRecursion: true,
+ contentOnly: true,
+ showIdleBlocks: "\m/"
+ });
+
+ ok(out, "Some data was outputted properly");
+ is(out.length, PALLETTE_SIZE, "The outputted length is correct.");
+
+ info("Got flame graph data:\n" + out.toSource() + "\n");
+
+ for (let i = 0; i < out.length; i++) {
+ let found = out[i];
+ let expected = EXPECTED_OUTPUT[i];
+
+ is(found.blocks.length, expected.blocks.length,
+ "The correct number of blocks were found in this bucket.");
+
+ for (let j = 0; j < found.blocks.length; j++) {
+ is(found.blocks[j].x, expected.blocks[j].x,
+ "The expected block X position is correct for this frame.");
+ is(found.blocks[j].y, expected.blocks[j].y,
+ "The expected block Y position is correct for this frame.");
+ is(found.blocks[j].width, expected.blocks[j].width,
+ "The expected block width is correct for this frame.");
+ is(found.blocks[j].height, expected.blocks[j].height,
+ "The expected block height is correct for this frame.");
+ is(found.blocks[j].text, expected.blocks[j].text,
+ "The expected block text is correct for this frame.");
+ }
+ }
+}
+
+var TEST_DATA = synthesizeProfileForTest([{
+ frames: [{
+ location: "http://A"
+ }, {
+ location: "http://A"
+ }, {
+ location: "http://A"
+ }, {
+ location: "https://B"
+ }, {
+ location: "https://B"
+ }, {
+ location: "file://C",
+ }, {
+ location: "chrome://D"
+ }, {
+ location: "resource://E"
+ }],
+ time: 50
+}, {
+ frames: [],
+ time: 100
+}, {
+ frames: [{
+ location: "http://A"
+ }, {
+ location: "https://B"
+ }, {
+ location: "file://C",
+ }],
+ time: 150
+}]);
+
+var EXPECTED_OUTPUT = [{
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "http://A",
+ x: 0,
+ y: 0,
+ width: 50,
+ height: 15,
+ text: "http://A"
+ }, {
+ startTime: 100,
+ frameKey: "http://A",
+ x: 100,
+ y: 0,
+ width: 50,
+ height: 15,
+ text: "http://A"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "Gecko",
+ x: 0,
+ y: 45,
+ width: 50,
+ height: 15,
+ text: "Gecko"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "https://B",
+ x: 0,
+ y: 15,
+ width: 50,
+ height: 15,
+ text: "https://B"
+ }, {
+ startTime: 100,
+ frameKey: "https://B",
+ x: 100,
+ y: 15,
+ width: 50,
+ height: 15,
+ text: "https://B"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "file://C",
+ x: 0,
+ y: 30,
+ width: 50,
+ height: 15,
+ text: "file://C"
+ }, {
+ startTime: 100,
+ frameKey: "file://C",
+ x: 100,
+ y: 30,
+ width: 50,
+ height: 15,
+ text: "file://C"
+ }]
+}, {
+ blocks: [{
+ startTime: 50,
+ frameKey: "m/",
+ x: 50,
+ y: 0,
+ width: 50,
+ height: 15,
+ text: "m/"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}];
diff --git a/devtools/client/shared/test/browser_flame-graph-utils-05.js b/devtools/client/shared/test/browser_flame-graph-utils-05.js
new file mode 100644
index 000000000..5abdd708a
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-utils-05.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that flame graph data is cached, and that the cache may be cleared.
+
+const {FlameGraphUtils} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let out1 = FlameGraphUtils.createFlameGraphDataFromThread(TEST_DATA);
+ let out2 = FlameGraphUtils.createFlameGraphDataFromThread(TEST_DATA);
+ is(out1, out2, "The outputted data is identical.");
+
+ let out3 = FlameGraphUtils.createFlameGraphDataFromThread(
+ TEST_DATA, { flattenRecursion: true }
+ );
+ is(out2, out3, "The outputted data is still identical.");
+
+ FlameGraphUtils.removeFromCache(TEST_DATA);
+ let out4 = FlameGraphUtils.createFlameGraphDataFromThread(
+ TEST_DATA, { flattenRecursion: true }
+ );
+ isnot(out3, out4, "The outputted data is not identical anymore.");
+}
+
+var TEST_DATA = synthesizeProfileForTest([{
+ frames: [{
+ location: "A"
+ }, {
+ location: "A"
+ }, {
+ location: "A"
+ }, {
+ location: "B",
+ }, {
+ location: "B",
+ }, {
+ location: "C"
+ }],
+ time: 50,
+}]);
diff --git a/devtools/client/shared/test/browser_flame-graph-utils-06.js b/devtools/client/shared/test/browser_flame-graph-utils-06.js
new file mode 100644
index 000000000..886a1035b
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-utils-06.js
@@ -0,0 +1,117 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the text displayed is the function name, file name and line number
+// if applicable and demangling.
+
+const {FlameGraphUtils} = require("devtools/client/shared/widgets/FlameGraph");
+const {PALLETTE_SIZE} = require("devtools/client/shared/widgets/FlameGraph");
+const MANGLED_FN = "__Z3FooIiEvv";
+const UNMANGLED_FN = "void Foo<int>()";
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let out = FlameGraphUtils.createFlameGraphDataFromThread(TEST_DATA, {
+ flattenRecursion: true
+ });
+
+ ok(out, "Some data was outputted properly");
+ is(out.length, PALLETTE_SIZE, "The outputted length is correct.");
+
+ info("Got flame graph data:\n" + out.toSource() + "\n");
+
+ for (let i = 0; i < out.length; i++) {
+ let found = out[i];
+ let expected = EXPECTED_OUTPUT[i];
+
+ is(found.blocks.length, expected.blocks.length,
+ "The correct number of blocks were found in this bucket.");
+
+ for (let j = 0; j < found.blocks.length; j++) {
+ is(found.blocks[j].x, expected.blocks[j].x,
+ "The expected block X position is correct for this frame.");
+ is(found.blocks[j].y, expected.blocks[j].y,
+ "The expected block Y position is correct for this frame.");
+ is(found.blocks[j].width, expected.blocks[j].width,
+ "The expected block width is correct for this frame.");
+ is(found.blocks[j].height, expected.blocks[j].height,
+ "The expected block height is correct for this frame.");
+ is(found.blocks[j].text, expected.blocks[j].text,
+ "The expected block text is correct for this frame.");
+ }
+ }
+}
+
+var TEST_DATA = synthesizeProfileForTest([{
+ frames: [{
+ location: "A (http://path/to/file.js:10:5)"
+ }, {
+ location: `${MANGLED_FN} (http://path/to/file.js:100:5)`
+ }],
+ time: 50,
+}]);
+
+var EXPECTED_OUTPUT = [{
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "A (http://path/to/file.js:10:5)",
+ x: 0,
+ y: 0,
+ width: 50,
+ height: 15,
+ text: "A (file.js:10)"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: `${MANGLED_FN} (http://path/to/file.js:100:5)`,
+ x: 0,
+ y: 15,
+ width: 50,
+ height: 15,
+ text: `${UNMANGLED_FN} (file.js:100)`
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}];
diff --git a/devtools/client/shared/test/browser_flame-graph-utils-hash.js b/devtools/client/shared/test/browser_flame-graph-utils-hash.js
new file mode 100644
index 000000000..6b441bbf5
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-utils-hash.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests if (idle) nodes are added when necessary in the flame graph data.
+
+const {FlameGraphUtils} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ let hash1 = FlameGraphUtils._getStringHash("abc");
+ let hash2 = FlameGraphUtils._getStringHash("acb");
+ let hash3 = FlameGraphUtils._getStringHash(Array.from(Array(100000)).join("a"));
+ let hash4 = FlameGraphUtils._getStringHash(Array.from(Array(100000)).join("b"));
+
+ isnot(hash1, hash2, "The hashes should not be equal (1).");
+ isnot(hash2, hash3, "The hashes should not be equal (2).");
+ isnot(hash3, hash4, "The hashes should not be equal (3).");
+
+ ok(Number.isInteger(hash1), "The hashes should be integers, not Infinity or NaN (1).");
+ ok(Number.isInteger(hash2), "The hashes should be integers, not Infinity or NaN (2).");
+ ok(Number.isInteger(hash3), "The hashes should be integers, not Infinity or NaN (3).");
+ ok(Number.isInteger(hash4), "The hashes should be integers, not Infinity or NaN (4).");
+});
diff --git a/devtools/client/shared/test/browser_graphs-01.js b/devtools/client/shared/test/browser_graphs-01.js
new file mode 100644
index 000000000..c4f5640d9
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-01.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that graph widgets works properly.
+
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+ finish();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new LineGraphWidget(doc.body, "fps");
+
+ let readyEventEmitted;
+ graph.once("ready", () => {
+ readyEventEmitted = true;
+ });
+
+ yield graph.ready();
+ ok(readyEventEmitted, "The 'ready' event should have been emitted");
+
+ testGraph(host, graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(host, graph) {
+ ok(graph._container.classList.contains("line-graph-widget-container"),
+ "The correct graph container was created.");
+ ok(graph._canvas.classList.contains("line-graph-widget-canvas"),
+ "The correct graph container was created.");
+
+ let bounds = host.frame.getBoundingClientRect();
+
+ is(graph.width, bounds.width * window.devicePixelRatio,
+ "The graph has the correct width.");
+ is(graph.height, bounds.height * window.devicePixelRatio,
+ "The graph has the correct height.");
+
+ ok(graph._cursor.x === null,
+ "The graph's cursor X coordinate is initially null.");
+ ok(graph._cursor.y === null,
+ "The graph's cursor Y coordinate is initially null.");
+
+ ok(graph._selection.start === null,
+ "The graph's selection start value is initially null.");
+ ok(graph._selection.end === null,
+ "The graph's selection end value is initially null.");
+
+ ok(graph._selectionDragger.origin === null,
+ "The graph's dragger origin value is initially null.");
+ ok(graph._selectionDragger.anchor.start === null,
+ "The graph's dragger anchor start value is initially null.");
+ ok(graph._selectionDragger.anchor.end === null,
+ "The graph's dragger anchor end value is initially null.");
+
+ ok(graph._selectionResizer.margin === null,
+ "The graph's resizer margin value is initially null.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-02.js b/devtools/client/shared/test/browser_graphs-02.js
new file mode 100644
index 000000000..def728722
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-02.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that graph widgets can properly add data, regions and highlights.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const TEST_REGIONS = [{ start: 320, end: 460 }, { start: 780, end: 860 }];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ testDataAndRegions(graph);
+ testHighlights(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testDataAndRegions(graph) {
+ let thrown1;
+ try {
+ graph.setRegions(TEST_REGIONS);
+ } catch (e) {
+ thrown1 = true;
+ }
+ ok(thrown1, "Setting regions before setting data shouldn't work.");
+
+ graph.setData(TEST_DATA);
+ graph.setRegions(TEST_REGIONS);
+
+ let thrown2;
+ try {
+ graph.setRegions(TEST_REGIONS);
+ } catch (e) {
+ thrown2 = true;
+ }
+ ok(thrown2, "Setting regions twice shouldn't work.");
+
+ ok(graph.hasData(), "The graph should now have the data source set.");
+ ok(graph.hasRegions(), "The graph should now have the regions set.");
+
+ is(graph.dataScaleX,
+ // last & first tick in TEST_DATA
+ graph.width / 4180,
+ "The data scale on the X axis is correct.");
+
+ is(graph.dataScaleY,
+ // max value in TEST_DATA * GRAPH_DAMPEN_VALUES
+ graph.height / 60 * 0.85,
+ "The data scale on the Y axis is correct.");
+
+ for (let i = 0; i < TEST_REGIONS.length; i++) {
+ let original = TEST_REGIONS[i];
+ let normalized = graph._regions[i];
+
+ is(original.start * graph.dataScaleX, normalized.start,
+ "The region's start value was properly normalized.");
+ is(original.end * graph.dataScaleX, normalized.end,
+ "The region's end value was properly normalized.");
+ }
+}
+
+function testHighlights(graph) {
+ graph.setMask(TEST_REGIONS);
+ ok(graph.hasMask(),
+ "The graph should now have the highlights set.");
+
+ graph.setMask([]);
+ ok(graph.hasMask(),
+ "The graph shouldn't have anything highlighted.");
+
+ graph.setMask(null);
+ ok(!graph.hasMask(),
+ "The graph should have everything highlighted.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-03.js b/devtools/client/shared/test/browser_graphs-03.js
new file mode 100644
index 000000000..b44d4620a
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-03.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that graph widgets can handle clients getting/setting the
+// selection or cursor.
+
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ yield testSelection(graph);
+ yield testCursor(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testSelection(graph) {
+ ok(graph.getSelection().start === null,
+ "The graph's selection should initially have a null start value.");
+ ok(graph.getSelection().end === null,
+ "The graph's selection should initially have a null end value.");
+ ok(!graph.hasSelection(),
+ "There shouldn't initially be any selection.");
+
+ let selected = graph.once("selecting");
+ graph.setSelection({ start: 100, end: 200 });
+
+ yield selected;
+ ok(true, "A 'selecting' event has been fired.");
+
+ ok(graph.hasSelection(),
+ "There should now be a selection.");
+ is(graph.getSelection().start, 100,
+ "The graph's selection now has an updated start value.");
+ is(graph.getSelection().end, 200,
+ "The graph's selection now has an updated end value.");
+
+ let thrown;
+ try {
+ graph.setSelection({ start: null, end: null });
+ } catch (e) {
+ thrown = true;
+ }
+ ok(thrown, "Setting a null selection shouldn't work.");
+
+ ok(graph.hasSelection(),
+ "There should still be a selection.");
+
+ let deselected = graph.once("deselecting");
+ graph.dropSelection();
+
+ yield deselected;
+ ok(true, "A 'deselecting' event has been fired.");
+
+ ok(!graph.hasSelection(),
+ "There shouldn't be any selection anymore.");
+ ok(graph.getSelection().start === null,
+ "The graph's selection now has a null start value.");
+ ok(graph.getSelection().end === null,
+ "The graph's selection now has a null end value.");
+}
+
+function* testCursor(graph) {
+ ok(graph.getCursor().x === null,
+ "The graph's cursor should initially have a null X value.");
+ ok(graph.getCursor().y === null,
+ "The graph's cursor should initially have a null Y value.");
+ ok(!graph.hasCursor(),
+ "There shouldn't initially be any cursor.");
+
+ graph.setCursor({ x: 100, y: 50 });
+
+ ok(graph.hasCursor(),
+ "There should now be a cursor.");
+ is(graph.getCursor().x, 100,
+ "The graph's cursor now has an updated start value.");
+ is(graph.getCursor().y, 50,
+ "The graph's cursor now has an updated end value.");
+
+ let thrown;
+ try {
+ graph.setCursor({ x: null, y: null });
+ } catch (e) {
+ thrown = true;
+ }
+ ok(thrown, "Setting a null cursor shouldn't work.");
+
+ ok(graph.hasCursor(),
+ "There should still be a cursor.");
+
+ graph.dropCursor();
+
+ ok(!graph.hasCursor(),
+ "There shouldn't be any cursor anymore.");
+ ok(graph.getCursor().x === null,
+ "The graph's cursor now has a null start value.");
+ ok(graph.getCursor().y === null,
+ "The graph's cursor now has a null end value.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-04.js b/devtools/client/shared/test/browser_graphs-04.js
new file mode 100644
index 000000000..452b27c4a
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-04.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that graph widgets can correctly compare selections and cursors.
+
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ ok(!graph.hasSelection(),
+ "There shouldn't initially be any selection.");
+ is(graph.getSelectionWidth(), 0,
+ "The selection width should be 0 when there's no selection.");
+
+ graph.setSelection({ start: 100, end: 200 });
+
+ ok(graph.hasSelection(),
+ "There should now be a selection.");
+ is(graph.getSelectionWidth(), 100,
+ "The selection width should now be 100.");
+
+ ok(graph.isSelectionDifferent({ start: 100, end: 201 }),
+ "The selection was correctly reported to be different (1).");
+ ok(graph.isSelectionDifferent({ start: 101, end: 200 }),
+ "The selection was correctly reported to be different (2).");
+ ok(graph.isSelectionDifferent({ start: null, end: null }),
+ "The selection was correctly reported to be different (3).");
+ ok(graph.isSelectionDifferent(null),
+ "The selection was correctly reported to be different (4).");
+
+ ok(!graph.isSelectionDifferent({ start: 100, end: 200 }),
+ "The selection was incorrectly reported to be different (1).");
+ ok(!graph.isSelectionDifferent(graph.getSelection()),
+ "The selection was incorrectly reported to be different (2).");
+
+ graph.setCursor({ x: 100, y: 50 });
+
+ ok(graph.isCursorDifferent({ x: 100, y: 51 }),
+ "The cursor was correctly reported to be different (1).");
+ ok(graph.isCursorDifferent({ x: 101, y: 50 }),
+ "The cursor was correctly reported to be different (2).");
+ ok(graph.isCursorDifferent({ x: null, y: null }),
+ "The cursor was correctly reported to be different (3).");
+ ok(graph.isCursorDifferent(null),
+ "The cursor was correctly reported to be different (4).");
+
+ ok(!graph.isCursorDifferent({ x: 100, y: 50 }),
+ "The cursor was incorrectly reported to be different (1).");
+ ok(!graph.isCursorDifferent(graph.getCursor()),
+ "The cursor was incorrectly reported to be different (2).");
+}
diff --git a/devtools/client/shared/test/browser_graphs-05.js b/devtools/client/shared/test/browser_graphs-05.js
new file mode 100644
index 000000000..bd3da9128
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-05.js
@@ -0,0 +1,154 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that graph widgets can correctly determine which regions are hovered.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const TEST_REGIONS = [{ start: 320, end: 460 }, { start: 780, end: 860 }];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ ok(!graph.getHoveredRegion(),
+ "There should be no hovered region yet because there's no regions.");
+
+ ok(!graph._isHoveringStartBoundary(),
+ "The graph start boundary should not be hovered.");
+ ok(!graph._isHoveringEndBoundary(),
+ "The graph end boundary should not be hovered.");
+ ok(!graph._isHoveringSelectionContents(),
+ "The graph contents should not be hovered.");
+ ok(!graph._isHoveringSelectionContentsOrBoundaries(),
+ "The graph contents or boundaries should not be hovered.");
+
+ graph.setData(TEST_DATA);
+ graph.setRegions(TEST_REGIONS);
+
+ ok(!graph.getHoveredRegion(),
+ "There should be no hovered region yet because there's no cursor.");
+
+ graph.setCursor({ x: TEST_REGIONS[0].start * graph.dataScaleX - 1, y: 0 });
+ ok(!graph.getHoveredRegion(),
+ "There shouldn't be any hovered region yet.");
+
+ graph.setCursor({ x: TEST_REGIONS[0].start * graph.dataScaleX + 1, y: 0 });
+ ok(graph.getHoveredRegion(),
+ "There should be a hovered region now.");
+ is(graph.getHoveredRegion().start, 320 * graph.dataScaleX,
+ "The reported hovered region is correct (1).");
+ is(graph.getHoveredRegion().end, 460 * graph.dataScaleX,
+ "The reported hovered region is correct (2).");
+
+ graph.setSelection({ start: 100, end: 200 });
+
+ info("Setting cursor over the left boundary.");
+ graph.setCursor({ x: 100, y: 0 });
+
+ ok(graph._isHoveringStartBoundary(),
+ "The graph start boundary should be hovered.");
+ ok(!graph._isHoveringEndBoundary(),
+ "The graph end boundary should not be hovered.");
+ ok(!graph._isHoveringSelectionContents(),
+ "The graph contents should not be hovered.");
+ ok(graph._isHoveringSelectionContentsOrBoundaries(),
+ "The graph contents or boundaries should be hovered.");
+
+ info("Setting cursor near the left boundary.");
+ graph.setCursor({ x: 105, y: 0 });
+
+ ok(graph._isHoveringStartBoundary(),
+ "The graph start boundary should be hovered.");
+ ok(!graph._isHoveringEndBoundary(),
+ "The graph end boundary should not be hovered.");
+ ok(graph._isHoveringSelectionContents(),
+ "The graph contents should be hovered.");
+ ok(graph._isHoveringSelectionContentsOrBoundaries(),
+ "The graph contents or boundaries should be hovered.");
+
+ info("Setting cursor over the selection.");
+ graph.setCursor({ x: 150, y: 0 });
+
+ ok(!graph._isHoveringStartBoundary(),
+ "The graph start boundary should not be hovered.");
+ ok(!graph._isHoveringEndBoundary(),
+ "The graph end boundary should not be hovered.");
+ ok(graph._isHoveringSelectionContents(),
+ "The graph contents should be hovered.");
+ ok(graph._isHoveringSelectionContentsOrBoundaries(),
+ "The graph contents or boundaries should be hovered.");
+
+ info("Setting cursor near the right boundary.");
+ graph.setCursor({ x: 195, y: 0 });
+
+ ok(!graph._isHoveringStartBoundary(),
+ "The graph start boundary should not be hovered.");
+ ok(graph._isHoveringEndBoundary(),
+ "The graph end boundary should be hovered.");
+ ok(graph._isHoveringSelectionContents(),
+ "The graph contents should be hovered.");
+ ok(graph._isHoveringSelectionContentsOrBoundaries(),
+ "The graph contents or boundaries should be hovered.");
+
+ info("Setting cursor over the right boundary.");
+ graph.setCursor({ x: 200, y: 0 });
+
+ ok(!graph._isHoveringStartBoundary(),
+ "The graph start boundary should not be hovered.");
+ ok(graph._isHoveringEndBoundary(),
+ "The graph end boundary should be hovered.");
+ ok(!graph._isHoveringSelectionContents(),
+ "The graph contents should not be hovered.");
+ ok(graph._isHoveringSelectionContentsOrBoundaries(),
+ "The graph contents or boundaries should be hovered.");
+
+ info("Setting away from the selection.");
+ graph.setCursor({ x: 300, y: 0 });
+
+ ok(!graph._isHoveringStartBoundary(),
+ "The graph start boundary should not be hovered.");
+ ok(!graph._isHoveringEndBoundary(),
+ "The graph end boundary should not be hovered.");
+ ok(!graph._isHoveringSelectionContents(),
+ "The graph contents should not be hovered.");
+ ok(!graph._isHoveringSelectionContentsOrBoundaries(),
+ "The graph contents or boundaries should not be hovered.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-06.js b/devtools/client/shared/test/browser_graphs-06.js
new file mode 100644
index 000000000..596fe7702
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-06.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests if clicking on regions adds a selection spanning that region.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const TEST_REGIONS = [{ start: 320, end: 460 }, { start: 780, end: 860 }];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.setData(TEST_DATA);
+ graph.setRegions(TEST_REGIONS);
+
+ click(graph, (graph._regions[0].start + graph._regions[0].end) / 2);
+ is(graph.getSelection().start, graph._regions[0].start,
+ "The first region is now selected (1).");
+ is(graph.getSelection().end, graph._regions[0].end,
+ "The first region is now selected (2).");
+
+ let min = map(graph.getSelection().start, 0, graph.width, 112, 4180);
+ let max = map(graph.getSelection().end, 0, graph.width, 112, 4180);
+ is(graph.getMappedSelection().min, min,
+ "The mapped selection's min value is correct (1).");
+ is(graph.getMappedSelection().max, max,
+ "The mapped selection's max value is correct (2).");
+
+ click(graph, (graph._regions[1].start + graph._regions[1].end) / 2);
+ is(graph.getSelection().start, graph._regions[1].start,
+ "The second region is now selected (1).");
+ is(graph.getSelection().end, graph._regions[1].end,
+ "The second region is now selected (2).");
+
+ min = map(graph.getSelection().start, 0, graph.width, 112, 4180);
+ max = map(graph.getSelection().end, 0, graph.width, 112, 4180);
+ is(graph.getMappedSelection().min, min,
+ "The mapped selection's min value is correct (3).");
+ is(graph.getMappedSelection().max, max,
+ "The mapped selection's max value is correct (4).");
+
+ graph.setSelection({ start: graph.width, end: 0 });
+ min = map(0, 0, graph.width, 112, 4180);
+ max = map(graph.width, 0, graph.width, 112, 4180);
+ is(graph.getMappedSelection().min, min,
+ "The mapped selection's min value is correct (5).");
+ is(graph.getMappedSelection().max, max,
+ "The mapped selection's max value is correct (6).");
+
+ graph.setSelection({ start: graph.width + 100, end: -100 });
+ min = map(0, 0, graph.width, 112, 4180);
+ max = map(graph.width, 0, graph.width, 112, 4180);
+ is(graph.getMappedSelection().min, min,
+ "The mapped selection's min value is correct (7).");
+ is(graph.getMappedSelection().max, max,
+ "The mapped selection's max value is correct (8).");
+}
+
+/**
+ * Maps a value from one range to another.
+ */
+function map(value, istart, istop, ostart, ostop) {
+ return ostart + (ostop - ostart) * ((value - istart) / (istop - istart));
+}
+
+// EventUtils just doesn't work!
+
+function click(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
diff --git a/devtools/client/shared/test/browser_graphs-07a.js b/devtools/client/shared/test/browser_graphs-07a.js
new file mode 100644
index 000000000..44e166cc5
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-07a.js
@@ -0,0 +1,232 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests if selecting, resizing, moving selections and zooming in/out works.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+ testGraph(graph, normalDragStop);
+ yield graph.destroy();
+
+ let graph2 = new LineGraphWidget(doc.body, "fps");
+ yield graph2.once("ready");
+ testGraph(graph2, buggyDragStop);
+ yield graph2.destroy();
+
+ host.destroy();
+}
+
+function testGraph(graph, dragStop) {
+ graph.setData(TEST_DATA);
+
+ info("Making a selection.");
+
+ dragStart(graph, 300);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should start (1).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (1).");
+ is(graph.getSelection().end, 300,
+ "The current selection end value is correct (1).");
+
+ hover(graph, 400);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should still be in progress (2).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (2).");
+ is(graph.getSelection().end, 400,
+ "The current selection end value is correct (2).");
+
+ dragStop(graph, 500);
+ ok(!graph.hasSelectionInProgress(),
+ "The selection should have stopped (3).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (3).");
+ is(graph.getSelection().end, 500,
+ "The current selection end value is correct (3).");
+
+ info("Making a new selection.");
+
+ dragStart(graph, 200);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should start (4).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (4).");
+ is(graph.getSelection().end, 200,
+ "The current selection end value is correct (4).");
+
+ hover(graph, 300);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should still be in progress (5).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (5).");
+ is(graph.getSelection().end, 300,
+ "The current selection end value is correct (5).");
+
+ dragStop(graph, 400);
+ ok(!graph.hasSelectionInProgress(),
+ "The selection should have stopped (6).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (6).");
+ is(graph.getSelection().end, 400,
+ "The current selection end value is correct (6).");
+
+ info("Resizing by dragging the end handlebar.");
+
+ dragStart(graph, 400);
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (7).");
+ is(graph.getSelection().end, 400,
+ "The current selection end value is correct (7).");
+
+ dragStop(graph, 600);
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (8).");
+ is(graph.getSelection().end, 600,
+ "The current selection end value is correct (8).");
+
+ info("Resizing by dragging the start handlebar.");
+
+ dragStart(graph, 200);
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (9).");
+ is(graph.getSelection().end, 600,
+ "The current selection end value is correct (9).");
+
+ dragStop(graph, 100);
+ is(graph.getSelection().start, 100,
+ "The current selection start value is correct (10).");
+ is(graph.getSelection().end, 600,
+ "The current selection end value is correct (10).");
+
+ info("Moving by dragging the selection.");
+
+ dragStart(graph, 300);
+ hover(graph, 400);
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (11).");
+ is(graph.getSelection().end, 700,
+ "The current selection end value is correct (11).");
+
+ dragStop(graph, 500);
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (12).");
+ is(graph.getSelection().end, 800,
+ "The current selection end value is correct (12).");
+
+ info("Zooming in by scrolling inside the selection.");
+
+ scroll(graph, -1000, 600);
+ is(graph.getSelection().start, 525,
+ "The current selection start value is correct (13).");
+ is(graph.getSelection().end, 650,
+ "The current selection end value is correct (13).");
+
+ info("Zooming out by scrolling inside the selection.");
+
+ scroll(graph, 1000, 600);
+ is(graph.getSelection().start, 468.75,
+ "The current selection start value is correct (14).");
+ is(graph.getSelection().end, 687.5,
+ "The current selection end value is correct (14).");
+
+ info("Sliding left by scrolling outside the selection.");
+
+ scroll(graph, 100, 900);
+ is(graph.getSelection().start, 458.75,
+ "The current selection start value is correct (15).");
+ is(graph.getSelection().end, 677.5,
+ "The current selection end value is correct (15).");
+
+ info("Sliding right by scrolling outside the selection.");
+
+ scroll(graph, -100, 900);
+ is(graph.getSelection().start, 468.75,
+ "The current selection start value is correct (16).");
+ is(graph.getSelection().end, 687.5,
+ "The current selection end value is correct (16).");
+
+ info("Zooming out a lot.");
+
+ scroll(graph, Number.MAX_SAFE_INTEGER, 500);
+ is(graph.getSelection().start, 1,
+ "The current selection start value is correct (17).");
+ is(graph.getSelection().end, graph.width - 1,
+ "The current selection end value is correct (17).");
+}
+
+// EventUtils just doesn't work!
+
+function hover(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+}
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+}
+
+function normalDragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
+
+function buggyDragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+
+ graph._onMouseMove({ testX: x, testY: y });
+
+ // Only fire a mousemove with no buttons instead of a mouseup.
+ // This happens when the mouseup happens outside of the window.
+ // Send different coordinates to make sure the selection is preserved,
+ // see Bugs 1066504 and 1144779.
+ graph._onMouseMove({ testX: x + 1, testY: y + 1, buttons: 0 });
+}
+
+function scroll(graph, wheel, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseWheel({ testX: x, testY: y, detail: wheel });
+}
diff --git a/devtools/client/shared/test/browser_graphs-07b.js b/devtools/client/shared/test/browser_graphs-07b.js
new file mode 100644
index 000000000..3b4bc5740
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-07b.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests if selections can't be added via clicking, while not allowed.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+var LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.setData(TEST_DATA);
+ graph.selectionEnabled = false;
+
+ info("Attempting to make a selection.");
+
+ dragStart(graph, 300);
+ is(graph.hasSelection() || graph.hasSelectionInProgress(), false,
+ "The graph shouldn't have a selection (1).");
+
+ hover(graph, 400);
+ is(graph.hasSelection() || graph.hasSelectionInProgress(), false,
+ "The graph shouldn't have a selection (2).");
+
+ dragStop(graph, 500);
+ is(graph.hasSelection() || graph.hasSelectionInProgress(), false,
+ "The graph shouldn't have a selection (3).");
+}
+
+// EventUtils just doesn't work!
+
+function hover(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+}
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
diff --git a/devtools/client/shared/test/browser_graphs-07c.js b/devtools/client/shared/test/browser_graphs-07c.js
new file mode 100644
index 000000000..1791ced12
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-07c.js
@@ -0,0 +1,139 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests if movement via event dispatching using screenX / screenY
+// works. All of the other tests directly use the graph's mouse event
+// callbacks with textX / testY for convenience.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+ testGraph(graph);
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.setData(TEST_DATA);
+
+ info("Making a selection.");
+
+ dragStart(graph, 300);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should start (1).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (1).");
+ is(graph.getSelection().end, 300,
+ "The current selection end value is correct (1).");
+
+ hover(graph, 400);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should still be in progress (2).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (2).");
+ is(graph.getSelection().end, 400,
+ "The current selection end value is correct (2).");
+
+ dragStop(graph, 500);
+ ok(!graph.hasSelectionInProgress(),
+ "The selection should have stopped (3).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (3).");
+ is(graph.getSelection().end, 500,
+ "The current selection end value is correct (3).");
+
+ info("Making a new selection.");
+
+ dragStart(graph, 200);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should start (4).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (4).");
+ is(graph.getSelection().end, 200,
+ "The current selection end value is correct (4).");
+
+ hover(graph, 300);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should still be in progress (5).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (5).");
+ is(graph.getSelection().end, 300,
+ "The current selection end value is correct (5).");
+
+ dragStop(graph, 400);
+ ok(!graph.hasSelectionInProgress(),
+ "The selection should have stopped (6).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (6).");
+ is(graph.getSelection().end, 400,
+ "The current selection end value is correct (6).");
+}
+
+// EventUtils just doesn't work!
+
+function dispatchEvent(graph, x, y, type) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ let quad = graph._canvas.getBoxQuads({
+ relativeTo: window.document
+ })[0];
+
+ let screenX = window.screenX + quad.p1.x + x;
+ let screenY = window.screenY + quad.p1.y + y;
+
+ graph._canvas.dispatchEvent(new MouseEvent(type, {
+ bubbles: true,
+ cancelable: true,
+ buttons: 1,
+ view: window,
+ screenX: screenX,
+ screenY: screenY,
+ }));
+}
+
+function hover(graph, x, y = 1) {
+ dispatchEvent(graph, x, y, "mousemove");
+}
+
+function dragStart(graph, x, y = 1) {
+ dispatchEvent(graph, x, y, "mousemove");
+ dispatchEvent(graph, x, y, "mousedown");
+}
+
+function dragStop(graph, x, y = 1) {
+ dispatchEvent(graph, x, y, "mousemove");
+ dispatchEvent(graph, x, y, "mouseup");
+}
diff --git a/devtools/client/shared/test/browser_graphs-07d.js b/devtools/client/shared/test/browser_graphs-07d.js
new file mode 100644
index 000000000..8a64f7d75
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-07d.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that selections are drawn onto the canvas.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const TEST_REGIONS = [{ start: 320, end: 460 }, { start: 780, end: 860 }];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.setData(TEST_DATA);
+ graph.setRegions(TEST_REGIONS);
+
+ // Measure the color of the first pixel before any selection is made.
+ graph._onAnimationFrame();
+ let pixelNoSelection = graph._ctx.getImageData(1, 1, 1, 1).data;
+
+ graph.setSelection({ start: 0, end: 10 });
+ graph._onAnimationFrame();
+ let pixelNormalSelection = graph._ctx.getImageData(1, 1, 1, 1).data;
+
+ Assert.notDeepEqual(pixelNormalSelection, pixelNoSelection,
+ "The first pixel is part of the drawn selection.");
+
+ graph.setSelection({ start: graph.width + 100, end: -100 });
+ graph._onAnimationFrame();
+ let pixelFullSelection = graph._ctx.getImageData(1, 1, 1, 1).data;
+
+ Assert.deepEqual(pixelFullSelection, pixelNormalSelection,
+ "The first pixel is still part of the drawn selection.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-07e.js b/devtools/client/shared/test/browser_graphs-07e.js
new file mode 100644
index 000000000..814284b9d
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-07e.js
@@ -0,0 +1,127 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that selections are drawn onto the canvas.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+let CURRENT_ZOOM = 1;
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+ graph.setData(TEST_DATA);
+
+ info("Testing with normal zoom.");
+ testGraph(graph);
+
+ info("Testing while zoomed out.");
+ setZoom(host.frame, .5);
+ testGraph(graph);
+
+ info("Testing while zoomed in.");
+ setZoom(host.frame, 2);
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.dropSelection();
+
+ info("Making a selection.");
+
+ dragStart(graph, 100);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should start (1).");
+ is(graph.getSelection().start, 100,
+ "The current selection start value is correct (1).");
+ is(graph.getSelection().end, 100,
+ "The current selection end value is correct (1).");
+
+ hover(graph, 200);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should still be in progress (2).");
+ is(graph.getSelection().start, 100,
+ "The current selection start value is correct (2).");
+ is(graph.getSelection().end, 200,
+ "The current selection end value is correct (2).");
+
+ dragStop(graph, 300);
+ ok(!graph.hasSelectionInProgress(),
+ "The selection should have stopped (3).");
+ is(graph.getSelection().start, 100,
+ "The current selection start value is correct (3).");
+ is(graph.getSelection().end, 300,
+ "The current selection end value is correct (3).");
+}
+
+function setZoom(frame, zoomValue) {
+ let contViewer = frame.docShell.contentViewer;
+ CURRENT_ZOOM = contViewer.fullZoom = zoomValue;
+}
+
+// EventUtils just doesn't work!
+
+function dispatchEvent(graph, x, y, fn) {
+ x *= CURRENT_ZOOM;
+ y *= CURRENT_ZOOM;
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ let quad = graph._canvas.getBoxQuads({
+ relativeTo: window.document
+ })[0];
+
+ let screenX = (window.screenX + quad.p1.x + x);
+ let screenY = (window.screenY + quad.p1.y + y);
+
+ fn({
+ screenX: screenX,
+ screenY: screenY,
+ });
+}
+
+function hover(graph, x, y = 1) {
+ dispatchEvent(graph, x, y, graph._onMouseMove);
+}
+
+function dragStart(graph, x, y = 1) {
+ dispatchEvent(graph, x, y, graph._onMouseMove);
+ dispatchEvent(graph, x, y, graph._onMouseDown);
+}
+
+function dragStop(graph, x, y = 1) {
+ dispatchEvent(graph, x, y, graph._onMouseMove);
+ dispatchEvent(graph, x, y, graph._onMouseUp);
+}
diff --git a/devtools/client/shared/test/browser_graphs-08.js b/devtools/client/shared/test/browser_graphs-08.js
new file mode 100644
index 000000000..ae6b54fb5
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-08.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests if a selection is dropped when clicking outside of it.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.setData(TEST_DATA);
+
+ dragStart(graph, 300);
+ dragStop(graph, 500);
+ ok(graph.hasSelection(),
+ "A selection should be available.");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct.");
+ is(graph.getSelection().end, 500,
+ "The current selection end value is correct.");
+
+ click(graph, 600);
+ ok(!graph.hasSelection(),
+ "The selection should be dropped.");
+}
+
+// EventUtils just doesn't work!
+
+function click(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
diff --git a/devtools/client/shared/test/browser_graphs-09a.js b/devtools/client/shared/test/browser_graphs-09a.js
new file mode 100644
index 000000000..8e6e65c24
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-09a.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that line graphs properly create the gutter and tooltips.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, { metric: "fps" });
+
+ yield testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ info("Should be able to set the graph data before waiting for the ready event.");
+
+ yield graph.setDataWhenReady(TEST_DATA);
+ ok(graph.hasData(), "Data was set successfully.");
+
+ is(graph._gutter.hidden, false,
+ "The gutter should not be hidden because the tooltips have arrows.");
+ is(graph._maxTooltip.hidden, false,
+ "The max tooltip should not be hidden.");
+ is(graph._avgTooltip.hidden, false,
+ "The avg tooltip should not be hidden.");
+ is(graph._minTooltip.hidden, false,
+ "The min tooltip should not be hidden.");
+
+ is(graph._maxTooltip.getAttribute("with-arrows"), "true",
+ "The maximum tooltip has the correct 'with-arrows' attribute.");
+ is(graph._avgTooltip.getAttribute("with-arrows"), "true",
+ "The average tooltip has the correct 'with-arrows' attribute.");
+ is(graph._minTooltip.getAttribute("with-arrows"), "true",
+ "The minimum tooltip has the correct 'with-arrows' attribute.");
+
+ is(graph._maxTooltip.querySelector("[text=info]").textContent, "max",
+ "The maximum tooltip displays the correct info.");
+ is(graph._avgTooltip.querySelector("[text=info]").textContent, "avg",
+ "The average tooltip displays the correct info.");
+ is(graph._minTooltip.querySelector("[text=info]").textContent, "min",
+ "The minimum tooltip displays the correct info.");
+
+ is(graph._maxTooltip.querySelector("[text=value]").textContent, "60",
+ "The maximum tooltip displays the correct value.");
+ is(graph._avgTooltip.querySelector("[text=value]").textContent, "41.72",
+ "The average tooltip displays the correct value.");
+ is(graph._minTooltip.querySelector("[text=value]").textContent, "10",
+ "The minimum tooltip displays the correct value.");
+
+ is(graph._maxTooltip.querySelector("[text=metric]").textContent, "fps",
+ "The maximum tooltip displays the correct metric.");
+ is(graph._avgTooltip.querySelector("[text=metric]").textContent, "fps",
+ "The average tooltip displays the correct metric.");
+ is(graph._minTooltip.querySelector("[text=metric]").textContent, "fps",
+ "The minimum tooltip displays the correct metric.");
+
+ is(parseInt(graph._maxTooltip.style.top, 10), 22,
+ "The maximum tooltip is positioned correctly.");
+ is(parseInt(graph._avgTooltip.style.top, 10), 61,
+ "The average tooltip is positioned correctly.");
+ is(parseInt(graph._minTooltip.style.top, 10), 128,
+ "The minimum tooltip is positioned correctly.");
+
+ is(parseInt(graph._maxGutterLine.style.top, 10), 22,
+ "The maximum gutter line is positioned correctly.");
+ is(parseInt(graph._avgGutterLine.style.top, 10), 61,
+ "The average gutter line is positioned correctly.");
+ is(parseInt(graph._minGutterLine.style.top, 10), 128,
+ "The minimum gutter line is positioned correctly.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-09b.js b/devtools/client/shared/test/browser_graphs-09b.js
new file mode 100644
index 000000000..58dc552fb
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-09b.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that line graphs properly use the tooltips configuration properties.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ graph.withTooltipArrows = false;
+ graph.withFixedTooltipPositions = true;
+
+ yield testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ yield graph.setDataWhenReady(TEST_DATA);
+
+ is(graph._gutter.hidden, false,
+ "The gutter should be visible even if the tooltips don't have arrows.");
+ is(graph._maxTooltip.hidden, false,
+ "The max tooltip should not be hidden.");
+ is(graph._avgTooltip.hidden, false,
+ "The avg tooltip should not be hidden.");
+ is(graph._minTooltip.hidden, false,
+ "The min tooltip should not be hidden.");
+
+ is(graph._maxTooltip.getAttribute("with-arrows"), "false",
+ "The maximum tooltip has the correct 'with-arrows' attribute.");
+ is(graph._avgTooltip.getAttribute("with-arrows"), "false",
+ "The average tooltip has the correct 'with-arrows' attribute.");
+ is(graph._minTooltip.getAttribute("with-arrows"), "false",
+ "The minimum tooltip has the correct 'with-arrows' attribute.");
+
+ is(parseInt(graph._maxTooltip.style.top, 10), 8,
+ "The maximum tooltip is positioned correctly.");
+ is(parseInt(graph._avgTooltip.style.top, 10), 8,
+ "The average tooltip is positioned correctly.");
+ is(parseInt(graph._minTooltip.style.top, 10), 142,
+ "The minimum tooltip is positioned correctly.");
+
+ is(parseInt(graph._maxGutterLine.style.top, 10), 22,
+ "The maximum gutter line is positioned correctly.");
+ is(parseInt(graph._avgGutterLine.style.top, 10), 61,
+ "The average gutter line is positioned correctly.");
+ is(parseInt(graph._minGutterLine.style.top, 10), 128,
+ "The minimum gutter line is positioned correctly.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-09c.js b/devtools/client/shared/test/browser_graphs-09c.js
new file mode 100644
index 000000000..ba3d3c1c4
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-09c.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that line graphs hide the tooltips when there's no data available.
+
+const TEST_DATA = [];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+
+ yield testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ yield graph.setDataWhenReady(TEST_DATA);
+
+ is(graph._gutter.hidden, true,
+ "The gutter should be hidden, since there's no data available.");
+ is(graph._maxTooltip.hidden, true,
+ "The max tooltip should be hidden.");
+ is(graph._avgTooltip.hidden, true,
+ "The avg tooltip should be hidden.");
+ is(graph._minTooltip.hidden, true,
+ "The min tooltip should be hidden.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-09d.js b/devtools/client/shared/test/browser_graphs-09d.js
new file mode 100644
index 000000000..7d0c01c15
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-09d.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that line graphs hide the 'max' tooltip when the distance between
+// the 'min' and 'max' tooltip is too small.
+
+const TEST_DATA = [{ delta: 100, value: 60 }, { delta: 200, value: 59.9 }];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+
+ yield testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ yield graph.setDataWhenReady(TEST_DATA);
+
+ is(graph._gutter.hidden, false,
+ "The gutter should not be hidden.");
+ is(graph._maxTooltip.hidden, true,
+ "The max tooltip should be hidden.");
+ is(graph._avgTooltip.hidden, false,
+ "The avg tooltip should not be hidden.");
+ is(graph._minTooltip.hidden, false,
+ "The min tooltip should not be hidden.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-09e.js b/devtools/client/shared/test/browser_graphs-09e.js
new file mode 100644
index 000000000..72f2f7bdd
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-09e.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that line graphs hide the gutter and tooltips when there's no data,
+// but show them when there is.
+
+const NO_DATA = [];
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+
+ yield testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ yield graph.setDataWhenReady(NO_DATA);
+
+ is(graph._gutter.hidden, true,
+ "The gutter should be hidden when there's no data available.");
+ is(graph._maxTooltip.hidden, true,
+ "The max tooltip should be hidden when there's no data available.");
+ is(graph._avgTooltip.hidden, true,
+ "The avg tooltip should be hidden when there's no data available.");
+ is(graph._minTooltip.hidden, true,
+ "The min tooltip should be hidden when there's no data available.");
+
+ yield graph.setDataWhenReady(TEST_DATA);
+
+ is(graph._gutter.hidden, false,
+ "The gutter should be visible now.");
+ is(graph._maxTooltip.hidden, false,
+ "The max tooltip should be visible now.");
+ is(graph._avgTooltip.hidden, false,
+ "The avg tooltip should be visible now.");
+ is(graph._minTooltip.hidden, false,
+ "The min tooltip should be visible now.");
+
+ yield graph.setDataWhenReady(NO_DATA);
+
+ is(graph._gutter.hidden, true,
+ "The gutter should be hidden again.");
+ is(graph._maxTooltip.hidden, true,
+ "The max tooltip should be hidden again.");
+ is(graph._avgTooltip.hidden, true,
+ "The avg tooltip should be hidden again.");
+ is(graph._minTooltip.hidden, true,
+ "The min tooltip should be hidden again.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-09f.js b/devtools/client/shared/test/browser_graphs-09f.js
new file mode 100644
index 000000000..32b5819b2
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-09f.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the constructor options for `min`, `max` and `avg` on displaying the
+// gutter/tooltips and lines.
+
+const TEST_DATA = [{ delta: 100, value: 60 }, { delta: 200, value: 1 }];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+
+ yield testGraph(doc.body, { avg: false });
+ yield testGraph(doc.body, { min: false });
+ yield testGraph(doc.body, { max: false });
+ yield testGraph(doc.body, { min: false, max: false, avg: false });
+ yield testGraph(doc.body, {});
+
+ host.destroy();
+}
+
+function* testGraph(parent, options) {
+ options.metric = "fps";
+ let graph = new LineGraphWidget(parent, options);
+ yield graph.setDataWhenReady(TEST_DATA);
+ let shouldGutterShow = options.min === false && options.max === false;
+
+ is(graph._gutter.hidden, shouldGutterShow,
+ `The gutter should ${shouldGutterShow ? "" : "not "}be shown`);
+
+ is(graph._maxTooltip.hidden, options.max === false,
+ `The max tooltip should ${options.max === false ? "not " : ""}be shown`);
+ is(graph._maxGutterLine.hidden, options.max === false,
+ `The max gutter should ${options.max === false ? "not " : ""}be shown`);
+ is(graph._minTooltip.hidden, options.min === false,
+ `The min tooltip should ${options.min === false ? "not " : ""}be shown`);
+ is(graph._minGutterLine.hidden, options.min === false,
+ `The min gutter should ${options.min === false ? "not " : ""}be shown`);
+ is(graph._avgTooltip.hidden, options.avg === false,
+ `The avg tooltip should ${options.avg === false ? "not " : ""}be shown`);
+ is(graph._avgGutterLine.hidden, options.avg === false,
+ `The avg gutter should ${options.avg === false ? "not " : ""}be shown`);
+
+ yield graph.destroy();
+}
diff --git a/devtools/client/shared/test/browser_graphs-10a.js b/devtools/client/shared/test/browser_graphs-10a.js
new file mode 100644
index 000000000..7f66156f4
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-10a.js
@@ -0,0 +1,162 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that graphs properly handle resizing.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost("window");
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ let refreshCount = 0;
+ graph.on("refresh", () => refreshCount++);
+
+ yield testGraph(host, graph);
+
+ is(refreshCount, 2, "The graph should've been refreshed 2 times.");
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(host, graph) {
+ graph.setData(TEST_DATA);
+ let initialBounds = host.frame.getBoundingClientRect();
+
+ host._window.resizeBy(-100, -100);
+ yield graph.once("refresh");
+ let newBounds = host.frame.getBoundingClientRect();
+
+ is(initialBounds.width - newBounds.width, 100,
+ "The window was properly resized (1).");
+ is(initialBounds.height - newBounds.height, 100,
+ "The window was properly resized (2).");
+
+ is(graph.width, newBounds.width * window.devicePixelRatio,
+ "The graph has the correct width (1).");
+ is(graph.height, newBounds.height * window.devicePixelRatio,
+ "The graph has the correct height (1).");
+
+ info("Making a selection.");
+
+ dragStart(graph, 300);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should start (1).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (1).");
+ is(graph.getSelection().end, 300,
+ "The current selection end value is correct (1).");
+
+ hover(graph, 400);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should still be in progress (2).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (2).");
+ is(graph.getSelection().end, 400,
+ "The current selection end value is correct (2).");
+
+ dragStop(graph, 500);
+ ok(!graph.hasSelectionInProgress(),
+ "The selection should have stopped (3).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (3).");
+ is(graph.getSelection().end, 500,
+ "The current selection end value is correct (3).");
+
+ host._window.resizeBy(100, 100);
+ yield graph.once("refresh");
+ let newerBounds = host.frame.getBoundingClientRect();
+
+ is(initialBounds.width - newerBounds.width, 0,
+ "The window was properly resized (3).");
+ is(initialBounds.height - newerBounds.height, 0,
+ "The window was properly resized (4).");
+
+ is(graph.width, newerBounds.width * window.devicePixelRatio,
+ "The graph has the correct width (2).");
+ is(graph.height, newerBounds.height * window.devicePixelRatio,
+ "The graph has the correct height (2).");
+
+ info("Making a new selection.");
+
+ dragStart(graph, 200);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should start (4).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (4).");
+ is(graph.getSelection().end, 200,
+ "The current selection end value is correct (4).");
+
+ hover(graph, 300);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should still be in progress (5).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (5).");
+ is(graph.getSelection().end, 300,
+ "The current selection end value is correct (5).");
+
+ dragStop(graph, 400);
+ ok(!graph.hasSelectionInProgress(),
+ "The selection should have stopped (6).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (6).");
+ is(graph.getSelection().end, 400,
+ "The current selection end value is correct (6).");
+}
+
+// EventUtils just doesn't work!
+
+function hover(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+}
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
diff --git a/devtools/client/shared/test/browser_graphs-10b.js b/devtools/client/shared/test/browser_graphs-10b.js
new file mode 100644
index 000000000..a29bdfd25
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-10b.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that graphs aren't refreshed when the owner window resizes but
+// the graph dimensions stay the same.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost("window");
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new LineGraphWidget(doc.body, "fps");
+ graph.fixedWidth = 200;
+ graph.fixedHeight = 100;
+ yield graph.once("ready");
+
+ let refreshCount = 0;
+ let refreshCancelledCount = 0;
+ graph.on("refresh", () => refreshCount++);
+ graph.on("refresh-cancelled", () => refreshCancelledCount++);
+
+ yield testGraph(host, graph);
+
+ is(refreshCount, 0, "The graph shouldn't have been refreshed at all.");
+ is(refreshCancelledCount, 2, "The graph should've had 2 refresh attempts.");
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(host, graph) {
+ graph.setData(TEST_DATA);
+
+ host._window.resizeBy(-100, -100);
+ yield graph.once("refresh-cancelled");
+
+ host._window.resizeBy(100, 100);
+ yield graph.once("refresh-cancelled");
+}
diff --git a/devtools/client/shared/test/browser_graphs-10c.js b/devtools/client/shared/test/browser_graphs-10c.js
new file mode 100644
index 000000000..f68a3e804
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-10c.js
@@ -0,0 +1,109 @@
+
+"use strict";
+
+// Tests that graphs properly handle resizing.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost("window");
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ let refreshCount = 0;
+ graph.on("refresh", () => refreshCount++);
+
+ yield testGraph(host, graph);
+
+ is(refreshCount, 2, "The graph should've been refreshed 2 times.");
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(host, graph) {
+ graph.setData(TEST_DATA);
+
+ host._window.resizeTo(500, 500);
+ yield graph.once("refresh");
+ let oldBounds = host.frame.getBoundingClientRect();
+
+ is(graph._width, oldBounds.width * window.devicePixelRatio,
+ "The window was properly resized (1).");
+ is(graph._height, oldBounds.height * window.devicePixelRatio,
+ "The window was properly resized (1).");
+
+ dragStart(graph, 100);
+ dragStop(graph, 400);
+
+ is(graph.getSelection().start, 100,
+ "The current selection start value is correct (1).");
+ is(graph.getSelection().end, 400,
+ "The current selection end value is correct (1).");
+
+ info("Making sure the selection updates when the window is resized");
+
+ host._window.resizeTo(250, 250);
+ yield graph.once("refresh");
+ let newBounds = host.frame.getBoundingClientRect();
+
+ is(graph._width, newBounds.width * window.devicePixelRatio,
+ "The window was properly resized (2).");
+ is(graph._height, newBounds.height * window.devicePixelRatio,
+ "The window was properly resized (2).");
+
+ let ratio = oldBounds.width / newBounds.width;
+ info("The window resize ratio is: " + ratio);
+
+ is(graph.getSelection().start, Math.round(100 / ratio),
+ "The current selection start value is correct (2).");
+ is(graph.getSelection().end, Math.round(400 / ratio),
+ "The current selection end value is correct (2).");
+}
+
+// EventUtils just doesn't work!
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
diff --git a/devtools/client/shared/test/browser_graphs-11a.js b/devtools/client/shared/test/browser_graphs-11a.js
new file mode 100644
index 000000000..27e5b292c
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-11a.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that bar graph create a legend as expected.
+
+const BarGraphWidget = require("devtools/client/shared/widgets/BarGraphWidget");
+
+const CATEGORIES = [
+ { color: "#46afe3", label: "Foo" },
+ { color: "#eb5368", label: "Bar" },
+ { color: "#70bf53", label: "Baz" }
+];
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new BarGraphWidget(doc.body);
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.format = CATEGORIES;
+ graph.setData([{ delta: 0, values: [] }]);
+
+ let legendContainer = graph._document.querySelector(".bar-graph-widget-legend");
+ ok(legendContainer,
+ "A legend container should be available.");
+ is(legendContainer.childNodes.length, 3,
+ "Three legend items should have been created.");
+
+ let legendItems = graph._document.querySelectorAll(".bar-graph-widget-legend-item");
+ is(legendItems.length, 3,
+ "Three legend items should exist in the entire graph.");
+
+ is(legendItems[0].querySelector("[view=color]").style.backgroundColor,
+ "rgb(70, 175, 227)", "The first legend item has the correct color.");
+ is(legendItems[1].querySelector("[view=color]").style.backgroundColor,
+ "rgb(235, 83, 104)", "The second legend item has the correct color.");
+ is(legendItems[2].querySelector("[view=color]").style.backgroundColor,
+ "rgb(112, 191, 83)", "The third legend item has the correct color.");
+
+ is(legendItems[0].querySelector("[view=label]").textContent, "Foo",
+ "The first legend item has the correct label.");
+ is(legendItems[1].querySelector("[view=label]").textContent, "Bar",
+ "The second legend item has the correct label.");
+ is(legendItems[2].querySelector("[view=label]").textContent, "Baz",
+ "The third legend item has the correct label.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-11b.js b/devtools/client/shared/test/browser_graphs-11b.js
new file mode 100644
index 000000000..4df1c4495
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-11b.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that bar graph's legend items handle mouseover/mouseout.
+
+const BarGraphWidget = require("devtools/client/shared/widgets/BarGraphWidget");
+
+const CATEGORIES = [
+ { color: "#46afe3", label: "Foo" },
+ { color: "#eb5368", label: "Bar" },
+ { color: "#70bf53", label: "Baz" }
+];
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new BarGraphWidget(doc.body, 1);
+ graph.fixedWidth = 200;
+ graph.fixedHeight = 100;
+
+ yield graph.once("ready");
+ yield testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ graph.format = CATEGORIES;
+ graph.dataOffsetX = 1000;
+ graph.setData([{
+ delta: 1100, values: [0, 2, 3]
+ }, {
+ delta: 1200, values: [1, 0, 2]
+ }, {
+ delta: 1300, values: [2, 1, 0]
+ }, {
+ delta: 1400, values: [0, 3, 1]
+ }, {
+ delta: 1500, values: [3, 0, 2]
+ }, {
+ delta: 1600, values: [3, 2, 0]
+ }]);
+
+ /* eslint-disable max-len */
+ is(graph._blocksBoundingRects.toSource(), "[{type:1, start:0, end:33.33333333333333, top:70, bottom:100}, {type:2, start:0, end:33.33333333333333, top:24, bottom:69}, {type:0, start:34.33333333333333, end:66.66666666666666, top:85, bottom:100}, {type:2, start:34.33333333333333, end:66.66666666666666, top:54, bottom:84}, {type:0, start:67.66666666666666, end:100, top:70, bottom:100}, {type:1, start:67.66666666666666, end:100, top:54, bottom:69}, {type:1, start:101, end:133.33333333333331, top:55, bottom:100}, {type:2, start:101, end:133.33333333333331, top:39, bottom:54}, {type:0, start:134.33333333333331, end:166.66666666666666, top:55, bottom:100}, {type:2, start:134.33333333333331, end:166.66666666666666, top:24, bottom:54}, {type:0, start:167.66666666666666, end:200, top:55, bottom:100}, {type:1, start:167.66666666666666, end:200, top:24, bottom:54}]",
+ "The correct blocks bounding rects were calculated for the bar graph.");
+
+ let legendItems = graph._document.querySelectorAll(".bar-graph-widget-legend-item");
+ is(legendItems.length, 3,
+ "Three legend items should exist in the entire graph.");
+
+ yield testLegend(graph, 0, {
+ highlights: "[{type:0, start:34.33333333333333, end:66.66666666666666, top:85, bottom:100}, {type:0, start:67.66666666666666, end:100, top:70, bottom:100}, {type:0, start:134.33333333333331, end:166.66666666666666, top:55, bottom:100}, {type:0, start:167.66666666666666, end:200, top:55, bottom:100}]",
+ selection: "({start:34.33333333333333, end:200})",
+ leftmost: "({type:0, start:34.33333333333333, end:66.66666666666666, top:85, bottom:100})",
+ rightmost: "({type:0, start:167.66666666666666, end:200, top:55, bottom:100})"
+ });
+ yield testLegend(graph, 1, {
+ highlights: "[{type:1, start:0, end:33.33333333333333, top:70, bottom:100}, {type:1, start:67.66666666666666, end:100, top:54, bottom:69}, {type:1, start:101, end:133.33333333333331, top:55, bottom:100}, {type:1, start:167.66666666666666, end:200, top:24, bottom:54}]",
+ selection: "({start:0, end:200})",
+ leftmost: "({type:1, start:0, end:33.33333333333333, top:70, bottom:100})",
+ rightmost: "({type:1, start:167.66666666666666, end:200, top:24, bottom:54})"
+ });
+ yield testLegend(graph, 2, {
+ highlights: "[{type:2, start:0, end:33.33333333333333, top:24, bottom:69}, {type:2, start:34.33333333333333, end:66.66666666666666, top:54, bottom:84}, {type:2, start:101, end:133.33333333333331, top:39, bottom:54}, {type:2, start:134.33333333333331, end:166.66666666666666, top:24, bottom:54}]",
+ selection: "({start:0, end:166.66666666666666})",
+ leftmost: "({type:2, start:0, end:33.33333333333333, top:24, bottom:69})",
+ rightmost: "({type:2, start:134.33333333333331, end:166.66666666666666, top:24, bottom:54})"
+ });
+ /* eslint-enable max-len */
+}
+
+function* testLegend(graph, index, { highlights, selection, leftmost, rightmost }) {
+ // Hover.
+
+ let legendItems = graph._document.querySelectorAll(".bar-graph-widget-legend-item");
+ let colorBlock = legendItems[index].querySelector("[view=color]");
+
+ let debounced = graph.once("legend-hover");
+ graph._onLegendMouseOver({ target: colorBlock });
+ ok(!graph.hasMask(), "The graph shouldn't get highlights immediately.");
+
+ let [type, rects] = yield debounced;
+ ok(graph.hasMask(), "The graph should now have highlights.");
+
+ is(type, index,
+ "The legend item was correctly hovered.");
+ is(rects.toSource(), highlights,
+ "The legend item highlighted the correct regions.");
+
+ // Unhover.
+
+ let unhovered = graph.once("legend-unhover");
+ graph._onLegendMouseOut();
+ ok(!graph.hasMask(), "The graph shouldn't have highlights anymore.");
+
+ yield unhovered;
+ ok(true, "The 'legend-mouseout' event was emitted.");
+
+ // Select.
+
+ let selected = graph.once("legend-selection");
+ graph._onLegendMouseDown(mockEvent(colorBlock));
+ ok(graph.hasSelection(), "The graph should now have a selection.");
+ is(graph.getSelection().toSource(), selection, "The graph has a correct selection.");
+
+ let [left, right] = yield selected;
+ is(left.toSource(), leftmost, "The correct leftmost data block was found.");
+ is(right.toSource(), rightmost, "The correct rightmost data block was found.");
+
+ // Deselect.
+
+ graph.dropSelection();
+}
+
+function mockEvent(node) {
+ return {
+ target: node,
+ preventDefault: () => {},
+ stopPropagation: () => {}
+ };
+}
diff --git a/devtools/client/shared/test/browser_graphs-12.js b/devtools/client/shared/test/browser_graphs-12.js
new file mode 100644
index 000000000..1836d016c
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-12.js
@@ -0,0 +1,157 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that canvas graphs can have their selection linked.
+
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+const BarGraphWidget = require("devtools/client/shared/widgets/BarGraphWidget");
+const {CanvasGraphUtils} = require("devtools/client/shared/widgets/Graphs");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let first = document.createElement("div");
+ first.setAttribute("style", "display: inline-block; width: 100%; height: 50%;");
+ doc.body.appendChild(first);
+
+ let second = document.createElement("div");
+ second.setAttribute("style", "display: inline-block; width: 100%; height: 50%;");
+ doc.body.appendChild(second);
+
+ let graph1 = new LineGraphWidget(first, "js");
+ let graph2 = new BarGraphWidget(second);
+
+ CanvasGraphUtils.linkAnimation(graph1, graph2);
+ CanvasGraphUtils.linkSelection(graph1, graph2);
+
+ yield graph1.ready();
+ yield graph2.ready();
+
+ testGraphs(graph1, graph2);
+
+ yield graph1.destroy();
+ yield graph2.destroy();
+ host.destroy();
+}
+
+function testGraphs(graph1, graph2) {
+ info("Making a selection in the first graph.");
+
+ dragStart(graph1, 300);
+ ok(graph1.hasSelectionInProgress(),
+ "The selection should start (1.1).");
+ ok(!graph2.hasSelectionInProgress(),
+ "The selection should not start yet in the second graph (1.2).");
+ is(graph1.getSelection().start, 300,
+ "The current selection start value is correct (1.1).");
+ is(graph2.getSelection().start, 300,
+ "The current selection start value is correct (1.2).");
+ is(graph1.getSelection().end, 300,
+ "The current selection end value is correct (1.1).");
+ is(graph2.getSelection().end, 300,
+ "The current selection end value is correct (1.2).");
+
+ hover(graph1, 400);
+ ok(graph1.hasSelectionInProgress(),
+ "The selection should still be in progress (2.1).");
+ ok(!graph2.hasSelectionInProgress(),
+ "The selection should not be in progress in the second graph (2.2).");
+ is(graph1.getSelection().start, 300,
+ "The current selection start value is correct (2.1).");
+ is(graph2.getSelection().start, 300,
+ "The current selection start value is correct (2.2).");
+ is(graph1.getSelection().end, 400,
+ "The current selection end value is correct (2.1).");
+ is(graph2.getSelection().end, 400,
+ "The current selection end value is correct (2.2).");
+
+ dragStop(graph1, 500);
+ ok(!graph1.hasSelectionInProgress(),
+ "The selection should have stopped (3.1).");
+ ok(!graph2.hasSelectionInProgress(),
+ "The selection should have stopped (3.2).");
+ is(graph1.getSelection().start, 300,
+ "The current selection start value is correct (3.1).");
+ is(graph2.getSelection().start, 300,
+ "The current selection start value is correct (3.2).");
+ is(graph1.getSelection().end, 500,
+ "The current selection end value is correct (3.1).");
+ is(graph2.getSelection().end, 500,
+ "The current selection end value is correct (3.2).");
+
+ info("Making a new selection in the second graph.");
+
+ dragStart(graph2, 200);
+ ok(!graph1.hasSelectionInProgress(),
+ "The selection should not start yet in the first graph (4.1).");
+ ok(graph2.hasSelectionInProgress(),
+ "The selection should start (4.2).");
+ is(graph1.getSelection().start, 200,
+ "The current selection start value is correct (4.1).");
+ is(graph2.getSelection().start, 200,
+ "The current selection start value is correct (4.2).");
+ is(graph1.getSelection().end, 200,
+ "The current selection end value is correct (4.1).");
+ is(graph2.getSelection().end, 200,
+ "The current selection end value is correct (4.2).");
+
+ hover(graph2, 300);
+ ok(!graph1.hasSelectionInProgress(),
+ "The selection should not be in progress in the first graph (2.2).");
+ ok(graph2.hasSelectionInProgress(),
+ "The selection should still be in progress (5.2).");
+ is(graph1.getSelection().start, 200,
+ "The current selection start value is correct (5.1).");
+ is(graph2.getSelection().start, 200,
+ "The current selection start value is correct (5.2).");
+ is(graph1.getSelection().end, 300,
+ "The current selection end value is correct (5.1).");
+ is(graph2.getSelection().end, 300,
+ "The current selection end value is correct (5.2).");
+
+ dragStop(graph2, 400);
+ ok(!graph1.hasSelectionInProgress(),
+ "The selection should have stopped (6.1).");
+ ok(!graph2.hasSelectionInProgress(),
+ "The selection should have stopped (6.2).");
+ is(graph1.getSelection().start, 200,
+ "The current selection start value is correct (6.1).");
+ is(graph2.getSelection().start, 200,
+ "The current selection start value is correct (6.2).");
+ is(graph1.getSelection().end, 400,
+ "The current selection end value is correct (6.1).");
+ is(graph2.getSelection().end, 400,
+ "The current selection end value is correct (6.2).");
+}
+
+// EventUtils just doesn't work!
+
+function hover(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+}
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
diff --git a/devtools/client/shared/test/browser_graphs-13.js b/devtools/client/shared/test/browser_graphs-13.js
new file mode 100644
index 000000000..d671291ed
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-13.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that graph widgets may have a fixed width or height.
+
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new LineGraphWidget(doc.body, "fps");
+ graph.fixedWidth = 200;
+ graph.fixedHeight = 100;
+
+ yield graph.ready();
+ testGraph(host, graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(host, graph) {
+ let bounds = host.frame.getBoundingClientRect();
+
+ isnot(graph.width, bounds.width * window.devicePixelRatio,
+ "The graph should not span all the parent node's width.");
+ isnot(graph.height, bounds.height * window.devicePixelRatio,
+ "The graph should not span all the parent node's height.");
+
+ is(graph.width, graph.fixedWidth * window.devicePixelRatio,
+ "The graph has the correct width.");
+ is(graph.height, graph.fixedHeight * window.devicePixelRatio,
+ "The graph has the correct height.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-14.js b/devtools/client/shared/test/browser_graphs-14.js
new file mode 100644
index 000000000..4001f8e6d
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-14.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that graph widgets correctly emit mouse input events.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+
+ yield testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ let mouseDownEvents = 0;
+ let mouseUpEvents = 0;
+ let scrollEvents = 0;
+ graph.on("mousedown", () => mouseDownEvents++);
+ graph.on("mouseup", () => mouseUpEvents++);
+ graph.on("scroll", () => scrollEvents++);
+
+ yield graph.setDataWhenReady(TEST_DATA);
+
+ info("Making a selection.");
+
+ dragStart(graph, 300);
+ dragStop(graph, 500);
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (1).");
+ is(graph.getSelection().end, 500,
+ "The current selection end value is correct (1).");
+
+ is(mouseDownEvents, 1,
+ "One mousedown event should have been fired.");
+ is(mouseUpEvents, 1,
+ "One mouseup event should have been fired.");
+ is(scrollEvents, 0,
+ "No scroll event should have been fired.");
+
+ info("Zooming in by scrolling inside the selection.");
+
+ scroll(graph, -1000, 400);
+ is(graph.getSelection().start, 375,
+ "The current selection start value is correct (2).");
+ is(graph.getSelection().end, 425,
+ "The current selection end value is correct (2).");
+
+ is(mouseDownEvents, 1,
+ "No more mousedown events should have been fired.");
+ is(mouseUpEvents, 1,
+ "No more mouseup events should have been fired.");
+ is(scrollEvents, 1,
+ "One scroll event should have been fired.");
+}
+
+// EventUtils just doesn't work!
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
+
+function scroll(graph, wheel, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseWheel({ testX: x, testY: y, detail: wheel });
+}
diff --git a/devtools/client/shared/test/browser_graphs-15.js b/devtools/client/shared/test/browser_graphs-15.js
new file mode 100644
index 000000000..af2c9875e
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-15.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that graph widgets correctly emit mouse input events.
+
+const FAST_FPS = 60;
+const SLOW_FPS = 10;
+
+// Each element represents a second
+const FRAMES = [FAST_FPS, FAST_FPS, FAST_FPS, SLOW_FPS, FAST_FPS];
+const TEST_DATA = [];
+const INTERVAL = 100;
+const DURATION = 5000;
+var t = 0;
+for (let frameRate of FRAMES) {
+ for (let i = 0; i < frameRate; i++) {
+ // Duration between frames at this rate
+ let delta = Math.floor(1000 / frameRate);
+ t += delta;
+ TEST_DATA.push(t);
+ }
+}
+
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+
+ yield testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ console.log("test data", TEST_DATA);
+ yield graph.setDataFromTimestamps(TEST_DATA, INTERVAL, DURATION);
+ is(graph._avgTooltip.querySelector("[text=value]").textContent, "50",
+ "The average tooltip displays the correct value.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-16.js b/devtools/client/shared/test/browser_graphs-16.js
new file mode 100644
index 000000000..194cb751c
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-16.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that mounta graphs work as expected.
+
+const MountainGraphWidget = require("devtools/client/shared/widgets/MountainGraphWidget");
+
+const TEST_DATA = [
+ { delta: 0, values: [0.1, 0.5, 0.3] },
+ { delta: 1, values: [0.25, 0, 0.5] },
+ { delta: 2, values: [0.5, 0.25, 0.1] },
+ { delta: 3, values: [0, 0.75, 0] },
+ { delta: 4, values: [0.75, 0, 0.25] }
+];
+
+const SECTIONS = [
+ { color: "red" },
+ { color: "green" },
+ { color: "blue" }
+];
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new MountainGraphWidget(doc.body);
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.format = SECTIONS;
+ graph.setData(TEST_DATA);
+ ok(true, "The graph didn't throw any erorrs.");
+}
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 000000000..7752881b9
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip-01.js
@@ -0,0 +1,88 @@
+/* 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 = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tooltip test">
+ <vbox flex="1">
+ <hbox id="box1" flex="1">test1</hbox>
+ <hbox id="box2" flex="1">test2</hbox>
+ <hbox id="box3" flex="1">test3</hbox>
+ <hbox id="box4" flex="1">test4</hbox>
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+let useXulWrapper;
+
+function getTooltipContent(doc) {
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "50px";
+ div.style.boxSizing = "border-box";
+ div.textContent = "tooltip";
+ return div;
+}
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Run tests for a Tooltip without using a XUL panel");
+ useXulWrapper = false;
+ yield runTests(doc);
+
+ info("Run tests for a Tooltip with a XUL panel");
+ useXulWrapper = true;
+ yield runTests(doc);
+});
+
+function* runTests(doc) {
+ yield addTab("about:blank");
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper});
+
+ info("Set tooltip content");
+ tooltip.setContent(getTooltipContent(doc), {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++);
+
+ let onShown = tooltip.once("shown");
+ tooltip.show(doc.getElementById("box1"));
+
+ yield onShown;
+ is(shown, 1, "Event shown was fired once");
+
+ yield 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++);
+
+ let onPopupHidden = tooltip.once("hidden");
+ tooltip.hide();
+
+ yield onPopupHidden;
+ is(hidden, 1, "Event hidden was fired once");
+
+ yield 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 000000000..4ebe9185b
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip-02.js
@@ -0,0 +1,174 @@
+/* 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 = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ htmlns="http://www.w3.org/1999/xhtml"
+ title="Tooltip test">
+ <vbox flex="1">
+ <hbox id="box1" flex="1">test1</hbox>
+ <hbox id="box2" flex="1">test2</hbox>
+ <hbox id="box3" flex="1">test3</hbox>
+ <hbox id="box4" flex="1">test4</hbox>
+ <iframe id="frame" width="200"></iframe>
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+let useXulWrapper;
+
+add_task(function* () {
+ yield addTab("about:blank");
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Run tests for a Tooltip without using a XUL panel");
+ useXulWrapper = false;
+ yield runTests(doc);
+
+ info("Run tests for a Tooltip with a XUL panel");
+ useXulWrapper = true;
+ yield runTests(doc);
+});
+
+function* runTests(doc) {
+ yield testClickInTooltipContent(doc);
+ yield testConsumeOutsideClicksFalse(doc);
+ yield testConsumeOutsideClicksTrue(doc);
+ yield testConsumeWithRightClick(doc);
+ yield testClickInOuterIframe(doc);
+ yield testClickInInnerIframe(doc);
+}
+
+function* testClickInTooltipContent(doc) {
+ info("Test a tooltip is not closed when clicking inside itself");
+
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper});
+ tooltip.setContent(getTooltipContent(doc), {width: 100, height: 50});
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+
+ let onTooltipContainerClick = once(tooltip.container, "click");
+ EventUtils.synthesizeMouseAtCenter(tooltip.container, {}, doc.defaultView);
+ yield onTooltipContainerClick;
+ is(tooltip.isVisible(), true, "Tooltip is still visible");
+
+ tooltip.destroy();
+}
+
+function* testConsumeOutsideClicksFalse(doc) {
+ info("Test closing a tooltip via click with consumeOutsideClicks: false");
+ let box4 = doc.getElementById("box4");
+
+ let tooltip = new HTMLTooltip(doc, {consumeOutsideClicks: false, useXulWrapper});
+ tooltip.setContent(getTooltipContent(doc), {width: 100, height: 50});
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+
+ let onBox4Clicked = once(box4, "click");
+ let onHidden = once(tooltip, "hidden");
+ EventUtils.synthesizeMouseAtCenter(box4, {}, doc.defaultView);
+ yield onHidden;
+ yield onBox4Clicked;
+
+ is(tooltip.isVisible(), false, "Tooltip is hidden");
+
+ tooltip.destroy();
+}
+
+function* testConsumeOutsideClicksTrue(doc) {
+ info("Test closing a tooltip via click with consumeOutsideClicks: true");
+ let box4 = doc.getElementById("box4");
+
+ // Count clicks on box4
+ let box4clicks = 0;
+ box4.addEventListener("click", () => box4clicks++);
+
+ let tooltip = new HTMLTooltip(doc, {consumeOutsideClicks: true, useXulWrapper});
+ tooltip.setContent(getTooltipContent(doc), {width: 100, height: 50});
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+
+ let onHidden = once(tooltip, "hidden");
+ EventUtils.synthesizeMouseAtCenter(box4, {}, doc.defaultView);
+ yield onHidden;
+
+ is(box4clicks, 0, "box4 catched no click event");
+ is(tooltip.isVisible(), false, "Tooltip is hidden");
+
+ tooltip.destroy();
+}
+
+function* testConsumeWithRightClick(doc) {
+ info("Test closing a tooltip with a right-click, with consumeOutsideClicks: true");
+ let box4 = doc.getElementById("box4");
+
+ let tooltip = new HTMLTooltip(doc, {consumeOutsideClicks: true, useXulWrapper});
+ tooltip.setContent(getTooltipContent(doc), {width: 100, height: 50});
+ yield 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");
+ let onBox4Clicked = once(box4, "click");
+ let onHidden = once(tooltip, "hidden");
+ EventUtils.synthesizeMouseAtCenter(box4, {button: 2}, doc.defaultView);
+ yield onHidden;
+ yield onBox4Clicked;
+
+ is(tooltip.isVisible(), false, "Tooltip is hidden");
+
+ tooltip.destroy();
+}
+
+function* testClickInOuterIframe(doc) {
+ info("Test clicking an iframe outside of the tooltip closes the tooltip");
+ let frame = doc.getElementById("frame");
+
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper});
+ tooltip.setContent(getTooltipContent(doc), {width: 100, height: 50});
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+
+ let onHidden = once(tooltip, "hidden");
+ EventUtils.synthesizeMouseAtCenter(frame, {}, doc.defaultView);
+ yield onHidden;
+
+ is(tooltip.isVisible(), false, "Tooltip is hidden");
+ tooltip.destroy();
+}
+
+function* testClickInInnerIframe(doc) {
+ info("Test clicking an iframe inside the tooltip content does not close the tooltip");
+
+ let tooltip = new HTMLTooltip(doc, {consumeOutsideClicks: false, useXulWrapper});
+
+ let iframe = doc.createElementNS(HTML_NS, "iframe");
+ iframe.style.width = "100px";
+ iframe.style.height = "50px";
+ tooltip.setContent(iframe, {width: 100, height: 50});
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+
+ let onTooltipContainerClick = once(tooltip.container, "click");
+ EventUtils.synthesizeMouseAtCenter(tooltip.container, {}, doc.defaultView);
+ yield onTooltipContainerClick;
+
+ is(tooltip.isVisible(), true, "Tooltip is still visible");
+
+ tooltip.destroy();
+}
+
+function getTooltipContent(doc) {
+ let 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 000000000..6c189c127
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip-03.js
@@ -0,0 +1,155 @@
+/* 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 autofocus configuration option.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tooltip test">
+ <vbox flex="1">
+ <hbox id="box1" flex="1">
+ <textbox></textbox>
+ </hbox>
+ <hbox id="box2" flex="1">test2</hbox>
+ <hbox id="box3" flex="1">
+ <textbox id="box3-input"></textbox>
+ </hbox>
+ <hbox id="box4" flex="1">
+ <textbox id="box4-input"></textbox>
+ </hbox>
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+let useXulWrapper;
+
+add_task(function* () {
+ yield addTab("about:blank");
+ let [, , doc] = yield createHost("bottom", TEST_URI);
+
+ info("Run tests for a Tooltip without using a XUL panel");
+ useXulWrapper = false;
+ yield runTests(doc);
+
+ info("Run tests for a Tooltip with a XUL panel");
+ useXulWrapper = true;
+ yield runTests(doc);
+});
+
+function* runTests(doc) {
+ yield testNoAutoFocus(doc);
+ yield testAutoFocus(doc);
+ yield testAutoFocusPreservesFocusChange(doc);
+}
+
+function* testNoAutoFocus(doc) {
+ yield focusNode(doc, "#box4-input");
+ ok(doc.activeElement.closest("#box4-input"), "Focus is in the #box4-input");
+
+ info("Test a tooltip without autofocus will not take focus");
+ let tooltip = yield createTooltip(doc, false);
+
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+ ok(doc.activeElement.closest("#box4-input"), "Focus is still in the #box4-input");
+
+ yield hideTooltip(tooltip);
+ yield blurNode(doc, "#box4-input");
+
+ tooltip.destroy();
+}
+
+function* testAutoFocus(doc) {
+ yield focusNode(doc, "#box4-input");
+ ok(doc.activeElement.closest("#box4-input"), "Focus is in the #box4-input");
+
+ info("Test autofocus tooltip takes focus when displayed, " +
+ "and restores the focus when hidden");
+ let tooltip = yield createTooltip(doc, true);
+
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+ ok(doc.activeElement.closest(".tooltip-content"), "Focus is in the tooltip");
+
+ yield hideTooltip(tooltip);
+ ok(doc.activeElement.closest("#box4-input"), "Focus is in the #box4-input");
+
+ info("Blur the textbox before moving to the next test to reset the state.");
+ yield blurNode(doc, "#box4-input");
+
+ tooltip.destroy();
+}
+
+function* testAutoFocusPreservesFocusChange(doc) {
+ yield focusNode(doc, "#box4-input");
+ ok(doc.activeElement.closest("#box4-input"), "Focus is still in the #box3-input");
+
+ info("Test autofocus tooltip takes focus when displayed, " +
+ "but does not try to restore the active element if it is not focused when hidden");
+ let tooltip = yield createTooltip(doc, true);
+
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+ ok(doc.activeElement.closest(".tooltip-content"), "Focus is in the tooltip");
+
+ info("Move the focus to #box3-input while the tooltip is displayed");
+ yield focusNode(doc, "#box3-input");
+ ok(doc.activeElement.closest("#box3-input"), "Focus moved to the #box3-input");
+
+ yield hideTooltip(tooltip);
+ ok(doc.activeElement.closest("#box3-input"), "Focus is still in the #box3-input");
+
+ info("Blur the textbox before moving to the next test to reset the state.");
+ yield blurNode(doc, "#box3-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) {
+ let node = doc.querySelector(selector);
+ let 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) {
+ let node = doc.querySelector(selector);
+ let onBlur = once(node, "blur");
+ node.blur();
+ return onBlur;
+}
+
+/**
+ * Create an HTMLTooltip instance with the provided autofocus setting.
+ *
+ * @param {Document} doc
+ * Document in which the tooltip should be created
+ * @param {Boolean} autofocus
+ * @return {Promise} promise that will resolve the HTMLTooltip instance created when the
+ * tooltip content will be ready.
+ */
+function* createTooltip(doc, autofocus) {
+ let tooltip = new HTMLTooltip(doc, {autofocus, useXulWrapper});
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.classList.add("tooltip-content");
+ div.style.height = "50px";
+ div.innerHTML = '<input type="text"></input>';
+
+ tooltip.setContent(div, {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 000000000..90164840e
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip-04.js
@@ -0,0 +1,110 @@
+/* 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 = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tooltip test">
+ <vbox flex="1">
+ <hbox style="height: 10px">spacer</hbox>
+ <hbox id="box1" style="height: 50px">test1</hbox>
+ <hbox id="box2" style="height: 50px">test2</hbox>
+ <hbox flex="1">MIDDLE</hbox>
+ <hbox id="box3" style="height: 50px">test3</hbox>
+ <hbox id="box4" style="height: 50px">test4</hbox>
+ <hbox style="height: 10px">spacer</hbox>
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+const TOOLTIP_HEIGHT = 30;
+const TOOLTIP_WIDTH = 100;
+
+add_task(function* () {
+ // Force the toolbox to be 400px high;
+ yield pushPref("devtools.toolbox.footer.height", 400);
+
+ yield addTab("about:blank");
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Create HTML tooltip");
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper: false});
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "100%";
+ tooltip.setContent(div, {width: TOOLTIP_WIDTH, height: TOOLTIP_HEIGHT});
+
+ let box1 = doc.getElementById("box1");
+ let box2 = doc.getElementById("box2");
+ let box3 = doc.getElementById("box3");
+ let box4 = doc.getElementById("box4");
+ let height = TOOLTIP_HEIGHT, width = TOOLTIP_WIDTH;
+
+ // box1: Can only fit below box1
+ info("Display the tooltip on box1.");
+ yield showTooltip(tooltip, box1);
+ let expectedTooltipGeometry = {position: "bottom", height, width};
+ checkTooltipGeometry(tooltip, box1, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ info("Try to display the tooltip on top of box1.");
+ yield showTooltip(tooltip, box1, {position: "top"});
+ expectedTooltipGeometry = {position: "bottom", height, width};
+ checkTooltipGeometry(tooltip, box1, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ // box2: Can fit above or below, will default to bottom, more height
+ // available.
+ info("Try to display the tooltip on box2.");
+ yield showTooltip(tooltip, box2);
+ expectedTooltipGeometry = {position: "bottom", height, width};
+ checkTooltipGeometry(tooltip, box2, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ info("Try to display the tooltip on top of box2.");
+ yield showTooltip(tooltip, box2, {position: "top"});
+ expectedTooltipGeometry = {position: "top", height, width};
+ checkTooltipGeometry(tooltip, box2, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ // box3: Can fit above or below, will default to top, more height available.
+ info("Try to display the tooltip on box3.");
+ yield showTooltip(tooltip, box3);
+ expectedTooltipGeometry = {position: "top", height, width};
+ checkTooltipGeometry(tooltip, box3, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ info("Try to display the tooltip on bottom of box3.");
+ yield showTooltip(tooltip, box3, {position: "bottom"});
+ expectedTooltipGeometry = {position: "bottom", height, width};
+ checkTooltipGeometry(tooltip, box3, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ // box4: Can only fit above box4
+ info("Display the tooltip on box4.");
+ yield showTooltip(tooltip, box4);
+ expectedTooltipGeometry = {position: "top", height, width};
+ checkTooltipGeometry(tooltip, box4, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ info("Try to display the tooltip on bottom of box4.");
+ yield showTooltip(tooltip, box4, {position: "bottom"});
+ expectedTooltipGeometry = {position: "top", height, width};
+ checkTooltipGeometry(tooltip, box4, expectedTooltipGeometry);
+ yield 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 000000000..58be4f831
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip-05.js
@@ -0,0 +1,109 @@
+/* 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 = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tooltip test">
+ <vbox flex="1">
+ <hbox id="box1" style="height: 50px">test1</hbox>
+ <hbox id="box2" style="height: 50px">test2</hbox>
+ <hbox id="box3" style="height: 50px">test3</hbox>
+ <hbox id="box4" style="height: 50px">test4</hbox>
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+const TOOLTIP_HEIGHT = 200;
+const TOOLTIP_WIDTH = 200;
+
+add_task(function* () {
+ // Force the toolbox to be 200px high;
+ yield pushPref("devtools.toolbox.footer.height", 200);
+ yield addTab("about:blank");
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Create HTML tooltip");
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper: false});
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "100%";
+ tooltip.setContent(div, {width: TOOLTIP_WIDTH, height: TOOLTIP_HEIGHT});
+
+ let box1 = doc.getElementById("box1");
+ let box2 = doc.getElementById("box2");
+ let box3 = doc.getElementById("box3");
+ let box4 = doc.getElementById("box4");
+ let 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.");
+ yield showTooltip(tooltip, box1);
+ let expectedTooltipGeometry = {position: "bottom", height: 150, width};
+ checkTooltipGeometry(tooltip, box1, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ info("Try to display the tooltip on top of box1.");
+ yield showTooltip(tooltip, box1, {position: "top"});
+ expectedTooltipGeometry = {position: "bottom", height: 150, width};
+ checkTooltipGeometry(tooltip, box1, expectedTooltipGeometry);
+ yield 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.");
+ yield showTooltip(tooltip, box2);
+ expectedTooltipGeometry = {position: "bottom", height: 100, width};
+ checkTooltipGeometry(tooltip, box2, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ info("Try to display the tooltip on top of box2.");
+ yield showTooltip(tooltip, box2, {position: "top"});
+ expectedTooltipGeometry = {position: "bottom", height: 100, width};
+ checkTooltipGeometry(tooltip, box2, expectedTooltipGeometry);
+ yield 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.");
+ yield showTooltip(tooltip, box3);
+ expectedTooltipGeometry = {position: "top", height: 100, width};
+ checkTooltipGeometry(tooltip, box3, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ info("Try to display the tooltip on bottom of box3.");
+ yield showTooltip(tooltip, box3, {position: "bottom"});
+ expectedTooltipGeometry = {position: "top", height: 100, width};
+ checkTooltipGeometry(tooltip, box3, expectedTooltipGeometry);
+ yield 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.");
+ yield showTooltip(tooltip, box4);
+ expectedTooltipGeometry = {position: "top", height: 150, width};
+ checkTooltipGeometry(tooltip, box4, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ info("Try to display the tooltip on bottom of box4.");
+ yield showTooltip(tooltip, box4, {position: "bottom"});
+ expectedTooltipGeometry = {position: "top", height: 150, width};
+ checkTooltipGeometry(tooltip, box4, expectedTooltipGeometry);
+ yield 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 000000000..a20c67529
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_arrow-01.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 "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 getAnchor = function (position) {
+ return `<html:div class="anchor" style="width:10px;
+ height: 10px;
+ position: absolute;
+ background: red;
+ ${position}"></html:div>`;
+};
+
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/light-theme.css"?>
+
+ <window class="theme-light"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Tooltip test">
+ <vbox flex="1" style="position: relative">
+ ${getAnchor("top: 0; left: 0;")}
+ ${getAnchor("top: 0; left: 25px;")}
+ ${getAnchor("top: 0; left: 50px;")}
+ ${getAnchor("top: 0; left: 75px;")}
+ ${getAnchor("bottom: 0; left: 0;")}
+ ${getAnchor("bottom: 0; left: 25px;")}
+ ${getAnchor("bottom: 0; left: 50px;")}
+ ${getAnchor("bottom: 0; left: 75px;")}
+ ${getAnchor("bottom: 0; right: 0;")}
+ ${getAnchor("bottom: 0; right: 25px;")}
+ ${getAnchor("bottom: 0; right: 50px;")}
+ ${getAnchor("bottom: 0; right: 75px;")}
+ ${getAnchor("top: 0; right: 0;")}
+ ${getAnchor("top: 0; right: 25px;")}
+ ${getAnchor("top: 0; right: 50px;")}
+ ${getAnchor("top: 0; right: 75px;")}
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+let useXulWrapper;
+
+add_task(function* () {
+ // Force the toolbox to be 200px high;
+ yield pushPref("devtools.toolbox.footer.height", 200);
+
+ yield addTab("about:blank");
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Run tests for a Tooltip without using a XUL panel");
+ useXulWrapper = false;
+ yield runTests(doc);
+
+ info("Run tests for a Tooltip with a XUL panel");
+ useXulWrapper = true;
+ yield runTests(doc);
+});
+
+function* runTests(doc) {
+ info("Create HTML tooltip");
+ let tooltip = new HTMLTooltip(doc, {type: "arrow", useXulWrapper});
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "35px";
+ tooltip.setContent(div, {width: 200, height: 35});
+
+ let {right: docRight} = doc.documentElement.getBoundingClientRect();
+
+ let elements = [...doc.querySelectorAll(".anchor")];
+ for (let el of elements) {
+ info("Display the tooltip on an anchor.");
+ yield showTooltip(tooltip, el);
+
+ let arrow = tooltip.arrow;
+ ok(arrow, "Tooltip has an arrow");
+
+ // Get the geometry of the anchor, the tooltip panel & arrow.
+ let arrowBounds = arrow.getBoxQuads({relativeTo: doc})[0].bounds;
+ let panelBounds = tooltip.panel.getBoxQuads({relativeTo: doc})[0].bounds;
+ let anchorBounds = el.getBoxQuads({relativeTo: doc})[0].bounds;
+
+ let intersects = arrowBounds.left <= anchorBounds.right &&
+ arrowBounds.right >= anchorBounds.left;
+ let isBlockedByViewport = arrowBounds.left == 0 ||
+ arrowBounds.right == docRight;
+ ok(intersects || isBlockedByViewport,
+ "Tooltip arrow is aligned with the anchor, or stuck on viewport's edge.");
+
+ let isInPanel = arrowBounds.left >= panelBounds.left &&
+ arrowBounds.right <= panelBounds.right;
+ ok(isInPanel,
+ "The tooltip arrow remains inside the tooltip panel horizontally");
+
+ yield 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 000000000..098f1ac7b
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_arrow-02.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 "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 getAnchor = function (position) {
+ return `<html:div class="anchor" style="height: 5px;
+ position: absolute;
+ background: red;
+ ${position}"></html:div>`;
+};
+
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/light-theme.css"?>
+
+ <window class="theme-light"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Tooltip test">
+ <vbox flex="1" style="position: relative">
+ ${getAnchor("top: 0; left: 0; width: 50px;")}
+ ${getAnchor("top: 10px; left: 0; width: 100px;")}
+ ${getAnchor("top: 20px; left: 0; width: 150px;")}
+ ${getAnchor("top: 30px; left: 0; width: 200px;")}
+ ${getAnchor("top: 40px; left: 0; width: 250px;")}
+ ${getAnchor("top: 50px; left: 100px; width: 250px;")}
+ ${getAnchor("top: 100px; width: 50px; right: 0;")}
+ ${getAnchor("top: 110px; width: 100px; right: 0;")}
+ ${getAnchor("top: 120px; width: 150px; right: 0;")}
+ ${getAnchor("top: 130px; width: 200px; right: 0;")}
+ ${getAnchor("top: 140px; width: 250px; right: 0;")}
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+let useXulWrapper;
+
+add_task(function* () {
+ // Force the toolbox to be 200px high;
+ yield pushPref("devtools.toolbox.footer.height", 200);
+
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Run tests for a Tooltip without using a XUL panel");
+ useXulWrapper = false;
+ yield runTests(doc);
+
+ info("Run tests for a Tooltip with a XUL panel");
+ useXulWrapper = true;
+ yield runTests(doc);
+});
+
+function* runTests(doc) {
+ info("Create HTML tooltip");
+ let tooltip = new HTMLTooltip(doc, {type: "arrow", useXulWrapper});
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "35px";
+ tooltip.setContent(div, {width: 200, height: 35});
+
+ let {right: docRight} = doc.documentElement.getBoundingClientRect();
+
+ let elements = [...doc.querySelectorAll(".anchor")];
+ for (let el of elements) {
+ info("Display the tooltip on an anchor.");
+ yield showTooltip(tooltip, el);
+
+ let arrow = tooltip.arrow;
+ ok(arrow, "Tooltip has an arrow");
+
+ // Get the geometry of the anchor, the tooltip panel & arrow.
+ let arrowBounds = arrow.getBoxQuads({relativeTo: doc})[0].bounds;
+ let panelBounds = tooltip.panel.getBoxQuads({relativeTo: doc})[0].bounds;
+ let anchorBounds = el.getBoxQuads({relativeTo: doc})[0].bounds;
+
+ let intersects = arrowBounds.left <= anchorBounds.right &&
+ arrowBounds.right >= anchorBounds.left;
+ let isBlockedByViewport = arrowBounds.left == 0 ||
+ arrowBounds.right == docRight;
+ ok(intersects || isBlockedByViewport,
+ "Tooltip arrow is aligned with the anchor, or stuck on viewport's edge.");
+
+ let isInPanel = arrowBounds.left >= panelBounds.left &&
+ arrowBounds.right <= panelBounds.right;
+ ok(isInPanel,
+ "The tooltip arrow remains inside the tooltip panel horizontally");
+ yield 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 000000000..7ed1d6dc1
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_consecutive-show.js
@@ -0,0 +1,71 @@
+/* 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 = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tooltip test">
+ <vbox flex="1">
+ <hbox id="box1" flex="1">test1</hbox>
+ <hbox id="box2" flex="1">test2</hbox>
+ <hbox id="box3" flex="1">test3</hbox>
+ <hbox id="box4" flex="1">test4</hbox>
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+function getTooltipContent(doc) {
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "50px";
+ div.textContent = "tooltip";
+ return div;
+}
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+
+ let box1 = doc.getElementById("box1");
+ let box2 = doc.getElementById("box2");
+ let box3 = doc.getElementById("box3");
+ let box4 = doc.getElementById("box4");
+
+ let width = 100, height = 50;
+
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper: false});
+ tooltip.setContent(getTooltipContent(doc), {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");
+ yield 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 000000000..8a661bbe1
--- /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 = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="resource://devtools/client/themes/variables.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ class="theme-light" title="Tooltip hover test">
+ <vbox id="container" flex="1">
+ <hbox id="box1" flex="1"><label>test1</label></hbox>
+ <hbox id="box2" flex="1"><label>test2</label></hbox>
+ <hbox id="box3" flex="1"><label>test3</label></hbox>
+ <hbox id="box4" flex="1"><label>test4</label></hbox>
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ // Wait for full page load before synthesizing events on the page.
+ yield waitUntil(() => doc.readyState === "complete");
+
+ let width = 100, height = 50;
+ let tooltipContent = doc.createElementNS(HTML_NS, "div");
+ tooltipContent.textContent = "tooltip";
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper: false});
+ tooltip.setContent(tooltipContent, {width, height});
+
+ let container = doc.getElementById("container");
+ tooltip.startTogglingOnHover(container, () => true);
+
+ info("Hover on each of the 4 boxes, expect the tooltip to appear");
+ function* showAndCheck(boxId, position) {
+ info(`Show tooltip on ${boxId}`);
+ let box = doc.getElementById(boxId);
+ let shown = tooltip.once("shown");
+ EventUtils.synthesizeMouseAtCenter(box, { type: "mousemove" }, doc.defaultView);
+ yield shown;
+ checkTooltipGeometry(tooltip, box, {position, width, height});
+ }
+
+ yield showAndCheck("box1", "bottom");
+ yield showAndCheck("box2", "bottom");
+ yield showAndCheck("box3", "top");
+ yield showAndCheck("box4", "top");
+
+ info("Move out of the container");
+ let hidden = tooltip.once("hidden");
+ EventUtils.synthesizeMouseAtCenter(container, { type: "mouseout" }, doc.defaultView);
+ yield 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 000000000..dfbdef723
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_offset.js
@@ -0,0 +1,99 @@
+/* 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 = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ htmlns="http://www.w3.org/1999/xhtml"
+ title="Tooltip test">
+ <vbox flex="1">
+ <hbox id="box1" flex="1">test1</hbox>
+ <hbox id="box2" flex="1">test2</hbox>
+ <hbox id="box3" flex="1">test3</hbox>
+ <hbox id="box4" flex="1">test4</hbox>
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+add_task(function* () {
+ // Force the toolbox to be 200px high;
+ yield pushPref("devtools.toolbox.footer.height", 200);
+
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Test a tooltip is not closed when clicking inside itself");
+
+ let box1 = doc.getElementById("box1");
+ let box2 = doc.getElementById("box2");
+ let box3 = doc.getElementById("box3");
+ let box4 = doc.getElementById("box4");
+
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper: false});
+
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "100px";
+ div.style.boxSizing = "border-box";
+ div.textContent = "tooltip";
+ tooltip.setContent(div, {width: 50, height: 100});
+
+ info("Display the tooltip on box1.");
+ yield 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.");
+ yield 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.");
+ yield 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.");
+ yield 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");
+
+ yield 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 000000000..e41716c80
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_rtl.js
@@ -0,0 +1,140 @@
+/* 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 = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ htmlns="http://www.w3.org/1999/xhtml"
+ title="Tooltip test">
+ <hbox style="padding: 90px 0;" flex="1">
+ <hbox id="box1" flex="1" style="background:red; direction: rtl;">test1</hbox>
+ <hbox id="box2" flex="1" style="background:blue; direction: rtl;">test2</hbox>
+ <hbox id="box3" flex="1" style="background:red; direction: ltr;">test3</hbox>
+ <hbox id="box4" flex="1" style="background:blue; direction: ltr;">test4</hbox>
+ </hbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+const TOOLBOX_WIDTH = 500;
+const TOOLTIP_WIDTH = 150;
+const TOOLTIP_HEIGHT = 30;
+
+add_task(function* () {
+ // Force the toolbox to be 500px wide (min width is 465px);
+ yield pushPref("devtools.toolbox.sidebar.width", TOOLBOX_WIDTH);
+
+ let [,, doc] = yield createHost("side", TEST_URI);
+
+ info("Test a tooltip is not closed when clicking inside itself");
+
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper: false});
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.textContent = "tooltip";
+ div.style.cssText = "box-sizing: border-box; border: 1px solid black";
+ tooltip.setContent(div, {width: TOOLTIP_WIDTH, height: TOOLTIP_HEIGHT});
+
+ yield testRtlAnchors(doc, tooltip);
+ yield testLtrAnchors(doc, tooltip);
+ yield hideTooltip(tooltip);
+
+ tooltip.destroy();
+});
+
+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
+ */
+
+ let box1 = doc.getElementById("box1");
+ let box2 = doc.getElementById("box2");
+
+ info("Display the tooltip on box1.");
+ yield 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.");
+ yield 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(panelRect.right, 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");
+}
+
+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
+ */
+
+ let box3 = doc.getElementById("box3");
+ let box4 = doc.getElementById("box4");
+
+ info("Display the tooltip on box3.");
+ yield 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(panelRect.left, 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.");
+ yield 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");
+}
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 000000000..e64ec4a2e
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_variable-height.js
@@ -0,0 +1,75 @@
+/* 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 = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tooltip test">
+ <vbox flex="1">
+ <hbox id="box1" flex="1">test1</hbox>
+ <hbox id="box2" flex="1">test2</hbox>
+ <hbox id="box3" flex="1">test3</hbox>
+ <hbox id="box4" flex="1">test4</hbox>
+ </vbox>
+ </window>`;
+
+const CONTAINER_HEIGHT = 300;
+const CONTAINER_WIDTH = 200;
+const TOOLTIP_HEIGHT = 50;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+add_task(function* () {
+ // Force the toolbox to be 400px tall => 50px for each box.
+ yield pushPref("devtools.toolbox.footer.height", 400);
+
+ yield addTab("about:blank");
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper: false});
+ info("Set tooltip content 50px tall, but request a container 200px tall");
+ let tooltipContent = doc.createElementNS(HTML_NS, "div");
+ tooltipContent.style.cssText = "height: " + TOOLTIP_HEIGHT + "px; background: red;";
+ tooltip.setContent(tooltipContent, {width: CONTAINER_WIDTH, height: Infinity});
+
+ info("Show the tooltip and check the container and panel height.");
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+
+ let containerRect = tooltip.container.getBoundingClientRect();
+ let 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);
+ yield onHidden;
+
+ info("Show the tooltip one more time, and increase the content height");
+ yield 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.");
+ let onPanelClick = once(tooltip.panel, "click");
+ EventUtils.synthesizeMouse(tooltip.container, 100, 100, {}, doc.defaultView);
+ yield 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);
+ yield 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 000000000..66e33673e
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_width-auto.js
@@ -0,0 +1,61 @@
+/* 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 = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tooltip test">
+ <vbox flex="1">
+ <hbox id="box1" flex="1">test1</hbox>
+ <hbox id="box2" flex="1">test2</hbox>
+ <hbox id="box3" flex="1">test3</hbox>
+ <hbox id="box4" flex="1">test4</hbox>
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+let useXulWrapper;
+
+add_task(function* () {
+ yield addTab("about:blank");
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Run tests for a Tooltip without using a XUL panel");
+ useXulWrapper = false;
+ yield runTests(doc);
+
+ info("Run tests for a Tooltip with a XUL panel");
+ useXulWrapper = true;
+ yield runTests(doc);
+});
+
+function* runTests(doc) {
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper});
+ info("Create tooltip content width to 150px");
+ let tooltipContent = doc.createElementNS(HTML_NS, "div");
+ tooltipContent.style.cssText = "height: 100%; width: 150px; background: red;";
+
+ info("Set tooltip content using width:auto");
+ tooltip.setContent(tooltipContent, {width: "auto", height: 50});
+
+ info("Show the tooltip and check the tooltip panel width.");
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+
+ let panelRect = tooltip.panel.getBoundingClientRect();
+ is(panelRect.width, 150, "Tooltip panel has the expected width.");
+
+ yield 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 000000000..5c21f21c3
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_xul-wrapper.js
@@ -0,0 +1,80 @@
+/* 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 = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tooltip test">
+ <vbox flex="1">
+ <hbox id="box1" style="height: 50px">test1</hbox>
+ <hbox id="box2" style="height: 50px">test2</hbox>
+ <hbox id="box3" style="height: 50px">test3</hbox>
+ <hbox id="box4" style="height: 50px">test4</hbox>
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+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(function* () {
+ // Force the toolbox to be 200px high;
+ yield pushPref("devtools.toolbox.footer.height", 200);
+
+ let [, win, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Resizing window to have some space below the window.");
+ let originalWidth = win.top.outerWidth;
+ let originalHeight = win.top.outerHeight;
+ win.top.resizeBy(0, -100);
+
+ info("Create HTML tooltip");
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper: true});
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "200px";
+ div.style.background = "red";
+ tooltip.setContent(div, {width: TOOLTIP_WIDTH, height: TOOLTIP_HEIGHT});
+
+ let box1 = doc.getElementById("box1");
+
+ // Above box1: check that the tooltip can overflow onto the content page.
+ info("Display the tooltip above box1.");
+ yield showTooltip(tooltip, box1, {position: "top"});
+ checkTooltip(tooltip, "top", TOOLTIP_HEIGHT);
+ yield hideTooltip(tooltip);
+
+ // Below box1: check that the tooltip can overflow out of the browser window.
+ info("Display the tooltip below box1.");
+ yield showTooltip(tooltip, box1, {position: "bottom"});
+ checkTooltip(tooltip, "bottom", TOOLTIP_HEIGHT);
+ yield hideTooltip(tooltip);
+
+ is(tooltip.isVisible(), false, "Tooltip is not visible");
+
+ tooltip.destroy();
+
+ info("Restore original window dimensions.");
+ win.top.resizeTo(originalWidth, originalHeight);
+});
+
+function checkTooltip(tooltip, position, height) {
+ is(tooltip.position, position, "Actual tooltip position is " + position);
+ let 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_inplace-editor-01.js b/devtools/client/shared/test/browser_inplace-editor-01.js
new file mode 100644
index 000000000..6308602f1
--- /dev/null
+++ b/devtools/client/shared/test/browser_inplace-editor-01.js
@@ -0,0 +1,150 @@
+/* vim: set ts=2 et sw=2 tw=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 the inplace-editor behavior.
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8,inline editor tests");
+ let [host, , doc] = yield createHost();
+
+ yield testMultipleInitialization(doc);
+ yield testReturnCommit(doc);
+ yield testBlurCommit(doc);
+ yield testAdvanceCharCommit(doc);
+ yield testAdvanceCharsFunction(doc);
+ yield testEscapeCancel(doc);
+
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function testMultipleInitialization(doc) {
+ doc.body.innerHTML = "";
+ let options = {};
+ let 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 <span> is hidden");
+ is(doc.querySelectorAll("input").length, 1, "Only one <input>");
+ is(doc.querySelectorAll("span").length, 2,
+ "Correct number of <span> 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");
+ let def = defer();
+
+ createInplaceEditorAndClick({
+ initial: "explicit initial",
+ start: function (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, def)
+ }, doc);
+
+ return def.promise;
+}
+
+function testBlurCommit(doc) {
+ info("Testing that bluring the field commits the new value");
+ let def = defer();
+
+ createInplaceEditorAndClick({
+ start: function (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, def)
+ }, doc, "Edit Me!");
+
+ return def.promise;
+}
+
+function testAdvanceCharCommit(doc) {
+ info("Testing that configured advanceChars commit the new value");
+ let def = defer();
+
+ createInplaceEditorAndClick({
+ advanceChars: ":",
+ start: function (editor) {
+ EventUtils.sendString("Test:");
+ },
+ done: onDone("Test", true, def)
+ }, doc);
+
+ return def.promise;
+}
+
+function testAdvanceCharsFunction(doc) {
+ info("Testing advanceChars as a function");
+ let def = defer();
+
+ let firstTime = true;
+
+ createInplaceEditorAndClick({
+ initial: "",
+ advanceChars: function (charCode, text, insertionPoint) {
+ if (charCode !== Components.interfaces.nsIDOMKeyEvent.DOM_VK_COLON) {
+ return false;
+ }
+ if (firstTime) {
+ firstTime = false;
+ return false;
+ }
+
+ // Just to make sure we check it somehow.
+ return text.length > 0;
+ },
+ start: function (editor) {
+ for (let ch of ":Test:") {
+ EventUtils.sendChar(ch);
+ }
+ },
+ done: onDone(":Test", true, def)
+ }, doc);
+
+ return def.promise;
+}
+
+function testEscapeCancel(doc) {
+ info("Testing that escape cancels the new value");
+ let def = defer();
+
+ createInplaceEditorAndClick({
+ initial: "initial text",
+ start: function (editor) {
+ editor.input.value = "Test Value";
+ EventUtils.sendKey("escape");
+ },
+ done: onDone("initial text", false, def)
+ }, doc);
+
+ return def.promise;
+}
+
+function onDone(value, isCommit, def) {
+ 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");
+ def.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 000000000..811c30123
--- /dev/null
+++ b/devtools/client/shared/test/browser_inplace-editor-02.js
@@ -0,0 +1,71 @@
+/* vim: set ts=2 et sw=2 tw=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(function* () {
+ yield addTab("data:text/html;charset=utf-8,inline editor tests");
+ let [host, , doc] = yield createHost();
+
+ yield testNonTrimmed(doc);
+ yield testTrimmed(doc);
+
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function testNonTrimmed(doc) {
+ info("Testing the trimOutput=false option");
+ let def = defer();
+
+ let initial = "\nMultiple\nLines\n";
+ let changed = " \nMultiple\nLines\n with more whitespace ";
+ createInplaceEditorAndClick({
+ trimOutput: false,
+ multiline: true,
+ initial: initial,
+ start: function (editor) {
+ is(editor.input.value, initial, "Explicit initial value should be used.");
+ editor.input.value = changed;
+ EventUtils.sendKey("return");
+ },
+ done: onDone(changed, true, def)
+ }, doc);
+
+ return def.promise;
+}
+
+function testTrimmed(doc) {
+ info("Testing the trimOutput=true option (default value)");
+ let def = defer();
+
+ let initial = "\nMultiple\nLines\n";
+ let changed = " \nMultiple\nLines\n with more whitespace ";
+ createInplaceEditorAndClick({
+ initial: initial,
+ multiline: true,
+ start: function (editor) {
+ is(editor.input.value, initial, "Explicit initial value should be used.");
+ editor.input.value = changed;
+ EventUtils.sendKey("return");
+ },
+ done: onDone(changed.trim(), true, def)
+ }, doc);
+
+ return def.promise;
+}
+
+function onDone(value, isCommit, def) {
+ 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");
+ def.resolve();
+ };
+}
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 000000000..e9ceb11ad
--- /dev/null
+++ b/devtools/client/shared/test/browser_inplace-editor_autocomplete_01.js
@@ -0,0 +1,75 @@
+/* vim: set ts=2 et sw=2 tw=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";
+
+const { InplaceEditor } = require("devtools/client/shared/inplace-editor");
+const { AutocompletePopup } = require("devtools/client/shared/autocomplete-popup");
+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 mockGetCSSPropertyList = function () {
+ return [
+ "background",
+ "border",
+ "box-sizing",
+ "color",
+ "display",
+ "visibility",
+ ];
+};
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," +
+ "inplace editor CSS property autocomplete");
+ let [host, win, doc] = yield createHost();
+
+ let xulDocument = win.top.document;
+ let popup = new AutocompletePopup(xulDocument, { autoSelect: true });
+ yield new Promise(resolve => {
+ createInplaceEditorAndClick({
+ start: runPropertyAutocompletionTest,
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY,
+ done: resolve,
+ popup: popup
+ }, doc);
+ });
+
+ popup.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+let runPropertyAutocompletionTest = Task.async(function* (editor) {
+ info("Starting to test for css property completion");
+ editor._getCSSPropertyList = mockGetCSSPropertyList;
+
+ for (let data of testData) {
+ yield 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 000000000..3100026b4
--- /dev/null
+++ b/devtools/client/shared/test/browser_inplace-editor_autocomplete_02.js
@@ -0,0 +1,80 @@
+/* vim: set ts=2 et sw=2 tw=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";
+
+const { InplaceEditor } = require("devtools/client/shared/inplace-editor");
+const { AutocompletePopup } = require("devtools/client/shared/autocomplete-popup");
+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 mockGetCSSValuesForPropertyName = function (propertyName) {
+ let values = {
+ "display": [
+ "block",
+ "flex",
+ "inline",
+ "inline-block",
+ "none",
+ ]
+ };
+ return values[propertyName] || [];
+};
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," +
+ "inplace editor CSS value autocomplete");
+ let [host, win, doc] = yield createHost();
+
+ let xulDocument = win.top.document;
+ let popup = new AutocompletePopup(xulDocument, { autoSelect: true });
+
+ yield new Promise(resolve => {
+ createInplaceEditorAndClick({
+ start: runAutocompletionTest,
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE,
+ property: {
+ name: "display"
+ },
+ done: resolve,
+ popup: popup
+ }, doc);
+ });
+
+ popup.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+let runAutocompletionTest = Task.async(function* (editor) {
+ info("Starting to test for css property completion");
+ editor._getCSSValuesForPropertyName = mockGetCSSValuesForPropertyName;
+
+ for (let data of testData) {
+ yield 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 000000000..5d1737bfd
--- /dev/null
+++ b/devtools/client/shared/test/browser_inplace-editor_autocomplete_offset.js
@@ -0,0 +1,119 @@
+/* vim: set ts=2 et sw=2 tw=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";
+
+const { InplaceEditor } = require("devtools/client/shared/inplace-editor");
+const { AutocompletePopup } = require("devtools/client/shared/autocomplete-popup");
+loadHelperScript("helper_inplace_editor.js");
+
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="resource://devtools/client/themes/common.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tooltip test">
+ </window>`;
+
+// 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 mockGetCSSPropertyList = function () {
+ return [
+ "clear",
+ "color",
+ "direction",
+ "display",
+ ];
+};
+
+const mockGetCSSValuesForPropertyName = function (propertyName) {
+ let values = {
+ "color": ["blue", "red"],
+ "display": ["block", "flex", "none"]
+ };
+ return values[propertyName] || [];
+};
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8,inplace editor CSS value autocomplete");
+ let [host,, doc] = yield createHost("bottom", TEST_URI);
+
+ let popup = new AutocompletePopup(doc, { autoSelect: true });
+
+ info("Create a CSS_MIXED type autocomplete");
+ yield new Promise(resolve => {
+ createInplaceEditorAndClick({
+ initial: "style=",
+ start: runAutocompletionTest,
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED,
+ done: resolve,
+ popup: popup
+ }, doc);
+ });
+
+ popup.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+let runAutocompletionTest = Task.async(function* (editor) {
+ info("Starting autocomplete test for inplace-editor popup offset");
+ editor._getCSSPropertyList = mockGetCSSPropertyList;
+ editor._getCSSValuesForPropertyName = mockGetCSSValuesForPropertyName;
+
+ let previousOffset = -1;
+ for (let 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.
+ let offset = getPopupOffset(editor);
+ ok(offset > previousOffset, "New popup offset is greater than the previous one");
+ previousOffset = offset;
+ } else {
+ yield 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}) {
+ let popupQuads = popup._panel.getBoxQuads({relativeTo: input});
+ return popupQuads[0].bounds.left;
+}
diff --git a/devtools/client/shared/test/browser_inplace-editor_maxwidth.js b/devtools/client/shared/test/browser_inplace-editor_maxwidth.js
new file mode 100644
index 000000000..205f4418e
--- /dev/null
+++ b/devtools/client/shared/test/browser_inplace-editor_maxwidth.js
@@ -0,0 +1,114 @@
+/* vim: set ts=2 et sw=2 tw=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");
+
+const MAX_WIDTH = 300;
+const START_TEXT = "Start text";
+const LONG_TEXT = "I am a long text and I will not fit in a 300px container. " +
+ "I expect the inplace editor to wrap.";
+
+// Test the inplace-editor behavior with a maxWidth configuration option
+// defined.
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8,inplace editor max width tests");
+ let [host, , doc] = yield createHost();
+
+ info("Testing the maxWidth option in pixels, to precisely check the size");
+ yield new Promise(resolve => {
+ createInplaceEditorAndClick({
+ multiline: true,
+ maxWidth: MAX_WIDTH,
+ start: testMaxWidth,
+ done: resolve
+ }, doc, START_TEXT);
+ });
+
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+let testMaxWidth = Task.async(function* (editor) {
+ is(editor.input.value, START_TEXT, "Span text content should be used");
+ ok(editor.input.offsetWidth < MAX_WIDTH,
+ "Input width should be strictly smaller than MAX_WIDTH");
+ is(getLines(editor.input), 1, "Input should display 1 line of text");
+
+ info("Check a text is on several lines if it does not fit MAX_WIDTH");
+ for (let key of LONG_TEXT) {
+ EventUtils.sendChar(key);
+ checkScrollbars(editor.input);
+ }
+
+ is(editor.input.value, LONG_TEXT, "Long text should be the input value");
+ is(editor.input.offsetWidth, MAX_WIDTH,
+ "Input width should be the same as MAX_WIDTH");
+ is(getLines(editor.input), 3, "Input should display 3 lines of text");
+ checkScrollbars(editor.input);
+
+ info("Delete all characters on line 3.");
+ while (getLines(editor.input) === 3) {
+ EventUtils.sendKey("BACK_SPACE");
+ checkScrollbars(editor.input);
+ }
+
+ is(editor.input.offsetWidth, MAX_WIDTH,
+ "Input width should be the same as MAX_WIDTH");
+ is(getLines(editor.input), 2, "Input should display 2 lines of text");
+ checkScrollbars(editor.input);
+
+ info("Delete all characters on line 2.");
+ while (getLines(editor.input) === 2) {
+ EventUtils.sendKey("BACK_SPACE");
+ checkScrollbars(editor.input);
+ }
+
+ is(getLines(editor.input), 1, "Input should display 1 line of text");
+ checkScrollbars(editor.input);
+
+ info("Delete all characters.");
+ while (editor.input.value !== "") {
+ EventUtils.sendKey("BACK_SPACE");
+ checkScrollbars(editor.input);
+ }
+
+ ok(editor.input.offsetWidth < MAX_WIDTH,
+ "Input width should again be strictly smaller than MAX_WIDTH");
+ ok(editor.input.offsetWidth > 0,
+ "Even with no content, the input has a non-zero width");
+ is(getLines(editor.input), 1, "Input should display 1 line of text");
+ checkScrollbars(editor.input);
+
+ info("Leave the inplace-editor");
+ EventUtils.sendKey("RETURN");
+});
+
+/**
+ * Retrieve the current number of lines displayed in the provided textarea.
+ *
+ * @param {DOMNode} textarea
+ * @return {Number} the number of lines
+ */
+function getLines(textarea) {
+ let win = textarea.ownerDocument.defaultView;
+ let style = win.getComputedStyle(textarea);
+ let lineHeight = style.getPropertyCSSValue("line-height").cssText;
+ return Math.floor(textarea.clientHeight / parseFloat(lineHeight));
+}
+
+/**
+ * Verify that the provided textarea has no vertical or horizontal scrollbar.
+ *
+ * @param {DOMNode} textarea
+ */
+function checkScrollbars(textarea) {
+ is(textarea.scrollHeight, textarea.clientHeight,
+ "Textarea should never have vertical scrollbars");
+ is(textarea.scrollWidth, textarea.clientWidth,
+ "Textarea should never have horizontal scrollbars");
+}
diff --git a/devtools/client/shared/test/browser_key_shortcuts.js b/devtools/client/shared/test/browser_key_shortcuts.js
new file mode 100644
index 000000000..c88782f85
--- /dev/null
+++ b/devtools/client/shared/test/browser_key_shortcuts.js
@@ -0,0 +1,425 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var isOSX = Services.appinfo.OS === "Darwin";
+
+add_task(function* () {
+ let shortcuts = new KeyShortcuts({
+ window
+ });
+
+ yield testSimple(shortcuts);
+ yield testNonLetterCharacter(shortcuts);
+ yield testPlusCharacter(shortcuts);
+ yield testFunctionKey(shortcuts);
+ yield testMixup(shortcuts);
+ yield testLooseDigits(shortcuts);
+ yield testExactModifiers(shortcuts);
+ yield testLooseShiftModifier(shortcuts);
+ yield testStrictLetterShiftModifier(shortcuts);
+ yield testAltModifier(shortcuts);
+ yield testCommandOrControlModifier(shortcuts);
+ yield testCtrlModifier(shortcuts);
+ yield testInvalidShortcutString(shortcuts);
+ yield testCmdShiftShortcut(shortcuts);
+ shortcuts.destroy();
+
+ yield testTarget();
+});
+
+// Test helper to listen to the next key press for a given key,
+// returning a promise to help using Tasks.
+function once(shortcuts, key, listener) {
+ let called = false;
+ return new Promise(done => {
+ let onShortcut = (key2, event) => {
+ shortcuts.off(key, onShortcut);
+ ok(!called, "once listener called only once (i.e. off() works)");
+ is(key, key2, "listener first argument match the key we listen");
+ called = true;
+ listener(key2, event);
+ done();
+ };
+ shortcuts.on(key, onShortcut);
+ });
+}
+
+function* testSimple(shortcuts) {
+ info("Test simple key shortcuts");
+
+ let onKey = once(shortcuts, "0", (key, event) => {
+ is(event.key, "0");
+
+ // Display another key press to ensure that once() correctly stop listening
+ EventUtils.synthesizeKey("0", {}, window);
+ });
+
+ EventUtils.synthesizeKey("0", {}, window);
+ yield onKey;
+}
+
+function* testNonLetterCharacter(shortcuts) {
+ info("Test non-naive character key shortcuts");
+
+ let onKey = once(shortcuts, "[", (key, event) => {
+ is(event.key, "[");
+ });
+
+ EventUtils.synthesizeKey("[", {}, window);
+ yield onKey;
+}
+
+function* testFunctionKey(shortcuts) {
+ info("Test function key shortcuts");
+
+ let onKey = once(shortcuts, "F12", (key, event) => {
+ is(event.key, "F12");
+ });
+
+ EventUtils.synthesizeKey("F12", { keyCode: 123 }, window);
+ yield onKey;
+}
+
+// Plus is special. It's keycode is the one for "=". That's because it requires
+// shift to be pressed and is behind "=" key. So it should be considered as a
+// character key
+function* testPlusCharacter(shortcuts) {
+ info("Test 'Plus' key shortcuts");
+
+ let onKey = once(shortcuts, "Plus", (key, event) => {
+ is(event.key, "+");
+ });
+
+ EventUtils.synthesizeKey("+", { keyCode: 61, shiftKey: true }, window);
+ yield onKey;
+}
+
+// Test they listeners are not mixed up between shortcuts
+function* testMixup(shortcuts) {
+ info("Test possible listener mixup");
+
+ let hitFirst = false, hitSecond = false;
+ let onFirstKey = once(shortcuts, "0", (key, event) => {
+ is(event.key, "0");
+ hitFirst = true;
+ });
+ let onSecondKey = once(shortcuts, "Alt+A", (key, event) => {
+ is(event.key, "a");
+ ok(event.altKey);
+ hitSecond = true;
+ });
+
+ // Dispatch the first shortcut and expect only this one to be notified
+ ok(!hitFirst, "First shortcut isn't notified before firing the key event");
+ EventUtils.synthesizeKey("0", {}, window);
+ yield onFirstKey;
+ ok(hitFirst, "Got the first shortcut notified");
+ ok(!hitSecond, "No mixup, second shortcut is still not notified (1/2)");
+
+ // Wait an extra time, just to be sure this isn't racy
+ yield new Promise(done => {
+ window.setTimeout(done, 0);
+ });
+ ok(!hitSecond, "No mixup, second shortcut is still not notified (2/2)");
+
+ // Finally dispatch the second shortcut
+ EventUtils.synthesizeKey("a", { altKey: true }, window);
+ yield onSecondKey;
+ ok(hitSecond, "Got the second shortcut notified once it is actually fired");
+}
+
+// On azerty keyboard, digits are only available by pressing Shift/Capslock,
+// but we accept them even if we omit doing that.
+function* testLooseDigits(shortcuts) {
+ info("Test Loose digits");
+ let onKey = once(shortcuts, "0", (key, event) => {
+ is(event.key, "à");
+ ok(!event.altKey);
+ ok(!event.ctrlKey);
+ ok(!event.metaKey);
+ ok(!event.shiftKey);
+ });
+ // Simulate a press on the "0" key, without shift pressed on a french
+ // keyboard
+ EventUtils.synthesizeKey(
+ "à",
+ { keyCode: 48 },
+ window);
+ yield onKey;
+
+ onKey = once(shortcuts, "0", (key, event) => {
+ is(event.key, "0");
+ ok(!event.altKey);
+ ok(!event.ctrlKey);
+ ok(!event.metaKey);
+ ok(event.shiftKey);
+ });
+ // Simulate the same press with shift pressed
+ EventUtils.synthesizeKey(
+ "0",
+ { keyCode: 48, shiftKey: true },
+ window);
+ yield onKey;
+}
+
+// Test that shortcuts is notified only when the modifiers match exactly
+function* testExactModifiers(shortcuts) {
+ info("Test exact modifiers match");
+
+ let hit = false;
+ let onKey = once(shortcuts, "Alt+A", (key, event) => {
+ is(event.key, "a");
+ ok(event.altKey);
+ ok(!event.ctrlKey);
+ ok(!event.metaKey);
+ ok(!event.shiftKey);
+ hit = true;
+ });
+
+ // Dispatch with unexpected set of modifiers
+ ok(!hit, "Shortcut isn't notified before firing the key event");
+ EventUtils.synthesizeKey("a",
+ { accelKey: true, altKey: true, shiftKey: true },
+ window);
+ EventUtils.synthesizeKey(
+ "a",
+ { accelKey: true, altKey: false, shiftKey: false },
+ window);
+ EventUtils.synthesizeKey(
+ "a",
+ { accelKey: false, altKey: false, shiftKey: true },
+ window);
+ EventUtils.synthesizeKey(
+ "a",
+ { accelKey: false, altKey: false, shiftKey: false },
+ window);
+
+ // Wait an extra time to let a chance to call the listener
+ yield new Promise(done => {
+ window.setTimeout(done, 0);
+ });
+ ok(!hit, "Listener isn't called when modifiers aren't exactly matching");
+
+ // Dispatch the expected modifiers
+ EventUtils.synthesizeKey("a", { accelKey: false, altKey: true, shiftKey: false},
+ window);
+ yield onKey;
+ ok(hit, "Got shortcut notified once it is actually fired");
+}
+
+// Some keys are only accessible via shift and listener should also be called
+// even if the key didn't explicitely requested Shift modifier.
+// For example, `%` on french keyboards is only accessible via Shift.
+// Same thing for `@` on US keybords.
+function* testLooseShiftModifier(shortcuts) {
+ info("Test Loose shift modifier");
+ let onKey = once(shortcuts, "%", (key, event) => {
+ is(event.key, "%");
+ ok(!event.altKey);
+ ok(!event.ctrlKey);
+ ok(!event.metaKey);
+ ok(event.shiftKey);
+ });
+ EventUtils.synthesizeKey(
+ "%",
+ { accelKey: false, altKey: false, ctrlKey: false, shiftKey: true},
+ window);
+ yield onKey;
+
+ onKey = once(shortcuts, "@", (key, event) => {
+ is(event.key, "@");
+ ok(!event.altKey);
+ ok(!event.ctrlKey);
+ ok(!event.metaKey);
+ ok(event.shiftKey);
+ });
+ EventUtils.synthesizeKey(
+ "@",
+ { accelKey: false, altKey: false, ctrlKey: false, shiftKey: true},
+ window);
+ yield onKey;
+}
+
+// But Shift modifier is strict on all letter characters (a to Z)
+function* testStrictLetterShiftModifier(shortcuts) {
+ info("Test strict shift modifier on letters");
+ let hitFirst = false;
+ let onKey = once(shortcuts, "a", (key, event) => {
+ is(event.key, "a");
+ ok(!event.altKey);
+ ok(!event.ctrlKey);
+ ok(!event.metaKey);
+ ok(!event.shiftKey);
+ hitFirst = true;
+ });
+ let onShiftKey = once(shortcuts, "Shift+a", (key, event) => {
+ is(event.key, "a");
+ ok(!event.altKey);
+ ok(!event.ctrlKey);
+ ok(!event.metaKey);
+ ok(event.shiftKey);
+ });
+ EventUtils.synthesizeKey(
+ "a",
+ { shiftKey: true},
+ window);
+ yield onShiftKey;
+ ok(!hitFirst, "Didn't fire the explicit shift+a");
+
+ EventUtils.synthesizeKey(
+ "a",
+ { shiftKey: false},
+ window);
+ yield onKey;
+}
+
+function* testAltModifier(shortcuts) {
+ info("Test Alt modifier");
+ let onKey = once(shortcuts, "Alt+F1", (key, event) => {
+ is(event.keyCode, window.KeyboardEvent.DOM_VK_F1);
+ ok(event.altKey);
+ ok(!event.ctrlKey);
+ ok(!event.metaKey);
+ ok(!event.shiftKey);
+ });
+ EventUtils.synthesizeKey(
+ "VK_F1",
+ { altKey: true },
+ window);
+ yield onKey;
+}
+
+function* testCommandOrControlModifier(shortcuts) {
+ info("Test CommandOrControl modifier");
+ let onKey = once(shortcuts, "CommandOrControl+F1", (key, event) => {
+ is(event.keyCode, window.KeyboardEvent.DOM_VK_F1);
+ ok(!event.altKey);
+ if (isOSX) {
+ ok(!event.ctrlKey);
+ ok(event.metaKey);
+ } else {
+ ok(event.ctrlKey);
+ ok(!event.metaKey);
+ }
+ ok(!event.shiftKey);
+ });
+ let onKeyAlias = once(shortcuts, "CmdOrCtrl+F1", (key, event) => {
+ is(event.keyCode, window.KeyboardEvent.DOM_VK_F1);
+ ok(!event.altKey);
+ if (isOSX) {
+ ok(!event.ctrlKey);
+ ok(event.metaKey);
+ } else {
+ ok(event.ctrlKey);
+ ok(!event.metaKey);
+ }
+ ok(!event.shiftKey);
+ });
+ if (isOSX) {
+ EventUtils.synthesizeKey(
+ "VK_F1",
+ { metaKey: true },
+ window);
+ } else {
+ EventUtils.synthesizeKey(
+ "VK_F1",
+ { ctrlKey: true },
+ window);
+ }
+ yield onKey;
+ yield onKeyAlias;
+}
+
+function* testCtrlModifier(shortcuts) {
+ info("Test Ctrl modifier");
+ let onKey = once(shortcuts, "Ctrl+F1", (key, event) => {
+ is(event.keyCode, window.KeyboardEvent.DOM_VK_F1);
+ ok(!event.altKey);
+ ok(event.ctrlKey);
+ ok(!event.metaKey);
+ ok(!event.shiftKey);
+ });
+ let onKeyAlias = once(shortcuts, "Control+F1", (key, event) => {
+ is(event.keyCode, window.KeyboardEvent.DOM_VK_F1);
+ ok(!event.altKey);
+ ok(event.ctrlKey);
+ ok(!event.metaKey);
+ ok(!event.shiftKey);
+ });
+ EventUtils.synthesizeKey(
+ "VK_F1",
+ { ctrlKey: true },
+ window);
+ yield onKey;
+ yield onKeyAlias;
+}
+
+function* testCmdShiftShortcut(shortcuts) {
+ if (!isOSX) {
+ // This test is OSX only (Bug 1300458).
+ return;
+ }
+
+ let onCmdKey = once(shortcuts, "CmdOrCtrl+[", (key, event) => {
+ is(event.key, "[");
+ ok(!event.altKey);
+ ok(!event.ctrlKey);
+ ok(event.metaKey);
+ ok(!event.shiftKey);
+ });
+ let onCmdShiftKey = once(shortcuts, "CmdOrCtrl+Shift+[", (key, event) => {
+ is(event.key, "[");
+ ok(!event.altKey);
+ ok(!event.ctrlKey);
+ ok(event.metaKey);
+ ok(event.shiftKey);
+ });
+
+ EventUtils.synthesizeKey(
+ "[",
+ { metaKey: true, shiftKey: true },
+ window);
+ EventUtils.synthesizeKey(
+ "[",
+ { metaKey: true },
+ window);
+
+ yield onCmdKey;
+ yield onCmdShiftKey;
+}
+
+function* testTarget() {
+ info("Test KeyShortcuts with target argument");
+
+ let target = document.createElementNS("http://www.w3.org/1999/xhtml",
+ "input");
+ document.documentElement.appendChild(target);
+ target.focus();
+
+ let shortcuts = new KeyShortcuts({
+ window,
+ target
+ });
+ let onKey = once(shortcuts, "0", (key, event) => {
+ is(event.key, "0");
+ is(event.target, target);
+ });
+ EventUtils.synthesizeKey("0", {}, window);
+ yield onKey;
+
+ target.remove();
+
+ shortcuts.destroy();
+}
+
+function testInvalidShortcutString(shortcuts) {
+ info("Test wrong shortcut string");
+
+ let shortcut = KeyShortcuts.parseElectronKey(window, "Cmmd+F");
+ ok(!shortcut, "Passing a invalid shortcut string should return a null object");
+
+ shortcuts.on("Cmmd+F", function () {});
+ ok(true, "on() shouldn't throw when passing invalid shortcut string");
+}
diff --git a/devtools/client/shared/test/browser_keycodes.js b/devtools/client/shared/test/browser_keycodes.js
new file mode 100644
index 000000000..9e6b4a4ee
--- /dev/null
+++ b/devtools/client/shared/test/browser_keycodes.js
@@ -0,0 +1,12 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+
+add_task(function* () {
+ for (let key in KeyCodes) {
+ is(KeyCodes[key], Ci.nsIDOMKeyEvent[key], "checking value for " + key);
+ }
+});
diff --git a/devtools/client/shared/test/browser_layoutHelpers-getBoxQuads.html b/devtools/client/shared/test/browser_layoutHelpers-getBoxQuads.html
new file mode 100644
index 000000000..070792b9a
--- /dev/null
+++ b/devtools/client/shared/test/browser_layoutHelpers-getBoxQuads.html
@@ -0,0 +1,65 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Layout Helpers</title>
+<style id="styles">
+ body {
+ margin: 0;
+ padding: 0;
+ }
+
+ #hidden-node {
+ display: none;
+ }
+
+ #simple-node-with-margin-padding-border {
+ width: 200px;
+ height: 200px;
+ background: #f06;
+
+ padding: 20px;
+ margin: 50px;
+ border: 10px solid black;
+ }
+
+ #scrolled-node {
+ position: absolute;
+ top: 0;
+ left: 0;
+
+ width: 300px;
+ height: 100px;
+ overflow: scroll;
+ background: linear-gradient(red, pink);
+ }
+
+ #sub-scrolled-node {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+ background: linear-gradient(yellow, green);
+ }
+
+ #inner-scrolled-node {
+ width: 100px;
+ height: 400px;
+ background: linear-gradient(black, white);
+ }
+</style>
+<div id="hidden-node"></div>
+<div id="simple-node-with-margin-padding-border"></div>
+<!-- The inline encoded code below corresponds to:
+<iframe style="margin:10px;border:0;width:300px;height:300px;">
+ <iframe style="margin:10px;border:0;width:200px;height:200px;">
+ <div id="inner-node" style="width:100px;height:100px;border:10px solid red;margin:10px;padding:10px;"></div>
+ </iframe>
+</iframe>
+ -->
+<iframe src="data:text/html,%3Cstyle%3Ebody%7Bmargin:0;padding:0;%7D%3C/style%3E%3Ciframe%20src=%22data:text/html,%253Cstyle%253Ebody%257Bmargin:0;padding:0;%257D%253C/style%253E%253Cdiv%2520id='inner-node'%2520style='width:100px;height:100px;border:10px%2520solid%2520red;margin:10px;padding:10px;'%253E%253C/div%253E%22%20style=%22margin:10px;border:0;width:200px;height:200px;%22%3E%3C/iframe%3E" style="margin:10px;border:0;width:300px;height:300px;"></iframe>
+<div id="scrolled-node">
+ <div id="sub-scrolled-node">
+ <div id="inner-scrolled-node"></div>
+ </div>
+</div>
+<span id="inline">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus porttitor luctus sem id scelerisque. Cras quis velit sed risus euismod lacinia. Donec viverra enim eu ligula efficitur, quis vulputate metus cursus. Duis sed interdum risus. Ut blandit velit vitae faucibus efficitur. Lorem ipsum dolor sit amet, consectetur adipiscing elit.<br/ >
+Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Sed vitae dolor metus. Aliquam sed velit sit amet libero vestibulum aliquam vel a lorem. Integer eget ex eget justo auctor ullamcorper.<br/ >
+Praesent tristique maximus lacus, nec ultricies neque ultrices non. Phasellus vel lobortis justo. </span> \ No newline at end of file
diff --git a/devtools/client/shared/test/browser_layoutHelpers-getBoxQuads.js b/devtools/client/shared/test/browser_layoutHelpers-getBoxQuads.js
new file mode 100644
index 000000000..221127a11
--- /dev/null
+++ b/devtools/client/shared/test/browser_layoutHelpers-getBoxQuads.js
@@ -0,0 +1,219 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests getAdjustedQuads works properly in a variety of use cases including
+// iframes, scroll and zoom
+
+"use strict";
+
+const {getAdjustedQuads} = require("devtools/shared/layout/utils");
+
+const TEST_URI = TEST_URI_ROOT + "browser_layoutHelpers-getBoxQuads.html";
+
+add_task(function* () {
+ let tab = yield addTab(TEST_URI);
+ let doc = tab.linkedBrowser.contentDocument;
+
+ ok(typeof getAdjustedQuads === "function", "getAdjustedQuads is defined");
+
+ info("Running tests");
+
+ returnsTheRightDataStructure(doc);
+ isEmptyForMissingNode(doc);
+ isEmptyForHiddenNodes(doc);
+ defaultsToBorderBoxIfNoneProvided(doc);
+ returnsLikeGetBoxQuadsInSimpleCase(doc);
+ takesIframesOffsetsIntoAccount(doc);
+ takesScrollingIntoAccount(doc);
+ yield takesZoomIntoAccount(doc);
+ returnsMultipleItemsForWrappingInlineElements(doc);
+
+ gBrowser.removeCurrentTab();
+});
+
+function returnsTheRightDataStructure(doc) {
+ info("Checks that the returned data contains bounds and 4 points");
+
+ let node = doc.querySelector("body");
+ let [res] = getAdjustedQuads(doc.defaultView, node, "content");
+
+ ok("bounds" in res, "The returned data has a bounds property");
+ ok("p1" in res, "The returned data has a p1 property");
+ ok("p2" in res, "The returned data has a p2 property");
+ ok("p3" in res, "The returned data has a p3 property");
+ ok("p4" in res, "The returned data has a p4 property");
+
+ for (let boundProp of
+ ["bottom", "top", "right", "left", "width", "height", "x", "y"]) {
+ ok(boundProp in res.bounds, "The bounds has a " + boundProp + " property");
+ }
+
+ for (let point of ["p1", "p2", "p3", "p4"]) {
+ for (let pointProp of ["x", "y", "z", "w"]) {
+ ok(pointProp in res[point], point + " has a " + pointProp + " property");
+ }
+ }
+}
+
+function isEmptyForMissingNode(doc) {
+ info("Checks that null is returned for invalid nodes");
+
+ for (let input of [null, undefined, "", 0]) {
+ is(getAdjustedQuads(doc.defaultView, input).length, 0,
+ "A 0-length array is returned for input " + input);
+ }
+}
+
+function isEmptyForHiddenNodes(doc) {
+ info("Checks that null is returned for nodes that aren't rendered");
+
+ let style = doc.querySelector("#styles");
+ is(getAdjustedQuads(doc.defaultView, style).length, 0,
+ "null is returned for a <style> node");
+
+ let hidden = doc.querySelector("#hidden-node");
+ is(getAdjustedQuads(doc.defaultView, hidden).length, 0,
+ "null is returned for a hidden node");
+}
+
+function defaultsToBorderBoxIfNoneProvided(doc) {
+ info("Checks that if no boxtype is passed, then border is the default one");
+
+ let node = doc.querySelector("#simple-node-with-margin-padding-border");
+ let [withBoxType] = getAdjustedQuads(doc.defaultView, node, "border");
+ let [withoutBoxType] = getAdjustedQuads(doc.defaultView, node);
+
+ for (let boundProp of
+ ["bottom", "top", "right", "left", "width", "height", "x", "y"]) {
+ is(withBoxType.bounds[boundProp], withoutBoxType.bounds[boundProp],
+ boundProp + " bound is equal with or without the border box type");
+ }
+
+ for (let point of ["p1", "p2", "p3", "p4"]) {
+ for (let pointProp of ["x", "y", "z", "w"]) {
+ is(withBoxType[point][pointProp], withoutBoxType[point][pointProp],
+ point + "." + pointProp +
+ " is equal with or without the border box type");
+ }
+ }
+}
+
+function returnsLikeGetBoxQuadsInSimpleCase(doc) {
+ info("Checks that for an element in the main frame, without scroll nor zoom" +
+ "that the returned value is similar to the returned value of getBoxQuads");
+
+ let node = doc.querySelector("#simple-node-with-margin-padding-border");
+
+ for (let region of ["content", "padding", "border", "margin"]) {
+ let expected = node.getBoxQuads({
+ box: region
+ })[0];
+ let [actual] = getAdjustedQuads(doc.defaultView, node, region);
+
+ for (let boundProp of
+ ["bottom", "top", "right", "left", "width", "height", "x", "y"]) {
+ is(actual.bounds[boundProp], expected.bounds[boundProp],
+ boundProp + " bound is equal to the one returned by getBoxQuads for " +
+ region + " box");
+ }
+
+ for (let point of ["p1", "p2", "p3", "p4"]) {
+ for (let pointProp of ["x", "y", "z", "w"]) {
+ is(actual[point][pointProp], expected[point][pointProp],
+ point + "." + pointProp +
+ " is equal to the one returned by getBoxQuads for " + region + " box");
+ }
+ }
+ }
+}
+
+function takesIframesOffsetsIntoAccount(doc) {
+ info("Checks that the quad returned for a node inside iframes that have " +
+ "margins takes those offsets into account");
+
+ let rootIframe = doc.querySelector("iframe");
+ let subIframe = rootIframe.contentDocument.querySelector("iframe");
+ let innerNode = subIframe.contentDocument.querySelector("#inner-node");
+
+ let [quad] = getAdjustedQuads(doc.defaultView, innerNode, "content");
+
+ // rootIframe margin + subIframe margin + node margin + node border + node padding
+ let p1x = 10 + 10 + 10 + 10 + 10;
+ is(quad.p1.x, p1x, "The inner node's p1 x position is correct");
+
+ // Same as p1x + the inner node width
+ let p2x = p1x + 100;
+ is(quad.p2.x, p2x, "The inner node's p2 x position is correct");
+}
+
+function takesScrollingIntoAccount(doc) {
+ info("Checks that the quad returned for a node inside multiple scrolled " +
+ "containers takes the scroll values into account");
+
+ // For info, the container being tested here is absolutely positioned at 0 0
+ // to simplify asserting the coordinates
+
+ info("Scroll the container nodes down");
+ let scrolledNode = doc.querySelector("#scrolled-node");
+ scrolledNode.scrollTop = 100;
+ let subScrolledNode = doc.querySelector("#sub-scrolled-node");
+ subScrolledNode.scrollTop = 200;
+ let innerNode = doc.querySelector("#inner-scrolled-node");
+
+ let [quad] = getAdjustedQuads(doc.defaultView, innerNode, "content");
+ is(quad.p1.x, 0, "p1.x of the scrolled node is correct after scrolling down");
+ is(quad.p1.y, -300, "p1.y of the scrolled node is correct after scrolling down");
+
+ info("Scrolling back up");
+ scrolledNode.scrollTop = 0;
+ subScrolledNode.scrollTop = 0;
+
+ [quad] = getAdjustedQuads(doc.defaultView, innerNode, "content");
+ is(quad.p1.x, 0, "p1.x of the scrolled node is correct after scrolling up");
+ is(quad.p1.y, 0, "p1.y of the scrolled node is correct after scrolling up");
+}
+
+function* takesZoomIntoAccount(doc) {
+ info("Checks that if the page is zoomed in/out, the quad returned is correct");
+
+ // Hard-coding coordinates in this zoom test is a bad idea as it can vary
+ // depending on the platform, so we simply test that zooming in produces a
+ // bigger quad and zooming out produces a smaller quad
+
+ let node = doc.querySelector("#simple-node-with-margin-padding-border");
+ let [defaultQuad] = getAdjustedQuads(doc.defaultView, node);
+
+ info("Zoom in");
+ window.FullZoom.enlarge();
+ let [zoomedInQuad] = getAdjustedQuads(doc.defaultView, node);
+
+ ok(zoomedInQuad.bounds.width > defaultQuad.bounds.width,
+ "The zoomed in quad is bigger than the default one");
+ ok(zoomedInQuad.bounds.height > defaultQuad.bounds.height,
+ "The zoomed in quad is bigger than the default one");
+
+ info("Zoom out");
+ yield window.FullZoom.reset();
+ window.FullZoom.reduce();
+ let [zoomedOutQuad] = getAdjustedQuads(doc.defaultView, node);
+
+ ok(zoomedOutQuad.bounds.width < defaultQuad.bounds.width,
+ "The zoomed out quad is smaller than the default one");
+ ok(zoomedOutQuad.bounds.height < defaultQuad.bounds.height,
+ "The zoomed out quad is smaller than the default one");
+
+ yield window.FullZoom.reset();
+}
+
+function returnsMultipleItemsForWrappingInlineElements(doc) {
+ info("Checks that several quads are returned " +
+ "for inline elements that span line-breaks");
+
+ let node = doc.querySelector("#inline");
+ let quads = getAdjustedQuads(doc.defaultView, node, "content");
+ // At least 3 because of the 2 <br />, maybe more depending on the window size.
+ ok(quads.length >= 3, "Multiple quads were returned");
+
+ is(quads.length, node.getBoxQuads().length,
+ "The same number of boxes as getBoxQuads was returned");
+}
diff --git a/devtools/client/shared/test/browser_layoutHelpers.html b/devtools/client/shared/test/browser_layoutHelpers.html
new file mode 100644
index 000000000..f50bfffdf
--- /dev/null
+++ b/devtools/client/shared/test/browser_layoutHelpers.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<meta charset=utf-8>
+<title> Layout Helpers </title>
+
+<style>
+ html {
+ height: 300%;
+ width: 300%;
+ }
+ div#some {
+ position: absolute;
+ background: black;
+ width: 2px;
+ height: 2px;
+ }
+ iframe {
+ position: absolute;
+ width: 40px;
+ height: 40px;
+ border: 0;
+ }
+</style>
+
+<div id=some></div>
diff --git a/devtools/client/shared/test/browser_layoutHelpers.js b/devtools/client/shared/test/browser_layoutHelpers.js
new file mode 100644
index 000000000..3274ad686
--- /dev/null
+++ b/devtools/client/shared/test/browser_layoutHelpers.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that scrollIntoViewIfNeeded works properly.
+const {scrollIntoViewIfNeeded} = require("devtools/client/shared/scroll");
+
+const TEST_URI = TEST_URI_ROOT + "browser_layoutHelpers.html";
+
+add_task(function* () {
+ let [host, win] = yield createHost("bottom", TEST_URI);
+ runTest(win);
+ host.destroy();
+});
+
+function runTest(win) {
+ let some = win.document.getElementById("some");
+
+ some.style.top = win.innerHeight + "px";
+ some.style.left = win.innerWidth + "px";
+ // The tests start with a black 2x2 pixels square below bottom right.
+ // Do not resize the window during the tests.
+
+ let xPos = Math.floor(win.innerWidth / 2);
+ // Above the viewport.
+ win.scroll(xPos, win.innerHeight + 2);
+ scrollIntoViewIfNeeded(some);
+ is(win.scrollY, Math.floor(win.innerHeight / 2) + 1,
+ "Element completely hidden above should appear centered.");
+ is(win.scrollX, xPos,
+ "scrollX position has not changed.");
+
+ // On the top edge.
+ win.scroll(win.innerWidth / 2, win.innerHeight + 1);
+ scrollIntoViewIfNeeded(some);
+ is(win.scrollY, win.innerHeight,
+ "Element partially visible above should appear above.");
+ is(win.scrollX, xPos,
+ "scrollX position has not changed.");
+
+ // Just below the viewport.
+ win.scroll(win.innerWidth / 2, 0);
+ scrollIntoViewIfNeeded(some);
+ is(win.scrollY, Math.floor(win.innerHeight / 2) + 1,
+ "Element completely hidden below should appear centered.");
+ is(win.scrollX, xPos,
+ "scrollX position has not changed.");
+
+ // On the bottom edge.
+ win.scroll(win.innerWidth / 2, 1);
+ scrollIntoViewIfNeeded(some);
+ is(win.scrollY, 2,
+ "Element partially visible below should appear below.");
+ is(win.scrollX, xPos,
+ "scrollX position has not changed.");
+
+ // Above the viewport.
+ win.scroll(win.innerWidth / 2, win.innerHeight + 2);
+ scrollIntoViewIfNeeded(some, false);
+ is(win.scrollY, win.innerHeight,
+ "Element completely hidden above should appear above " +
+ "if parameter is false.");
+ is(win.scrollX, xPos,
+ "scrollX position has not changed.");
+
+ // On the top edge.
+ win.scroll(win.innerWidth / 2, win.innerHeight + 1);
+ scrollIntoViewIfNeeded(some, false);
+ is(win.scrollY, win.innerHeight,
+ "Element partially visible above should appear above " +
+ "if parameter is false.");
+ is(win.scrollX, xPos,
+ "scrollX position has not changed.");
+
+ // Below the viewport.
+ win.scroll(win.innerWidth / 2, 0);
+ scrollIntoViewIfNeeded(some, false);
+ is(win.scrollY, 2,
+ "Element completely hidden below should appear below " +
+ "if parameter is false.");
+ is(win.scrollX, xPos,
+ "scrollX position has not changed.");
+
+ // On the bottom edge.
+ win.scroll(win.innerWidth / 2, 1);
+ scrollIntoViewIfNeeded(some, false);
+ is(win.scrollY, 2,
+ "Element partially visible below should appear below " +
+ "if parameter is false.");
+ is(win.scrollX, xPos,
+ "scrollX position has not changed.");
+}
diff --git a/devtools/client/shared/test/browser_mdn-docs-01.js b/devtools/client/shared/test/browser_mdn-docs-01.js
new file mode 100644
index 000000000..6490dfef7
--- /dev/null
+++ b/devtools/client/shared/test/browser_mdn-docs-01.js
@@ -0,0 +1,168 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests the MdnDocsWidget object, and specifically its
+ * loadCssDocs() function.
+ *
+ * The MdnDocsWidget is initialized with a document which has a specific
+ * structure. You then call loadCssDocs(), passing in a CSS property name.
+ * MdnDocsWidget then fetches docs for that property by making an XHR to
+ * a docs page, and loads the results into the document. While the XHR is
+ * still not resolved the document is put into an "initializing" state in
+ * which the devtools throbber is displayed.
+ *
+ * In this file we test:
+ * - the initial state of the document before the docs have loaded
+ * - the state of the document after the docs have loaded
+ */
+
+"use strict";
+
+const {setBaseCssDocsUrl, MdnDocsWidget} = require("devtools/client/shared/widgets/MdnDocsWidget");
+
+/**
+ * Test properties
+ *
+ * In the real tooltip, a CSS property name is used to look up an MDN page
+ * for that property.
+ * In the test code, the names defined here is used to look up a page
+ * served by the test server.
+ */
+const BASIC_TESTING_PROPERTY = "html-mdn-css-basic-testing.html";
+
+const BASIC_EXPECTED_SUMMARY = "A summary of the property.";
+const BASIC_EXPECTED_SYNTAX = [{type: "comment", text: "/* The part we want */"},
+ {type: "text", text: "\n"},
+ {type: "property-name", text: "this"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "is-the-part-we-want"},
+ {type: "text", text: ";"}];
+
+const URI_PARAMS =
+ "?utm_source=mozilla&utm_medium=firefox-inspector&utm_campaign=default";
+
+add_task(function* () {
+ setBaseCssDocsUrl(TEST_URI_ROOT);
+
+ yield addTab("about:blank");
+ let [host, win] = yield createHost("bottom", "data:text/html," +
+ "<div class='mdn-container'></div>");
+ let widget = new MdnDocsWidget(win.document.querySelector("div"));
+
+ yield testTheBasics(widget);
+
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+/**
+ * Test all the basics
+ * - initial content, before docs have loaded, is as expected
+ * - throbber is set before docs have loaded
+ * - contents are as expected after docs have loaded
+ * - throbber is gone after docs have loaded
+ * - mdn link text is correct and onclick behavior is correct
+ */
+function* testTheBasics(widget) {
+ info("Test all the basic functionality in the widget");
+
+ info("Get the widget state before docs have loaded");
+ let promise = widget.loadCssDocs(BASIC_TESTING_PROPERTY);
+
+ info("Check initial contents before docs have loaded");
+ checkTooltipContents(widget.elements, {
+ propertyName: BASIC_TESTING_PROPERTY,
+ summary: "",
+ syntax: ""
+ });
+
+ // throbber is set
+ ok(widget.elements.info.classList.contains("devtools-throbber"),
+ "Throbber is set");
+
+ info("Now let the widget finish loading");
+ yield promise;
+
+ info("Check contents after docs have loaded");
+ checkTooltipContents(widget.elements, {
+ propertyName: BASIC_TESTING_PROPERTY,
+ summary: BASIC_EXPECTED_SUMMARY,
+ syntax: BASIC_EXPECTED_SYNTAX
+ });
+
+ // throbber is gone
+ ok(!widget.elements.info.classList.contains("devtools-throbber"),
+ "Throbber is not set");
+
+ info("Check that MDN link text is correct and onclick behavior is correct");
+
+ let mdnLink = widget.elements.linkToMdn;
+ let expectedHref = TEST_URI_ROOT + BASIC_TESTING_PROPERTY + URI_PARAMS;
+ is(mdnLink.href, expectedHref, "MDN link href is correct");
+
+ let uri = yield checkLinkClick(mdnLink);
+ is(uri, expectedHref, "New tab opened with the expected URI");
+}
+
+ /**
+ * Clicking the "Visit MDN Page" in the tooltip panel
+ * should open a new browser tab with the page loaded.
+ *
+ * To test this we'll listen for a new tab opening, and
+ * when it does, add a listener to that new tab to tell
+ * us when it has loaded.
+ *
+ * Then we click the link.
+ *
+ * In the tab's load listener, we'll resolve the promise
+ * with the URI, which is expected to match the href
+ * in the orginal link.
+ *
+ * One complexity is that when you open a new tab,
+ * "about:blank" is first loaded into the tab before the
+ * actual page. So we ignore that first load event, and keep
+ * listening until "load" is triggered for a different URI.
+ */
+function checkLinkClick(link) {
+ function loadListener(tab) {
+ let browser = getBrowser().getBrowserForTab(tab);
+ let uri = browser.currentURI.spec;
+
+ info("New browser tab has loaded");
+ gBrowser.removeTab(tab);
+ info("Resolve promise with new tab URI");
+ deferred.resolve(uri);
+ }
+
+ function newTabListener(e) {
+ gBrowser.tabContainer.removeEventListener("TabOpen", newTabListener);
+ let tab = e.target;
+ BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, url => url != "about:blank")
+ .then(url => loadListener(tab));
+ }
+
+ let deferred = defer();
+ info("Check that clicking the link opens a new tab with the correct URI");
+ gBrowser.tabContainer.addEventListener("TabOpen", newTabListener, false);
+ info("Click the link to MDN");
+ link.click();
+ return deferred.promise;
+}
+
+/**
+ * Utility function to check content of the tooltip.
+ */
+function checkTooltipContents(doc, expected) {
+ is(doc.heading.textContent,
+ expected.propertyName,
+ "Property name is correct");
+
+ is(doc.summary.textContent,
+ expected.summary,
+ "Summary is correct");
+
+ checkCssSyntaxHighlighterOutput(expected.syntax, doc.syntax);
+}
diff --git a/devtools/client/shared/test/browser_mdn-docs-02.js b/devtools/client/shared/test/browser_mdn-docs-02.js
new file mode 100644
index 000000000..000dc7261
--- /dev/null
+++ b/devtools/client/shared/test/browser_mdn-docs-02.js
@@ -0,0 +1,128 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests the MdnDocsWidget object, and specifically its
+ * loadCssDocs() function.
+ *
+ * The MdnDocsWidget is initialized with a document which has a specific
+ * structure. You then call loadCssDocs(), passing in a CSS property name.
+ * MdnDocsWidget then fetches docs for that property by making an XHR to
+ * a docs page, and loads the results into the document.
+ *
+ * In this file we test that the tooltip can properly handle the different
+ * structures that the docs page might have, including variant structures and
+ * error conditions like parts of the document being missing.
+ *
+ * We also test that the tooltip properly handles the case where the page
+ * doesn't exist at all.
+ */
+
+"use strict";
+
+const {
+ setBaseCssDocsUrl,
+ MdnDocsWidget
+} = require("devtools/client/shared/widgets/MdnDocsWidget");
+
+const BASIC_EXPECTED_SUMMARY = "A summary of the property.";
+const BASIC_EXPECTED_SYNTAX = [{type: "comment", text: "/* The part we want */"},
+ {type: "text", text: "\n"},
+ {type: "property-name", text: "this"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "is-the-part-we-want"},
+ {type: "text", text: ";"}];
+
+const ERROR_MESSAGE = "Could not load docs page.";
+
+/**
+ * Test properties
+ *
+ * In the real tooltip, a CSS property name is used to look up an MDN page
+ * for that property.
+ * In the test code, the names defined here are used to look up a page
+ * served by the test server. We have different properties to test
+ * different ways that the docs pages might be constructed, including errors
+ * like pages that don't include docs where we expect.
+ */
+const SYNTAX_OLD_STYLE = "html-mdn-css-syntax-old-style.html";
+const NO_SUMMARY = "html-mdn-css-no-summary.html";
+const NO_SYNTAX = "html-mdn-css-no-syntax.html";
+const NO_SUMMARY_OR_SYNTAX = "html-mdn-css-no-summary-or-syntax.html";
+
+const TEST_DATA = [{
+ desc: "Test a property for which we don't have a page",
+ docsPageUrl: "i-dont-exist.html",
+ expectedContents: {
+ propertyName: "i-dont-exist.html",
+ summary: ERROR_MESSAGE,
+ syntax: []
+ }
+}, {
+ desc: "Test a property whose syntax section is specified using an old-style page",
+ docsPageUrl: SYNTAX_OLD_STYLE,
+ expectedContents: {
+ propertyName: SYNTAX_OLD_STYLE,
+ summary: BASIC_EXPECTED_SUMMARY,
+ syntax: BASIC_EXPECTED_SYNTAX
+ }
+}, {
+ desc: "Test a property whose page doesn't have a summary",
+ docsPageUrl: NO_SUMMARY,
+ expectedContents: {
+ propertyName: NO_SUMMARY,
+ summary: "",
+ syntax: BASIC_EXPECTED_SYNTAX
+ }
+}, {
+ desc: "Test a property whose page doesn't have a syntax",
+ docsPageUrl: NO_SYNTAX,
+ expectedContents: {
+ propertyName: NO_SYNTAX,
+ summary: BASIC_EXPECTED_SUMMARY,
+ syntax: []
+ }
+}, {
+ desc: "Test a property whose page doesn't have a summary or a syntax",
+ docsPageUrl: NO_SUMMARY_OR_SYNTAX,
+ expectedContents: {
+ propertyName: NO_SUMMARY_OR_SYNTAX,
+ summary: ERROR_MESSAGE,
+ syntax: []
+ }
+}
+];
+
+add_task(function* () {
+ setBaseCssDocsUrl(TEST_URI_ROOT);
+
+ yield addTab("about:blank");
+ let [host, win] = yield createHost("bottom", "data:text/html," +
+ "<div class='mdn-container'></div>");
+ let widget = new MdnDocsWidget(win.document.querySelector("div"));
+
+ for (let {desc, docsPageUrl, expectedContents} of TEST_DATA) {
+ info(desc);
+ yield widget.loadCssDocs(docsPageUrl);
+ checkTooltipContents(widget.elements, expectedContents);
+ }
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+/*
+ * Utility function to check content of the tooltip.
+ */
+function checkTooltipContents(doc, expected) {
+ is(doc.heading.textContent,
+ expected.propertyName,
+ "Property name is correct");
+
+ is(doc.summary.textContent,
+ expected.summary,
+ "Summary is correct");
+
+ checkCssSyntaxHighlighterOutput(expected.syntax, doc.syntax);
+}
diff --git a/devtools/client/shared/test/browser_mdn-docs-03.js b/devtools/client/shared/test/browser_mdn-docs-03.js
new file mode 100644
index 000000000..c686aa6a9
--- /dev/null
+++ b/devtools/client/shared/test/browser_mdn-docs-03.js
@@ -0,0 +1,277 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests the CSS syntax highlighter in the MdnDocsWidget object.
+ *
+ * The CSS syntax highlighter accepts:
+ * - a string containing CSS
+ * - a DOM node
+ *
+ * It parses the string and creates a collection of DOM nodes for different
+ * CSS token types. These DOM nodes have CSS classes applied to them,
+ * to apply the right style for that particular token type. The DOM nodes
+ * are returned as children of the node that was passed to the function.
+ *
+ * This test code defines a number of different strings containing valid and
+ * invalid CSS in various forms. For each string it defines the DOM nodes
+ * that it expects to get from the syntax highlighter.
+ *
+ * It then calls the syntax highlighter, and checks that the resulting
+ * collection of DOM nodes is what we expected.
+ */
+
+"use strict";
+
+const {appendSyntaxHighlightedCSS} = require("devtools/client/shared/widgets/MdnDocsWidget");
+
+/**
+ * An array containing the actual test cases.
+ *
+ * The test code tests every case in the array. If you want to add more
+ * test cases, just add more items to the array.
+ *
+ * Each test case consists of:
+ * - description: string describing the salient features of this test case
+ * - example: the string to test
+ * - expected: an array of objects, one for each DOM node we expect, that
+ * captures the information about the node that we expect to test.
+ */
+const TEST_DATA = [{
+ description: "Valid syntax, string value.",
+ example: "name: stringValue;",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, numeric value.",
+ example: "name: 1;",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "1"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, url value.",
+ example: "name: url(./name);",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "url(./name)"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, space before ':'.",
+ example: "name : stringValue;",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: " "},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, space before ';'.",
+ example: "name: stringValue ;",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: " "},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, trailing space.",
+ example: "name: stringValue; ",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"},
+ {type: "text", text: " "}
+ ]}, {
+ description: "Valid syntax, leading space.",
+ example: " name: stringValue;",
+ expected: [{type: "text", text: " "},
+ {type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, two spaces.",
+ example: "name: stringValue;",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, no spaces.",
+ example: "name:stringValue;",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, two-part value.",
+ example: "name: stringValue 1;",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "1"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, two declarations.",
+ example: "name: stringValue;\n" +
+ "name: 1;",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"},
+ {type: "text", text: "\n"},
+ {type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "1"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, commented, numeric value.",
+ example: "/* comment */\n" +
+ "name: 1;",
+ expected: [{type: "comment", text: "/* comment */"},
+ {type: "text", text: "\n"},
+ {type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "1"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, multiline commented, string value.",
+ example: "/* multiline \n" +
+ "comment */\n" +
+ "name: stringValue;",
+ expected: [{type: "comment", text: "/* multiline \ncomment */"},
+ {type: "text", text: "\n"},
+ {type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, commented, two declarations.",
+ example: "/* comment 1 */\n" +
+ "name: 1;\n" +
+ "/* comment 2 */\n" +
+ "name: stringValue;",
+ expected: [{type: "comment", text: "/* comment 1 */"},
+ {type: "text", text: "\n"},
+ {type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "1"},
+ {type: "text", text: ";"},
+ {type: "text", text: "\n"},
+ {type: "comment", text: "/* comment 2 */"},
+ {type: "text", text: "\n"},
+ {type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, multiline.",
+ example: "name: \n" +
+ "stringValue;",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " \n"},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, multiline, two declarations.",
+ example: "name: \n" +
+ "stringValue \n" +
+ "stringValue2;",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " \n"},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: " \n"},
+ {type: "property-value", text: "stringValue2"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Invalid: not CSS at all.",
+ example: "not CSS at all",
+ expected: [{type: "property-name", text: "not"},
+ {type: "text", text: " "},
+ {type: "property-name", text: "CSS"},
+ {type: "text", text: " "},
+ {type: "property-name", text: "at"},
+ {type: "text", text: " "},
+ {type: "property-name", text: "all"}
+ ]}, {
+ description: "Invalid: switched ':' and ';'.",
+ example: "name; stringValue:",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ";"},
+ {type: "text", text: " "},
+ {type: "property-name", text: "stringValue"},
+ {type: "text", text: ":"}
+ ]}, {
+ description: "Invalid: unterminated comment.",
+ example: "/* unterminated comment\n" +
+ "name: stringValue;",
+ expected: [{type: "comment", text: "/* unterminated comment\nname: stringValue;"}
+ ]}, {
+ description: "Invalid: bad comment syntax.",
+ example: "// invalid comment\n" +
+ "name: stringValue;",
+ expected: [{type: "text", text: "/"},
+ {type: "text", text: "/"},
+ {type: "text", text: " "},
+ {type: "property-name", text: "invalid"},
+ {type: "text", text: " "},
+ {type: "property-name", text: "comment"},
+ {type: "text", text: "\n"},
+ {type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Invalid: no trailing ';'.",
+ example: "name: stringValue\n" +
+ "name: stringValue2",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: "\n"},
+ {type: "property-value", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue2"},
+ ]}
+];
+
+/**
+ * Iterate through every test case, calling the syntax highlighter,
+ * then calling a helper function to check the output.
+ */
+add_task(function* () {
+ let doc = gBrowser.selectedTab.ownerDocument;
+ let parent = doc.createElement("div");
+ info("Testing all CSS syntax highlighter test cases");
+ for (let {description, example, expected} of TEST_DATA) {
+ info("Testing: " + description);
+ appendSyntaxHighlightedCSS(example, parent);
+ checkCssSyntaxHighlighterOutput(expected, parent);
+ while (parent.firstChild) {
+ parent.firstChild.remove();
+ }
+ }
+});
diff --git a/devtools/client/shared/test/browser_num-l10n.js b/devtools/client/shared/test/browser_num-l10n.js
new file mode 100644
index 000000000..fb4ef6cc7
--- /dev/null
+++ b/devtools/client/shared/test/browser_num-l10n.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the localization utils work properly.
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+
+function test() {
+ let l10n = new LocalizationHelper();
+
+ is(l10n.numberWithDecimals(1234.56789, 2), "1,234.57",
+ "The first number was properly localized.");
+ is(l10n.numberWithDecimals(0.0001, 2), "0",
+ "The second number was properly localized.");
+ is(l10n.numberWithDecimals(1.0001, 2), "1",
+ "The third number was properly localized.");
+ is(l10n.numberWithDecimals(NaN, 2), "0",
+ "NaN was properly localized.");
+ is(l10n.numberWithDecimals(null, 2), "0",
+ "`null` was properly localized.");
+ is(l10n.numberWithDecimals(undefined, 2), "0",
+ "`undefined` was properly localized.");
+
+ finish();
+}
diff --git a/devtools/client/shared/test/browser_options-view-01.js b/devtools/client/shared/test/browser_options-view-01.js
new file mode 100644
index 000000000..7dae05a3c
--- /dev/null
+++ b/devtools/client/shared/test/browser_options-view-01.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that options-view OptionsView responds to events correctly.
+
+const {OptionsView} = require("devtools/client/shared/options-view");
+
+const BRANCH = "devtools.debugger.";
+const BLACK_BOX_PREF = "auto-black-box";
+const PRETTY_PRINT_PREF = "auto-pretty-print";
+
+const originalBlackBox = Services.prefs.getBoolPref(BRANCH + BLACK_BOX_PREF);
+const originalPrettyPrint = Services.prefs.getBoolPref(BRANCH + PRETTY_PRINT_PREF);
+
+add_task(function* () {
+ info("Setting a couple of preferences");
+ Services.prefs.setBoolPref(BRANCH + BLACK_BOX_PREF, false);
+ Services.prefs.setBoolPref(BRANCH + PRETTY_PRINT_PREF, true);
+
+ info("Opening a test tab and a toolbox host to create the options view in");
+ yield addTab("about:blank");
+ let [host, win] = yield createHost("bottom", OPTIONS_VIEW_URL);
+
+ yield testOptionsView(win);
+
+ info("Closing the host and current tab");
+ host.destroy();
+ gBrowser.removeCurrentTab();
+
+ info("Resetting the preferences");
+ Services.prefs.setBoolPref(BRANCH + BLACK_BOX_PREF, originalBlackBox);
+ Services.prefs.setBoolPref(BRANCH + PRETTY_PRINT_PREF, originalPrettyPrint);
+});
+
+function* testOptionsView(win) {
+ let events = [];
+ let options = createOptionsView(win);
+ yield options.initialize();
+
+ let $ = win.document.querySelector.bind(win.document);
+
+ options.on("pref-changed", (_, pref) => events.push(pref));
+
+ let ppEl = $("menuitem[data-pref='auto-pretty-print']");
+ let bbEl = $("menuitem[data-pref='auto-black-box']");
+
+ // Test default config
+ is(ppEl.getAttribute("checked"), "true", "`true` prefs are checked on start");
+ is(bbEl.getAttribute("checked"), "", "`false` prefs are unchecked on start");
+
+ // Test buttons update when preferences update outside of the menu
+ Services.prefs.setBoolPref(BRANCH + PRETTY_PRINT_PREF, false);
+ Services.prefs.setBoolPref(BRANCH + BLACK_BOX_PREF, true);
+
+ is(options.getPref(PRETTY_PRINT_PREF), false, "getPref returns correct value");
+ is(options.getPref(BLACK_BOX_PREF), true, "getPref returns correct value");
+
+ is(ppEl.getAttribute("checked"), "", "menuitems update when preferences change");
+ is(bbEl.getAttribute("checked"), "true", "menuitems update when preferences change");
+
+ // Tests events are fired when preferences update outside of the menu
+ is(events.length, 2, "two 'pref-changed' events fired");
+ is(events[0], "auto-pretty-print",
+ "correct pref passed in 'pref-changed' event (auto-pretty-print)");
+ is(events[1], "auto-black-box",
+ "correct pref passed in 'pref-changed' event (auto-black-box)");
+
+ // Test buttons update when clicked and preferences are updated
+ yield click(options, win, ppEl);
+ is(ppEl.getAttribute("checked"), "true", "menuitems update when clicked");
+ is(Services.prefs.getBoolPref(BRANCH + PRETTY_PRINT_PREF),
+ true, "preference updated via click");
+
+ yield click(options, win, bbEl);
+ is(bbEl.getAttribute("checked"), "", "menuitems update when clicked");
+ is(Services.prefs.getBoolPref(BRANCH + BLACK_BOX_PREF),
+ false, "preference updated via click");
+
+ // Tests events are fired when preferences updated via click
+ is(events.length, 4, "two 'pref-changed' events fired");
+ is(events[2], "auto-pretty-print",
+ "correct pref passed in 'pref-changed' event (auto-pretty-print)");
+ is(events[3], "auto-black-box",
+ "correct pref passed in 'pref-changed' event (auto-black-box)");
+
+ yield options.destroy();
+}
+
+function createOptionsView(win) {
+ return new OptionsView({
+ branchName: BRANCH,
+ menupopup: win.document.querySelector("#options-menupopup")
+ });
+}
+
+function* click(view, win, menuitem) {
+ let opened = view.once("options-shown");
+ let closed = view.once("options-hidden");
+
+ let button = win.document.querySelector("#options-button");
+ EventUtils.synthesizeMouseAtCenter(button, {}, win);
+ yield opened;
+ is(button.getAttribute("open"), "true", "button has `open` attribute");
+
+ EventUtils.synthesizeMouseAtCenter(menuitem, {}, win);
+ yield closed;
+ ok(!button.hasAttribute("open"), "button does not have `open` attribute");
+}
diff --git a/devtools/client/shared/test/browser_outputparser.js b/devtools/client/shared/test/browser_outputparser.js
new file mode 100644
index 000000000..a231ad903
--- /dev/null
+++ b/devtools/client/shared/test/browser_outputparser.js
@@ -0,0 +1,292 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {OutputParser} = require("devtools/client/shared/output-parser");
+const {initCssProperties, getCssProperties} = require("devtools/shared/fronts/css-properties");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, , doc] = yield createHost("bottom", "data:text/html," +
+ "<h1>browser_outputParser.js</h1><div></div>");
+
+ // Mock the toolbox that initCssProperties expect so we get the fallback css properties.
+ let toolbox = {target: {client: {}, hasActor: () => false}};
+ yield initCssProperties(toolbox);
+ let cssProperties = getCssProperties(toolbox);
+
+ let parser = new OutputParser(doc, cssProperties);
+ testParseCssProperty(doc, parser);
+ testParseCssVar(doc, parser);
+ testParseURL(doc, parser);
+ testParseFilter(doc, parser);
+ testParseAngle(doc, parser);
+
+ host.destroy();
+}
+
+// Class name used in color swatch.
+var COLOR_TEST_CLASS = "test-class";
+
+// Create a new CSS color-parsing test. |name| is the name of the CSS
+// property. |value| is the CSS text to use. |segments| is an array
+// describing the expected result. If an element of |segments| is a
+// string, it is simply appended to the expected string. Otherwise,
+// it must be an object with a |name| property, which is the color
+// name as it appears in the input.
+//
+// This approach is taken to reduce boilerplate and to make it simpler
+// to modify the test when the parseCssProperty output changes.
+function makeColorTest(name, value, segments) {
+ let result = {
+ name,
+ value,
+ expected: ""
+ };
+
+ for (let segment of segments) {
+ if (typeof (segment) === "string") {
+ result.expected += segment;
+ } else {
+ result.expected += "<span data-color=\"" + segment.name + "\">" +
+ "<span class=\"" + COLOR_TEST_CLASS + "\" style=\"background-color:" +
+ segment.name + "\"></span><span>" +
+ segment.name + "</span></span>";
+ }
+ }
+
+ result.desc = "Testing " + name + ": " + value;
+
+ return result;
+}
+
+function testParseCssProperty(doc, parser) {
+ let tests = [
+ makeColorTest("border", "1px solid red",
+ ["1px solid ", {name: "red"}]),
+
+ makeColorTest("background-image",
+ "linear-gradient(to right, #F60 10%, rgba(0,0,0,1))",
+ ["linear-gradient(to right, ", {name: "#F60"},
+ " 10%, ", {name: "rgba(0,0,0,1)"},
+ ")"]),
+
+ // In "arial black", "black" is a font, not a color.
+ makeColorTest("font-family", "arial black", ["arial black"]),
+
+ makeColorTest("box-shadow", "0 0 1em red",
+ ["0 0 1em ", {name: "red"}]),
+
+ makeColorTest("box-shadow",
+ "0 0 1em red, 2px 2px 0 0 rgba(0,0,0,.5)",
+ ["0 0 1em ", {name: "red"},
+ ", 2px 2px 0 0 ",
+ {name: "rgba(0,0,0,.5)"}]),
+
+ makeColorTest("content", "\"red\"", ["\"red\""]),
+
+ // Invalid property names should not cause exceptions.
+ makeColorTest("hellothere", "'red'", ["'red'"]),
+
+ makeColorTest("filter",
+ "blur(1px) drop-shadow(0 0 0 blue) url(red.svg#blue)",
+ ["<span data-filters=\"blur(1px) drop-shadow(0 0 0 blue) ",
+ "url(red.svg#blue)\"><span>",
+ "blur(1px) drop-shadow(0 0 0 ",
+ {name: "blue"},
+ ") url(red.svg#blue)</span></span>"]),
+
+ makeColorTest("color", "currentColor", ["currentColor"]),
+
+ // Test a very long property.
+ makeColorTest("background-image",
+ /* eslint-disable max-len */
+ "linear-gradient(to left, transparent 0, transparent 5%,#F00 0, #F00 10%,#FF0 0, #FF0 15%,#0F0 0, #0F0 20%,#0FF 0, #0FF 25%,#00F 0, #00F 30%,#800 0, #800 35%,#880 0, #880 40%,#080 0, #080 45%,#088 0, #088 50%,#008 0, #008 55%,#FFF 0, #FFF 60%,#EEE 0, #EEE 65%,#CCC 0, #CCC 70%,#999 0, #999 75%,#666 0, #666 80%,#333 0, #333 85%,#111 0, #111 90%,#000 0, #000 95%,transparent 0, transparent 100%)",
+ /* eslint-enable max-len */
+ ["linear-gradient(to left, ", {name: "transparent"},
+ " 0, ", {name: "transparent"},
+ " 5%,", {name: "#F00"},
+ " 0, ", {name: "#F00"},
+ " 10%,", {name: "#FF0"},
+ " 0, ", {name: "#FF0"},
+ " 15%,", {name: "#0F0"},
+ " 0, ", {name: "#0F0"},
+ " 20%,", {name: "#0FF"},
+ " 0, ", {name: "#0FF"},
+ " 25%,", {name: "#00F"},
+ " 0, ", {name: "#00F"},
+ " 30%,", {name: "#800"},
+ " 0, ", {name: "#800"},
+ " 35%,", {name: "#880"},
+ " 0, ", {name: "#880"},
+ " 40%,", {name: "#080"},
+ " 0, ", {name: "#080"},
+ " 45%,", {name: "#088"},
+ " 0, ", {name: "#088"},
+ " 50%,", {name: "#008"},
+ " 0, ", {name: "#008"},
+ " 55%,", {name: "#FFF"},
+ " 0, ", {name: "#FFF"},
+ " 60%,", {name: "#EEE"},
+ " 0, ", {name: "#EEE"},
+ " 65%,", {name: "#CCC"},
+ " 0, ", {name: "#CCC"},
+ " 70%,", {name: "#999"},
+ " 0, ", {name: "#999"},
+ " 75%,", {name: "#666"},
+ " 0, ", {name: "#666"},
+ " 80%,", {name: "#333"},
+ " 0, ", {name: "#333"},
+ " 85%,", {name: "#111"},
+ " 0, ", {name: "#111"},
+ " 90%,", {name: "#000"},
+ " 0, ", {name: "#000"},
+ " 95%,", {name: "transparent"},
+ " 0, ", {name: "transparent"},
+ " 100%)"]),
+ ];
+
+ let target = doc.querySelector("div");
+ ok(target, "captain, we have the div");
+
+ for (let test of tests) {
+ info(test.desc);
+
+ let frag = parser.parseCssProperty(test.name, test.value, {
+ colorSwatchClass: COLOR_TEST_CLASS
+ });
+
+ target.appendChild(frag);
+
+ is(target.innerHTML, test.expected,
+ "CSS property correctly parsed for " + test.name + ": " + test.value);
+
+ target.innerHTML = "";
+ }
+}
+
+function testParseCssVar(doc, parser) {
+ let frag = parser.parseCssProperty("color", "var(--some-kind-of-green)", {
+ colorSwatchClass: "test-colorswatch"
+ });
+
+ let target = doc.querySelector("div");
+ ok(target, "captain, we have the div");
+ target.appendChild(frag);
+
+ is(target.innerHTML, "var(--some-kind-of-green)",
+ "CSS property correctly parsed");
+
+ target.innerHTML = "";
+}
+
+function testParseURL(doc, parser) {
+ info("Test that URL parsing preserves quoting style");
+
+ const tests = [
+ {
+ desc: "simple test without quotes",
+ leader: "url(",
+ trailer: ")",
+ },
+ {
+ desc: "simple test with single quotes",
+ leader: "url('",
+ trailer: "')",
+ },
+ {
+ desc: "simple test with double quotes",
+ leader: "url(\"",
+ trailer: "\")",
+ },
+ {
+ desc: "test with single quotes and whitespace",
+ leader: "url( \t'",
+ trailer: "'\r\n\f)",
+ },
+ {
+ desc: "simple test with uppercase",
+ leader: "URL(",
+ trailer: ")",
+ },
+ {
+ desc: "bad url, missing paren",
+ leader: "url(",
+ trailer: "",
+ expectedTrailer: ")"
+ },
+ {
+ desc: "bad url, missing paren, with baseURI",
+ baseURI: "data:text/html,<style></style>",
+ leader: "url(",
+ trailer: "",
+ expectedTrailer: ")"
+ },
+ {
+ desc: "bad url, double quote, missing paren",
+ leader: "url(\"",
+ trailer: "\"",
+ expectedTrailer: "\")",
+ },
+ {
+ desc: "bad url, single quote, missing paren and quote",
+ leader: "url('",
+ trailer: "",
+ expectedTrailer: "')"
+ }
+ ];
+
+ for (let test of tests) {
+ let url = test.leader + "something.jpg" + test.trailer;
+ let frag = parser.parseCssProperty("background", url, {
+ urlClass: "test-urlclass",
+ baseURI: test.baseURI,
+ });
+
+ let target = doc.querySelector("div");
+ target.appendChild(frag);
+
+ let expectedTrailer = test.expectedTrailer || test.trailer;
+
+ let expected = test.leader +
+ "<a target=\"_blank\" class=\"test-urlclass\" " +
+ "href=\"something.jpg\">something.jpg</a>" +
+ expectedTrailer;
+
+ is(target.innerHTML, expected, test.desc);
+
+ target.innerHTML = "";
+ }
+}
+
+function testParseFilter(doc, parser) {
+ let frag = parser.parseCssProperty("filter", "something invalid", {
+ filterSwatchClass: "test-filterswatch"
+ });
+
+ let swatchCount = frag.querySelectorAll(".test-filterswatch").length;
+ is(swatchCount, 1, "filter swatch was created");
+}
+
+function testParseAngle(doc, parser) {
+ let frag = parser.parseCssProperty("image-orientation", "90deg", {
+ angleSwatchClass: "test-angleswatch"
+ });
+
+ let swatchCount = frag.querySelectorAll(".test-angleswatch").length;
+ is(swatchCount, 1, "angle swatch was created");
+
+ frag = parser.parseCssProperty("background-image",
+ "linear-gradient(90deg, red, blue", {
+ angleSwatchClass: "test-angleswatch"
+ });
+
+ swatchCount = frag.querySelectorAll(".test-angleswatch").length;
+ is(swatchCount, 1, "angle swatch was created");
+}
diff --git a/devtools/client/shared/test/browser_poller.js b/devtools/client/shared/test/browser_poller.js
new file mode 100644
index 000000000..281d055ff
--- /dev/null
+++ b/devtools/client/shared/test/browser_poller.js
@@ -0,0 +1,136 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the Poller class.
+
+const { Poller } = require("devtools/client/shared/poller");
+
+add_task(function* () {
+ let count1 = 0, count2 = 0, count3 = 0;
+
+ let poller1 = new Poller(function () {
+ count1++;
+ }, 1000000000, true);
+ let poller2 = new Poller(function () {
+ count2++;
+ }, 10);
+ let poller3 = new Poller(function () {
+ count3++;
+ }, 1000000000);
+
+ poller2.on();
+
+ ok(!poller1.isPolling(), "isPolling() returns false for an off poller");
+ ok(poller2.isPolling(), "isPolling() returns true for an on poller");
+
+ yield waitUntil(() => count2 > 10);
+
+ ok(count2 > 10, "poller that was turned on polled several times");
+ ok(count1 === 0, "poller that was never turned on never polled");
+
+ yield poller2.off();
+ let currentCount2 = count2;
+
+ // Really high poll time!
+ poller1.on();
+ poller3.on();
+
+ yield waitUntil(() => count1 === 1);
+ ok(true, "Poller calls fn immediately when `immediate` is true");
+ ok(count3 === 0, "Poller does not call fn immediately when `immediate` is not set");
+
+ ok(count2 === currentCount2, "a turned off poller does not continue to poll");
+ yield poller2.off();
+ yield poller2.off();
+ yield poller2.off();
+ ok(true, "Poller.prototype.off() is idempotent");
+
+ // This should still have not polled a second time
+ is(count1, 1, "wait time works");
+
+ ok(poller1.isPolling(), "isPolling() returns true for an on poller");
+ ok(!poller2.isPolling(), "isPolling() returns false for an off poller");
+});
+
+add_task(function* () {
+ let count = -1;
+ // Create a poller that returns a promise.
+ // The promise is resolved asynchronously after adding 9 to the count, ensuring
+ // that on every poll, we have a multiple of 10.
+ let asyncPoller = new Poller(function () {
+ count++;
+ ok(!(count % 10), `Async poller called with a multiple of 10: ${count}`);
+ return new Promise(function (resolve, reject) {
+ let add9 = 9;
+ let interval = setInterval(() => {
+ if (add9--) {
+ count++;
+ } else {
+ clearInterval(interval);
+ resolve();
+ }
+ }, 10);
+ });
+ });
+
+ asyncPoller.on(1);
+ yield waitUntil(() => count > 50);
+ yield asyncPoller.off();
+});
+
+add_task(function* () {
+ // Create a poller that returns a promise. This poll call
+ // is called immediately, and then subsequently turned off.
+ // The call to `off` should not resolve until the inflight call
+ // finishes.
+ let inflightFinished = null;
+ let pollCalls = 0;
+ let asyncPoller = new Poller(function () {
+ pollCalls++;
+ return new Promise(function (resolve, reject) {
+ setTimeout(() => {
+ inflightFinished = true;
+ resolve();
+ }, 1000);
+ });
+ }, 1, true);
+ asyncPoller.on();
+
+ yield asyncPoller.off();
+ ok(inflightFinished,
+ "off() method does not resolve until remaining inflight poll calls finish");
+ is(pollCalls, 1, "should only be one poll call to occur before turning off polling");
+});
+
+add_task(function* () {
+ // Create a poller that returns a promise. This poll call
+ // is called immediately, and then subsequently turned off.
+ // The call to `off` should not resolve until the inflight call
+ // finishes.
+ let inflightFinished = null;
+ let pollCalls = 0;
+ let asyncPoller = new Poller(function () {
+ pollCalls++;
+ return new Promise(function (resolve, reject) {
+ setTimeout(() => {
+ inflightFinished = true;
+ resolve();
+ }, 1000);
+ });
+ }, 1, true);
+ asyncPoller.on();
+
+ yield asyncPoller.destroy();
+ ok(inflightFinished,
+ "destroy() method does not resolve until remaining inflight poll calls finish");
+ is(pollCalls, 1, "should only be one poll call to occur before destroying polling");
+
+ try {
+ asyncPoller.on();
+ ok(false, "Calling on() after destruction should throw");
+ } catch (e) {
+ ok(true, "Calling on() after destruction should throw");
+ }
+});
diff --git a/devtools/client/shared/test/browser_prefs-01.js b/devtools/client/shared/test/browser_prefs-01.js
new file mode 100644
index 000000000..193348361
--- /dev/null
+++ b/devtools/client/shared/test/browser_prefs-01.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the preference helpers work properly.
+
+const { PrefsHelper } = require("devtools/client/shared/prefs");
+
+function test() {
+ let Prefs = new PrefsHelper("devtools.debugger", {
+ "foo": ["Bool", "enabled"]
+ });
+
+ let originalPrefValue = Services.prefs.getBoolPref("devtools.debugger.enabled");
+ is(Prefs.foo, originalPrefValue, "The pref value was correctly fetched.");
+
+ Prefs.foo = !originalPrefValue;
+ is(Prefs.foo, !originalPrefValue,
+ "The pref was was correctly changed (1).");
+ is(Services.prefs.getBoolPref("devtools.debugger.enabled"), !originalPrefValue,
+ "The pref was was correctly changed (2).");
+
+ Services.prefs.setBoolPref("devtools.debugger.enabled", originalPrefValue);
+ info("The pref value was reset (1).");
+ is(Prefs.foo, !originalPrefValue,
+ "The cached pref value hasn't changed yet (1).");
+
+ Services.prefs.setBoolPref("devtools.debugger.enabled", !originalPrefValue);
+ info("The pref value was reset (2).");
+ is(Prefs.foo, !originalPrefValue,
+ "The cached pref value hasn't changed yet (2).");
+
+ Prefs.registerObserver();
+
+ Services.prefs.setBoolPref("devtools.debugger.enabled", originalPrefValue);
+ info("The pref value was reset (3).");
+ is(Prefs.foo, originalPrefValue,
+ "The cached pref value has changed now.");
+
+ Prefs.unregisterObserver();
+
+ finish();
+}
diff --git a/devtools/client/shared/test/browser_prefs-02.js b/devtools/client/shared/test/browser_prefs-02.js
new file mode 100644
index 000000000..f0f638d63
--- /dev/null
+++ b/devtools/client/shared/test/browser_prefs-02.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that preference helpers work properly with custom types of Float and Json.
+
+const { PrefsHelper } = require("devtools/client/shared/prefs");
+
+function test() {
+ let originalJson = Services.prefs.getCharPref(
+ "devtools.performance.timeline.hidden-markers");
+ let originalFloat = Services.prefs.getCharPref(
+ "devtools.performance.memory.sample-probability");
+
+ let Prefs = new PrefsHelper("devtools.performance", {
+ "float": ["Float", "memory.sample-probability"],
+ "json": ["Json", "timeline.hidden-markers"]
+ });
+
+ Prefs.registerObserver();
+
+ // Float
+ Services.prefs.setCharPref("devtools.performance.timeline.hidden-markers", "{\"a\":1}");
+ is(Prefs.json.a, 1, "The JSON pref value is correctly casted on get.");
+
+ Prefs.json = { b: 2 };
+ is(Prefs.json.a, undefined, "The JSON pref value is correctly casted on set (1).");
+ is(Prefs.json.b, 2, "The JSON pref value is correctly casted on set (2).");
+
+ // Float
+ Services.prefs.setCharPref("devtools.performance.memory.sample-probability", "3.14");
+ is(Prefs.float, 3.14, "The float pref value is correctly casted on get.");
+
+ Prefs.float = 6.28;
+ is(Prefs.float, 6.28, "The float pref value is correctly casted on set.");
+
+ Prefs.unregisterObserver();
+
+ Services.prefs.setCharPref("devtools.performance.timeline.hidden-markers",
+ originalJson);
+ Services.prefs.setCharPref("devtools.performance.memory.sample-probability",
+ originalFloat);
+ finish();
+}
diff --git a/devtools/client/shared/test/browser_require_raw.js b/devtools/client/shared/test/browser_require_raw.js
new file mode 100644
index 000000000..d40b84c35
--- /dev/null
+++ b/devtools/client/shared/test/browser_require_raw.js
@@ -0,0 +1,20 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
+
+const { require: browserRequire } = BrowserLoader({
+ baseURI: "resource://devtools/client/shared/",
+ window
+});
+
+const variableFileContents = browserRequire("raw!devtools/client/themes/variables.css");
+
+function test() {
+ ok(variableFileContents.length > 0, "raw browserRequire worked");
+ finish();
+}
diff --git a/devtools/client/shared/test/browser_spectrum.js b/devtools/client/shared/test/browser_spectrum.js
new file mode 100644
index 000000000..9e72ef621
--- /dev/null
+++ b/devtools/client/shared/test/browser_spectrum.js
@@ -0,0 +1,114 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the spectrum color picker works correctly
+
+const {Spectrum} = require("devtools/client/shared/widgets/Spectrum");
+
+const TEST_URI = `data:text/html,
+ <link rel="stylesheet" href="chrome://devtools/content/shared/widgets/spectrum.css" type="text/css"/>
+ <div id="spectrum-container" />`;
+
+add_task(function* () {
+ let [host,, doc] = yield createHost("bottom", TEST_URI);
+
+ let container = doc.getElementById("spectrum-container");
+
+ yield testCreateAndDestroyShouldAppendAndRemoveElements(container);
+ yield testPassingAColorAtInitShouldSetThatColor(container);
+ yield testSettingAndGettingANewColor(container);
+ yield testChangingColorShouldEmitEvents(container);
+ yield testSettingColorShoudUpdateTheUI(container);
+
+ host.destroy();
+});
+
+function testCreateAndDestroyShouldAppendAndRemoveElements(container) {
+ ok(container, "We have the root node to append spectrum to");
+ is(container.childElementCount, 0, "Root node is empty");
+
+ let s = new Spectrum(container, [255, 126, 255, 1]);
+ s.show();
+ ok(container.childElementCount > 0, "Spectrum has appended elements");
+
+ s.destroy();
+ is(container.childElementCount, 0, "Destroying spectrum removed all nodes");
+}
+
+function testPassingAColorAtInitShouldSetThatColor(container) {
+ let initRgba = [255, 126, 255, 1];
+
+ let s = new Spectrum(container, initRgba);
+ s.show();
+
+ let setRgba = s.rgb;
+
+ is(initRgba[0], setRgba[0], "Spectrum initialized with the right color");
+ is(initRgba[1], setRgba[1], "Spectrum initialized with the right color");
+ is(initRgba[2], setRgba[2], "Spectrum initialized with the right color");
+ is(initRgba[3], setRgba[3], "Spectrum initialized with the right color");
+
+ s.destroy();
+}
+
+function testSettingAndGettingANewColor(container) {
+ let s = new Spectrum(container, [0, 0, 0, 1]);
+ s.show();
+
+ let colorToSet = [255, 255, 255, 1];
+ s.rgb = colorToSet;
+ let newColor = s.rgb;
+
+ is(colorToSet[0], newColor[0], "Spectrum set with the right color");
+ is(colorToSet[1], newColor[1], "Spectrum set with the right color");
+ is(colorToSet[2], newColor[2], "Spectrum set with the right color");
+ is(colorToSet[3], newColor[3], "Spectrum set with the right color");
+
+ s.destroy();
+}
+
+function testChangingColorShouldEmitEvents(container) {
+ return new Promise(resolve => {
+ let s = new Spectrum(container, [255, 255, 255, 1]);
+ s.show();
+
+ s.once("changed", (event, rgba, color) => {
+ ok(true, "Changed event was emitted on color change");
+ is(rgba[0], 128, "New color is correct");
+ is(rgba[1], 64, "New color is correct");
+ is(rgba[2], 64, "New color is correct");
+ is(rgba[3], 1, "New color is correct");
+ is(`rgba(${rgba.join(", ")})`, color, "RGBA and css color correspond");
+
+ s.destroy();
+ resolve();
+ });
+
+ // Simulate a drag move event by calling the handler directly.
+ s.onDraggerMove(s.dragger.offsetWidth / 2, s.dragger.offsetHeight / 2);
+ });
+}
+
+function testSettingColorShoudUpdateTheUI(container) {
+ let s = new Spectrum(container, [255, 255, 255, 1]);
+ s.show();
+ let dragHelperOriginalPos = [s.dragHelper.style.top, s.dragHelper.style.left];
+ let alphaHelperOriginalPos = s.alphaSliderHelper.style.left;
+
+ s.rgb = [50, 240, 234, .2];
+ s.updateUI();
+
+ ok(s.alphaSliderHelper.style.left != alphaHelperOriginalPos, "Alpha helper has moved");
+ ok(s.dragHelper.style.top !== dragHelperOriginalPos[0], "Drag helper has moved");
+ ok(s.dragHelper.style.left !== dragHelperOriginalPos[1], "Drag helper has moved");
+
+ s.rgb = [240, 32, 124, 0];
+ s.updateUI();
+ is(s.alphaSliderHelper.style.left, -(s.alphaSliderHelper.offsetWidth / 2) + "px",
+ "Alpha range UI has been updated again");
+
+ s.destroy();
+}
diff --git a/devtools/client/shared/test/browser_tableWidget_basic.js b/devtools/client/shared/test/browser_tableWidget_basic.js
new file mode 100644
index 000000000..684ca99bb
--- /dev/null
+++ b/devtools/client/shared/test/browser_tableWidget_basic.js
@@ -0,0 +1,390 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the table widget api works fine
+
+"use strict";
+
+const TEST_URI = "data:text/xml;charset=UTF-8,<?xml version='1.0'?>" +
+ "<?xml-stylesheet href='chrome://global/skin/global.css'?>" +
+
+ // Uncomment these lines to help with visual debugging. When uncommented they
+ // dump a couple of thousand errors in the log (bug 1258285)
+ // "<?xml-stylesheet href='chrome://devtools/skin/light-theme.css'?>" +
+ // "<?xml-stylesheet href='chrome://devtools/skin/widgets.css'?>" +
+
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='Table Widget' width='600' height='500'>" +
+ "<box flex='1' class='theme-light'/></window>";
+
+const {TableWidget} = require("devtools/client/shared/widgets/TableWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ let [host, , doc] = yield createHost("bottom", TEST_URI);
+
+ let table = new TableWidget(doc.querySelector("box"), {
+ initialColumns: {
+ col1: "Column 1",
+ col2: "Column 2",
+ col3: "Column 3",
+ col4: "Column 4"
+ },
+ uniqueId: "col1",
+ emptyText: "This is dummy empty text",
+ highlightUpdated: true,
+ removableColumns: true,
+ firstColumn: "col4"
+ });
+
+ startTests(doc, table);
+ endTests(doc, host, table);
+});
+
+function startTests(doc, table) {
+ populateTable(doc, table);
+
+ testTreeItemInsertedCorrectly(doc, table);
+ testAPI(doc, table);
+}
+
+function endTests(doc, host, table) {
+ table.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+ table = null;
+ finish();
+}
+
+function populateTable(doc, table) {
+ table.push({
+ col1: "id1",
+ col2: "value10",
+ col3: "value20",
+ col4: "value30"
+ });
+ table.push({
+ col1: "id2",
+ col2: "value14",
+ col3: "value29",
+ col4: "value32"
+ });
+ table.push({
+ col1: "id3",
+ col2: "value17",
+ col3: "value21",
+ col4: "value31",
+ extraData: "foobar",
+ extraData2: 42
+ });
+ table.push({
+ col1: "id4",
+ col2: "value12",
+ col3: "value26",
+ col4: "value33"
+ });
+ table.push({
+ col1: "id5",
+ col2: "value19",
+ col3: "value26",
+ col4: "value37"
+ });
+ table.push({
+ col1: "id6",
+ col2: "value15",
+ col3: "value25",
+ col4: "value37"
+ });
+ table.push({
+ col1: "id7",
+ col2: "value18",
+ col3: "value21",
+ col4: "value36",
+ somethingExtra: "Hello World!"
+ });
+ table.push({
+ col1: "id8",
+ col2: "value11",
+ col3: "value27",
+ col4: "value34"
+ });
+
+ let span = doc.createElement("span");
+ span.textContent = "domnode";
+
+ table.push({
+ col1: "id9",
+ col2: "value11",
+ col3: "value23",
+ col4: span
+ });
+}
+
+/**
+ * Test if the nodes are inserted correctly in the table.
+ */
+function testTreeItemInsertedCorrectly(doc, table) {
+ // double because of splitters
+ is(table.tbody.children.length, 4 * 2, "4 columns exist");
+
+ // Test firstColumn option and check if the nodes are inserted correctly
+ is(table.tbody.children[0].firstChild.children.length, 9 + 1,
+ "Correct rows in column 4");
+ is(table.tbody.children[0].firstChild.firstChild.value, "Column 4",
+ "Correct column header value");
+
+ for (let i = 1; i < 4; i++) {
+ is(table.tbody.children[i * 2].firstChild.children.length, 9 + 1,
+ `Correct rows in column ${i}`);
+ is(table.tbody.children[i * 2].firstChild.firstChild.value, `Column ${i}`,
+ "Correct column header value");
+ }
+ for (let i = 1; i < 10; i++) {
+ is(table.tbody.children[2].firstChild.children[i].value, `id${i}`,
+ `Correct value in row ${i}`);
+ }
+
+ // Remove firstColumn option and reset the table
+ table.clear();
+ table.firstColumn = "";
+ table.setColumns({
+ col1: "Column 1",
+ col2: "Column 2",
+ col3: "Column 3",
+ col4: "Column 4"
+ });
+ populateTable(doc, table);
+
+ // Check if the nodes are inserted correctly without firstColumn option
+ for (let i = 0; i < 4; i++) {
+ is(table.tbody.children[i * 2].firstChild.children.length, 9 + 1,
+ `Correct rows in column ${i}`);
+ is(table.tbody.children[i * 2].firstChild.firstChild.value,
+ `Column ${i + 1}`,
+ "Correct column header value");
+ }
+
+ for (let i = 1; i < 10; i++) {
+ is(table.tbody.firstChild.firstChild.children[i].value, `id${i}`,
+ `Correct value in row ${i}`);
+ }
+}
+
+/**
+ * Tests if the API exposed by TableWidget works properly
+ */
+function testAPI(doc, table) {
+ info("Testing TableWidget API");
+ // Check if selectRow and selectedRow setter works as expected
+ // Nothing should be selected beforehand
+ ok(!doc.querySelector(".theme-selected"), "Nothing is selected");
+ table.selectRow("id4");
+ let node = doc.querySelector(".theme-selected");
+ ok(!!node, "Somthing got selected");
+ is(node.getAttribute("data-id"), "id4", "Correct node selected");
+
+ table.selectRow("id7");
+ let node2 = doc.querySelector(".theme-selected");
+ ok(!!node2, "Somthing is still selected");
+ isnot(node, node2, "Newly selected node is different from previous");
+ is(node2.getAttribute("data-id"), "id7", "Correct node selected");
+
+ // test if selectedIRow getter works
+ is(table.selectedRow.col1, "id7", "Correct result of selectedRow getter");
+
+ // test if isSelected works
+ ok(table.isSelected("id7"), "isSelected with column id works");
+ ok(table.isSelected({
+ col1: "id7",
+ col2: "value18",
+ col3: "value21",
+ col4: "value36",
+ somethingExtra: "Hello World!"
+ }), "isSelected with json works");
+
+ table.selectedRow = "id4";
+ let node3 = doc.querySelector(".theme-selected");
+ ok(!!node3, "Somthing is still selected");
+ isnot(node2, node3, "Newly selected node is different from previous");
+ is(node3, node, "First and third selected nodes should be same");
+ is(node3.getAttribute("data-id"), "id4", "Correct node selected");
+
+ // test if selectedRow getter works
+ is(table.selectedRow.col1, "id4", "Correct result of selectedRow getter");
+
+ // test if clear selection works
+ table.clearSelection();
+ ok(!doc.querySelector(".theme-selected"),
+ "Nothing selected after clear selection call");
+
+ // test if selectNextRow and selectPreviousRow work
+ table.selectedRow = "id7";
+ ok(table.isSelected("id7"), "Correct row selected");
+ table.selectNextRow();
+ ok(table.isSelected("id8"), "Correct row selected after selectNextRow call");
+
+ table.selectNextRow();
+ ok(table.isSelected("id9"), "Correct row selected after selectNextRow call");
+
+ table.selectNextRow();
+ ok(table.isSelected("id1"),
+ "Properly cycled to first row after selectNextRow call on last row");
+
+ table.selectNextRow();
+ ok(table.isSelected("id2"), "Correct row selected after selectNextRow call");
+
+ table.selectPreviousRow();
+ ok(table.isSelected("id1"),
+ "Correct row selected after selectPreviousRow call");
+
+ table.selectPreviousRow();
+ ok(table.isSelected("id9"),
+ "Properly cycled to last row after selectPreviousRow call on first row");
+
+ // test if remove works
+ ok(doc.querySelector("[data-id='id4']"), "id4 row exists before removal");
+ table.remove("id4");
+ ok(!doc.querySelector("[data-id='id4']"),
+ "id4 row does not exist after removal through id");
+
+ ok(doc.querySelector("[data-id='id6']"), "id6 row exists before removal");
+ table.remove({
+ col1: "id6",
+ col2: "value15",
+ col3: "value25",
+ col4: "value37"
+ });
+ ok(!doc.querySelector("[data-id='id6']"),
+ "id6 row does not exist after removal through json");
+
+ table.push({
+ col1: "id4",
+ col2: "value12",
+ col3: "value26",
+ col4: "value33"
+ });
+ table.push({
+ col1: "id6",
+ col2: "value15",
+ col3: "value25",
+ col4: "value37"
+ });
+
+ // test if selectedIndex getter setter works
+ table.selectedIndex = 2;
+ ok(table.isSelected("id3"), "Correct row selected by selectedIndex setter");
+
+ table.selectedIndex = 4;
+ ok(table.isSelected("id5"), "Correct row selected by selectedIndex setter");
+
+ table.selectRow("id8");
+ is(table.selectedIndex, 7, "Correct value of selectedIndex getter");
+
+ // testing if clear works
+ table.clear();
+ // double because splitters
+ is(table.tbody.children.length, 4 * 2,
+ "4 columns exist even after clear");
+ for (let i = 0; i < 4; i++) {
+ is(table.tbody.children[i * 2].firstChild.children.length, 1,
+ `Only header in the column ${i} after clear call`);
+ is(table.tbody.children[i * 2].firstChild.firstChild.value,
+ `Column ${i + 1}`,
+ "Correct column header value");
+ }
+
+ // testing if setColumns work
+ table.setColumns({
+ col1: "Foobar",
+ col2: "Testing"
+ });
+
+ // double because splitters
+ is(table.tbody.children.length, 2 * 2,
+ "2 columns exist after setColumn call");
+ is(table.tbody.children[0].firstChild.firstChild.value, "Foobar",
+ "Correct column header value for first column");
+ is(table.tbody.children[2].firstChild.firstChild.value, "Testing",
+ "Correct column header value for second column");
+
+ table.setColumns({
+ col1: "Column 1",
+ col2: "Column 2",
+ col3: "Column 3",
+ col4: "Column 4"
+ });
+ // double because splitters
+ is(table.tbody.children.length, 4 * 2,
+ "4 columns exist after second setColumn call");
+
+ populateTable(doc, table);
+
+ // testing if update works
+ is(doc.querySelectorAll("[data-id='id4']")[1].value, "value12",
+ "Correct value before update");
+ table.update({
+ col1: "id4",
+ col2: "UPDATED",
+ col3: "value26",
+ col4: "value33"
+ });
+ is(doc.querySelectorAll("[data-id='id4']")[1].value, "UPDATED",
+ "Correct value after update");
+
+ // testing if sorting works by calling it once on an already sorted column
+ // should sort descending
+ table.sortBy("col1");
+ for (let i = 1; i < 10; i++) {
+ is(table.tbody.firstChild.firstChild.children[i].value, `id${10 - i}`,
+ `Correct value in row ${i} after descending sort by on col1`);
+ }
+ // Calling it on an unsorted column should sort by it in ascending manner
+ table.sortBy("col2");
+ let cell = table.tbody.children[2].firstChild.children[2];
+ checkAscendingOrder(cell);
+
+ // Calling it again should sort by it in descending manner
+ table.sortBy("col2");
+ cell = table.tbody.children[2].firstChild.lastChild.previousSibling;
+ checkDescendingOrder(cell);
+
+ // Calling it again should sort by it in ascending manner
+ table.sortBy("col2");
+ cell = table.tbody.children[2].firstChild.children[2];
+ checkAscendingOrder(cell);
+
+ table.clear();
+ populateTable(doc, table);
+
+ // testing if sorting works should sort by ascending manner
+ table.sortBy("col4");
+ cell = table.tbody.children[6].firstChild.children[1];
+ is(cell.textContent, "domnode", "DOMNode sorted correctly");
+ checkAscendingOrder(cell.nextSibling);
+
+ // Calling it again should sort it in descending order
+ table.sortBy("col4");
+ cell = table.tbody.children[6].firstChild.children[9];
+ is(cell.textContent, "domnode", "DOMNode sorted correctly");
+ checkDescendingOrder(cell.previousSibling);
+}
+
+function checkAscendingOrder(cell) {
+ while (cell) {
+ let currentCell = cell.value || cell.textContent;
+ let prevCell = cell.previousSibling.value ||
+ cell.previousSibling.textContent;
+ ok(currentCell >= prevCell, "Sorting is in ascending order");
+ cell = cell.nextSibling;
+ }
+}
+
+function checkDescendingOrder(cell) {
+ while (cell != cell.parentNode.firstChild) {
+ let currentCell = cell.value || cell.textContent;
+ let nextCell = cell.nextSibling.value || cell.nextSibling.textContent;
+ ok(currentCell >= nextCell, "Sorting is in descending order");
+ cell = cell.previousSibling;
+ }
+}
diff --git a/devtools/client/shared/test/browser_tableWidget_keyboard_interaction.js b/devtools/client/shared/test/browser_tableWidget_keyboard_interaction.js
new file mode 100644
index 000000000..ce052bd88
--- /dev/null
+++ b/devtools/client/shared/test/browser_tableWidget_keyboard_interaction.js
@@ -0,0 +1,194 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that keyboard interaction works fine with the table widget
+
+"use strict";
+
+const TEST_URI = "data:text/xml;charset=UTF-8,<?xml version='1.0'?>" +
+ "<?xml-stylesheet href='chrome://global/skin/global.css'?>" +
+
+ // Uncomment these lines to help with visual debugging. When uncommented they
+ // dump a couple of thousand errors in the log (bug 1258285)
+ // "<?xml-stylesheet href='chrome://devtools/skin/light-theme.css'?>" +
+ // "<?xml-stylesheet href='chrome://devtools/skin/widgets.css'?>" +
+
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='Table Widget' width='600' height='500'>" +
+ "<box flex='1' class='theme-light'/></window>";
+const TEST_OPT = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+const {TableWidget} = require("devtools/client/shared/widgets/TableWidget");
+
+var doc, table;
+
+function test() {
+ waitForExplicitFinish();
+ let win = Services.ww.openWindow(null, TEST_URI, "_blank", TEST_OPT, null);
+
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad, false);
+
+ waitForFocus(function () {
+ doc = win.document;
+ table = new TableWidget(doc.querySelector("box"), {
+ initialColumns: {
+ col1: "Column 1",
+ col2: "Column 2",
+ col3: "Column 3",
+ col4: "Column 4"
+ },
+ uniqueId: "col1",
+ emptyText: "This is dummy empty text",
+ highlightUpdated: true,
+ removableColumns: true,
+ });
+ startTests();
+ });
+ });
+}
+
+function endTests() {
+ table.destroy();
+ doc.defaultView.close();
+ doc = table = null;
+ finish();
+}
+
+var startTests = Task.async(function* () {
+ populateTable();
+ yield testKeyboardInteraction();
+ endTests();
+});
+
+function populateTable() {
+ table.push({
+ col1: "id1",
+ col2: "value10",
+ col3: "value20",
+ col4: "value30"
+ });
+ table.push({
+ col1: "id2",
+ col2: "value14",
+ col3: "value29",
+ col4: "value32"
+ });
+ table.push({
+ col1: "id3",
+ col2: "value17",
+ col3: "value21",
+ col4: "value31",
+ extraData: "foobar",
+ extraData2: 42
+ });
+ table.push({
+ col1: "id4",
+ col2: "value12",
+ col3: "value26",
+ col4: "value33"
+ });
+ table.push({
+ col1: "id5",
+ col2: "value19",
+ col3: "value26",
+ col4: "value37"
+ });
+ table.push({
+ col1: "id6",
+ col2: "value15",
+ col3: "value25",
+ col4: "value37"
+ });
+ table.push({
+ col1: "id7",
+ col2: "value18",
+ col3: "value21",
+ col4: "value36",
+ somethingExtra: "Hello World!"
+ });
+ table.push({
+ col1: "id8",
+ col2: "value11",
+ col3: "value27",
+ col4: "value34"
+ });
+ table.push({
+ col1: "id9",
+ col2: "value11",
+ col3: "value23",
+ col4: "value38"
+ });
+}
+
+// Sends a click event on the passed DOM node in an async manner
+function click(node, button = 0) {
+ if (button == 0) {
+ executeSoon(() => EventUtils.synthesizeMouseAtCenter(node, {},
+ doc.defaultView));
+ } else {
+ executeSoon(() => EventUtils.synthesizeMouseAtCenter(node, {
+ button: button,
+ type: "contextmenu"
+ }, doc.defaultView));
+ }
+}
+
+function getNodeByValue(value) {
+ return table.tbody.querySelector("[value=" + value + "]");
+}
+
+/**
+ * Tests if pressing navigation keys on the table items does the expected
+ * behavior.
+ */
+var testKeyboardInteraction = Task.async(function* () {
+ info("Testing keyboard interaction with the table");
+ info("clicking on the row containing id2");
+ let node = getNodeByValue("id2");
+ let event = table.once(TableWidget.EVENTS.ROW_SELECTED);
+ click(node);
+ yield event;
+
+ yield testRow("id3", "DOWN", "next row");
+ yield testRow("id4", "DOWN", "next row");
+ yield testRow("id3", "UP", "previous row");
+ yield testRow("id4", "DOWN", "next row");
+ yield testRow("id5", "DOWN", "next row");
+ yield testRow("id6", "DOWN", "next row");
+ yield testRow("id5", "UP", "previous row");
+ yield testRow("id4", "UP", "previous row");
+ yield testRow("id3", "UP", "previous row");
+
+ // selecting last item node to test edge navigation cycling case
+ table.selectedRow = "id9";
+
+ // pressing down on last row should move to first row.
+ yield testRow("id1", "DOWN", "first row");
+
+ // pressing up now should move to last row.
+ yield testRow("id9", "UP", "last row");
+});
+
+function* testRow(id, key, destination) {
+ let node = getNodeByValue(id);
+ // node should not have selected class
+ ok(!node.classList.contains("theme-selected"),
+ "Row should not have selected class");
+ info(`Pressing ${key} to select ${destination}`);
+
+ let event = table.once(TableWidget.EVENTS.ROW_SELECTED);
+ EventUtils.sendKey(key, doc.defaultView);
+
+ let uniqueId = yield event;
+ is(id, uniqueId, `Correct row was selected after pressing ${key}`);
+
+ ok(node.classList.contains("theme-selected"), "row has selected class");
+
+ let nodes = doc.querySelectorAll(".theme-selected");
+ for (let i = 0; i < nodes.length; i++) {
+ is(nodes[i].getAttribute("data-id"), id,
+ "Correct cell selected in all columns");
+ }
+}
diff --git a/devtools/client/shared/test/browser_tableWidget_mouse_interaction.js b/devtools/client/shared/test/browser_tableWidget_mouse_interaction.js
new file mode 100644
index 000000000..4d7de94e3
--- /dev/null
+++ b/devtools/client/shared/test/browser_tableWidget_mouse_interaction.js
@@ -0,0 +1,317 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that mosue interaction works fine with the table widget
+
+"use strict";
+
+const TEST_URI = "data:text/xml;charset=UTF-8,<?xml version='1.0'?>" +
+ "<?xml-stylesheet href='chrome://global/skin/global.css'?>" +
+
+ // Uncomment these lines to help with visual debugging. When uncommented they
+ // dump a couple of thousand errors in the log (bug 1258285)
+ // "<?xml-stylesheet href='chrome://devtools/skin/light-theme.css'?>" +
+ // "<?xml-stylesheet href='chrome://devtools/skin/widgets.css'?>" +
+
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='Table Widget' width='600' height='500'>" +
+ "<box flex='1' class='theme-light'/></window>";
+const TEST_OPT = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+const {TableWidget} = require("devtools/client/shared/widgets/TableWidget");
+
+var doc, table;
+
+function test() {
+ waitForExplicitFinish();
+ let win = Services.ww.openWindow(null, TEST_URI, "_blank", TEST_OPT, null);
+
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad, false);
+
+ waitForFocus(function () {
+ doc = win.document;
+ table = new TableWidget(doc.querySelector("box"), {
+ initialColumns: {
+ col1: "Column 1",
+ col2: "Column 2",
+ col3: "Column 3",
+ col4: "Column 4"
+ },
+ uniqueId: "col1",
+ emptyText: "This is dummy empty text",
+ highlightUpdated: true,
+ removableColumns: true,
+ wrapTextInElements: true,
+ });
+ startTests();
+ });
+ });
+}
+
+function endTests() {
+ table.destroy();
+ doc.defaultView.close();
+ doc = table = null;
+ finish();
+}
+
+var startTests = Task.async(function* () {
+ populateTable();
+ yield testMouseInteraction();
+ endTests();
+});
+
+function populateTable() {
+ table.push({
+ col1: "id1",
+ col2: "value10",
+ col3: "value20",
+ col4: "value30"
+ });
+ table.push({
+ col1: "id2",
+ col2: "value14",
+ col3: "value29",
+ col4: "value32"
+ });
+ table.push({
+ col1: "id3",
+ col2: "value17",
+ col3: "value21",
+ col4: "value31",
+ extraData: "foobar",
+ extraData2: 42
+ });
+ table.push({
+ col1: "id4",
+ col2: "value12",
+ col3: "value26",
+ col4: "value33"
+ });
+ table.push({
+ col1: "id5",
+ col2: "value19",
+ col3: "value26",
+ col4: "value37"
+ });
+ table.push({
+ col1: "id6",
+ col2: "value15",
+ col3: "value25",
+ col4: "value37"
+ });
+ table.push({
+ col1: "id7",
+ col2: "value18",
+ col3: "value21",
+ col4: "value36",
+ somethingExtra: "Hello World!"
+ });
+ table.push({
+ col1: "id8",
+ col2: "value11",
+ col3: "value27",
+ col4: "value34"
+ });
+ table.push({
+ col1: "id9",
+ col2: "value11",
+ col3: "value23",
+ col4: "value38"
+ });
+}
+
+// Sends a click event on the passed DOM node in an async manner
+function click(node, button = 0) {
+ if (button == 0) {
+ executeSoon(() => EventUtils.synthesizeMouseAtCenter(node, {},
+ doc.defaultView));
+ } else {
+ executeSoon(() => EventUtils.synthesizeMouseAtCenter(node, {
+ button: button,
+ type: "contextmenu"
+ }, doc.defaultView));
+ }
+}
+
+/**
+ * Tests if clicking the table items does the expected behavior
+ */
+var testMouseInteraction = Task.async(function* () {
+ info("Testing mouse interaction with the table");
+ ok(!table.selectedRow, "Nothing should be selected beforehand");
+
+ let event = table.once(TableWidget.EVENTS.ROW_SELECTED);
+ let firstColumnFirstRowCell = table.tbody.firstChild.firstChild.children[1];
+ info("clicking on the first row");
+ ok(!firstColumnFirstRowCell.classList.contains("theme-selected"),
+ "Node should not have selected class before clicking");
+ click(firstColumnFirstRowCell);
+ let id = yield event;
+ ok(firstColumnFirstRowCell.classList.contains("theme-selected"),
+ "Node has selected class after click");
+ is(id, "id1", "Correct row was selected");
+
+ info("clicking on second row to select it");
+ event = table.once(TableWidget.EVENTS.ROW_SELECTED);
+ let firstColumnSecondRowCell = table.tbody.firstChild.firstChild.children[2];
+ // node should not have selected class
+ ok(!firstColumnSecondRowCell.classList.contains("theme-selected"),
+ "New node should not have selected class before clicking");
+ click(firstColumnSecondRowCell);
+ id = yield event;
+ ok(firstColumnSecondRowCell.classList.contains("theme-selected"),
+ "New node has selected class after clicking");
+ is(id, "id2", "Correct table path is emitted for new node");
+ isnot(firstColumnFirstRowCell, firstColumnSecondRowCell,
+ "Old and new node are different");
+ ok(!firstColumnFirstRowCell.classList.contains("theme-selected"),
+ "Old node should not have selected class after the click on new node");
+
+ info("clicking on the third row cell content to select third row");
+ event = table.once(TableWidget.EVENTS.ROW_SELECTED);
+ let firstColumnThirdRowCell = table.tbody.firstChild.firstChild.children[3];
+ let firstColumnThirdRowCellInnerNode = firstColumnThirdRowCell.querySelector("span");
+ // node should not have selected class
+ ok(!firstColumnThirdRowCell.classList.contains("theme-selected"),
+ "New node should not have selected class before clicking");
+ click(firstColumnThirdRowCellInnerNode);
+ id = yield event;
+ ok(firstColumnThirdRowCell.classList.contains("theme-selected"),
+ "New node has selected class after clicking the cell content");
+ is(id, "id3", "Correct table path is emitted for new node");
+
+ // clicking on table header to sort by it
+ event = table.once(TableWidget.EVENTS.COLUMN_SORTED);
+ let node = table.tbody.children[6].firstChild.children[0];
+ info("clicking on the 4th coulmn header to sort the table by it");
+ ok(!node.hasAttribute("sorted"),
+ "Node should not have sorted attribute before clicking");
+ ok(doc.querySelector("[sorted]"),
+ "Although, something else should be sorted on");
+ isnot(doc.querySelector("[sorted]"), node, "Which is not equal to this node");
+ click(node);
+ id = yield event;
+ is(id, "col4", "Correct column was sorted on");
+ ok(node.hasAttribute("sorted"),
+ "Node should now have sorted attribute after clicking");
+ is(doc.querySelectorAll("[sorted]").length, 1,
+ "Now only one column should be sorted on");
+ is(doc.querySelector("[sorted]"), node, "Which should be this column");
+
+ // test context menu opening.
+ // hiding second column
+ // event listener for popupshown
+ info("right click on the first column header");
+ node = table.tbody.firstChild.firstChild.firstChild;
+ let onPopupShown = once(table.menupopup, "popupshown");
+ click(node, 2);
+ yield onPopupShown;
+
+ is(table.menupopup.querySelectorAll("[disabled]").length, 1,
+ "Only 1 menuitem is disabled");
+ is(table.menupopup.querySelector("[disabled]"),
+ table.menupopup.querySelector("[data-id='col1']"),
+ "Which is the unique column");
+ // popup should be open now
+ // clicking on second column label
+ let onPopupHidden = once(table.menupopup, "popuphidden");
+ event = table.once(TableWidget.EVENTS.HEADER_CONTEXT_MENU);
+ node = table.menupopup.querySelector("[data-id='col2']");
+ info("selecting to hide the second column");
+ ok(!table.tbody.children[2].hasAttribute("hidden"),
+ "Column is not hidden before hiding it");
+ click(node);
+ id = yield event;
+ yield onPopupHidden;
+ is(id, "col2", "Correct column was triggered to be hidden");
+ is(table.tbody.children[2].getAttribute("hidden"), "true",
+ "Column is hidden after hiding it");
+
+ // hiding third column
+ // event listener for popupshown
+ info("right clicking on the first column header");
+ node = table.tbody.firstChild.firstChild.firstChild;
+ onPopupShown = once(table.menupopup, "popupshown");
+ click(node, 2);
+ yield onPopupShown;
+
+ is(table.menupopup.querySelectorAll("[disabled]").length, 1,
+ "Only 1 menuitem is disabled");
+ // popup should be open now
+ // clicking on second column label
+ onPopupHidden = once(table.menupopup, "popuphidden");
+ event = table.once(TableWidget.EVENTS.HEADER_CONTEXT_MENU);
+ node = table.menupopup.querySelector("[data-id='col3']");
+ info("selecting to hide the second column");
+ ok(!table.tbody.children[4].hasAttribute("hidden"),
+ "Column is not hidden before hiding it");
+ click(node);
+ id = yield event;
+ yield onPopupHidden;
+ is(id, "col3", "Correct column was triggered to be hidden");
+ is(table.tbody.children[4].getAttribute("hidden"), "true",
+ "Column is hidden after hiding it");
+
+ // opening again to see if 2 items are disabled now
+ // event listener for popupshown
+ info("right clicking on the first column header");
+ node = table.tbody.firstChild.firstChild.firstChild;
+ onPopupShown = once(table.menupopup, "popupshown");
+ click(node, 2);
+ yield onPopupShown;
+
+ is(table.menupopup.querySelectorAll("[disabled]").length, 2,
+ "2 menuitems are disabled now as only 2 columns remain visible");
+ is(table.menupopup.querySelectorAll("[disabled]")[0],
+ table.menupopup.querySelector("[data-id='col1']"),
+ "First is the unique column");
+ is(table.menupopup.querySelectorAll("[disabled]")[1],
+ table.menupopup.querySelector("[data-id='col4']"),
+ "Second is the last column");
+
+ // showing back 2nd column
+ // popup should be open now
+ // clicking on second column label
+ onPopupHidden = once(table.menupopup, "popuphidden");
+ event = table.once(TableWidget.EVENTS.HEADER_CONTEXT_MENU);
+ node = table.menupopup.querySelector("[data-id='col2']");
+ info("selecting to hide the second column");
+ is(table.tbody.children[2].getAttribute("hidden"), "true",
+ "Column is hidden before unhiding it");
+ click(node);
+ id = yield event;
+ yield onPopupHidden;
+ is(id, "col2", "Correct column was triggered to be hidden");
+ ok(!table.tbody.children[2].hasAttribute("hidden"),
+ "Column is not hidden after unhiding it");
+
+ // showing back 3rd column
+ // event listener for popupshown
+ info("right clicking on the first column header");
+ node = table.tbody.firstChild.firstChild.firstChild;
+ onPopupShown = once(table.menupopup, "popupshown");
+ click(node, 2);
+ yield onPopupShown;
+
+ // popup should be open now
+ // clicking on second column label
+ onPopupHidden = once(table.menupopup, "popuphidden");
+ event = table.once(TableWidget.EVENTS.HEADER_CONTEXT_MENU);
+ node = table.menupopup.querySelector("[data-id='col3']");
+ info("selecting to hide the second column");
+ is(table.tbody.children[4].getAttribute("hidden"), "true",
+ "Column is hidden before unhiding it");
+ click(node);
+ id = yield event;
+ yield onPopupHidden;
+ is(id, "col3", "Correct column was triggered to be hidden");
+ ok(!table.tbody.children[4].hasAttribute("hidden"),
+ "Column is not hidden after unhiding it");
+
+ // reset table state
+ table.clearSelection();
+ table.sortBy("col1");
+});
diff --git a/devtools/client/shared/test/browser_telemetry_button_eyedropper.js b/devtools/client/shared/test/browser_telemetry_button_eyedropper.js
new file mode 100644
index 000000000..76546ce83
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_button_eyedropper.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_button_eyedropper.js</p><div>test</div>";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target, "inspector");
+ info("inspector opened");
+
+ info("testing the eyedropper button");
+ yield testButton(toolbox, Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ yield gDevTools.closeToolbox(target);
+ gBrowser.removeCurrentTab();
+});
+
+function* testButton(toolbox, Telemetry) {
+ info("Calling the eyedropper button's callback");
+ // We call the button callback directly because we don't need to test the UI here, we're
+ // only concerned about testing the telemetry probe.
+ yield toolbox.getPanel("inspector").showEyeDropper();
+
+ checkResults("_EYEDROPPER_", Telemetry);
+}
+
+function checkResults(histIdFocus, Telemetry) {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Object.entries(result)) {
+ if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
+ !histId.includes(histIdFocus)) {
+ // Inspector stats are tested in
+ // browser_telemetry_toolboxtabs_{toolname}.js so we skip them here
+ // because we only open the inspector once for this test.
+ continue;
+ }
+
+ if (histId.endsWith("OPENED_COUNT")) {
+ is(value.length, 1, histId + " has one entry");
+
+ let okay = value.every(element => element === true);
+ ok(okay, "All " + histId + " entries are === true");
+ }
+ }
+}
diff --git a/devtools/client/shared/test/browser_telemetry_button_paintflashing.js b/devtools/client/shared/test/browser_telemetry_button_paintflashing.js
new file mode 100644
index 000000000..dcce8f738
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_button_paintflashing.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_button_paintflashing.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target, "inspector");
+ info("inspector opened");
+
+ info("testing the paintflashing button");
+ yield testButton(toolbox, Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ yield gDevTools.closeToolbox(target);
+ gBrowser.removeCurrentTab();
+});
+
+function* testButton(toolbox, Telemetry) {
+ info("Testing command-button-paintflashing");
+
+ let button = toolbox.doc.querySelector("#command-button-paintflashing");
+ ok(button, "Captain, we have the button");
+
+ yield* delayedClicks(toolbox, button, 4);
+ checkResults("_PAINTFLASHING_", Telemetry);
+}
+
+function* delayedClicks(toolbox, node, clicks) {
+ for (let i = 0; i < clicks; i++) {
+ yield new Promise(resolve => {
+ // See TOOL_DELAY for why we need setTimeout here
+ setTimeout(() => resolve(), TOOL_DELAY);
+ });
+
+ // this event will fire once the command execution starts and
+ // the output object is created
+ let clicked = toolbox._requisition.commandOutputManager.onOutput.once();
+
+ info("Clicking button " + node.id);
+ node.click();
+
+ let outputEvent = yield clicked;
+ // promise gets resolved once execution finishes and output is ready
+ yield outputEvent.output.promise;
+ }
+}
+
+function checkResults(histIdFocus, Telemetry) {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Object.entries(result)) {
+ if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
+ !histId.includes(histIdFocus)) {
+ // Inspector stats are tested in
+ // browser_telemetry_toolboxtabs_{toolname}.js so we skip them here
+ // because we only open the inspector once for this test.
+ continue;
+ }
+
+ if (histId.endsWith("OPENED_COUNT")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function (element) {
+ return element === true;
+ });
+
+ ok(okay, "All " + histId + " entries are === true");
+ } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function (element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ }
+ }
+}
diff --git a/devtools/client/shared/test/browser_telemetry_button_responsive.js b/devtools/client/shared/test/browser_telemetry_button_responsive.js
new file mode 100644
index 000000000..41e53f0f8
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_button_responsive.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_button_responsive.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+const { ResponsiveUIManager } = Cu.import("resource://devtools/client/responsivedesign/responsivedesign.jsm", {});
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target, "inspector");
+ info("inspector opened");
+
+ info("testing the responsivedesign button");
+ yield testButton(toolbox, Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ yield gDevTools.closeToolbox(target);
+ gBrowser.removeCurrentTab();
+});
+
+function* testButton(toolbox, Telemetry) {
+ info("Testing command-button-responsive");
+
+ let button = toolbox.doc.querySelector("#command-button-responsive");
+ ok(button, "Captain, we have the button");
+
+ yield delayedClicks(button, 4);
+
+ checkResults("_RESPONSIVE_", Telemetry);
+}
+
+function waitForToggle() {
+ return new Promise(resolve => {
+ let handler = () => {
+ ResponsiveUIManager.off("on", handler);
+ ResponsiveUIManager.off("off", handler);
+ resolve();
+ };
+ ResponsiveUIManager.on("on", handler);
+ ResponsiveUIManager.on("off", handler);
+ });
+}
+
+var delayedClicks = Task.async(function* (node, clicks) {
+ for (let i = 0; i < clicks; i++) {
+ info("Clicking button " + node.id);
+ let toggled = waitForToggle();
+ node.click();
+ yield toggled;
+ // See TOOL_DELAY for why we need setTimeout here
+ yield DevToolsUtils.waitForTime(TOOL_DELAY);
+ }
+});
+
+function checkResults(histIdFocus, Telemetry) {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Object.entries(result)) {
+ if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
+ !histId.includes(histIdFocus)) {
+ // Inspector stats are tested in
+ // browser_telemetry_toolboxtabs_{toolname}.js so we skip them here
+ // because we only open the inspector once for this test.
+ continue;
+ }
+
+ if (histId.endsWith("OPENED_COUNT")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function (element) {
+ return element === true;
+ });
+
+ ok(okay, "All " + histId + " entries are === true");
+ } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function (element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ }
+ }
+}
diff --git a/devtools/client/shared/test/browser_telemetry_button_scratchpad.js b/devtools/client/shared/test/browser_telemetry_button_scratchpad.js
new file mode 100644
index 000000000..e191bb257
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_button_scratchpad.js
@@ -0,0 +1,127 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_button_scratchpad.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target, "inspector");
+ info("inspector opened");
+
+ let onAllWindowsOpened = trackScratchpadWindows();
+
+ info("testing the scratchpad button");
+ yield testButton(toolbox, Telemetry);
+ yield onAllWindowsOpened;
+
+ checkResults("_SCRATCHPAD_", Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ yield gDevTools.closeToolbox(target);
+ gBrowser.removeCurrentTab();
+});
+
+function trackScratchpadWindows() {
+ info("register the window observer to track when scratchpad windows open");
+
+ let numScratchpads = 0;
+
+ return new Promise(resolve => {
+ Services.ww.registerNotification(function observer(subject, topic) {
+ if (topic == "domwindowopened") {
+ let win = subject.QueryInterface(Ci.nsIDOMWindow);
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad, false);
+
+ if (win.Scratchpad) {
+ win.Scratchpad.addObserver({
+ onReady: function () {
+ win.Scratchpad.removeObserver(this);
+ numScratchpads++;
+ win.close();
+
+ info("another scratchpad was opened and closed, " +
+ `count is now ${numScratchpads}`);
+
+ if (numScratchpads === 4) {
+ Services.ww.unregisterNotification(observer);
+ info("4 scratchpads have been opened and closed, checking results");
+ resolve();
+ }
+ },
+ });
+ }
+ }, false);
+ }
+ });
+ });
+}
+
+function* testButton(toolbox, Telemetry) {
+ info("Testing command-button-scratchpad");
+ let button = toolbox.doc.querySelector("#command-button-scratchpad");
+ ok(button, "Captain, we have the button");
+
+ yield delayedClicks(button, 4);
+}
+
+function delayedClicks(node, clicks) {
+ return new Promise(resolve => {
+ let clicked = 0;
+
+ // See TOOL_DELAY for why we need setTimeout here
+ setTimeout(function delayedClick() {
+ info("Clicking button " + node.id);
+ node.click();
+ clicked++;
+
+ if (clicked >= clicks) {
+ resolve(node);
+ } else {
+ setTimeout(delayedClick, TOOL_DELAY);
+ }
+ }, TOOL_DELAY);
+ });
+}
+
+function checkResults(histIdFocus, Telemetry) {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Object.entries(result)) {
+ if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
+ !histId.includes(histIdFocus)) {
+ // Inspector stats are tested in
+ // browser_telemetry_toolboxtabs_{toolname}.js so we skip them here
+ // because we only open the inspector once for this test.
+ continue;
+ }
+
+ if (histId.endsWith("OPENED_COUNT")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function (element) {
+ return element === true;
+ });
+
+ ok(okay, "All " + histId + " entries are === true");
+ } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function (element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ }
+ }
+}
diff --git a/devtools/client/shared/test/browser_telemetry_sidebar.js b/devtools/client/shared/test/browser_telemetry_sidebar.js
new file mode 100644
index 000000000..8a8f35578
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_sidebar.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_sidebar.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target, "inspector");
+ info("inspector opened");
+
+ yield testSidebar(toolbox);
+ checkResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ yield gDevTools.closeToolbox(target);
+ gBrowser.removeCurrentTab();
+});
+
+function* testSidebar(toolbox) {
+ info("Testing sidebar");
+
+ let inspector = toolbox.getCurrentPanel();
+ let sidebarTools = ["ruleview", "computedview", "fontinspector",
+ "animationinspector"];
+
+ // Concatenate the array with itself so that we can open each tool twice.
+ sidebarTools.push.apply(sidebarTools, sidebarTools);
+
+ return new Promise(resolve => {
+ // See TOOL_DELAY for why we need setTimeout here
+ setTimeout(function selectSidebarTab() {
+ let tool = sidebarTools.pop();
+ if (tool) {
+ inspector.sidebar.select(tool);
+ setTimeout(function () {
+ setTimeout(selectSidebarTab, TOOL_DELAY);
+ }, TOOL_DELAY);
+ } else {
+ resolve();
+ }
+ }, TOOL_DELAY);
+ });
+}
+
+function checkResults(Telemetry) {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Object.entries(result)) {
+ if (histId.startsWith("DEVTOOLS_INSPECTOR_")) {
+ // Inspector stats are tested in browser_telemetry_toolboxtabs.js so we
+ // skip them here because we only open the inspector once for this test.
+ continue;
+ }
+
+ if (histId === "DEVTOOLS_TOOLBOX_OPENED_COUNT") {
+ is(value.length, 1, histId + " has only one entry");
+ } else if (histId.endsWith("OPENED_COUNT")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function (element) {
+ return element === true;
+ });
+
+ ok(okay, "All " + histId + " entries are === true");
+ } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function (element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ }
+ }
+}
diff --git a/devtools/client/shared/test/browser_telemetry_toolbox.js b/devtools/client/shared/test/browser_telemetry_toolbox.js
new file mode 100644
index 000000000..85328cf14
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolbox.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolbox.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(3, TOOL_DELAY, "inspector");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_canvasdebugger.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_canvasdebugger.js
new file mode 100644
index 000000000..81ab9470c
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_canvasdebugger.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_canvasdebugger.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ info("Activate the canvasdebugger");
+ let originalPref = Services.prefs.getBoolPref("devtools.canvasdebugger.enabled");
+ Services.prefs.setBoolPref("devtools.canvasdebugger.enabled", true);
+
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "canvasdebugger");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+
+ info("De-activate the canvasdebugger");
+ Services.prefs.setBoolPref("devtools.canvasdebugger.enabled", originalPref);
+});
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_inspector.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_inspector.js
new file mode 100644
index 000000000..a50c8d203
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_inspector.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_inspector.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "inspector");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_jsdebugger.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_jsdebugger.js
new file mode 100644
index 000000000..ba1c26643
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_jsdebugger.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_jsdebugger.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "jsdebugger");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_jsprofiler.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_jsprofiler.js
new file mode 100644
index 000000000..0a5dfb048
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_jsprofiler.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_jsprofiler.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "performance");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_netmonitor.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_netmonitor.js
new file mode 100644
index 000000000..6d5292b11
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_netmonitor.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_netmonitor.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "netmonitor");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
+
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_options.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_options.js
new file mode 100644
index 000000000..26b3bf77c
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_options.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_options.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "options");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_shadereditor.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_shadereditor.js
new file mode 100644
index 000000000..5cf1eb0a6
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_shadereditor.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed(
+ "Error: Shader Editor is still waiting for a WebGL context to be created.");
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_shadereditor.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+const TOOL_PREF = "devtools.shadereditor.enabled";
+
+add_task(function* () {
+ info("Active the sharer editor");
+ let originalPref = Services.prefs.getBoolPref(TOOL_PREF);
+ Services.prefs.setBoolPref(TOOL_PREF, true);
+
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "shadereditor");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+
+ info("De-activate the sharer editor");
+ Services.prefs.setBoolPref(TOOL_PREF, originalPref);
+});
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_storage.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_storage.js
new file mode 100644
index 000000000..838b06fcb
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_storage.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_storage.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 1000;
+
+add_task(function* () {
+ info("Activating the storage inspector");
+ Services.prefs.setBoolPref("devtools.storage.enabled", true);
+
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "storage");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+
+ info("De-activating the storage inspector");
+ Services.prefs.clearUserPref("devtools.storage.enabled");
+});
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_styleeditor.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_styleeditor.js
new file mode 100644
index 000000000..cdd9e3fb3
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_styleeditor.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_styleeditor.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "styleeditor");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
+
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_webaudioeditor.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_webaudioeditor.js
new file mode 100644
index 000000000..a75ebad1d
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_webaudioeditor.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_webaudioeditor.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ info("Activating the webaudioeditor");
+ let originalPref = Services.prefs.getBoolPref("devtools.webaudioeditor.enabled");
+ Services.prefs.setBoolPref("devtools.webaudioeditor.enabled", true);
+
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "webaudioeditor");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+
+ info("De-activating the webaudioeditor");
+ Services.prefs.setBoolPref("devtools.webaudioeditor.enabled", originalPref);
+});
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_webconsole.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_webconsole.js
new file mode 100644
index 000000000..4e15dbf4b
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_webconsole.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_styleeditor_webconsole.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "webconsole");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/shared/test/browser_templater_basic.html b/devtools/client/shared/test/browser_templater_basic.html
new file mode 100644
index 000000000..473c731f3
--- /dev/null
+++ b/devtools/client/shared/test/browser_templater_basic.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<html>
+<head>
+ <title>DOM Template Tests</title>
+</head>
+<body>
+
+</body>
+</html>
+
diff --git a/devtools/client/shared/test/browser_templater_basic.js b/devtools/client/shared/test/browser_templater_basic.js
new file mode 100644
index 000000000..256900cf5
--- /dev/null
+++ b/devtools/client/shared/test/browser_templater_basic.js
@@ -0,0 +1,286 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the DOM Template engine works properly
+
+/*
+ * These tests run both in Mozilla/Mochitest and plain browsers (as does
+ * domtemplate)
+ * We should endevour to keep the source in sync.
+ */
+
+const {template} = require("devtools/shared/gcli/templater");
+
+const TEST_URI = TEST_URI_ROOT + "browser_templater_basic.html";
+
+var test = Task.async(function* () {
+ yield addTab("about:blank");
+ let [host,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Starting DOM Templater Tests");
+ runTest(0, host, doc);
+});
+
+function runTest(index, host, doc) {
+ let options = tests[index] = tests[index]();
+ let holder = doc.createElement("div");
+ holder.id = options.name;
+ let body = doc.body;
+ body.appendChild(holder);
+ holder.innerHTML = options.template;
+
+ info("Running " + options.name);
+ template(holder, options.data, options.options);
+
+ if (typeof options.result == "string") {
+ is(holder.innerHTML, options.result, options.name);
+ } else {
+ ok(holder.innerHTML.match(options.result) != null,
+ options.name + " result='" + holder.innerHTML + "'");
+ }
+
+ if (options.also) {
+ options.also(options);
+ }
+
+ function runNextTest() {
+ index++;
+ if (index < tests.length) {
+ runTest(index, host, doc);
+ } else {
+ finished(host);
+ }
+ }
+
+ if (options.later) {
+ let ais = is.bind(this);
+
+ function createTester(testHolder, testOptions) {
+ return () => {
+ ais(testHolder.innerHTML, testOptions.later, testOptions.name + " later");
+ runNextTest();
+ };
+ }
+
+ executeSoon(createTester(holder, options));
+ } else {
+ runNextTest();
+ }
+}
+
+function finished(host) {
+ host.destroy();
+ gBrowser.removeCurrentTab();
+ info("Finishing DOM Templater Tests");
+ tests = null;
+ finish();
+}
+
+/**
+ * Why have an array of functions that return data rather than just an array
+ * of the data itself? Some of these tests contain calls to delayReply() which
+ * sets up async processing using executeSoon(). Since the execution of these
+ * tests is asynchronous, the delayed reply will probably arrive before the
+ * test is executed, making the test be synchronous. So we wrap the data in a
+ * function so we only set it up just before we use it.
+ */
+var tests = [
+ () => ({
+ name: "simpleNesting",
+ template: '<div id="ex1">${nested.value}</div>',
+ data: { nested: { value: "pass 1" } },
+ result: '<div id="ex1">pass 1</div>'
+ }),
+
+ () => ({
+ name: "returnDom",
+ template: '<div id="ex2">${__element.ownerDocument.createTextNode(\'pass 2\')}</div>',
+ options: { allowEval: true },
+ data: {},
+ result: '<div id="ex2">pass 2</div>'
+ }),
+
+ () => ({
+ name: "srcChange",
+ template: '<img _src="${fred}" id="ex3">',
+ data: { fred: "green.png" },
+ result: /<img( id="ex3")? src="green.png"( id="ex3")?>/
+ }),
+
+ () => ({
+ name: "ifTrue",
+ template: '<p if="${name !== \'jim\'}">hello ${name}</p>',
+ options: { allowEval: true },
+ data: { name: "fred" },
+ result: "<p>hello fred</p>"
+ }),
+
+ () => ({
+ name: "ifFalse",
+ template: '<p if="${name !== \'jim\'}">hello ${name}</p>',
+ options: { allowEval: true },
+ data: { name: "jim" },
+ result: ""
+ }),
+
+ () => ({
+ name: "simpleLoop",
+ template: '<p foreach="index in ${[ 1, 2, 3 ]}">${index}</p>',
+ options: { allowEval: true },
+ data: {},
+ result: "<p>1</p><p>2</p><p>3</p>"
+ }),
+
+ () => ({
+ name: "loopElement",
+ template: '<loop foreach="i in ${array}">${i}</loop>',
+ data: { array: [ 1, 2, 3 ] },
+ result: "123"
+ }),
+
+ // Bug 692028: DOMTemplate memory leak with asynchronous arrays
+ // Bug 692031: DOMTemplate async loops do not drop the loop element
+ () => ({
+ name: "asyncLoopElement",
+ template: '<loop foreach="i in ${array}">${i}</loop>',
+ data: { array: delayReply([1, 2, 3]) },
+ result: "<span></span>",
+ later: "123"
+ }),
+
+ () => ({
+ name: "saveElement",
+ template: '<p save="${element}">${name}</p>',
+ data: { name: "pass 8" },
+ result: "<p>pass 8</p>",
+ also: function (options) {
+ ok(options.data.element.innerHTML, "pass 9", "saveElement saved");
+ delete options.data.element;
+ }
+ }),
+
+ () => ({
+ name: "useElement",
+ template: '<p id="pass9">${adjust(__element)}</p>',
+ options: { allowEval: true },
+ data: {
+ adjust: function (element) {
+ is("pass9", element.id, "useElement adjust");
+ return "pass 9b";
+ }
+ },
+ result: '<p id="pass9">pass 9b</p>'
+ }),
+
+ () => ({
+ name: "asyncInline",
+ template: "${delayed}",
+ data: { delayed: delayReply("inline") },
+ result: "<span></span>",
+ later: "inline"
+ }),
+
+ // Bug 692028: DOMTemplate memory leak with asynchronous arrays
+ () => ({
+ name: "asyncArray",
+ template: '<p foreach="i in ${delayed}">${i}</p>',
+ data: { delayed: delayReply([1, 2, 3]) },
+ result: "<span></span>",
+ later: "<p>1</p><p>2</p><p>3</p>"
+ }),
+
+ () => ({
+ name: "asyncMember",
+ template: '<p foreach="i in ${delayed}">${i}</p>',
+ data: { delayed: [delayReply(4), delayReply(5), delayReply(6)] },
+ result: "<span></span><span></span><span></span>",
+ later: "<p>4</p><p>5</p><p>6</p>"
+ }),
+
+ // Bug 692028: DOMTemplate memory leak with asynchronous arrays
+ () => ({
+ name: "asyncBoth",
+ template: '<p foreach="i in ${delayed}">${i}</p>',
+ data: {
+ delayed: delayReply([
+ delayReply(4),
+ delayReply(5),
+ delayReply(6)
+ ])
+ },
+ result: "<span></span>",
+ later: "<p>4</p><p>5</p><p>6</p>"
+ }),
+
+ // Bug 701762: DOMTemplate fails when ${foo()} returns undefined
+ () => ({
+ name: "functionReturningUndefiend",
+ template: "<p>${foo()}</p>",
+ options: { allowEval: true },
+ data: {
+ foo: function () {}
+ },
+ result: "<p>undefined</p>"
+ }),
+
+ // Bug 702642: DOMTemplate is relatively slow when evaluating JS ${}
+ () => ({
+ name: "propertySimple",
+ template: "<p>${a.b.c}</p>",
+ data: { a: { b: { c: "hello" } } },
+ result: "<p>hello</p>"
+ }),
+
+ () => ({
+ name: "propertyPass",
+ template: "<p>${Math.max(1, 2)}</p>",
+ options: { allowEval: true },
+ result: "<p>2</p>"
+ }),
+
+ () => ({
+ name: "propertyFail",
+ template: "<p>${Math.max(1, 2)}</p>",
+ result: "<p>${Math.max(1, 2)}</p>"
+ }),
+
+ // Bug 723431: DOMTemplate should allow customisation of display of
+ // null/undefined values
+ () => ({
+ name: "propertyUndefAttrFull",
+ template: "<p>${nullvar}|${undefinedvar1}|${undefinedvar2}</p>",
+ data: { nullvar: null, undefinedvar1: undefined },
+ result: "<p>null|undefined|undefined</p>"
+ }),
+
+ () => ({
+ name: "propertyUndefAttrBlank",
+ template: "<p>${nullvar}|${undefinedvar1}|${undefinedvar2}</p>",
+ data: { nullvar: null, undefinedvar1: undefined },
+ options: { blankNullUndefined: true },
+ result: "<p>||</p>"
+ }),
+
+ /* eslint-disable max-len */
+ () => ({
+ name: "propertyUndefAttrFull",
+ template: '<div><p value="${nullvar}"></p><p value="${undefinedvar1}"></p><p value="${undefinedvar2}"></p></div>',
+ data: { nullvar: null, undefinedvar1: undefined },
+ result: '<div><p value="null"></p><p value="undefined"></p><p value="undefined"></p></div>'
+ }),
+
+ () => ({
+ name: "propertyUndefAttrBlank",
+ template: '<div><p value="${nullvar}"></p><p value="${undefinedvar1}"></p><p value="${undefinedvar2}"></p></div>',
+ data: { nullvar: null, undefinedvar1: undefined },
+ options: { blankNullUndefined: true },
+ result: '<div><p value=""></p><p value=""></p><p value=""></p></div>'
+ })
+ /* eslint-enable max-len */
+];
+
+function delayReply(data) {
+ return new Promise(resolve => resolve(data));
+}
diff --git a/devtools/client/shared/test/browser_theme.js b/devtools/client/shared/test/browser_theme.js
new file mode 100644
index 000000000..174e5aeec
--- /dev/null
+++ b/devtools/client/shared/test/browser_theme.js
@@ -0,0 +1,98 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that theme utilities work
+
+const {getColor, getTheme, setTheme} = require("devtools/client/shared/theme");
+
+add_task(function* () {
+ testGetTheme();
+ testSetTheme();
+ testGetColor();
+ testColorExistence();
+});
+
+function testGetTheme() {
+ let originalTheme = getTheme();
+ ok(originalTheme, "has some theme to start with.");
+ Services.prefs.setCharPref("devtools.theme", "light");
+ is(getTheme(), "light", "getTheme() correctly returns light theme");
+ Services.prefs.setCharPref("devtools.theme", "dark");
+ is(getTheme(), "dark", "getTheme() correctly returns dark theme");
+ Services.prefs.setCharPref("devtools.theme", "firebug");
+ is(getTheme(), "firebug", "getTheme() correctly returns firebug theme");
+ Services.prefs.setCharPref("devtools.theme", "unknown");
+ is(getTheme(), "unknown", "getTheme() correctly returns an unknown theme");
+ Services.prefs.setCharPref("devtools.theme", originalTheme);
+}
+
+function testSetTheme() {
+ let originalTheme = getTheme();
+ gDevTools.once("pref-changed", (_, { pref, oldValue, newValue }) => {
+ is(pref, "devtools.theme",
+ "The 'pref-changed' event triggered by setTheme has correct pref.");
+ is(oldValue, originalTheme,
+ "The 'pref-changed' event triggered by setTheme has correct oldValue.");
+ is(newValue, "dark",
+ "The 'pref-changed' event triggered by setTheme has correct newValue.");
+ });
+ setTheme("dark");
+ is(Services.prefs.getCharPref("devtools.theme"), "dark",
+ "setTheme() correctly sets dark theme.");
+ setTheme("light");
+ is(Services.prefs.getCharPref("devtools.theme"), "light",
+ "setTheme() correctly sets light theme.");
+ setTheme("firebug");
+ is(Services.prefs.getCharPref("devtools.theme"), "firebug",
+ "setTheme() correctly sets firebug theme.");
+ setTheme("unknown");
+ is(Services.prefs.getCharPref("devtools.theme"), "unknown",
+ "setTheme() correctly sets an unknown theme.");
+ Services.prefs.setCharPref("devtools.theme", originalTheme);
+}
+
+function testGetColor() {
+ let BLUE_DARK = "#46afe3";
+ let BLUE_LIGHT = "#0088cc";
+ let BLUE_FIREBUG = "#3455db";
+ let originalTheme = getTheme();
+
+ setTheme("dark");
+ is(getColor("highlight-blue"), BLUE_DARK, "correctly gets color for enabled theme.");
+ setTheme("light");
+ is(getColor("highlight-blue"), BLUE_LIGHT, "correctly gets color for enabled theme.");
+ setTheme("firebug");
+ is(getColor("highlight-blue"), BLUE_FIREBUG, "correctly gets color for enabled theme.");
+ setTheme("metal");
+ is(getColor("highlight-blue"), BLUE_LIGHT,
+ "correctly uses light for default theme if enabled theme not found");
+
+ is(getColor("highlight-blue", "dark"), BLUE_DARK,
+ "if provided and found, uses the provided theme.");
+ is(getColor("highlight-blue", "firebug"), BLUE_FIREBUG,
+ "if provided and found, uses the provided theme.");
+ is(getColor("highlight-blue", "metal"), BLUE_LIGHT,
+ "if provided and not found, defaults to light theme.");
+ is(getColor("somecomponents"), null, "if a type cannot be found, should return null.");
+
+ setTheme(originalTheme);
+}
+
+function testColorExistence() {
+ const vars = ["body-background", "sidebar-background", "contrast-background",
+ "tab-toolbar-background", "toolbar-background", "selection-background",
+ "selection-color", "selection-background-semitransparent", "splitter-color", "comment",
+ "body-color", "body-color-alt", "content-color1", "content-color2", "content-color3",
+ "highlight-green", "highlight-blue", "highlight-bluegrey", "highlight-purple",
+ "highlight-lightorange", "highlight-orange", "highlight-red", "highlight-pink"
+ ];
+
+ for (let type of vars) {
+ ok(getColor(type, "light"), `${type} is a valid color in light theme`);
+ ok(getColor(type, "dark"), `${type} is a valid color in light theme`);
+ ok(getColor(type, "firebug"), `${type} is a valid color in light theme`);
+ }
+}
diff --git a/devtools/client/shared/test/browser_theme_switching.js b/devtools/client/shared/test/browser_theme_switching.js
new file mode 100644
index 000000000..392462a67
--- /dev/null
+++ b/devtools/client/shared/test/browser_theme_switching.js
@@ -0,0 +1,53 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(function* () {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target);
+ let doc = toolbox.doc;
+ let root = doc.documentElement;
+
+ let platform = root.getAttribute("platform");
+ let expectedPlatform = getPlatform();
+ is(platform, expectedPlatform, ":root[platform] is correct");
+
+ let theme = Services.prefs.getCharPref("devtools.theme");
+ let className = "theme-" + theme;
+ ok(root.classList.contains(className),
+ ":root has " + className + " class (current theme)");
+
+ // Convert the xpath result into an array of strings
+ // like `href="{URL}" type="text/css"`
+ let sheetsIterator = doc.evaluate("processing-instruction('xml-stylesheet')",
+ doc, null, XPathResult.ANY_TYPE, null);
+ let sheetsInDOM = [];
+
+ /* eslint-disable no-cond-assign */
+ let sheet;
+ while (sheet = sheetsIterator.iterateNext()) {
+ sheetsInDOM.push(sheet.data);
+ }
+ /* eslint-enable no-cond-assign */
+
+ let sheetsFromTheme = gDevTools.getThemeDefinition(theme).stylesheets;
+ info("Checking for existence of " + sheetsInDOM.length + " sheets");
+ for (let themeSheet of sheetsFromTheme) {
+ ok(sheetsInDOM.some(s => s.includes(themeSheet)),
+ "There is a stylesheet for " + themeSheet);
+ }
+
+ yield toolbox.destroy();
+});
+
+function getPlatform() {
+ let {OS} = Services.appinfo;
+ if (OS == "WINNT") {
+ return "win";
+ } else if (OS == "Darwin") {
+ return "mac";
+ }
+ return "linux";
+}
diff --git a/devtools/client/shared/test/browser_toolbar_basic.html b/devtools/client/shared/test/browser_toolbar_basic.html
new file mode 100644
index 000000000..2ea3773b0
--- /dev/null
+++ b/devtools/client/shared/test/browser_toolbar_basic.html
@@ -0,0 +1,40 @@
+<!doctype html>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Developer Toolbar Tests</title>
+ <style type="text/css">
+ #single { color: red; }
+ </style>
+ <script type="text/javascript">
+ /* eslint-disable */
+ var a = 1;
+ </script>
+</head>
+<body>
+
+<p id=single>
+1
+</p>
+
+<p class=twin>
+2a
+</p>
+
+<p class=twin>
+2b
+</p>
+
+<style>
+.twin { color: blue; }
+</style>
+<script>
+/* eslint-disable */
+var b = 2;
+</script>
+
+</body>
+</html>
diff --git a/devtools/client/shared/test/browser_toolbar_basic.js b/devtools/client/shared/test/browser_toolbar_basic.js
new file mode 100644
index 000000000..12da27ab1
--- /dev/null
+++ b/devtools/client/shared/test/browser_toolbar_basic.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the developer toolbar works properly
+
+const TEST_URI = TEST_URI_ROOT + "browser_toolbar_basic.html";
+
+add_task(function* () {
+ info("Starting browser_toolbar_basic.js");
+ yield addTab(TEST_URI);
+
+ ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible in to start");
+
+ let shown = oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.SHOW);
+ document.getElementById("menu_devToolbar").doCommand();
+ yield shown;
+ ok(DeveloperToolbar.visible, "DeveloperToolbar is visible in checkOpen");
+
+ let close = document.getElementById("developer-toolbar-closebutton");
+ ok(close, "Close button exists");
+
+ let toggleToolbox =
+ document.getElementById("menu_devToolbox");
+ ok(!isChecked(toggleToolbox), "toggle toolbox button is not checked");
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ yield gDevTools.showToolbox(target, "inspector");
+ ok(isChecked(toggleToolbox), "toggle toolbox button is checked");
+
+ yield addTab("about:blank");
+ info("Opened a new tab");
+
+ ok(!isChecked(toggleToolbox), "toggle toolbox button is not checked");
+
+ gBrowser.removeCurrentTab();
+
+ let hidden = oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.HIDE);
+ document.getElementById("menu_devToolbar").doCommand();
+ yield hidden;
+ ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible in hidden");
+
+ shown = oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.SHOW);
+ document.getElementById("menu_devToolbar").doCommand();
+ yield shown;
+ ok(DeveloperToolbar.visible, "DeveloperToolbar is visible in after open");
+
+ ok(isChecked(toggleToolbox), "toggle toolbox button is checked");
+
+ hidden = oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.HIDE);
+ document.getElementById("developer-toolbar-closebutton").doCommand();
+ yield hidden;
+
+ ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible after re-close");
+});
+
+function isChecked(b) {
+ return b.getAttribute("checked") == "true";
+}
diff --git a/devtools/client/shared/test/browser_toolbar_tooltip.js b/devtools/client/shared/test/browser_toolbar_tooltip.js
new file mode 100644
index 000000000..bc09f705c
--- /dev/null
+++ b/devtools/client/shared/test/browser_toolbar_tooltip.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the developer toolbar works properly
+
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed(
+ "Protocol error (unknownError): Error: Got an invalid root window in DocumentWalker"
+);
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>Tooltip Tests</p>";
+const PREF_DEVTOOLS_THEME = "devtools.theme";
+
+registerCleanupFunction(() => {
+ // Set preferences back to their original values
+ Services.prefs.clearUserPref(PREF_DEVTOOLS_THEME);
+});
+
+add_task(function* showToolbar() {
+ yield addTab(TEST_URI);
+
+ info("Starting browser_toolbar_tooltip.js");
+
+ ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible in runTest");
+
+ let showPromise = observeOnce(DeveloperToolbar.NOTIFICATIONS.SHOW);
+ document.getElementById("menu_devToolbar").doCommand();
+ yield showPromise;
+});
+
+add_task(function* testDimensions() {
+ let tooltipPanel = DeveloperToolbar.tooltipPanel;
+
+ DeveloperToolbar.focusManager.helpRequest();
+ yield DeveloperToolbar.inputter.setInput("help help");
+
+ DeveloperToolbar.inputter.setCursor({ start: "help help".length });
+ is(tooltipPanel._dimensions.start, "help ".length,
+ "search param start, when cursor at end");
+ ok(getLeftMargin() > 30, "tooltip offset, when cursor at end");
+
+ DeveloperToolbar.inputter.setCursor({ start: "help".length });
+ is(tooltipPanel._dimensions.start, 0,
+ "search param start, when cursor at end of command");
+ ok(getLeftMargin() > 9, "tooltip offset, when cursor at end of command");
+
+ DeveloperToolbar.inputter.setCursor({ start: "help help".length - 1 });
+ is(tooltipPanel._dimensions.start, "help ".length,
+ "search param start, when cursor at penultimate position");
+ ok(getLeftMargin() > 30, "tooltip offset, when cursor at penultimate position");
+
+ DeveloperToolbar.inputter.setCursor({ start: 0 });
+ is(tooltipPanel._dimensions.start, 0,
+ "search param start, when cursor at start");
+ ok(getLeftMargin() > 9, "tooltip offset, when cursor at start");
+});
+
+add_task(function* testThemes() {
+ let tooltipPanel = DeveloperToolbar.tooltipPanel;
+ ok(tooltipPanel.document, "Tooltip panel is initialized");
+
+ Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "dark");
+
+ yield DeveloperToolbar.inputter.setInput("");
+ yield DeveloperToolbar.inputter.setInput("help help");
+ is(tooltipPanel.document.documentElement.getAttribute("devtoolstheme"),
+ "dark", "Tooltip panel has correct theme");
+
+ Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "light");
+
+ yield DeveloperToolbar.inputter.setInput("");
+ yield DeveloperToolbar.inputter.setInput("help help");
+ is(tooltipPanel.document.documentElement.getAttribute("devtoolstheme"),
+ "light", "Tooltip panel has correct theme");
+});
+
+add_task(function* hideToolbar() {
+ info("Ending browser_toolbar_tooltip.js");
+ yield DeveloperToolbar.inputter.setInput("");
+
+ ok(DeveloperToolbar.visible, "DeveloperToolbar is visible in hideToolbar");
+
+ info("Hide toolbar");
+ let hidePromise = observeOnce(DeveloperToolbar.NOTIFICATIONS.HIDE);
+ document.getElementById("menu_devToolbar").doCommand();
+ yield hidePromise;
+
+ ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible in hideToolbar");
+
+ info("Done test");
+});
+
+function getLeftMargin() {
+ let style = DeveloperToolbar.tooltipPanel._panel.style.marginLeft;
+ return parseInt(style.slice(0, -2), 10);
+}
+
+function observeOnce(topic, ownsWeak = false) {
+ return new Promise(function (resolve, reject) {
+ let resolver = function (subject) {
+ Services.obs.removeObserver(resolver, topic);
+ resolve(subject);
+ };
+ Services.obs.addObserver(resolver, topic, ownsWeak);
+ });
+}
diff --git a/devtools/client/shared/test/browser_toolbar_webconsole_errors_count.html b/devtools/client/shared/test/browser_toolbar_webconsole_errors_count.html
new file mode 100644
index 000000000..d09902af0
--- /dev/null
+++ b/devtools/client/shared/test/browser_toolbar_webconsole_errors_count.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Developer Toolbar Tests - errors count in the Web Console button</title>
+ <script type="text/javascript">
+ "use strict";
+ console.log("foobarBug762996consoleLog");
+ window.onload = function () {
+ window.foobarBug762996load();
+ };
+ window.foobarBug762996a();
+ </script>
+ <script type="text/javascript">
+ window.foobarBug762996b();
+ </script>
+</head>
+<body>
+ <p>Hello world! Test for errors count in the Web Console button (developer
+ toolbar).</p>
+ <p style="color: foobarBug762996css"><button>click me</button></p>
+ <script type="text/javascript;version=1.8">
+ "use strict";
+ let testObj = {};
+ document.querySelector("button").onclick = function() {
+ let test = testObj.fooBug788445 + "warning";
+ window.foobarBug762996click();
+ };
+ </script>
+</body>
+</html>
diff --git a/devtools/client/shared/test/browser_toolbar_webconsole_errors_count.js b/devtools/client/shared/test/browser_toolbar_webconsole_errors_count.js
new file mode 100644
index 000000000..c61666585
--- /dev/null
+++ b/devtools/client/shared/test/browser_toolbar_webconsole_errors_count.js
@@ -0,0 +1,256 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-disable mozilla/no-cpows-in-tests */
+
+"use strict";
+
+// Tests that the developer toolbar errors count works properly.
+
+// Use the old webconsole since this is directly accessing old DOM, and
+// the error count isn't reset when pressing the clear button in new one
+// See Bug 1304794.
+Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.webconsole.new-frontend-enabled");
+});
+
+function test() {
+ const TEST_URI = TEST_URI_ROOT + "browser_toolbar_webconsole_errors_count.html";
+
+ let tab1, tab2, webconsole;
+
+ Services.prefs.setBoolPref("javascript.options.strict", true);
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("javascript.options.strict");
+ });
+
+ ignoreAllUncaughtExceptions();
+ addTab(TEST_URI).then(openToolbar);
+
+ function openToolbar(tab) {
+ tab1 = tab;
+ ignoreAllUncaughtExceptions(false);
+
+ expectUncaughtException();
+
+ if (!DeveloperToolbar.visible) {
+ DeveloperToolbar.show(true).then(onOpenToolbar);
+ } else {
+ onOpenToolbar();
+ }
+ }
+
+ function onOpenToolbar() {
+ ok(DeveloperToolbar.visible, "DeveloperToolbar is visible");
+ webconsole = document.getElementById("developer-toolbar-toolbox-button");
+
+ waitForButtonUpdate({
+ name: "web console button shows page errors",
+ errors: 3,
+ warnings: 0,
+ callback: addErrors,
+ });
+ }
+
+ function addErrors() {
+ expectUncaughtException();
+
+ waitForFocus(function () {
+ let button = content.document.querySelector("button");
+ executeSoon(function () {
+ EventUtils.synthesizeMouse(button, 3, 2, {}, content);
+ });
+ }, content);
+
+ waitForButtonUpdate({
+ name: "button shows one more error after click in page",
+ errors: 4,
+ warnings: 1,
+ callback: () => {
+ ignoreAllUncaughtExceptions();
+ addTab(TEST_URI).then(onOpenSecondTab);
+ },
+ });
+ }
+
+ function onOpenSecondTab(tab) {
+ tab2 = tab;
+
+ ignoreAllUncaughtExceptions(false);
+ expectUncaughtException();
+
+ waitForButtonUpdate({
+ name: "button shows correct number of errors after new tab is open",
+ errors: 3,
+ warnings: 0,
+ callback: switchToTab1,
+ });
+ }
+
+ function switchToTab1() {
+ gBrowser.selectedTab = tab1;
+ waitForButtonUpdate({
+ name: "button shows the page errors from tab 1",
+ errors: 4,
+ warnings: 1,
+ callback: openWebConsole.bind(null, tab1, onWebConsoleOpen),
+ });
+ }
+
+ function onWebConsoleOpen(hud) {
+ dump("lolz!!\n");
+ waitForValue({
+ name: "web console shows the page errors",
+ validator: function () {
+ let selector = ".message[category=exception][severity=error]";
+ return hud.outputNode.querySelectorAll(selector).length;
+ },
+ value: 4,
+ success: checkConsoleOutput.bind(null, hud),
+ failure: () => {
+ finish();
+ },
+ });
+ }
+
+ function checkConsoleOutput(hud) {
+ let msgs = ["foobarBug762996a", "foobarBug762996b", "foobarBug762996load",
+ "foobarBug762996click", "foobarBug762996consoleLog",
+ "foobarBug762996css", "fooBug788445"];
+ msgs.forEach(function (msg) {
+ isnot(hud.outputNode.textContent.indexOf(msg), -1,
+ msg + " found in the Web Console output");
+ });
+
+ hud.jsterm.clearOutput();
+
+ is(hud.outputNode.textContent.indexOf("foobarBug762996color"), -1,
+ "clearOutput() worked");
+
+ expectUncaughtException();
+ let button = content.document.querySelector("button");
+ EventUtils.synthesizeMouse(button, 2, 2, {}, content);
+
+ waitForButtonUpdate({
+ name: "button shows one more error after another click in page",
+ errors: 5,
+ // warnings are not repeated by the js engine
+ warnings: 1,
+ callback: () => waitForValue(waitForNewError),
+ });
+
+ let waitForNewError = {
+ name: "the Web Console displays the new error",
+ validator: function () {
+ return hud.outputNode.textContent.indexOf("foobarBug762996click") > -1;
+ },
+ success: doClearConsoleButton.bind(null, hud),
+ failure: finish,
+ };
+ }
+
+ function doClearConsoleButton(hud) {
+ let clearButton = hud.ui.rootElement
+ .querySelector(".webconsole-clear-console-button");
+ EventUtils.synthesizeMouse(clearButton, 2, 2, {}, hud.iframeWindow);
+
+ is(hud.outputNode.textContent.indexOf("foobarBug762996click"), -1,
+ "clear console button worked");
+ is(getErrorsCount(), 0, "page errors counter has been reset");
+ let tooltip = getTooltipValues();
+ is(tooltip[1], 0, "page warnings counter has been reset");
+
+ doPageReload(hud);
+ }
+
+ function doPageReload(hud) {
+ tab1.linkedBrowser.addEventListener("load", onReload, true);
+
+ ignoreAllUncaughtExceptions();
+ content.location.reload();
+
+ function onReload() {
+ tab1.linkedBrowser.removeEventListener("load", onReload, true);
+ ignoreAllUncaughtExceptions(false);
+ expectUncaughtException();
+
+ waitForButtonUpdate({
+ name: "the Web Console button count has been reset after page reload",
+ errors: 3,
+ warnings: 0,
+ callback: waitForValue.bind(null, waitForConsoleOutputAfterReload),
+ });
+ }
+
+ let waitForConsoleOutputAfterReload = {
+ name: "the Web Console displays the correct number of errors after reload",
+ validator: function () {
+ let selector = ".message[category=exception][severity=error]";
+ return hud.outputNode.querySelectorAll(selector).length;
+ },
+ value: 3,
+ success: function () {
+ isnot(hud.outputNode.textContent.indexOf("foobarBug762996load"), -1,
+ "foobarBug762996load found in console output after page reload");
+ testEnd();
+ },
+ failure: testEnd,
+ };
+ }
+
+ function testEnd() {
+ document.getElementById("developer-toolbar-closebutton").doCommand();
+ let target1 = TargetFactory.forTab(tab1);
+ gDevTools.closeToolbox(target1).then(() => {
+ gBrowser.removeTab(tab1);
+ gBrowser.removeTab(tab2);
+ finish();
+ });
+ }
+
+ // Utility functions
+
+ function getErrorsCount() {
+ let count = webconsole.getAttribute("error-count");
+ return count ? count : "0";
+ }
+
+ function getTooltipValues() {
+ let matches = webconsole.getAttribute("tooltiptext")
+ .match(/(\d+) errors?, (\d+) warnings?/);
+ return matches ? [matches[1], matches[2]] : [0, 0];
+ }
+
+ function waitForButtonUpdate(options) {
+ function check() {
+ let errors = getErrorsCount();
+ let tooltip = getTooltipValues();
+ let result = errors == options.errors && tooltip[1] == options.warnings;
+ if (result) {
+ ok(true, options.name);
+ is(errors, tooltip[0], "button error-count is the same as in the tooltip");
+
+ // Get out of the toolbar event execution loop.
+ executeSoon(options.callback);
+ }
+ return result;
+ }
+
+ if (!check()) {
+ info("wait for: " + options.name);
+ DeveloperToolbar.on("errors-counter-updated", function onUpdate(event) {
+ if (check()) {
+ DeveloperToolbar.off(event, onUpdate);
+ }
+ });
+ }
+ }
+
+ function openWebConsole(tab, callback) {
+ let target = TargetFactory.forTab(tab);
+ gDevTools.showToolbox(target, "webconsole").then((toolbox) =>
+ callback(toolbox.getCurrentPanel().hud));
+ }
+}
diff --git a/devtools/client/shared/test/browser_treeWidget_basic.js b/devtools/client/shared/test/browser_treeWidget_basic.js
new file mode 100644
index 000000000..b1d7772f7
--- /dev/null
+++ b/devtools/client/shared/test/browser_treeWidget_basic.js
@@ -0,0 +1,267 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the tree widget api works fine
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,<head>" +
+ "<link rel='stylesheet' type='text/css' href='chrome://devtools/skin/widg" +
+ "ets.css'></head><body><div></div><span></span></body>";
+const {TreeWidget} = require("devtools/client/shared/widgets/TreeWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ let [host,, doc] = yield createHost("bottom", TEST_URI);
+
+ let tree = new TreeWidget(doc.querySelector("div"), {
+ defaultType: "store"
+ });
+
+ populateTree(tree, doc);
+ testTreeItemInsertedCorrectly(tree, doc);
+ testAPI(tree, doc);
+ populateUnsortedTree(tree, doc);
+ testUnsortedTreeItemInsertedCorrectly(tree, doc);
+
+ tree.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function populateTree(tree, doc) {
+ tree.add([{
+ id: "level1",
+ label: "Level 1"
+ }, {
+ id: "level2-1",
+ label: "Level 2"
+ }, {
+ id: "level3-1",
+ label: "Level 3 - Child 1",
+ type: "dir"
+ }]);
+ tree.add(["level1", "level2-1", {
+ id: "level3-2",
+ label: "Level 3 - Child 2"
+ }]);
+ tree.add(["level1", "level2-1", {
+ id: "level3-3",
+ label: "Level 3 - Child 3"
+ }]);
+ tree.add(["level1", {
+ id: "level2-2",
+ label: "Level 2.1"
+ }, {
+ id: "level3-1",
+ label: "Level 3.1"
+ }]);
+ tree.add([{
+ id: "level1",
+ label: "Level 1"
+ }, {
+ id: "level2",
+ label: "Level 2"
+ }, {
+ id: "level3",
+ label: "Level 3",
+ type: "js"
+ }]);
+ tree.add(["level1.1", "level2", {id: "level3", type: "url"}]);
+}
+
+/**
+ * Test if the nodes are inserted correctly in the tree.
+ */
+function testTreeItemInsertedCorrectly(tree, doc) {
+ is(tree.root.children.children.length, 2,
+ "Number of top level elements match");
+ is(tree.root.children.firstChild.lastChild.children.length, 3,
+ "Number of first second level elements match");
+ is(tree.root.children.lastChild.lastChild.children.length, 1,
+ "Number of second second level elements match");
+
+ ok(tree.root.items.has("level1"), "Level1 top level element exists");
+ is(tree.root.children.firstChild.dataset.id, JSON.stringify(["level1"]),
+ "Data id of first top level element matches");
+ is(tree.root.children.firstChild.firstChild.textContent, "Level 1",
+ "Text content of first top level element matches");
+
+ ok(tree.root.items.has("level1.1"), "Level1.1 top level element exists");
+ is(tree.root.children.firstChild.nextSibling.dataset.id,
+ JSON.stringify(["level1.1"]),
+ "Data id of second top level element matches");
+ is(tree.root.children.firstChild.nextSibling.firstChild.textContent,
+ "level1.1",
+ "Text content of second top level element matches");
+
+ // Adding a new non text item in the tree.
+ let node = doc.createElement("div");
+ node.textContent = "Foo Bar";
+ node.className = "foo bar";
+ tree.add([{
+ id: "level1.2",
+ node: node,
+ attachment: {
+ foo: "bar"
+ }
+ }]);
+
+ is(tree.root.children.children.length, 3,
+ "Number of top level elements match after update");
+ ok(tree.root.items.has("level1.2"), "New level node got added");
+ ok(tree.attachments.has(JSON.stringify(["level1.2"])),
+ "Attachment is present for newly added node");
+ // The item should be added before level1 and level 1.1 as lexical sorting
+ is(tree.root.children.firstChild.dataset.id, JSON.stringify(["level1.2"]),
+ "Data id of last top level element matches");
+ is(tree.root.children.firstChild.firstChild.firstChild, node,
+ "Newly added node is inserted at the right location");
+}
+
+/**
+ * Populate the unsorted tree.
+ */
+function populateUnsortedTree(tree, doc) {
+ tree.sorted = false;
+
+ tree.add([{ id: "g-1", label: "g-1"}]);
+ tree.add(["g-1", { id: "d-2", label: "d-2.1"}]);
+ tree.add(["g-1", { id: "b-2", label: "b-2.2"}]);
+ tree.add(["g-1", { id: "a-2", label: "a-2.3"}]);
+}
+
+/**
+ * Test if the nodes are inserted correctly in the unsorted tree.
+ */
+function testUnsortedTreeItemInsertedCorrectly(tree, doc) {
+ ok(tree.root.items.has("g-1"), "g-1 top level element exists");
+
+ is(tree.root.children.firstChild.lastChild.children.length, 3,
+ "Number of children for g-1 matches");
+ is(tree.root.children.firstChild.dataset.id, JSON.stringify(["g-1"]),
+ "Data id of g-1 matches");
+ is(tree.root.children.firstChild.firstChild.textContent, "g-1",
+ "Text content of g-1 matches");
+ is(tree.root.children.firstChild.lastChild.firstChild.dataset.id,
+ JSON.stringify(["g-1", "d-2"]),
+ "Data id of d-2 matches");
+ is(tree.root.children.firstChild.lastChild.firstChild.textContent, "d-2.1",
+ "Text content of d-2 matches");
+ is(tree.root.children.firstChild.lastChild.firstChild.nextSibling.textContent,
+ "b-2.2", "Text content of b-2 matches");
+ is(tree.root.children.firstChild.lastChild.lastChild.textContent, "a-2.3",
+ "Text content of a-2 matches");
+}
+
+/**
+ * Tests if the API exposed by TreeWidget works properly
+ */
+function testAPI(tree, doc) {
+ info("Testing TreeWidget API");
+ // Check if selectItem and selectedItem setter works as expected
+ // Nothing should be selected beforehand
+ ok(!doc.querySelector(".theme-selected"), "Nothing is selected");
+ tree.selectItem(["level1"]);
+ let node = doc.querySelector(".theme-selected");
+ ok(!!node, "Something got selected");
+ is(node.parentNode.dataset.id, JSON.stringify(["level1"]),
+ "Correct node selected");
+
+ tree.selectItem(["level1", "level2"]);
+ let node2 = doc.querySelector(".theme-selected");
+ ok(!!node2, "Something is still selected");
+ isnot(node, node2, "Newly selected node is different from previous");
+ is(node2.parentNode.dataset.id, JSON.stringify(["level1", "level2"]),
+ "Correct node selected");
+
+ // test if selectedItem getter works
+ is(tree.selectedItem.length, 2, "Correct length of selected item");
+ is(tree.selectedItem[0], "level1", "Correct selected item");
+ is(tree.selectedItem[1], "level2", "Correct selected item");
+
+ // test if isSelected works
+ ok(tree.isSelected(["level1", "level2"]), "isSelected works");
+
+ tree.selectedItem = ["level1"];
+ let node3 = doc.querySelector(".theme-selected");
+ ok(!!node3, "Something is still selected");
+ isnot(node2, node3, "Newly selected node is different from previous");
+ is(node3, node, "First and third selected nodes should be same");
+ is(node3.parentNode.dataset.id, JSON.stringify(["level1"]),
+ "Correct node selected");
+
+ // test if selectedItem getter works
+ is(tree.selectedItem.length, 1, "Correct length of selected item");
+ is(tree.selectedItem[0], "level1", "Correct selected item");
+
+ // test if clear selection works
+ tree.clearSelection();
+ ok(!doc.querySelector(".theme-selected"),
+ "Nothing selected after clear selection call");
+
+ // test if collapseAll/expandAll work
+ ok(doc.querySelectorAll("[expanded]").length > 0,
+ "Some nodes are expanded");
+ tree.collapseAll();
+ is(doc.querySelectorAll("[expanded]").length, 0,
+ "Nothing is expanded after collapseAll call");
+ tree.expandAll();
+ is(doc.querySelectorAll("[expanded]").length, 13,
+ "All tree items expanded after expandAll call");
+
+ // test if selectNextItem and selectPreviousItem work
+ tree.selectedItem = ["level1", "level2"];
+ ok(tree.isSelected(["level1", "level2"]), "Correct item selected");
+ tree.selectNextItem();
+ ok(tree.isSelected(["level1", "level2", "level3"]),
+ "Correct item selected after selectNextItem call");
+
+ tree.selectNextItem();
+ ok(tree.isSelected(["level1", "level2-1"]),
+ "Correct item selected after second selectNextItem call");
+
+ tree.selectNextItem();
+ ok(tree.isSelected(["level1", "level2-1", "level3-1"]),
+ "Correct item selected after third selectNextItem call");
+
+ tree.selectPreviousItem();
+ ok(tree.isSelected(["level1", "level2-1"]),
+ "Correct item selected after selectPreviousItem call");
+
+ tree.selectPreviousItem();
+ ok(tree.isSelected(["level1", "level2", "level3"]),
+ "Correct item selected after second selectPreviousItem call");
+
+ // test if remove works
+ ok(doc.querySelector("[data-id='" +
+ JSON.stringify(["level1", "level2", "level3"]) + "']"),
+ "level1-level2-level3 item exists before removing");
+ tree.remove(["level1", "level2", "level3"]);
+ ok(!doc.querySelector("[data-id='" +
+ JSON.stringify(["level1", "level2", "level3"]) + "']"),
+ "level1-level2-level3 item does not exist after removing");
+ let level2item = doc.querySelector("[data-id='" +
+ JSON.stringify(["level1", "level2"]) + "'] > .tree-widget-item");
+ ok(level2item.hasAttribute("empty"),
+ "level1-level2 item is marked as empty after removing");
+
+ tree.add([{
+ id: "level1",
+ label: "Level 1"
+ }, {
+ id: "level2",
+ label: "Level 2"
+ }, {
+ id: "level3",
+ label: "Level 3",
+ type: "js"
+ }]);
+
+ // test if clearing the tree works
+ is(doc.querySelectorAll("[level='1']").length, 3,
+ "Correct number of top level items before clearing");
+ tree.clear();
+ is(doc.querySelectorAll("[level='1']").length, 0,
+ "No top level item after clearing the tree");
+}
diff --git a/devtools/client/shared/test/browser_treeWidget_keyboard_interaction.js b/devtools/client/shared/test/browser_treeWidget_keyboard_interaction.js
new file mode 100644
index 000000000..9b214fe3f
--- /dev/null
+++ b/devtools/client/shared/test/browser_treeWidget_keyboard_interaction.js
@@ -0,0 +1,228 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that keyboard interaction works fine with the tree widget
+
+const TEST_URI = "data:text/html;charset=utf-8,<head>" +
+ "<link rel='stylesheet' type='text/css' href='chrome://devtools/skin/widg" +
+ "ets.css'></head><body><div></div><span></span></body>";
+const {TreeWidget} = require("devtools/client/shared/widgets/TreeWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ let [host, win, doc] = yield createHost("bottom", TEST_URI);
+
+ let tree = new TreeWidget(doc.querySelector("div"), {
+ defaultType: "store"
+ });
+
+ populateTree(tree, doc);
+ yield testKeyboardInteraction(tree, win);
+
+ tree.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function populateTree(tree, doc) {
+ tree.add([{
+ id: "level1",
+ label: "Level 1"
+ }, {
+ id: "level2-1",
+ label: "Level 2"
+ }, {
+ id: "level3-1",
+ label: "Level 3 - Child 1",
+ type: "dir"
+ }]);
+ tree.add(["level1", "level2-1", { id: "level3-2", label: "Level 3 - Child 2"}]);
+ tree.add(["level1", "level2-1", { id: "level3-3", label: "Level 3 - Child 3"}]);
+ tree.add(["level1", {
+ id: "level2-2",
+ label: "Level 2.1"
+ }, {
+ id: "level3-1",
+ label: "Level 3.1"
+ }]);
+ tree.add([{
+ id: "level1",
+ label: "Level 1"
+ }, {
+ id: "level2",
+ label: "Level 2"
+ }, {
+ id: "level3",
+ label: "Level 3",
+ type: "js"
+ }]);
+ tree.add(["level1.1", "level2", {id: "level3", type: "url"}]);
+
+ // Adding a new non text item in the tree.
+ let node = doc.createElement("div");
+ node.textContent = "Foo Bar";
+ node.className = "foo bar";
+ tree.add([{
+ id: "level1.2",
+ node: node,
+ attachment: {
+ foo: "bar"
+ }
+ }]);
+}
+
+// Sends a click event on the passed DOM node in an async manner
+function click(node) {
+ let win = node.ownerDocument.defaultView;
+ executeSoon(() => EventUtils.synthesizeMouseAtCenter(node, {}, win));
+}
+
+/**
+ * Tests if pressing navigation keys on the tree items does the expected behavior
+ */
+function* testKeyboardInteraction(tree, win) {
+ info("Testing keyboard interaction with the tree");
+ let event;
+ let pass = (e, d, a) => event.resolve([e, d, a]);
+
+ info("clicking on first top level item");
+ let node = tree.root.children.firstChild.firstChild;
+ event = defer();
+ tree.once("select", pass);
+ click(node);
+ yield event.promise;
+ node = tree.root.children.firstChild.nextSibling.firstChild;
+ // node should not have selected class
+ ok(!node.classList.contains("theme-selected"), "Node should not have selected class");
+ ok(!node.hasAttribute("expanded"), "Node is not expanded");
+
+ info("Pressing down key to select next item");
+ event = defer();
+ tree.once("select", pass);
+ EventUtils.sendKey("DOWN", win);
+ let [name, data, attachment] = yield event.promise;
+ is(name, "select", "Select event was fired after pressing down");
+ is(data[0], "level1", "Correct item was selected after pressing down");
+ ok(!attachment, "null attachment was emitted");
+ ok(node.classList.contains("theme-selected"), "Node has selected class");
+ ok(node.hasAttribute("expanded"), "Node is expanded now");
+
+ info("Pressing down key again to select next item");
+ event = defer();
+ tree.once("select", pass);
+ EventUtils.sendKey("DOWN", win);
+ [name, data, attachment] = yield event.promise;
+ is(data.length, 2, "Correct level item was selected after second down keypress");
+ is(data[0], "level1", "Correct parent level");
+ is(data[1], "level2", "Correct second level");
+
+ info("Pressing down key again to select next item");
+ event = defer();
+ tree.once("select", pass);
+ EventUtils.sendKey("DOWN", win);
+ [name, data, attachment] = yield event.promise;
+ is(data.length, 3, "Correct level item was selected after third down keypress");
+ is(data[0], "level1", "Correct parent level");
+ is(data[1], "level2", "Correct second level");
+ is(data[2], "level3", "Correct third level");
+
+ info("Pressing down key again to select next item");
+ event = defer();
+ tree.once("select", pass);
+ EventUtils.sendKey("DOWN", win);
+ [name, data, attachment] = yield event.promise;
+ is(data.length, 2, "Correct level item was selected after fourth down keypress");
+ is(data[0], "level1", "Correct parent level");
+ is(data[1], "level2-1", "Correct second level");
+
+ // pressing left to check expand collapse feature.
+ // This does not emit any event, so listening for keypress
+ tree.root.children.addEventListener("keypress", function onClick() {
+ tree.root.children.removeEventListener("keypress", onClick);
+ // executeSoon so that other listeners on the same method are executed first
+ executeSoon(() => event.resolve(null));
+ });
+ info("Pressing left key to collapse the item");
+ event = defer();
+ node = tree._selectedLabel;
+ ok(node.hasAttribute("expanded"), "Item is expanded before left keypress");
+ EventUtils.sendKey("LEFT", win);
+ yield event.promise;
+
+ ok(!node.hasAttribute("expanded"), "Item is not expanded after left keypress");
+
+ // pressing left on collapsed item should select the previous item
+
+ info("Pressing left key on collapsed item to select previous");
+ tree.once("select", pass);
+ event = defer();
+ // parent node should have no effect of this keypress
+ node = tree.root.children.firstChild.nextSibling.firstChild;
+ ok(node.hasAttribute("expanded"), "Parent is expanded");
+ EventUtils.sendKey("LEFT", win);
+ [name, data] = yield event.promise;
+ is(data.length, 3, "Correct level item was selected after second left keypress");
+ is(data[0], "level1", "Correct parent level");
+ is(data[1], "level2", "Correct second level");
+ is(data[2], "level3", "Correct third level");
+ ok(node.hasAttribute("expanded"), "Parent is still expanded after left keypress");
+
+ // pressing down again
+
+ info("Pressing down key to select next item");
+ event = defer();
+ tree.once("select", pass);
+ EventUtils.sendKey("DOWN", win);
+ [name, data, attachment] = yield event.promise;
+ is(data.length, 2, "Correct level item was selected after fifth down keypress");
+ is(data[0], "level1", "Correct parent level");
+ is(data[1], "level2-1", "Correct second level");
+
+ // collapsing the item to check expand feature.
+
+ tree.root.children.addEventListener("keypress", function onClick() {
+ tree.root.children.removeEventListener("keypress", onClick);
+ executeSoon(() => event.resolve(null));
+ });
+ info("Pressing left key to collapse the item");
+ event = defer();
+ node = tree._selectedLabel;
+ ok(node.hasAttribute("expanded"), "Item is expanded before left keypress");
+ EventUtils.sendKey("LEFT", win);
+ yield event.promise;
+ ok(!node.hasAttribute("expanded"), "Item is collapsed after left keypress");
+
+ // pressing right should expand this now.
+
+ tree.root.children.addEventListener("keypress", function onClick() {
+ tree.root.children.removeEventListener("keypress", onClick);
+ executeSoon(() => event.resolve(null));
+ });
+ info("Pressing right key to expend the collapsed item");
+ event = defer();
+ node = tree._selectedLabel;
+ ok(!node.hasAttribute("expanded"), "Item is collapsed before right keypress");
+ EventUtils.sendKey("RIGHT", win);
+ yield event.promise;
+ ok(node.hasAttribute("expanded"), "Item is expanded after right keypress");
+
+ // selecting last item node to test edge navigation case
+
+ tree.selectedItem = ["level1.1", "level2", "level3"];
+ node = tree._selectedLabel;
+ // pressing down again should not change selection
+ event = defer();
+ tree.root.children.addEventListener("keypress", function onClick() {
+ tree.root.children.removeEventListener("keypress", onClick);
+ executeSoon(() => event.resolve(null));
+ });
+ info("Pressing down key on last item of the tree");
+ EventUtils.sendKey("DOWN", win);
+ yield event.promise;
+
+ ok(tree.isSelected(["level1.1", "level2", "level3"]),
+ "Last item is still selected after pressing down on last item of the tree");
+}
diff --git a/devtools/client/shared/test/browser_treeWidget_mouse_interaction.js b/devtools/client/shared/test/browser_treeWidget_mouse_interaction.js
new file mode 100644
index 000000000..f42fd16ad
--- /dev/null
+++ b/devtools/client/shared/test/browser_treeWidget_mouse_interaction.js
@@ -0,0 +1,135 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that mouse interaction works fine with tree widget
+
+const TEST_URI = "data:text/html;charset=utf-8,<head>" +
+ "<link rel='stylesheet' type='text/css' href='chrome://devtools/skin/widg" +
+ "ets.css'></head><body><div></div><span></span></body>";
+const {TreeWidget} = require("devtools/client/shared/widgets/TreeWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ let [host,, doc] = yield createHost("bottom", TEST_URI);
+
+ let tree = new TreeWidget(doc.querySelector("div"), {
+ defaultType: "store"
+ });
+
+ populateTree(tree, doc);
+ yield testMouseInteraction(tree);
+
+ tree.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function populateTree(tree, doc) {
+ tree.add([{
+ id: "level1",
+ label: "Level 1"
+ }, {
+ id: "level2-1",
+ label: "Level 2"
+ }, {
+ id: "level3-1",
+ label: "Level 3 - Child 1",
+ type: "dir"
+ }]);
+ tree.add(["level1", "level2-1", { id: "level3-2", label: "Level 3 - Child 2"}]);
+ tree.add(["level1", "level2-1", { id: "level3-3", label: "Level 3 - Child 3"}]);
+ tree.add(["level1", {
+ id: "level2-2",
+ label: "Level 2.1"
+ }, {
+ id: "level3-1",
+ label: "Level 3.1"
+ }]);
+ tree.add([{
+ id: "level1",
+ label: "Level 1"
+ }, {
+ id: "level2",
+ label: "Level 2"
+ }, {
+ id: "level3",
+ label: "Level 3",
+ type: "js"
+ }]);
+ tree.add(["level1.1", "level2", {id: "level3", type: "url"}]);
+
+ // Adding a new non text item in the tree.
+ let node = doc.createElement("div");
+ node.textContent = "Foo Bar";
+ node.className = "foo bar";
+ tree.add([{
+ id: "level1.2",
+ node: node,
+ attachment: {
+ foo: "bar"
+ }
+ }]);
+}
+
+// Sends a click event on the passed DOM node in an async manner
+function click(node) {
+ let win = node.ownerDocument.defaultView;
+ executeSoon(() => EventUtils.synthesizeMouseAtCenter(node, {}, win));
+}
+
+/**
+ * Tests if clicking the tree items does the expected behavior
+ */
+function* testMouseInteraction(tree) {
+ info("Testing mouse interaction with the tree");
+ let event;
+ let pass = (e, d, a) => event.resolve([e, d, a]);
+
+ ok(!tree.selectedItem, "Nothing should be selected beforehand");
+
+ tree.once("select", pass);
+ let node = tree.root.children.firstChild.firstChild;
+ info("clicking on first top level item");
+ event = defer();
+ ok(!node.classList.contains("theme-selected"),
+ "Node should not have selected class before clicking");
+ click(node);
+ let [, data, attachment] = yield event.promise;
+ ok(node.classList.contains("theme-selected"),
+ "Node has selected class after click");
+ is(data[0], "level1.2", "Correct tree path is emitted");
+ ok(attachment && attachment.foo, "Correct attachment is emitted");
+ is(attachment.foo, "bar", "Correct attachment value is emitted");
+
+ info("clicking second top level item with children to check if it expands");
+ let node2 = tree.root.children.firstChild.nextSibling.firstChild;
+ event = defer();
+ // node should not have selected class
+ ok(!node2.classList.contains("theme-selected"),
+ "New node should not have selected class before clicking");
+ ok(!node2.hasAttribute("expanded"), "New node is not expanded before clicking");
+ tree.once("select", pass);
+ click(node2);
+ [, data, attachment] = yield event.promise;
+ ok(node2.classList.contains("theme-selected"),
+ "New node has selected class after clicking");
+ is(data[0], "level1", "Correct tree path is emitted for new node");
+ ok(!attachment, "null attachment should be emitted for new node");
+ ok(node2.hasAttribute("expanded"), "New node expanded after click");
+
+ ok(!node.classList.contains("theme-selected"),
+ "Old node should not have selected class after the click on new node");
+
+ // clicking again should just collapse
+ // this will not emit "select" event
+ event = defer();
+ node2.addEventListener("click", () => {
+ executeSoon(() => event.resolve(null));
+ }, { once: true });
+ click(node2);
+ yield event.promise;
+ ok(!node2.hasAttribute("expanded"), "New node collapsed after click again");
+}
diff --git a/devtools/client/shared/test/doc_options-view.xul b/devtools/client/shared/test/doc_options-view.xul
new file mode 100644
index 000000000..06ae1cbf1
--- /dev/null
+++ b/devtools/client/shared/test/doc_options-view.xul
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?>
+<!DOCTYPE window []>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <popupset id="options-popupset">
+ <menupopup id="options-menupopup" position="before_end">
+ <menuitem id="option-autoprettyprint"
+ type="checkbox"
+ data-pref="auto-pretty-print"
+ label="pretty print"/>
+ <menuitem id="option-autoblackbox"
+ type="checkbox"
+ data-pref="auto-black-box"
+ label="black box"/>
+ </menupopup>
+ </popupset>
+ <button id="options-button"
+ popup="options-menupopup"/>
+</window>
diff --git a/devtools/client/shared/test/head.js b/devtools/client/shared/test/head.js
new file mode 100644
index 000000000..fbf0e84af
--- /dev/null
+++ b/devtools/client/shared/test/head.js
@@ -0,0 +1,346 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
+/* import-globals-from ../../framework/test/shared-head.js */
+
+"use strict";
+
+// shared-head.js handles imports, constants, and utility functions
+Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js", this);
+
+const {DOMHelpers} = Cu.import("resource://devtools/client/shared/DOMHelpers.jsm", {});
+const {Hosts} = require("devtools/client/framework/toolbox-hosts");
+
+const TEST_URI_ROOT = "http://example.com/browser/devtools/client/shared/test/";
+const OPTIONS_VIEW_URL = TEST_URI_ROOT + "doc_options-view.xul";
+
+function catchFail(func) {
+ return function () {
+ try {
+ return func.apply(null, arguments);
+ } catch (ex) {
+ ok(false, ex);
+ console.error(ex);
+ finish();
+ throw ex;
+ }
+ };
+}
+
+/**
+ * Polls a given function waiting for the given value.
+ *
+ * @param object options
+ * Options object with the following properties:
+ * - validator
+ * A validator function that should return the expected value. This is
+ * called every few milliseconds to check if the result is the expected
+ * one. When the returned result is the expected one, then the |success|
+ * function is called and polling stops. If |validator| never returns
+ * the expected value, then polling timeouts after several tries and
+ * a failure is recorded - the given |failure| function is invoked.
+ * - success
+ * A function called when the validator function returns the expected
+ * value.
+ * - failure
+ * A function called if the validator function timeouts - fails to return
+ * the expected value in the given time.
+ * - name
+ * Name of test. This is used to generate the success and failure
+ * messages.
+ * - timeout
+ * Timeout for validator function, in milliseconds. Default is 5000 ms.
+ * - value
+ * The expected value. If this option is omitted then the |validator|
+ * function must return a trueish value.
+ * Each of the provided callback functions will receive two arguments:
+ * the |options| object and the last value returned by |validator|.
+ */
+function waitForValue(options) {
+ let start = Date.now();
+ let timeout = options.timeout || 5000;
+ let lastValue;
+
+ function wait(validatorFn, successFn, failureFn) {
+ if ((Date.now() - start) > timeout) {
+ // Log the failure.
+ ok(false, "Timed out while waiting for: " + options.name);
+ let expected = "value" in options ?
+ "'" + options.value + "'" :
+ "a trueish value";
+ info("timeout info :: got '" + lastValue + "', expected " + expected);
+ failureFn(options, lastValue);
+ return;
+ }
+
+ lastValue = validatorFn(options, lastValue);
+ let successful = "value" in options ?
+ lastValue == options.value :
+ lastValue;
+ if (successful) {
+ ok(true, options.name);
+ successFn(options, lastValue);
+ } else {
+ setTimeout(() => {
+ wait(validatorFn, successFn, failureFn);
+ }, 100);
+ }
+ }
+
+ wait(options.validator, options.success, options.failure);
+}
+
+function oneTimeObserve(name, callback) {
+ return new Promise((resolve) => {
+ let func = function () {
+ Services.obs.removeObserver(func, name);
+ if (callback) {
+ callback();
+ }
+ resolve();
+ };
+ Services.obs.addObserver(func, name, false);
+ });
+}
+
+let createHost =
+Task.async(function* (type = "bottom", src = "data:text/html;charset=utf-8,") {
+ let host = new Hosts[type](gBrowser.selectedTab);
+ let iframe = yield host.create();
+
+ yield new Promise(resolve => {
+ let domHelper = new DOMHelpers(iframe.contentWindow);
+ iframe.setAttribute("src", src);
+ domHelper.onceDOMReady(resolve);
+ });
+
+ return [host, iframe.contentWindow, iframe.contentDocument];
+});
+
+/**
+ * Check the correctness of the data recorded in Telemetry after
+ * loadTelemetryAndRecordLogs was called.
+ */
+function checkTelemetryResults(Telemetry) {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let histId in result) {
+ let value = result[histId];
+
+ if (histId.endsWith("OPENED_COUNT")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function (element) {
+ return element === true;
+ });
+
+ ok(okay, "All " + histId + " entries are === true");
+ } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function (element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ }
+ }
+}
+
+/**
+ * Open and close the toolbox in the current browser tab, several times, waiting
+ * some amount of time in between.
+ * @param {Number} nbOfTimes
+ * @param {Number} usageTime in milliseconds
+ * @param {String} toolId
+ */
+function* openAndCloseToolbox(nbOfTimes, usageTime, toolId) {
+ for (let i = 0; i < nbOfTimes; i++) {
+ info("Opening toolbox " + (i + 1));
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ yield gDevTools.showToolbox(target, toolId);
+
+ // We use a timeout to check the toolbox's active time
+ yield new Promise(resolve => setTimeout(resolve, usageTime));
+
+ info("Closing toolbox " + (i + 1));
+ yield gDevTools.closeToolbox(target);
+ }
+}
+
+/**
+ * Synthesize a profile for testing.
+ */
+function synthesizeProfileForTest(samples) {
+ const RecordingUtils = require("devtools/shared/performance/recording-utils");
+
+ samples.unshift({
+ time: 0,
+ frames: []
+ });
+
+ let uniqueStacks = new RecordingUtils.UniqueStacks();
+ return RecordingUtils.deflateThread({
+ samples: samples,
+ markers: []
+ }, uniqueStacks);
+}
+
+/**
+ * Waits until a predicate returns true.
+ *
+ * @param function predicate
+ * Invoked once in a while until it returns true.
+ * @param number interval [optional]
+ * How often the predicate is invoked, in milliseconds.
+ */
+function waitUntil(predicate, interval = 10) {
+ if (predicate()) {
+ return Promise.resolve(true);
+ }
+ return new Promise(resolve => {
+ setTimeout(function () {
+ waitUntil(predicate).then(() => resolve(true));
+ }, interval);
+ });
+}
+
+/**
+ * Show the presets list sidebar in the cssfilter widget popup
+ * @param {CSSFilterWidget} widget
+ * @return {Promise}
+ */
+function showFilterPopupPresets(widget) {
+ let onRender = widget.once("render");
+ widget._togglePresets();
+ return onRender;
+}
+
+/**
+ * Show presets list and create a sample preset with the name and value provided
+ * @param {CSSFilterWidget} widget
+ * @param {string} name
+ * @param {string} value
+ * @return {Promise}
+ */
+let showFilterPopupPresetsAndCreatePreset =
+Task.async(function* (widget, name, value) {
+ yield showFilterPopupPresets(widget);
+
+ let onRender = widget.once("render");
+ widget.setCssValue(value);
+ yield onRender;
+
+ let footer = widget.el.querySelector(".presets-list .footer");
+ footer.querySelector("input").value = name;
+
+ onRender = widget.once("render");
+ widget._savePreset({
+ preventDefault: () => {}
+ });
+
+ yield onRender;
+});
+
+/**
+ * Utility function for testing CSS code samples that have been
+ * syntax-highlighted.
+ *
+ * The CSS syntax highlighter emits a collection of DOM nodes that have
+ * CSS classes applied to them. This function checks that those nodes
+ * are what we expect.
+ *
+ * @param {array} expectedNodes
+ * A representation of the nodes we expect to see.
+ * Each node is an object containing two properties:
+ * - type: a string which can be one of:
+ * - text, comment, property-name, property-value
+ * - text: the textContent of the node
+ *
+ * For example, given a string like this:
+ * "<comment> The part we want </comment>\n this: is-the-part-we-want;"
+ *
+ * we would represent the expected output like this:
+ * [{type: "comment", text: "<comment> The part we want </comment>"},
+ * {type: "text", text: "\n"},
+ * {type: "property-name", text: "this"},
+ * {type: "text", text: ":"},
+ * {type: "text", text: " "},
+ * {type: "property-value", text: "is-the-part-we-want"},
+ * {type: "text", text: ";"}];
+ *
+ * @param {Node} parent
+ * The DOM node whose children are the output of the syntax highlighter.
+ */
+function checkCssSyntaxHighlighterOutput(expectedNodes, parent) {
+ /**
+ * The classes applied to the output nodes by the syntax highlighter.
+ * These must be same as the definitions in MdnDocsWidget.js.
+ */
+ const PROPERTY_NAME_COLOR = "theme-fg-color5";
+ const PROPERTY_VALUE_COLOR = "theme-fg-color1";
+ const COMMENT_COLOR = "theme-comment";
+
+ /**
+ * Check the type and content of a single node.
+ */
+ function checkNode(expected, actual) {
+ ok(actual.textContent == expected.text,
+ "Check that node has the expected textContent");
+ info("Expected text content: [" + expected.text + "]");
+ info("Actual text content: [" + actual.textContent + "]");
+
+ info("Check that node has the expected type");
+ if (expected.type == "text") {
+ ok(actual.nodeType == 3, "Check that node is a text node");
+ } else {
+ ok(actual.tagName.toUpperCase() == "SPAN", "Check that node is a SPAN");
+ }
+
+ info("Check that node has the expected className");
+
+ let expectedClassName = null;
+ let actualClassName = null;
+
+ switch (expected.type) {
+ case "property-name":
+ expectedClassName = PROPERTY_NAME_COLOR;
+ break;
+ case "property-value":
+ expectedClassName = PROPERTY_VALUE_COLOR;
+ break;
+ case "comment":
+ expectedClassName = COMMENT_COLOR;
+ break;
+ default:
+ ok(!actual.classList, "No className expected");
+ return;
+ }
+
+ ok(actual.classList.length == 1, "One className expected");
+ actualClassName = actual.classList[0];
+
+ ok(expectedClassName == actualClassName, "Check className value");
+ info("Expected className: " + expectedClassName);
+ info("Actual className: " + actualClassName);
+ }
+
+ info("Logging the actual nodes we have:");
+ for (let j = 0; j < parent.childNodes.length; j++) {
+ let n = parent.childNodes[j];
+ info(j + " / " +
+ "nodeType: " + n.nodeType + " / " +
+ "textContent: " + n.textContent);
+ }
+
+ ok(parent.childNodes.length == parent.childNodes.length,
+ "Check we have the expected number of nodes");
+ info("Expected node count " + expectedNodes.length);
+ info("Actual node count " + expectedNodes.length);
+
+ for (let i = 0; i < expectedNodes.length; i++) {
+ info("Check node " + i);
+ checkNode(expectedNodes[i], parent.childNodes[i]);
+ }
+}
diff --git a/devtools/client/shared/test/helper_color_data.js b/devtools/client/shared/test/helper_color_data.js
new file mode 100644
index 000000000..3407102ae
--- /dev/null
+++ b/devtools/client/shared/test/helper_color_data.js
@@ -0,0 +1,175 @@
+"use strict";
+
+/* eslint-disable max-len */
+function getFixtureColorData() {
+ return [
+ {authored: "aliceblue", name: "aliceblue", hex: "#f0f8ff", hsl: "hsl(208, 100%, 97.1%)", rgb: "rgb(240, 248, 255)", cycle: 4},
+ {authored: "antiquewhite", name: "antiquewhite", hex: "#faebd7", hsl: "hsl(34.3, 77.8%, 91.2%)", rgb: "rgb(250, 235, 215)", cycle: 4},
+ {authored: "aqua", name: "aqua", hex: "#0ff", hsl: "hsl(180, 100%, 50%)", rgb: "rgb(0, 255, 255)", cycle: 4},
+ {authored: "aquamarine", name: "aquamarine", hex: "#7fffd4", hsl: "hsl(159.8, 100%, 74.9%)", rgb: "rgb(127, 255, 212)", cycle: 4},
+ {authored: "azure", name: "azure", hex: "#f0ffff", hsl: "hsl(180, 100%, 97.1%)", rgb: "rgb(240, 255, 255)", cycle: 4},
+ {authored: "beige", name: "beige", hex: "#f5f5dc", hsl: "hsl(60, 55.6%, 91.2%)", rgb: "rgb(245, 245, 220)", cycle: 4},
+ {authored: "bisque", name: "bisque", hex: "#ffe4c4", hsl: "hsl(32.5, 100%, 88.4%)", rgb: "rgb(255, 228, 196)", cycle: 4},
+ {authored: "black", name: "black", hex: "#000", hsl: "hsl(0, 0%, 0%)", rgb: "rgb(0, 0, 0)", cycle: 4},
+ {authored: "blanchedalmond", name: "blanchedalmond", hex: "#ffebcd", hsl: "hsl(36, 100%, 90.2%)", rgb: "rgb(255, 235, 205)", cycle: 4},
+ {authored: "blue", name: "blue", hex: "#00f", hsl: "hsl(240, 100%, 50%)", rgb: "rgb(0, 0, 255)", cycle: 4},
+ {authored: "blueviolet", name: "blueviolet", hex: "#8a2be2", hsl: "hsl(271.1, 75.9%, 52.7%)", rgb: "rgb(138, 43, 226)", cycle: 4},
+ {authored: "brown", name: "brown", hex: "#a52a2a", hsl: "hsl(0, 59.4%, 40.6%)", rgb: "rgb(165, 42, 42)", cycle: 4},
+ {authored: "burlywood", name: "burlywood", hex: "#deb887", hsl: "hsl(33.8, 56.9%, 70%)", rgb: "rgb(222, 184, 135)", cycle: 4},
+ {authored: "cadetblue", name: "cadetblue", hex: "#5f9ea0", hsl: "hsl(181.8, 25.5%, 50%)", rgb: "rgb(95, 158, 160)", cycle: 4},
+ {authored: "chartreuse", name: "chartreuse", hex: "#7fff00", hsl: "hsl(90.1, 100%, 50%)", rgb: "rgb(127, 255, 0)", cycle: 4},
+ {authored: "chocolate", name: "chocolate", hex: "#d2691e", hsl: "hsl(25, 75%, 47.1%)", rgb: "rgb(210, 105, 30)", cycle: 4},
+ {authored: "coral", name: "coral", hex: "#ff7f50", hsl: "hsl(16.1, 100%, 65.7%)", rgb: "rgb(255, 127, 80)", cycle: 4},
+ {authored: "cornflowerblue", name: "cornflowerblue", hex: "#6495ed", hsl: "hsl(218.5, 79.2%, 66.1%)", rgb: "rgb(100, 149, 237)", cycle: 4},
+ {authored: "cornsilk", name: "cornsilk", hex: "#fff8dc", hsl: "hsl(48, 100%, 93.1%)", rgb: "rgb(255, 248, 220)", cycle: 4},
+ {authored: "crimson", name: "crimson", hex: "#dc143c", hsl: "hsl(348, 83.3%, 47.1%)", rgb: "rgb(220, 20, 60)", cycle: 4},
+ {authored: "cyan", name: "aqua", hex: "#0ff", hsl: "hsl(180, 100%, 50%)", rgb: "rgb(0, 255, 255)", cycle: 4},
+ {authored: "darkblue", name: "darkblue", hex: "#00008b", hsl: "hsl(240, 100%, 27.3%)", rgb: "rgb(0, 0, 139)", cycle: 4},
+ {authored: "darkcyan", name: "darkcyan", hex: "#008b8b", hsl: "hsl(180, 100%, 27.3%)", rgb: "rgb(0, 139, 139)", cycle: 4},
+ {authored: "darkgoldenrod", name: "darkgoldenrod", hex: "#b8860b", hsl: "hsl(42.7, 88.7%, 38.2%)", rgb: "rgb(184, 134, 11)", cycle: 4},
+ {authored: "darkgray", name: "darkgray", hex: "#a9a9a9", hsl: "hsl(0, 0%, 66.3%)", rgb: "rgb(169, 169, 169)", cycle: 4},
+ {authored: "darkgreen", name: "darkgreen", hex: "#006400", hsl: "hsl(120, 100%, 19.6%)", rgb: "rgb(0, 100, 0)", cycle: 4},
+ {authored: "darkgrey", name: "darkgray", hex: "#a9a9a9", hsl: "hsl(0, 0%, 66.3%)", rgb: "rgb(169, 169, 169)", cycle: 4},
+ {authored: "darkkhaki", name: "darkkhaki", hex: "#bdb76b", hsl: "hsl(55.6, 38.3%, 58%)", rgb: "rgb(189, 183, 107)", cycle: 4},
+ {authored: "darkmagenta", name: "darkmagenta", hex: "#8b008b", hsl: "hsl(300, 100%, 27.3%)", rgb: "rgb(139, 0, 139)", cycle: 4},
+ {authored: "darkolivegreen", name: "darkolivegreen", hex: "#556b2f", hsl: "hsl(82, 39%, 30.2%)", rgb: "rgb(85, 107, 47)", cycle: 4},
+ {authored: "darkorange", name: "darkorange", hex: "#ff8c00", hsl: "hsl(32.9, 100%, 50%)", rgb: "rgb(255, 140, 0)", cycle: 4},
+ {authored: "darkorchid", name: "darkorchid", hex: "#9932cc", hsl: "hsl(280.1, 60.6%, 49.8%)", rgb: "rgb(153, 50, 204)", cycle: 4},
+ {authored: "darkred", name: "darkred", hex: "#8b0000", hsl: "hsl(0, 100%, 27.3%)", rgb: "rgb(139, 0, 0)", cycle: 4},
+ {authored: "darksalmon", name: "darksalmon", hex: "#e9967a", hsl: "hsl(15.1, 71.6%, 69.6%)", rgb: "rgb(233, 150, 122)", cycle: 4},
+ {authored: "darkseagreen", name: "darkseagreen", hex: "#8fbc8f", hsl: "hsl(120, 25.1%, 64.9%)", rgb: "rgb(143, 188, 143)", cycle: 4},
+ {authored: "darkslateblue", name: "darkslateblue", hex: "#483d8b", hsl: "hsl(248.5, 39%, 39.2%)", rgb: "rgb(72, 61, 139)", cycle: 4},
+ {authored: "darkslategray", name: "darkslategray", hex: "#2f4f4f", hsl: "hsl(180, 25.4%, 24.7%)", rgb: "rgb(47, 79, 79)", cycle: 4},
+ {authored: "darkslategrey", name: "darkslategray", hex: "#2f4f4f", hsl: "hsl(180, 25.4%, 24.7%)", rgb: "rgb(47, 79, 79)", cycle: 4},
+ {authored: "darkturquoise", name: "darkturquoise", hex: "#00ced1", hsl: "hsl(180.9, 100%, 41%)", rgb: "rgb(0, 206, 209)", cycle: 4},
+ {authored: "darkviolet", name: "darkviolet", hex: "#9400d3", hsl: "hsl(282.1, 100%, 41.4%)", rgb: "rgb(148, 0, 211)", cycle: 4},
+ {authored: "deeppink", name: "deeppink", hex: "#ff1493", hsl: "hsl(327.6, 100%, 53.9%)", rgb: "rgb(255, 20, 147)", cycle: 4},
+ {authored: "deepskyblue", name: "deepskyblue", hex: "#00bfff", hsl: "hsl(195.1, 100%, 50%)", rgb: "rgb(0, 191, 255)", cycle: 4},
+ {authored: "dimgray", name: "dimgray", hex: "#696969", hsl: "hsl(0, 0%, 41.2%)", rgb: "rgb(105, 105, 105)", cycle: 4},
+ {authored: "dodgerblue", name: "dodgerblue", hex: "#1e90ff", hsl: "hsl(209.6, 100%, 55.9%)", rgb: "rgb(30, 144, 255)", cycle: 4},
+ {authored: "firebrick", name: "firebrick", hex: "#b22222", hsl: "hsl(0, 67.9%, 41.6%)", rgb: "rgb(178, 34, 34)", cycle: 4},
+ {authored: "floralwhite", name: "floralwhite", hex: "#fffaf0", hsl: "hsl(40, 100%, 97.1%)", rgb: "rgb(255, 250, 240)", cycle: 4},
+ {authored: "forestgreen", name: "forestgreen", hex: "#228b22", hsl: "hsl(120, 60.7%, 33.9%)", rgb: "rgb(34, 139, 34)", cycle: 4},
+ {authored: "fuchsia", name: "fuchsia", hex: "#f0f", hsl: "hsl(300, 100%, 50%)", rgb: "rgb(255, 0, 255)", cycle: 4},
+ {authored: "gainsboro", name: "gainsboro", hex: "#dcdcdc", hsl: "hsl(0, 0%, 86.3%)", rgb: "rgb(220, 220, 220)", cycle: 4},
+ {authored: "ghostwhite", name: "ghostwhite", hex: "#f8f8ff", hsl: "hsl(240, 100%, 98.6%)", rgb: "rgb(248, 248, 255)", cycle: 4},
+ {authored: "gold", name: "gold", hex: "#ffd700", hsl: "hsl(50.6, 100%, 50%)", rgb: "rgb(255, 215, 0)", cycle: 4},
+ {authored: "goldenrod", name: "goldenrod", hex: "#daa520", hsl: "hsl(42.9, 74.4%, 49%)", rgb: "rgb(218, 165, 32)", cycle: 4},
+ {authored: "gray", name: "gray", hex: "#808080", hsl: "hsl(0, 0%, 50.2%)", rgb: "rgb(128, 128, 128)", cycle: 4},
+ {authored: "green", name: "green", hex: "#008000", hsl: "hsl(120, 100%, 25.1%)", rgb: "rgb(0, 128, 0)", cycle: 4},
+ {authored: "greenyellow", name: "greenyellow", hex: "#adff2f", hsl: "hsl(83.7, 100%, 59.2%)", rgb: "rgb(173, 255, 47)", cycle: 4},
+ {authored: "grey", name: "gray", hex: "#808080", hsl: "hsl(0, 0%, 50.2%)", rgb: "rgb(128, 128, 128)", cycle: 4},
+ {authored: "honeydew", name: "honeydew", hex: "#f0fff0", hsl: "hsl(120, 100%, 97.1%)", rgb: "rgb(240, 255, 240)", cycle: 4},
+ {authored: "hotpink", name: "hotpink", hex: "#ff69b4", hsl: "hsl(330, 100%, 70.6%)", rgb: "rgb(255, 105, 180)", cycle: 4},
+ {authored: "indianred", name: "indianred", hex: "#cd5c5c", hsl: "hsl(0, 53.1%, 58.2%)", rgb: "rgb(205, 92, 92)", cycle: 4},
+ {authored: "indigo", name: "indigo", hex: "#4b0082", hsl: "hsl(274.6, 100%, 25.5%)", rgb: "rgb(75, 0, 130)", cycle: 4},
+ {authored: "ivory", name: "ivory", hex: "#fffff0", hsl: "hsl(60, 100%, 97.1%)", rgb: "rgb(255, 255, 240)", cycle: 4},
+ {authored: "khaki", name: "khaki", hex: "#f0e68c", hsl: "hsl(54, 76.9%, 74.5%)", rgb: "rgb(240, 230, 140)", cycle: 4},
+ {authored: "lavender", name: "lavender", hex: "#e6e6fa", hsl: "hsl(240, 66.7%, 94.1%)", rgb: "rgb(230, 230, 250)", cycle: 4},
+ {authored: "lavenderblush", name: "lavenderblush", hex: "#fff0f5", hsl: "hsl(340, 100%, 97.1%)", rgb: "rgb(255, 240, 245)", cycle: 4},
+ {authored: "lawngreen", name: "lawngreen", hex: "#7cfc00", hsl: "hsl(90.5, 100%, 49.4%)", rgb: "rgb(124, 252, 0)", cycle: 4},
+ {authored: "lemonchiffon", name: "lemonchiffon", hex: "#fffacd", hsl: "hsl(54, 100%, 90.2%)", rgb: "rgb(255, 250, 205)", cycle: 4},
+ {authored: "lightblue", name: "lightblue", hex: "#add8e6", hsl: "hsl(194.7, 53.3%, 79%)", rgb: "rgb(173, 216, 230)", cycle: 4},
+ {authored: "lightcoral", name: "lightcoral", hex: "#f08080", hsl: "hsl(0, 78.9%, 72.2%)", rgb: "rgb(240, 128, 128)", cycle: 4},
+ {authored: "lightcyan", name: "lightcyan", hex: "#e0ffff", hsl: "hsl(180, 100%, 93.9%)", rgb: "rgb(224, 255, 255)", cycle: 4},
+ {authored: "lightgoldenrodyellow", name: "lightgoldenrodyellow", hex: "#fafad2", hsl: "hsl(60, 80%, 90.2%)", rgb: "rgb(250, 250, 210)", cycle: 4},
+ {authored: "lightgray", name: "lightgray", hex: "#d3d3d3", hsl: "hsl(0, 0%, 82.7%)", rgb: "rgb(211, 211, 211)", cycle: 4},
+ {authored: "lightgreen", name: "lightgreen", hex: "#90ee90", hsl: "hsl(120, 73.4%, 74.9%)", rgb: "rgb(144, 238, 144)", cycle: 4},
+ {authored: "lightgrey", name: "lightgray", hex: "#d3d3d3", hsl: "hsl(0, 0%, 82.7%)", rgb: "rgb(211, 211, 211)", cycle: 4},
+ {authored: "lightpink", name: "lightpink", hex: "#ffb6c1", hsl: "hsl(351, 100%, 85.7%)", rgb: "rgb(255, 182, 193)", cycle: 4},
+ {authored: "lightsalmon", name: "lightsalmon", hex: "#ffa07a", hsl: "hsl(17.1, 100%, 73.9%)", rgb: "rgb(255, 160, 122)", cycle: 4},
+ {authored: "lightseagreen", name: "lightseagreen", hex: "#20b2aa", hsl: "hsl(176.7, 69.5%, 41.2%)", rgb: "rgb(32, 178, 170)", cycle: 4},
+ {authored: "lightskyblue", name: "lightskyblue", hex: "#87cefa", hsl: "hsl(203, 92%, 75.5%)", rgb: "rgb(135, 206, 250)", cycle: 4},
+ {authored: "lightslategray", name: "lightslategray", hex: "#789", hsl: "hsl(210, 14.3%, 53.3%)", rgb: "rgb(119, 136, 153)", cycle: 4},
+ {authored: "lightslategrey", name: "lightslategray", hex: "#789", hsl: "hsl(210, 14.3%, 53.3%)", rgb: "rgb(119, 136, 153)", cycle: 4},
+ {authored: "lightsteelblue", name: "lightsteelblue", hex: "#b0c4de", hsl: "hsl(213.9, 41.1%, 78%)", rgb: "rgb(176, 196, 222)", cycle: 4},
+ {authored: "lightyellow", name: "lightyellow", hex: "#ffffe0", hsl: "hsl(60, 100%, 93.9%)", rgb: "rgb(255, 255, 224)", cycle: 4},
+ {authored: "lime", name: "lime", hex: "#0f0", hsl: "hsl(120, 100%, 50%)", rgb: "rgb(0, 255, 0)", cycle: 4},
+ {authored: "limegreen", name: "limegreen", hex: "#32cd32", hsl: "hsl(120, 60.8%, 50%)", rgb: "rgb(50, 205, 50)", cycle: 4},
+ {authored: "linen", name: "linen", hex: "#faf0e6", hsl: "hsl(30, 66.7%, 94.1%)", rgb: "rgb(250, 240, 230)", cycle: 4},
+ {authored: "magenta", name: "fuchsia", hex: "#f0f", hsl: "hsl(300, 100%, 50%)", rgb: "rgb(255, 0, 255)", cycle: 4},
+ {authored: "maroon", name: "maroon", hex: "#800000", hsl: "hsl(0, 100%, 25.1%)", rgb: "rgb(128, 0, 0)", cycle: 4},
+ {authored: "mediumaquamarine", name: "mediumaquamarine", hex: "#66cdaa", hsl: "hsl(159.6, 50.7%, 60.2%)", rgb: "rgb(102, 205, 170)", cycle: 4},
+ {authored: "mediumblue", name: "mediumblue", hex: "#0000cd", hsl: "hsl(240, 100%, 40.2%)", rgb: "rgb(0, 0, 205)", cycle: 4},
+ {authored: "mediumorchid", name: "mediumorchid", hex: "#ba55d3", hsl: "hsl(288.1, 58.9%, 58%)", rgb: "rgb(186, 85, 211)", cycle: 4},
+ {authored: "mediumpurple", name: "mediumpurple", hex: "#9370db", hsl: "hsl(259.6, 59.8%, 64.9%)", rgb: "rgb(147, 112, 219)", cycle: 4},
+ {authored: "mediumseagreen", name: "mediumseagreen", hex: "#3cb371", hsl: "hsl(146.7, 49.8%, 46.9%)", rgb: "rgb(60, 179, 113)", cycle: 4},
+ {authored: "mediumslateblue", name: "mediumslateblue", hex: "#7b68ee", hsl: "hsl(248.5, 79.8%, 67.1%)", rgb: "rgb(123, 104, 238)", cycle: 4},
+ {authored: "mediumspringgreen", name: "mediumspringgreen", hex: "#00fa9a", hsl: "hsl(157, 100%, 49%)", rgb: "rgb(0, 250, 154)", cycle: 4},
+ {authored: "mediumturquoise", name: "mediumturquoise", hex: "#48d1cc", hsl: "hsl(177.8, 59.8%, 55.1%)", rgb: "rgb(72, 209, 204)", cycle: 4},
+ {authored: "mediumvioletred", name: "mediumvioletred", hex: "#c71585", hsl: "hsl(322.2, 80.9%, 43.1%)", rgb: "rgb(199, 21, 133)", cycle: 4},
+ {authored: "midnightblue", name: "midnightblue", hex: "#191970", hsl: "hsl(240, 63.5%, 26.9%)", rgb: "rgb(25, 25, 112)", cycle: 4},
+ {authored: "mintcream", name: "mintcream", hex: "#f5fffa", hsl: "hsl(150, 100%, 98%)", rgb: "rgb(245, 255, 250)", cycle: 4},
+ {authored: "mistyrose", name: "mistyrose", hex: "#ffe4e1", hsl: "hsl(6, 100%, 94.1%)", rgb: "rgb(255, 228, 225)", cycle: 4},
+ {authored: "moccasin", name: "moccasin", hex: "#ffe4b5", hsl: "hsl(38.1, 100%, 85.5%)", rgb: "rgb(255, 228, 181)", cycle: 4},
+ {authored: "navajowhite", name: "navajowhite", hex: "#ffdead", hsl: "hsl(35.9, 100%, 83.9%)", rgb: "rgb(255, 222, 173)", cycle: 4},
+ {authored: "navy", name: "navy", hex: "#000080", hsl: "hsl(240, 100%, 25.1%)", rgb: "rgb(0, 0, 128)", cycle: 4},
+ {authored: "oldlace", name: "oldlace", hex: "#fdf5e6", hsl: "hsl(39.1, 85.2%, 94.7%)", rgb: "rgb(253, 245, 230)", cycle: 4},
+ {authored: "olive", name: "olive", hex: "#808000", hsl: "hsl(60, 100%, 25.1%)", rgb: "rgb(128, 128, 0)", cycle: 4},
+ {authored: "olivedrab", name: "olivedrab", hex: "#6b8e23", hsl: "hsl(79.6, 60.5%, 34.7%)", rgb: "rgb(107, 142, 35)", cycle: 4},
+ {authored: "orange", name: "orange", hex: "#ffa500", hsl: "hsl(38.8, 100%, 50%)", rgb: "rgb(255, 165, 0)", cycle: 4},
+ {authored: "orangered", name: "orangered", hex: "#ff4500", hsl: "hsl(16.2, 100%, 50%)", rgb: "rgb(255, 69, 0)", cycle: 4},
+ {authored: "orchid", name: "orchid", hex: "#da70d6", hsl: "hsl(302.3, 58.9%, 64.7%)", rgb: "rgb(218, 112, 214)", cycle: 4},
+ {authored: "palegoldenrod", name: "palegoldenrod", hex: "#eee8aa", hsl: "hsl(54.7, 66.7%, 80%)", rgb: "rgb(238, 232, 170)", cycle: 4},
+ {authored: "palegreen", name: "palegreen", hex: "#98fb98", hsl: "hsl(120, 92.5%, 79%)", rgb: "rgb(152, 251, 152)", cycle: 4},
+ {authored: "paleturquoise", name: "paleturquoise", hex: "#afeeee", hsl: "hsl(180, 64.9%, 81%)", rgb: "rgb(175, 238, 238)", cycle: 4},
+ {authored: "palevioletred", name: "palevioletred", hex: "#db7093", hsl: "hsl(340.4, 59.8%, 64.9%)", rgb: "rgb(219, 112, 147)", cycle: 4},
+ {authored: "papayawhip", name: "papayawhip", hex: "#ffefd5", hsl: "hsl(37.1, 100%, 91.8%)", rgb: "rgb(255, 239, 213)", cycle: 4},
+ {authored: "peachpuff", name: "peachpuff", hex: "#ffdab9", hsl: "hsl(28.3, 100%, 86.3%)", rgb: "rgb(255, 218, 185)", cycle: 4},
+ {authored: "peru", name: "peru", hex: "#cd853f", hsl: "hsl(29.6, 58.7%, 52.5%)", rgb: "rgb(205, 133, 63)", cycle: 4},
+ {authored: "pink", name: "pink", hex: "#ffc0cb", hsl: "hsl(349.5, 100%, 87.6%)", rgb: "rgb(255, 192, 203)", cycle: 4},
+ {authored: "plum", name: "plum", hex: "#dda0dd", hsl: "hsl(300, 47.3%, 74.7%)", rgb: "rgb(221, 160, 221)", cycle: 4},
+ {authored: "powderblue", name: "powderblue", hex: "#b0e0e6", hsl: "hsl(186.7, 51.9%, 79.6%)", rgb: "rgb(176, 224, 230)", cycle: 4},
+ {authored: "purple", name: "purple", hex: "#800080", hsl: "hsl(300, 100%, 25.1%)", rgb: "rgb(128, 0, 128)", cycle: 4},
+ {authored: "rebeccapurple", name: "rebeccapurple", hex: "#639", hsl: "hsl(270, 50%, 40%)", rgb: "rgb(102, 51, 153)", cycle: 4},
+ {authored: "red", name: "red", hex: "#f00", hsl: "hsl(0, 100%, 50%)", rgb: "rgb(255, 0, 0)", cycle: 4},
+ {authored: "rosybrown", name: "rosybrown", hex: "#bc8f8f", hsl: "hsl(0, 25.1%, 64.9%)", rgb: "rgb(188, 143, 143)", cycle: 4},
+ {authored: "royalblue", name: "royalblue", hex: "#4169e1", hsl: "hsl(225, 72.7%, 56.9%)", rgb: "rgb(65, 105, 225)", cycle: 4},
+ {authored: "saddlebrown", name: "saddlebrown", hex: "#8b4513", hsl: "hsl(25, 75.9%, 31%)", rgb: "rgb(139, 69, 19)", cycle: 4},
+ {authored: "salmon", name: "salmon", hex: "#fa8072", hsl: "hsl(6.2, 93.2%, 71.4%)", rgb: "rgb(250, 128, 114)", cycle: 4},
+ {authored: "sandybrown", name: "sandybrown", hex: "#f4a460", hsl: "hsl(27.6, 87.1%, 66.7%)", rgb: "rgb(244, 164, 96)", cycle: 4},
+ {authored: "seagreen", name: "seagreen", hex: "#2e8b57", hsl: "hsl(146.5, 50.3%, 36.3%)", rgb: "rgb(46, 139, 87)", cycle: 4},
+ {authored: "seashell", name: "seashell", hex: "#fff5ee", hsl: "hsl(24.7, 100%, 96.7%)", rgb: "rgb(255, 245, 238)", cycle: 4},
+ {authored: "sienna", name: "sienna", hex: "#a0522d", hsl: "hsl(19.3, 56.1%, 40.2%)", rgb: "rgb(160, 82, 45)", cycle: 4},
+ {authored: "silver", name: "silver", hex: "#c0c0c0", hsl: "hsl(0, 0%, 75.3%)", rgb: "rgb(192, 192, 192)", cycle: 4},
+ {authored: "skyblue", name: "skyblue", hex: "#87ceeb", hsl: "hsl(197.4, 71.4%, 72.5%)", rgb: "rgb(135, 206, 235)", cycle: 4},
+ {authored: "slateblue", name: "slateblue", hex: "#6a5acd", hsl: "hsl(248.3, 53.5%, 57.8%)", rgb: "rgb(106, 90, 205)", cycle: 4},
+ {authored: "slategray", name: "slategray", hex: "#708090", hsl: "hsl(210, 12.6%, 50.2%)", rgb: "rgb(112, 128, 144)", cycle: 4},
+ {authored: "slategrey", name: "slategray", hex: "#708090", hsl: "hsl(210, 12.6%, 50.2%)", rgb: "rgb(112, 128, 144)", cycle: 4},
+ {authored: "snow", name: "snow", hex: "#fffafa", hsl: "hsl(0, 100%, 99%)", rgb: "rgb(255, 250, 250)", cycle: 4},
+ {authored: "springgreen", name: "springgreen", hex: "#00ff7f", hsl: "hsl(149.9, 100%, 50%)", rgb: "rgb(0, 255, 127)", cycle: 4},
+ {authored: "steelblue", name: "steelblue", hex: "#4682b4", hsl: "hsl(207.3, 44%, 49%)", rgb: "rgb(70, 130, 180)", cycle: 4},
+ {authored: "tan", name: "tan", hex: "#d2b48c", hsl: "hsl(34.3, 43.7%, 68.6%)", rgb: "rgb(210, 180, 140)", cycle: 4},
+ {authored: "teal", name: "teal", hex: "#008080", hsl: "hsl(180, 100%, 25.1%)", rgb: "rgb(0, 128, 128)", cycle: 4},
+ {authored: "thistle", name: "thistle", hex: "#d8bfd8", hsl: "hsl(300, 24.3%, 79.8%)", rgb: "rgb(216, 191, 216)", cycle: 4},
+ {authored: "tomato", name: "tomato", hex: "#ff6347", hsl: "hsl(9.1, 100%, 63.9%)", rgb: "rgb(255, 99, 71)", cycle: 4},
+ {authored: "turquoise", name: "turquoise", hex: "#40e0d0", hsl: "hsl(174, 72.1%, 56.5%)", rgb: "rgb(64, 224, 208)", cycle: 4},
+ {authored: "violet", name: "violet", hex: "#ee82ee", hsl: "hsl(300, 76.1%, 72.2%)", rgb: "rgb(238, 130, 238)", cycle: 4},
+ {authored: "wheat", name: "wheat", hex: "#f5deb3", hsl: "hsl(39.1, 76.7%, 83.1%)", rgb: "rgb(245, 222, 179)", cycle: 4},
+ {authored: "white", name: "white", hex: "#fff", hsl: "hsl(0, 0%, 100%)", rgb: "rgb(255, 255, 255)", cycle: 4},
+ {authored: "whitesmoke", name: "whitesmoke", hex: "#f5f5f5", hsl: "hsl(0, 0%, 96.1%)", rgb: "rgb(245, 245, 245)", cycle: 4},
+ {authored: "yellow", name: "yellow", hex: "#ff0", hsl: "hsl(60, 100%, 50%)", rgb: "rgb(255, 255, 0)", cycle: 4},
+ {authored: "yellowgreen", name: "yellowgreen", hex: "#9acd32", hsl: "hsl(79.7, 60.8%, 50%)", rgb: "rgb(154, 205, 50)", cycle: 4},
+ {authored: "rgba(0, 0, 0, 0)", name: "#0000", hex: "#0000", hsl: "hsla(0, 0%, 0%, 0)", rgb: "rgba(0, 0, 0, 0)", cycle: 3},
+ {authored: "hsla(0, 0%, 0%, 0)", name: "#0000", hex: "#0000", hsl: "hsla(0, 0%, 0%, 0)", rgb: "rgba(0, 0, 0, 0)", cycle: 3},
+ {authored: "rgba(50, 60, 70, 0.5)", name: "#323c4680", hex: "#323c4680", hsl: "hsla(210, 16.7%, 23.5%, 0.5)", rgb: "rgba(50, 60, 70, 0.5)", cycle: 3},
+ {authored: "rgba(0, 0, 0, 0.3)", name: "#0000004d", hex: "#0000004d", hsl: "hsla(0, 0%, 0%, 0.3)", rgb: "rgba(0, 0, 0, 0.3)", cycle: 3},
+ {authored: "rgba(255, 255, 255, 0.6)", name: "#fff9", hex: "#fff9", hsl: "hsla(0, 0%, 100%, 0.6)", rgb: "rgba(255, 255, 255, 0.6)", cycle: 3},
+ {authored: "rgba(127, 89, 45, 1)", name: "#7f592d", hex: "#7f592d", hsl: "hsl(32.2, 47.7%, 33.7%)", rgb: "rgb(127, 89, 45)", cycle: 3},
+ {authored: "hsla(19.304, 56%, 40%, 1)", name: "#9f522d", hex: "#9f522d", hsl: "hsl(19.5, 55.9%, 40%)", rgb: "rgb(159, 82, 45)", cycle: 3},
+ {authored: "#f089", name: "#f089", hex: "#f089", hsl: "hsla(328, 100%, 50%, 0.6)", rgb: "rgba(255, 0, 136, 0.6)", cycle: 3},
+ {authored: "#00ff8080", name: "#00ff8080", hex: "#00ff8080", hsl: "hsla(150.1, 100%, 50%, 0.5)", rgb: "rgba(0, 255, 128, 0.5)", cycle: 3},
+ {authored: "currentcolor", name: "currentcolor", hex: "currentcolor", hsl: "currentcolor", rgb: "currentcolor", cycle: false},
+ {authored: "inherit", name: "inherit", hex: "inherit", hsl: "inherit", rgb: "inherit", cycle: false},
+ {authored: "initial", name: "initial", hex: "initial", hsl: "initial", rgb: "initial", cycle: false},
+ {authored: "invalidColor", name: "", hex: "", hsl: "", rgb: "", cycle: false},
+ {authored: "transparent", name: "transparent", hex: "transparent", hsl: "transparent", rgb: "transparent", cycle: false},
+ {authored: "unset", name: "unset", hex: "unset", hsl: "unset", rgb: "unset", cycle: false},
+ ];
+}
+/* eslint-enable max-len */
+
+// Allow this function to be shared on mochitests and xpcshell tests.
+if (typeof module === "object") {
+ module.exports = getFixtureColorData;
+}
diff --git a/devtools/client/shared/test/helper_html_tooltip.js b/devtools/client/shared/test/helper_html_tooltip.js
new file mode 100644
index 000000000..ffc6945f3
--- /dev/null
+++ b/devtools/client/shared/test/helper_html_tooltip.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
+
+"use strict";
+
+/**
+ * Helper methods for the HTMLTooltip integration tests.
+ */
+
+/**
+ * Display an existing HTMLTooltip on an anchor. After the tooltip "shown"
+ * event has been fired a reflow will be triggered.
+ *
+ * @param {HTMLTooltip} tooltip
+ * The tooltip instance to display
+ * @param {Node} anchor
+ * The anchor that should be used to display the tooltip
+ * @param {Object} see HTMLTooltip:show documentation
+ * @return {Promise} promise that resolves when "shown" has been fired, reflow
+ * and repaint done.
+ */
+function* showTooltip(tooltip, anchor, {position, x, y} = {}) {
+ let onShown = tooltip.once("shown");
+ tooltip.show(anchor, {position, x, y});
+ yield onShown;
+ return waitForReflow(tooltip);
+}
+
+/**
+ * Hide an existing HTMLTooltip. After the tooltip "hidden" event has been fired
+ * a reflow will be triggered.
+ *
+ * @param {HTMLTooltip} tooltip
+ * The tooltip instance to hide
+ * @return {Promise} promise that resolves when "hidden" has been fired, reflow
+ * and repaint done.
+ */
+function* hideTooltip(tooltip) {
+ let onPopupHidden = tooltip.once("hidden");
+ tooltip.hide();
+ yield onPopupHidden;
+ return waitForReflow(tooltip);
+}
+
+/**
+ * Forces the reflow of an HTMLTooltip document and waits for the next repaint.
+ *
+ * @param {HTMLTooltip} the tooltip to reflow
+ * @return {Promise} a promise that will resolve after the reflow and repaint
+ * have been executed.
+ */
+function waitForReflow(tooltip) {
+ let {doc} = tooltip;
+ return new Promise(resolve => {
+ doc.documentElement.offsetWidth;
+ doc.defaultView.requestAnimationFrame(resolve);
+ });
+}
+
+/**
+ * Test helper designed to check that a tooltip is displayed at the expected
+ * position relative to an anchor, given a set of expectations.
+ *
+ * @param {HTMLTooltip} tooltip
+ * The HTMLTooltip instance to check
+ * @param {Node} anchor
+ * The tooltip's anchor
+ * @param {Object} expected
+ * - {String} position : "top" or "bottom"
+ * - {Boolean} leftAligned
+ * - {Number} width: expected tooltip width
+ * - {Number} height: expected tooltip height
+ */
+function checkTooltipGeometry(tooltip, anchor,
+ {position, leftAligned = true, height, width} = {}) {
+ info("Check the tooltip geometry matches expected position and dimensions");
+ let tooltipRect = tooltip.container.getBoundingClientRect();
+ let anchorRect = anchor.getBoundingClientRect();
+
+ if (position === "top") {
+ is(tooltipRect.bottom, anchorRect.top, "Tooltip is above the anchor");
+ } else if (position === "bottom") {
+ is(tooltipRect.top, anchorRect.bottom, "Tooltip is below the anchor");
+ } else {
+ ok(false, "Invalid position provided to checkTooltipGeometry");
+ }
+
+ if (leftAligned) {
+ is(tooltipRect.left, anchorRect.left,
+ "Tooltip left-aligned with the anchor");
+ }
+
+ is(tooltipRect.height, height, "Tooltip has the expected height");
+ is(tooltipRect.width, width, "Tooltip has the expected width");
+}
diff --git a/devtools/client/shared/test/helper_inplace_editor.js b/devtools/client/shared/test/helper_inplace_editor.js
new file mode 100644
index 000000000..ec6b79e00
--- /dev/null
+++ b/devtools/client/shared/test/helper_inplace_editor.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
+/* import-globals-from head.js */
+
+"use strict";
+
+/**
+ * Helper methods for the HTMLTooltip integration tests.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const { editableField } = require("devtools/client/shared/inplace-editor");
+
+/**
+ * Create an inplace editor linked to a span element and click on the span to
+ * to turn to edit mode.
+ *
+ * @param {Object} options
+ * Options passed to the InplaceEditor/editableField constructor.
+ * @param {Document} doc
+ * Document where the span element will be created.
+ * @param {String} textContent
+ * (optional) String that will be used as the text content of the span.
+ */
+const createInplaceEditorAndClick = Task.async(function* (options, doc, textContent) {
+ let span = options.element = createSpan(doc);
+ if (textContent) {
+ span.textContent = textContent;
+ }
+
+ info("Creating an inplace-editor field");
+ editableField(options);
+
+ info("Clicking on the inplace-editor field to turn to edit mode");
+ span.click();
+});
+
+/**
+ * Helper to create a span in the provided document.
+ *
+ * @param {Document} doc
+ * Document where the span element will be created.
+ * @return {Element} the created span element.
+ */
+function createSpan(doc) {
+ info("Creating a new span element");
+ let div = doc.createElementNS(HTML_NS, "div");
+ let span = doc.createElementNS(HTML_NS, "span");
+ span.setAttribute("tabindex", "0");
+ span.style.fontSize = "11px";
+ span.style.display = "inline-block";
+ span.style.width = "100px";
+ span.style.border = "1px solid red";
+ span.style.fontFamily = "monospace";
+
+ div.style.height = "100%";
+ div.style.position = "absolute";
+ div.appendChild(span);
+
+ let parent = doc.querySelector("window") || doc.body;
+ parent.appendChild(div);
+ return span;
+}
+
+/**
+ * Test helper simulating a key event in an InplaceEditor and checking that the
+ * autocompletion works as expected.
+ *
+ * @param {Array} testData
+ * - {String} key, the key to send
+ * - {String} completion, the expected value of the auto-completion
+ * - {Number} index, the index of the selected suggestion in the popup
+ * - {Number} total, the total number of suggestions in the popup
+ * @param {InplaceEditor} editor
+ * The InplaceEditor instance being tested
+ */
+function* testCompletion([key, completion, index, total], editor) {
+ info("Pressing key " + key);
+ info("Expecting " + completion);
+
+ let onVisibilityChange = null;
+ let open = total > 0;
+ if (editor.popup.isOpen != open) {
+ onVisibilityChange = editor.popup.once(open ? "popup-opened" : "popup-closed");
+ }
+
+ let onSuggest;
+ if (/(left|right|back_space|escape)/ig.test(key)) {
+ info("Adding event listener for right|back_space|escape keys");
+ onSuggest = once(editor.input, "keypress");
+ } else {
+ info("Waiting for after-suggest event on the editor");
+ onSuggest = editor.once("after-suggest");
+ }
+
+ info("Synthesizing key " + key);
+ EventUtils.synthesizeKey(key, {}, editor.input.defaultView);
+
+ yield onSuggest;
+ yield onVisibilityChange;
+ yield waitForTick();
+
+ info("Checking the state");
+ if (completion !== null) {
+ is(editor.input.value, completion, "Correct value is autocompleted");
+ }
+ if (total === 0) {
+ ok(!(editor.popup && editor.popup.isOpen), "Popup is closed");
+ } else {
+ ok(editor.popup.isOpen, "Popup is open");
+ is(editor.popup.getItems().length, total, "Number of suggestions match");
+ is(editor.popup.selectedIndex, index, "Expected item is selected");
+ }
+}
diff --git a/devtools/client/shared/test/html-mdn-css-basic-testing.html b/devtools/client/shared/test/html-mdn-css-basic-testing.html
new file mode 100644
index 000000000..182fa6d9b
--- /dev/null
+++ b/devtools/client/shared/test/html-mdn-css-basic-testing.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+</head>
+
+<body>
+
+ <h2 id="Summary">Summary</h2>
+
+ <p>A summary of the property.</p>
+
+ <h2 id="Syntax">Syntax</h2>
+
+ <pre>/* The part we want */
+this: is-the-part-we-want;</pre>
+
+
+
+</body>
+</html> \ No newline at end of file
diff --git a/devtools/client/shared/test/html-mdn-css-no-summary-or-syntax.html b/devtools/client/shared/test/html-mdn-css-no-summary-or-syntax.html
new file mode 100644
index 000000000..8e1a7c3f6
--- /dev/null
+++ b/devtools/client/shared/test/html-mdn-css-no-summary-or-syntax.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+</head>
+
+<body>
+
+<p>This is not the summary or the syntax.</p>
+
+</body>
+</html> \ No newline at end of file
diff --git a/devtools/client/shared/test/html-mdn-css-no-summary.html b/devtools/client/shared/test/html-mdn-css-no-summary.html
new file mode 100644
index 000000000..3af52b4f6
--- /dev/null
+++ b/devtools/client/shared/test/html-mdn-css-no-summary.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+</head>
+
+<body>
+
+ <p>This is not the summary.</p>
+
+ <h2 id="Syntax">Syntax</h2>
+
+ <pre>To be ignored.</pre>
+
+ <pre>/* The part we want */
+this: is-the-part-we-want;</pre>
+
+
+
+</body>
+</html> \ No newline at end of file
diff --git a/devtools/client/shared/test/html-mdn-css-no-syntax.html b/devtools/client/shared/test/html-mdn-css-no-syntax.html
new file mode 100644
index 000000000..4e43bc434
--- /dev/null
+++ b/devtools/client/shared/test/html-mdn-css-no-syntax.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+</head>
+
+<body>
+
+ <h2 id="Summary">Summary</h2>
+
+ <p>A summary of the property.</p>
+
+ <p>This is not the syntax.</p>
+
+
+</body>
+</html>
diff --git a/devtools/client/shared/test/html-mdn-css-syntax-old-style.html b/devtools/client/shared/test/html-mdn-css-syntax-old-style.html
new file mode 100644
index 000000000..281267cc4
--- /dev/null
+++ b/devtools/client/shared/test/html-mdn-css-syntax-old-style.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+</head>
+
+<body>
+
+ <h2 id="Summary">Summary</h2>
+
+ <p>A summary of the property.</p>
+
+ <h2 id="Syntax">Syntax</h2>
+
+ <pre>The part we should ignore</pre>
+
+ <pre>/* The part we want */
+this: is-the-part-we-want;</pre>
+
+
+
+</body>
+</html> \ No newline at end of file
diff --git a/devtools/client/shared/test/leakhunt.js b/devtools/client/shared/test/leakhunt.js
new file mode 100644
index 000000000..e71244955
--- /dev/null
+++ b/devtools/client/shared/test/leakhunt.js
@@ -0,0 +1,165 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Memory leak hunter. Walks a tree of objects looking for DOM nodes.
+ * Usage:
+ * leakHunt({
+ * thing: thing,
+ * otherthing: otherthing
+ * });
+ */
+function leakHunt(root) {
+ let path = [];
+ let seen = [];
+
+ try {
+ let output = leakHunt.inner(root, path, seen);
+ output.forEach(function (line) {
+ dump(line + "\n");
+ });
+ } catch (ex) {
+ dump(ex + "\n");
+ }
+}
+
+leakHunt.inner = function (root, path, seen) {
+ let prefix = new Array(path.length).join(" ");
+
+ let reply = [];
+ function log(msg) {
+ reply.push(msg);
+ }
+
+ let direct;
+ try {
+ direct = Object.keys(root);
+ } catch (ex) {
+ log(prefix + " Error enumerating: " + ex);
+ return reply;
+ }
+
+ try {
+ let index = 0;
+ for (let data of root) {
+ let prop = "" + index;
+ leakHunt.digProperty(prop, data, path, seen, direct, log);
+ index++;
+ }
+ } catch (ex) {
+ /* Ignore things that are not enumerable */
+ }
+
+ for (let prop in root) {
+ let data;
+ try {
+ data = root[prop];
+ } catch (ex) {
+ log(prefix + " " + prop + " = Error: " + ex.toString().substring(0, 30));
+ continue;
+ }
+
+ leakHunt.digProperty(prop, data, path, seen, direct, log);
+ }
+
+ return reply;
+};
+
+leakHunt.hide = [ /^string$/, /^number$/, /^boolean$/, /^null/, /^undefined/ ];
+
+leakHunt.noRecurse = [
+ /^string$/, /^number$/, /^boolean$/, /^null/, /^undefined/,
+ /^Window$/, /^Document$/,
+ /^XULDocument$/, /^XULElement$/,
+ /^DOMWindow$/, /^HTMLDocument$/, /^HTML.*Element$/, /^ChromeWindow$/
+];
+
+leakHunt.digProperty = function (prop, data, path, seen, direct, log) {
+ let newPath = path.slice();
+ newPath.push(prop);
+ let prefix = new Array(newPath.length).join(" ");
+
+ let recurse = true;
+ let message = leakHunt.getType(data);
+
+ if (leakHunt.matchesAnyPattern(message, leakHunt.hide)) {
+ return;
+ }
+
+ if (message === "function" && direct.indexOf(prop) == -1) {
+ return;
+ }
+
+ if (message === "string") {
+ let extra = data.length > 10 ? data.substring(0, 9) + "_" : data;
+ message += ' "' + extra.replace(/\n/g, "|") + '"';
+ recurse = false;
+ } else if (leakHunt.matchesAnyPattern(message, leakHunt.noRecurse)) {
+ message += " (no recurse)";
+ recurse = false;
+ } else if (seen.indexOf(data) !== -1) {
+ message += " (already seen)";
+ recurse = false;
+ }
+
+ if (recurse) {
+ seen.push(data);
+ let lines = leakHunt.inner(data, newPath, seen);
+ if (lines.length == 0) {
+ if (message !== "function") {
+ log(prefix + prop + " = " + message + " { }");
+ }
+ } else {
+ log(prefix + prop + " = " + message + " {");
+ lines.forEach(function (line) {
+ log(line);
+ });
+ log(prefix + "}");
+ }
+ } else {
+ log(prefix + prop + " = " + message);
+ }
+};
+
+leakHunt.matchesAnyPattern = function (str, patterns) {
+ let match = false;
+ patterns.forEach(function (pattern) {
+ if (str.match(pattern)) {
+ match = true;
+ }
+ });
+ return match;
+};
+
+leakHunt.getType = function (data) {
+ if (data === null) {
+ return "null";
+ }
+ if (data === undefined) {
+ return "undefined";
+ }
+
+ let type = typeof data;
+ if (type === "object" || type === "Object") {
+ type = leakHunt.getCtorName(data);
+ }
+
+ return type;
+};
+
+leakHunt.getCtorName = function (obj) {
+ try {
+ if (obj.constructor && obj.constructor.name) {
+ return obj.constructor.name;
+ }
+ } catch (ex) {
+ return "UnknownObject";
+ }
+
+ // If that fails, use Objects toString which sometimes gives something
+ // better than 'Object', and at least defaults to Object if nothing better
+ return Object.prototype.toString.call(obj).slice(8, -1);
+};
diff --git a/devtools/client/shared/test/test-actor-registry.js b/devtools/client/shared/test/test-actor-registry.js
new file mode 100644
index 000000000..e3b47c154
--- /dev/null
+++ b/devtools/client/shared/test/test-actor-registry.js
@@ -0,0 +1,97 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+(function (exports) {
+ const Cu = Components.utils;
+ const CC = Components.Constructor;
+
+ const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+ const { fetch } = require("devtools/shared/DevToolsUtils");
+ const defer = require("devtools/shared/defer");
+ const { Task } = require("devtools/shared/task");
+
+ const TEST_URL_ROOT = "http://example.com/browser/devtools/client/shared/test/";
+ const ACTOR_URL = TEST_URL_ROOT + "test-actor.js";
+
+ // Register a test actor that can operate on the remote document
+ exports.registerTestActor = Task.async(function* (client) {
+ // First, instanciate ActorRegistryFront to be able to dynamically register an actor
+ let deferred = defer();
+ client.listTabs(deferred.resolve);
+ let response = yield deferred.promise;
+ let { ActorRegistryFront } = require("devtools/shared/fronts/actor-registry");
+ let registryFront = ActorRegistryFront(client, response);
+
+ // Then ask to register our test-actor to retrieve its front
+ let options = {
+ type: { tab: true },
+ constructor: "TestActor",
+ prefix: "testActor"
+ };
+ let testActorFront = yield registryFront.registerActor(ACTOR_URL, options);
+ return testActorFront;
+ });
+
+ // Load the test actor in a custom sandbox as we can't use SDK module loader with URIs
+ let loadFront = Task.async(function* () {
+ let sourceText = yield request(ACTOR_URL);
+ const principal = CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")();
+ const sandbox = Cu.Sandbox(principal);
+ sandbox.exports = {};
+ sandbox.require = require;
+ Cu.evalInSandbox(sourceText, sandbox, "1.8", ACTOR_URL, 1);
+ return sandbox.exports;
+ });
+
+ // Ensure fetching a live TabActor form for the targeted app
+ // (helps fetching the test actor registered dynamically)
+ let getUpdatedForm = function (client, tab) {
+ return client.getTab({tab: tab})
+ .then(response => response.tab);
+ };
+
+ // Spawn an instance of the test actor for the given toolbox
+ exports.getTestActor = Task.async(function* (toolbox) {
+ let client = toolbox.target.client;
+ return getTestActor(client, toolbox.target.tab, toolbox);
+ });
+
+ // Sometimes, we need the test actor before opening or without a toolbox then just
+ // create a front for the given `tab`
+ exports.getTestActorWithoutToolbox = Task.async(function* (tab) {
+ let { DebuggerServer } = require("devtools/server/main");
+ let { DebuggerClient } = require("devtools/shared/client/main");
+
+ // We need to spawn a client instance,
+ // but for that we have to first ensure a server is running
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+ let client = new DebuggerClient(DebuggerServer.connectPipe());
+
+ yield client.connect();
+
+ // We also need to make sure the test actor is registered on the server.
+ yield exports.registerTestActor(client);
+
+ return getTestActor(client, tab);
+ });
+
+ // Fetch the content of a URI
+ let request = function (uri) {
+ return fetch(uri).then(({ content }) => content);
+ };
+
+ let getTestActor = Task.async(function* (client, tab, toolbox) {
+ // We may have to update the form in order to get the dynamically registered
+ // test actor.
+ let form = yield getUpdatedForm(client, tab);
+
+ let { TestActorFront } = yield loadFront();
+
+ return new TestActorFront(client, form, toolbox);
+ });
+})(this);
diff --git a/devtools/client/shared/test/test-actor.js b/devtools/client/shared/test/test-actor.js
new file mode 100644
index 000000000..3aab5287b
--- /dev/null
+++ b/devtools/client/shared/test/test-actor.js
@@ -0,0 +1,1138 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* exported TestActor, TestActorFront */
+
+"use strict";
+
+// A helper actor for inspector and markupview tests.
+
+const { Cc, Ci, Cu } = require("chrome");
+const {getRect, getElementFromPoint, getAdjustedQuads} = require("devtools/shared/layout/utils");
+const defer = require("devtools/shared/defer");
+const {Task} = require("devtools/shared/task");
+const {isContentStylesheet} = require("devtools/shared/inspector/css-logic");
+const DOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+const loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
+ .getService(Ci.mozIJSSubScriptLoader);
+
+// Set up a dummy environment so that EventUtils works. We need to be careful to
+// pass a window object into each EventUtils method we call rather than having
+// it rely on the |window| global.
+let EventUtils = {};
+EventUtils.window = {};
+EventUtils.parent = {};
+/* eslint-disable camelcase */
+EventUtils._EU_Ci = Components.interfaces;
+EventUtils._EU_Cc = Components.classes;
+/* eslint-disable camelcase */
+loader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
+
+const protocol = require("devtools/shared/protocol");
+const {Arg, RetVal} = protocol;
+
+const dumpn = msg => {
+ dump(msg + "\n");
+};
+
+/**
+ * Get the instance of CanvasFrameAnonymousContentHelper used by a given
+ * highlighter actor.
+ * The instance provides methods to get/set attributes/text/style on nodes of
+ * the highlighter, inserted into the nsCanvasFrame.
+ * @see /devtools/server/actors/highlighters.js
+ * @param {String} actorID
+ */
+function getHighlighterCanvasFrameHelper(conn, actorID) {
+ let actor = conn.getActor(actorID);
+ if (actor && actor._highlighter) {
+ return actor._highlighter.markup;
+ }
+ return null;
+}
+
+var testSpec = protocol.generateActorSpec({
+ typeName: "testActor",
+
+ methods: {
+ getNumberOfElementMatches: {
+ request: {
+ selector: Arg(0, "string"),
+ },
+ response: {
+ value: RetVal("number")
+ }
+ },
+ getHighlighterAttribute: {
+ request: {
+ nodeID: Arg(0, "string"),
+ name: Arg(1, "string"),
+ actorID: Arg(2, "string")
+ },
+ response: {
+ value: RetVal("string")
+ }
+ },
+ getHighlighterNodeTextContent: {
+ request: {
+ nodeID: Arg(0, "string"),
+ actorID: Arg(1, "string")
+ },
+ response: {
+ value: RetVal("string")
+ }
+ },
+ getSelectorHighlighterBoxNb: {
+ request: {
+ highlighter: Arg(0, "string"),
+ },
+ response: {
+ value: RetVal("number")
+ }
+ },
+ changeHighlightedNodeWaitForUpdate: {
+ request: {
+ name: Arg(0, "string"),
+ value: Arg(1, "string"),
+ actorID: Arg(2, "string")
+ },
+ response: {}
+ },
+ waitForHighlighterEvent: {
+ request: {
+ event: Arg(0, "string"),
+ actorID: Arg(1, "string")
+ },
+ response: {}
+ },
+ waitForEventOnNode: {
+ request: {
+ eventName: Arg(0, "string"),
+ selector: Arg(1, "nullable:string")
+ },
+ response: {}
+ },
+ changeZoomLevel: {
+ request: {
+ level: Arg(0, "string"),
+ actorID: Arg(1, "string"),
+ },
+ response: {}
+ },
+ assertElementAtPoint: {
+ request: {
+ x: Arg(0, "number"),
+ y: Arg(1, "number"),
+ selector: Arg(2, "string")
+ },
+ response: {
+ value: RetVal("boolean")
+ }
+ },
+ getAllAdjustedQuads: {
+ request: {
+ selector: Arg(0, "string")
+ },
+ response: {
+ value: RetVal("json")
+ }
+ },
+ synthesizeMouse: {
+ request: {
+ object: Arg(0, "json")
+ },
+ response: {}
+ },
+ synthesizeKey: {
+ request: {
+ args: Arg(0, "json")
+ },
+ response: {}
+ },
+ scrollIntoView: {
+ request: {
+ args: Arg(0, "string")
+ },
+ response: {}
+ },
+ hasPseudoClassLock: {
+ request: {
+ selector: Arg(0, "string"),
+ pseudo: Arg(1, "string")
+ },
+ response: {
+ value: RetVal("boolean")
+ }
+ },
+ loadAndWaitForCustomEvent: {
+ request: {
+ url: Arg(0, "string")
+ },
+ response: {}
+ },
+ hasNode: {
+ request: {
+ selector: Arg(0, "string")
+ },
+ response: {
+ value: RetVal("boolean")
+ }
+ },
+ getBoundingClientRect: {
+ request: {
+ selector: Arg(0, "string"),
+ },
+ response: {
+ value: RetVal("json")
+ }
+ },
+ setProperty: {
+ request: {
+ selector: Arg(0, "string"),
+ property: Arg(1, "string"),
+ value: Arg(2, "string")
+ },
+ response: {}
+ },
+ getProperty: {
+ request: {
+ selector: Arg(0, "string"),
+ property: Arg(1, "string")
+ },
+ response: {
+ value: RetVal("string")
+ }
+ },
+ getAttribute: {
+ request: {
+ selector: Arg(0, "string"),
+ property: Arg(1, "string")
+ },
+ response: {
+ value: RetVal("string")
+ }
+ },
+ setAttribute: {
+ request: {
+ selector: Arg(0, "string"),
+ property: Arg(1, "string"),
+ value: Arg(2, "string")
+ },
+ response: {}
+ },
+ removeAttribute: {
+ request: {
+ selector: Arg(0, "string"),
+ property: Arg(1, "string")
+ },
+ response: {}
+ },
+ reload: {
+ request: {},
+ response: {}
+ },
+ reloadFrame: {
+ request: {
+ selector: Arg(0, "string"),
+ },
+ response: {}
+ },
+ eval: {
+ request: {
+ js: Arg(0, "string")
+ },
+ response: {
+ value: RetVal("nullable:json")
+ }
+ },
+ scrollWindow: {
+ request: {
+ x: Arg(0, "number"),
+ y: Arg(1, "number"),
+ relative: Arg(2, "nullable:boolean"),
+ },
+ response: {
+ value: RetVal("json")
+ }
+ },
+ reflow: {},
+ getNodeRect: {
+ request: {
+ selector: Arg(0, "string")
+ },
+ response: {
+ value: RetVal("json")
+ }
+ },
+ getTextNodeRect: {
+ request: {
+ parentSelector: Arg(0, "string"),
+ childNodeIndex: Arg(1, "number")
+ },
+ response: {
+ value: RetVal("json")
+ }
+ },
+ getNodeInfo: {
+ request: {
+ selector: Arg(0, "string")
+ },
+ response: {
+ value: RetVal("json")
+ }
+ },
+ getStyleSheetsInfoForNode: {
+ request: {
+ selector: Arg(0, "string")
+ },
+ response: {
+ value: RetVal("json")
+ }
+ }
+ }
+});
+
+var TestActor = exports.TestActor = protocol.ActorClassWithSpec(testSpec, {
+ initialize: function (conn, tabActor, options) {
+ this.conn = conn;
+ this.tabActor = tabActor;
+ },
+
+ get content() {
+ return this.tabActor.window;
+ },
+
+ /**
+ * Helper to retrieve a DOM element.
+ * @param {string | array} selector Either a regular selector string
+ * or a selector array. If an array, each item, except the last one
+ * are considered matching an iframe, so that we can query element
+ * within deep iframes.
+ */
+ _querySelector: function (selector) {
+ let document = this.content.document;
+ if (Array.isArray(selector)) {
+ let fullSelector = selector.join(" >> ");
+ while (selector.length > 1) {
+ let str = selector.shift();
+ let iframe = document.querySelector(str);
+ if (!iframe) {
+ throw new Error("Unable to find element with selector \"" + str + "\"" +
+ " (full selector:" + fullSelector + ")");
+ }
+ if (!iframe.contentWindow) {
+ throw new Error("Iframe selector doesn't target an iframe \"" + str + "\"" +
+ " (full selector:" + fullSelector + ")");
+ }
+ document = iframe.contentWindow.document;
+ }
+ selector = selector.shift();
+ }
+ let node = document.querySelector(selector);
+ if (!node) {
+ throw new Error("Unable to find element with selector \"" + selector + "\"");
+ }
+ return node;
+ },
+ /**
+ * Helper to get the number of elements matching a selector
+ * @param {string} CSS selector.
+ */
+ getNumberOfElementMatches: function (selector, root = this.content.document) {
+ return root.querySelectorAll(selector).length;
+ },
+
+ /**
+ * Get a value for a given attribute name, on one of the elements of the box
+ * model highlighter, given its ID.
+ * @param {Object} msg The msg.data part expects the following properties
+ * - {String} nodeID The full ID of the element to get the attribute for
+ * - {String} name The name of the attribute to get
+ * - {String} actorID The highlighter actor ID
+ * @return {String} The value, if found, null otherwise
+ */
+ getHighlighterAttribute: function (nodeID, name, actorID) {
+ let helper = getHighlighterCanvasFrameHelper(this.conn, actorID);
+ if (helper) {
+ return helper.getAttributeForElement(nodeID, name);
+ }
+ return null;
+ },
+
+ /**
+ * Get the textcontent of one of the elements of the box model highlighter,
+ * given its ID.
+ * @param {String} nodeID The full ID of the element to get the attribute for
+ * @param {String} actorID The highlighter actor ID
+ * @return {String} The textcontent value
+ */
+ getHighlighterNodeTextContent: function (nodeID, actorID) {
+ let value;
+ let helper = getHighlighterCanvasFrameHelper(this.conn, actorID);
+ if (helper) {
+ value = helper.getTextContentForElement(nodeID);
+ }
+ return value;
+ },
+
+ /**
+ * Get the number of box-model highlighters created by the SelectorHighlighter
+ * @param {String} actorID The highlighter actor ID
+ * @return {Number} The number of box-model highlighters created, or null if the
+ * SelectorHighlighter was not found.
+ */
+ getSelectorHighlighterBoxNb: function (actorID) {
+ let highlighter = this.conn.getActor(actorID);
+ let {_highlighter: h} = highlighter;
+ if (!h || !h._highlighters) {
+ return null;
+ }
+ return h._highlighters.length;
+ },
+
+ /**
+ * Subscribe to the box-model highlighter's update event, modify an attribute of
+ * the currently highlighted node and send a message when the highlighter has
+ * updated.
+ * @param {String} the name of the attribute to be changed
+ * @param {String} the new value for the attribute
+ * @param {String} actorID The highlighter actor ID
+ */
+ changeHighlightedNodeWaitForUpdate: function (name, value, actorID) {
+ return new Promise(resolve => {
+ let highlighter = this.conn.getActor(actorID);
+ let {_highlighter: h} = highlighter;
+
+ h.once("updated", resolve);
+
+ h.currentNode.setAttribute(name, value);
+ });
+ },
+
+ /**
+ * Subscribe to a given highlighter event and respond when the event is received.
+ * @param {String} event The name of the highlighter event to listen to
+ * @param {String} actorID The highlighter actor ID
+ */
+ waitForHighlighterEvent: function (event, actorID) {
+ let highlighter = this.conn.getActor(actorID);
+ let {_highlighter: h} = highlighter;
+
+ return h.once(event);
+ },
+
+ /**
+ * Wait for a specific event on a node matching the provided selector.
+ * @param {String} eventName The name of the event to listen to
+ * @param {String} selector Optional: css selector of the node which should
+ * trigger the event. If ommitted, target will be the content window
+ */
+ waitForEventOnNode: function (eventName, selector) {
+ return new Promise(resolve => {
+ let node = selector ? this._querySelector(selector) : this.content;
+ node.addEventListener(eventName, function onEvent() {
+ node.removeEventListener(eventName, onEvent);
+ resolve();
+ });
+ });
+ },
+
+ /**
+ * Change the zoom level of the page.
+ * Optionally subscribe to the box-model highlighter's update event and waiting
+ * for it to refresh before responding.
+ * @param {Number} level The new zoom level
+ * @param {String} actorID Optional. The highlighter actor ID
+ */
+ changeZoomLevel: function (level, actorID) {
+ dumpn("Zooming page to " + level);
+ return new Promise(resolve => {
+ if (actorID) {
+ let actor = this.conn.getActor(actorID);
+ let {_highlighter: h} = actor;
+ h.once("updated", resolve);
+ } else {
+ resolve();
+ }
+
+ let docShell = this.content.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell);
+ docShell.contentViewer.fullZoom = level;
+ });
+ },
+
+ assertElementAtPoint: function (x, y, selector) {
+ let elementAtPoint = getElementFromPoint(this.content.document, x, y);
+ if (!elementAtPoint) {
+ throw new Error("Unable to find element at (" + x + ", " + y + ")");
+ }
+ let node = this._querySelector(selector);
+ return node == elementAtPoint;
+ },
+
+ /**
+ * Get all box-model regions' adjusted boxquads for the given element
+ * @param {String} selector The node selector to target a given element
+ * @return {Object} An object with each property being a box-model region, each
+ * of them being an object with the p1/p2/p3/p4 properties
+ */
+ getAllAdjustedQuads: function (selector) {
+ let regions = {};
+ let node = this._querySelector(selector);
+ for (let boxType of ["content", "padding", "border", "margin"]) {
+ regions[boxType] = getAdjustedQuads(this.content, node, boxType);
+ }
+
+ return regions;
+ },
+
+ /**
+ * Synthesize a mouse event on an element, after ensuring that it is visible
+ * in the viewport. This handler doesn't send a message back. Consumers
+ * should listen to specific events on the inspector/highlighter to know when
+ * the event got synthesized.
+ * @param {String} selector The node selector to get the node target for the event
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Boolean} center If set to true, x/y will be ignored and
+ * synthesizeMouseAtCenter will be used instead
+ * @param {Object} options Other event options
+ */
+ synthesizeMouse: function ({ selector, x, y, center, options }) {
+ let node = this._querySelector(selector);
+ node.scrollIntoView();
+ if (center) {
+ EventUtils.synthesizeMouseAtCenter(node, options, node.ownerDocument.defaultView);
+ } else {
+ EventUtils.synthesizeMouse(node, x, y, options, node.ownerDocument.defaultView);
+ }
+ },
+
+ /**
+ * Synthesize a key event for an element. This handler doesn't send a message
+ * back. Consumers should listen to specific events on the inspector/highlighter
+ * to know when the event got synthesized.
+ */
+ synthesizeKey: function ({key, options, content}) {
+ EventUtils.synthesizeKey(key, options, this.content);
+ },
+
+ /**
+ * Scroll an element into view.
+ * @param {String} selector The selector for the node to scroll into view.
+ */
+ scrollIntoView: function (selector) {
+ let node = this._querySelector(selector);
+ node.scrollIntoView();
+ },
+
+ /**
+ * Check that an element currently has a pseudo-class lock.
+ * @param {String} selector The node selector to get the pseudo-class from
+ * @param {String} pseudo The pseudoclass to check for
+ * @return {Boolean}
+ */
+ hasPseudoClassLock: function (selector, pseudo) {
+ let node = this._querySelector(selector);
+ return DOMUtils.hasPseudoClassLock(node, pseudo);
+ },
+
+ loadAndWaitForCustomEvent: function (url) {
+ return new Promise(resolve => {
+ // Wait for DOMWindowCreated first, as listening on the current outerwindow
+ // doesn't allow receiving test-page-processing-done.
+ this.tabActor.chromeEventHandler.addEventListener("DOMWindowCreated", () => {
+ this.content.addEventListener(
+ "test-page-processing-done", resolve, { once: true }
+ );
+ }, { once: true });
+
+ this.content.location = url;
+ });
+ },
+
+ hasNode: function (selector) {
+ try {
+ // _querySelector throws if the node doesn't exists
+ this._querySelector(selector);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ },
+
+ /**
+ * Get the bounding rect for a given DOM node once.
+ * @param {String} selector selector identifier to select the DOM node
+ * @return {json} the bounding rect info
+ */
+ getBoundingClientRect: function (selector) {
+ let node = this._querySelector(selector);
+ let rect = node.getBoundingClientRect();
+ // DOMRect can't be stringified directly, so return a simple object instead.
+ return {
+ x: rect.x,
+ y: rect.y,
+ width: rect.width,
+ height: rect.height,
+ top: rect.top,
+ right: rect.right,
+ bottom: rect.bottom,
+ left: rect.left
+ };
+ },
+
+ /**
+ * Set a JS property on a DOM Node.
+ * @param {String} selector The node selector
+ * @param {String} property The property name
+ * @param {String} value The attribute value
+ */
+ setProperty: function (selector, property, value) {
+ let node = this._querySelector(selector);
+ node[property] = value;
+ },
+
+ /**
+ * Get a JS property on a DOM Node.
+ * @param {String} selector The node selector
+ * @param {String} property The property name
+ * @return {String} value The attribute value
+ */
+ getProperty: function (selector, property) {
+ let node = this._querySelector(selector);
+ return node[property];
+ },
+
+ /**
+ * Get an attribute on a DOM Node.
+ * @param {String} selector The node selector
+ * @param {String} attribute The attribute name
+ * @return {String} value The attribute value
+ */
+ getAttribute: function (selector, attribute) {
+ let node = this._querySelector(selector);
+ return node.getAttribute(attribute);
+ },
+
+ /**
+ * Set an attribute on a DOM Node.
+ * @param {String} selector The node selector
+ * @param {String} attribute The attribute name
+ * @param {String} value The attribute value
+ */
+ setAttribute: function (selector, attribute, value) {
+ let node = this._querySelector(selector);
+ node.setAttribute(attribute, value);
+ },
+
+ /**
+ * Remove an attribute from a DOM Node.
+ * @param {String} selector The node selector
+ * @param {String} attribute The attribute name
+ */
+ removeAttribute: function (selector, attribute) {
+ let node = this._querySelector(selector);
+ node.removeAttribute(attribute);
+ },
+
+ /**
+ * Reload the content window.
+ */
+ reload: function () {
+ this.content.location.reload();
+ },
+
+ /**
+ * Reload an iframe and wait for its load event.
+ * @param {String} selector The node selector
+ */
+ reloadFrame: function (selector) {
+ let node = this._querySelector(selector);
+
+ let deferred = defer();
+
+ let onLoad = function () {
+ node.removeEventListener("load", onLoad);
+ deferred.resolve();
+ };
+ node.addEventListener("load", onLoad);
+
+ node.contentWindow.location.reload();
+ return deferred.promise;
+ },
+
+ /**
+ * Evaluate a JS string in the context of the content document.
+ * @param {String} js JS string to evaluate
+ * @return {json} The evaluation result
+ */
+ eval: function (js) {
+ // We have to use a sandbox, as CSP prevent us from using eval on apps...
+ let sb = Cu.Sandbox(this.content, { sandboxPrototype: this.content });
+ return Cu.evalInSandbox(js, sb);
+ },
+
+ /**
+ * Scrolls the window to a particular set of coordinates in the document, or
+ * by the given amount if `relative` is set to `true`.
+ *
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Boolean} relative
+ *
+ * @return {Object} An object with x / y properties, representing the number
+ * of pixels that the document has been scrolled horizontally and vertically.
+ */
+ scrollWindow: function (x, y, relative) {
+ if (isNaN(x) || isNaN(y)) {
+ return {};
+ }
+
+ let deferred = defer();
+ this.content.addEventListener("scroll", function onScroll(event) {
+ this.removeEventListener("scroll", onScroll);
+
+ let data = {x: this.content.scrollX, y: this.content.scrollY};
+ deferred.resolve(data);
+ });
+
+ this.content[relative ? "scrollBy" : "scrollTo"](x, y);
+
+ return deferred.promise;
+ },
+
+ /**
+ * Forces the reflow and waits for the next repaint.
+ */
+ reflow: function () {
+ let deferred = defer();
+ this.content.document.documentElement.offsetWidth;
+ this.content.requestAnimationFrame(deferred.resolve);
+
+ return deferred.promise;
+ },
+
+ getNodeRect: Task.async(function* (selector) {
+ let node = this._querySelector(selector);
+ return getRect(this.content, node, this.content);
+ }),
+
+ getTextNodeRect: Task.async(function* (parentSelector, childNodeIndex) {
+ let parentNode = this._querySelector(parentSelector);
+ let node = parentNode.childNodes[childNodeIndex];
+ return getAdjustedQuads(this.content, node)[0].bounds;
+ }),
+
+ /**
+ * Get information about a DOM element, identified by a selector.
+ * @param {String} selector The CSS selector to get the node (can be an array
+ * of selectors to get elements in an iframe).
+ * @return {Object} data Null if selector didn't match any node, otherwise:
+ * - {String} tagName.
+ * - {String} namespaceURI.
+ * - {Number} numChildren The number of children in the element.
+ * - {Array} attributes An array of {name, value, namespaceURI} objects.
+ * - {String} outerHTML.
+ * - {String} innerHTML.
+ * - {String} textContent.
+ */
+ getNodeInfo: function (selector) {
+ let node = this._querySelector(selector);
+ let info = null;
+
+ if (node) {
+ info = {
+ tagName: node.tagName,
+ namespaceURI: node.namespaceURI,
+ numChildren: node.children.length,
+ numNodes: node.childNodes.length,
+ attributes: [...node.attributes].map(({name, value, namespaceURI}) => {
+ return {name, value, namespaceURI};
+ }),
+ outerHTML: node.outerHTML,
+ innerHTML: node.innerHTML,
+ textContent: node.textContent
+ };
+ }
+
+ return info;
+ },
+
+ /**
+ * Get information about the stylesheets which have CSS rules that apply to a given DOM
+ * element, identified by a selector.
+ * @param {String} selector The CSS selector to get the node (can be an array
+ * of selectors to get elements in an iframe).
+ * @return {Array} A list of stylesheet objects, each having the following properties:
+ * - {String} href.
+ * - {Boolean} isContentSheet.
+ */
+ getStyleSheetsInfoForNode: function (selector) {
+ let node = this._querySelector(selector);
+ let domRules = DOMUtils.getCSSStyleRules(node);
+
+ let sheets = [];
+
+ for (let i = 0, n = domRules.Count(); i < n; i++) {
+ let sheet = domRules.GetElementAt(i).parentStyleSheet;
+ sheets.push({
+ href: sheet.href,
+ isContentSheet: isContentStylesheet(sheet)
+ });
+ }
+
+ return sheets;
+ }
+});
+
+var TestActorFront = exports.TestActorFront = protocol.FrontClassWithSpec(testSpec, {
+ initialize: function (client, { testActor }, toolbox) {
+ protocol.Front.prototype.initialize.call(this, client, { actor: testActor });
+ this.manage(this);
+ this.toolbox = toolbox;
+ },
+
+ /**
+ * Zoom the current page to a given level.
+ * @param {Number} level The new zoom level.
+ * @return {Promise} The returned promise will only resolve when the
+ * highlighter has updated to the new zoom level.
+ */
+ zoomPageTo: function (level) {
+ return this.changeZoomLevel(level, this.toolbox.highlighter.actorID);
+ },
+
+ /* eslint-disable max-len */
+ changeHighlightedNodeWaitForUpdate: protocol.custom(function (name, value, highlighter) {
+ /* eslint-enable max-len */
+ return this._changeHighlightedNodeWaitForUpdate(
+ name, value, (highlighter || this.toolbox.highlighter).actorID
+ );
+ }, {
+ impl: "_changeHighlightedNodeWaitForUpdate"
+ }),
+
+ /**
+ * Get the value of an attribute on one of the highlighter's node.
+ * @param {String} nodeID The Id of the node in the highlighter.
+ * @param {String} name The name of the attribute.
+ * @param {Object} highlighter Optional custom highlither to target
+ * @return {String} value
+ */
+ getHighlighterNodeAttribute: function (nodeID, name, highlighter) {
+ return this.getHighlighterAttribute(
+ nodeID, name, (highlighter || this.toolbox.highlighter).actorID
+ );
+ },
+
+ getHighlighterNodeTextContent: protocol.custom(function (nodeID, highlighter) {
+ return this._getHighlighterNodeTextContent(
+ nodeID, (highlighter || this.toolbox.highlighter).actorID
+ );
+ }, {
+ impl: "_getHighlighterNodeTextContent"
+ }),
+
+ /**
+ * Is the highlighter currently visible on the page?
+ */
+ isHighlighting: function () {
+ return this.getHighlighterNodeAttribute("box-model-elements", "hidden")
+ .then(value => value === null);
+ },
+
+ /**
+ * Assert that the box-model highlighter's current position corresponds to the
+ * given node boxquads.
+ * @param {String} selector The node selector to get the boxQuads from
+ * @param {Function} is assertion function to call for equality checks
+ * @param {String} prefix An optional prefix for logging information to the
+ * console.
+ */
+ isNodeCorrectlyHighlighted: Task.async(function* (selector, is, prefix = "") {
+ prefix += (prefix ? " " : "") + selector + " ";
+
+ let boxModel = yield this._getBoxModelStatus();
+ let regions = yield this.getAllAdjustedQuads(selector);
+
+ for (let boxType of ["content", "padding", "border", "margin"]) {
+ let [quad] = regions[boxType];
+ for (let point in boxModel[boxType].points) {
+ is(boxModel[boxType].points[point].x, quad[point].x,
+ prefix + boxType + " point " + point + " x coordinate is correct");
+ is(boxModel[boxType].points[point].y, quad[point].y,
+ prefix + boxType + " point " + point + " y coordinate is correct");
+ }
+ }
+ }),
+
+ /**
+ * Get the current rect of the border region of the box-model highlighter
+ */
+ getSimpleBorderRect: Task.async(function* (toolbox) {
+ let {border} = yield this._getBoxModelStatus(toolbox);
+ let {p1, p2, p4} = border.points;
+
+ return {
+ top: p1.y,
+ left: p1.x,
+ width: p2.x - p1.x,
+ height: p4.y - p1.y
+ };
+ }),
+
+ /**
+ * Get the current positions and visibility of the various box-model highlighter
+ * elements.
+ */
+ _getBoxModelStatus: Task.async(function* () {
+ let isVisible = yield this.isHighlighting();
+
+ let ret = {
+ visible: isVisible
+ };
+
+ for (let region of ["margin", "border", "padding", "content"]) {
+ let points = yield this._getPointsForRegion(region);
+ let visible = yield this._isRegionHidden(region);
+ ret[region] = {points, visible};
+ }
+
+ ret.guides = {};
+ for (let guide of ["top", "right", "bottom", "left"]) {
+ ret.guides[guide] = yield this._getGuideStatus(guide);
+ }
+
+ return ret;
+ }),
+
+ /**
+ * Check that the box-model highlighter is currently highlighting the node matching the
+ * given selector.
+ * @param {String} selector
+ * @return {Boolean}
+ */
+ assertHighlightedNode: Task.async(function* (selector) {
+ let rect = yield this.getNodeRect(selector);
+ return yield this.isNodeRectHighlighted(rect);
+ }),
+
+ /**
+ * Check that the box-model highlighter is currently highlighting the text node that can
+ * be found at a given index within the list of childNodes of a parent element matching
+ * the given selector.
+ * @param {String} parentSelector
+ * @param {Number} childNodeIndex
+ * @return {Boolean}
+ */
+ assertHighlightedTextNode: Task.async(function* (parentSelector, childNodeIndex) {
+ let rect = yield this.getTextNodeRect(parentSelector, childNodeIndex);
+ return yield this.isNodeRectHighlighted(rect);
+ }),
+
+ /**
+ * Check that the box-model highlighter is currently highlighting the given rect.
+ * @param {Object} rect
+ * @return {Boolean}
+ */
+ isNodeRectHighlighted: Task.async(function* ({ left, top, width, height }) {
+ let {visible, border} = yield this._getBoxModelStatus();
+ let points = border.points;
+ if (!visible) {
+ return false;
+ }
+
+ // Check that the node is within the box model
+ let right = left + width;
+ let bottom = top + height;
+
+ // Converts points dictionnary into an array
+ let list = [];
+ for (let i = 1; i <= 4; i++) {
+ let p = points["p" + i];
+ list.push([p.x, p.y]);
+ }
+ points = list;
+
+ // Check that each point of the node is within the box model
+ return isInside([left, top], points) &&
+ isInside([right, top], points) &&
+ isInside([right, bottom], points) &&
+ isInside([left, bottom], points);
+ }),
+
+ /**
+ * Get the coordinate (points attribute) from one of the polygon elements in the
+ * box model highlighter.
+ */
+ _getPointsForRegion: Task.async(function* (region) {
+ let d = yield this.getHighlighterNodeAttribute("box-model-" + region, "d");
+
+ let polygons = d.match(/M[^M]+/g);
+ if (!polygons) {
+ return null;
+ }
+
+ let points = polygons[0].trim().split(" ").map(i => {
+ return i.replace(/M|L/, "").split(",");
+ });
+
+ return {
+ p1: {
+ x: parseFloat(points[0][0]),
+ y: parseFloat(points[0][1])
+ },
+ p2: {
+ x: parseFloat(points[1][0]),
+ y: parseFloat(points[1][1])
+ },
+ p3: {
+ x: parseFloat(points[2][0]),
+ y: parseFloat(points[2][1])
+ },
+ p4: {
+ x: parseFloat(points[3][0]),
+ y: parseFloat(points[3][1])
+ }
+ };
+ }),
+
+ /**
+ * Is a given region polygon element of the box-model highlighter currently
+ * hidden?
+ */
+ _isRegionHidden: Task.async(function* (region) {
+ let value = yield this.getHighlighterNodeAttribute("box-model-" + region, "hidden");
+ return value !== null;
+ }),
+
+ _getGuideStatus: Task.async(function* (location) {
+ let id = "box-model-guide-" + location;
+
+ let hidden = yield this.getHighlighterNodeAttribute(id, "hidden");
+ let x1 = yield this.getHighlighterNodeAttribute(id, "x1");
+ let y1 = yield this.getHighlighterNodeAttribute(id, "y1");
+ let x2 = yield this.getHighlighterNodeAttribute(id, "x2");
+ let y2 = yield this.getHighlighterNodeAttribute(id, "y2");
+
+ return {
+ visible: !hidden,
+ x1: x1,
+ y1: y1,
+ x2: x2,
+ y2: y2
+ };
+ }),
+
+ /**
+ * Get the coordinates of the rectangle that is defined by the 4 guides displayed
+ * in the toolbox box-model highlighter.
+ * @return {Object} Null if at least one guide is hidden. Otherwise an object
+ * with p1, p2, p3, p4 properties being {x, y} objects.
+ */
+ getGuidesRectangle: Task.async(function* () {
+ let tGuide = yield this._getGuideStatus("top");
+ let rGuide = yield this._getGuideStatus("right");
+ let bGuide = yield this._getGuideStatus("bottom");
+ let lGuide = yield this._getGuideStatus("left");
+
+ if (!tGuide.visible || !rGuide.visible || !bGuide.visible || !lGuide.visible) {
+ return null;
+ }
+
+ return {
+ p1: {x: lGuide.x1, y: tGuide.y1},
+ p2: {x: rGuide.x1, y: tGuide. y1},
+ p3: {x: rGuide.x1, y: bGuide.y1},
+ p4: {x: lGuide.x1, y: bGuide.y1}
+ };
+ }),
+
+ waitForHighlighterEvent: protocol.custom(function (event) {
+ return this._waitForHighlighterEvent(event, this.toolbox.highlighter.actorID);
+ }, {
+ impl: "_waitForHighlighterEvent"
+ }),
+
+ /**
+ * Get the "d" attribute value for one of the box-model highlighter's region
+ * <path> elements, and parse it to a list of points.
+ * @param {String} region The box model region name.
+ * @param {Front} highlighter The front of the highlighter.
+ * @return {Object} The object returned has the following form:
+ * - d {String} the d attribute value
+ * - points {Array} an array of all the polygons defined by the path. Each box
+ * is itself an Array of points, themselves being [x,y] coordinates arrays.
+ */
+ getHighlighterRegionPath: Task.async(function* (region, highlighter) {
+ let d = yield this.getHighlighterNodeAttribute(
+ `box-model-${region}`, "d", highlighter
+ );
+ if (!d) {
+ return {d: null};
+ }
+
+ let polygons = d.match(/M[^M]+/g);
+ if (!polygons) {
+ return {d};
+ }
+
+ let points = [];
+ for (let polygon of polygons) {
+ points.push(polygon.trim().split(" ").map(i => {
+ return i.replace(/M|L/, "").split(",");
+ }));
+ }
+
+ return {d, points};
+ })
+});
+
+/**
+ * Check whether a point is included in a polygon.
+ * Taken and tweaked from:
+ * https://github.com/iominh/point-in-polygon-extended/blob/master/src/index.js#L30-L85
+ * @param {Array} point [x,y] coordinates
+ * @param {Array} polygon An array of [x,y] points
+ * @return {Boolean}
+ */
+function isInside(point, polygon) {
+ if (polygon.length === 0) {
+ return false;
+ }
+
+ const n = polygon.length;
+ const newPoints = polygon.slice(0);
+ newPoints.push(polygon[0]);
+ let wn = 0;
+
+ // loop through all edges of the polygon
+ for (let i = 0; i < n; i++) {
+ // Accept points on the edges
+ let r = isLeft(newPoints[i], newPoints[i + 1], point);
+ if (r === 0) {
+ return true;
+ }
+ if (newPoints[i][1] <= point[1]) {
+ if (newPoints[i + 1][1] > point[1] && r > 0) {
+ wn++;
+ }
+ } else if (newPoints[i + 1][1] <= point[1] && r < 0) {
+ wn--;
+ }
+ }
+ if (wn === 0) {
+ dumpn(JSON.stringify(point) + " is outside of " + JSON.stringify(polygon));
+ }
+ // the point is outside only when this winding number wn===0, otherwise it's inside
+ return wn !== 0;
+}
+
+function isLeft(p0, p1, p2) {
+ let l = ((p1[0] - p0[0]) * (p2[1] - p0[1])) -
+ ((p2[0] - p0[0]) * (p1[1] - p0[1]));
+ return l;
+}
diff --git a/devtools/client/shared/test/unit/.eslintrc.js b/devtools/client/shared/test/unit/.eslintrc.js
new file mode 100644
index 000000000..59adf410a
--- /dev/null
+++ b/devtools/client/shared/test/unit/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the common devtools xpcshell eslintrc config.
+ "extends": "../../../../.eslintrc.xpcshell.js"
+};
diff --git a/devtools/client/shared/test/unit/test_VariablesView_filtering-without-controller.js b/devtools/client/shared/test/unit/test_VariablesView_filtering-without-controller.js
new file mode 100644
index 000000000..5f4438234
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_VariablesView_filtering-without-controller.js
@@ -0,0 +1,36 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that VariablesView._doSearch() works even without an attached
+// VariablesViewController (bug 1196341).
+
+var Cu = Components.utils;
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+const DOMParser = Cc["@mozilla.org/xmlextras/domparser;1"]
+ .createInstance(Ci.nsIDOMParser);
+const { VariablesView } =
+ Cu.import("resource://devtools/client/shared/widgets/VariablesView.jsm", {});
+
+function run_test() {
+ let doc = DOMParser.parseFromString("<div>", "text/html");
+ let container = doc.body.firstChild;
+ ok(container, "Got a container.");
+
+ let vv = new VariablesView(container, { searchEnabled: true });
+ let scope = vv.addScope("Test scope");
+ let item1 = scope.addItem("a", { value: "1" });
+ let item2 = scope.addItem("b", { value: "2" });
+
+ do_print("Performing a search without a controller.");
+ vv._doSearch("a");
+
+ equal(item1.target.hasAttribute("unmatched"), false,
+ "First item that matched the filter is visible.");
+ equal(item2.target.hasAttribute("unmatched"), true,
+ "The second item that did not match the filter is hidden.");
+}
diff --git a/devtools/client/shared/test/unit/test_VariablesView_getString_promise.js b/devtools/client/shared/test/unit/test_VariablesView_getString_promise.js
new file mode 100644
index 000000000..a70c870bb
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_VariablesView_getString_promise.js
@@ -0,0 +1,76 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { VariablesView } = Components.utils.import("resource://devtools/client/shared/widgets/VariablesView.jsm", {});
+
+const PENDING = {
+ "type": "object",
+ "class": "Promise",
+ "actor": "conn0.pausedobj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "promiseState": {
+ "state": "pending"
+ },
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+};
+
+const FULFILLED = {
+ "type": "object",
+ "class": "Promise",
+ "actor": "conn0.pausedobj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "promiseState": {
+ "state": "fulfilled",
+ "value": 10
+ },
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+};
+
+const REJECTED = {
+ "type": "object",
+ "class": "Promise",
+ "actor": "conn0.pausedobj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "promiseState": {
+ "state": "rejected",
+ "reason": 10
+ },
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+};
+
+function run_test() {
+ equal(VariablesView.getString(PENDING, { concise: true }), "Promise");
+ equal(VariablesView.getString(PENDING), 'Promise {<state>: "pending"}');
+
+ equal(VariablesView.getString(FULFILLED, { concise: true }), "Promise");
+ equal(VariablesView.getString(FULFILLED),
+ 'Promise {<state>: "fulfilled", <value>: 10}');
+
+ equal(VariablesView.getString(REJECTED, { concise: true }), "Promise");
+ equal(VariablesView.getString(REJECTED), 'Promise {<state>: "rejected", <reason>: 10}');
+}
diff --git a/devtools/client/shared/test/unit/test_advanceValidate.js b/devtools/client/shared/test/unit/test_advanceValidate.js
new file mode 100644
index 000000000..2b3122a6f
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_advanceValidate.js
@@ -0,0 +1,31 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the advanceValidate function from rule-view.js.
+
+const {utils: Cu, interfaces: Ci} = Components;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {advanceValidate} = require("devtools/client/inspector/shared/utils");
+
+// 1 2 3
+// 0123456789012345678901234567890
+const sampleInput = '\\symbol "string" url(somewhere)';
+
+function testInsertion(where, result, testName) {
+ do_print(testName);
+ equal(advanceValidate(Ci.nsIDOMKeyEvent.DOM_VK_SEMICOLON, sampleInput, where),
+ result, "testing advanceValidate at " + where);
+}
+
+function run_test() {
+ testInsertion(4, true, "inside a symbol");
+ testInsertion(1, false, "after a backslash");
+ testInsertion(8, true, "after whitespace");
+ testInsertion(11, false, "inside a string");
+ testInsertion(24, false, "inside a URL");
+ testInsertion(31, true, "at the end");
+}
diff --git a/devtools/client/shared/test/unit/test_attribute-parsing-01.js b/devtools/client/shared/test/unit/test_attribute-parsing-01.js
new file mode 100644
index 000000000..b6b1a301d
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_attribute-parsing-01.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test splitBy from node-attribute-parser.js
+
+const {require} = Components.utils.import("resource://devtools/shared/Loader.jsm", {});
+const {splitBy} = require("devtools/client/shared/node-attribute-parser");
+
+const TEST_DATA = [{
+ value: "this is a test",
+ splitChar: " ",
+ expected: [
+ {value: "this"},
+ {value: " ", type: "string"},
+ {value: "is"},
+ {value: " ", type: "string"},
+ {value: "a"},
+ {value: " ", type: "string"},
+ {value: "test"}
+ ]
+}, {
+ value: "/path/to/handler",
+ splitChar: " ",
+ expected: [
+ {value: "/path/to/handler"}
+ ]
+}, {
+ value: "test",
+ splitChar: " ",
+ expected: [
+ {value: "test"}
+ ]
+}, {
+ value: " test ",
+ splitChar: " ",
+ expected: [
+ {value: " ", type: "string"},
+ {value: "test"},
+ {value: " ", type: "string"}
+ ]
+}, {
+ value: "",
+ splitChar: " ",
+ expected: []
+}, {
+ value: " ",
+ splitChar: " ",
+ expected: [
+ {value: " ", type: "string"},
+ {value: " ", type: "string"},
+ {value: " ", type: "string"}
+ ]
+}];
+
+function run_test() {
+ for (let {value, splitChar, expected} of TEST_DATA) {
+ do_print("Splitting string: " + value);
+ let tokens = splitBy(value, splitChar);
+
+ do_print("Checking that the number of parsed tokens is correct");
+ do_check_eq(tokens.length, expected.length);
+
+ for (let i = 0; i < tokens.length; i++) {
+ do_print("Checking the data in token " + i);
+ do_check_eq(tokens[i].value, expected[i].value);
+ if (expected[i].type) {
+ do_check_eq(tokens[i].type, expected[i].type);
+ }
+ }
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_attribute-parsing-02.js b/devtools/client/shared/test/unit/test_attribute-parsing-02.js
new file mode 100644
index 000000000..2c24d8f05
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_attribute-parsing-02.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test parseAttribute from node-attribute-parser.js
+
+const {require} = Components.utils.import("resource://devtools/shared/Loader.jsm", {});
+const {parseAttribute} = require("devtools/client/shared/node-attribute-parser");
+
+const TEST_DATA = [{
+ tagName: "body",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "class",
+ attributeValue: "some css class names",
+ expected: [
+ {value: "some css class names", type: "string"}
+ ]
+}, {
+ tagName: "box",
+ namespaceURI: "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+ attributeName: "datasources",
+ attributeValue: "/url/1?test=1#test http://mozilla.org/wow",
+ expected: [
+ {value: "/url/1?test=1#test", type: "uri"},
+ {value: " ", type: "string"},
+ {value: "http://mozilla.org/wow", type: "uri"}
+ ]
+}, {
+ tagName: "form",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "action",
+ attributeValue: "/path/to/handler",
+ expected: [
+ {value: "/path/to/handler", type: "uri"}
+ ]
+}, {
+ tagName: "a",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "ping",
+ attributeValue: "http://analytics.com/track?id=54 http://analytics.com/track?id=55",
+ expected: [
+ {value: "http://analytics.com/track?id=54", type: "uri"},
+ {value: " ", type: "string"},
+ {value: "http://analytics.com/track?id=55", type: "uri"}
+ ]
+}, {
+ tagName: "link",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "href",
+ attributeValue: "styles.css",
+ otherAttributes: [{name: "rel", value: "stylesheet"}],
+ expected: [
+ {value: "styles.css", type: "cssresource"}
+ ]
+}, {
+ tagName: "link",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "href",
+ attributeValue: "styles.css",
+ expected: [
+ {value: "styles.css", type: "uri"}
+ ]
+}, {
+ tagName: "output",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "for",
+ attributeValue: "element-id something id",
+ expected: [
+ {value: "element-id", type: "idref"},
+ {value: " ", type: "string"},
+ {value: "something", type: "idref"},
+ {value: " ", type: "string"},
+ {value: "id", type: "idref"}
+ ]
+}, {
+ tagName: "img",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "contextmenu",
+ attributeValue: "id-of-menu",
+ expected: [
+ {value: "id-of-menu", type: "idref"}
+ ]
+}, {
+ tagName: "img",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "src",
+ attributeValue: "omg-thats-so-funny.gif",
+ expected: [
+ {value: "omg-thats-so-funny.gif", type: "uri"}
+ ]
+}, {
+ tagName: "key",
+ namespaceURI: "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+ attributeName: "command",
+ attributeValue: "some_command_id",
+ expected: [
+ {value: "some_command_id", type: "idref"}
+ ]
+}, {
+ tagName: "script",
+ namespaceURI: "whatever",
+ attributeName: "src",
+ attributeValue: "script.js",
+ expected: [
+ {value: "script.js", type: "jsresource"}
+ ]
+}];
+
+function run_test() {
+ for (let {tagName, namespaceURI, attributeName,
+ otherAttributes, attributeValue, expected} of TEST_DATA) {
+ do_print("Testing <" + tagName + " " + attributeName + "='" + attributeValue + "'>");
+
+ let attributes = [
+ ...otherAttributes || [],
+ { name: attributeName, value: attributeValue }
+ ];
+ let tokens = parseAttribute(namespaceURI, tagName, attributes, attributeName);
+ if (!expected) {
+ do_check_true(!tokens);
+ continue;
+ }
+
+ do_print("Checking that the number of parsed tokens is correct");
+ do_check_eq(tokens.length, expected.length);
+
+ for (let i = 0; i < tokens.length; i++) {
+ do_print("Checking the data in token " + i);
+ do_check_eq(tokens[i].value, expected[i].value);
+ do_check_eq(tokens[i].type, expected[i].type);
+ }
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_bezierCanvas.js b/devtools/client/shared/test/unit/test_bezierCanvas.js
new file mode 100644
index 000000000..1decceebb
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_bezierCanvas.js
@@ -0,0 +1,117 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the BezierCanvas API in the CubicBezierWidget module
+
+var Cu = Components.utils;
+var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var {CubicBezier, BezierCanvas} = require("devtools/client/shared/widgets/CubicBezierWidget");
+
+function run_test() {
+ offsetsGetterReturnsData();
+ convertsOffsetsToCoordinates();
+ plotsCanvas();
+}
+
+function offsetsGetterReturnsData() {
+ do_print("offsets getter returns an array of 2 offset objects");
+
+ let b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [.25, 0]);
+ let offsets = b.offsets;
+
+ do_check_eq(offsets.length, 2);
+
+ do_check_true("top" in offsets[0]);
+ do_check_true("left" in offsets[0]);
+ do_check_true("top" in offsets[1]);
+ do_check_true("left" in offsets[1]);
+
+ do_check_eq(offsets[0].top, "300px");
+ do_check_eq(offsets[0].left, "0px");
+ do_check_eq(offsets[1].top, "100px");
+ do_check_eq(offsets[1].left, "200px");
+
+ do_print("offsets getter returns data according to current padding");
+
+ b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [0, 0]);
+ offsets = b.offsets;
+
+ do_check_eq(offsets[0].top, "400px");
+ do_check_eq(offsets[0].left, "0px");
+ do_check_eq(offsets[1].top, "0px");
+ do_check_eq(offsets[1].left, "200px");
+}
+
+function convertsOffsetsToCoordinates() {
+ do_print("Converts offsets to coordinates");
+
+ let b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [.25, 0]);
+
+ let coordinates = b.offsetsToCoordinates({style: {
+ left: "0px",
+ top: "0px"
+ }});
+ do_check_eq(coordinates.length, 2);
+ do_check_eq(coordinates[0], 0);
+ do_check_eq(coordinates[1], 1.5);
+
+ coordinates = b.offsetsToCoordinates({style: {
+ left: "0px",
+ top: "300px"
+ }});
+ do_check_eq(coordinates[0], 0);
+ do_check_eq(coordinates[1], 0);
+
+ coordinates = b.offsetsToCoordinates({style: {
+ left: "200px",
+ top: "100px"
+ }});
+ do_check_eq(coordinates[0], 1);
+ do_check_eq(coordinates[1], 1);
+}
+
+function plotsCanvas() {
+ do_print("Plots the curve to the canvas");
+
+ let hasDrawnCurve = false;
+ let b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [.25, 0]);
+ b.ctx.bezierCurveTo = () => {
+ hasDrawnCurve = true;
+ };
+ b.plot();
+
+ do_check_true(hasDrawnCurve);
+}
+
+function getCubicBezier() {
+ return new CubicBezier([0, 0, 1, 1]);
+}
+
+function getCanvasMock(w = 200, h = 400) {
+ return {
+ getContext: function () {
+ return {
+ scale: () => {},
+ translate: () => {},
+ clearRect: () => {},
+ beginPath: () => {},
+ closePath: () => {},
+ moveTo: () => {},
+ lineTo: () => {},
+ stroke: () => {},
+ arc: () => {},
+ fill: () => {},
+ bezierCurveTo: () => {},
+ save: () => {},
+ restore: () => {},
+ setTransform: () => {}
+ };
+ },
+ width: w,
+ height: h
+ };
+}
diff --git a/devtools/client/shared/test/unit/test_cssAngle.js b/devtools/client/shared/test/unit/test_cssAngle.js
new file mode 100644
index 000000000..ecb93bc8f
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_cssAngle.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test classifyAngle.
+
+"use strict";
+
+var Cu = Components.utils;
+var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+
+const {angleUtils} = require("devtools/client/shared/css-angle");
+
+const CLASSIFY_TESTS = [
+ { input: "180deg", output: "deg" },
+ { input: "-180deg", output: "deg" },
+ { input: "180DEG", output: "deg" },
+ { input: "200rad", output: "rad" },
+ { input: "-200rad", output: "rad" },
+ { input: "200RAD", output: "rad" },
+ { input: "0.5grad", output: "grad" },
+ { input: "-0.5grad", output: "grad" },
+ { input: "0.5GRAD", output: "grad" },
+ { input: "0.33turn", output: "turn" },
+ { input: "0.33TURN", output: "turn" },
+ { input: "-0.33turn", output: "turn" }
+];
+
+function run_test() {
+ for (let test of CLASSIFY_TESTS) {
+ let result = angleUtils.classifyAngle(test.input);
+ equal(result, test.output, "test classifyAngle(" + test.input + ")");
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_cssColor-01.js b/devtools/client/shared/test/unit/test_cssColor-01.js
new file mode 100644
index 000000000..13b9b5fa0
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_cssColor-01.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test classifyColor.
+
+"use strict";
+
+var Cu = Components.utils;
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+
+var {require, loader} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {colorUtils} = require("devtools/shared/css/color");
+
+loader.lazyGetter(this, "DOMUtils", function () {
+ return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+});
+
+const CLASSIFY_TESTS = [
+ { input: "rgb(255,0,192)", output: "rgb" },
+ { input: "RGB(255,0,192)", output: "rgb" },
+ { input: "RGB(100%,0%,83%)", output: "rgb" },
+ { input: "rgba(255,0,192, 0.25)", output: "rgb" },
+ { input: "hsl(5, 5%, 5%)", output: "hsl" },
+ { input: "hsla(5, 5%, 5%, 0.25)", output: "hsl" },
+ { input: "hSlA(5, 5%, 5%, 0.25)", output: "hsl" },
+ { input: "#f0c", output: "hex" },
+ { input: "#f0c0", output: "hex" },
+ { input: "#fe01cb", output: "hex" },
+ { input: "#fe01cb80", output: "hex" },
+ { input: "#FE01CB", output: "hex" },
+ { input: "#FE01CB80", output: "hex" },
+ { input: "blue", output: "name" },
+ { input: "orange", output: "name" }
+];
+
+function compareWithDomutils(input, isColor) {
+ let ours = colorUtils.colorToRGBA(input);
+ let platform = DOMUtils.colorToRGBA(input);
+ deepEqual(ours, platform, "color " + input + " matches DOMUtils");
+ if (isColor) {
+ ok(ours !== null, "'" + input + "' is a color");
+ } else {
+ ok(ours === null, "'" + input + "' is not a color");
+ }
+}
+
+function run_test() {
+ for (let test of CLASSIFY_TESTS) {
+ let result = colorUtils.classifyColor(test.input);
+ equal(result, test.output, "test classifyColor(" + test.input + ")");
+
+ let obj = new colorUtils.CssColor("purple");
+ obj.setAuthoredUnitFromColor(test.input);
+ equal(obj.colorUnit, test.output,
+ "test setAuthoredUnitFromColor(" + test.input + ")");
+
+ // Check that our implementation matches DOMUtils.
+ compareWithDomutils(test.input, true);
+
+ // And check some obvious errors.
+ compareWithDomutils("mumble" + test.input, false);
+ compareWithDomutils(test.input + "trailingstuff", false);
+ }
+
+ // Regression test for bug 1303826.
+ let black = new colorUtils.CssColor("#000");
+ black.colorUnit = "name";
+ equal(black.toString(), "black", "test non-upper-case color cycling");
+
+ let upper = new colorUtils.CssColor("BLACK");
+ upper.colorUnit = "hex";
+ equal(upper.toString(), "#000", "test upper-case color cycling");
+ upper.colorUnit = "name";
+ equal(upper.toString(), "BLACK", "test upper-case color preservation");
+}
diff --git a/devtools/client/shared/test/unit/test_cssColor-02.js b/devtools/client/shared/test/unit/test_cssColor-02.js
new file mode 100644
index 000000000..c6a039028
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_cssColor-02.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Test color cycling regression - Bug 1303748.
+ *
+ * Values should cycle from a starting value, back to their original values. This can
+ * potentially be a little flaky due to the precision of different color representations.
+ */
+
+const {require} = Components.utils.import("resource://devtools/shared/Loader.jsm", {});
+const {colorUtils} = require("devtools/shared/css/color");
+const getFixtureColorData = require("resource://test/helper_color_data.js");
+
+function run_test() {
+ getFixtureColorData().forEach(({authored, name, hex, hsl, rgb, cycle}) => {
+ if (cycle) {
+ const nameCycled = runCycle(name, cycle);
+ const hexCycled = runCycle(hex, cycle);
+ const hslCycled = runCycle(hsl, cycle);
+ const rgbCycled = runCycle(rgb, cycle);
+ // Cut down on log output by only reporting a single pass/fail for the color.
+ ok(nameCycled && hexCycled && hslCycled && rgbCycled,
+ `${authored} was able to cycle back to the original value`);
+ }
+ });
+}
+
+/**
+ * Test a color cycle to see if a color cycles back to its original value in a fixed
+ * number of steps.
+ *
+ * @param {string} value - The color value, e.g. "#000".
+ * @param {integer) times - The number of times it takes to cycle back to the
+ * original color.
+ */
+function runCycle(value, times) {
+ let color = new colorUtils.CssColor(value);
+ for (let i = 0; i < times; i++) {
+ color.nextColorUnit();
+ color = new colorUtils.CssColor(color.toString());
+ }
+ return color.toString() === value;
+}
diff --git a/devtools/client/shared/test/unit/test_cssColor-03.js b/devtools/client/shared/test/unit/test_cssColor-03.js
new file mode 100644
index 000000000..c3ef5a5c2
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_cssColor-03.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test css-color-4 color function syntax and old-style syntax.
+
+"use strict";
+
+var Cu = Components.utils;
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+
+var {require, loader} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {colorUtils} = require("devtools/shared/css/color");
+
+loader.lazyGetter(this, "DOMUtils", function () {
+ return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+});
+
+const OLD_STYLE_TESTS = [
+ "rgb(255,0,192)",
+ "RGB(255,0,192)",
+ "RGB(100%,0%,83%)",
+ "rgba(255,0,192,0.25)",
+ "hsl(120, 100%, 40%)",
+ "hsla(120, 100%, 40%, 0.25)",
+ "hSlA(240, 100%, 50%, 0.25)",
+];
+
+const CSS_COLOR_4_TESTS = [
+ "rgb(255.0,0.0,192.0)",
+ "RGB(255 0 192)",
+ "RGB(100% 0% 83% / 0.5)",
+ "RGB(100%,0%,83%,0.5)",
+ "RGB(100%,0%,83%,50%)",
+ "rgba(255,0,192)",
+ "hsl(50deg,15%,25%)",
+ "hsl(240 25% 33%)",
+ "hsl(50deg 25% 33% / 0.25)",
+ "hsl(60 120% 60% / 0.25)",
+ "hSlA(5turn 40% 4%)",
+];
+
+function run_test() {
+ for (let test of OLD_STYLE_TESTS) {
+ let ours = colorUtils.colorToRGBA(test, true);
+ let platform = DOMUtils.colorToRGBA(test);
+ deepEqual(ours, platform, "color " + test + " matches DOMUtils");
+ ok(ours !== null, "'" + test + "' is a color");
+ }
+
+ for (let test of CSS_COLOR_4_TESTS) {
+ let oursOld = colorUtils.colorToRGBA(test, true);
+ let oursNew = colorUtils.colorToRGBA(test, false);
+ let platform = DOMUtils.colorToRGBA(test);
+ notEqual(oursOld, platform, "old style parser for color " + test +
+ " should not match DOMUtils");
+ ok(oursOld === null, "'" + test + "' is not a color with old parser");
+ deepEqual(oursNew, platform, `css-color-4 parser for color ${test} matches DOMUtils`);
+ ok(oursNew !== null, "'" + test + "' is a color with css-color-4 parser");
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_cssColorDatabase.js b/devtools/client/shared/test/unit/test_cssColorDatabase.js
new file mode 100644
index 000000000..eb6363ba4
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_cssColorDatabase.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that css-color-db matches platform.
+
+"use strict";
+
+var Cu = Components.utils;
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+
+var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+
+const DOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+
+const {colorUtils} = require("devtools/shared/css/color");
+const {cssColors} = require("devtools/shared/css/color-db");
+
+function isValid(colorName) {
+ ok(colorUtils.isValidCSSColor(colorName),
+ colorName + " is valid in database");
+ ok(DOMUtils.isValidCSSColor(colorName),
+ colorName + " is valid in DOMUtils");
+}
+
+function checkOne(colorName, checkName) {
+ let ours = colorUtils.colorToRGBA(colorName);
+ let fromDom = DOMUtils.colorToRGBA(colorName);
+ deepEqual(ours, fromDom, colorName + " agrees with DOMUtils");
+
+ isValid(colorName);
+
+ if (checkName) {
+ let {r, g, b} = ours;
+
+ // The color we got might not map back to the same name; but our
+ // implementation should agree with DOMUtils about which name is
+ // canonical.
+ let ourName = colorUtils.rgbToColorName(r, g, b);
+ let domName = DOMUtils.rgbToColorName(r, g, b);
+
+ equal(ourName, domName,
+ colorName + " canonical name agrees with DOMUtils");
+ }
+}
+
+function run_test() {
+ for (let name in cssColors) {
+ checkOne(name, true);
+ }
+ checkOne("transparent", false);
+
+ // Now check that platform didn't add a new name when we weren't
+ // looking.
+ let names = DOMUtils.getCSSValuesForProperty("background-color");
+ for (let name of names) {
+ if (name !== "hsl" && name !== "hsla" &&
+ name !== "rgb" && name !== "rgba" &&
+ name !== "inherit" && name !== "initial" && name !== "unset") {
+ checkOne(name, true);
+ }
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_cubicBezier.js b/devtools/client/shared/test/unit/test_cubicBezier.js
new file mode 100644
index 000000000..9ed6c4eb1
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_cubicBezier.js
@@ -0,0 +1,146 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the CubicBezier API in the CubicBezierWidget module
+
+var Cu = Components.utils;
+var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var {CubicBezier, _parseTimingFunction} = require("devtools/client/shared/widgets/CubicBezierWidget");
+
+function run_test() {
+ throwsWhenMissingCoordinates();
+ throwsWhenIncorrectCoordinates();
+ convertsStringCoordinates();
+ coordinatesToStringOutputsAString();
+ pointGettersReturnPointCoordinatesArrays();
+ toStringOutputsCubicBezierValue();
+ toStringOutputsCssPresetValues();
+ testParseTimingFunction();
+}
+
+function throwsWhenMissingCoordinates() {
+ do_check_throws(() => {
+ new CubicBezier();
+ }, "Throws an exception when coordinates are missing");
+}
+
+function throwsWhenIncorrectCoordinates() {
+ do_check_throws(() => {
+ new CubicBezier([]);
+ }, "Throws an exception when coordinates are incorrect (empty array)");
+
+ do_check_throws(() => {
+ new CubicBezier([0, 0]);
+ }, "Throws an exception when coordinates are incorrect (incomplete array)");
+
+ do_check_throws(() => {
+ new CubicBezier(["a", "b", "c", "d"]);
+ }, "Throws an exception when coordinates are incorrect (invalid type)");
+
+ do_check_throws(() => {
+ new CubicBezier([1.5, 0, 1.5, 0]);
+ }, "Throws an exception when coordinates are incorrect (time range invalid)");
+
+ do_check_throws(() => {
+ new CubicBezier([-0.5, 0, -0.5, 0]);
+ }, "Throws an exception when coordinates are incorrect (time range invalid)");
+}
+
+function convertsStringCoordinates() {
+ do_print("Converts string coordinates to numbers");
+ let c = new CubicBezier(["0", "1", ".5", "-2"]);
+
+ do_check_eq(c.coordinates[0], 0);
+ do_check_eq(c.coordinates[1], 1);
+ do_check_eq(c.coordinates[2], .5);
+ do_check_eq(c.coordinates[3], -2);
+}
+
+function coordinatesToStringOutputsAString() {
+ do_print("coordinates.toString() outputs a string representation");
+
+ let c = new CubicBezier(["0", "1", "0.5", "-2"]);
+ let string = c.coordinates.toString();
+ do_check_eq(string, "0,1,.5,-2");
+
+ c = new CubicBezier([1, 1, 1, 1]);
+ string = c.coordinates.toString();
+ do_check_eq(string, "1,1,1,1");
+}
+
+function pointGettersReturnPointCoordinatesArrays() {
+ do_print("Points getters return arrays of coordinates");
+
+ let c = new CubicBezier([0, .2, .5, 1]);
+ do_check_eq(c.P1[0], 0);
+ do_check_eq(c.P1[1], .2);
+ do_check_eq(c.P2[0], .5);
+ do_check_eq(c.P2[1], 1);
+}
+
+function toStringOutputsCubicBezierValue() {
+ do_print("toString() outputs the cubic-bezier() value");
+
+ let c = new CubicBezier([0, 1, 1, 0]);
+ do_check_eq(c.toString(), "cubic-bezier(0,1,1,0)");
+}
+
+function toStringOutputsCssPresetValues() {
+ do_print("toString() outputs the css predefined values");
+
+ let c = new CubicBezier([0, 0, 1, 1]);
+ do_check_eq(c.toString(), "linear");
+
+ c = new CubicBezier([0.25, 0.1, 0.25, 1]);
+ do_check_eq(c.toString(), "ease");
+
+ c = new CubicBezier([0.42, 0, 1, 1]);
+ do_check_eq(c.toString(), "ease-in");
+
+ c = new CubicBezier([0, 0, 0.58, 1]);
+ do_check_eq(c.toString(), "ease-out");
+
+ c = new CubicBezier([0.42, 0, 0.58, 1]);
+ do_check_eq(c.toString(), "ease-in-out");
+}
+
+function testParseTimingFunction() {
+ do_print("test parseTimingFunction");
+
+ for (let test of ["ease", "linear", "ease-in", "ease-out", "ease-in-out"]) {
+ ok(_parseTimingFunction(test), test);
+ }
+
+ ok(!_parseTimingFunction("something"), "non-function token");
+ ok(!_parseTimingFunction("something()"), "non-cubic-bezier function");
+ ok(!_parseTimingFunction("cubic-bezier(something)",
+ "cubic-bezier with non-numeric argument"));
+ ok(!_parseTimingFunction("cubic-bezier(1,2,3:7)",
+ "did not see comma"));
+ ok(!_parseTimingFunction("cubic-bezier(1,2,3,7:",
+ "did not see close paren"));
+ ok(!_parseTimingFunction("cubic-bezier(1,2", "early EOF after number"));
+ ok(!_parseTimingFunction("cubic-bezier(1,2,", "early EOF after comma"));
+ deepEqual(_parseTimingFunction("cubic-bezier(1,2,3,7)"), [1, 2, 3, 7],
+ "correct invocation");
+ deepEqual(_parseTimingFunction("cubic-bezier(1, /* */ 2,3, 7 )"),
+ [1, 2, 3, 7],
+ "correct with comments and whitespace");
+}
+
+function do_check_throws(cb, info) {
+ do_print(info);
+
+ let hasThrown = false;
+ try {
+ cb();
+ } catch (e) {
+ hasThrown = true;
+ }
+
+ do_check_true(hasThrown);
+}
diff --git a/devtools/client/shared/test/unit/test_escapeCSSComment.js b/devtools/client/shared/test/unit/test_escapeCSSComment.js
new file mode 100644
index 000000000..19d8a2902
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_escapeCSSComment.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {escapeCSSComment, _unescapeCSSComment} = require("devtools/shared/css/parsing-utils");
+
+const TEST_DATA = [
+ {
+ input: "simple",
+ expected: "simple"
+ },
+ {
+ input: "/* comment */",
+ expected: "/\\* comment *\\/"
+ },
+ {
+ input: "/* two *//* comments */",
+ expected: "/\\* two *\\//\\* comments *\\/"
+ },
+ {
+ input: "/* nested /\\* comment *\\/ */",
+ expected: "/\\* nested /\\\\* comment *\\\\/ *\\/",
+ }
+];
+
+function run_test() {
+ let i = 0;
+ for (let test of TEST_DATA) {
+ ++i;
+ do_print("Test #" + i);
+
+ let escaped = escapeCSSComment(test.input);
+ equal(escaped, test.expected);
+ let unescaped = _unescapeCSSComment(escaped);
+ equal(unescaped, test.input);
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_parseDeclarations.js b/devtools/client/shared/test/unit/test_parseDeclarations.js
new file mode 100644
index 000000000..d400a5359
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_parseDeclarations.js
@@ -0,0 +1,439 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {require} = Components.utils.import("resource://devtools/shared/Loader.jsm", {});
+const {parseDeclarations, _parseCommentDeclarations} = require("devtools/shared/css/parsing-utils");
+const {isCssPropertyKnown} = require("devtools/server/actors/css-properties");
+
+const TEST_DATA = [
+ // Simple test
+ {
+ input: "p:v;",
+ expected: [{name: "p", value: "v", priority: "", offsets: [0, 4]}]
+ },
+ // Simple test
+ {
+ input: "this:is;a:test;",
+ expected: [
+ {name: "this", value: "is", priority: "", offsets: [0, 8]},
+ {name: "a", value: "test", priority: "", offsets: [8, 15]}
+ ]
+ },
+ // Test a single declaration with semi-colon
+ {
+ input: "name:value;",
+ expected: [{name: "name", value: "value", priority: "", offsets: [0, 11]}]
+ },
+ // Test a single declaration without semi-colon
+ {
+ input: "name:value",
+ expected: [{name: "name", value: "value", priority: "", offsets: [0, 10]}]
+ },
+ // Test multiple declarations separated by whitespaces and carriage
+ // returns and tabs
+ {
+ input: "p1 : v1 ; \t\t \n p2:v2; \n\n\n\n\t p3 : v3;",
+ expected: [
+ {name: "p1", value: "v1", priority: "", offsets: [0, 9]},
+ {name: "p2", value: "v2", priority: "", offsets: [16, 22]},
+ {name: "p3", value: "v3", priority: "", offsets: [32, 45]},
+ ]
+ },
+ // Test simple priority
+ {
+ input: "p1: v1; p2: v2 !important;",
+ expected: [
+ {name: "p1", value: "v1", priority: "", offsets: [0, 7]},
+ {name: "p2", value: "v2", priority: "important", offsets: [8, 26]}
+ ]
+ },
+ // Test simple priority
+ {
+ input: "p1: v1 !important; p2: v2",
+ expected: [
+ {name: "p1", value: "v1", priority: "important", offsets: [0, 18]},
+ {name: "p2", value: "v2", priority: "", offsets: [19, 25]}
+ ]
+ },
+ // Test simple priority
+ {
+ input: "p1: v1 ! important; p2: v2 ! important;",
+ expected: [
+ {name: "p1", value: "v1", priority: "important", offsets: [0, 20]},
+ {name: "p2", value: "v2", priority: "important", offsets: [21, 40]}
+ ]
+ },
+ // Test invalid priority
+ {
+ input: "p1: v1 important;",
+ expected: [
+ {name: "p1", value: "v1 important", priority: "", offsets: [0, 17]}
+ ]
+ },
+ // Test various types of background-image urls
+ {
+ input: "background-image: url(../../relative/image.png)",
+ expected: [{
+ name: "background-image",
+ value: "url(../../relative/image.png)",
+ priority: "",
+ offsets: [0, 47]
+ }]
+ },
+ {
+ input: "background-image: url(http://site.com/test.png)",
+ expected: [{
+ name: "background-image",
+ value: "url(http://site.com/test.png)",
+ priority: "",
+ offsets: [0, 47]
+ }]
+ },
+ {
+ input: "background-image: url(wow.gif)",
+ expected: [{
+ name: "background-image",
+ value: "url(wow.gif)",
+ priority: "",
+ offsets: [0, 30]
+ }]
+ },
+ // Test that urls with :;{} characters in them are parsed correctly
+ {
+ input: "background: red url(\"http://site.com/image{}:;.png?id=4#wat\") "
+ + "repeat top right",
+ expected: [{
+ name: "background",
+ value: "red url(\"http://site.com/image{}:;.png?id=4#wat\") " +
+ "repeat top right",
+ priority: "",
+ offsets: [0, 78]
+ }]
+ },
+ // Test that an empty string results in an empty array
+ {input: "", expected: []},
+ // Test that a string comprised only of whitespaces results in an empty array
+ {input: " \n \n \n \n \t \t\t\t ", expected: []},
+ // Test that a null input throws an exception
+ {input: null, throws: true},
+ // Test that a undefined input throws an exception
+ {input: undefined, throws: true},
+ // Test that :;{} characters in quoted content are not parsed as multiple
+ // declarations
+ {
+ input: "content: \";color:red;}selector{color:yellow;\"",
+ expected: [{
+ name: "content",
+ value: "\";color:red;}selector{color:yellow;\"",
+ priority: "",
+ offsets: [0, 45]
+ }]
+ },
+ // Test that rules aren't parsed, just declarations. So { and } found after a
+ // property name should be part of the property name, same for values.
+ {
+ input: "body {color:red;} p {color: blue;}",
+ expected: [
+ {name: "body {color", value: "red", priority: "", offsets: [0, 16]},
+ {name: "} p {color", value: "blue", priority: "", offsets: [16, 33]},
+ {name: "}", value: "", priority: "", offsets: [33, 34]}
+ ]
+ },
+ // Test unbalanced : and ;
+ {
+ input: "color :red : font : arial;",
+ expected: [
+ {name: "color", value: "red : font : arial", priority: "",
+ offsets: [0, 26]}
+ ]
+ },
+ {
+ input: "background: red;;;;;",
+ expected: [{name: "background", value: "red", priority: "",
+ offsets: [0, 16]}]
+ },
+ {
+ input: "background:;",
+ expected: [{name: "background", value: "", priority: "",
+ offsets: [0, 12]}]
+ },
+ {input: ";;;;;", expected: []},
+ {input: ":;:;", expected: []},
+ // Test name only
+ {input: "color", expected: [
+ {name: "color", value: "", priority: "", offsets: [0, 5]}
+ ]},
+ // Test trailing name without :
+ {input: "color:blue;font", expected: [
+ {name: "color", value: "blue", priority: "", offsets: [0, 11]},
+ {name: "font", value: "", priority: "", offsets: [11, 15]}
+ ]},
+ // Test trailing name with :
+ {input: "color:blue;font:", expected: [
+ {name: "color", value: "blue", priority: "", offsets: [0, 11]},
+ {name: "font", value: "", priority: "", offsets: [11, 16]}
+ ]},
+ // Test leading value
+ {input: "Arial;color:blue;", expected: [
+ {name: "", value: "Arial", priority: "", offsets: [0, 6]},
+ {name: "color", value: "blue", priority: "", offsets: [6, 17]}
+ ]},
+ // Test hex colors
+ {
+ input: "color: #333",
+ expected: [{name: "color", value: "#333", priority: "", offsets: [0, 11]}]
+ },
+ {
+ input: "color: #456789",
+ expected: [{name: "color", value: "#456789", priority: "",
+ offsets: [0, 14]}]
+ },
+ {
+ input: "wat: #XYZ",
+ expected: [{name: "wat", value: "#XYZ", priority: "", offsets: [0, 9]}]
+ },
+ // Test string/url quotes escaping
+ {
+ input: "content: \"this is a 'string'\"",
+ expected: [{name: "content", value: "\"this is a 'string'\"", priority: "",
+ offsets: [0, 29]}]
+ },
+ {
+ input: 'content: "this is a \\"string\\""',
+ expected: [{
+ name: "content",
+ value: '"this is a \\"string\\""',
+ priority: "",
+ offsets: [0, 31]}]
+ },
+ {
+ input: "content: 'this is a \"string\"'",
+ expected: [{
+ name: "content",
+ value: '\'this is a "string"\'',
+ priority: "",
+ offsets: [0, 29]
+ }]
+ },
+ {
+ input: "content: 'this is a \\'string\\''",
+ expected: [{
+ name: "content",
+ value: "'this is a \\'string\\''",
+ priority: "",
+ offsets: [0, 31],
+ }]
+ },
+ {
+ input: "content: 'this \\' is a \" really strange string'",
+ expected: [{
+ name: "content",
+ value: "'this \\' is a \" really strange string'",
+ priority: "",
+ offsets: [0, 47]
+ }]
+ },
+ {
+ input: "content: \"a not s\\ o very long title\"",
+ expected: [{
+ name: "content",
+ value: '"a not s\\ o very long title"',
+ priority: "",
+ offsets: [0, 46]
+ }]
+ },
+ // Test calc with nested parentheses
+ {
+ input: "width: calc((100% - 3em) / 2)",
+ expected: [{name: "width", value: "calc((100% - 3em) / 2)", priority: "",
+ offsets: [0, 29]}]
+ },
+
+ // Simple embedded comment test.
+ {
+ parseComments: true,
+ input: "width: 5; /* background: green; */ background: red;",
+ expected: [{name: "width", value: "5", priority: "", offsets: [0, 9]},
+ {name: "background", value: "green", priority: "",
+ offsets: [13, 31], commentOffsets: [10, 34]},
+ {name: "background", value: "red", priority: "",
+ offsets: [35, 51]}]
+ },
+
+ // Embedded comment where the parsing heuristic fails.
+ {
+ parseComments: true,
+ input: "width: 5; /* background something: green; */ background: red;",
+ expected: [{name: "width", value: "5", priority: "", offsets: [0, 9]},
+ {name: "background", value: "red", priority: "",
+ offsets: [45, 61]}]
+ },
+
+ // Embedded comment where the parsing heuristic is a bit funny.
+ {
+ parseComments: true,
+ input: "width: 5; /* background: */ background: red;",
+ expected: [{name: "width", value: "5", priority: "", offsets: [0, 9]},
+ {name: "background", value: "", priority: "",
+ offsets: [13, 24], commentOffsets: [10, 27]},
+ {name: "background", value: "red", priority: "",
+ offsets: [28, 44]}]
+ },
+
+ // Another case where the parsing heuristic says not to bother; note
+ // that there is no ";" in the comment.
+ {
+ parseComments: true,
+ input: "width: 5; /* background: yellow */ background: red;",
+ expected: [{name: "width", value: "5", priority: "", offsets: [0, 9]},
+ {name: "background", value: "yellow", priority: "",
+ offsets: [13, 31], commentOffsets: [10, 34]},
+ {name: "background", value: "red", priority: "",
+ offsets: [35, 51]}]
+ },
+
+ // Parsing a comment should yield text that has been unescaped, and
+ // the offsets should refer to the original text.
+ {
+ parseComments: true,
+ input: "/* content: '*\\/'; */",
+ expected: [{name: "content", value: "'*/'", priority: "",
+ offsets: [3, 18], commentOffsets: [0, 21]}]
+ },
+
+ // Parsing a comment should yield text that has been unescaped, and
+ // the offsets should refer to the original text. This variant
+ // tests the no-semicolon path.
+ {
+ parseComments: true,
+ input: "/* content: '*\\/' */",
+ expected: [{name: "content", value: "'*/'", priority: "",
+ offsets: [3, 17], commentOffsets: [0, 20]}]
+ },
+
+ // A comment-in-a-comment should yield the correct offsets.
+ {
+ parseComments: true,
+ input: "/* color: /\\* comment *\\/ red; */",
+ expected: [{name: "color", value: "red", priority: "",
+ offsets: [3, 30], commentOffsets: [0, 33]}]
+ },
+
+ // HTML comments are ignored.
+ {
+ parseComments: true,
+ input: "<!-- color: red; --> color: blue;",
+ expected: [{name: "color", value: "red", priority: "",
+ offsets: [5, 16]},
+ {name: "color", value: "blue", priority: "",
+ offsets: [21, 33]}]
+ },
+
+ // Don't error on an empty comment.
+ {
+ parseComments: true,
+ input: "/**/",
+ expected: []
+ },
+
+ // Parsing our special comments skips the name-check heuristic.
+ {
+ parseComments: true,
+ input: "/*! walrus: zebra; */",
+ expected: [{name: "walrus", value: "zebra", priority: "",
+ offsets: [4, 18], commentOffsets: [0, 21]}]
+ },
+
+ // Regression test for bug 1287620.
+ {
+ input: "color: blue \\9 no\\_need",
+ expected: [{name: "color", value: "blue \\9 no_need", priority: "", offsets: [0, 23]}]
+ },
+
+ // Regression test for bug 1297890 - don't paste tokens.
+ {
+ parseComments: true,
+ input: "stroke-dasharray: 1/*ThisIsAComment*/2;",
+ expected: [{name: "stroke-dasharray", value: "1 2", priority: "", offsets: [0, 39]}]
+ },
+];
+
+function run_test() {
+ run_basic_tests();
+ run_comment_tests();
+}
+
+// Test parseDeclarations.
+function run_basic_tests() {
+ for (let test of TEST_DATA) {
+ do_print("Test input string " + test.input);
+ let output;
+ try {
+ output = parseDeclarations(isCssPropertyKnown, test.input,
+ test.parseComments);
+ } catch (e) {
+ do_print("parseDeclarations threw an exception with the given input " +
+ "string");
+ if (test.throws) {
+ do_print("Exception expected");
+ do_check_true(true);
+ } else {
+ do_print("Exception unexpected\n" + e);
+ do_check_true(false);
+ }
+ }
+ if (output) {
+ assertOutput(output, test.expected);
+ }
+ }
+}
+
+const COMMENT_DATA = [
+ {
+ input: "content: 'hi",
+ expected: [{name: "content", value: "'hi", priority: "", terminator: "';",
+ offsets: [2, 14], colonOffsets: [9, 11],
+ commentOffsets: [0, 16]}],
+ },
+ {
+ input: "text that once confounded the parser;",
+ expected: []
+ },
+];
+
+// Test parseCommentDeclarations.
+function run_comment_tests() {
+ for (let test of COMMENT_DATA) {
+ do_print("Test input string " + test.input);
+ let output = _parseCommentDeclarations(isCssPropertyKnown, test.input, 0,
+ test.input.length + 4);
+ deepEqual(output, test.expected);
+ }
+}
+
+function assertOutput(actual, expected) {
+ if (actual.length === expected.length) {
+ for (let i = 0; i < expected.length; i++) {
+ do_check_true(!!actual[i]);
+ do_print("Check that the output item has the expected name, " +
+ "value and priority");
+ do_check_eq(expected[i].name, actual[i].name);
+ do_check_eq(expected[i].value, actual[i].value);
+ do_check_eq(expected[i].priority, actual[i].priority);
+ deepEqual(expected[i].offsets, actual[i].offsets);
+ if ("commentOffsets" in expected[i]) {
+ deepEqual(expected[i].commentOffsets, actual[i].commentOffsets);
+ }
+ }
+ } else {
+ for (let prop of actual) {
+ do_print("Actual output contained: {name: " + prop.name + ", value: " +
+ prop.value + ", priority: " + prop.priority + "}");
+ }
+ do_check_eq(actual.length, expected.length);
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_parsePseudoClassesAndAttributes.js b/devtools/client/shared/test/unit/test_parsePseudoClassesAndAttributes.js
new file mode 100644
index 000000000..ccd778c4a
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_parsePseudoClassesAndAttributes.js
@@ -0,0 +1,213 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {
+ parsePseudoClassesAndAttributes,
+ SELECTOR_ATTRIBUTE,
+ SELECTOR_ELEMENT,
+ SELECTOR_PSEUDO_CLASS
+} = require("devtools/shared/css/parsing-utils");
+
+const TEST_DATA = [
+ // Test that a null input throws an exception
+ {
+ input: null,
+ throws: true
+ },
+ // Test that a undefined input throws an exception
+ {
+ input: undefined,
+ throws: true
+ },
+ {
+ input: ":root",
+ expected: [
+ { value: ":root", type: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ input: ".testclass",
+ expected: [
+ { value: ".testclass", type: SELECTOR_ELEMENT }
+ ]
+ },
+ {
+ input: "div p",
+ expected: [
+ { value: "div p", type: SELECTOR_ELEMENT }
+ ]
+ },
+ {
+ input: "div > p",
+ expected: [
+ { value: "div > p", type: SELECTOR_ELEMENT }
+ ]
+ },
+ {
+ input: "a[hidden]",
+ expected: [
+ { value: "a", type: SELECTOR_ELEMENT },
+ { value: "[hidden]", type: SELECTOR_ATTRIBUTE }
+ ]
+ },
+ {
+ input: "a[hidden=true]",
+ expected: [
+ { value: "a", type: SELECTOR_ELEMENT },
+ { value: "[hidden=true]", type: SELECTOR_ATTRIBUTE }
+ ]
+ },
+ {
+ input: "a[hidden=true] p:hover",
+ expected: [
+ { value: "a", type: SELECTOR_ELEMENT },
+ { value: "[hidden=true]", type: SELECTOR_ATTRIBUTE },
+ { value: " p", type: SELECTOR_ELEMENT },
+ { value: ":hover", type: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ input: "a[checked=\"true\"]",
+ expected: [
+ { value: "a", type: SELECTOR_ELEMENT },
+ { value: "[checked=\"true\"]", type: SELECTOR_ATTRIBUTE }
+ ]
+ },
+ {
+ input: "a[title~=test]",
+ expected: [
+ { value: "a", type: SELECTOR_ELEMENT },
+ { value: "[title~=test]", type: SELECTOR_ATTRIBUTE }
+ ]
+ },
+ {
+ input: "h1[hidden=\"true\"][title^=\"Important\"]",
+ expected: [
+ { value: "h1", type: SELECTOR_ELEMENT },
+ { value: "[hidden=\"true\"]", type: SELECTOR_ATTRIBUTE },
+ { value: "[title^=\"Important\"]", type: SELECTOR_ATTRIBUTE}
+ ]
+ },
+ {
+ input: "p:hover",
+ expected: [
+ { value: "p", type: SELECTOR_ELEMENT },
+ { value: ":hover", type: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ input: "p + .testclass:hover",
+ expected: [
+ { value: "p + .testclass", type: SELECTOR_ELEMENT },
+ { value: ":hover", type: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ input: "p::before",
+ expected: [
+ { value: "p", type: SELECTOR_ELEMENT },
+ { value: "::before", type: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ input: "p:nth-child(2)",
+ expected: [
+ { value: "p", type: SELECTOR_ELEMENT },
+ { value: ":nth-child(2)", type: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ input: "p:not([title=\"test\"]) .testclass",
+ expected: [
+ { value: "p", type: SELECTOR_ELEMENT },
+ { value: ":not([title=\"test\"])", type: SELECTOR_PSEUDO_CLASS },
+ { value: " .testclass", type: SELECTOR_ELEMENT }
+ ]
+ },
+ {
+ input: "a\\:hover",
+ expected: [
+ { value: "a\\:hover", type: SELECTOR_ELEMENT }
+ ]
+ },
+ {
+ input: ":not(:lang(it))",
+ expected: [
+ { value: ":not(:lang(it))", type: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ input: "p:not(:lang(it))",
+ expected: [
+ { value: "p", type: SELECTOR_ELEMENT },
+ { value: ":not(:lang(it))", type: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ input: "p:not(p:lang(it))",
+ expected: [
+ { value: "p", type: SELECTOR_ELEMENT },
+ { value: ":not(p:lang(it))", type: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ input: ":not(:lang(it)",
+ expected: [
+ { value: ":not(:lang(it)", type: SELECTOR_ELEMENT }
+ ]
+ },
+ {
+ input: ":not(:lang(it)))",
+ expected: [
+ { value: ":not(:lang(it))", type: SELECTOR_PSEUDO_CLASS },
+ { value: ")", type: SELECTOR_ELEMENT }
+ ]
+ }
+];
+
+function run_test() {
+ for (let test of TEST_DATA) {
+ dump("Test input string " + test.input + "\n");
+ let output;
+
+ try {
+ output = parsePseudoClassesAndAttributes(test.input);
+ } catch (e) {
+ dump("parsePseudoClassesAndAttributes threw an exception with the " +
+ "given input string\n");
+ if (test.throws) {
+ ok(true, "Exception expected");
+ } else {
+ dump();
+ ok(false, "Exception unexpected\n" + e);
+ }
+ }
+
+ if (output) {
+ assertOutput(output, test.expected);
+ }
+ }
+}
+
+function assertOutput(actual, expected) {
+ if (actual.length === expected.length) {
+ for (let i = 0; i < expected.length; i++) {
+ dump("Check that the output item has the expected value and type\n");
+ ok(!!actual[i]);
+ equal(expected[i].value, actual[i].value);
+ equal(expected[i].type, actual[i].type);
+ }
+ } else {
+ for (let prop of actual) {
+ dump("Actual output contained: {value: " + prop.value + ", type: " +
+ prop.type + "}\n");
+ }
+ equal(actual.length, expected.length);
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_parseSingleValue.js b/devtools/client/shared/test/unit/test_parseSingleValue.js
new file mode 100644
index 000000000..73e4f0ac4
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_parseSingleValue.js
@@ -0,0 +1,93 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {parseSingleValue} = require("devtools/shared/css/parsing-utils");
+const {isCssPropertyKnown} = require("devtools/server/actors/css-properties");
+
+const TEST_DATA = [
+ {input: null, throws: true},
+ {input: undefined, throws: true},
+ {input: "", expected: {value: "", priority: ""}},
+ {input: " \t \t \n\n ", expected: {value: "", priority: ""}},
+ {input: "blue", expected: {value: "blue", priority: ""}},
+ {input: "blue !important", expected: {value: "blue", priority: "important"}},
+ {input: "blue!important", expected: {value: "blue", priority: "important"}},
+ {input: "blue ! important", expected: {value: "blue", priority: "important"}},
+ {
+ input: "blue ! important",
+ expected: {value: "blue", priority: "important"}
+ },
+ {input: "blue !", expected: {value: "blue", priority: ""}},
+ {input: "blue !mportant", expected: {value: "blue !mportant", priority: ""}},
+ {
+ input: " blue !important ",
+ expected: {value: "blue", priority: "important"}
+ },
+ {
+ input: "url(\"http://url.com/whyWouldYouDoThat!important.png\") !important",
+ expected: {
+ value: "url(\"http://url.com/whyWouldYouDoThat!important.png\")",
+ priority: "important"
+ }
+ },
+ {
+ input: "url(\"http://url.com/whyWouldYouDoThat!important.png\")",
+ expected: {
+ value: "url(\"http://url.com/whyWouldYouDoThat!important.png\")",
+ priority: ""
+ }
+ },
+ {
+ input: "\"content!important\" !important",
+ expected: {
+ value: "\"content!important\"",
+ priority: "important"
+ }
+ },
+ {
+ input: "\"content!important\"",
+ expected: {
+ value: "\"content!important\"",
+ priority: ""
+ }
+ },
+ {
+ input: "\"all the \\\"'\\\\ special characters\"",
+ expected: {
+ value: "\"all the \\\"'\\\\ special characters\"",
+ priority: ""
+ }
+ }
+];
+
+function run_test() {
+ for (let test of TEST_DATA) {
+ do_print("Test input value " + test.input);
+ try {
+ let output = parseSingleValue(isCssPropertyKnown, test.input);
+ assertOutput(output, test.expected);
+ } catch (e) {
+ do_print("parseSingleValue threw an exception with the given input " +
+ "value");
+ if (test.throws) {
+ do_print("Exception expected");
+ do_check_true(true);
+ } else {
+ do_print("Exception unexpected\n" + e);
+ do_check_true(false);
+ }
+ }
+ }
+}
+
+function assertOutput(actual, expected) {
+ do_print("Check that the output has the expected value and priority");
+ do_check_eq(expected.value, actual.value);
+ do_check_eq(expected.priority, actual.priority);
+}
diff --git a/devtools/client/shared/test/unit/test_rewriteDeclarations.js b/devtools/client/shared/test/unit/test_rewriteDeclarations.js
new file mode 100644
index 000000000..0183ea3c5
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_rewriteDeclarations.js
@@ -0,0 +1,529 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {RuleRewriter} = require("devtools/shared/css/parsing-utils");
+const {isCssPropertyKnown} = require("devtools/server/actors/css-properties");
+
+const TEST_DATA = [
+ {
+ desc: "simple set",
+ input: "p:v;",
+ instruction: {type: "set", name: "p", value: "N", priority: "",
+ index: 0},
+ expected: "p:N;"
+ },
+ {
+ desc: "simple set clearing !important",
+ input: "p:v !important;",
+ instruction: {type: "set", name: "p", value: "N", priority: "",
+ index: 0},
+ expected: "p:N;"
+ },
+ {
+ desc: "simple set adding !important",
+ input: "p:v;",
+ instruction: {type: "set", name: "p", value: "N", priority: "important",
+ index: 0},
+ expected: "p:N !important;"
+ },
+ {
+ desc: "simple set between comments",
+ input: "/*color:red;*/ p:v; /*color:green;*/",
+ instruction: {type: "set", name: "p", value: "N", priority: "",
+ index: 1},
+ expected: "/*color:red;*/ p:N; /*color:green;*/"
+ },
+ // The rule view can generate a "set" with a previously unknown
+ // property index; which should work like "create".
+ {
+ desc: "set at unknown index",
+ input: "a:b; e: f;",
+ instruction: {type: "set", name: "c", value: "d", priority: "",
+ index: 2},
+ expected: "a:b; e: f;c: d;"
+ },
+ {
+ desc: "simple rename",
+ input: "p:v;",
+ instruction: {type: "rename", name: "p", newName: "q", index: 0},
+ expected: "q:v;"
+ },
+ // "rename" is passed the name that the user entered, and must do
+ // any escaping necessary to ensure that this is an identifier.
+ {
+ desc: "rename requiring escape",
+ input: "p:v;",
+ instruction: {type: "rename", name: "p", newName: "a b", index: 0},
+ expected: "a\\ b:v;"
+ },
+ {
+ desc: "simple create",
+ input: "",
+ instruction: {type: "create", name: "p", value: "v", priority: "important",
+ index: 0, enabled: true},
+ expected: "p: v !important;"
+ },
+ {
+ desc: "create between two properties",
+ input: "a:b; e: f;",
+ instruction: {type: "create", name: "c", value: "d", priority: "",
+ index: 1, enabled: true},
+ expected: "a:b; c: d;e: f;"
+ },
+ // "create" is passed the name that the user entered, and must do
+ // any escaping necessary to ensure that this is an identifier.
+ {
+ desc: "create requiring escape",
+ input: "",
+ instruction: {type: "create", name: "a b", value: "d", priority: "",
+ index: 1, enabled: true},
+ expected: "a\\ b: d;"
+ },
+ {
+ desc: "simple disable",
+ input: "p:v;",
+ instruction: {type: "enable", name: "p", value: false, index: 0},
+ expected: "/*! p:v; */"
+ },
+ {
+ desc: "simple enable",
+ input: "/* color:v; */",
+ instruction: {type: "enable", name: "color", value: true, index: 0},
+ expected: "color:v;"
+ },
+ {
+ desc: "enable with following property in comment",
+ input: "/* color:red; color: blue; */",
+ instruction: {type: "enable", name: "color", value: true, index: 0},
+ expected: "color:red; /* color: blue; */"
+ },
+ {
+ desc: "enable with preceding property in comment",
+ input: "/* color:red; color: blue; */",
+ instruction: {type: "enable", name: "color", value: true, index: 1},
+ expected: "/* color:red; */ color: blue;"
+ },
+ {
+ desc: "simple remove",
+ input: "a:b;c:d;e:f;",
+ instruction: {type: "remove", name: "c", index: 1},
+ expected: "a:b;e:f;"
+ },
+ {
+ desc: "disable with comment ender in string",
+ input: "content: '*/';",
+ instruction: {type: "enable", name: "content", value: false, index: 0},
+ expected: "/*! content: '*\\/'; */"
+ },
+ {
+ desc: "enable with comment ender in string",
+ input: "/* content: '*\\/'; */",
+ instruction: {type: "enable", name: "content", value: true, index: 0},
+ expected: "content: '*/';"
+ },
+ {
+ desc: "enable requiring semicolon insertion",
+ // Note the lack of a trailing semicolon in the comment.
+ input: "/* color:red */ color: blue;",
+ instruction: {type: "enable", name: "color", value: true, index: 0},
+ expected: "color:red; color: blue;"
+ },
+ {
+ desc: "create requiring semicolon insertion",
+ // Note the lack of a trailing semicolon.
+ input: "color: red",
+ instruction: {type: "create", name: "a", value: "b", priority: "",
+ index: 1, enabled: true},
+ expected: "color: red;a: b;"
+ },
+
+ // Newline insertion.
+ {
+ desc: "simple newline insertion",
+ input: "\ncolor: red;\n",
+ instruction: {type: "create", name: "a", value: "b", priority: "",
+ index: 1, enabled: true},
+ expected: "\ncolor: red;\na: b;\n"
+ },
+ // Newline insertion.
+ {
+ desc: "semicolon insertion before newline",
+ // Note the lack of a trailing semicolon.
+ input: "\ncolor: red\n",
+ instruction: {type: "create", name: "a", value: "b", priority: "",
+ index: 1, enabled: true},
+ expected: "\ncolor: red;\na: b;\n"
+ },
+ // Newline insertion.
+ {
+ desc: "newline and semicolon insertion",
+ // Note the lack of a trailing semicolon and newline.
+ input: "\ncolor: red",
+ instruction: {type: "create", name: "a", value: "b", priority: "",
+ index: 1, enabled: true},
+ expected: "\ncolor: red;\na: b;\n"
+ },
+
+ // Newline insertion and indentation.
+ {
+ desc: "indentation with create",
+ input: "\n color: red;\n",
+ instruction: {type: "create", name: "a", value: "b", priority: "",
+ index: 1, enabled: true},
+ expected: "\n color: red;\n a: b;\n"
+ },
+ // Newline insertion and indentation.
+ {
+ desc: "indentation plus semicolon insertion before newline",
+ // Note the lack of a trailing semicolon.
+ input: "\n color: red\n",
+ instruction: {type: "create", name: "a", value: "b", priority: "",
+ index: 1, enabled: true},
+ expected: "\n color: red;\n a: b;\n"
+ },
+ {
+ desc: "indentation inserted before trailing whitespace",
+ // Note the trailing whitespace. This could come from a rule
+ // like:
+ // @supports (mumble) {
+ // body {
+ // color: red;
+ // }
+ // }
+ // Here if we create a rule we don't want it to follow
+ // the indentation of the "}".
+ input: "\n color: red;\n ",
+ instruction: {type: "create", name: "a", value: "b", priority: "",
+ index: 1, enabled: true},
+ expected: "\n color: red;\n a: b;\n "
+ },
+ // Newline insertion and indentation.
+ {
+ desc: "indentation comes from preceding comment",
+ // Note how the comment comes before the declaration.
+ input: "\n /* comment */ color: red\n",
+ instruction: {type: "create", name: "a", value: "b", priority: "",
+ index: 1, enabled: true},
+ expected: "\n /* comment */ color: red;\n a: b;\n"
+ },
+ // Default indentation.
+ {
+ desc: "use of default indentation",
+ input: "\n",
+ instruction: {type: "create", name: "a", value: "b", priority: "",
+ index: 0, enabled: true},
+ expected: "\n\ta: b;\n"
+ },
+
+ // Deletion handles newlines properly.
+ {
+ desc: "deletion removes newline",
+ input: "a:b;\nc:d;\ne:f;",
+ instruction: {type: "remove", name: "c", index: 1},
+ expected: "a:b;\ne:f;"
+ },
+ // Deletion handles newlines properly.
+ {
+ desc: "deletion remove blank line",
+ input: "\n a:b;\n c:d; \ne:f;",
+ instruction: {type: "remove", name: "c", index: 1},
+ expected: "\n a:b;\ne:f;"
+ },
+ // Deletion handles newlines properly.
+ {
+ desc: "deletion leaves comment",
+ input: "\n a:b;\n /* something */ c:d; \ne:f;",
+ instruction: {type: "remove", name: "c", index: 1},
+ expected: "\n a:b;\n /* something */ \ne:f;"
+ },
+ // Deletion handles newlines properly.
+ {
+ desc: "deletion leaves previous newline",
+ input: "\n a:b;\n c:d; \ne:f;",
+ instruction: {type: "remove", name: "e", index: 2},
+ expected: "\n a:b;\n c:d; \n"
+ },
+ // Deletion handles newlines properly.
+ {
+ desc: "deletion removes trailing whitespace",
+ input: "\n a:b;\n c:d; \n e:f;",
+ instruction: {type: "remove", name: "e", index: 2},
+ expected: "\n a:b;\n c:d; \n"
+ },
+ // Deletion handles newlines properly.
+ {
+ desc: "deletion preserves indentation",
+ input: " a:b;\n c:d; \n e:f;",
+ instruction: {type: "remove", name: "a", index: 0},
+ expected: " c:d; \n e:f;"
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "enable single quote termination",
+ input: "/* content: 'hi */ color: red;",
+ instruction: {type: "enable", name: "content", value: true, index: 0},
+ expected: "content: 'hi'; color: red;",
+ changed: {0: "'hi'"}
+ },
+ // Termination insertion corner case.
+ {
+ desc: "create single quote termination",
+ input: "content: 'hi",
+ instruction: {type: "create", name: "color", value: "red", priority: "",
+ index: 1, enabled: true},
+ expected: "content: 'hi';color: red;",
+ changed: {0: "'hi'"}
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "enable double quote termination",
+ input: "/* content: \"hi */ color: red;",
+ instruction: {type: "enable", name: "content", value: true, index: 0},
+ expected: "content: \"hi\"; color: red;",
+ changed: {0: "\"hi\""}
+ },
+ // Termination insertion corner case.
+ {
+ desc: "create double quote termination",
+ input: "content: \"hi",
+ instruction: {type: "create", name: "color", value: "red", priority: "",
+ index: 1, enabled: true},
+ expected: "content: \"hi\";color: red;",
+ changed: {0: "\"hi\""}
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "enable url termination",
+ input: "/* background-image: url(something.jpg */ color: red;",
+ instruction: {type: "enable", name: "background-image", value: true,
+ index: 0},
+ expected: "background-image: url(something.jpg); color: red;",
+ changed: {0: "url(something.jpg)"}
+ },
+ // Termination insertion corner case.
+ {
+ desc: "create url termination",
+ input: "background-image: url(something.jpg",
+ instruction: {type: "create", name: "color", value: "red", priority: "",
+ index: 1, enabled: true},
+ expected: "background-image: url(something.jpg);color: red;",
+ changed: {0: "url(something.jpg)"}
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "enable url single quote termination",
+ input: "/* background-image: url('something.jpg */ color: red;",
+ instruction: {type: "enable", name: "background-image", value: true,
+ index: 0},
+ expected: "background-image: url('something.jpg'); color: red;",
+ changed: {0: "url('something.jpg')"}
+ },
+ // Termination insertion corner case.
+ {
+ desc: "create url single quote termination",
+ input: "background-image: url('something.jpg",
+ instruction: {type: "create", name: "color", value: "red", priority: "",
+ index: 1, enabled: true},
+ expected: "background-image: url('something.jpg');color: red;",
+ changed: {0: "url('something.jpg')"}
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "create url double quote termination",
+ input: "/* background-image: url(\"something.jpg */ color: red;",
+ instruction: {type: "enable", name: "background-image", value: true,
+ index: 0},
+ expected: "background-image: url(\"something.jpg\"); color: red;",
+ changed: {0: "url(\"something.jpg\")"}
+ },
+ // Termination insertion corner case.
+ {
+ desc: "enable url double quote termination",
+ input: "background-image: url(\"something.jpg",
+ instruction: {type: "create", name: "color", value: "red", priority: "",
+ index: 1, enabled: true},
+ expected: "background-image: url(\"something.jpg\");color: red;",
+ changed: {0: "url(\"something.jpg\")"}
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "create backslash termination",
+ input: "something: \\",
+ instruction: {type: "create", name: "color", value: "red", priority: "",
+ index: 1, enabled: true},
+ expected: "something: \\\\;color: red;",
+ // The lexer rewrites the token before we see it. However this is
+ // so obscure as to be inconsequential.
+ changed: {0: "\uFFFD\\"}
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "enable backslash single quote termination",
+ input: "something: '\\",
+ instruction: {type: "create", name: "color", value: "red", priority: "",
+ index: 1, enabled: true},
+ expected: "something: '\\\\';color: red;",
+ changed: {0: "'\\\\'"}
+ },
+ {
+ desc: "enable backslash double quote termination",
+ input: "something: \"\\",
+ instruction: {type: "create", name: "color", value: "red", priority: "",
+ index: 1, enabled: true},
+ expected: "something: \"\\\\\";color: red;",
+ changed: {0: "\"\\\\\""}
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "enable comment termination",
+ input: "something: blah /* comment ",
+ instruction: {type: "create", name: "color", value: "red", priority: "",
+ index: 1, enabled: true},
+ expected: "something: blah /* comment*/; color: red;"
+ },
+
+ // Rewrite a "heuristic override" comment.
+ {
+ desc: "enable with heuristic override comment",
+ input: "/*! walrus: zebra; */",
+ instruction: {type: "enable", name: "walrus", value: true, index: 0},
+ expected: "walrus: zebra;"
+ },
+
+ // Sanitize a bad value.
+ {
+ desc: "create sanitize unpaired brace",
+ input: "",
+ instruction: {type: "create", name: "p", value: "}", priority: "",
+ index: 0, enabled: true},
+ expected: "p: \\};",
+ changed: {0: "\\}"}
+ },
+ // Sanitize a bad value.
+ {
+ desc: "set sanitize unpaired brace",
+ input: "walrus: zebra;",
+ instruction: {type: "set", name: "walrus", value: "{{}}}", priority: "",
+ index: 0},
+ expected: "walrus: {{}}\\};",
+ changed: {0: "{{}}\\}"}
+ },
+ // Sanitize a bad value.
+ {
+ desc: "enable sanitize unpaired brace",
+ input: "/*! walrus: }*/",
+ instruction: {type: "enable", name: "walrus", value: true, index: 0},
+ expected: "walrus: \\};",
+ changed: {0: "\\}"}
+ },
+
+ // Creating a new declaration does not require an attempt to
+ // terminate a previous commented declaration.
+ {
+ desc: "disabled declaration does not need semicolon insertion",
+ input: "/*! no: semicolon */\n",
+ instruction: {type: "create", name: "walrus", value: "zebra", priority: "",
+ index: 1, enabled: true},
+ expected: "/*! no: semicolon */\nwalrus: zebra;\n",
+ changed: {}
+ },
+
+ {
+ desc: "create commented-out property",
+ input: "p: v",
+ instruction: {type: "create", name: "shoveler", value: "duck", priority: "",
+ index: 1, enabled: false},
+ expected: "p: v;/*! shoveler: duck; */",
+ },
+ {
+ desc: "disabled create with comment ender in string",
+ input: "",
+ instruction: {type: "create", name: "content", value: "'*/'", priority: "",
+ index: 0, enabled: false},
+ expected: "/*! content: '*\\/'; */"
+ },
+
+ {
+ desc: "delete disabled property",
+ input: "\n a:b;\n /* color:#f0c; */\n e:f;",
+ instruction: {type: "remove", name: "color", index: 1},
+ expected: "\n a:b;\n e:f;",
+ },
+ {
+ desc: "delete heuristic-disabled property",
+ input: "\n a:b;\n /*! c:d; */\n e:f;",
+ instruction: {type: "remove", name: "c", index: 1},
+ expected: "\n a:b;\n e:f;",
+ },
+ {
+ desc: "delete disabled property leaving other disabled property",
+ input: "\n a:b;\n /* color:#f0c; background-color: seagreen; */\n e:f;",
+ instruction: {type: "remove", name: "color", index: 1},
+ expected: "\n a:b;\n /* background-color: seagreen; */\n e:f;",
+ },
+];
+
+function rewriteDeclarations(inputString, instruction, defaultIndentation) {
+ let rewriter = new RuleRewriter(isCssPropertyKnown, null, inputString);
+ rewriter.defaultIndentation = defaultIndentation;
+
+ switch (instruction.type) {
+ case "rename":
+ rewriter.renameProperty(instruction.index, instruction.name,
+ instruction.newName);
+ break;
+
+ case "enable":
+ rewriter.setPropertyEnabled(instruction.index, instruction.name,
+ instruction.value);
+ break;
+
+ case "create":
+ rewriter.createProperty(instruction.index, instruction.name,
+ instruction.value, instruction.priority,
+ instruction.enabled);
+ break;
+
+ case "set":
+ rewriter.setProperty(instruction.index, instruction.name,
+ instruction.value, instruction.priority);
+ break;
+
+ case "remove":
+ rewriter.removeProperty(instruction.index, instruction.name);
+ break;
+
+ default:
+ throw new Error("unrecognized instruction");
+ }
+
+ return rewriter.getResult();
+}
+
+function run_test() {
+ for (let test of TEST_DATA) {
+ let {changed, text} = rewriteDeclarations(test.input, test.instruction,
+ "\t");
+ equal(text, test.expected, "output for " + test.desc);
+
+ let expectChanged;
+ if ("changed" in test) {
+ expectChanged = test.changed;
+ } else {
+ expectChanged = {};
+ }
+ deepEqual(changed, expectChanged, "changed result for " + test.desc);
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_source-utils.js b/devtools/client/shared/test/unit/test_source-utils.js
new file mode 100644
index 000000000..2ff55b92e
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_source-utils.js
@@ -0,0 +1,181 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests utility functions contained in `source-utils.js`
+ */
+
+const { require } = Components.utils.import("resource://devtools/shared/Loader.jsm", {});
+const sourceUtils = require("devtools/client/shared/source-utils");
+
+function run_test() {
+ run_next_test();
+}
+
+const CHROME_URLS = [
+ "chrome://foo", "resource://baz", "jar:file:///Users/root"
+];
+
+const CONTENT_URLS = [
+ "http://mozilla.org", "https://mozilla.org", "file:///Users/root", "app://fxosapp"
+];
+
+// Test `sourceUtils.parseURL`
+add_task(function* () {
+ let parsed = sourceUtils.parseURL("https://foo.com:8888/boo/bar.js?q=query");
+ equal(parsed.fileName, "bar.js", "parseURL parsed valid fileName");
+ equal(parsed.host, "foo.com:8888", "parseURL parsed valid host");
+ equal(parsed.hostname, "foo.com", "parseURL parsed valid hostname");
+ equal(parsed.port, "8888", "parseURL parsed valid port");
+ equal(parsed.href, "https://foo.com:8888/boo/bar.js?q=query", "parseURL parsed valid href");
+
+ parsed = sourceUtils.parseURL("https://foo.com");
+ equal(parsed.host, "foo.com", "parseURL parsed valid host when no port given");
+ equal(parsed.hostname, "foo.com", "parseURL parsed valid hostname when no port given");
+
+ equal(sourceUtils.parseURL("self-hosted"), null,
+ "parseURL returns `null` for invalid URLs");
+});
+
+// Test `sourceUtils.isContentScheme`.
+add_task(function* () {
+ for (let url of CHROME_URLS) {
+ ok(!sourceUtils.isContentScheme(url),
+ `${url} correctly identified as not content scheme`);
+ }
+ for (let url of CONTENT_URLS) {
+ ok(sourceUtils.isContentScheme(url), `${url} correctly identified as content scheme`);
+ }
+});
+
+// Test `sourceUtils.isChromeScheme`.
+add_task(function* () {
+ for (let url of CHROME_URLS) {
+ ok(sourceUtils.isChromeScheme(url), `${url} correctly identified as chrome scheme`);
+ }
+ for (let url of CONTENT_URLS) {
+ ok(!sourceUtils.isChromeScheme(url),
+ `${url} correctly identified as not chrome scheme`);
+ }
+});
+
+// Test `sourceUtils.isDataScheme`.
+add_task(function* () {
+ let dataURI = "data:text/html;charset=utf-8,<!DOCTYPE html></html>";
+ ok(sourceUtils.isDataScheme(dataURI), `${dataURI} correctly identified as data scheme`);
+
+ for (let url of CHROME_URLS) {
+ ok(!sourceUtils.isDataScheme(url), `${url} correctly identified as not data scheme`);
+ }
+ for (let url of CONTENT_URLS) {
+ ok(!sourceUtils.isDataScheme(url), `${url} correctly identified as not data scheme`);
+ }
+});
+
+// Test `sourceUtils.getSourceNames`.
+add_task(function* () {
+ testAbbreviation("http://example.com/foo/bar/baz/boo.js",
+ "boo.js",
+ "http://example.com/foo/bar/baz/boo.js",
+ "example.com");
+});
+
+// Test `sourceUtils.isScratchpadTheme`
+add_task(function* () {
+ ok(sourceUtils.isScratchpadScheme("Scratchpad/1"),
+ "Scratchpad/1 identified as scratchpad");
+ ok(sourceUtils.isScratchpadScheme("Scratchpad/20"),
+ "Scratchpad/20 identified as scratchpad");
+ ok(!sourceUtils.isScratchpadScheme("http://www.mozilla.org"), "http://www.mozilla.org not identified as scratchpad");
+});
+
+// Test `sourceUtils.getSourceNames`.
+add_task(function* () {
+ // Check length
+ let longMalformedURL = `example.com${new Array(100).fill("/a").join("")}/file.js`;
+ ok(sourceUtils.getSourceNames(longMalformedURL).short.length <= 100,
+ "`short` names are capped at 100 characters");
+
+ testAbbreviation("self-hosted", "self-hosted", "self-hosted");
+ testAbbreviation("", "(unknown)", "(unknown)");
+
+ // Test shortening data URIs, stripping mime/charset
+ testAbbreviation("data:text/html;charset=utf-8,<!DOCTYPE html></html>",
+ "data:<!DOCTYPE html></html>",
+ "data:text/html;charset=utf-8,<!DOCTYPE html></html>");
+
+ let longDataURI = `data:image/png;base64,${new Array(100).fill("a").join("")}`;
+ let longDataURIShort = sourceUtils.getSourceNames(longDataURI).short;
+
+ // Test shortening data URIs and that the `short` result is capped
+ ok(longDataURIShort.length <= 100,
+ "`short` names are capped at 100 characters for data URIs");
+ equal(longDataURIShort.substr(0, 10), "data:aaaaa",
+ "truncated data URI short names still have `data:...`");
+
+ // Test simple URL and cache retrieval by calling the same input multiple times.
+ let testUrl = "http://example.com/foo/bar/baz/boo.js";
+ testAbbreviation(testUrl, "boo.js", testUrl, "example.com");
+ testAbbreviation(testUrl, "boo.js", testUrl, "example.com");
+
+ // Check query and hash and port
+ testAbbreviation("http://example.com:8888/foo/bar/baz.js?q=query#go",
+ "baz.js",
+ "http://example.com:8888/foo/bar/baz.js",
+ "example.com:8888");
+
+ // Trailing "/" with nothing beyond host
+ testAbbreviation("http://example.com/",
+ "/",
+ "http://example.com/",
+ "example.com");
+
+ // Trailing "/"
+ testAbbreviation("http://example.com/foo/bar/",
+ "bar",
+ "http://example.com/foo/bar/",
+ "example.com");
+
+ // Non-extension ending
+ testAbbreviation("http://example.com/bar",
+ "bar",
+ "http://example.com/bar",
+ "example.com");
+
+ // Check query
+ testAbbreviation("http://example.com/foo.js?bar=1&baz=2",
+ "foo.js",
+ "http://example.com/foo.js",
+ "example.com");
+
+ // Check query with trailing slash
+ testAbbreviation("http://example.com/foo/?bar=1&baz=2",
+ "foo",
+ "http://example.com/foo/",
+ "example.com");
+});
+
+// Test for source mapped file name
+add_task(function* () {
+ const { getSourceMappedFile } = sourceUtils;
+ const source = "baz.js";
+ const output = getSourceMappedFile(source);
+ equal(output, "baz.js", "correctly formats file name");
+ // Test for OSX file path
+ const source1 = "/foo/bar/baz.js";
+ const output1 = getSourceMappedFile(source1);
+ equal(output1, "baz.js", "correctly formats Linux file path");
+ // Test for Windows file path
+ const source2 = "Z:\\foo\\bar\\baz.js";
+ const output2 = getSourceMappedFile(source2);
+ equal(output2, "baz.js", "correctly formats Windows file path");
+});
+
+function testAbbreviation(source, short, long, host) {
+ let results = sourceUtils.getSourceNames(source);
+ equal(results.short, short, `${source} has correct "short" name`);
+ equal(results.long, long, `${source} has correct "long" name`);
+ equal(results.host, host, `${source} has correct "host" name`);
+}
diff --git a/devtools/client/shared/test/unit/test_suggestion-picker.js b/devtools/client/shared/test/unit/test_suggestion-picker.js
new file mode 100644
index 000000000..28c9df13b
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_suggestion-picker.js
@@ -0,0 +1,149 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test the suggestion-picker helper methods.
+ */
+const {require} = Components.utils.import("resource://devtools/shared/Loader.jsm", {});
+const {
+ findMostRelevantIndex,
+ findMostRelevantCssPropertyIndex
+} = require("devtools/client/shared/suggestion-picker");
+
+/**
+ * Run all tests defined below.
+ */
+function run_test() {
+ ensureMostRelevantIndexProvidedByHelperFunction();
+ ensureMostRelevantIndexProvidedByClassMethod();
+ ensureErrorThrownWithInvalidArguments();
+}
+
+/**
+ * Generic test data.
+ */
+const TEST_DATA = [
+ {
+ // Match in sortedItems array.
+ items: ["chrome", "edge", "firefox"],
+ sortedItems: ["firefox", "chrome", "edge"],
+ expectedIndex: 2
+ }, {
+ // No match in sortedItems array.
+ items: ["apple", "oranges", "banana"],
+ sortedItems: ["kiwi", "pear", "peach"],
+ expectedIndex: 0
+ }, {
+ // Empty items array.
+ items: [],
+ sortedItems: ["empty", "arrays", "can't", "have", "relevant", "indexes"],
+ expectedIndex: -1
+ }
+];
+
+function ensureMostRelevantIndexProvidedByHelperFunction() {
+ do_print("Running ensureMostRelevantIndexProvidedByHelperFunction()");
+
+ for (let testData of TEST_DATA) {
+ let { items, sortedItems, expectedIndex } = testData;
+ let mostRelevantIndex = findMostRelevantIndex(items, sortedItems);
+ strictEqual(mostRelevantIndex, expectedIndex);
+ }
+}
+
+/**
+ * CSS properties test data.
+ */
+const CSS_TEST_DATA = [
+ {
+ items: [
+ "backface-visibility",
+ "background",
+ "background-attachment",
+ "background-blend-mode",
+ "background-clip",
+ "background-color",
+ "background-image",
+ "background-origin",
+ "background-position",
+ "background-repeat"
+ ],
+ expectedIndex: 1
+ },
+ {
+ items: [
+ "caption-side",
+ "clear",
+ "clip",
+ "clip-path",
+ "clip-rule",
+ "color",
+ "color-interpolation",
+ "color-interpolation-filters",
+ "content",
+ "counter-increment"
+ ],
+ expectedIndex: 5
+ },
+ {
+ items: [
+ "direction",
+ "display",
+ "dominant-baseline"
+ ],
+ expectedIndex: 1
+ },
+ {
+ items: [
+ "object-fit",
+ "object-position",
+ "offset-block-end",
+ "offset-block-start",
+ "offset-inline-end",
+ "offset-inline-start",
+ "opacity",
+ "order",
+ "orphans",
+ "outline"
+ ],
+ expectedIndex: 6
+ },
+ {
+ items: [
+ "white-space",
+ "widows",
+ "width",
+ "will-change",
+ "word-break",
+ "word-spacing",
+ "word-wrap",
+ "writing-mode"
+ ],
+ expectedIndex: 2
+ }
+];
+
+function ensureMostRelevantIndexProvidedByClassMethod() {
+ do_print("Running ensureMostRelevantIndexProvidedByClassMethod()");
+
+ for (let testData of CSS_TEST_DATA) {
+ let { items, expectedIndex } = testData;
+ let mostRelevantIndex = findMostRelevantCssPropertyIndex(items);
+ strictEqual(mostRelevantIndex, expectedIndex);
+ }
+}
+
+function ensureErrorThrownWithInvalidArguments() {
+ do_print("Running ensureErrorThrownWithInvalidTypeArgument()");
+
+ let expectedError = "Please provide valid items and sortedItems arrays.";
+ // No arguments passed.
+ throws(() => findMostRelevantIndex(), expectedError);
+ // Invalid arguments passed.
+ throws(() => findMostRelevantIndex([]), expectedError);
+ throws(() => findMostRelevantIndex(null, []), expectedError);
+ throws(() => findMostRelevantIndex([], "string"), expectedError);
+ throws(() => findMostRelevantIndex("string", []), expectedError);
+}
diff --git a/devtools/client/shared/test/unit/test_undoStack.js b/devtools/client/shared/test/unit/test_undoStack.js
new file mode 100644
index 000000000..7499614fd
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_undoStack.js
@@ -0,0 +1,98 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {Loader} = Components.utils.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
+
+const loader = new Loader.Loader({
+ paths: {
+ "": "resource://gre/modules/commonjs/",
+ "devtools": "resource://devtools",
+ },
+ globals: {},
+});
+const require = Loader.Require(loader, { id: "undo-test" });
+
+const {UndoStack} = require("devtools/client/shared/undo");
+
+const MAX_SIZE = 5;
+
+function run_test() {
+ let str = "";
+ let stack = new UndoStack(MAX_SIZE);
+
+ function add(ch) {
+ stack.do(function () {
+ str += ch;
+ }, function () {
+ str = str.slice(0, -1);
+ });
+ }
+
+ do_check_false(stack.canUndo());
+ do_check_false(stack.canRedo());
+
+ // Check adding up to the limit of the size
+ add("a");
+ do_check_true(stack.canUndo());
+ do_check_false(stack.canRedo());
+
+ add("b");
+ add("c");
+ add("d");
+ add("e");
+
+ do_check_eq(str, "abcde");
+
+ // Check a simple undo+redo
+ stack.undo();
+
+ do_check_eq(str, "abcd");
+ do_check_true(stack.canRedo());
+
+ stack.redo();
+ do_check_eq(str, "abcde");
+ do_check_false(stack.canRedo());
+
+ // Check an undo followed by a new action
+ stack.undo();
+ do_check_eq(str, "abcd");
+
+ add("q");
+ do_check_eq(str, "abcdq");
+ do_check_false(stack.canRedo());
+
+ stack.undo();
+ do_check_eq(str, "abcd");
+ stack.redo();
+ do_check_eq(str, "abcdq");
+
+ // Revert back to the beginning of the queue...
+ while (stack.canUndo()) {
+ stack.undo();
+ }
+ do_check_eq(str, "");
+
+ // Now put it all back....
+ while (stack.canRedo()) {
+ stack.redo();
+ }
+ do_check_eq(str, "abcdq");
+
+ // Now go over the undo limit...
+ add("1");
+ add("2");
+ add("3");
+
+ do_check_eq(str, "abcdq123");
+
+ // And now undoing the whole stack should only undo 5 actions.
+ while (stack.canUndo()) {
+ stack.undo();
+ }
+
+ do_check_eq(str, "abc");
+}
diff --git a/devtools/client/shared/test/unit/xpcshell.ini b/devtools/client/shared/test/unit/xpcshell.ini
new file mode 100644
index 000000000..b3c5791ec
--- /dev/null
+++ b/devtools/client/shared/test/unit/xpcshell.ini
@@ -0,0 +1,30 @@
+[DEFAULT]
+tags = devtools
+head =
+tail =
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+
+support-files =
+ ../helper_color_data.js
+
+[test_advanceValidate.js]
+[test_attribute-parsing-01.js]
+[test_attribute-parsing-02.js]
+[test_bezierCanvas.js]
+[test_cssAngle.js]
+[test_cssColor-01.js]
+[test_cssColor-02.js]
+[test_cssColor-03.js]
+[test_cssColorDatabase.js]
+[test_cubicBezier.js]
+[test_escapeCSSComment.js]
+[test_parseDeclarations.js]
+[test_parsePseudoClassesAndAttributes.js]
+[test_parseSingleValue.js]
+[test_rewriteDeclarations.js]
+[test_source-utils.js]
+[test_suggestion-picker.js]
+[test_undoStack.js]
+[test_VariablesView_filtering-without-controller.js]
+[test_VariablesView_getString_promise.js]