From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- devtools/client/shared/test/.eslintrc.js | 9 + devtools/client/shared/test/browser.ini | 188 ++++ devtools/client/shared/test/browser_css_angle.js | 176 +++ devtools/client/shared/test/browser_css_color.js | 137 +++ .../client/shared/test/browser_cubic-bezier-01.js | 38 + .../client/shared/test/browser_cubic-bezier-02.js | 200 ++++ .../client/shared/test/browser_cubic-bezier-03.js | 68 ++ .../client/shared/test/browser_cubic-bezier-04.js | 50 + .../client/shared/test/browser_cubic-bezier-05.js | 48 + .../client/shared/test/browser_cubic-bezier-06.js | 79 ++ devtools/client/shared/test/browser_devices.js | 57 + devtools/client/shared/test/browser_devices.json | 23 + .../client/shared/test/browser_filter-editor-01.js | 114 ++ .../client/shared/test/browser_filter-editor-02.js | 107 ++ .../client/shared/test/browser_filter-editor-03.js | 65 ++ .../client/shared/test/browser_filter-editor-04.js | 87 ++ .../client/shared/test/browser_filter-editor-05.js | 148 +++ .../client/shared/test/browser_filter-editor-06.js | 71 ++ .../client/shared/test/browser_filter-editor-07.js | 27 + .../client/shared/test/browser_filter-editor-08.js | 84 ++ .../client/shared/test/browser_filter-editor-09.js | 125 +++ .../client/shared/test/browser_filter-editor-10.js | 87 ++ .../shared/test/browser_filter-presets-01.js | 99 ++ .../shared/test/browser_filter-presets-02.js | 45 + .../shared/test/browser_filter-presets-03.js | 40 + .../client/shared/test/browser_flame-graph-01.js | 61 ++ .../client/shared/test/browser_flame-graph-02.js | 44 + .../client/shared/test/browser_flame-graph-03a.js | 138 +++ .../client/shared/test/browser_flame-graph-03b.js | 92 ++ .../client/shared/test/browser_flame-graph-03c.js | 155 +++ .../client/shared/test/browser_flame-graph-04.js | 90 ++ .../client/shared/test/browser_flame-graph-05.js | 113 ++ .../shared/test/browser_flame-graph-utils-01.js | 256 +++++ .../shared/test/browser_flame-graph-utils-02.js | 130 +++ .../shared/test/browser_flame-graph-utils-03.js | 136 +++ .../shared/test/browser_flame-graph-utils-04.js | 188 ++++ .../shared/test/browser_flame-graph-utils-05.js | 48 + .../shared/test/browser_flame-graph-utils-06.js | 117 ++ .../shared/test/browser_flame-graph-utils-hash.js | 24 + devtools/client/shared/test/browser_graphs-01.js | 70 ++ devtools/client/shared/test/browser_graphs-02.js | 107 ++ devtools/client/shared/test/browser_graphs-03.js | 111 ++ devtools/client/shared/test/browser_graphs-04.js | 69 ++ devtools/client/shared/test/browser_graphs-05.js | 154 +++ devtools/client/shared/test/browser_graphs-06.js | 112 ++ devtools/client/shared/test/browser_graphs-07a.js | 232 ++++ devtools/client/shared/test/browser_graphs-07b.js | 88 ++ devtools/client/shared/test/browser_graphs-07c.js | 139 +++ devtools/client/shared/test/browser_graphs-07d.js | 71 ++ devtools/client/shared/test/browser_graphs-07e.js | 127 +++ devtools/client/shared/test/browser_graphs-08.js | 88 ++ devtools/client/shared/test/browser_graphs-09a.js | 104 ++ devtools/client/shared/test/browser_graphs-09b.js | 82 ++ devtools/client/shared/test/browser_graphs-09c.js | 38 + devtools/client/shared/test/browser_graphs-09d.js | 39 + devtools/client/shared/test/browser_graphs-09e.js | 84 ++ devtools/client/shared/test/browser_graphs-09f.js | 53 + devtools/client/shared/test/browser_graphs-10a.js | 162 +++ devtools/client/shared/test/browser_graphs-10b.js | 71 ++ devtools/client/shared/test/browser_graphs-10c.js | 109 ++ devtools/client/shared/test/browser_graphs-11a.js | 60 ++ devtools/client/shared/test/browser_graphs-11b.js | 133 +++ devtools/client/shared/test/browser_graphs-12.js | 157 +++ devtools/client/shared/test/browser_graphs-13.js | 44 + devtools/client/shared/test/browser_graphs-14.js | 111 ++ devtools/client/shared/test/browser_graphs-15.js | 49 + devtools/client/shared/test/browser_graphs-16.js | 45 + .../client/shared/test/browser_html_tooltip-01.js | 88 ++ .../client/shared/test/browser_html_tooltip-02.js | 174 +++ .../client/shared/test/browser_html_tooltip-03.js | 155 +++ .../client/shared/test/browser_html_tooltip-04.js | 110 ++ .../client/shared/test/browser_html_tooltip-05.js | 109 ++ .../shared/test/browser_html_tooltip_arrow-01.js | 108 ++ .../shared/test/browser_html_tooltip_arrow-02.js | 100 ++ .../test/browser_html_tooltip_consecutive-show.js | 71 ++ .../shared/test/browser_html_tooltip_hover.js | 65 ++ .../shared/test/browser_html_tooltip_offset.js | 99 ++ .../client/shared/test/browser_html_tooltip_rtl.js | 140 +++ .../test/browser_html_tooltip_variable-height.js | 75 ++ .../shared/test/browser_html_tooltip_width-auto.js | 61 ++ .../test/browser_html_tooltip_xul-wrapper.js | 80 ++ .../shared/test/browser_inplace-editor-01.js | 150 +++ .../shared/test/browser_inplace-editor-02.js | 71 ++ .../test/browser_inplace-editor_autocomplete_01.js | 75 ++ .../test/browser_inplace-editor_autocomplete_02.js | 80 ++ .../browser_inplace-editor_autocomplete_offset.js | 119 ++ .../shared/test/browser_inplace-editor_maxwidth.js | 114 ++ .../client/shared/test/browser_key_shortcuts.js | 425 ++++++++ devtools/client/shared/test/browser_keycodes.js | 12 + .../test/browser_layoutHelpers-getBoxQuads.html | 65 ++ .../test/browser_layoutHelpers-getBoxQuads.js | 219 ++++ .../client/shared/test/browser_layoutHelpers.html | 24 + .../client/shared/test/browser_layoutHelpers.js | 93 ++ devtools/client/shared/test/browser_mdn-docs-01.js | 168 +++ devtools/client/shared/test/browser_mdn-docs-02.js | 128 +++ devtools/client/shared/test/browser_mdn-docs-03.js | 277 +++++ devtools/client/shared/test/browser_num-l10n.js | 27 + .../client/shared/test/browser_options-view-01.js | 110 ++ .../client/shared/test/browser_outputparser.js | 292 +++++ devtools/client/shared/test/browser_poller.js | 136 +++ devtools/client/shared/test/browser_prefs-01.js | 44 + devtools/client/shared/test/browser_prefs-02.js | 45 + devtools/client/shared/test/browser_require_raw.js | 20 + devtools/client/shared/test/browser_spectrum.js | 114 ++ .../shared/test/browser_tableWidget_basic.js | 390 +++++++ .../browser_tableWidget_keyboard_interaction.js | 194 ++++ .../test/browser_tableWidget_mouse_interaction.js | 317 ++++++ .../test/browser_telemetry_button_eyedropper.js | 52 + .../test/browser_telemetry_button_paintflashing.js | 89 ++ .../test/browser_telemetry_button_responsive.js | 95 ++ .../test/browser_telemetry_button_scratchpad.js | 127 +++ .../shared/test/browser_telemetry_sidebar.js | 84 ++ .../shared/test/browser_telemetry_toolbox.js | 22 + ...browser_telemetry_toolboxtabs_canvasdebugger.js | 29 + .../browser_telemetry_toolboxtabs_inspector.js | 22 + .../browser_telemetry_toolboxtabs_jsdebugger.js | 22 + .../browser_telemetry_toolboxtabs_jsprofiler.js | 22 + .../browser_telemetry_toolboxtabs_netmonitor.js | 23 + .../test/browser_telemetry_toolboxtabs_options.js | 22 + .../browser_telemetry_toolboxtabs_shadereditor.js | 37 + .../test/browser_telemetry_toolboxtabs_storage.js | 28 + .../browser_telemetry_toolboxtabs_styleeditor.js | 23 + ...browser_telemetry_toolboxtabs_webaudioeditor.js | 29 + .../browser_telemetry_toolboxtabs_webconsole.js | 22 + .../shared/test/browser_templater_basic.html | 13 + .../client/shared/test/browser_templater_basic.js | 286 +++++ devtools/client/shared/test/browser_theme.js | 98 ++ .../client/shared/test/browser_theme_switching.js | 53 + .../client/shared/test/browser_toolbar_basic.html | 40 + .../client/shared/test/browser_toolbar_basic.js | 60 ++ .../client/shared/test/browser_toolbar_tooltip.js | 111 ++ .../browser_toolbar_webconsole_errors_count.html | 33 + .../browser_toolbar_webconsole_errors_count.js | 256 +++++ .../client/shared/test/browser_treeWidget_basic.js | 267 +++++ .../browser_treeWidget_keyboard_interaction.js | 228 ++++ .../test/browser_treeWidget_mouse_interaction.js | 135 +++ devtools/client/shared/test/doc_options-view.xul | 26 + devtools/client/shared/test/head.js | 346 ++++++ devtools/client/shared/test/helper_color_data.js | 175 +++ devtools/client/shared/test/helper_html_tooltip.js | 96 ++ .../client/shared/test/helper_inplace_editor.js | 115 ++ .../shared/test/html-mdn-css-basic-testing.html | 21 + .../test/html-mdn-css-no-summary-or-syntax.html | 12 + .../shared/test/html-mdn-css-no-summary.html | 21 + .../client/shared/test/html-mdn-css-no-syntax.html | 17 + .../shared/test/html-mdn-css-syntax-old-style.html | 23 + devtools/client/shared/test/leakhunt.js | 165 +++ devtools/client/shared/test/test-actor-registry.js | 97 ++ devtools/client/shared/test/test-actor.js | 1138 ++++++++++++++++++++ devtools/client/shared/test/unit/.eslintrc.js | 6 + ...t_VariablesView_filtering-without-controller.js | 36 + .../unit/test_VariablesView_getString_promise.js | 76 ++ .../shared/test/unit/test_advanceValidate.js | 31 + .../shared/test/unit/test_attribute-parsing-01.js | 73 ++ .../shared/test/unit/test_attribute-parsing-02.js | 134 +++ .../client/shared/test/unit/test_bezierCanvas.js | 117 ++ devtools/client/shared/test/unit/test_cssAngle.js | 33 + .../client/shared/test/unit/test_cssColor-01.js | 76 ++ .../client/shared/test/unit/test_cssColor-02.js | 45 + .../client/shared/test/unit/test_cssColor-03.js | 61 ++ .../shared/test/unit/test_cssColorDatabase.js | 63 ++ .../client/shared/test/unit/test_cubicBezier.js | 146 +++ .../shared/test/unit/test_escapeCSSComment.js | 40 + .../shared/test/unit/test_parseDeclarations.js | 439 ++++++++ .../unit/test_parsePseudoClassesAndAttributes.js | 213 ++++ .../shared/test/unit/test_parseSingleValue.js | 93 ++ .../shared/test/unit/test_rewriteDeclarations.js | 529 +++++++++ .../client/shared/test/unit/test_source-utils.js | 181 ++++ .../shared/test/unit/test_suggestion-picker.js | 149 +++ devtools/client/shared/test/unit/test_undoStack.js | 98 ++ devtools/client/shared/test/unit/xpcshell.ini | 30 + 171 files changed, 18866 insertions(+) create mode 100644 devtools/client/shared/test/.eslintrc.js create mode 100644 devtools/client/shared/test/browser.ini create mode 100644 devtools/client/shared/test/browser_css_angle.js create mode 100644 devtools/client/shared/test/browser_css_color.js create mode 100644 devtools/client/shared/test/browser_cubic-bezier-01.js create mode 100644 devtools/client/shared/test/browser_cubic-bezier-02.js create mode 100644 devtools/client/shared/test/browser_cubic-bezier-03.js create mode 100644 devtools/client/shared/test/browser_cubic-bezier-04.js create mode 100644 devtools/client/shared/test/browser_cubic-bezier-05.js create mode 100644 devtools/client/shared/test/browser_cubic-bezier-06.js create mode 100644 devtools/client/shared/test/browser_devices.js create mode 100644 devtools/client/shared/test/browser_devices.json create mode 100644 devtools/client/shared/test/browser_filter-editor-01.js create mode 100644 devtools/client/shared/test/browser_filter-editor-02.js create mode 100644 devtools/client/shared/test/browser_filter-editor-03.js create mode 100644 devtools/client/shared/test/browser_filter-editor-04.js create mode 100644 devtools/client/shared/test/browser_filter-editor-05.js create mode 100644 devtools/client/shared/test/browser_filter-editor-06.js create mode 100644 devtools/client/shared/test/browser_filter-editor-07.js create mode 100644 devtools/client/shared/test/browser_filter-editor-08.js create mode 100644 devtools/client/shared/test/browser_filter-editor-09.js create mode 100644 devtools/client/shared/test/browser_filter-editor-10.js create mode 100644 devtools/client/shared/test/browser_filter-presets-01.js create mode 100644 devtools/client/shared/test/browser_filter-presets-02.js create mode 100644 devtools/client/shared/test/browser_filter-presets-03.js create mode 100644 devtools/client/shared/test/browser_flame-graph-01.js create mode 100644 devtools/client/shared/test/browser_flame-graph-02.js create mode 100644 devtools/client/shared/test/browser_flame-graph-03a.js create mode 100644 devtools/client/shared/test/browser_flame-graph-03b.js create mode 100644 devtools/client/shared/test/browser_flame-graph-03c.js create mode 100644 devtools/client/shared/test/browser_flame-graph-04.js create mode 100644 devtools/client/shared/test/browser_flame-graph-05.js create mode 100644 devtools/client/shared/test/browser_flame-graph-utils-01.js create mode 100644 devtools/client/shared/test/browser_flame-graph-utils-02.js create mode 100644 devtools/client/shared/test/browser_flame-graph-utils-03.js create mode 100644 devtools/client/shared/test/browser_flame-graph-utils-04.js create mode 100644 devtools/client/shared/test/browser_flame-graph-utils-05.js create mode 100644 devtools/client/shared/test/browser_flame-graph-utils-06.js create mode 100644 devtools/client/shared/test/browser_flame-graph-utils-hash.js create mode 100644 devtools/client/shared/test/browser_graphs-01.js create mode 100644 devtools/client/shared/test/browser_graphs-02.js create mode 100644 devtools/client/shared/test/browser_graphs-03.js create mode 100644 devtools/client/shared/test/browser_graphs-04.js create mode 100644 devtools/client/shared/test/browser_graphs-05.js create mode 100644 devtools/client/shared/test/browser_graphs-06.js create mode 100644 devtools/client/shared/test/browser_graphs-07a.js create mode 100644 devtools/client/shared/test/browser_graphs-07b.js create mode 100644 devtools/client/shared/test/browser_graphs-07c.js create mode 100644 devtools/client/shared/test/browser_graphs-07d.js create mode 100644 devtools/client/shared/test/browser_graphs-07e.js create mode 100644 devtools/client/shared/test/browser_graphs-08.js create mode 100644 devtools/client/shared/test/browser_graphs-09a.js create mode 100644 devtools/client/shared/test/browser_graphs-09b.js create mode 100644 devtools/client/shared/test/browser_graphs-09c.js create mode 100644 devtools/client/shared/test/browser_graphs-09d.js create mode 100644 devtools/client/shared/test/browser_graphs-09e.js create mode 100644 devtools/client/shared/test/browser_graphs-09f.js create mode 100644 devtools/client/shared/test/browser_graphs-10a.js create mode 100644 devtools/client/shared/test/browser_graphs-10b.js create mode 100644 devtools/client/shared/test/browser_graphs-10c.js create mode 100644 devtools/client/shared/test/browser_graphs-11a.js create mode 100644 devtools/client/shared/test/browser_graphs-11b.js create mode 100644 devtools/client/shared/test/browser_graphs-12.js create mode 100644 devtools/client/shared/test/browser_graphs-13.js create mode 100644 devtools/client/shared/test/browser_graphs-14.js create mode 100644 devtools/client/shared/test/browser_graphs-15.js create mode 100644 devtools/client/shared/test/browser_graphs-16.js create mode 100644 devtools/client/shared/test/browser_html_tooltip-01.js create mode 100644 devtools/client/shared/test/browser_html_tooltip-02.js create mode 100644 devtools/client/shared/test/browser_html_tooltip-03.js create mode 100644 devtools/client/shared/test/browser_html_tooltip-04.js create mode 100644 devtools/client/shared/test/browser_html_tooltip-05.js create mode 100644 devtools/client/shared/test/browser_html_tooltip_arrow-01.js create mode 100644 devtools/client/shared/test/browser_html_tooltip_arrow-02.js create mode 100644 devtools/client/shared/test/browser_html_tooltip_consecutive-show.js create mode 100644 devtools/client/shared/test/browser_html_tooltip_hover.js create mode 100644 devtools/client/shared/test/browser_html_tooltip_offset.js create mode 100644 devtools/client/shared/test/browser_html_tooltip_rtl.js create mode 100644 devtools/client/shared/test/browser_html_tooltip_variable-height.js create mode 100644 devtools/client/shared/test/browser_html_tooltip_width-auto.js create mode 100644 devtools/client/shared/test/browser_html_tooltip_xul-wrapper.js create mode 100644 devtools/client/shared/test/browser_inplace-editor-01.js create mode 100644 devtools/client/shared/test/browser_inplace-editor-02.js create mode 100644 devtools/client/shared/test/browser_inplace-editor_autocomplete_01.js create mode 100644 devtools/client/shared/test/browser_inplace-editor_autocomplete_02.js create mode 100644 devtools/client/shared/test/browser_inplace-editor_autocomplete_offset.js create mode 100644 devtools/client/shared/test/browser_inplace-editor_maxwidth.js create mode 100644 devtools/client/shared/test/browser_key_shortcuts.js create mode 100644 devtools/client/shared/test/browser_keycodes.js create mode 100644 devtools/client/shared/test/browser_layoutHelpers-getBoxQuads.html create mode 100644 devtools/client/shared/test/browser_layoutHelpers-getBoxQuads.js create mode 100644 devtools/client/shared/test/browser_layoutHelpers.html create mode 100644 devtools/client/shared/test/browser_layoutHelpers.js create mode 100644 devtools/client/shared/test/browser_mdn-docs-01.js create mode 100644 devtools/client/shared/test/browser_mdn-docs-02.js create mode 100644 devtools/client/shared/test/browser_mdn-docs-03.js create mode 100644 devtools/client/shared/test/browser_num-l10n.js create mode 100644 devtools/client/shared/test/browser_options-view-01.js create mode 100644 devtools/client/shared/test/browser_outputparser.js create mode 100644 devtools/client/shared/test/browser_poller.js create mode 100644 devtools/client/shared/test/browser_prefs-01.js create mode 100644 devtools/client/shared/test/browser_prefs-02.js create mode 100644 devtools/client/shared/test/browser_require_raw.js create mode 100644 devtools/client/shared/test/browser_spectrum.js create mode 100644 devtools/client/shared/test/browser_tableWidget_basic.js create mode 100644 devtools/client/shared/test/browser_tableWidget_keyboard_interaction.js create mode 100644 devtools/client/shared/test/browser_tableWidget_mouse_interaction.js create mode 100644 devtools/client/shared/test/browser_telemetry_button_eyedropper.js create mode 100644 devtools/client/shared/test/browser_telemetry_button_paintflashing.js create mode 100644 devtools/client/shared/test/browser_telemetry_button_responsive.js create mode 100644 devtools/client/shared/test/browser_telemetry_button_scratchpad.js create mode 100644 devtools/client/shared/test/browser_telemetry_sidebar.js create mode 100644 devtools/client/shared/test/browser_telemetry_toolbox.js create mode 100644 devtools/client/shared/test/browser_telemetry_toolboxtabs_canvasdebugger.js create mode 100644 devtools/client/shared/test/browser_telemetry_toolboxtabs_inspector.js create mode 100644 devtools/client/shared/test/browser_telemetry_toolboxtabs_jsdebugger.js create mode 100644 devtools/client/shared/test/browser_telemetry_toolboxtabs_jsprofiler.js create mode 100644 devtools/client/shared/test/browser_telemetry_toolboxtabs_netmonitor.js create mode 100644 devtools/client/shared/test/browser_telemetry_toolboxtabs_options.js create mode 100644 devtools/client/shared/test/browser_telemetry_toolboxtabs_shadereditor.js create mode 100644 devtools/client/shared/test/browser_telemetry_toolboxtabs_storage.js create mode 100644 devtools/client/shared/test/browser_telemetry_toolboxtabs_styleeditor.js create mode 100644 devtools/client/shared/test/browser_telemetry_toolboxtabs_webaudioeditor.js create mode 100644 devtools/client/shared/test/browser_telemetry_toolboxtabs_webconsole.js create mode 100644 devtools/client/shared/test/browser_templater_basic.html create mode 100644 devtools/client/shared/test/browser_templater_basic.js create mode 100644 devtools/client/shared/test/browser_theme.js create mode 100644 devtools/client/shared/test/browser_theme_switching.js create mode 100644 devtools/client/shared/test/browser_toolbar_basic.html create mode 100644 devtools/client/shared/test/browser_toolbar_basic.js create mode 100644 devtools/client/shared/test/browser_toolbar_tooltip.js create mode 100644 devtools/client/shared/test/browser_toolbar_webconsole_errors_count.html create mode 100644 devtools/client/shared/test/browser_toolbar_webconsole_errors_count.js create mode 100644 devtools/client/shared/test/browser_treeWidget_basic.js create mode 100644 devtools/client/shared/test/browser_treeWidget_keyboard_interaction.js create mode 100644 devtools/client/shared/test/browser_treeWidget_mouse_interaction.js create mode 100644 devtools/client/shared/test/doc_options-view.xul create mode 100644 devtools/client/shared/test/head.js create mode 100644 devtools/client/shared/test/helper_color_data.js create mode 100644 devtools/client/shared/test/helper_html_tooltip.js create mode 100644 devtools/client/shared/test/helper_inplace_editor.js create mode 100644 devtools/client/shared/test/html-mdn-css-basic-testing.html create mode 100644 devtools/client/shared/test/html-mdn-css-no-summary-or-syntax.html create mode 100644 devtools/client/shared/test/html-mdn-css-no-summary.html create mode 100644 devtools/client/shared/test/html-mdn-css-no-syntax.html create mode 100644 devtools/client/shared/test/html-mdn-css-syntax-old-style.html create mode 100644 devtools/client/shared/test/leakhunt.js create mode 100644 devtools/client/shared/test/test-actor-registry.js create mode 100644 devtools/client/shared/test/test-actor.js create mode 100644 devtools/client/shared/test/unit/.eslintrc.js create mode 100644 devtools/client/shared/test/unit/test_VariablesView_filtering-without-controller.js create mode 100644 devtools/client/shared/test/unit/test_VariablesView_getString_promise.js create mode 100644 devtools/client/shared/test/unit/test_advanceValidate.js create mode 100644 devtools/client/shared/test/unit/test_attribute-parsing-01.js create mode 100644 devtools/client/shared/test/unit/test_attribute-parsing-02.js create mode 100644 devtools/client/shared/test/unit/test_bezierCanvas.js create mode 100644 devtools/client/shared/test/unit/test_cssAngle.js create mode 100644 devtools/client/shared/test/unit/test_cssColor-01.js create mode 100644 devtools/client/shared/test/unit/test_cssColor-02.js create mode 100644 devtools/client/shared/test/unit/test_cssColor-03.js create mode 100644 devtools/client/shared/test/unit/test_cssColorDatabase.js create mode 100644 devtools/client/shared/test/unit/test_cubicBezier.js create mode 100644 devtools/client/shared/test/unit/test_escapeCSSComment.js create mode 100644 devtools/client/shared/test/unit/test_parseDeclarations.js create mode 100644 devtools/client/shared/test/unit/test_parsePseudoClassesAndAttributes.js create mode 100644 devtools/client/shared/test/unit/test_parseSingleValue.js create mode 100644 devtools/client/shared/test/unit/test_rewriteDeclarations.js create mode 100644 devtools/client/shared/test/unit/test_source-utils.js create mode 100644 devtools/client/shared/test/unit/test_suggestion-picker.js create mode 100644 devtools/client/shared/test/unit/test_undoStack.js create mode 100644 devtools/client/shared/test/unit/xpcshell.ini (limited to 'devtools/client/shared/test') 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,
`; + +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 +// in order to remove its margin and prevent shifted positions +const TEST_URI = `data:text/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,
`; + +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,
`; + +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,
`; + +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,
`; + +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,
`; +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," + + "#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,
`; + +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,
`; + +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,
`; + +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,
`; + +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,
`; + +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,
`; + +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,
`; + +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,
`; + +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,
`; + +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,
`; + +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,
`; + +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,
`; + +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()"; + +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, + + + + + test1 + test2 + test3 + test4 + + `; + +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, + + + + + test1 + test2 + test3 + test4 + + + `; + +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, + + + + + + + + test2 + + + + + + + + `; + +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 = ''; + + 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, + + + + + spacer + test1 + test2 + MIDDLE + test3 + test4 + spacer + + `; + +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, + + + + + test1 + test2 + test3 + test4 + + `; + +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 ``; +}; + +const TEST_URI = `data:text/xml;charset=UTF-8, + + + + + + ${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;")} + + `; + +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 ``; +}; + +const TEST_URI = `data:text/xml;charset=UTF-8, + + + + + + ${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;")} + + `; + +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, + + + + + test1 + test2 + test3 + test4 + + `; + +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, + + + + + + + + + + + `; + +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, + + + + + test1 + test2 + test3 + test4 + + `; + +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, + + + + + test1 + test2 + test3 + test4 + + `; + +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, + + + + + test1 + test2 + test3 + test4 + + `; + +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, + + + + + test1 + test2 + test3 + test4 + + `; + +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, + + + + + test1 + test2 + test3 + test4 + + `; + +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 is hidden"); + is(doc.querySelectorAll("input").length, 1, "Only one "); + is(doc.querySelectorAll("span").length, 2, + "Correct number of elements"); + is(doc.querySelectorAll("span.autosizer").length, 1, + "There is an autosizer element"); +} + +function testReturnCommit(doc) { + info("Testing that pressing return commits the new value"); + 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, + + + + + `; + +// 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 @@ + + +Layout Helpers + +
+
+ + +
+
+
+
+
+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.
+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.
+Praesent tristique maximus lacus, nec ultricies neque ultrices non. Phasellus vel lobortis justo.
\ 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 + +
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," + + "
"); + 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," + + "
"); + 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," + + "

browser_outputParser.js

"); + + // 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 += "" + + "" + + segment.name + ""; + } + } + + 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)", + ["", + "blur(1px) drop-shadow(0 0 0 ", + {name: "blue"}, + ") url(red.svg#blue)"]), + + 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,", + 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 + + "something.jpg" + + 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, + +
`; + +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," + + "" + + + // Uncomment these lines to help with visual debugging. When uncommented they + // dump a couple of thousand errors in the log (bug 1258285) + // "" + + // "" + + + "" + + ""; + +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," + + "" + + + // Uncomment these lines to help with visual debugging. When uncommented they + // dump a couple of thousand errors in the log (bug 1258285) + // "" + + // "" + + + "" + + ""; +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," + + "" + + + // Uncomment these lines to help with visual debugging. When uncommented they + // dump a couple of thousand errors in the log (bug 1258285) + // "" + + // "" + + + "" + + ""; +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," + + "

browser_telemetry_button_eyedropper.js

test
"; + +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," + + "

browser_telemetry_button_paintflashing.js

"; + +// 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," + + "

browser_telemetry_button_responsive.js

"; + +// 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," + + "

browser_telemetry_button_scratchpad.js

"; + +// 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,

browser_telemetry_sidebar.js

"; + +// 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," + + "

browser_telemetry_toolbox.js

"; + +// 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," + + "

browser_telemetry_toolboxtabs_canvasdebugger.js

"; + +// 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," + + "

browser_telemetry_toolboxtabs_inspector.js

"; + +// 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," + + "

browser_telemetry_toolboxtabs_jsdebugger.js

"; + +// 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," + + "

browser_telemetry_toolboxtabs_jsprofiler.js

"; + +// 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," + + "

browser_telemetry_toolboxtabs_netmonitor.js

"; + +// 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," + + "

browser_telemetry_toolboxtabs_options.js

"; + +// 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," + + "

browser_telemetry_toolboxtabs_shadereditor.js

"; + +// 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," + + "

browser_telemetry_toolboxtabs_storage.js

"; + +// 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," + + "

browser_telemetry_toolboxtabs_styleeditor.js

"; + +// 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," + + "

browser_telemetry_toolboxtabs_webaudioeditor.js

"; + +// 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," + + "

browser_telemetry_toolboxtabs_styleeditor_webconsole.js

"; + +// 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 @@ + + + + + + DOM Template Tests + + + + + + 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: '
${nested.value}
', + data: { nested: { value: "pass 1" } }, + result: '
pass 1
' + }), + + () => ({ + name: "returnDom", + template: '
${__element.ownerDocument.createTextNode(\'pass 2\')}
', + options: { allowEval: true }, + data: {}, + result: '
pass 2
' + }), + + () => ({ + name: "srcChange", + template: '', + data: { fred: "green.png" }, + result: // + }), + + () => ({ + name: "ifTrue", + template: '

hello ${name}

', + options: { allowEval: true }, + data: { name: "fred" }, + result: "

hello fred

" + }), + + () => ({ + name: "ifFalse", + template: '

hello ${name}

', + options: { allowEval: true }, + data: { name: "jim" }, + result: "" + }), + + () => ({ + name: "simpleLoop", + template: '

${index}

', + options: { allowEval: true }, + data: {}, + result: "

1

2

3

" + }), + + () => ({ + name: "loopElement", + template: '${i}', + 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: '${i}', + data: { array: delayReply([1, 2, 3]) }, + result: "", + later: "123" + }), + + () => ({ + name: "saveElement", + template: '

${name}

', + data: { name: "pass 8" }, + result: "

pass 8

", + also: function (options) { + ok(options.data.element.innerHTML, "pass 9", "saveElement saved"); + delete options.data.element; + } + }), + + () => ({ + name: "useElement", + template: '

${adjust(__element)}

', + options: { allowEval: true }, + data: { + adjust: function (element) { + is("pass9", element.id, "useElement adjust"); + return "pass 9b"; + } + }, + result: '

pass 9b

' + }), + + () => ({ + name: "asyncInline", + template: "${delayed}", + data: { delayed: delayReply("inline") }, + result: "", + later: "inline" + }), + + // Bug 692028: DOMTemplate memory leak with asynchronous arrays + () => ({ + name: "asyncArray", + template: '

${i}

', + data: { delayed: delayReply([1, 2, 3]) }, + result: "", + later: "

1

2

3

" + }), + + () => ({ + name: "asyncMember", + template: '

${i}

', + data: { delayed: [delayReply(4), delayReply(5), delayReply(6)] }, + result: "", + later: "

4

5

6

" + }), + + // Bug 692028: DOMTemplate memory leak with asynchronous arrays + () => ({ + name: "asyncBoth", + template: '

${i}

', + data: { + delayed: delayReply([ + delayReply(4), + delayReply(5), + delayReply(6) + ]) + }, + result: "", + later: "

4

5

6

" + }), + + // Bug 701762: DOMTemplate fails when ${foo()} returns undefined + () => ({ + name: "functionReturningUndefiend", + template: "

${foo()}

", + options: { allowEval: true }, + data: { + foo: function () {} + }, + result: "

undefined

" + }), + + // Bug 702642: DOMTemplate is relatively slow when evaluating JS ${} + () => ({ + name: "propertySimple", + template: "

${a.b.c}

", + data: { a: { b: { c: "hello" } } }, + result: "

hello

" + }), + + () => ({ + name: "propertyPass", + template: "

${Math.max(1, 2)}

", + options: { allowEval: true }, + result: "

2

" + }), + + () => ({ + name: "propertyFail", + template: "

${Math.max(1, 2)}

", + result: "

${Math.max(1, 2)}

" + }), + + // Bug 723431: DOMTemplate should allow customisation of display of + // null/undefined values + () => ({ + name: "propertyUndefAttrFull", + template: "

${nullvar}|${undefinedvar1}|${undefinedvar2}

", + data: { nullvar: null, undefinedvar1: undefined }, + result: "

null|undefined|undefined

" + }), + + () => ({ + name: "propertyUndefAttrBlank", + template: "

${nullvar}|${undefinedvar1}|${undefinedvar2}

", + data: { nullvar: null, undefinedvar1: undefined }, + options: { blankNullUndefined: true }, + result: "

||

" + }), + + /* eslint-disable max-len */ + () => ({ + name: "propertyUndefAttrFull", + template: '

', + data: { nullvar: null, undefinedvar1: undefined }, + result: '

' + }), + + () => ({ + name: "propertyUndefAttrBlank", + template: '

', + data: { nullvar: null, undefinedvar1: undefined }, + options: { blankNullUndefined: true }, + result: '

' + }) + /* 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 @@ + + + + + + + Developer Toolbar Tests + + + + + +

+1 +

+ +

+2a +

+ +

+2b +

+ + + + + + 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,

Tooltip Tests

"; +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 @@ + + + + + + Developer Toolbar Tests - errors count in the Web Console button + + + + +

Hello world! Test for errors count in the Web Console button (developer + toolbar).

+

+ + + 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," + + "
"; +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," + + "
"; +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," + + "
"; +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 @@ + + + + + + + + + + + + + + + +