diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /devtools/client/webconsole/test/browser | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/webconsole/test/browser')
509 files changed, 36867 insertions, 0 deletions
diff --git a/devtools/client/webconsole/test/browser/.eslintrc.js b/devtools/client/webconsole/test/browser/.eslintrc.js new file mode 100644 index 0000000000..a0fb3e89e9 --- /dev/null +++ b/devtools/client/webconsole/test/browser/.eslintrc.js @@ -0,0 +1,14 @@ +"use strict"; + +module.exports = { + // Extend from the shared list of defined globals for mochitests. + extends: "../../../../.eslintrc.mochitests.js", + overrides: [ + { + files: ["test-dynamic-import.js", "test-error-worklet.js"], + parserOptions: { + sourceType: "module", + }, + }, + ], +}; diff --git a/devtools/client/webconsole/test/browser/_browser_console.ini b/devtools/client/webconsole/test/browser/_browser_console.ini new file mode 100644 index 0000000000..ff96738def --- /dev/null +++ b/devtools/client/webconsole/test/browser/_browser_console.ini @@ -0,0 +1,49 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + head.js + test-console-iframes.html + test-console.html + test-cu-reporterror.js + test-iframe1.html + test-iframe2.html + test-iframe3.html + test-image.png + test-image.png^headers^ + !/devtools/client/shared/test/shared-head.js + !/devtools/client/debugger/test/mochitest/helpers.js + !/devtools/client/debugger/test/mochitest/helpers/context.js + !/devtools/client/framework/browser-toolbox/test/helpers-browser-toolbox.js + !/devtools/client/shared/test/telemetry-test-helpers.js + +[browser_console.js] +[browser_console_chrome_context_message.js] +[browser_console_clear_cache.js] +[browser_console_clear_closed_tab.js] +[browser_console_clear_method.js] +skip-if = true # Bug 1437843 +[browser_console_consolejsm_output.js] +[browser_console_content_getters.js] +[browser_console_content_longstring.js] +[browser_console_content_object_context_menu.js] +[browser_console_content_object_in_sidebar.js] +[browser_console_content_object.js] +[browser_console_context_menu_entries.js] +skip-if = (os == "linux") || (os == "win") || (os == "mac" && !debug) # Bug 1440059, disabled for all build types, Bug 1609460 +[browser_console_dead_objects.js] +[browser_console_devtools_loader_exception.js] +[browser_console_eager_eval.js] +[browser_console_error_source_click.js] +[browser_console_filters.js] +[browser_console_jsterm_await.js] +[browser_console_nsiconsolemessage.js] +[browser_console_open_or_focus.js] +[browser_console_restore.js] +skip-if = verify +[browser_console_webconsole_console_api_calls.js] +[browser_console_webconsole_ctrlw_close_tab.js] +[browser_console_webconsole_iframe_messages.js] +[browser_console_webconsole_private_browsing.js] +[browser_toolbox_console_new_process.js] +skip-if = asan || debug || ccov # Bug 1591590 diff --git a/devtools/client/webconsole/test/browser/_jsterm.ini b/devtools/client/webconsole/test/browser/_jsterm.ini new file mode 100644 index 0000000000..6386d5d791 --- /dev/null +++ b/devtools/client/webconsole/test/browser/_jsterm.ini @@ -0,0 +1,152 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + head.js + test-autocomplete-in-stackframe.html + test-block-action.html + test-block-action-style.css + test-console-evaluation-context-selector-child.html + test-console-evaluation-context-selector.html + test-console.html + test-dynamic-import.html + test-dynamic-import.js + test-iframe-child.html + test-iframe-parent.html + test_jsterm_screenshot_command.html + test-mangled-function.js + test-mangled-function.js.map + test-mangled-function.src.js + test-simple-function.html + test-simple-function.js + !/devtools/client/shared/test/shared-head.js + !/devtools/client/debugger/test/mochitest/helpers.js + !/devtools/client/debugger/test/mochitest/helpers/context.js + !/devtools/client/framework/browser-toolbox/test/helpers-browser-toolbox.js + !/devtools/client/shared/test/telemetry-test-helpers.js + !/devtools/client/shared/test/test-actor.js + ../../../../../toolkit/components/reader/test/readerModeArticle.html + +[browser_jsterm_add_edited_input_to_history.js] +[browser_jsterm_autocomplete_accept_no_scroll.js] +[browser_jsterm_autocomplete_array_no_index.js] +[browser_jsterm_autocomplete_arrow_keys.js] +skip-if = debug && (os == "win" && bits == 32) #bug 1620638 +[browser_jsterm_autocomplete_await.js] +[browser_jsterm_autocomplete_toggle.js] +[browser_jsterm_autocomplete_cached_results.js] +[browser_jsterm_autocomplete_commands.js] +[browser_jsterm_autocomplete_control_space.js] +[browser_jsterm_autocomplete_crossdomain_iframe.js] +[browser_jsterm_autocomplete_disabled.js] +[browser_jsterm_autocomplete_eager_evaluation.js] +[browser_jsterm_autocomplete_del_key.js] +[browser_jsterm_autocomplete_escape_key.js] +[browser_jsterm_autocomplete_expression_variables.js] +[browser_jsterm_autocomplete_extraneous_closing_brackets.js] +[browser_jsterm_autocomplete_getters_cache.js] +[browser_jsterm_autocomplete_getters_cancel.js] +[browser_jsterm_autocomplete_getters_confirm.js] +[browser_jsterm_autocomplete_getters_learn_more_link.js] +[browser_jsterm_autocomplete_helpers.js] +[browser_jsterm_autocomplete_in_chrome_tab.js] +[browser_jsterm_autocomplete_in_debugger_stackframe.js] +skip-if = (os == "win" && os_version == "6.1") # Bug 1620521 +[browser_jsterm_autocomplete_inside_text.js] +skip-if = (os == "win" && os_version == "6.1") # Bug 1620521 +[browser_jsterm_autocomplete_native_getters.js] +[browser_jsterm_autocomplete_nav_and_tab_key.js] +[browser_jsterm_autocomplete_null.js] +[browser_jsterm_autocomplete_paste_undo.js] +[browser_jsterm_autocomplete_race_on_enter.js] +[browser_jsterm_autocomplete_return_key_no_selection.js] +[browser_jsterm_autocomplete_return_key.js] +[browser_jsterm_autocomplete_width.js] +[browser_jsterm_autocomplete_will_navigate.js] +[browser_jsterm_autocomplete-properties-with-non-alphanumeric-names.js] +skip-if = debug && (os == "win" && bits == 32) # Bug 1620856 +[browser_jsterm_await_assignments.js] +[browser_jsterm_await_concurrent_same_result.js] +[browser_jsterm_await_concurrent.js] +[browser_jsterm_await_dynamic_import.js] +[browser_jsterm_await_error.js] +[browser_jsterm_await_helper_dollar_underscore.js] +[browser_jsterm_await_paused.js] +skip-if = debug # crashes on "Unexpected UpdateTransformLayer hint" bug 1570685 +[browser_jsterm_await.js] +[browser_jsterm_block_command.js] +[browser_jsterm_completion_bracket_cached_results.js] +[browser_jsterm_completion_bracket.js] +skip-if = debug && (os == "win" && os_version == "6.1") # Bug 1620724 +[browser_jsterm_completion_case_sensitivity.js] +[browser_jsterm_completion_perfect_match.js] +[browser_jsterm_completion_dollar_underscore.js] +[browser_jsterm_completion_dollar_zero.js] +[browser_jsterm_completion.js] +[browser_jsterm_content_defined_helpers.js] +[browser_jsterm_context_menu_labels.js] +skip-if = (os == "win" && processor == "aarch64") # disabled on aarch64 due to 1531571 +[browser_jsterm_copy_command.js] +[browser_jsterm_ctrl_a_select_all.js] +[browser_jsterm_ctrl_key_nav.js] +skip-if = os != 'mac' # The tested ctrl+key shortcuts are OSX only +[browser_jsterm_document_no_xray.js] +[browser_jsterm_eager_evaluation_element_highlight.js] +[browser_jsterm_eager_evaluation_in_debugger_stackframe.js] +[browser_jsterm_eager_evaluation_on_webextension_target.js] +[browser_jsterm_eager_evaluation_warnings.js] +[browser_jsterm_eager_evaluation.js] +[browser_jsterm_editor.js] +[browser_jsterm_editor_code_folding.js] +[browser_jsterm_editor_disabled_history_nav_with_keyboard.js] +[browser_jsterm_editor_enter.js] +[browser_jsterm_editor_execute_selection.js] +[browser_jsterm_editor_execute.js] +[browser_jsterm_editor_gutter.js] +[browser_jsterm_editor_onboarding.js] +[browser_jsterm_editor_toggle_keyboard_shortcut.js] +[browser_jsterm_editor_resize.js] +[browser_jsterm_editor_reverse_search_button.js] +[browser_jsterm_editor_reverse_search_keyboard_navigation.js] +[browser_jsterm_editor_toolbar.js] +[browser_jsterm_error_docs.js] +[browser_jsterm_error_outside_valid_range.js] +[browser_jsterm_evaluation_context_selector_pause_in_debugger.js] +skip-if = !fission # context selector is only visible when fission is enabled. +[browser_jsterm_evaluation_context_selector_targets_update.js] +skip-if = !fission # context selector is only visible when fission is enabled. +[browser_jsterm_evaluation_context_selector_inspector.js] +skip-if = !fission # context selector is only visible when fission is enabled. +[browser_jsterm_evaluation_context_selector.js] +skip-if = !fission # context selector is only visible when fission is enabled. +[browser_jsterm_file_load_save_keyboard_shortcut.js] +[browser_jsterm_focus_reload.js] +[browser_jsterm_helper_clear.js] +[browser_jsterm_helper_dollar_dollar.js] +[browser_jsterm_helper_dollar_x.js] +[browser_jsterm_helper_dollar.js] +[browser_jsterm_helper_help.js] +[browser_jsterm_helper_keys_values.js] +[browser_jsterm_hide_when_devtools_chrome_enabled_false.js] +[browser_jsterm_history.js] +[browser_jsterm_history_arrow_keys.js] +[browser_jsterm_history_nav.js] +[browser_jsterm_history_persist.js] +[browser_jsterm_insert_tab_when_overflows_no_scroll.js] +[browser_jsterm_inspect.js] +[browser_jsterm_inspect_panels.js] +[browser_jsterm_instance_of.js] +[browser_jsterm_middle_click_paste.js] +[browser_jsterm_multiline.js] +[browser_jsterm_no_input_and_tab_key_pressed.js] +skip-if = (os == "win" && processor == "aarch64") # disabled on aarch64 due to 1531573 +[browser_jsterm_null_undefined.js] +[browser_jsterm_popup_close_on_tab_switch.js] +[browser_jsterm_screenshot_command_clipboard.js] +[browser_jsterm_screenshot_command_user.js] +[browser_jsterm_screenshot_command_file.js] +[browser_jsterm_screenshot_command_warnings.js] +skip-if = (os == "win" && os_version == "6.1") # Getting the clipboard image dimensions throws an exception +[browser_jsterm_selfxss.js] +[browser_jsterm_syntax_highlight_output.js] +skip-if = (os == "win" && processor == "aarch64") # disabled on aarch64 due to 1531574 diff --git a/devtools/client/webconsole/test/browser/_webconsole.ini b/devtools/client/webconsole/test/browser/_webconsole.ini new file mode 100644 index 0000000000..b6afc7ec4b --- /dev/null +++ b/devtools/client/webconsole/test/browser/_webconsole.ini @@ -0,0 +1,402 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + code_bundle_invalidmap.js + code_bundle_invalidmap.js.map + code_bundle_nosource.js + code_bundle_nosource.js.map + cookieSetter.html + head.js + sjs_cors-test-server.sjs + sjs_slow-response-test-server.sjs + source-mapped.css + source-mapped.css.map + source-mapped.scss + stub-generator-helpers.js + test_console_csp_ignore_reflected_xss_message.html + test_console_csp_ignore_reflected_xss_message.html^headers^ + test_hsts-invalid-headers.sjs + test-batching.html + test-console-trace-duplicates.html + test-console-api-iframe.html + test-console-api.html + test-csp-violation.html + test-csp-violation-inline.html + test-csp-violation-inline.html^headers^ + test-csp-violation-base-uri.html + test-csp-violation-base-uri.html^headers^ + test-csp-violation-form-action.html + test-csp-violation-form-action.html^headers^ + test-csp-violation-frame-ancestor-child.html^headers^ + test-csp-violation-frame-ancestor-child.html + test-csp-violation-frame-ancestor-parent.html^headers^ + test-csp-violation-frame-ancestor-parent.html + test-cspro.html + test-cspro.html^headers^ + test-iframe-child.html + test-iframe-parent.html + test-certificate-messages.html + test-checkloaduri-failure.html + test-click-function-to-source.html + test-click-function-to-source.js + test-click-function-to-mapped-source.html + test-click-function-to-prettyprinted-source.html + test-click-function-to-source.min.js + test-click-function-to-source.unmapped.min.js + test-click-function-to-source.min.js.map + test-closure-optimized-out.html + test-console-filters.html + test-console-filter-by-regex-input.html + test-console-filter-groups.html + test-console-group.html + test-console-iframes.html + test-console-stacktrace-mapped.html + test-console-table.html + test-console-workers.html + test-console.html + test-cu-reporterror.js + test-data.json + test-data.json^headers^ + test-duplicate-error.html + test-dynamic-import.html + test-dynamic-import.js + test-error.html + test-error-worker.html + test-error-worker.js + test-error-worker2.js + test-error-worklet.html + test-error-worklet.js + test-eval-error.html + test-eval-in-stackframe.html + test-eval-sources.html + test-evaluate-worker.html + test-evaluate-worker.js + test-external-script-errors.html + test-external-script-errors.js + test-iframe-insecure-form-action.html + test-iframe1.html + test-iframe2.html + test-iframe3.html + test-iframe-wrong-hud-iframe.html + test-iframe-wrong-hud.html + test-image.png + test-image.png^headers^ + test-ineffective-iframe-sandbox-warning-inner.html + test-ineffective-iframe-sandbox-warning-nested1.html + test-ineffective-iframe-sandbox-warning-nested2.html + test-ineffective-iframe-sandbox-warning0.html + test-ineffective-iframe-sandbox-warning1.html + test-ineffective-iframe-sandbox-warning2.html + test-ineffective-iframe-sandbox-warning3.html + test-ineffective-iframe-sandbox-warning4.html + test-ineffective-iframe-sandbox-warning5.html + test-insecure-frame.html + test-insecure-passwords-about-blank-web-console-warning.html + test-insecure-passwords-web-console-warning.html + test-inspect-cross-domain-objects-frame.html + test-inspect-cross-domain-objects-top.html + test-local-session-storage.html + test-location-debugger-link-console-log.js + test-location-debugger-link-errors.js + test-location-debugger-link.html + test-location-debugger-link-logpoint-1.js + test-location-debugger-link-logpoint-2.js + test-location-debugger-link-logpoint.html + test-location-styleeditor-link-1.css + test-location-styleeditor-link-2.css + test-location-styleeditor-link-minified.css + test-location-styleeditor-link.html + test-message-categories-canvas-css.html + test-message-categories-canvas-css.js + test-message-categories-css-loader.css + test-message-categories-css-loader.css^headers^ + test-message-categories-css-loader.html + test-message-categories-css-parser.css + test-message-categories-css-parser.html + test-message-categories-empty-getelementbyid.html + test-message-categories-empty-getelementbyid.js + test-message-categories-html.html + test-message-categories-image.html + test-message-categories-image.jpg + test-message-categories-imagemap.html + test-message-categories-malformedxml-external.html + test-message-categories-malformedxml-external.xml + test-message-categories-malformedxml.xhtml + test-message-categories-svg.xhtml + test-message-categories-workers.html + test-message-categories-workers.js + test-mixedcontent-securityerrors.html + test-navigate-to-parse-error.html + test-nested-iframe-storageaccess-errors.html + test-network-exceptions.html + test-network-request.html + test-network.html + test-non-javascript-mime.html + test-non-javascript-mime.js + test-non-javascript-mime.js^headers^ + test-non-javascript-mime-worker.html + test-reopen-closed-tab.html + test-same-origin-required-load.html + test-sourcemap-error-01.html + test-sourcemap-error-01.js + test-sourcemap-error-02.html + test-sourcemap-error-02.js + test-sourcemap-original.js + test-sourcemap.min.js + test-sourcemap.min.js.map + test-stacktrace-location-debugger-link.html + test-storageaccess-errors.html + test-subresource-security-error.html + test-subresource-security-error.js + test-subresource-security-error.js^headers^ + test-syntaxerror-worklet.js + test-time-methods.html + test-trackingprotection-securityerrors.html + test-trackingprotection-securityerrors-thirdpartyonly.html + test-warning-groups.html + test-websocket.html + test-websocket.js + test-worker-promise-error.html + testscript.js + !/devtools/client/netmonitor/test/sjs_cors-test-server.sjs + !/image/test/mochitest/blue.png + !/devtools/client/shared/test/shared-head.js + !/devtools/client/debugger/test/mochitest/helpers.js + !/devtools/client/debugger/test/mochitest/helpers/context.js + !/devtools/client/framework/browser-toolbox/test/helpers-browser-toolbox.js + !/devtools/client/shared/test/telemetry-test-helpers.js + !/devtools/client/shared/test/test-actor.js + !/devtools/client/webconsole/test/node/fixtures/stubs/consoleApi.js + !/devtools/client/webconsole/test/node/fixtures/stubs/cssMessage.js + !/devtools/client/webconsole/test/node/fixtures/stubs/evaluationResult.js + !/devtools/client/webconsole/test/node/fixtures/stubs/index.js + !/devtools/client/webconsole/test/node/fixtures/stubs/networkEvent.js + !/devtools/client/webconsole/test/node/fixtures/stubs/pageError.js + !/devtools/client/webconsole/test/node/fixtures/stubs/platformMessage.js + +[browser_webconsole_allow_mixedcontent_securityerrors.js] +tags = mcb +[browser_webconsole_async_stack.js] +[browser_webconsole_batching.js] +[browser_webconsole_block_mixedcontent_securityerrors.js] +tags = mcb +[browser_webconsole_cached_messages_cross_domain_iframe.js] +[browser_webconsole_cached_messages_no_duplicate.js] +[browser_webconsole_cached_messages.js] +[browser_webconsole_certificate_messages.js] +[browser_webconsole_checkloaduri_errors.js] +[browser_webconsole_clear_cache.js] +[browser_webconsole_click_function_to_source.js] +[browser_webconsole_click_function_to_mapped_source.js] +[browser_webconsole_click_function_to_prettyprinted_source.js] +[browser_webconsole_clickable_urls.js] +[browser_webconsole_close_unfocused_window.js] +[browser_webconsole_closing_after_completion.js] +[browser_webconsole_close_groups_after_navigation.js] +[browser_webconsole_close_sidebar.js] +skip-if = true # Bug 1405250 +[browser_webconsole_console_api_iframe.js] +[browser_webconsole_console_dir.js] +[browser_webconsole_console_dir_uninspectable.js] +[browser_webconsole_console_error_expand_object.js] +[browser_webconsole_console_group_open_no_scroll.js] +[browser_webconsole_console_group.js] +[browser_webconsole_console_logging_workers_api.js] +[browser_webconsole_console_table_post_alterations.js] +[browser_webconsole_console_table.js] +[browser_webconsole_console_timeStamp.js] +[browser_webconsole_console_trace_distinct.js] +[browser_webconsole_console_trace_duplicates.js] +[browser_webconsole_context_menu_export_console_output.js] +[browser_webconsole_context_menu_copy_entire_message.js] +skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts +[browser_webconsole_context_menu_copy_link_location.js] +skip-if = (os == 'linux' && bits == 32 && debug) || (os == 'linux') # bug 1328915, disable linux32 debug devtools for timeouts, bug 1473120 +[browser_webconsole_context_menu_copy_message_with_async_stacktrace.js] +[browser_webconsole_context_menu_copy_message_with_framework_stacktrace.js] +[browser_webconsole_context_menu_copy_object.js] +[browser_webconsole_context_menu_object_in_sidebar.js] +[browser_webconsole_context_menu_open_url.js] +[browser_webconsole_context_menu_store_as_global.js] +[browser_webconsole_context_menu_reveal_in_inspector.js] +[browser_webconsole_cors_errors.js] +[browser_webconsole_csp_ignore_reflected_xss_message.js] +[browser_webconsole_csp_violation.js] +[browser_webconsole_cspro.js] +[browser_webconsole_css_error_impacted_elements.js] +[browser_webconsole_document_focus.js] +[browser_webconsole_duplicate_errors.js] +[browser_webconsole_error_with_grouped_stack.js] +[browser_webconsole_error_with_longstring_stack.js] +[browser_webconsole_error_with_unicode.js] +[browser_webconsole_error_with_url.js] +[browser_webconsole_errors_after_page_reload.js] +[browser_webconsole_eval_error.js] +[browser_webconsole_eval_in_debugger_stackframe.js] +[browser_webconsole_eval_in_debugger_stackframe2.js] +skip-if = !debug && ((os == "linux" && bits == 64) || (os == "mac" && os_version == "10.14") || (os == "win")) #Bug 1598205 +[browser_webconsole_eval_sources.js] +fail-if = a11y_checks # bug 1687728 frame-link-filename is not accessible +[browser_webconsole_execution_scope.js] +[browser_webconsole_external_script_errors.js] +[browser_webconsole_file_uri.js] +skip-if = true # Bug 1404382 +[browser_webconsole_filter_buttons_overflow.js] +[browser_webconsole_filter_by_input.js] +[browser_webconsole_filter_by_regex_input.js] +[browser_webconsole_filter_groups.js] +[browser_webconsole_filter_navigation_marker.js] +[browser_webconsole_filter_scroll.js] +[browser_webconsole_filters.js] +[browser_webconsole_filters_persist.js] +[browser_webconsole_highlighter_console_helper.js] +[browser_webconsole_hsts_invalid-headers.js] +[browser_webconsole_iframe_wrong_hud.js] +[browser_webconsole_ineffective_iframe_sandbox_warning.js] +[browser_webconsole_in_line_layout.js] +[browser_webconsole_init.js] +[browser_webconsole_input_field_focus_on_panel_select.js] +[browser_webconsole_input_focus.js] +[browser_webconsole_insecure_passwords_about_blank_web_console_warning.js] +[browser_webconsole_insecure_passwords_web_console_warning.js] +[browser_webconsole_inspect_cross_domain_object.js] +[browser_webconsole_keyboard_accessibility.js] +[browser_webconsole_limit_multiline.js] +[browser_webconsole_location_debugger_link.js] +fail-if = a11y_checks # bug 1687728 frame-link-filename is not accessible +[browser_webconsole_location_logpoint_debugger_link.js] +skip-if = ccov #Bug 1594897 +fail-if = a11y_checks # bug 1687728 frame-link-filename is not accessible +[browser_webconsole_location_styleeditor_link.js] +fail-if = a11y_checks # bug 1687728 frame-link-filename is not accessible +[browser_webconsole_logErrorInPage.js] +[browser_webconsole_loglimit.js] +[browser_webconsole_logWarningInPage.js] +[browser_webconsole_longstring_getter.js] +[browser_webconsole_longstring.js] +[browser_webconsole_message_categories.js] +[browser_webconsole_mime_css_blocked.js] +[browser_webconsole_multiple_windows_and_tabs.js] +[browser_webconsole_navigate_to_parse_error.js] +[browser_webconsole_network_attach.js] +[browser_webconsole_network_exceptions.js] +[browser_webconsole_network_message_close_on_escape.js] +[browser_webconsole_network_message_ctrl_click.js] +fail-if = a11y_checks # bug 1687736 cm-s-mozilla message is not accessible +[browser_webconsole_network_messages_expand.js] +skip-if = true # Bug 1438979 +[browser_webconsole_network_messages_openinnet.js] +fail-if = fission # Bug 1613081 +[browser_webconsole_network_messages_resend_request.js] +fail-if = fission # Bug 1613081 +[browser_webconsole_network_messages_stacktrace_console_initiated_request.js] +[browser_webconsole_network_messages_status_code.js] +[browser_webconsole_network_requests_from_chrome.js] +[browser_webconsole_network_reset_filter.js] +skip-if = (os == "linux" && fission && !ccov) || (os == "win" && fission) #Bug 1601331 +[browser_webconsole_nodes_highlight.js] +[browser_webconsole_nodes_select.js] +[browser_webconsole_non_javascript_mime_warning.js] +[browser_webconsole_non_javascript_mime_worker_error.js] +[browser_webconsole_object_ctrl_click.js] +[browser_webconsole_object_in_sidebar_keyboard_nav.js] +[browser_webconsole_object_inspector.js] +[browser_webconsole_object_inspector__proto__.js] +[browser_webconsole_object_inspector_entries.js] +[browser_webconsole_object_inspector_getters.js] +[browser_webconsole_object_inspector_getters_prototype.js] +[browser_webconsole_object_inspector_getters_shadowed.js] +[browser_webconsole_object_inspector_key_sorting.js] +[browser_webconsole_object_inspector_local_session_storage.js] +[browser_webconsole_object_inspector_nested_promise.js] +[browser_webconsole_object_inspector_nested_proxy.js] +[browser_webconsole_object_inspector_selected_text.js] +[browser_webconsole_object_inspector_scroll.js] +[browser_webconsole_object_inspector_symbols.js] +[browser_webconsole_object_inspector_while_debugging_and_inspecting.js] +[browser_webconsole_observer_notifications.js] +[browser_webconsole_optimized_out_vars.js] +[browser_webconsole_output_copy.js] +[browser_webconsole_output_copy_newlines.js] +[browser_webconsole_output_order.js] +[browser_webconsole_output_trimmed.js] +[browser_webconsole_persist.js] +[browser_webconsole_promise_rejected_object.js] +[browser_webconsole_reopen_closed_tab.js] +[browser_webconsole_repeat_different_objects.js] +[browser_webconsole_requestStorageAccess_errors.js] +skip-if = (os == 'win' && debug) #Bug 1653057 +[browser_webconsole_responsive_design_mode.js] +[browser_webconsole_reverse_search.js] +[browser_webconsole_reverse_search_initial_value.js] +[browser_webconsole_reverse_search_keyboard_navigation.js] +[browser_webconsole_reverse_search_mouse_navigation.js] +[browser_webconsole_reverse_search_toggle.js] +[browser_webconsole_same_origin_errors.js] +[browser_webconsole_sandbox_update_after_navigation.js] +[browser_webconsole_script_errordoc_urls.js] +[browser_webconsole_scroll.js] +[browser_webconsole_select_all.js] +[browser_webconsole_show_subresource_security_errors.js] +skip-if = verify +[browser_webconsole_shows_reqs_from_netmonitor.js] +skip-if = fission # Bug 1613081 +[browser_webconsole_shows_reqs_in_netmonitor.js] +fail-if = fission # Bug 1613081 +[browser_webconsole_sidebar_object_expand_when_message_pruned.js] +[browser_webconsole_sidebar_scroll.js] +[browser_webconsole_sourcemap_css.js] +[browser_webconsole_sourcemap_error.js] +[browser_webconsole_sourcemap_invalid.js] +[browser_webconsole_sourcemap_nosource.js] +skip-if = verify +fail-if = a11y_checks # bug 1687728 frame-link-filename is not accessible +[browser_webconsole_split.js] +[browser_webconsole_split_close_button.js] +[browser_webconsole_split_escape_key.js] +[browser_webconsole_split_focus.js] +[browser_webconsole_split_persist.js] +[browser_webconsole_stacktrace_location_debugger_link.js] +[browser_webconsole_stacktrace_mapped_location_debugger_link.js] +[browser_webconsole_strict_mode_errors.js] +[browser_webconsole_string.js] +[browser_webconsole_stubs_console_api.js] +[browser_webconsole_stubs_css_message.js] +[browser_webconsole_stubs_evaluation_result.js] +[browser_webconsole_stubs_network_event.js] +[browser_webconsole_stubs_page_error.js] +[browser_webconsole_stubs_platform_messages.js] +[browser_webconsole_telemetry_execute_js.js] +[browser_webconsole_telemetry_js_errors.js] +[browser_webconsole_telemetry_filters_changed.js] +[browser_webconsole_telemetry_persist_toggle_changed.js] +[browser_webconsole_telemetry_jump_to_definition.js] +[browser_webconsole_telemetry_object_expanded.js] +[browser_webconsole_telemetry_reverse_search.js] +[browser_webconsole_time_methods.js] +[browser_webconsole_timestamps.js] +[browser_webconsole_trackingprotection_errors.js] +skip-if = fission #Bug 1535451 +tags = trackingprotection +[browser_webconsole_uncaught_exception.js] +[browser_webconsole_view_source.js] +[browser_webconsole_visibility_messages.js] +[browser_webconsole_warn_about_replaced_api.js] +[browser_webconsole_warning_group_content_blocking.js] +skip-if = fission # Bug 1654522 +[browser_webconsole_warning_group_storage_isolation.js] +skip-if = fission || (os == 'win' && debug) # Bug 1654522, 1653057 +[browser_webconsole_warning_group_cookies.js] +[browser_webconsole_warning_groups_filtering.js] +skip-if = (os == "win" && bits == 32) && !debug # Bug 1560261 +[browser_webconsole_warning_group_multiples.js] +[browser_webconsole_warning_groups_outside_console_group.js] +[browser_webconsole_warning_groups_toggle.js] +[browser_webconsole_wasm_errors.js] +[browser_webconsole_warning_groups.js] +[browser_webconsole_websocket.js] +[browser_webconsole_worker_error.js] +[browser_webconsole_worker_evaluate.js] +[browser_webconsole_worker_promise_error.js] +[browser_webconsole_worklet_error.js] diff --git a/devtools/client/webconsole/test/browser/browser_console.js b/devtools/client/webconsole/test/browser/browser_console.js new file mode 100644 index 0000000000..59b779f58d --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_console.js @@ -0,0 +1,134 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test the basic features of the Browser Console. + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-console.html?" + + Date.now(); +const TEST_FILE = + "chrome://mochitests/content/browser/devtools/client/" + + "webconsole/test/browser/" + + "test-cu-reporterror.js"; + +const TEST_XHR_ERROR_URI = `http://example.com/404.html?${Date.now()}`; + +const TEST_IMAGE = + "http://example.com/browser/devtools/client/webconsole/" + + "test/test-image.png"; + +add_task(async function() { + // Needed for the execute() function below + await pushPref("security.allow_parent_unrestricted_js_loads", true); + await pushPref("devtools.browserconsole.contentMessages", true); + // Bug 1605036: Disable Multiprocess Browser Toolbox for now as it introduces intermittent failure in this test + await pushPref("devtools.browsertoolbox.fission", false); + + await addTab(TEST_URI); + + const opened = waitForBrowserConsole(); + + let hud = BrowserConsoleManager.getBrowserConsole(); + ok(!hud, "browser console is not open"); + info("wait for the browser console to open with ctrl-shift-j"); + EventUtils.synthesizeKey("j", { accelKey: true, shiftKey: true }, window); + + hud = await opened; + ok(hud, "browser console opened"); + + await setFilterState(hud, { + netxhr: true, + }); + + await testMessages(hud); + await resetFilters(hud); +}); + +async function testMessages(hud) { + await clearOutput(hud); + + executeSoon(() => { + expectUncaughtException(); + // eslint-disable-next-line no-undef + foobarException(); + }); + + // Add a message from a chrome window. + hud.iframeWindow.console.log("message from chrome window"); + + // Check Cu.reportError stack. + // Use another js script to not depend on the test file line numbers. + Services.scriptloader.loadSubScript(TEST_FILE, hud.iframeWindow); + + const sandbox = new Cu.Sandbox(null, { + wantComponents: false, + wantGlobalProperties: ["URL", "URLSearchParams"], + }); + const error = Cu.evalInSandbox( + `new Error("error from nuked globals");`, + sandbox + ); + Cu.reportError(error); + Cu.nukeSandbox(sandbox); + + // Add a message from a content window. + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.console.log("message from content window"); + }); + + // Test eval. + execute(hud, "document.location.href"); + + // Test eval frame script + execute( + hud, + `gBrowser.selectedBrowser.messageManager.loadFrameScript(` + + `'data:application/javascript,console.log("framescript-message")', false);` + + `"framescript-eval";` + ); + + // Check for network requests. + const xhr = new XMLHttpRequest(); + xhr.onload = () => console.log("xhr loaded, status is: " + xhr.status); + xhr.open("get", TEST_URI, true); + xhr.send(); + + // Check for xhr error. + const xhrErr = new XMLHttpRequest(); + xhrErr.onload = () => { + console.log("xhr error loaded, status is: " + xhrErr.status); + }; + xhrErr.open("get", TEST_XHR_ERROR_URI, true); + xhrErr.send(); + + // Check that Fetch requests are categorized as "XHR". + await fetch(TEST_IMAGE); + console.log("fetch loaded"); + + await checkMessageExists(hud, "message from chrome window"); + await checkMessageExists( + hud, + "error thrown from test-cu-reporterror.js via Cu.reportError()" + ); + await checkMessageExists(hud, "error from nuked globals"); + await checkMessageExists(hud, "message from content window"); + await checkMessageExists(hud, "browser.xhtml"); + await checkMessageExists(hud, "framescript-eval"); + await checkMessageExists(hud, "framescript-message"); + await checkMessageExists(hud, "foobarException"); + await checkMessageExists(hud, "test-console.html"); + await checkMessageExists(hud, "404.html"); + await checkMessageExists(hud, "test-image.png"); +} + +async function checkMessageExists(hud, msg) { + info(`Checking "${msg}" was logged`); + const message = await waitFor( + () => findMessage(hud, msg), + `Couldn't find "${msg}"` + ); + ok(message, `"${msg}" was logged`); +} diff --git a/devtools/client/webconsole/test/browser/browser_console_chrome_context_message.js b/devtools/client/webconsole/test/browser/browser_console_chrome_context_message.js new file mode 100644 index 0000000000..9ac024dd4e --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_console_chrome_context_message.js @@ -0,0 +1,49 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Test that console API calls in the content page appear in the browser console. + +"use strict"; + +add_task(async function() { + // Needed for the execute() function below + await pushPref("security.allow_parent_unrestricted_js_loads", true); + + // Show the content messages + await pushPref("devtools.browserconsole.contentMessages", true); + + const hud = await BrowserConsoleManager.toggleBrowserConsole(); + await clearOutput(hud); + await openNewTabAndConsole( + `data:text/html,<script>console.log("hello from content")</script>` + ); + + const expectedMessages = [ + `Cu.reportError`, // bug 1561930 + ]; + + execute(hud, `Cu.reportError("Cu.reportError");`); // bug 1561930 + info("Wait for expected message are shown on browser console"); + await waitFor(() => + expectedMessages.every(expectedMessage => findMessage(hud, expectedMessage)) + ); + await waitFor(() => findMessage(hud, "hello from content")); + + ok(true, "Expected messages are displayed in the browser console"); + + info("Uncheck the Show content messages checkbox"); + await toggleConsoleSetting( + hud, + ".webconsole-console-settings-menu-item-contentMessages" + ); + await waitFor(() => !findMessage(hud, "hello from content")); + + info("Check the expected messages are still visiable in the browser console"); + for (const expectedMessage of expectedMessages) { + ok( + findMessage(hud, expectedMessage), + `"${expectedMessage}" should be still visible` + ); + } +}); diff --git a/devtools/client/webconsole/test/browser/browser_console_clear_cache.js b/devtools/client/webconsole/test/browser/browser_console_clear_cache.js new file mode 100644 index 0000000000..1cf1da4689 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_console_clear_cache.js @@ -0,0 +1,53 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Check that clearing the browser console output also clears the console cache. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,Test browser console clear cache"; + +add_task(async function() { + await pushPref("devtools.browserconsole.contentMessages", true); + // Bug 1605036: Disable Multiprocess Browser Toolbox for now as it introduces intermittent failure in this test + await pushPref("devtools.browsertoolbox.fission", false); + + await addTab(TEST_URI); + let hud = await BrowserConsoleManager.toggleBrowserConsole(); + const CACHED_MESSAGE = "CACHED_MESSAGE"; + await logTextToConsole(hud, CACHED_MESSAGE); + + info("Click the clear output button"); + const onBrowserConsoleOutputCleared = waitFor( + () => !findMessage(hud, CACHED_MESSAGE) + ); + hud.ui.window.document.querySelector(".devtools-clear-icon").click(); + await onBrowserConsoleOutputCleared; + + // Check that there are no other messages logged (see Bug 1457478). + // Log a message to make sure the console handled any prior log. + await logTextToConsole(hud, "after clear"); + const messages = hud.ui.outputNode.querySelectorAll(".message"); + is(messages.length, 1, "There is only the new message in the output"); + + info("Close and re-open the browser console"); + await safeCloseBrowserConsole(); + hud = await BrowserConsoleManager.toggleBrowserConsole(); + + info("Log a smoke message in order to know that the console is ready"); + await logTextToConsole(hud, "Smoke message"); + is( + findMessage(hud, CACHED_MESSAGE), + undefined, + "The cached message is not visible anymore" + ); +}); + +function logTextToConsole(hud, text) { + const onMessage = waitForMessage(hud, text); + SpecialPowers.spawn(gBrowser.selectedBrowser, [text], function(str) { + content.wrappedJSObject.console.log(str); + }); + return onMessage; +} diff --git a/devtools/client/webconsole/test/browser/browser_console_clear_closed_tab.js b/devtools/client/webconsole/test/browser/browser_console_clear_closed_tab.js new file mode 100644 index 0000000000..ba89a0c48f --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_console_clear_closed_tab.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check that clearing the browser console output still works if the tab that emitted some +// was closed. See Bug 1628626. + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/test/browser/test-console.html"; + +add_task(async function() { + // Show the content messages + await pushPref("devtools.browserconsole.contentMessages", true); + // Enable Fission browser console to see the logged content object + await pushPref("devtools.browsertoolbox.fission", true); + + // Disable the preloaded process as it creates processes intermittently + // which forces the emission of RDP requests we aren't correctly waiting for. + await pushPref("dom.ipc.processPrelaunch.enabled", false); + + const tab = await addTab(TEST_URI); + const hud = await BrowserConsoleManager.toggleBrowserConsole(); + + info("Log a new message from the content page"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.console.log({ hello: "world" }); + }); + + await waitFor(() => findMessage(hud, "hello")); + + await removeTab(tab); + // Wait for a bit, so the actors and fronts are released. + await wait(500); + + info("Clear the console output"); + hud.ui.outputNode.querySelector(".devtools-clear-icon").click(); + + await waitFor(() => !findMessage(hud, "hello")); + ok(true, "Browser Console was cleared"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_console_clear_method.js b/devtools/client/webconsole/test/browser/browser_console_clear_method.js new file mode 100644 index 0000000000..e3b623d5a9 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_console_clear_method.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// XXX Remove this when the file is migrated to the new frontend. +/* eslint-disable no-undef */ + +// Check that console.clear() does not clear the output of the browser console. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,<p>Bug 1296870"; + +add_task(async function() { + await loadTab(TEST_URI); + const hud = await BrowserConsoleManager.toggleBrowserConsole(); + + info("Log a new message from the content page"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() { + content.wrappedJSObject.console.log("msg"); + }); + await waitForMessage("msg", hud); + + info("Send a console.clear() from the content page"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() { + content.wrappedJSObject.console.clear(); + }); + await waitForMessage("Console was cleared", hud); + + info( + "Check that the messages logged after the first clear are still displayed" + ); + ok(hud.ui.outputNode.textContent.includes("msg"), "msg is in the output"); +}); + +function waitForMessage(message, webconsole) { + return waitForMessages({ + webconsole, + messages: [ + { + text: message, + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }, + ], + }); +} diff --git a/devtools/client/webconsole/test/browser/browser_console_consolejsm_output.js b/devtools/client/webconsole/test/browser/browser_console_consolejsm_output.js new file mode 100644 index 0000000000..37c1260789 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_console_consolejsm_output.js @@ -0,0 +1,136 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that Console.jsm outputs messages to the Browser Console. + +"use strict"; + +add_task(async function testCategoryLogs() { + const consoleStorage = Cc["@mozilla.org/consoleAPI-storage;1"]; + const storage = consoleStorage.getService(Ci.nsIConsoleAPIStorage); + storage.clearEvents(); + + const { console } = ChromeUtils.import("resource://gre/modules/Console.jsm"); + console.log("bug861338-log-cached"); + + const hud = await BrowserConsoleManager.toggleBrowserConsole(); + + await checkMessageExists(hud, "bug861338-log-cached"); + + await clearOutput(hud); + + function testTrace() { + console.trace(); + } + + console.time("foobarTimer"); + const foobar = { bug851231prop: "bug851231value" }; + + console.log("bug851231-log"); + console.info("bug851231-info"); + console.warn("bug851231-warn"); + console.error("bug851231-error", foobar); + console.debug("bug851231-debug"); + console.dir({ "bug851231-dir": 1 }); + testTrace(); + console.timeEnd("foobarTimer"); + + info("wait for the Console.jsm messages"); + + await checkMessageExists(hud, "bug851231-log"); + await checkMessageExists(hud, "bug851231-info"); + await checkMessageExists(hud, "bug851231-warn"); + await checkMessageExists(hud, "bug851231-error"); + await checkMessageExists(hud, "bug851231-debug"); + await checkMessageExists(hud, "bug851231-dir"); + await checkMessageExists(hud, "console.trace()"); + await checkMessageExists(hud, "foobarTimer"); + + await clearOutput(hud); + await BrowserConsoleManager.toggleBrowserConsole(); +}); + +add_task(async function testFilter() { + const consoleStorage = Cc["@mozilla.org/consoleAPI-storage;1"]; + const storage = consoleStorage.getService(Ci.nsIConsoleAPIStorage); + storage.clearEvents(); + + const { ConsoleAPI } = ChromeUtils.import( + "resource://gre/modules/Console.jsm" + ); + const console2 = new ConsoleAPI(); + const hud = await BrowserConsoleManager.toggleBrowserConsole(); + + // Enable the error category and disable the log category. + await setFilterState(hud, { + error: true, + log: false, + }); + + const shouldBeVisible = "Should be visible"; + const shouldBeHidden = "Should be hidden"; + + console2.log(shouldBeHidden); + console2.error(shouldBeVisible); + + await checkMessageExists(hud, shouldBeVisible); + // Here we can safely assert that the log message is not visible, since the + // error message was logged after and is visible. + await checkMessageHidden(hud, shouldBeHidden); + + await resetFilters(hud); + await clearOutput(hud); +}); + +// Test that console.profile / profileEnd trigger the right events +add_task(async function testProfile() { + const consoleStorage = Cc["@mozilla.org/consoleAPI-storage;1"]; + const storage = consoleStorage.getService(Ci.nsIConsoleAPIStorage); + const { console } = ChromeUtils.import("resource://gre/modules/Console.jsm"); + + storage.clearEvents(); + + const profilerEvents = []; + + function observer(subject, topic) { + is(topic, "console-api-profiler", "The topic is 'console-api-profiler'"); + const subjectObj = subject.wrappedJSObject; + const event = { action: subjectObj.action, name: subjectObj.arguments[0] }; + info(`Profiler event: action=${event.action}, name=${event.name}`); + profilerEvents.push(event); + } + + Services.obs.addObserver(observer, "console-api-profiler"); + + console.profile("test"); + console.profileEnd("test"); + + Services.obs.removeObserver(observer, "console-api-profiler"); + + // Test that no messages were logged to the storage + const consoleEvents = storage.getEvents(); + is(consoleEvents.length, 0, "There are zero logged messages"); + + // Test that two profiler events were fired + is(profilerEvents.length, 2, "Got two profiler events"); + is(profilerEvents[0].action, "profile", "First event has the right action"); + is(profilerEvents[0].name, "test", "First event has the right name"); + is( + profilerEvents[1].action, + "profileEnd", + "Second event has the right action" + ); + is(profilerEvents[1].name, "test", "Second event has the right name"); +}); + +async function checkMessageExists(hud, msg) { + info(`Checking "${msg}" was logged`); + const message = await waitFor(() => findMessage(hud, msg)); + ok(message, `"${msg}" was logged`); +} + +async function checkMessageHidden(hud, msg) { + info(`Checking "${msg}" was not logged`); + await waitFor(() => findMessage(hud, msg) == null); + ok(true, `"${msg}" was not logged`); +} diff --git a/devtools/client/webconsole/test/browser/browser_console_content_getters.js b/devtools/client/webconsole/test/browser/browser_console_content_getters.js new file mode 100644 index 0000000000..3061b9eb3d --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_console_content_getters.js @@ -0,0 +1,585 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check evaluating and expanding getters in the Browser Console. +const TEST_URI = + "data:text/html;charset=utf8,<h1>Object Inspector on Getters</h1>"; +const { ELLIPSIS } = require("devtools/shared/l10n"); + +add_task(async function() { + // Show the content messages + await pushPref("devtools.browserconsole.contentMessages", true); + // Enable Fission browser console to see the logged content object + await pushPref("devtools.browsertoolbox.fission", true); + + await addTab(TEST_URI); + + info("Open the Browser Console"); + const hud = await BrowserConsoleManager.toggleBrowserConsole(); + + const LONGSTRING = "ab ".repeat(1e5); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [LONGSTRING], function( + longString + ) { + content.wrappedJSObject.console.log( + "oi-test", + Object.create( + null, + Object.getOwnPropertyDescriptors({ + get myStringGetter() { + return "hello"; + }, + get myNumberGetter() { + return 123; + }, + get myUndefinedGetter() { + return undefined; + }, + get myNullGetter() { + return null; + }, + get myZeroGetter() { + return 0; + }, + get myEmptyStringGetter() { + return ""; + }, + get myFalseGetter() { + return false; + }, + get myTrueGetter() { + return true; + }, + get myObjectGetter() { + return { foo: "bar" }; + }, + get myArrayGetter() { + return Array.from({ length: 1000 }, (_, i) => i); + }, + get myMapGetter() { + return new Map([["foo", { bar: "baz" }]]); + }, + get myProxyGetter() { + const handler = { + get: function(target, name) { + return name in target ? target[name] : 37; + }, + }; + return new Proxy({ a: 1 }, handler); + }, + get myThrowingGetter() { + throw new Error("myError"); + }, + get myLongStringGetter() { + return longString; + }, + }) + ) + ); + }); + + const node = await waitFor(() => findMessage(hud, "oi-test")); + const oi = node.querySelector(".tree"); + + expandObjectInspectorNode(oi); + await waitFor(() => getObjectInspectorNodes(oi).length > 1); + + await testStringGetter(oi); + await testNumberGetter(oi); + await testUndefinedGetter(oi); + await testNullGetter(oi); + await testZeroGetter(oi); + await testEmptyStringGetter(oi); + await testFalseGetter(oi); + await testTrueGetter(oi); + await testObjectGetter(oi); + await testArrayGetter(oi); + await testMapGetter(oi); + await testProxyGetter(oi); + await testThrowingGetter(oi); + await testLongStringGetter(oi, LONGSTRING); +}); + +async function testStringGetter(oi) { + let node = findObjectInspectorNode(oi, "myStringGetter"); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); + const invokeButton = getObjectInspectorInvokeGetterButton(node); + ok(invokeButton, "There is an invoke button as expected"); + + invokeButton.click(); + await waitFor( + () => + !getObjectInspectorInvokeGetterButton( + findObjectInspectorNode(oi, "myStringGetter") + ) + ); + + node = findObjectInspectorNode(oi, "myStringGetter"); + ok( + node.textContent.includes(`myStringGetter: "hello"`), + "String getter now has the expected text content" + ); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); +} + +async function testNumberGetter(oi) { + let node = findObjectInspectorNode(oi, "myNumberGetter"); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); + const invokeButton = getObjectInspectorInvokeGetterButton(node); + ok(invokeButton, "There is an invoke button as expected"); + + invokeButton.click(); + await waitFor( + () => + !getObjectInspectorInvokeGetterButton( + findObjectInspectorNode(oi, "myNumberGetter") + ) + ); + + node = findObjectInspectorNode(oi, "myNumberGetter"); + ok( + node.textContent.includes(`myNumberGetter: 123`), + "Number getter now has the expected text content" + ); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); +} + +async function testUndefinedGetter(oi) { + let node = findObjectInspectorNode(oi, "myUndefinedGetter"); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); + const invokeButton = getObjectInspectorInvokeGetterButton(node); + ok(invokeButton, "There is an invoke button as expected"); + + invokeButton.click(); + await waitFor( + () => + !getObjectInspectorInvokeGetterButton( + findObjectInspectorNode(oi, "myUndefinedGetter") + ) + ); + + node = findObjectInspectorNode(oi, "myUndefinedGetter"); + ok( + node.textContent.includes(`myUndefinedGetter: undefined`), + "undefined getter now has the expected text content" + ); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); +} + +async function testNullGetter(oi) { + let node = findObjectInspectorNode(oi, "myNullGetter"); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); + const invokeButton = getObjectInspectorInvokeGetterButton(node); + ok(invokeButton, "There is an invoke button as expected"); + + invokeButton.click(); + await waitFor( + () => + !getObjectInspectorInvokeGetterButton( + findObjectInspectorNode(oi, "myNullGetter") + ) + ); + + node = findObjectInspectorNode(oi, "myNullGetter"); + ok( + node.textContent.includes(`myNullGetter: null`), + "null getter now has the expected text content" + ); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); +} + +async function testZeroGetter(oi) { + let node = findObjectInspectorNode(oi, "myZeroGetter"); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); + const invokeButton = getObjectInspectorInvokeGetterButton(node); + ok(invokeButton, "There is an invoke button as expected"); + + invokeButton.click(); + await waitFor( + () => + !getObjectInspectorInvokeGetterButton( + findObjectInspectorNode(oi, "myZeroGetter") + ) + ); + + node = findObjectInspectorNode(oi, "myZeroGetter"); + ok( + node.textContent.includes(`myZeroGetter: 0`), + "0 getter now has the expected text content" + ); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); +} + +async function testEmptyStringGetter(oi) { + let node = findObjectInspectorNode(oi, "myEmptyStringGetter"); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); + const invokeButton = getObjectInspectorInvokeGetterButton(node); + ok(invokeButton, "There is an invoke button as expected"); + + invokeButton.click(); + await waitFor( + () => + !getObjectInspectorInvokeGetterButton( + findObjectInspectorNode(oi, "myEmptyStringGetter") + ) + ); + + node = findObjectInspectorNode(oi, "myEmptyStringGetter"); + ok( + node.textContent.includes(`myEmptyStringGetter: ""`), + "empty string getter now has the expected text content" + ); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); +} + +async function testFalseGetter(oi) { + let node = findObjectInspectorNode(oi, "myFalseGetter"); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); + const invokeButton = getObjectInspectorInvokeGetterButton(node); + ok(invokeButton, "There is an invoke button as expected"); + + invokeButton.click(); + await waitFor( + () => + !getObjectInspectorInvokeGetterButton( + findObjectInspectorNode(oi, "myFalseGetter") + ) + ); + + node = findObjectInspectorNode(oi, "myFalseGetter"); + ok( + node.textContent.includes(`myFalseGetter: false`), + "false getter now has the expected text content" + ); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); +} + +async function testTrueGetter(oi) { + let node = findObjectInspectorNode(oi, "myTrueGetter"); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); + const invokeButton = getObjectInspectorInvokeGetterButton(node); + ok(invokeButton, "There is an invoke button as expected"); + + invokeButton.click(); + await waitFor( + () => + !getObjectInspectorInvokeGetterButton( + findObjectInspectorNode(oi, "myTrueGetter") + ) + ); + + node = findObjectInspectorNode(oi, "myTrueGetter"); + ok( + node.textContent.includes(`myTrueGetter: true`), + "false getter now has the expected text content" + ); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); +} + +async function testObjectGetter(oi) { + let node = findObjectInspectorNode(oi, "myObjectGetter"); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); + const invokeButton = getObjectInspectorInvokeGetterButton(node); + ok(invokeButton, "There is an invoke button as expected"); + + invokeButton.click(); + await waitFor( + () => + !getObjectInspectorInvokeGetterButton( + findObjectInspectorNode(oi, "myObjectGetter") + ) + ); + + node = findObjectInspectorNode(oi, "myObjectGetter"); + ok( + node.textContent.includes(`myObjectGetter: Object { foo: "bar" }`), + "object getter now has the expected text content" + ); + is(isObjectInspectorNodeExpandable(node), true, "The node can be expanded"); + + expandObjectInspectorNode(node); + await waitFor(() => getObjectInspectorChildrenNodes(node).length > 0); + checkChildren(node, [`foo: "bar"`, `<prototype>`]); +} + +async function testArrayGetter(oi) { + let node = findObjectInspectorNode(oi, "myArrayGetter"); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); + const invokeButton = getObjectInspectorInvokeGetterButton(node); + ok(invokeButton, "There is an invoke button as expected"); + + invokeButton.click(); + await waitFor( + () => + !getObjectInspectorInvokeGetterButton( + findObjectInspectorNode(oi, "myArrayGetter") + ) + ); + + node = findObjectInspectorNode(oi, "myArrayGetter"); + ok( + node.textContent.includes( + `myArrayGetter: Array(1000) [ 0, 1, 2, ${ELLIPSIS} ]` + ), + "Array getter now has the expected text content - " + ); + is(isObjectInspectorNodeExpandable(node), true, "The node can be expanded"); + + expandObjectInspectorNode(node); + await waitFor(() => getObjectInspectorChildrenNodes(node).length > 0); + const children = getObjectInspectorChildrenNodes(node); + + const firstBucket = children[0]; + ok(firstBucket.textContent.includes(`[0${ELLIPSIS}99]`), "Array has buckets"); + + is( + isObjectInspectorNodeExpandable(firstBucket), + true, + "The bucket can be expanded" + ); + expandObjectInspectorNode(firstBucket); + await waitFor(() => getObjectInspectorChildrenNodes(firstBucket).length > 0); + checkChildren( + firstBucket, + Array.from({ length: 100 }, (_, i) => `${i}: ${i}`) + ); +} + +async function testMapGetter(oi) { + let node = findObjectInspectorNode(oi, "myMapGetter"); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); + const invokeButton = getObjectInspectorInvokeGetterButton(node); + ok(invokeButton, "There is an invoke button as expected"); + + invokeButton.click(); + await waitFor( + () => + !getObjectInspectorInvokeGetterButton( + findObjectInspectorNode(oi, "myMapGetter") + ) + ); + + node = findObjectInspectorNode(oi, "myMapGetter"); + ok( + node.textContent.includes(`myMapGetter: Map`), + "map getter now has the expected text content" + ); + is(isObjectInspectorNodeExpandable(node), true, "The node can be expanded"); + + expandObjectInspectorNode(node); + await waitFor(() => getObjectInspectorChildrenNodes(node).length > 0); + checkChildren(node, [`size`, `<entries>`, `<prototype>`]); + + const entriesNode = findObjectInspectorNode(oi, "<entries>"); + expandObjectInspectorNode(entriesNode); + await waitFor(() => getObjectInspectorChildrenNodes(entriesNode).length > 0); + checkChildren(entriesNode, [`foo → Object { bar: "baz" }`]); + + const entryNode = getObjectInspectorChildrenNodes(entriesNode)[0]; + expandObjectInspectorNode(entryNode); + await waitFor(() => getObjectInspectorChildrenNodes(entryNode).length > 0); + checkChildren(entryNode, [`<key>: "foo"`, `<value>: Object { bar: "baz" }`]); +} + +async function testProxyGetter(oi) { + let node = findObjectInspectorNode(oi, "myProxyGetter"); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); + const invokeButton = getObjectInspectorInvokeGetterButton(node); + ok(invokeButton, "There is an invoke button as expected"); + + invokeButton.click(); + await waitFor( + () => + !getObjectInspectorInvokeGetterButton( + findObjectInspectorNode(oi, "myProxyGetter") + ) + ); + + node = findObjectInspectorNode(oi, "myProxyGetter"); + ok( + node.textContent.includes(`myProxyGetter: Proxy`), + "proxy getter now has the expected text content" + ); + is(isObjectInspectorNodeExpandable(node), true, "The node can be expanded"); + + expandObjectInspectorNode(node); + await waitFor(() => getObjectInspectorChildrenNodes(node).length > 0); + checkChildren(node, [`<target>`, `<handler>`]); + + const targetNode = findObjectInspectorNode(oi, "<target>"); + expandObjectInspectorNode(targetNode); + await waitFor(() => getObjectInspectorChildrenNodes(targetNode).length > 0); + checkChildren(targetNode, [`a: 1`, `<prototype>`]); + + const handlerNode = findObjectInspectorNode(oi, "<handler>"); + expandObjectInspectorNode(handlerNode); + await waitFor(() => getObjectInspectorChildrenNodes(handlerNode).length > 0); + checkChildren(handlerNode, [`get:`, `<prototype>`]); +} + +async function testThrowingGetter(oi) { + let node = findObjectInspectorNode(oi, "myThrowingGetter"); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); + const invokeButton = getObjectInspectorInvokeGetterButton(node); + ok(invokeButton, "There is an invoke button as expected"); + + invokeButton.click(); + await waitFor( + () => + !getObjectInspectorInvokeGetterButton( + findObjectInspectorNode(oi, "myThrowingGetter") + ) + ); + + node = findObjectInspectorNode(oi, "myThrowingGetter"); + ok( + node.textContent.includes(`myThrowingGetter: Error`), + "throwing getter does show the error" + ); + is(isObjectInspectorNodeExpandable(node), true, "The node can be expanded"); + + expandObjectInspectorNode(node); + await waitFor(() => getObjectInspectorChildrenNodes(node).length > 0); + checkChildren(node, [ + `columnNumber`, + `fileName`, + `lineNumber`, + `message`, + `stack`, + `<prototype>`, + ]); +} + +async function testLongStringGetter(oi, longString) { + const getLongStringNode = () => + findObjectInspectorNode(oi, "myLongStringGetter"); + const node = getLongStringNode(); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); + const invokeButton = getObjectInspectorInvokeGetterButton(node); + ok(invokeButton, "There is an invoke button as expected"); + + invokeButton.click(); + await waitFor(() => + getLongStringNode().textContent.includes(`myLongStringGetter: "ab ab`) + ); + ok(true, "longstring getter shows the initial text"); + is( + isObjectInspectorNodeExpandable(getLongStringNode()), + true, + "The node can be expanded" + ); + + expandObjectInspectorNode(getLongStringNode()); + await waitFor(() => + getLongStringNode().textContent.includes( + `myLongStringGetter: "${longString}"` + ) + ); + ok(true, "the longstring was expanded"); +} + +function checkChildren(node, expectedChildren) { + const children = getObjectInspectorChildrenNodes(node); + is( + children.length, + expectedChildren.length, + "There is the expected number of children" + ); + children.forEach((child, index) => { + ok( + child.textContent.includes(expectedChildren[index]), + `Expected "${child.textContent}" to include "${expectedChildren[index]}"` + ); + }); +} diff --git a/devtools/client/webconsole/test/browser/browser_console_content_longstring.js b/devtools/client/webconsole/test/browser/browser_console_content_longstring.js new file mode 100644 index 0000000000..26d5c345c3 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_console_content_longstring.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that very long content strings can be expanded and collapsed in the +// Browser Console, and do not hang the browser. + +"use strict"; + +const TEST_URI = "data:text/html,<meta charset=utf8>Test LongString hang"; + +const LONGSTRING = `foobar${"a".repeat( + 9000 +)}foobaz${"abbababazomglolztest".repeat(100)}boom!`; + +add_task(async function() { + // Show the content messages + await pushPref("devtools.browserconsole.contentMessages", true); + // Enable Fission browser console to see the logged content object + await pushPref("devtools.browsertoolbox.fission", true); + + await addTab(TEST_URI); + + info("Open the Browser Console"); + const hud = await BrowserConsoleManager.toggleBrowserConsole(); + + info("Log a longString"); + const onMessage = waitForMessage(hud, LONGSTRING.slice(0, 50)); + SpecialPowers.spawn(gBrowser.selectedBrowser, [LONGSTRING], str => { + content.console.log(str); + }); + + const { node } = await onMessage; + const arrow = node.querySelector(".arrow"); + ok(arrow, "longString expand arrow is shown"); + + info("wait for long string expansion"); + const onLongStringFullTextDisplayed = waitFor(() => + findMessage(hud, LONGSTRING) + ); + arrow.click(); + await onLongStringFullTextDisplayed; + + ok(true, "The full text of the longString is displayed"); + + info("wait for long string collapse"); + const onLongStringCollapsed = waitFor(() => !findMessage(hud, LONGSTRING)); + arrow.click(); + await onLongStringCollapsed; + + ok(true, "The longString can be collapsed"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_console_content_object.js b/devtools/client/webconsole/test/browser/browser_console_content_object.js new file mode 100644 index 0000000000..6932b8f252 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_console_content_object.js @@ -0,0 +1,78 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Test that console API calls in the content page appear in the browser console. + +"use strict"; + +const TEST_URI = `data:text/html,<meta charset=utf8>console API calls<script> + console.log({ contentObject: "YAY!", deep: ["yes!"] }); +</script>`; + +add_task(async function() { + // Show the content messages + await pushPref("devtools.browserconsole.contentMessages", true); + // Enable Fission browser console to see the logged content object + await pushPref("devtools.browsertoolbox.fission", true); + + await addTab(TEST_URI); + + info("Open the Browser Console"); + let hud = await BrowserConsoleManager.toggleBrowserConsole(); + + info("Wait until the content object is displayed"); + let objectMessage = await waitFor(() => + findMessage(hud, `Object { contentObject: "YAY!", deep: (1) […] }`) + ); + ok(true, "Content object is displayed in the Browser Console"); + + await testExpandObject(objectMessage); + + info("Restart the Browser Console"); + await safeCloseBrowserConsole(); + hud = await BrowserConsoleManager.toggleBrowserConsole(); + + info("Wait until the content object is displayed"); + objectMessage = await waitFor(() => + findMessage(hud, `Object { contentObject: "YAY!", deep: (1) […] }`) + ); + ok(true, "Content object is displayed in the Browser Console after restart"); + + await testExpandObject(objectMessage); +}); + +async function testExpandObject(objectMessage) { + info("Check that the logged content object can be expanded"); + const oi = objectMessage.querySelector(".tree"); + + ok(oi, "There's an object inspector component for the content object"); + + oi.querySelector(".arrow").click(); + // The object inspector now looks like: + // ▼ {…} + // | contentObject: "YAY!" + // | ▶︎ deep: Array [ "yes!" ] + // | ▶︎ <prototype> + await waitFor(() => oi.querySelectorAll(".node").length === 4); + ok(true, "The ObjectInspector was expanded"); + const [root, contentObjectProp, deepProp, prototypeProp] = [ + ...oi.querySelectorAll(".node"), + ]; + ok(root.textContent.includes(`{…}`)); + ok(contentObjectProp.textContent.includes(`contentObject: "YAY!"`)); + ok(deepProp.textContent.includes(`deep: Array [ "yes!" ]`)); + ok(prototypeProp.textContent.includes(`<prototype>`)); + + // The object inspector now looks like: + // ▼ {…} + // | contentObject: "YAY!" + // | ▼︎ deep: (1) […] + // | | 0: "yes!" + // | | length: 1 + // | | ▶︎ <prototype> + // | ▶︎ <prototype> + deepProp.querySelector(".arrow").click(); + await waitFor(() => oi.querySelectorAll(".node").length === 7); + ok(true, "The nested array was expanded"); +} diff --git a/devtools/client/webconsole/test/browser/browser_console_content_object_context_menu.js b/devtools/client/webconsole/test/browser/browser_console_content_object_context_menu.js new file mode 100644 index 0000000000..4b5ef2f101 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_console_content_object_context_menu.js @@ -0,0 +1,71 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Test that "Copy Object" on a the content message works in the browser console. + +"use strict"; + +const TEST_URI = `data:text/html,<meta charset=utf8>console API calls<script> + console.log({ + contentObject: "YAY!", + deep: ["hello", "world"] + }); +</script>`; + +add_task(async function() { + // Show the content messages + await pushPref("devtools.browserconsole.contentMessages", true); + // Enable Fission browser console to see the logged content object + await pushPref("devtools.browsertoolbox.fission", true); + + await addTab(TEST_URI); + + info("Open the Browser Console"); + const hud = await BrowserConsoleManager.toggleBrowserConsole(); + + info("Wait until the content object is displayed"); + const objectMessage = await waitFor(() => + findMessage(hud, `Object { contentObject: "YAY!", deep: (2) […] }`) + ); + ok(true, "Content object is displayed in the Browser Console"); + + info("Expand the object"); + const oi = objectMessage.querySelector(".tree"); + oi.querySelector(".arrow").click(); + // The object inspector now looks like: + // ▼ {…} + // | contentObject: "YAY!" + // | ▶︎ deep: Array [ "hello", "world" ] + // | ▶︎ <prototype> + await waitFor(() => oi.querySelectorAll(".node").length === 4); + ok(true, "The ObjectInspector was expanded"); + oi.scrollIntoView(); + + info("Check that the object can be copied to clipboard"); + await testCopyObject( + hud, + oi.querySelector(".objectBox-object"), + JSON.stringify({ contentObject: "YAY!", deep: ["hello", "world"] }, null, 2) + ); + + info("Check that inner object can be copied to clipboard"); + await testCopyObject( + hud, + oi.querySelector(".objectBox-array"), + JSON.stringify(["hello", "world"], null, 2) + ); +}); + +async function testCopyObject(hud, element, expected) { + info("Check `Copy object` is enabled"); + const menuPopup = await openContextMenu(hud, element); + const copyObjectMenuItem = menuPopup.querySelector( + "#console-menu-copy-object" + ); + ok(!copyObjectMenuItem.disabled, "`Copy object` is enabled"); + + info("Click on `Copy object`"); + await waitForClipboardPromise(() => copyObjectMenuItem.click(), expected); + await hideContextMenu(hud); +} diff --git a/devtools/client/webconsole/test/browser/browser_console_content_object_in_sidebar.js b/devtools/client/webconsole/test/browser/browser_console_content_object_in_sidebar.js new file mode 100644 index 0000000000..d11a60a02f --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_console_content_object_in_sidebar.js @@ -0,0 +1,144 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the "Open in sidebar" context menu entry is active for +// the content objects and opens the sidebar when clicked. + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf8,<script> + console.log( + {a:1}, + 100, + {b:1}, + 'foo', + false, + null, + undefined + ); +</script>`; + +add_task(async function() { + // Enable sidebar + await pushPref("devtools.webconsole.sidebarToggle", true); + // Show the content messages + await pushPref("devtools.browserconsole.contentMessages", true); + // Enable Fission browser console to see the logged content object + await pushPref("devtools.browsertoolbox.fission", true); + + await addTab(TEST_URI); + + info("Open the Browser Console"); + const hud = await BrowserConsoleManager.toggleBrowserConsole(); + + const message = await waitFor(() => findMessage(hud, "foo")); + const [objectA, objectB] = message.querySelectorAll( + ".object-inspector .objectBox-object" + ); + const number = findMessage(hud, "100", ".objectBox"); + const string = findMessage(hud, "foo", ".objectBox"); + const bool = findMessage(hud, "false", ".objectBox"); + const nullMessage = findMessage(hud, "null", ".objectBox"); + const undefinedMsg = findMessage(hud, "undefined", ".objectBox"); + + info("Showing sidebar for {a:1}"); + await showSidebarWithContextMenu(hud, objectA, true); + + let sidebarContents = hud.ui.document.querySelector(".sidebar-contents"); + let objectInspector = sidebarContents.querySelector(".object-inspector"); + let oiNodes = objectInspector.querySelectorAll(".node"); + if (oiNodes.length === 1) { + // If this is the case, we wait for the properties to be fetched and displayed. + await waitForNodeMutation(objectInspector, { + childList: true, + }); + } + + let sidebarText = hud.ui.document.querySelector(".sidebar-contents") + .textContent; + ok(sidebarText.includes("a: 1"), "Sidebar is shown for {a:1}"); + + info("Showing sidebar for {a:1} again"); + await showSidebarWithContextMenu(hud, objectA, false); + ok( + hud.ui.document.querySelector(".sidebar"), + "Sidebar is still shown after clicking on same object" + ); + is( + hud.ui.document.querySelector(".sidebar-contents").textContent, + sidebarText, + "Sidebar is not updated after clicking on same object" + ); + + info("Showing sidebar for {b:1}"); + await showSidebarWithContextMenu(hud, objectB, false); + + sidebarContents = hud.ui.document.querySelector(".sidebar-contents"); + objectInspector = sidebarContents.querySelector(".object-inspector"); + oiNodes = objectInspector.querySelectorAll(".node"); + if (oiNodes.length === 1) { + // If this is the case, we wait for the properties to be fetched and displayed. + await waitForNodeMutation(objectInspector, { + childList: true, + }); + } + + isnot( + hud.ui.document.querySelector(".sidebar-contents").textContent, + sidebarText, + "Sidebar is updated for {b:1}" + ); + sidebarText = hud.ui.document.querySelector(".sidebar-contents").textContent; + + ok(sidebarText.includes("b: 1"), "Sidebar contents shown for {b:1}"); + + info("Checking context menu entry is disabled for number"); + const numberContextMenuEnabled = await isContextMenuEntryEnabled(hud, number); + ok(!numberContextMenuEnabled, "Context menu entry is disabled for number"); + + info("Checking context menu entry is disabled for string"); + const stringContextMenuEnabled = await isContextMenuEntryEnabled(hud, string); + ok(!stringContextMenuEnabled, "Context menu entry is disabled for string"); + + info("Checking context menu entry is disabled for bool"); + const boolContextMenuEnabled = await isContextMenuEntryEnabled(hud, bool); + ok(!boolContextMenuEnabled, "Context menu entry is disabled for bool"); + + info("Checking context menu entry is disabled for null message"); + const nullContextMenuEnabled = await isContextMenuEntryEnabled( + hud, + nullMessage + ); + ok(!nullContextMenuEnabled, "Context menu entry is disabled for nullMessage"); + + info("Checking context menu entry is disabled for undefined message"); + const undefinedContextMenuEnabled = await isContextMenuEntryEnabled( + hud, + undefinedMsg + ); + ok( + !undefinedContextMenuEnabled, + "Context menu entry is disabled for undefinedMsg" + ); +}); + +async function showSidebarWithContextMenu(hud, node, expectMutation) { + const appNode = hud.ui.document.querySelector(".webconsole-app"); + const onSidebarShown = waitForNodeMutation(appNode, { childList: true }); + + const contextMenu = await openContextMenu(hud, node); + const openInSidebar = contextMenu.querySelector("#console-menu-open-sidebar"); + openInSidebar.click(); + if (expectMutation) { + await onSidebarShown; + } + await hideContextMenu(hud); +} + +async function isContextMenuEntryEnabled(hud, node) { + const contextMenu = await openContextMenu(hud, node); + const openInSidebar = contextMenu.querySelector("#console-menu-open-sidebar"); + const enabled = !openInSidebar.attributes.disabled; + await hideContextMenu(hud); + return enabled; +} diff --git a/devtools/client/webconsole/test/browser/browser_console_context_menu_entries.js b/devtools/client/webconsole/test/browser/browser_console_context_menu_entries.js new file mode 100644 index 0000000000..1d119a0767 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_console_context_menu_entries.js @@ -0,0 +1,140 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that we display the expected context menu entries. + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-console.html"; + +add_task(async function() { + await pushPref("devtools.browserconsole.contentMessages", true); + // Enable net messages in the console for this test. + await pushPref("devtools.browserconsole.filter.net", true); + // This is required for testing the text input in the browser console: + await pushPref("devtools.chrome.enabled", true); + + await addTab(TEST_URI); + const hud = await BrowserConsoleManager.toggleBrowserConsole(); + + info("Reload the content window to produce a network log"); + const onNetworkMessage = waitForMessage(hud, "test-console.html"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.wrappedJSObject.location.reload(); + }); + const networkMessage = await onNetworkMessage; + + info("Open and check the context menu for the network message"); + let menuPopup = await openContextMenu(hud, networkMessage.node); + ok(menuPopup, "The context menu is displayed on a network message"); + + let expectedContextMenu = addPrefBasedEntries([ + "#console-menu-copy-url (a)", + "#console-menu-open-url (T)", + "#console-menu-store (S) [disabled]", + "#console-menu-copy (C)", + "#console-menu-copy-object (o) [disabled]", + "#console-menu-select (A)", + "#console-menu-export-clipboard ()", + "#console-menu-export-file ()", + ]); + is( + getSimplifiedContextMenu(menuPopup).join("\n"), + expectedContextMenu.join("\n"), + "The context menu has the expected entries for a network message" + ); + + info("Logging a text message in the content window"); + const onLogMessage = waitForMessage(hud, "simple text message"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.wrappedJSObject.console.log("simple text message"); + }); + + const logMessage = await onLogMessage; + menuPopup = await openContextMenu(hud, logMessage.node); + ok(menuPopup, "The context menu is displayed on a log message"); + + expectedContextMenu = addPrefBasedEntries([ + "#console-menu-store (S) [disabled]", + "#console-menu-copy (C)", + "#console-menu-copy-object (o) [disabled]", + "#console-menu-select (A)", + "#console-menu-export-clipboard ()", + "#console-menu-export-file ()", + ]); + is( + getSimplifiedContextMenu(menuPopup).join("\n"), + expectedContextMenu.join("\n"), + "The context menu has the expected entries for a simple log message" + ); + + menuPopup = await openContextMenu(hud, hud.jsterm.node); + + let actualEntries = getL10NContextMenu(menuPopup); + is( + actualEntries.length, + 6, + "The context menu has the right number of entries." + ); + is(actualEntries[0], "#editmenu-undo (text-action-undo) [disabled]"); + is(actualEntries[1], "#editmenu-cut (text-action-cut) [disabled]"); + is(actualEntries[2], "#editmenu-copy (text-action-copy) [disabled]"); + // Paste may or may not be enabled depending on what ran before this. + // If emptyClipboard is fixed (666254) we could assert if it's enabled/disabled. + ok(actualEntries[3].startsWith("#editmenu-paste (text-action-paste)")); + is(actualEntries[4], "#editmenu-delete (text-action-delete) [disabled]"); + is( + actualEntries[5], + "#editmenu-selectAll (text-action-select-all) [disabled]" + ); + + const node = hud.jsterm.node; + const inputContainer = node.closest(".jsterm-input-container"); + await openContextMenu(hud, inputContainer); + + actualEntries = getL10NContextMenu(menuPopup); + is( + actualEntries.length, + 6, + "The context menu has the right number of entries." + ); + is(actualEntries[0], "#editmenu-undo (text-action-undo) [disabled]"); + is(actualEntries[1], "#editmenu-cut (text-action-cut) [disabled]"); + is(actualEntries[2], "#editmenu-copy (text-action-copy) [disabled]"); + // Paste may or may not be enabled depending on what ran before this. + // If emptyClipboard is fixed (666254) we could assert if it's enabled/disabled. + ok(actualEntries[3].startsWith("#editmenu-paste (text-action-paste)")); + is(actualEntries[4], "#editmenu-delete (text-action-delete) [disabled]"); + is( + actualEntries[5], + "#editmenu-selectAll (text-action-select-all) [disabled]" + ); + + await hideContextMenu(hud); +}); + +function addPrefBasedEntries(expectedEntries) { + if (Services.prefs.getBoolPref("devtools.webconsole.sidebarToggle", false)) { + expectedEntries.push("#console-menu-open-sidebar (V) [disabled]"); + } + + return expectedEntries; +} + +function getL10NContextMenu(popupElement) { + return [...popupElement.querySelectorAll("menuitem")].map(entry => { + const l10nID = entry.getAttribute("data-l10n-id"); + const disabled = entry.hasAttribute("disabled"); + return `#${entry.id} (${l10nID})${disabled ? " [disabled]" : ""}`; + }); +} + +function getSimplifiedContextMenu(popupElement) { + return [...popupElement.querySelectorAll("menuitem")].map(entry => { + const key = entry.getAttribute("accesskey"); + const disabled = entry.hasAttribute("disabled"); + return `#${entry.id} (${key})${disabled ? " [disabled]" : ""}`; + }); +} diff --git a/devtools/client/webconsole/test/browser/browser_console_dead_objects.js b/devtools/client/webconsole/test/browser/browser_console_dead_objects.js new file mode 100644 index 0000000000..4d6f7ee233 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_console_dead_objects.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check that Dead Objects do not break the Web/Browser Consoles. +// +// This test: +// - Opens the Browser Console. +// - Creates a sandbox. +// - Stores a reference to the sandbox on the chrome window object. +// - Nukes the sandbox +// - Tries to use the sandbox. This is the dead object. + +"use strict"; + +add_task(async function() { + // Needed for the execute() function below + await pushPref("security.allow_parent_unrestricted_js_loads", true); + + const hud = await BrowserConsoleManager.toggleBrowserConsole(); + ok(hud, "browser console opened"); + + // Add the reference to the nuked sandbox. + execute( + hud, + "window.nukedSandbox = Cu.Sandbox(null); Cu.nukeSandbox(nukedSandbox);" + ); + + await executeAndWaitForMessage(hud, "nukedSandbox", "DeadObject"); + const msg = await executeAndWaitForMessage( + hud, + "nukedSandbox.hello", + "can't access dead object" + ); + + // Check that the link contains an anchor. We can't click on the link because + // clicking links from tests attempts to access an external URL and crashes Firefox. + const anchor = msg.node.querySelector("a"); + is(anchor.textContent, "[Learn More]", "Link text is correct"); + + await executeAndWaitForMessage(hud, "delete window.nukedSandbox; 1 + 1", "2"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_console_devtools_loader_exception.js b/devtools/client/webconsole/test/browser/browser_console_devtools_loader_exception.js new file mode 100644 index 0000000000..a72380625a --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_console_devtools_loader_exception.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check that exceptions from scripts loaded with the DevTools loader are +// opened correctly in View Source from the Browser Console. + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf8,<p>browser_console_devtools_loader_exception.js</p>"; + +add_task(async function() { + // Disable the preloaded process as it creates processes intermittently + // which forces the emission of RDP requests we aren't correctly waiting for. + await pushPref("dom.ipc.processPrelaunch.enabled", false); + + const wcHud = await openNewTabAndConsole(TEST_URI); + ok(wcHud, "web console opened"); + + const bcHud = await BrowserConsoleManager.toggleBrowserConsole(); + ok(bcHud, "browser console opened"); + + // Cause an exception in a script loaded with the DevTools loader. + const toolbox = wcHud.toolbox; + const oldPanels = toolbox._toolPanels; + // non-iterable + toolbox._toolPanels = {}; + + function fixToolbox() { + toolbox._toolPanels = oldPanels; + } + + info("generate exception and wait for message"); + + executeSoon(() => { + expectUncaughtException(); + executeSoon(fixToolbox); + toolbox.getToolPanels(); + }); + + const msg = await waitFor(() => + findMessage(bcHud, "TypeError: this._toolPanels is not iterable") + ); + + fixToolbox(); + + ok(msg, `Message found: "TypeError: this._toolPanels is not iterable"`); + + const locationNode = msg.querySelector( + ".message-location .frame-link-source" + ); + ok(locationNode, "Message location link element found"); + + const url = locationNode.href; + info("view-source url: " + url); + ok(url, "we have some source URL after the click"); + ok(url.includes("toolbox.js"), "we have the expected view source URL"); + ok(!url.includes("->"), "no -> in the URL given to view-source"); + + const isFissionEnabledForBrowserConsole = Services.prefs.getBoolPref( + "devtools.browsertoolbox.fission", + false + ); + + const { targetList } = bcHud; + // If Fission is not enabled for the Browser Console (e.g. in Beta at this moment), + // the target list won't watch for Frame targets, and as a result we won't have issues + // with pending connections to the server that we're observing when attaching the target. + const onViewSourceTargetAvailable = !isFissionEnabledForBrowserConsole + ? Promise.resolve() + : new Promise(resolve => { + const onAvailable = ({ targetFront }) => { + if (targetFront.url.includes("view-source:")) { + targetList.unwatchTargets([targetList.TYPES.FRAME], onAvailable); + resolve(); + } + }; + targetList.watchTargets([targetList.TYPES.FRAME], onAvailable); + }); + + const onTabOpen = BrowserTestUtils.waitForNewTab( + gBrowser, + tabUrl => tabUrl.startsWith("view-source:"), + true + ); + locationNode.click(); + + await onTabOpen; + ok(true, "The view source tab was opened in response to clicking the link"); + + info("Wait for the frame target to be available"); + await onViewSourceTargetAvailable; +}); diff --git a/devtools/client/webconsole/test/browser/browser_console_eager_eval.js b/devtools/client/webconsole/test/browser/browser_console_eager_eval.js new file mode 100644 index 0000000000..009e2a87b0 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_console_eager_eval.js @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Check evaluating eager-evaluation values. +const TEST_URI = "data:text/html;charset=utf8,"; + +add_task(async function() { + await addTab(TEST_URI); + + await pushPref("devtools.chrome.enabled", true); + + info("Open the Browser Console"); + const hud = await BrowserConsoleManager.toggleBrowserConsole(); + + await executeNonDebuggeeSideeffect(hud); +}); + +// Test that code is still terminated, even if it is calling into realms +// that aren't the normal debuggee realms (bug 1620087). +async function executeNonDebuggeeSideeffect(hud) { + await executeAndWaitForMessage( + hud, + `globalThis.eagerLoader = ChromeUtils.import("resource://devtools/shared/Loader.jsm");`, + `DevToolsLoader` + ); + + // "require" should terminate execution because it will try to create a new + // module record for the given URL, as long as the loader's debuggee + // has been properly added to the debugger. The termination should + // happen before it starts processing the path, so we don't need to provide + // a real path here. + setInputValue(hud, `globalThis.eagerLoader.require("fake://path");`); + + // Wait a bit to make sure that the command has time to fail before we + // validate the eager-eval result. + await wait(500); + await waitForEagerEvaluationResult(hud, ""); + + setInputValue(hud, ""); + + await executeAndWaitForMessage(hud, `delete globalThis.eagerLoader;`, `true`); +} diff --git a/devtools/client/webconsole/test/browser/browser_console_error_source_click.js b/devtools/client/webconsole/test/browser/browser_console_error_source_click.js new file mode 100644 index 0000000000..b2b102a03e --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_console_error_source_click.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check that JS errors and CSS warnings open view source when their source link +// is clicked in the Browser Console. + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf8,<p>hello world" + + "<button onclick='foobar.explode()'>click!</button>"; + +add_task(async function() { + // Disable the preloaded process as it creates processes intermittently + // which forces the emission of RDP requests we aren't correctly waiting for. + await pushPref("dom.ipc.processPrelaunch.enabled", false); + + await pushPref("devtools.browserconsole.contentMessages", true); + await addTab(TEST_URI); + + const hud = await BrowserConsoleManager.toggleBrowserConsole(); + ok(hud, "browser console opened"); + + // On e10s, the exception is triggered in child process + // and is ignored by test harness + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } + + info("generate exception and wait for the message"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + const button = content.document.querySelector("button"); + button.click(); + }); + + const messageText = "ReferenceError: foobar is not defined"; + + const msg = await waitFor( + () => findMessage(hud, messageText), + `Message "${messageText}" wasn't found` + ); + ok(msg, `Message found: "${messageText}"`); + + const locationNode = msg.querySelector( + ".message-location .frame-link-source" + ); + ok(locationNode, "Message location link element found"); + + const onTabOpen = BrowserTestUtils.waitForNewTab( + gBrowser, + url => url.startsWith("view-source:"), + true + ); + locationNode.click(); + await onTabOpen; + ok(true, "The view source tab was opened in response to clicking the link"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_console_filters.js b/devtools/client/webconsole/test/browser/browser_console_filters.js new file mode 100644 index 0000000000..00657d5c63 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_console_filters.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check that the Browser Console does not use the same filter prefs as the Web +// Console. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,<p>browser console filters"; + +add_task(async function() { + let hud = await openNewTabAndConsole(TEST_URI); + ok(hud, "web console opened"); + + let filterState = await getFilterState(hud); + ok(filterState.error, "The web console error filter is enabled"); + + info(`toggle "error" filter`); + await setFilterState(hud, { + error: false, + }); + + filterState = await getFilterState(hud); + ok(!filterState.error, "The web console error filter is disabled"); + + await resetFilters(hud); + await closeConsole(); + info("web console closed"); + + hud = await BrowserConsoleManager.toggleBrowserConsole(); + ok(hud, "browser console opened"); + + filterState = await getFilterState(hud); + ok(filterState.error, "The browser console error filter is enabled"); + + info(`toggle "error" filter in browser console`); + await setFilterState(hud, { + error: false, + }); + + filterState = await getFilterState(hud); + ok(!filterState.error, "The browser console error filter is disabled"); + + await resetFilters(hud); +}); diff --git a/devtools/client/webconsole/test/browser/browser_console_jsterm_await.js b/devtools/client/webconsole/test/browser/browser_console_jsterm_await.js new file mode 100644 index 0000000000..bf134e447f --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_console_jsterm_await.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This is a lightweight version of browser_jsterm_await.js to only ensure top-level await +// support in the Browser Console. + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf-8,Top-level await Browser Console test"; + +add_task(async function() { + // Needed for the execute() function below + await pushPref("security.allow_parent_unrestricted_js_loads", true); + // Enable await mapping. + await pushPref("devtools.debugger.features.map-await-expression", true); + + await addTab(TEST_URI); + const hud = await BrowserConsoleManager.toggleBrowserConsole(); + + const executeAndWaitForResultMessage = (input, expectedOutput) => + executeAndWaitForMessage(hud, input, expectedOutput, ".result"); + + info("Evaluate a top-level await expression"); + const simpleAwait = `await new Promise(r => setTimeout(() => r(["await1"]), 500))`; + await executeAndWaitForResultMessage(simpleAwait, `Array [ "await1" ]`); + + // Check that the resulting promise of the async iife is not displayed. + const messages = hud.ui.outputNode.querySelectorAll(".message .message-body"); + const messagesText = Array.from(messages) + .map(n => n.textContent) + .join(" - "); + is( + messagesText.includes("Promise {"), + false, + "The output does not contain a Promise" + ); + ok( + messagesText.includes(simpleAwait) && + messagesText.includes(`Array [ "await1" ]`), + "The output contains the the expected messages" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_console_nsiconsolemessage.js b/devtools/client/webconsole/test/browser/browser_console_nsiconsolemessage.js new file mode 100644 index 0000000000..f3b606a76b --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_console_nsiconsolemessage.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check that nsIConsoleMessages are displayed in the Browser Console. + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf8, +<title>browser_console_nsiconsolemessage.js</title> +<p>hello world<p> +nsIConsoleMessages ftw!`; + +add_task(async function() { + // We don't use `openNewTabAndConsole()` here because we need to log a message + // before opening the web console. + await addTab(TEST_URI); + + // Test for cached nsIConsoleMessages. + Services.console.logStringMessage("cachedBrowserConsoleMessage"); + + info("open web console"); + let hud = await openConsole(); + + ok(hud, "web console opened"); + + // This "liveBrowserConsoleMessage" message should not be displayed. + Services.console.logStringMessage("liveBrowserConsoleMessage"); + + // Log a "foobarz" message so that we can be certain the previous message is + // not displayed. + let text = "foobarz"; + const onFooBarzMessage = waitForMessage(hud, text); + SpecialPowers.spawn(gBrowser.selectedBrowser, [text], function(msg) { + content.console.log(msg); + }); + await onFooBarzMessage; + ok(true, `"${text}" log is displayed in the Web Console as expected`); + + // Ensure the "liveBrowserConsoleMessage" and "cachedBrowserConsoleMessage" + // messages are not displayed. + text = hud.ui.outputNode.textContent; + ok( + !text.includes("cachedBrowserConsoleMessage"), + "cached nsIConsoleMessages are not displayed" + ); + ok( + !text.includes("liveBrowserConsoleMessage"), + "nsIConsoleMessages are not displayed" + ); + + await closeConsole(); + + info("web console closed"); + hud = await BrowserConsoleManager.toggleBrowserConsole(); + ok(hud, "browser console opened"); + + await waitFor(() => findMessage(hud, "cachedBrowserConsoleMessage")); + Services.console.logStringMessage("liveBrowserConsoleMessage2"); + await waitFor(() => findMessage(hud, "liveBrowserConsoleMessage2")); + + const msg = await waitFor(() => + findMessage(hud, "liveBrowserConsoleMessage") + ); + ok(msg, "message element for liveBrowserConsoleMessage (nsIConsoleMessage)"); + + // Disable the log filter. + await setFilterState(hud, { + log: false, + }); + + // And then checking that the log messages are hidden. + await waitFor( + () => findMessages(hud, "cachedBrowserConsoleMessage").length === 0 + ); + await waitFor( + () => findMessages(hud, "liveBrowserConsoleMessage").length === 0 + ); + await waitFor( + () => findMessages(hud, "liveBrowserConsoleMessage2").length === 0 + ); + + resetFilters(hud); +}); diff --git a/devtools/client/webconsole/test/browser/browser_console_open_or_focus.js b/devtools/client/webconsole/test/browser/browser_console_open_or_focus.js new file mode 100644 index 0000000000..935bbe7f41 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_console_open_or_focus.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the "browser console" menu item opens or focuses (if already open) +// the console window instead of toggling it open/close. + +"use strict"; +requestLongerTimeout(2); + +const TEST_MESSAGE = "testmessage"; +const { Tools } = require("devtools/client/definitions"); + +add_task(async function() { + info("Get main browser window"); + const mainWindow = Services.wm.getMostRecentWindow(null); + + info("Open the Browser Console"); + await BrowserConsoleManager.openBrowserConsoleOrFocus(); + + let hud = BrowserConsoleManager.getBrowserConsole(); + await waitFor(() => hud.ui.document.hasFocus()); + ok(true, "Focus is in the Browser Console"); + + info("Emit a log message to display it in the Browser Console"); + console.log(TEST_MESSAGE); + await waitFor(() => findMessage(hud, TEST_MESSAGE)); + + let currWindow = Services.wm.getMostRecentWindow(null); + is( + currWindow.document.documentURI, + Tools.webConsole.url, + "The Browser Console is open and has focus" + ); + + info("Focus the main browser window"); + mainWindow.focus(); + + info("Focus the Browser Console window"); + await BrowserConsoleManager.openBrowserConsoleOrFocus(); + currWindow = Services.wm.getMostRecentWindow(null); + is( + currWindow.document.documentURI, + Tools.webConsole.url, + "The Browser Console is open and has focus" + ); + + info("Close the Browser Console"); + await safeCloseBrowserConsole(); + + hud = BrowserConsoleManager.getBrowserConsole(); + ok(!hud, "Browser Console has been closed"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_console_restore.js b/devtools/client/webconsole/test/browser/browser_console_restore.js new file mode 100644 index 0000000000..19f30bff8a --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_console_restore.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check that the browser console gets session state is set correctly, and that +// it re-opens when restore is requested. + +"use strict"; + +add_task(async function() { + is( + BrowserConsoleManager.getBrowserConsoleSessionState(), + false, + "Session state false by default" + ); + BrowserConsoleManager.storeBrowserConsoleSessionState(); + is( + BrowserConsoleManager.getBrowserConsoleSessionState(), + false, + "Session state still not true even after setting (since Browser Console is closed)" + ); + + const hud = await BrowserConsoleManager.toggleBrowserConsole(); + BrowserConsoleManager.storeBrowserConsoleSessionState(); + is( + BrowserConsoleManager.getBrowserConsoleSessionState(), + true, + "Session state true (since Browser Console is opened)" + ); + + info( + "Closing the browser console and waiting for the session restore to reopen it" + ); + await safeCloseBrowserConsole(); + + const opened = waitForBrowserConsole(hud); + await gDevTools.restoreDevToolsSession({ + browserConsole: true, + }); + + info("Waiting for the console to open after session restore"); + await opened; +}); diff --git a/devtools/client/webconsole/test/browser/browser_console_webconsole_console_api_calls.js b/devtools/client/webconsole/test/browser/browser_console_webconsole_console_api_calls.js new file mode 100644 index 0000000000..a71295f5e9 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_console_webconsole_console_api_calls.js @@ -0,0 +1,150 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Test that console API calls in the content page appear in the browser console. + +"use strict"; + +const FILTER_PREFIX = "BC_TEST|"; + +const contentArgs = { + log: FILTER_PREFIX + "MyLog", + warn: FILTER_PREFIX + "MyWarn", + error: FILTER_PREFIX + "MyError", + exception: FILTER_PREFIX + "MyException", + info: FILTER_PREFIX + "MyInfo", + debug: FILTER_PREFIX + "MyDebug", + counterName: FILTER_PREFIX + "MyCounter", + timerName: FILTER_PREFIX + "MyTimer", +}; + +const TEST_URI = `data:text/html,<meta charset=utf8>console API calls<script> + console.log("${contentArgs.log}", {hello: "world"}); + console.warn("${contentArgs.warn}", {hello: "world"}); + console.error("${contentArgs.error}", {hello: "world"}); + console.exception("${contentArgs.exception}", {hello: "world"}); + console.info("${contentArgs.info}", {hello: "world"}); + console.debug("${contentArgs.debug}", {hello: "world"}); + console.count("${contentArgs.counterName}"); + console.time("${contentArgs.timerName}"); + console.timeLog("${contentArgs.timerName}", "MyTimeLog", {hello: "world"}); + console.timeEnd("${contentArgs.timerName}"); + console.trace("${FILTER_PREFIX}", {hello: "world"}); + console.assert(false, "${FILTER_PREFIX}", {hello: "world"}); + console.table(["${FILTER_PREFIX}", {hello: "world"}]); +</script>`; + +add_task(async function() { + // Show the content messages + await pushPref("devtools.browserconsole.contentMessages", true); + + info("Run once with Fission enabled"); + await pushPref("devtools.browsertoolbox.fission", true); + await checkContentConsoleApiMessages(true); + + info("Run once with Fission disabled"); + await pushPref("devtools.browsertoolbox.fission", false); + await checkContentConsoleApiMessages(false); +}); + +async function checkContentConsoleApiMessages(nonPrimitiveVariablesDisplayed) { + // Add the tab first so it creates the ContentProcess + const tab = await addTab(TEST_URI); + + // Open the Browser Console + const hud = await BrowserConsoleManager.toggleBrowserConsole(); + await setFilterState(hud, { text: FILTER_PREFIX }); + + // In non fission world, we don't retrieve cached messages, so we need to reload the + // tab to see them. + if (!nonPrimitiveVariablesDisplayed) { + const loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + tab.linkedBrowser.reload(); + await loaded; + } + + const suffix = nonPrimitiveVariablesDisplayed + ? ` Object { hello: "world" }` + : ""; + const expectedMessages = [ + contentArgs.log + suffix, + contentArgs.warn + suffix, + contentArgs.error + suffix, + contentArgs.exception + suffix, + contentArgs.info + suffix, + contentArgs.debug + suffix, + `${contentArgs.counterName}: 1`, + `MyTimeLog${suffix}`, + `timer ended`, + `console.trace() ${FILTER_PREFIX}${suffix}`, + `Assertion failed: ${FILTER_PREFIX}${suffix}`, + ]; + + // console.table is rendered as <unavailable> in non-fission browser console. + if (nonPrimitiveVariablesDisplayed) { + expectedMessages.push("console.table()"); + } + + await waitFor( + () => + expectedMessages.every(expectedMessage => + findMessage(hud, expectedMessage) + ), + "wait for all the messages to be displayed", + 100 + ); + ok(true, "Expected messages are displayed in the browser console"); + + if (nonPrimitiveVariablesDisplayed) { + const tableMessage = findMessage(hud, "console.table()", ".message.table"); + + const table = await waitFor(() => + tableMessage.querySelector(".new-consoletable") + ); + ok(table, "There is a table element"); + const tableTextContent = table.textContent; + ok( + tableTextContent.includes(FILTER_PREFIX) && + tableTextContent.includes(`world`) && + tableTextContent.includes(`hello`), + "Table has expected content" + ); + } + + info("Uncheck the Show content messages checkbox"); + const onContentMessagesHidden = waitFor( + () => !findMessage(hud, contentArgs.log) + ); + await toggleConsoleSetting( + hud, + ".webconsole-console-settings-menu-item-contentMessages" + ); + await onContentMessagesHidden; + + for (const expectedMessage of expectedMessages) { + ok(!findMessage(hud, expectedMessage), `"${expectedMessage}" is hidden`); + } + + info("Check the Show content messages checkbox"); + const onContentMessagesDisplayed = waitFor(() => + expectedMessages.every(expectedMessage => findMessage(hud, expectedMessage)) + ); + await toggleConsoleSetting( + hud, + ".webconsole-console-settings-menu-item-contentMessages" + ); + await onContentMessagesDisplayed; + + for (const expectedMessage of expectedMessages) { + ok(findMessage(hud, expectedMessage), `"${expectedMessage}" is visible`); + } + + info("Clear and close the Browser Console"); + await clearOutput(hud); + // We use waitForTick here because of a race condition. Otherwise, the test + // would occassionally fail because the transport is closed before pending server + // responses have been sent. + await waitForTick(); + await safeCloseBrowserConsole(); +} diff --git a/devtools/client/webconsole/test/browser/browser_console_webconsole_ctrlw_close_tab.js b/devtools/client/webconsole/test/browser/browser_console_webconsole_ctrlw_close_tab.js new file mode 100644 index 0000000000..6b7ce88802 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_console_webconsole_ctrlw_close_tab.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check that Ctrl-W closes the Browser Console and that Ctrl-W closes the +// current tab when using the Web Console - bug 871156. + +"use strict"; + +add_task(async function() { + const TEST_URI = + "data:text/html;charset=utf8,<title>bug871156</title>\n" + "<p>hello world"; + const firstTab = gBrowser.selectedTab; + + let hud = await openNewTabAndConsole(TEST_URI); + + const target = await TargetFactory.forTab(gBrowser.selectedTab); + const toolbox = gDevTools.getToolbox(target); + + const tabClosed = once(gBrowser.tabContainer, "TabClose"); + tabClosed.then(() => info("tab closed")); + + const tabSelected = new Promise(resolve => { + gBrowser.tabContainer.addEventListener( + "TabSelect", + function() { + if (gBrowser.selectedTab == firstTab) { + info("tab selected"); + resolve(null); + } + }, + { once: true } + ); + }); + + const toolboxDestroyed = toolbox.once("destroyed", () => { + info("toolbox destroyed"); + }); + + // Get out of the web console initialization. + executeSoon(() => { + EventUtils.synthesizeKey("w", { accelKey: true }); + }); + + await Promise.all([tabClosed, toolboxDestroyed, tabSelected]); + info("Promise.all resolved. start testing the Browser Console"); + + hud = await BrowserConsoleManager.toggleBrowserConsole(); + ok(hud, "Browser Console opened"); + + const onBrowserConsoleClosed = new Promise(resolve => { + Services.obs.addObserver(function onDestroy() { + Services.obs.removeObserver(onDestroy, "web-console-destroyed"); + resolve(); + }, "web-console-destroyed"); + }); + + await waitForAllTargetsToBeAttached(hud.targetList); + waitForFocus(() => { + EventUtils.synthesizeKey("w", { accelKey: true }, hud.iframeWindow); + }, hud.iframeWindow); + + await onBrowserConsoleClosed; + ok(true, "the Browser Console closed"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_console_webconsole_iframe_messages.js b/devtools/client/webconsole/test/browser/browser_console_webconsole_iframe_messages.js new file mode 100644 index 0000000000..1ea572f826 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_console_webconsole_iframe_messages.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check that cached messages from nested iframes are displayed in the +// Web/Browser Console. + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-console-iframes.html"; + +const expectedMessages = ["main file", "blah", "iframe 2", "iframe 3"]; + +const expectedDupedMessage = "iframe 1"; + +add_task(async function() { + // On e10s, the exception is triggered in child process + // and is ignored by test harness + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } + + let hud = await openNewTabAndConsole(TEST_URI); + + await testMessages(hud); + await closeConsole(); + info("web console closed"); + + hud = await BrowserConsoleManager.toggleBrowserConsole(); + await testBrowserConsole(hud); + + // clear the browser console. + await clearOutput(hud); + await waitForTick(); + await safeCloseBrowserConsole(); +}); + +async function testMessages(hud) { + for (const message of expectedMessages) { + info(`checking that the message "${message}" exists`); + await waitFor(() => findMessage(hud, message)); + } + + info("first messages matched"); + + const messages = await findMessages(hud, expectedDupedMessage); + is(messages.length, 2, `${expectedDupedMessage} is present twice`); +} + +async function testBrowserConsole(hud) { + ok(hud, "browser console opened"); + + // TODO: The browser console doesn't show page's console.log statements + // in e10s windows. See Bug 1241289. + if (Services.appinfo.browserTabsRemoteAutostart) { + todo(false, "Bug 1241289"); + return; + } + + await testMessages(hud); +} diff --git a/devtools/client/webconsole/test/browser/browser_console_webconsole_private_browsing.js b/devtools/client/webconsole/test/browser/browser_console_webconsole_private_browsing.js new file mode 100644 index 0000000000..6bbb5c358e --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_console_webconsole_private_browsing.js @@ -0,0 +1,139 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Bug 874061: test for how the browser and web consoles display messages coming +// from private windows. See bug for description of expected behavior. + +"use strict"; + +const NON_PRIVATE_MESSAGE = "This is not a private message"; +const PRIVATE_MESSAGE = "This is a private message"; +const PRIVATE_UNDEFINED_FN = "privateException"; +const PRIVATE_EXCEPTION = `${PRIVATE_UNDEFINED_FN} is not defined`; + +const NON_PRIVATE_TEST_URI = "data:text/html;charset=utf8,Not private"; +const PRIVATE_TEST_URI = `data:text/html;charset=utf8,Test console in private windows + <script> + function logMessages() { + /* Wrap the exception so we don't throw in ContentTask. */ + setTimeout(() => { + console.log("${PRIVATE_MESSAGE}"); + ${PRIVATE_UNDEFINED_FN}(); + }, 10); + } + </script>`; + +add_task(async function() { + await pushPref("devtools.browserconsole.contentMessages", true); + await addTab(NON_PRIVATE_TEST_URI); + + const privateWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + ok(PrivateBrowsingUtils.isWindowPrivate(privateWindow), "window is private"); + const privateBrowser = privateWindow.gBrowser; + privateBrowser.selectedTab = BrowserTestUtils.addTab( + privateBrowser, + PRIVATE_TEST_URI + ); + const privateTab = privateBrowser.selectedTab; + + info("private tab opened"); + ok( + PrivateBrowsingUtils.isBrowserPrivate(privateBrowser.selectedBrowser), + "tab window is private" + ); + + let hud = await openConsole(privateTab); + ok(hud, "web console opened"); + + const onLogMessage = waitForMessage(hud, PRIVATE_MESSAGE); + const onErrorMessage = waitForMessage(hud, PRIVATE_EXCEPTION, ".error"); + logPrivateMessages(privateBrowser.selectedBrowser); + + await onLogMessage; + await onErrorMessage; + ok(true, "Messages are displayed as expected"); + + info("test cached messages"); + await closeConsole(privateTab); + info("web console closed"); + hud = await openConsole(privateTab); + ok(hud, "web console reopened"); + + await waitFor(() => findMessage(hud, PRIVATE_MESSAGE)); + await waitFor(() => findMessage(hud, PRIVATE_EXCEPTION, ".message.error")); + ok( + true, + "Messages are still displayed after closing and reopening the console" + ); + + info("Test browser console"); + await closeConsole(privateTab); + info("web console closed"); + hud = await BrowserConsoleManager.toggleBrowserConsole(); + + // Make sure that the cached messages from private tabs are not displayed in the + // browser console. + assertNoPrivateMessages(hud); + + // Add a non-private message to the console. + const onBrowserConsoleNonPrivateMessage = waitForMessage( + hud, + NON_PRIVATE_MESSAGE + ); + SpecialPowers.spawn(gBrowser.selectedBrowser, [NON_PRIVATE_MESSAGE], function( + msg + ) { + content.console.log(msg); + }); + await onBrowserConsoleNonPrivateMessage; + + const onBrowserConsolePrivateLogMessage = waitForMessage( + hud, + PRIVATE_MESSAGE + ); + const onBrowserConsolePrivateErrorMessage = waitForMessage( + hud, + PRIVATE_EXCEPTION, + ".error" + ); + logPrivateMessages(privateBrowser.selectedBrowser); + + await onBrowserConsolePrivateLogMessage; + await onBrowserConsolePrivateErrorMessage; + ok(true, "Messages are displayed as expected"); + + info("close the private window and check if private messages are removed"); + const onPrivateMessagesCleared = hud.ui.once("private-messages-cleared"); + privateWindow.BrowserTryToCloseWindow(); + await onPrivateMessagesCleared; + + ok( + findMessage(hud, NON_PRIVATE_MESSAGE), + "non-private messages are still shown after private window closed" + ); + assertNoPrivateMessages(hud); + + info("close the browser console"); + await safeCloseBrowserConsole(); + + info("reopen the browser console"); + hud = await BrowserConsoleManager.toggleBrowserConsole(); + ok(hud, "browser console reopened"); + + assertNoPrivateMessages(hud); +}); + +function logPrivateMessages(browser) { + SpecialPowers.spawn(browser, [], () => content.wrappedJSObject.logMessages()); +} + +function assertNoPrivateMessages(hud) { + is( + findMessage(hud, PRIVATE_MESSAGE), + undefined, + "no console message displayed" + ); + is(findMessage(hud, PRIVATE_EXCEPTION), undefined, "no exception displayed"); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_add_edited_input_to_history.js b/devtools/client/webconsole/test/browser/browser_jsterm_add_edited_input_to_history.js new file mode 100644 index 0000000000..1391d158a3 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_add_edited_input_to_history.js @@ -0,0 +1,79 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that user input that is not submitted in the command line input is not +// lost after navigating in history. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=817834 + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 817834"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + ok(!getInputValue(hud), "console input is empty"); + checkInputCursorPosition(hud, 0, "Cursor is at expected position"); + + setInputValue(hud, '"first item"'); + EventUtils.synthesizeKey("KEY_ArrowUp"); + is(getInputValue(hud), '"first item"', "null test history up"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + is(getInputValue(hud), '"first item"', "null test history down"); + + EventUtils.synthesizeKey("KEY_Enter"); + await waitFor(() => findMessage(hud, "first item", ".result")); + is(getInputValue(hud), "", "cleared input line after submit"); + + setInputValue(hud, '"editing input 1"'); + EventUtils.synthesizeKey("KEY_ArrowUp"); + is(getInputValue(hud), '"first item"', "test history up"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + is( + getInputValue(hud), + '"editing input 1"', + "test history down restores in-progress input" + ); + + setInputValue(hud, '"second item"'); + EventUtils.synthesizeKey("KEY_Enter"); + await waitFor(() => findMessage(hud, "second item", ".result")); + + setInputValue(hud, '"editing input 2"'); + EventUtils.synthesizeKey("KEY_ArrowUp"); + is(getInputValue(hud), '"second item"', "test history up"); + EventUtils.synthesizeKey("KEY_ArrowUp"); + is(getInputValue(hud), '"first item"', "test history up"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + is(getInputValue(hud), '"second item"', "test history down"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + is( + getInputValue(hud), + '"editing input 2"', + "test history down restores new in-progress input again" + ); + + // Appending the same value again should not impact the history. + // Let's also use some spaces around to check that the input value + // is properly trimmed. + setInputValue(hud, '"second item"'); + EventUtils.synthesizeKey("KEY_Enter"); + await waitFor(() => findMessages(hud, "second item", ".result").length == 2); + + setInputValue(hud, '"second item" '); + EventUtils.synthesizeKey("KEY_Enter"); + await waitFor(() => findMessages(hud, "second item", ".result").length == 3); + + EventUtils.synthesizeKey("KEY_ArrowUp"); + is( + getInputValue(hud), + '"second item"', + "test history up reaches duplicated entry just once" + ); + EventUtils.synthesizeKey("KEY_ArrowUp"); + is( + getInputValue(hud), + '"first item"', + "test history up reaches the previous value" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete-properties-with-non-alphanumeric-names.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete-properties-with-non-alphanumeric-names.js new file mode 100644 index 0000000000..0150b0c68f --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete-properties-with-non-alphanumeric-names.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that properties starting with underscores or dollars can be +// autocompleted (bug 967468). +const TEST_URI = `data:text/html;charset=utf8,test autocompletion with $ or _`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + await executeAndWaitForMessage( + hud, + "var testObject = {$$aaab: '', $$aaac: ''}", + "", + ".message.result" + ); + + // Should work with bug 967468. + await testAutocomplete(hud, "Object.__d"); + await testAutocomplete(hud, "testObject.$$a"); + + // Here's when things go wrong in bug 967468. + await testAutocomplete(hud, "Object.__de"); + await testAutocomplete(hud, "testObject.$$aa"); + + // Should work with bug 1207868. + await executeAndWaitForMessage( + hud, + "let foobar = {a: ''}; const blargh = {a: 1};", + "", + ".message.result" + ); + await testAutocomplete(hud, "foobar"); + await testAutocomplete(hud, "blargh"); + await testAutocomplete(hud, "foobar.a"); + await testAutocomplete(hud, "blargh.a"); +}); + +async function testAutocomplete(hud, inputString) { + await setInputValueForAutocompletion(hud, inputString); + const popup = hud.jsterm.autocompletePopup; + ok( + popup.itemCount > 0, + `There's ${popup.itemCount} suggestions for '${inputString}'` + ); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_accept_no_scroll.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_accept_no_scroll.js new file mode 100644 index 0000000000..5260f668f3 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_accept_no_scroll.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the accepting an autocompletion does not scroll the input. + +const TEST_URI = `data:text/html;charset=utf-8, + <script> + /* Create a prototype-less object so popup does not contain native + * Object prototype properties. + */ + window.foobar = Object.create(null, Object.getOwnPropertyDescriptors({ + item0: "value0", + item1: "value1", + })); + </script>`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm, ui } = hud; + const { autocompletePopup: popup } = jsterm; + + info("Insert multiple new lines so the input overflows"); + const onPopUpOpen = popup.once("popup-opened"); + const lines = "\n".repeat(200); + setInputValue(hud, lines); + + info("Fire the autocompletion popup"); + EventUtils.sendString("window.foobar."); + + await onPopUpOpen; + const scrollableEl = ui.window.document.querySelector(".CodeMirror-scroll"); + + ok(scrollableEl.scrollTop > 0, "The input overflows"); + const scrollTop = scrollableEl.scrollTop; + + info("Hit Enter to accept the autocompletion"); + const onPopupClose = popup.once("popup-closed"); + EventUtils.synthesizeKey("KEY_Enter"); + await onPopupClose; + + ok(!popup.isOpen, "popup is not open after KEY_Enter"); + is( + getInputValue(hud), + lines + "window.foobar.item0", + "completion was successful after KEY_Enter" + ); + is( + scrollableEl.scrollTop, + scrollTop, + "The scrolling position stayed the same when accepting the completion" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_array_no_index.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_array_no_index.js new file mode 100644 index 0000000000..d506dc089d --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_array_no_index.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// See Bug 585991. + +const TEST_URI = `data:text/html;charset=utf-8, +<head> + <script> + window.foo = [1,2,3]; + </script> +</head> +<body>bug 585991 - Autocomplete popup on array</body>`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { autocompletePopup: popup } = hud.jsterm; + + const onPopUpOpen = popup.once("popup-opened"); + + info("wait for popup to show"); + setInputValue(hud, "foo"); + EventUtils.sendString("."); + + await onPopUpOpen; + + ok( + !hasPopupLabel(popup, "0"), + "Completing on an array doesn't show numbers." + ); + + info("press Escape to close the popup"); + const onPopupClose = popup.once("popup-closed"); + EventUtils.synthesizeKey("KEY_Escape"); + + await onPopupClose; +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_arrow_keys.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_arrow_keys.js new file mode 100644 index 0000000000..d1fe18b2b6 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_arrow_keys.js @@ -0,0 +1,237 @@ +/* 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,<head><script> + /* Create a prototype-less object so popup does not contain native + * Object prototype properties. + */ + window.foo = Object.create(null, Object.getOwnPropertyDescriptors({ + aa: "a", + bbb: "b", + bbbb: "b", + })); + </script></head><body>Autocomplete text navigation key usage test</body>`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + const { autocompletePopup: popup } = jsterm; + + await checkWordNavigation(hud); + await checkArrowLeftDismissPopup(hud); + await checkArrowLeftDismissCompletion(hud); + await checkArrowRightAcceptCompletion(hud); + + info( + "Test that Ctrl/Cmd + Right closes the popup if there's text after cursor" + ); + setInputValue(hud, "."); + EventUtils.synthesizeKey("KEY_ArrowLeft"); + const onAutocompleteUpdated = jsterm.once("autocomplete-updated"); + EventUtils.sendString("win"); + await onAutocompleteUpdated; + ok(popup.isOpen, "popup is open"); + + const isOSX = Services.appinfo.OS == "Darwin"; + const onPopUpClose = popup.once("popup-closed"); + EventUtils.synthesizeKey("KEY_ArrowRight", { + [isOSX ? "metaKey" : "ctrlKey"]: true, + }); + await onPopUpClose; + is(getInputValue(hud), "win.", "input value wasn't modified"); +}); + +async function checkArrowLeftDismissPopup(hud) { + const popup = hud.jsterm.autocompletePopup; + let tests; + if (Services.appinfo.OS == "Darwin") { + tests = [ + { + keyOption: null, + expectedInput: "window.foo.b|b", + }, + { + keyOption: { metaKey: true }, + expectedInput: "|window.foo.bb", + }, + { + keyOption: { altKey: true }, + expectedInput: "window.foo.|bb", + }, + ]; + } else { + tests = [ + { + keyOption: null, + expectedInput: "window.foo.b|b", + }, + { + keyOption: { ctrlKey: true }, + expectedInput: "window.foo.|bb", + }, + ]; + } + + for (const test of tests) { + info("Trigger autocomplete popup opening"); + const onPopUpOpen = popup.once("popup-opened"); + await setInputValueForAutocompletion(hud, "window.foo.bb"); + await onPopUpOpen; + + // checkInput is asserting the cursor position with the "|" char. + checkInputValueAndCursorPosition(hud, "window.foo.bb|"); + is(popup.isOpen, true, "popup is open"); + checkInputCompletionValue(hud, "b", "completeNode has expected value"); + + const { keyOption, expectedInput } = test; + info(`Test that arrow left closes the popup and clears complete node`); + const onPopUpClose = popup.once("popup-closed"); + EventUtils.synthesizeKey("KEY_ArrowLeft", keyOption); + await onPopUpClose; + + checkInputValueAndCursorPosition(hud, expectedInput); + is(popup.isOpen, false, "popup is closed"); + checkInputCompletionValue(hud, "", "completeNode is empty"); + } + setInputValue(hud, ""); +} + +async function checkArrowLeftDismissCompletion(hud) { + let tests; + if (Services.appinfo.OS == "Darwin") { + tests = [ + { + keyOption: null, + expectedInput: "window.foo.|a", + }, + { + keyOption: { metaKey: true }, + expectedInput: "|window.foo.a", + }, + { + keyOption: { altKey: true }, + expectedInput: "window.foo.|a", + }, + ]; + } else { + tests = [ + { + keyOption: null, + expectedInput: "window.foo.|a", + }, + { + keyOption: { ctrlKey: true }, + expectedInput: "window.foo.|a", + }, + ]; + } + + for (const test of tests) { + await setInputValueForAutocompletion(hud, "window.foo.a"); + checkInputCompletionValue(hud, "a", "completeNode has expected value"); + + info(`Test that arrow left dismiss the completion text`); + const { keyOption, expectedInput } = test; + EventUtils.synthesizeKey("KEY_ArrowLeft", keyOption); + + checkInputValueAndCursorPosition(hud, expectedInput); + checkInputCompletionValue(hud, "", "completeNode is empty"); + } + setInputValue(hud, ""); +} + +async function checkArrowRightAcceptCompletion(hud) { + const popup = hud.jsterm.autocompletePopup; + let tests; + if (Services.appinfo.OS == "Darwin") { + tests = [ + { + keyOption: null, + }, + { + keyOption: { metaKey: true }, + }, + { + keyOption: { altKey: true }, + }, + ]; + } else { + tests = [ + { + keyOption: null, + }, + { + keyOption: { ctrlKey: true }, + }, + ]; + } + + for (const test of tests) { + info("Trigger autocomplete popup opening"); + const onPopUpOpen = popup.once("popup-opened"); + await setInputValueForAutocompletion(hud, `window.foo.bb`); + await onPopUpOpen; + + // checkInput is asserting the cursor position with the "|" char. + checkInputValueAndCursorPosition(hud, `window.foo.bb|`); + is(popup.isOpen, true, "popup is open"); + checkInputCompletionValue(hud, "b", "completeNode has expected value"); + + const { keyOption } = test; + info(`Test that arrow right closes the popup and accepts the completion`); + const onPopUpClose = popup.once("popup-closed"); + EventUtils.synthesizeKey("KEY_ArrowRight", keyOption); + await onPopUpClose; + + checkInputValueAndCursorPosition(hud, "window.foo.bbb|"); + is(popup.isOpen, false, "popup is closed"); + checkInputCompletionValue(hud, "", "completeNode is empty"); + } + setInputValue(hud, ""); +} + +async function checkWordNavigation(hud) { + const accelKey = Services.appinfo.OS == "Darwin" ? "altKey" : "ctrlKey"; + const goLeft = () => + EventUtils.synthesizeKey("KEY_ArrowLeft", { [accelKey]: true }); + const goRight = () => + EventUtils.synthesizeKey("KEY_ArrowRight", { [accelKey]: true }); + + setInputValue(hud, "aa bb cc dd"); + checkInputValueAndCursorPosition(hud, "aa bb cc dd|"); + + goRight(); + checkInputValueAndCursorPosition(hud, "aa bb cc dd|"); + + goLeft(); + checkInputValueAndCursorPosition(hud, "aa bb cc |dd"); + + goLeft(); + checkInputValueAndCursorPosition(hud, "aa bb |cc dd"); + + goLeft(); + checkInputValueAndCursorPosition(hud, "aa |bb cc dd"); + + goLeft(); + checkInputValueAndCursorPosition(hud, "|aa bb cc dd"); + + goLeft(); + checkInputValueAndCursorPosition(hud, "|aa bb cc dd"); + + goRight(); + // Windows differ from other platforms, going to the start of the next string. + checkInputValueAndCursorPosition(hud, "aa| bb cc dd"); + + goRight(); + checkInputValueAndCursorPosition(hud, "aa bb| cc dd"); + + goRight(); + checkInputValueAndCursorPosition(hud, "aa bb cc| dd"); + + goRight(); + checkInputValueAndCursorPosition(hud, "aa bb cc dd|"); + + setInputValue(hud, ""); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_await.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_await.js new file mode 100644 index 0000000000..1cf9eb1b32 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_await.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// See Bug 585991. + +const TEST_URI = `data:text/html;charset=utf-8,Autocomplete await expression`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + const { autocompletePopup } = jsterm; + + info("Check that the await keyword is in the autocomplete"); + await setInputValueForAutocompletion(hud, "aw"); + checkInputCompletionValue(hud, "ait", "completeNode has expected value"); + + EventUtils.synthesizeKey("KEY_Tab"); + is(getInputValue(hud), "await", "'await' tab completion"); + + const updated = jsterm.once("autocomplete-updated"); + EventUtils.sendString(" "); + await updated; + + info("Check that the autocomplete popup is displayed"); + const onPopUpOpen = autocompletePopup.once("popup-opened"); + EventUtils.sendString("P"); + await onPopUpOpen; + + ok(autocompletePopup.isOpen, "popup is open"); + ok( + autocompletePopup.items.some(item => item.label === "Promise"), + "popup has expected `Promise` item" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_cached_results.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_cached_results.js new file mode 100644 index 0000000000..db3fa48d2e --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_cached_results.js @@ -0,0 +1,144 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that the cached autocomplete results are used when the new +// user input is a subset of the existing completion results. + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf8,<script> + x = Object.create(null, Object.getOwnPropertyDescriptors({ + dog: "woof", + dos: "-", + dot: ".", + duh: 1, + wut: 2, + })) + </script>`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + const { autocompletePopup: popup } = jsterm; + + const jstermComplete = (value, pos) => + setInputValueForAutocompletion(hud, value, pos); + + await jstermComplete("x."); + is( + getAutocompletePopupLabels(popup).toString(), + ["dog", "dos", "dot", "duh", "wut"].toString(), + "'x.' gave a list of suggestions" + ); + ok(popup.isOpen, "popup is opened"); + + info("Add a property on the object"); + // Using the console front directly as we don't want to impact the UI state. + let { result } = await hud.evaluateJSAsync( + `x.docfoobar = "added"; x.docfoobar` + ); + is(result, "added", "The property was added on the window object"); + + info("Test typing d (i.e. input is now 'x.d')"); + let onUpdated = jsterm.once("autocomplete-updated"); + EventUtils.synthesizeKey("d"); + await onUpdated; + ok( + hasExactPopupLabels(popup, ["dog", "dos", "dot", "duh"]), + "autocomplete popup does not contain docfoobar. List has not been updated" + ); + + // Test typing o (i.e. input is now 'x.do'). + onUpdated = jsterm.once("autocomplete-updated"); + EventUtils.synthesizeKey("o"); + await onUpdated; + ok( + hasExactPopupLabels(popup, ["dog", "dos", "dot"]), + "autocomplete popup still does not contain docfoobar. List has not been updated" + ); + + // Test that backspace does not cause a request to the server + onUpdated = jsterm.once("autocomplete-updated"); + EventUtils.synthesizeKey("KEY_Backspace"); + await onUpdated; + ok( + hasExactPopupLabels(popup, ["dog", "dos", "dot", "duh"]), + "autocomplete cached results do not contain docfoobar. list has not been updated" + ); + + // Using the console front directly as we don't want to impact the UI state. + ({ result } = await hud.evaluateJSAsync(`delete x.docfoobar; x.docfoobar`)); + is(result.type, "undefined", "The property was removed"); + + // Test if 'window.getC' gives 'getComputedStyle' + await jstermComplete("window."); + await jstermComplete("window.getC"); + ok( + hasPopupLabel(popup, "getComputedStyle"), + "autocomplete results do contain getComputedStyle" + ); + + // Test if 'dump(d' gives non-zero results + await jstermComplete("dump(d"); + ok(popup.getItems().length > 0, "'dump(d' gives non-zero results"); + + // Test that 'dump(x.)' works. + await jstermComplete("dump(x)", -1); + onUpdated = jsterm.once("autocomplete-updated"); + EventUtils.sendString("."); + await onUpdated; + ok( + hasExactPopupLabels(popup, ["dog", "dos", "dot", "duh", "wut"]), + "'dump(x.' gave a list of suggestions" + ); + + info("Add a property on the x object"); + // Using the console front directly as we don't want to impact the UI state. + ({ result } = await hud.evaluateJSAsync( + `x.docfoobar = "added"; x.docfoobar` + )); + is(result, "added", "The property was added on the x object again"); + + // Make sure 'dump(x.d)' does not contain 'docfoobar'. + onUpdated = jsterm.once("autocomplete-updated"); + EventUtils.sendString("d"); + await onUpdated; + + ok( + !hasPopupLabel(popup, "docfoobar"), + "autocomplete cached results do not contain docfoobar. list has not been updated" + ); + + info("Ensure filtering from the cache does work"); + execute( + hud, + ` + window.testObject = Object.create(null); + window.testObject.zz = "zz"; + window.testObject.zzz = "zzz"; + window.testObject.zzzz = "zzzz"; + ` + ); + await jstermComplete("window.testObject."); + await jstermComplete("window.testObject.z"); + ok( + hasExactPopupLabels(popup, ["zz", "zzz", "zzzz"]), + "results are the expected ones" + ); + + onUpdated = jsterm.once("autocomplete-updated"); + EventUtils.sendString("z"); + await onUpdated; + ok( + hasExactPopupLabels(popup, ["zz", "zzz", "zzzz"]), + "filtering from the cache works - step 1" + ); + + onUpdated = jsterm.once("autocomplete-updated"); + EventUtils.sendString("z"); + await onUpdated; + ok( + hasExactPopupLabels(popup, ["zzz", "zzzz"]), + "filtering from the cache works - step 2" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_commands.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_commands.js new file mode 100644 index 0000000000..abcf281e71 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_commands.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that console commands are autocompleted. + +const TEST_URI = `data:text/html;charset=utf-8,Test command autocomplete`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + const { autocompletePopup } = jsterm; + + info(`Enter ":"`); + jsterm.focus(); + let onAutocompleUpdated = jsterm.once("autocomplete-updated"); + EventUtils.sendString(":"); + await onAutocompleUpdated; + + const expectedCommands = [":block", ":help", ":screenshot", ":unblock"]; + ok( + hasExactPopupLabels(autocompletePopup, expectedCommands), + "popup contains expected commands" + ); + + onAutocompleUpdated = jsterm.once("autocomplete-updated"); + EventUtils.sendString("s"); + await onAutocompleUpdated; + checkInputCompletionValue( + hud, + "creenshot", + "completion node has expected :screenshot value" + ); + + onAutocompleUpdated = jsterm.once("autocomplete-updated"); + EventUtils.synthesizeKey("KEY_Tab"); + await onAutocompleUpdated; + is( + getInputValue(hud), + ":screenshot", + "Tab key correctly completed :screenshot" + ); + + ok(!autocompletePopup.isOpen, "popup is closed after Tab"); + + info("Test :hel completion"); + await setInputValue(hud, ":he"); + onAutocompleUpdated = jsterm.once("autocomplete-updated"); + EventUtils.sendString("l"); + + await onAutocompleUpdated; + checkInputCompletionValue( + hud, + "p", + "completion node has expected :help value" + ); + + EventUtils.synthesizeKey("KEY_Tab"); + is(getInputValue(hud), ":help", "Tab key correctly completes :help"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_control_space.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_control_space.js new file mode 100644 index 0000000000..8f37235275 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_control_space.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that Ctrl+Space displays the autocompletion popup when it's hidden. + +const TEST_URI = `data:text/html;charset=utf-8, +<head> + <script> + /* Create a prototype-less object so popup does not contain native + * Object prototype properties. + */ + var foo = Object.create(null, Object.getOwnPropertyDescriptors({ + item0: "value0", + item1: "value1", + })); + </script> +</head> +<body>bug 585991 - autocomplete popup ctrl+space usage test</body>`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + info("web console opened"); + + const { autocompletePopup: popup } = hud.jsterm; + + info("wait for completion: foo."); + await setInputValueForAutocompletion(hud, "foo."); + + const { itemCount } = popup; + ok(popup.isOpen, "popup is open"); + ok(itemCount > 0, "popup has items"); + + info("Check that Ctrl+Space when the popup is opened has no effect"); + EventUtils.synthesizeKey(" ", { ctrlKey: true }); + ok(popup.isOpen, "The popup wasn't closed on Ctrl+Space"); + is(popup.itemCount, itemCount, "The popup wasn't modified on Ctrl+Space"); + + info("press Escape to close the popup"); + let onPopupClose = popup.once("popup-closed"); + EventUtils.synthesizeKey("KEY_Escape"); + await onPopupClose; + ok(!popup.isOpen, "popup is not open after VK_ESCAPE"); + + info("Check that Ctrl+Space opens the popup when it was closed"); + const onAutocompleteUpdated = hud.jsterm.once("autocomplete-updated"); + EventUtils.synthesizeKey(" ", { ctrlKey: true }); + await onAutocompleteUpdated; + + ok(popup.isOpen, "popup opens on Ctrl+Space"); + is(popup.itemCount, itemCount, "popup has the expected items"); + + info("Close the popup again"); + onPopupClose = popup.once("popup-closed"); + EventUtils.synthesizeKey("KEY_Escape"); + await onPopupClose; +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_crossdomain_iframe.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_crossdomain_iframe.js new file mode 100644 index 0000000000..661dded1f7 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_crossdomain_iframe.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that autocomplete doesn't break when trying to reach into objects from +// a different domain. See Bug 989025. + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-iframe-parent.html"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + await executeAndWaitForMessage( + hud, + "document.title", + "iframe parent", + ".result" + ); + ok(true, "root document's title is accessible"); + + // Make sure we don't throw when trying to autocomplete + const autocompleteUpdated = hud.jsterm.once("autocomplete-updated"); + setInputValue(hud, "window[0].document"); + EventUtils.sendString("."); + await autocompleteUpdated; + + setInputValue(hud, "window[0].document.title"); + const onPermissionDeniedMessage = waitForMessage(hud, "Permission denied"); + EventUtils.synthesizeKey("KEY_Enter"); + const permissionDenied = await onPermissionDeniedMessage; + ok( + permissionDenied.node.classList.contains("error"), + "A message error is shown when trying to inspect window[0]" + ); + + await executeAndWaitForMessage( + hud, + "window.location", + "test-iframe-parent.html", + ".result" + ); + ok(true, "root document's location is accessible"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_del_key.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_del_key.js new file mode 100644 index 0000000000..36685be812 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_del_key.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// See Bug 585991. + +const TEST_URI = `data:text/html;charset=utf-8, +<head> + <script> + foo = { + item0: "value0", + item1: "value1", + }; + </script> +</head> +<body>Autocomplete popup delete key usage test</body>`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + info("web console opened"); + + const { autocompletePopup: popup } = jsterm; + await setInputValueForAutocompletion(hud, "foo.i"); + + ok(popup.isOpen, "popup is open"); + + info("press Delete"); + const onPopupClose = popup.once("popup-closed"); + const onTimeout = wait(1000).then(() => "timeout"); + EventUtils.synthesizeKey("KEY_Delete"); + + const result = await Promise.race([onPopupClose, onTimeout]); + + is(result, "timeout", "The timeout won the race"); + ok(popup.isOpen, "popup is open after hitting delete key"); + + await closeAutocompletePopup(hud); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_disabled.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_disabled.js new file mode 100644 index 0000000000..e59557a896 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_disabled.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that disabling autocomplete for console + +const TEST_URI = `data:text/html;charset=utf-8,Test command autocomplete`; + +add_task(async function() { + // Run with autocomplete preference as false + await pushPref("devtools.webconsole.input.autocomplete", false); + await performTests_false(); +}); + +async function performTests_false() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + info("web console opened"); + + const { autocompletePopup: popup } = hud.jsterm; + + info(`Enter "w"`); + jsterm.focus(); + EventUtils.sendString("w"); + // delay of 2 seconds. + await wait(2000); + ok(!popup.isOpen, "popup is not open"); + + info("Check that Ctrl+Space opens the popup when preference is false"); + let onUpdated = jsterm.once("autocomplete-updated"); + EventUtils.synthesizeKey(" ", { ctrlKey: true }); + await onUpdated; + + ok(popup.isOpen, "popup opens on Ctrl+Space"); + ok(popup.getItems().length > 0, "'w' gave a list of suggestions"); + + onUpdated = jsterm.once("autocomplete-updated"); + EventUtils.synthesizeKey("in"); + await onUpdated; + ok(popup.getItems().length == 2, "'win' gave a list of suggestions"); + + info("Check that the completion text is updated when it was displayed"); + await setInputValue(hud, ""); + EventUtils.sendString("deb"); + let updated = jsterm.once("autocomplete-updated"); + EventUtils.synthesizeKey(" ", { ctrlKey: true }); + await updated; + + ok(!popup.isOpen, "popup is not open"); + is( + jsterm.getAutoCompletionText(), + "ugger", + "completion text has expected value" + ); + + updated = jsterm.once("autocomplete-updated"); + EventUtils.sendString("u"); + await updated; + is( + jsterm.getAutoCompletionText(), + "gger", + "completion text has expected value" + ); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_eager_evaluation.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_eager_evaluation.js new file mode 100644 index 0000000000..eccc5f1883 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_eager_evaluation.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that the main expression is eagerly evaluated and its results are used in the autocomple popup + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf8,Test autocompletion for expression variables<script> + var testObj = { + fun: () => ({ yay: "yay", yo: "yo", boo: "boo" }) + }; + </script>`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + const { autocompletePopup } = jsterm; + + const cases = [ + { input: "testObj.fun().y", results: ["yay", "yo"] }, + { + input: `Array.from([1,2,3]).reduce((i, agg) => agg + i).toS`, + results: ["toString"], + }, + { input: `1..toE`, results: ["toExponential"] }, + ]; + + for (const test of cases) { + info(`Test: ${test.input}`); + await setInputValueForAutocompletion(hud, test.input); + ok( + hasExactPopupLabels(autocompletePopup, test.results), + "Autocomplete popup shows expected results: " + + getAutocompletePopupLabels(autocompletePopup).join("\n") + ); + } +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_escape_key.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_escape_key.js new file mode 100644 index 0000000000..711ed63f24 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_escape_key.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// See Bug 585991. + +const TEST_URI = `data:text/html;charset=utf-8, +<head> + <script> + /* Create a prototype-less object so popup does not contain native + * Object prototype properties. + */ + window.foo = Object.create(null); + Object.assign(window.foo, { + item0: "value0", + item1: "value1", + }); + </script> +</head> +<body>bug 585991 - autocomplete popup escape key usage test</body>`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + info("web console opened"); + + const { autocompletePopup: popup } = jsterm; + + const onPopUpOpen = popup.once("popup-opened"); + + info("wait for completion: window.foo."); + setInputValue(hud, "window.foo"); + EventUtils.sendString("."); + + await onPopUpOpen; + + ok(popup.isOpen, "popup is open"); + ok(popup.itemCount, "popup has items"); + + info("press Escape to close the popup"); + const onPopupClose = popup.once("popup-closed"); + EventUtils.synthesizeKey("KEY_Escape"); + + await onPopupClose; + + ok(!popup.isOpen, "popup is not open after VK_ESCAPE"); + is(getInputValue(hud), "window.foo.", "completion was cancelled"); + ok(!getInputCompletionValue(hud), "completeNode is empty"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_expression_variables.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_expression_variables.js new file mode 100644 index 0000000000..f921748f20 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_expression_variables.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that variable created in the expression are displayed in the autocomplete. + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf8,Test autocompletion for expression variables<script> + var testGlobal; + </script>`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + const { autocompletePopup } = jsterm; + + await setInputValueForAutocompletion( + hud, + ` + var testVar; + let testLet; + const testConst; + class testClass {} + function testFunc(testParam1, testParam2, ...testParamRest) { + var [testParamRestFirst] = testParamRest; + let {testDeconstruct1,testDeconstruct2, ...testDeconstructRest} = testParam1; + test` + ); + + ok( + hasExactPopupLabels(autocompletePopup, [ + "testClass", + "testConst", + "testDeconstruct1", + "testDeconstruct2", + "testDeconstructRest", + "testFunc", + "testGlobal", + "testLet", + "testParam1", + "testParam2", + "testParamRest", + "testParamRestFirst", + "testVar", + ]), + "Autocomplete popup displays both global and local variables" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_extraneous_closing_brackets.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_extraneous_closing_brackets.js new file mode 100644 index 0000000000..8025b75090 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_extraneous_closing_brackets.js @@ -0,0 +1,20 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that, when the user types an extraneous closing bracket, no error +// appears. See Bug 592442. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,test for bug 592442"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + try { + await setInputValueForAutocompletion(hud, "document.getElementById)"); + ok(true, "no error was thrown when an extraneous bracket was inserted"); + } catch (ex) { + ok(false, "an error was thrown when an extraneous bracket was inserted"); + } +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_getters_cache.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_getters_cache.js new file mode 100644 index 0000000000..a9d2506d42 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_getters_cache.js @@ -0,0 +1,133 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the invoke getter authorizations are cleared when expected. + +const TEST_URI = `data:text/html;charset=utf-8, +<head> + <script> + /* Create a prototype-less object so popup does not contain native + * Object prototype properties. + */ + var obj = props => Object.create(null, Object.getOwnPropertyDescriptors(props)); + let sideEffectVar; + var foo = obj({ + get bar() { + sideEffectVar = "from bar"; + return obj({ + get baz() { + sideEffectVar = "from baz"; + return obj({ + hello: 1, + world: "", + }); + }, + bloop: true, + }) + } + }); + </script> +</head> +<body>Autocomplete popup - invoke getter cache test</body>`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + const { autocompletePopup } = jsterm; + const target = await TargetFactory.forTab(gBrowser.selectedTab); + const toolbox = gDevTools.getToolbox(target); + + let tooltip = await setInputValueForGetterConfirmDialog( + toolbox, + hud, + "foo.bar." + ); + let labelEl = tooltip.querySelector(".confirm-label"); + is( + labelEl.textContent, + "Invoke getter foo.bar to retrieve the property list?", + "Dialog has expected text content" + ); + + info( + "Check that hitting Tab does invoke the getter and return its properties" + ); + let onAutocompleteUpdated = jsterm.once("autocomplete-updated"); + EventUtils.synthesizeKey("KEY_Tab"); + await onAutocompleteUpdated; + ok(autocompletePopup.isOpen, "popup is open after Tab"); + ok( + hasExactPopupLabels(autocompletePopup, ["baz", "bloop"]), + "popup has expected items" + ); + checkInputValueAndCursorPosition(hud, "foo.bar.|"); + is(isConfirmDialogOpened(toolbox), false, "confirm tooltip is now closed"); + + info("Close autocomplete popup"); + let onPopupClose = autocompletePopup.once("popup-closed"); + EventUtils.synthesizeKey("KEY_Escape"); + await onPopupClose; + + info( + "Ctrl+Space again to ensure the autocomplete is shown, not the confirm dialog" + ); + onAutocompleteUpdated = jsterm.once("autocomplete-updated"); + EventUtils.synthesizeKey(" ", { ctrlKey: true }); + await onAutocompleteUpdated; + ok(autocompletePopup.isOpen, "popup is open after Ctrl + Space"); + ok( + hasExactPopupLabels(autocompletePopup, ["baz", "bloop"]), + "popup has expected items" + ); + checkInputValueAndCursorPosition(hud, "foo.bar.|"); + is(isConfirmDialogOpened(toolbox), false, "confirm tooltip is not open"); + + info( + "Type a space, then backspace and ensure the autocomplete popup is displayed" + ); + onAutocompleteUpdated = jsterm.once("autocomplete-updated"); + EventUtils.synthesizeKey(" "); + await onAutocompleteUpdated; + is(autocompletePopup.isOpen, true, "Autocomplete popup is still opened"); + ok( + hasExactPopupLabels(autocompletePopup, ["baz", "bloop"]), + "popup has expected items" + ); + + onAutocompleteUpdated = jsterm.once("autocomplete-updated"); + EventUtils.synthesizeKey("KEY_Backspace"); + await onAutocompleteUpdated; + is(autocompletePopup.isOpen, true, "Autocomplete popup is still opened"); + ok( + hasExactPopupLabels(autocompletePopup, ["baz", "bloop"]), + "popup has expected items" + ); + + info( + "Reload the page to ensure asking for autocomplete again show the confirm dialog" + ); + onPopupClose = autocompletePopup.once("popup-closed"); + EventUtils.synthesizeKey("KEY_Escape"); + await onPopupClose; + + await refreshTab(); + info("tab reloaded, waiting for the popup to close"); + + info("Press Ctrl+Space to open the confirm dialog again"); + EventUtils.synthesizeKey(" ", { ctrlKey: true }); + await waitFor(() => isConfirmDialogOpened(toolbox)); + ok(true, "Confirm Dialog is shown after tab navigation"); + tooltip = getConfirmDialog(toolbox); + labelEl = tooltip.querySelector(".confirm-label"); + is( + labelEl.textContent, + "Invoke getter foo.bar to retrieve the property list?", + "Dialog has expected text content" + ); + + info("Close tooltip"); + EventUtils.synthesizeKey("KEY_Escape"); + await waitFor(() => !isConfirmDialogOpened(toolbox)); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_getters_cancel.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_getters_cancel.js new file mode 100644 index 0000000000..b9e0cd62b9 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_getters_cancel.js @@ -0,0 +1,90 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the confirm dialog can be closed with different actions. + +const TEST_URI = `data:text/html;charset=utf-8, +<head> + <script> + let sideEffect; + window.foo = { + get rab() { + sideEffect = "getRab"; + return "rab"; + } + }; + </script> +</head> +<body>Autocomplete popup - invoke getter - close dialog test</body>`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + const target = await TargetFactory.forTab(gBrowser.selectedTab); + const toolbox = gDevTools.getToolbox(target); + + let tooltip = await setInputValueForGetterConfirmDialog( + toolbox, + hud, + "foo.rab." + ); + let labelEl = tooltip.querySelector(".confirm-label"); + is( + labelEl.textContent, + "Invoke getter foo.rab to retrieve the property list?", + "Dialog has expected text content" + ); + + info("Check that Escape closes the confirm tooltip"); + EventUtils.synthesizeKey("KEY_Escape"); + await waitFor(() => !isConfirmDialogOpened(toolbox)); + + info("Check that typing a letter won't show the tooltip"); + const onAutocompleteUpdate = jsterm.once("autocomplete-updated"); + EventUtils.sendString("t"); + await onAutocompleteUpdate; + is(isConfirmDialogOpened(toolbox), false, "The confirm dialog is not open"); + + info("Check that Ctrl+space show the confirm tooltip again"); + EventUtils.synthesizeKey(" ", { ctrlKey: true }); + await waitFor(() => isConfirmDialogOpened(toolbox)); + tooltip = getConfirmDialog(toolbox); + labelEl = tooltip.querySelector(".confirm-label"); + is( + labelEl.textContent, + "Invoke getter foo.rab to retrieve the property list?", + "Dialog has expected text content" + ); + + info("Check that clicking on the close button closes the tooltip"); + const closeButtonEl = tooltip.querySelector(".close-confirm-dialog-button"); + is(closeButtonEl.title, "Close (Esc)", "Close button has the expected title"); + closeButtonEl.click(); + await waitFor(() => !isConfirmDialogOpened(toolbox)); + ok(true, "Clicking the close button does close the tooltip"); + + info( + "Check that the tooltip closes when there's no more reason to display it" + ); + // Open the tooltip again + EventUtils.synthesizeKey(" ", { ctrlKey: true }); + await waitFor(() => isConfirmDialogOpened(toolbox)); + + // Adding a space will make the input `foo.rab.t `, which we shouldn't try to + // autocomplete. + EventUtils.sendString(" "); + await waitFor(() => !isConfirmDialogOpened(toolbox)); + ok( + true, + "The tooltip is now closed since the input doesn't match a getter name" + ); + info("Check that evaluating the expression closes the tooltip"); + tooltip = await setInputValueForGetterConfirmDialog(toolbox, hud, "foo.rab."); + EventUtils.sendString("length"); + EventUtils.synthesizeKey("KEY_Enter"); + await waitFor(() => !isConfirmDialogOpened(toolbox)); + await waitFor(() => findMessage(hud, "3", ".result")); + ok("Expression was evaluated and tooltip was closed"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_getters_confirm.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_getters_confirm.js new file mode 100644 index 0000000000..5ff032f64f --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_getters_confirm.js @@ -0,0 +1,148 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that accessing properties with getters displays the confirm dialog to invoke them, +// and then displays the autocomplete popup with the results. + +const TEST_URI = `data:text/html;charset=utf-8, +<head> + <script> + /* Create a prototype-less object so popup does not contain native + * Object prototype properties. + */ + var obj = props => Object.create(null, Object.getOwnPropertyDescriptors(props)); + let sideEffect; + var foo = obj({ + get bar() { + sideEffect = "bar"; + return obj({ + get baz() { + sideEffect = "baz"; + return obj({ + hello: 1, + world: "", + }); + }, + bloop: true, + }) + }, + get rab() { + sideEffect = "rab"; + return ""; + } + }); + </script> +</head> +<body>Autocomplete popup - invoke getter usage test</body>`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + const { autocompletePopup } = jsterm; + const target = await TargetFactory.forTab(gBrowser.selectedTab); + const toolbox = gDevTools.getToolbox(target); + + let tooltip = await setInputValueForGetterConfirmDialog( + toolbox, + hud, + "foo.bar." + ); + let labelEl = tooltip.querySelector(".confirm-label"); + is( + labelEl.textContent, + "Invoke getter foo.bar to retrieve the property list?", + "Dialog has expected text content" + ); + + info( + "Check that hitting Tab does invoke the getter and return its properties" + ); + let onPopUpOpen = autocompletePopup.once("popup-opened"); + EventUtils.synthesizeKey("KEY_Tab"); + await onPopUpOpen; + ok(autocompletePopup.isOpen, "popup is open after Tab"); + ok( + hasExactPopupLabels(autocompletePopup, ["baz", "bloop"]), + "popup has expected items" + ); + checkInputValueAndCursorPosition(hud, "foo.bar.|"); + is(isConfirmDialogOpened(toolbox), false, "confirm tooltip is now closed"); + + let onPopUpClose = autocompletePopup.once("popup-closed"); + EventUtils.synthesizeKey("KEY_Tab"); + await onPopUpClose; + checkInputValueAndCursorPosition(hud, "foo.bar.baz|"); + + info( + "Check that the invoke tooltip is displayed when performing an element access" + ); + EventUtils.sendString("["); + await waitFor(() => isConfirmDialogOpened(toolbox)); + + tooltip = getConfirmDialog(toolbox); + labelEl = tooltip.querySelector(".confirm-label"); + is( + labelEl.textContent, + "Invoke getter foo.bar.baz to retrieve the property list?", + "Dialog has expected text content" + ); + + info( + "Check that hitting Tab does invoke the getter and return its properties" + ); + onPopUpOpen = autocompletePopup.once("popup-opened"); + EventUtils.synthesizeKey("KEY_Tab"); + await onPopUpOpen; + ok(autocompletePopup.isOpen, "popup is open after Tab"); + ok( + hasExactPopupLabels(autocompletePopup, [`"hello"`, `"world"`]), + "popup has expected items" + ); + checkInputValueAndCursorPosition(hud, "foo.bar.baz[|]"); + is(isConfirmDialogOpened(toolbox), false, "confirm tooltip is now closed"); + + onPopUpClose = autocompletePopup.once("popup-closed"); + EventUtils.synthesizeKey("KEY_Tab"); + await onPopUpClose; + checkInputValueAndCursorPosition(hud, `foo.bar.baz["hello"]|`); + + info("Check that autocompletion work on a getter result"); + onPopUpOpen = autocompletePopup.once("popup-opened"); + EventUtils.sendString("."); + await onPopUpOpen; + ok(autocompletePopup.isOpen, "got items of getter result"); + ok( + hasPopupLabel(autocompletePopup, "toExponential"), + "popup has expected items" + ); + + tooltip = await setInputValueForGetterConfirmDialog(toolbox, hud, "foo.rab."); + labelEl = tooltip.querySelector(".confirm-label"); + is( + labelEl.textContent, + "Invoke getter foo.rab to retrieve the property list?", + "Dialog has expected text content" + ); + + info( + "Check clicking the confirm button invokes the getter and return its properties" + ); + onPopUpOpen = autocompletePopup.once("popup-opened"); + tooltip.querySelector(".confirm-button").click(); + await onPopUpOpen; + ok( + autocompletePopup.isOpen, + "popup is open after clicking on the confirm button" + ); + ok( + hasPopupLabel(autocompletePopup, "startsWith"), + "popup has expected items" + ); + checkInputValueAndCursorPosition(hud, "foo.rab.|"); + is(isConfirmDialogOpened(toolbox), false, "confirm tooltip is now closed"); + + info("Close autocomplete popup"); + await closeAutocompletePopup(hud); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_getters_learn_more_link.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_getters_learn_more_link.js new file mode 100644 index 0000000000..ea520f16a1 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_getters_learn_more_link.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that accessing properties with getters displays a "learn more" link in the confirm +// dialog that navigates the user to the expected mdn page. + +const TEST_URI = `data:text/html;charset=utf-8, +<head> + <script> + /* Create a prototype-less object so popup does not contain native + * Object prototype properties. + */ + let sideEffect; + window.foo = Object.create(null, Object.getOwnPropertyDescriptors({ + get bar() { + sideEffect = "bar"; + return "hello"; + } + })); + </script> +</head> +<body>Autocomplete popup - invoke getter usage test</body>`; + +const MDN_URL = + "https://developer.mozilla.org/docs/Tools/Web_Console/Invoke_getters_from_autocomplete"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const target = await TargetFactory.forTab(gBrowser.selectedTab); + const toolbox = gDevTools.getToolbox(target); + + const tooltip = await setInputValueForGetterConfirmDialog( + toolbox, + hud, + "window.foo.bar." + ); + const labelEl = tooltip.querySelector(".confirm-label"); + is( + labelEl.textContent, + "Invoke getter window.foo.bar to retrieve the property list?", + "Dialog has expected text content" + ); + const learnMoreEl = tooltip.querySelector(".learn-more-link"); + is(learnMoreEl.textContent, "Learn More", `There's a "Learn more" link`); + + info( + `Check that clicking on the "Learn more" link navigates to the expected page` + ); + const expectedUri = MDN_URL + GA_PARAMS; + const { link } = await simulateLinkClick(learnMoreEl); + is( + link, + expectedUri, + `Click on "Learn More" link navigates user to ${expectedUri}` + ); + + info("Close the popup"); + EventUtils.synthesizeKey("KEY_Escape"); + await waitFor(() => !isConfirmDialogOpened(toolbox)); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_helpers.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_helpers.js new file mode 100644 index 0000000000..3f1489c51f --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_helpers.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that the autocompletion results contain the names of JSTerm helpers. +// See Bug 686937. + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf8,<p>test JSTerm Helpers autocomplete"; + +add_task(async function() { + await pushPref("devtools.editor.autoclosebrackets", false); + const hud = await openNewTabAndConsole(TEST_URI); + await testInspectAutoCompletion(hud, "i", true); + await testInspectAutoCompletion(hud, "window.", false); + await testInspectAutoCompletion(hud, "dump(i", true); + await testInspectAutoCompletion(hud, "window.dump(i", true); + + info("Close autocomplete popup"); + await closeAutocompletePopup(hud); +}); + +async function testInspectAutoCompletion(hud, inputValue, expectInspect) { + await setInputValueForAutocompletion(hud, inputValue); + is( + hasPopupLabel(hud.jsterm.autocompletePopup, "inspect"), + expectInspect, + `autocomplete results${ + expectInspect ? "" : " does not" + } contain helper 'inspect'` + ); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_in_chrome_tab.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_in_chrome_tab.js new file mode 100644 index 0000000000..45a7be33e3 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_in_chrome_tab.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that code completion works properly in chrome tabs, like about:config. + +"use strict"; + +add_task(async function() { + const hud = await openNewTabAndConsole("about:config"); + ok(hud, "we have a console"); + ok(hud.iframeWindow, "we have the console UI window"); + + const { jsterm } = hud; + ok(jsterm, "we have a jsterm"); + ok(hud.ui.outputNode, "we have an output node"); + + // Test typing 'docu'. + await setInputValueForAutocompletion(hud, "docu"); + checkInputCompletionValue(hud, "ment", "'docu' completion"); + + info("Close autocomplete popup"); + await closeAutocompletePopup(hud); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_in_debugger_stackframe.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_in_debugger_stackframe.js new file mode 100644 index 0000000000..018103b737 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_in_debugger_stackframe.js @@ -0,0 +1,111 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that makes sure web console autocomplete happens in the user-selected +// stackframe from the js debugger. + +"use strict"; +/* import-globals-from head.js*/ + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-autocomplete-in-stackframe.html"; + +requestLongerTimeout(20); + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + const { autocompletePopup: popup } = jsterm; + + const target = await TargetFactory.forTab(gBrowser.selectedTab); + const toolbox = gDevTools.getToolbox(target); + + const jstermComplete = value => setInputValueForAutocompletion(hud, value); + + // Test that document.title gives string methods. Native getters must execute. + await jstermComplete("document.title."); + + const newItemsLabels = getAutocompletePopupLabels(popup); + ok(newItemsLabels.length > 0, "'document.title.' gave a list of suggestions"); + ok(newItemsLabels.includes("substr"), `results do contain "substr"`); + ok( + newItemsLabels.includes("toLowerCase"), + `results do contain "toLowerCase"` + ); + ok(newItemsLabels.includes("strike"), `results do contain "strike"`); + + // Test if 'foo' gives 'foo1' but not 'foo2' or 'foo3' + await jstermComplete("foo"); + ok( + hasExactPopupLabels(popup, ["foo1", "foo1Obj"]), + `"foo" gave the expected suggestions` + ); + + // Test if 'foo1Obj.' gives 'prop1' and 'prop2' + await jstermComplete("foo1Obj."); + checkInputCompletionValue(hud, "prop1", "foo1Obj completion"); + ok( + hasExactPopupLabels(popup, ["prop1", "prop2"]), + `"foo1Obj." gave the expected suggestions` + ); + + // Test if 'foo1Obj.prop2.' gives 'prop21' + await jstermComplete("foo1Obj.prop2."); + ok( + hasPopupLabel(popup, "prop21"), + `"foo1Obj.prop2." gave the expected suggestions` + ); + await closeAutocompletePopup(hud); + + info("Opening Debugger"); + await openDebugger(); + const dbg = createDebuggerContext(toolbox); + + info("Waiting for pause"); + await pauseDebugger(dbg); + const stackFrames = dbg.selectors.getCallStackFrames(); + + info("Opening Console again"); + await toolbox.selectTool("webconsole"); + + // Test if 'foo' gives 'foo3' and 'foo1' but not 'foo2', since we are paused in + // the `secondCall` function (called by `firstCall`, which we call in `pauseDebugger`). + await jstermComplete("foo"); + ok( + hasExactPopupLabels(popup, ["foo1", "foo1Obj", "foo3", "foo3Obj"]), + `"foo." gave the expected suggestions` + ); + + await openDebugger(); + + // Select the frame for the `firstCall` function. + await selectFrame(dbg, stackFrames[1]); + + info("openConsole"); + await toolbox.selectTool("webconsole"); + + // Test if 'foo' gives 'foo2' and 'foo1' but not 'foo3', since we are now in the + // `firstCall` frame. + await jstermComplete("foo"); + ok( + hasExactPopupLabels(popup, ["foo1", "foo1Obj", "foo2", "foo2Obj"]), + `"foo" gave the expected suggestions` + ); + + // Test if 'foo2Obj.' gives 'prop1' + await jstermComplete("foo2Obj."); + ok(hasPopupLabel(popup, "prop1"), `"foo2Obj." returns "prop1"`); + + // Test if 'foo2Obj.prop1.' gives 'prop11' + await jstermComplete("foo2Obj.prop1."); + ok(hasPopupLabel(popup, "prop11"), `"foo2Obj.prop1" returns "prop11"`); + + // Test if 'foo2Obj.prop1.prop11.' gives suggestions for a string,i.e. 'length' + await jstermComplete("foo2Obj.prop1.prop11."); + ok(hasPopupLabel(popup, "length"), `results do contain "length"`); + + // Test if 'foo2Obj[0].' throws no errors. + await jstermComplete("foo2Obj[0]."); + is(getAutocompletePopupLabels(popup).length, 0, "no items for foo2Obj[0]"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_inside_text.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_inside_text.js new file mode 100644 index 0000000000..23ca6f924c --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_inside_text.js @@ -0,0 +1,171 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that editing text inside parens behave as expected, i.e. +// - it does not show the autocompletion text +// - show popup when there's properties to complete +// - insert the selected item from the popup in the input +// - right arrow dismiss popup and don't autocomplete +// - tab key when there is not visible autocomplete suggestion insert a tab +// See Bug 812618, 1479521 and 1334130. + +const TEST_URI = `data:text/html;charset=utf-8, +<head> + <script> + window.testBugAA = "hello world"; + window.testBugBB = "hello world 2"; + window.x = "hello world 3"; + </script> +</head> +<body>bug 812618 - test completion inside text</body>`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + info("web console opened"); + + const { autocompletePopup: popup } = jsterm; + + await setInitialState(hud); + + ok(popup.isOpen, "popup is open"); + is(popup.itemCount, 2, "popup.itemCount is correct"); + is(popup.selectedIndex, 0, "popup.selectedIndex is correct"); + ok(!getInputCompletionValue(hud), "there is no completion text"); + + info("Pressing arrow right"); + let onPopupClose = popup.once("popup-closed"); + EventUtils.synthesizeKey("KEY_ArrowRight"); + await onPopupClose; + ok(true, "popup was closed"); + checkInputValueAndCursorPosition( + hud, + "dump(window.testB)|", + "input wasn't modified" + ); + + await setInitialState(hud); + EventUtils.synthesizeKey("KEY_ArrowDown"); + is(popup.selectedIndex, 1, "popup.selectedIndex is correct"); + ok(!getInputCompletionValue(hud), "completeNode.value is empty"); + + ok( + hasExactPopupLabels(popup, ["testBugAA", "testBugBB"]), + "getItems returns the items we expect" + ); + + info("press Tab and wait for popup to hide"); + onPopupClose = popup.once("popup-closed"); + EventUtils.synthesizeKey("KEY_Tab"); + await onPopupClose; + + // At this point the completion suggestion should be accepted. + ok(!popup.isOpen, "popup is not open"); + checkInputValueAndCursorPosition( + hud, + "dump(window.testBugBB|)", + "completion was successful after VK_TAB" + ); + ok(!getInputCompletionValue(hud), "there is no completion text"); + + info("Test ENTER key when popup is visible with a selected item"); + await setInitialState(hud); + info("press Enter and wait for popup to hide"); + onPopupClose = popup.once("popup-closed"); + EventUtils.synthesizeKey("KEY_Enter"); + await onPopupClose; + + ok(!popup.isOpen, "popup is not open"); + checkInputValueAndCursorPosition( + hud, + "dump(window.testBugAA|)", + "completion was successful after Enter" + ); + ok(!getInputCompletionValue(hud), "there is no completion text"); + + info("Test autocomplete inside parens"); + await setInputValueForAutocompletion(hud, "dump()", -1); + let onAutocompleteUpdated = jsterm.once("autocomplete-updated"); + EventUtils.sendString("window.testBugA"); + await onAutocompleteUpdated; + ok(popup.isOpen, "popup is open"); + ok(!getInputCompletionValue(hud), "there is no completion text"); + + info("Matching the completion proposal should close the popup"); + onPopupClose = popup.once("popup-closed"); + EventUtils.sendString("A"); + await onPopupClose; + + info("Test TAB key when there is no autocomplete suggestion"); + ok(!popup.isOpen, "popup is not open"); + ok(!getInputCompletionValue(hud), "there is no completion text"); + + EventUtils.synthesizeKey("KEY_Tab"); + checkInputValueAndCursorPosition( + hud, + "dump(window.testBugAA\t|)", + "completion was successful after Enter" + ); + + info("Check that we don't show the popup when editing words"); + await setInputValueForAutocompletion(hud, "estBug", 0); + onAutocompleteUpdated = jsterm.once("autocomplete-updated"); + EventUtils.sendString("t"); + await onAutocompleteUpdated; + is(getInputValue(hud), "testBug", "jsterm has expected value"); + is(popup.isOpen, false, "popup is not open"); + ok(!getInputCompletionValue(hud), "there is no completion text"); + + await setInputValueForAutocompletion(hud, "__foo", 1); + onAutocompleteUpdated = jsterm.once("autocomplete-updated"); + EventUtils.sendString("t"); + await onAutocompleteUpdated; + is(getInputValue(hud), "_t_foo", "jsterm has expected value"); + is(popup.isOpen, false, "popup is not open"); + ok(!getInputCompletionValue(hud), "there is no completion text"); + + await setInputValueForAutocompletion(hud, "$$bar", 1); + onAutocompleteUpdated = jsterm.once("autocomplete-updated"); + EventUtils.sendString("t"); + await onAutocompleteUpdated; + is(getInputValue(hud), "$t$bar", "jsterm has expected value"); + is(popup.isOpen, false, "popup is not open"); + ok(!getInputCompletionValue(hud), "there is no completion text"); + + await setInputValueForAutocompletion(hud, "99luftballons", 1); + onAutocompleteUpdated = jsterm.once("autocomplete-updated"); + EventUtils.sendString("t"); + await onAutocompleteUpdated; + is(getInputValue(hud), "9t9luftballons", "jsterm has expected value"); + is(popup.isOpen, false, "popup is not open"); + ok(!getInputCompletionValue(hud), "there is no completion text"); + + info("Check that typing the closing paren closes the autocomplete window"); + await setInputValueForAutocompletion(hud, "dump()", -1); + const onPopupOpen = popup.once("popup-opened"); + EventUtils.sendString("x"); + await onPopupOpen; + + onPopupClose = popup.once("popup-closed"); + // Since the paren is already here, it won't add any new character + EventUtils.sendString(")"); + checkInputValueAndCursorPosition( + hud, + "dump(x)|", + "the input is the expected one after typing the closing paren" + ); + await onPopupClose; + ok(true, "popup was closed when typing the closing paren"); +}); + +async function setInitialState(hud) { + const { jsterm } = hud; + await setInputValueForAutocompletion(hud, "dump()", -1); + + const onAutocompleteUpdated = jsterm.once("autocomplete-updated"); + EventUtils.sendString("window.testB"); + checkInputValueAndCursorPosition(hud, "dump(window.testB|)"); + await onAutocompleteUpdated; +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_native_getters.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_native_getters.js new file mode 100644 index 0000000000..401e0c58c9 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_native_getters.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that native getters (e.g. document.body) autocompletes in the web console. +// See Bug 651501. + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf-8,Test document.body autocompletion"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm, ui } = hud; + + const { autocompletePopup: popup } = jsterm; + + ok(!popup.isOpen, "popup is not open"); + const onPopupOpen = popup.once("popup-opened"); + + setInputValue(hud, "document.body"); + EventUtils.sendString("."); + + await onPopupOpen; + + ok(popup.isOpen, "popup is open"); + const cacheMatches = ui.wrapper.getStore().getState().autocomplete.cache + .matches; + is(popup.itemCount, cacheMatches.length, "popup.itemCount is correct"); + ok( + cacheMatches.includes("addEventListener"), + "addEventListener is in the list of suggestions" + ); + ok(cacheMatches.includes("bgColor"), "bgColor is in the list of suggestions"); + ok( + cacheMatches.includes("ATTRIBUTE_NODE"), + "ATTRIBUTE_NODE is in the list of suggestions" + ); + + const onPopupClose = popup.once("popup-closed"); + EventUtils.synthesizeKey("KEY_Escape"); + + await onPopupClose; + + ok(!popup.isOpen, "popup is not open"); + const onAutoCompleteUpdated = jsterm.once("autocomplete-updated"); + const inputStr = "document.b"; + setInputValue(hud, inputStr); + EventUtils.sendString("o"); + + await onAutoCompleteUpdated; + checkInputCompletionValue(hud, "dy", "autocomplete shows document.body"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_nav_and_tab_key.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_nav_and_tab_key.js new file mode 100644 index 0000000000..92ce7a905f --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_nav_and_tab_key.js @@ -0,0 +1,134 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// See Bug 585991. + +const TEST_URI = `data:text/html;charset=utf-8, +<head> + <script> + /* Create a prototype-less object so popup does not contain native + * Object prototype properties. + */ + window.foo = Object.create(null, Object.getOwnPropertyDescriptors({ + item00: "value0", + item1: "value1", + item2: "value2", + item3: "value3", + })); + </script> +</head> +<body>bug 585991 - autocomplete popup navigation and tab key usage test</body>`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + info("web console opened"); + + const { autocompletePopup: popup } = jsterm; + + ok(!popup.isOpen, "popup is not open"); + + const onPopUpOpen = popup.once("popup-opened"); + setInputValue(hud, "window.foo"); + + // Shows the popup + EventUtils.sendString("."); + await onPopUpOpen; + + ok(popup.isOpen, "popup is open"); + + const expectedPopupItems = ["item00", "item1", "item2", "item3"]; + ok( + hasExactPopupLabels(popup, expectedPopupItems), + "getItems returns the items we expect" + ); + is(popup.selectedIndex, 0, "Index of the first item is selected."); + + EventUtils.synthesizeKey("KEY_ArrowUp"); + + is(popup.selectedIndex, 3, "index 3 is selected"); + is(popup.selectedItem.label, "item3", "item3 is selected"); + checkInputCompletionValue(hud, "item3", "completeNode.value holds item3"); + + EventUtils.synthesizeKey("KEY_ArrowUp"); + + is(popup.selectedIndex, 2, "index 2 is selected"); + is(popup.selectedItem.label, "item2", "item2 is selected"); + checkInputCompletionValue(hud, "item2", "completeNode.value holds item2"); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + + is(popup.selectedIndex, 3, "index 3 is selected"); + is(popup.selectedItem.label, "item3", "item3 is selected"); + checkInputCompletionValue(hud, "item3", "completeNode.value holds item3"); + + let currentSelectionIndex = popup.selectedIndex; + + EventUtils.synthesizeKey("KEY_PageUp"); + ok( + popup.selectedIndex < currentSelectionIndex, + "Index is less after Page UP" + ); + + currentSelectionIndex = popup.selectedIndex; + EventUtils.synthesizeKey("KEY_PageDown"); + ok( + popup.selectedIndex > currentSelectionIndex, + "Index is greater after PGDN" + ); + + EventUtils.synthesizeKey("KEY_Home"); + is(popup.selectedIndex, 0, "index is first after Home"); + + EventUtils.synthesizeKey("KEY_End"); + is( + popup.selectedIndex, + expectedPopupItems.length - 1, + "index is last after End" + ); + + info("press Tab and wait for popup to hide"); + const onPopupClose = popup.once("popup-closed"); + EventUtils.synthesizeKey("KEY_Tab"); + + await onPopupClose; + + // At this point the completion suggestion should be accepted. + ok(!popup.isOpen, "popup is not open"); + is( + getInputValue(hud), + "window.foo.item3", + "completion was successful after KEY_Tab" + ); + ok(!getInputCompletionValue(hud), "completeNode is empty"); + + info( + "Check that hitting Home hides the completion text when the popup is hidden" + ); + await setInputValueForAutocompletion(hud, "window.foo.item0"); + checkInputCompletionValue(hud, "0", "completeNode has expected value"); + if (Services.appinfo.OS == "Darwin") { + EventUtils.synthesizeKey("a", { ctrlKey: true }); + } else { + EventUtils.synthesizeKey("KEY_Home"); + } + checkInputCompletionValue( + hud, + "", + "completeNode was cleared after hitting Home" + ); + + info( + "Check that hitting End hides the completion text when the popup is hidden" + ); + await setInputValueForAutocompletion(hud, "window.foo.item0"); + checkInputCompletionValue(hud, "0", "completeNode has expected value"); + EventUtils.synthesizeKey("KEY_End"); + checkInputCompletionValue( + hud, + "", + "completeNode was cleared after hitting End" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_null.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_null.js new file mode 100644 index 0000000000..f849b8e72a --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_null.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function() { + await pushPref("devtools.chrome.enabled", true); + await addTab("about:blank"); + + info(`Open browser console with ctrl-shift-j`); + const opened = waitForBrowserConsole(); + EventUtils.synthesizeKey("j", { accelKey: true, shiftKey: true }, window); + const hud = await opened; + const { jsterm } = hud; + const { autocompletePopup: popup } = jsterm; + + info(`Clear existing messages`); + const onMessagesCleared = hud.ui.once("messages-cleared"); + await clearOutput(hud); + await onMessagesCleared; + + info(`Create a null variable`); + // Using the console front directly as we don't want to impact the UI state. + await hud.evaluateJSAsync(`globalThis.nullVar = null;`); + + info(`Check completion suggestions for "null"`); + await setInputValueForAutocompletion(hud, "null"); + ok(popup.isOpen, "popup is open"); + const expectedPopupItems = ["null", "nullVar"]; + ok( + hasExactPopupLabels(popup, expectedPopupItems), + "popup has expected items" + ); + + info(`Check completion suggestions for "null."`); + let onAutocompleteUpdated = jsterm.once("autocomplete-updated"); + EventUtils.sendString(".", hud.iframeWindow); + await onAutocompleteUpdated; + is(popup.itemCount, 0, "popup has no items"); + + info(`Check completion suggestions for "null"`); + onAutocompleteUpdated = jsterm.once("autocomplete-updated"); + EventUtils.synthesizeKey("KEY_Backspace", undefined, hud.iframeWindow); + await onAutocompleteUpdated; + is(popup.itemCount, 2, "popup has 2 items"); + + info(`Check completion suggestions for "nullVar"`); + onAutocompleteUpdated = jsterm.once("autocomplete-updated"); + EventUtils.sendString("Var.", hud.iframeWindow); + await onAutocompleteUpdated; + is(popup.itemCount, 0, "popup has no items"); + is(popup.isOpen, false, "popup is closed"); + + info(`Check that no error was logged`); + await waitFor(() => + findMessage(hud, "", ".message.error:not(.network)") + ).then( + message => { + ok(false, `Got error ${JSON.stringify(message.textContent)}`); + }, + error => { + if (!error.message.includes("Failed waitFor")) { + throw error; + } + ok(true, `No error was logged`); + } + ); + + info(`Cleanup`); + await hud.evaluateJSAsync(`delete globalThis.nullVar;`); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_paste_undo.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_paste_undo.js new file mode 100644 index 0000000000..f9b144b7ed --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_paste_undo.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,<p>test for bug 642615</p>"; + +XPCOMUtils.defineLazyServiceGetter( + this, + "clipboardHelper", + "@mozilla.org/widget/clipboardhelper;1", + "nsIClipboardHelper" +); +const stringToCopy = "foobazbarBug642615"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + await clearOutput(hud); + ok(!getInputCompletionValue(hud), "no completeNode.value"); + + setInputValue(hud, "doc"); + + info("wait for completion value after typing 'docu'"); + let onAutocompleteUpdated = jsterm.once("autocomplete-updated"); + EventUtils.sendString("u"); + await onAutocompleteUpdated; + + const completionValue = getInputCompletionValue(hud); + + info(`Copy "${stringToCopy}" in clipboard`); + await waitForClipboardPromise( + () => clipboardHelper.copyString(stringToCopy), + stringToCopy + ); + + setInputValue(hud, "docu"); + info("wait for completion update after clipboard paste"); + onAutocompleteUpdated = jsterm.once("autocomplete-updated"); + EventUtils.synthesizeKey("v", { accelKey: true }); + + await onAutocompleteUpdated; + + ok(!getInputCompletionValue(hud), "no completion value after paste"); + + info("wait for completion update after undo"); + onAutocompleteUpdated = jsterm.once("autocomplete-updated"); + + EventUtils.synthesizeKey("z", { accelKey: true }); + + await onAutocompleteUpdated; + + checkInputCompletionValue( + hud, + completionValue, + "same completeNode.value after undo" + ); + + info("wait for completion update after clipboard paste (ctrl-v)"); + onAutocompleteUpdated = jsterm.once("autocomplete-updated"); + + EventUtils.synthesizeKey("v", { accelKey: true }); + + await onAutocompleteUpdated; + ok(!getInputCompletionValue(hud), "no completion value after paste (ctrl-v)"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_race_on_enter.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_race_on_enter.js new file mode 100644 index 0000000000..22b7aa9216 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_race_on_enter.js @@ -0,0 +1,169 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that pressing Enter quickly after a letter that makes the input exactly match the +// item in the autocomplete popup does not insert unwanted character. See Bug 1595068. + +const TEST_URI = `data:text/html;charset=utf-8,<script> + var uvwxyz = "zyxwvu"; +</script>Autocomplete race on Enter`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + const { autocompletePopup } = jsterm; + + info(`Enter "scre" and wait for the autocomplete popup to be displayed`); + let onPopupOpened = autocompletePopup.once("popup-opened"); + await setInputValueForAutocompletion(hud, "scre"); + await onPopupOpened; + checkInputCompletionValue(hud, "en", "completeNode has expected value"); + + info(`Type "n" and quickly after, "Enter"`); + let onPopupClosed = autocompletePopup.once("popup-closed"); + EventUtils.synthesizeKey("e"); + await waitForTime(50); + EventUtils.synthesizeKey("KEY_Enter"); + await onPopupClosed; + + is(getInputValue(hud), "screen", "the input has the expected value"); + + setInputValue(hud, ""); + + info( + "Check that it works when typed word match exactly the item in the popup" + ); + onPopupOpened = autocompletePopup.once("popup-opened"); + await setInputValueForAutocompletion(hud, "wind"); + await onPopupOpened; + checkInputCompletionValue(hud, "ow", "completeNode has expected value"); + + info(`Quickly type "o", "w" and "Enter"`); + onPopupClosed = autocompletePopup.once("popup-closed"); + EventUtils.synthesizeKey("o"); + await waitForTime(5); + EventUtils.synthesizeKey("w"); + await waitForTime(5); + EventUtils.synthesizeKey("KEY_Enter"); + await onPopupClosed; + + is(getInputValue(hud), "window", "the input has the expected value"); + + setInputValue(hud, ""); + + info("Check that it works when there's no autocomplete popup"); + let onAutocompleteUpdated = jsterm.once("autocomplete-updated"); + await setInputValueForAutocompletion(hud, "uvw"); + await onAutocompleteUpdated; + checkInputCompletionValue(hud, "xyz", "completeNode has expected value"); + + info(`Quickly type "x" and "Enter"`); + EventUtils.synthesizeKey("x"); + await waitForTime(5); + EventUtils.synthesizeKey("KEY_Enter"); + await waitFor( + () => getInputValue(hud) === "uvwxyz", + "input has expected 'uvwxyz' value" + ); + ok(true, "input has the expected value"); + + setInputValue(hud, ""); + + info( + "Check that it works when there's no autocomplete popup and the whole word is typed" + ); + onAutocompleteUpdated = jsterm.once("autocomplete-updated"); + await setInputValueForAutocompletion(hud, "uvw"); + await onAutocompleteUpdated; + checkInputCompletionValue(hud, "xyz", "completeNode has expected value"); + + info(`Quickly type "x", "y", "z" and "Enter"`); + const onResultMessage = waitForMessage(hud, "zyxwvu", ".result"); + EventUtils.synthesizeKey("x"); + await waitForTime(5); + EventUtils.synthesizeKey("y"); + await waitForTime(5); + EventUtils.synthesizeKey("z"); + await waitForTime(5); + EventUtils.synthesizeKey("KEY_Enter"); + info("wait for result message"); + await onResultMessage; + is(getInputValue(hud), "", "Expression was evaluated and input was cleared"); + + setInputValue(hud, ""); + + info("Check that it works when typed letter match another item in the popup"); + onPopupOpened = autocompletePopup.once("popup-opened"); + await setInputValueForAutocompletion(hud, "[].so"); + await onPopupOpened; + checkInputCompletionValue(hud, "me", "completeNode has expected value"); + is( + autocompletePopup.items.map(({ label }) => label).join("|"), + "some|sort", + "autocomplete has expected items" + ); + + info(`Quickly type "m" and "Enter"`); + onPopupClosed = autocompletePopup.once("popup-closed"); + EventUtils.synthesizeKey("m"); + await waitForTime(5); + EventUtils.synthesizeKey("KEY_Enter"); + await onPopupClosed; + is(getInputValue(hud), "[].some", "the input has the expected value"); + + setInputValue(hud, ""); + + info( + "Hitting Enter quickly after a letter that should close the popup evaluates the expression" + ); + onPopupOpened = autocompletePopup.once("popup-opened"); + await setInputValueForAutocompletion(hud, "doc"); + await onPopupOpened; + checkInputCompletionValue(hud, "ument", "completeNode has expected value"); + + info(`Quickly type "x" and "Enter"`); + onPopupClosed = autocompletePopup.once("popup-closed"); + const onMessage = waitForMessage(hud, "1", ".result"); + EventUtils.synthesizeKey("x"); + await waitForTime(5); + EventUtils.synthesizeKey("KEY_Enter"); + await Promise.all[(onPopupClosed, onMessage)]; + is( + getInputValue(hud), + "", + "the input is empty and the expression was evaluated" + ); + + info( + "Hitting Enter quickly after a letter that will make the expression exactly match another item than the selected one" + ); + onPopupOpened = autocompletePopup.once("popup-opened"); + await setInputValueForAutocompletion(hud, "cons"); + await onPopupOpened; + checkInputCompletionValue(hud, "ole", "completeNode has expected value"); + info(`Quickly type "t" and "Enter"`); + onPopupClosed = autocompletePopup.once("popup-closed"); + EventUtils.synthesizeKey("t"); + await waitForTime(5); + EventUtils.synthesizeKey("KEY_Enter"); + await onPopupClosed; + is(getInputValue(hud), "const", "the input has the expected item"); + + info( + "Hitting Enter quickly after a letter when the expression has text after" + ); + await setInputValueForAutocompletion(hud, "f(und"); + ok( + hasExactPopupLabels(autocompletePopup, ["undefined"]), + `the popup has the "undefined" item` + ); + info(`Quickly type "e" and "Enter"`); + onPopupClosed = autocompletePopup.once("popup-closed"); + EventUtils.synthesizeKey("e"); + await waitForTime(5); + EventUtils.synthesizeKey("KEY_Enter"); + await onPopupClosed; + is(getInputValue(hud), "f(undefined)", "the input has the expected item"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_return_key.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_return_key.js new file mode 100644 index 0000000000..70b624fd6b --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_return_key.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the Enter keys works as expected. See Bug 585991 and 1483880. + +const TEST_URI = `data:text/html;charset=utf-8, +<head> + <script> + /* Create a prototype-less object so popup does not contain native + * Object prototype properties. + */ + window.foobar = Object.create(null); + Object.assign(window.foobar, { + item0: "value0", + item1: "value1", + item2: "value2", + item3: "value3", + item33: "value33", + }); + </script> +</head> +<body>bug 585991 - test pressing return with open popup</body>`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + const { autocompletePopup: popup } = jsterm; + + info("wait for completion suggestions: window.foobar."); + await setInputValueForAutocompletion(hud, "window.foobar."); + + ok(popup.isOpen, "popup is open"); + const expectedPopupItems = ["item0", "item1", "item2", "item3", "item33"]; + hasExactPopupLabels(popup, expectedPopupItems); + is(popup.itemCount, expectedPopupItems.length, "popup.itemCount is correct"); + is(popup.selectedIndex, 0, "First index from top is selected"); + + EventUtils.synthesizeKey("KEY_ArrowUp"); + + is( + popup.selectedIndex, + expectedPopupItems.length - 1, + "last index is selected" + ); + is(popup.selectedItem.label, "item33", "item33 is selected"); + checkInputCompletionValue(hud, "item33", "completeNode.value holds item33"); + + info("press Return to accept suggestion. wait for popup to hide"); + let onPopupClose = popup.once("popup-closed"); + EventUtils.synthesizeKey("KEY_Enter"); + + await onPopupClose; + + ok(!popup.isOpen, "popup is not open after KEY_Enter"); + is( + getInputValue(hud), + "window.foobar.item33", + "completion was successful after KEY_Enter" + ); + ok(!getInputCompletionValue(hud), "completeNode is empty"); + + info( + "Test that hitting enter when the completeNode is empty closes the popup" + ); + info("wait for completion suggestions: window.foobar.item3"); + await setInputValueForAutocompletion(hud, "window.foobar.item3"); + + is(popup.selectedItem.label, "item3", "item3 is selected"); + ok(!getInputCompletionValue(hud), "completeNode is empty"); + + onPopupClose = popup.once("popup-closed"); + EventUtils.synthesizeKey("KEY_Enter"); + await onPopupClose; + + ok(!popup.isOpen, "popup is not open after KEY_Enter"); + is( + getInputValue(hud), + "window.foobar.item3", + "completion was successful after KEY_Enter" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_return_key_no_selection.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_return_key_no_selection.js new file mode 100644 index 0000000000..22281284fa --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_return_key_no_selection.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// See Bug 873250. + +const TEST_URI = `data:text/html;charset=utf-8, +<head> + <script> + window.testBugA = "hello world"; + window.testBugB = "hello world 2"; + </script> +</head> +<body>bug 873250 - test pressing return with open popup, but no selection</body>`; + +const { + getHistoryEntries, +} = require("devtools/client/webconsole/selectors/history"); + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm, ui } = hud; + + const { autocompletePopup: popup } = jsterm; + + const onPopUpOpen = popup.once("popup-opened"); + + info("wait for popup to show"); + setInputValue(hud, "window.testBu"); + EventUtils.sendString("g"); + + await onPopUpOpen; + + ok(popup.isOpen, "popup is open"); + is(popup.itemCount, 2, "popup.itemCount is correct"); + isnot(popup.selectedIndex, -1, "popup.selectedIndex is correct"); + + info("press Return and wait for popup to hide"); + const onPopUpClose = popup.once("popup-closed"); + EventUtils.synthesizeKey("KEY_Enter"); + await onPopUpClose; + + ok(!popup.isOpen, "popup is not open after KEY_Enter"); + is( + getInputValue(hud), + "window.testBugA", + "input was completed with the first item of the popup" + ); + ok(!getInputCompletionValue(hud), "completeNode is empty"); + + const onMessage = waitForMessage(hud, "hello world"); + EventUtils.synthesizeKey("KEY_Enter"); + is(getInputValue(hud), "", "input is empty after KEY_Enter"); + + const state = ui.wrapper.getStore().getState(); + const entries = getHistoryEntries(state); + is( + entries[entries.length - 1], + "window.testBugA", + "jsterm history is correct" + ); + + info("Wait for the execution value to appear"); + await onMessage; +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_toggle.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_toggle.js new file mode 100644 index 0000000000..9239a19e64 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_toggle.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test for the input autocomplete option: check if the preference toggles the +// autocomplete feature in the console output. See bug 1593607. + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf-8,`; +const PREF_INPUT_AUTOCOMPLETE = "devtools.webconsole.input.autocomplete"; + +add_task(async function() { + // making sure that input autocomplete is true at the start of test + await pushPref(PREF_INPUT_AUTOCOMPLETE, true); + const hud = await openNewTabAndConsole(TEST_URI); + + info( + "Check that console settings contain autocomplete input and its checked" + ); + await checkConsoleSettingState( + hud, + ".webconsole-console-settings-menu-item-autocomplete", + true + ); + + info("Check that popup opens"); + const { jsterm } = hud; + + const { autocompletePopup: popup } = jsterm; + + info(`Enter "w"`); + await setInputValueForAutocompletion(hud, "w"); + + ok(popup.isOpen, "autocomplete popup opens up"); + + info("Clear input value"); + let onPopupClosed = popup.once("popup-closed"); + setInputValue(hud, ""); + await onPopupClosed; + ok(!popup.open, "autocomplete popup closed"); + + info("toggle autocomplete preference"); + + await toggleConsoleSetting( + hud, + ".webconsole-console-settings-menu-item-autocomplete" + ); + + info("Checking that popup do not show"); + info(`Enter "w"`); + setInputValue(hud, "w"); + // delay of 2 seconds. + await wait(2000); + ok(!popup.isOpen, "popup is not open"); + + info("toggling autocomplete pref back to true"); + await toggleConsoleSetting( + hud, + ".webconsole-console-settings-menu-item-autocomplete" + ); + + const prefValue = Services.prefs.getBoolPref(PREF_INPUT_AUTOCOMPLETE); + ok(prefValue, "autocomplete pref value set to true"); + + info("Check that popup opens"); + + info(`Enter "w"`); + await setInputValueForAutocompletion(hud, "w"); + + ok(popup.isOpen, "autocomplete popup opens up"); + + info("Clear input value"); + onPopupClosed = popup.once("popup-closed"); + setInputValue(hud, ""); + await onPopupClosed; + ok(!popup.open, "autocomplete popup closed"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_width.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_width.js new file mode 100644 index 0000000000..6e8e77c83a --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_width.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the autocomplete popup is resized when needed. + +const TEST_URI = `data:text/html;charset=utf-8, +<head> + <script> + /* Create prototype-less object so popup does not contain native + * Object prototype properties. + */ + window.xx = Object.create(null, Object.getOwnPropertyDescriptors({ + ["y".repeat(10)]: 1, + ["z".repeat(20)]: 2 + })); + window.xxx = 1; + </script> +</head> +<body>Test</body>`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + const { autocompletePopup: popup } = jsterm; + + info(`wait for completion suggestions for "xx"`); + await setInputValueForAutocompletion(hud, "xx"); + ok(popup.isOpen, "popup is open"); + + const expectedPopupItems = ["xx", "xxx"]; + ok( + hasExactPopupLabels(popup, expectedPopupItems), + "popup has expected items" + ); + + const originalWidth = popup._tooltip.container.clientWidth; + ok( + originalWidth >= getLongestLabelWidth(jsterm), + `popup (${originalWidth}px) is at least wider than the width of the longest list item (${getLongestLabelWidth( + jsterm + )}px)` + ); + + info(`wait for completion suggestions for "xx."`); + let onAutocompleteUpdated = jsterm.once("autocomplete-updated"); + EventUtils.sendString("."); + await onAutocompleteUpdated; + + ok( + hasExactPopupLabels(popup, ["y".repeat(10), "z".repeat(20)]), + "popup has expected items" + ); + const newPopupWidth = popup._tooltip.container.clientWidth; + ok( + newPopupWidth >= originalWidth, + `The popup width was updated (${originalWidth}px -> ${newPopupWidth}px)` + ); + ok( + newPopupWidth >= getLongestLabelWidth(jsterm), + `popup (${newPopupWidth}px) is at least wider than the width of the longest list item (${getLongestLabelWidth( + jsterm + )}px)` + ); + + info(`wait for completion suggestions for "xx"`); + onAutocompleteUpdated = jsterm.once("autocomplete-updated"); + EventUtils.synthesizeKey("KEY_Backspace"); + await onAutocompleteUpdated; + + is( + popup._tooltip.container.clientWidth, + originalWidth, + "popup is back to its original width" + ); + + info("Close autocomplete popup"); + await closeAutocompletePopup(hud); +}); + +function getLongestLabelWidth(jsterm) { + return ( + jsterm._inputCharWidth * + getAutocompletePopupLabels(jsterm.autocompletePopup).sort( + (a, b) => a < b + )[0].length + ); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_will_navigate.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_will_navigate.js new file mode 100644 index 0000000000..4c65d0d0f4 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_will_navigate.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that navigating the page closes the autocomplete popup. + +const TEST_URI = `data:text/html;charset=utf-8, +<head> + <script> + /* Create a prototype-less object so popup does not contain native + * Object prototype properties. + */ + window.foo = Object.create(null, Object.getOwnPropertyDescriptors({ + item0: "value0", + item1: "value1", + })); + </script> +</head> +<body>Test autocomplete close on content navigation</body>`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + info("web console opened"); + + const { autocompletePopup: popup } = jsterm; + + const onPopUpOpen = popup.once("popup-opened"); + + info("wait for completion: window.foo."); + setInputValue(hud, "window.foo"); + EventUtils.sendString("."); + + await onPopUpOpen; + + ok(popup.isOpen, "popup is open"); + ok(popup.itemCount, "popup has items"); + + info("reload the page to close the popup"); + const onPopupClose = popup.once("popup-closed"); + await refreshTab(); + await onPopupClose; + + ok(!popup.isOpen, "popup is not open after reloading the page"); + is(getInputValue(hud), "window.foo.", "completion was cancelled"); + ok(!getInputCompletionValue(hud), "completeNode is empty"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_await.js b/devtools/client/webconsole/test/browser/browser_jsterm_await.js new file mode 100644 index 0000000000..dbaea46355 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_await.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that top-level await expressions work as expected. + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf-8,Web Console test top-level await"; + +add_task(async function() { + // Enable await mapping. + await pushPref("devtools.debugger.features.map-await-expression", true); + + const hud = await openNewTabAndConsole(TEST_URI); + + const executeAndWaitForResultMessage = (input, expectedOutput) => + executeAndWaitForMessage(hud, input, expectedOutput, ".result"); + + info("Evaluate a top-level await expression"); + const simpleAwait = `await new Promise(r => setTimeout(() => r(["await1"]), 500))`; + await executeAndWaitForResultMessage(simpleAwait, `Array [ "await1" ]`); + + // Check that the resulting promise of the async iife is not displayed. + const messages = hud.ui.outputNode.querySelectorAll(".message .message-body"); + const messagesText = Array.from(messages) + .map(n => n.textContent) + .join(" - "); + is( + messagesText, + `${simpleAwait} - Array [ "await1" ]`, + "The output contains the the expected messages" + ); + + // Check that the timestamp of the result is accurate + const { + visibleMessages, + messagesById, + } = hud.ui.wrapper.getStore().getState().messages; + const [commandId, resultId] = visibleMessages; + const delta = + messagesById.get(resultId).timeStamp - + messagesById.get(commandId).timeStamp; + ok( + delta >= 500, + `The result has a timestamp at least 500ms (${delta}ms) older than the command` + ); + + info("Check that assigning the result of a top-level await expression works"); + await executeAndWaitForResultMessage( + `x = await new Promise(r => setTimeout(() => r("await2"), 500))`, + `await2` + ); + + let message = await executeAndWaitForResultMessage( + `"-" + x + "-"`, + `"-await2-"` + ); + ok(message.node, "`x` was assigned as expected"); + + info("Check that a logged promise is still displayed as a promise"); + message = await executeAndWaitForResultMessage( + `new Promise(r => setTimeout(() => r(1), 1000))`, + `Promise {` + ); + ok(message, "Promise are displayed as expected"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_await_assignments.js b/devtools/client/webconsole/test/browser/browser_jsterm_await_assignments.js new file mode 100644 index 0000000000..5b59b9bbd7 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_await_assignments.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that top-level await expressions work as expected. + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf-8,Web Console test top-level await bindings"; + +add_task(async function() { + // Enable await mapping. + await pushPref("devtools.debugger.features.map-await-expression", true); + const hud = await openNewTabAndConsole(TEST_URI); + + info("Check that declaring a let variable does not create a global property"); + await executeAndWaitForMessage( + hud, + `let bazA = await new Promise(r => setTimeout(() => r("local-bazA"), 10))`, + "local-bazA", + ".result" + ); + await checkVariable(hud, "bazA"); + + info( + "Check that declaring a const variable does not create a global property" + ); + await executeAndWaitForMessage( + hud, + `const bazB = await new Promise(r => setTimeout(() => r("local-bazB"), 10))`, + "local-bazB", + ".result" + ); + await checkVariable(hud, "bazB"); + + info("Check that complex variable declarations work as expected"); + await executeAndWaitForMessage( + hud, + ` + let bazC = "local-bazC", bazD, bazE = "local-bazE"; + bazD = await new Promise(r => setTimeout(() => r("local-bazD"), 10)); + let { + a: bazF, + b: { + c: { + bazG = "local-bazG", + d: bazH, + e: [bazI, bazJ = "local-bazJ"] + }, + d: bazK = "local-bazK" + } + } = await ({ + a: "local-bazF", + b: { + c: { + d: "local-bazH", + e: ["local-bazI"] + } + } + });`, + "", + ".result" + ); + await checkVariable(hud, "bazC"); + await checkVariable(hud, "bazD"); + await checkVariable(hud, "bazE"); + await checkVariable(hud, "bazF"); + await checkVariable(hud, "bazG"); + await checkVariable(hud, "bazH"); + await checkVariable(hud, "bazI"); + await checkVariable(hud, "bazJ"); + await checkVariable(hud, "bazK"); +}); + +async function checkVariable(hud, varName) { + await executeAndWaitForMessage( + hud, + `window.${varName}`, + `undefined`, + ".result" + ); + ok(true, `The ${varName} assignment did not create a global variable`); + await executeAndWaitForMessage(hud, varName, `"local-${varName}"`, ".result"); + ok(true, `"${varName}" has the expected value`); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_await_concurrent.js b/devtools/client/webconsole/test/browser/browser_jsterm_await_concurrent.js new file mode 100644 index 0000000000..3ebe742786 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_await_concurrent.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that multiple concurrent top-level await expressions work as expected. + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf-8,Web Console test top-level await"; + +add_task(async function() { + // Enable await mapping. + await pushPref("devtools.debugger.features.map-await-expression", true); + const hud = await openNewTabAndConsole(TEST_URI); + + await clearOutput(hud); + const delays = [3000, 500, 9000, 6000]; + const inputs = delays.map( + delay => `await new Promise( + r => setTimeout(() => r("await-concurrent-" + ${delay}), ${delay}))` + ); + + // Let's wait for the message that sould be displayed last. + const onMessage = waitForMessage( + hud, + "await-concurrent-9000", + ".message.result" + ); + for (const input of inputs) { + execute(hud, input); + } + await onMessage; + + const messages = hud.ui.outputNode.querySelectorAll(".message .message-body"); + const messagesText = Array.from(messages).map(n => n.textContent); + const expectedMessages = [ + ...inputs, + `"await-concurrent-500"`, + `"await-concurrent-3000"`, + `"await-concurrent-6000"`, + `"await-concurrent-9000"`, + ]; + is( + JSON.stringify(messagesText, null, 2), + JSON.stringify(expectedMessages, null, 2), + "The output contains the the expected messages, in the expected order" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_await_concurrent_same_result.js b/devtools/client/webconsole/test/browser/browser_jsterm_await_concurrent_same_result.js new file mode 100644 index 0000000000..1dbe0e0230 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_await_concurrent_same_result.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that hitting Ctrl + E does toggle the editor mode. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=1519105 + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf-8,Test concurrent top-level await expressions returning same value"; + +add_task(async function() { + // Enable editor mode as we'll be able to quicly trigger multiple evaluations. + await pushPref("devtools.webconsole.input.editor", true); + + const hud = await openNewTabAndConsole(TEST_URI); + setInputValue( + hud, + `await new Promise(res => setTimeout(() => res("foo"), 5000))` + ); + + info("Evaluate the expression 3 times in a row"); + const executeButton = hud.ui.outputNode.querySelector( + ".webconsole-editor-toolbar-executeButton" + ); + + executeButton.click(); + executeButton.click(); + executeButton.click(); + + await waitFor( + () => findMessages(hud, "foo", ".result").length === 3, + "Waiting for all results to be printed in console", + 1000 + ); + ok(true, "There are as many results as commands"); + + Services.prefs.clearUserPref("devtools.webconsole.input.editor"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_await_dynamic_import.js b/devtools/client/webconsole/test/browser/browser_jsterm_await_dynamic_import.js new file mode 100644 index 0000000000..b031e889c9 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_await_dynamic_import.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that top-level await with dynamic import works as expected. + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/test/browser/test-dynamic-import.html"; + +add_task(async function() { + // Enable dynamic import + await pushPref("javascript.options.dynamicImport", true); + // Enable await mapping. + await pushPref("devtools.debugger.features.map-await-expression", true); + + const hud = await openNewTabAndConsole(TEST_URI); + + const executeAndWaitForResultMessage = (input, expectedOutput) => + executeAndWaitForMessage(hud, input, expectedOutput, ".result"); + + info("Evaluate an expression with a dynamic import"); + let importAwaitExpression = ` + var {sum} = await import("./test-dynamic-import.js"); + sum(1, 2, 3); + `; + await executeAndWaitForResultMessage(importAwaitExpression, `1 + 2 + 3 = 6`); + ok(true, "The `sum` module was imported and used successfully"); + + info("Import the same module a second time"); + // This used to make the content page crash (See Bug 1523897). + importAwaitExpression = ` + var {sum} = await import("./test-dynamic-import.js"); + sum(2, 3, 4); + `; + await executeAndWaitForResultMessage(importAwaitExpression, `2 + 3 + 4 = 9`); + ok(true, "The `sum` module was imported and used successfully a second time"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_await_error.js b/devtools/client/webconsole/test/browser/browser_jsterm_await_error.js new file mode 100644 index 0000000000..4cdcd5e108 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_await_error.js @@ -0,0 +1,198 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that failing top-level await expression (rejected or throwing) work as expected. + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf-8,Web Console test failing top-level await"; + +add_task(async function() { + // Needed for the execute() function below + await pushPref("security.allow_parent_unrestricted_js_loads", true); + + // Enable await mapping. + await pushPref("devtools.debugger.features.map-await-expression", true); + const hud = await openNewTabAndConsole(TEST_URI); + + const executeAndWaitForErrorMessage = (input, expectedOutput) => + executeAndWaitForMessage(hud, input, expectedOutput, ".error"); + + info("Check that awaiting for a rejecting promise displays an error"); + let res = await executeAndWaitForErrorMessage( + `await new Promise((resolve,reject) => setTimeout(() => reject("await-rej"), 250))`, + "Uncaught (in promise) await-rej" + ); + ok(res.node, "awaiting for a rejecting promise displays an error message"); + + res = await executeAndWaitForErrorMessage( + `await Promise.reject("await-rej-2")`, + `Uncaught (in promise) await-rej-2` + ); + ok(res.node, "awaiting for Promise.reject displays an error"); + + res = await executeAndWaitForErrorMessage( + `await Promise.reject("")`, + `Uncaught (in promise) <empty string>` + ); + ok( + res.node, + "awaiting for Promise rejecting with empty string displays the expected error" + ); + + res = await executeAndWaitForErrorMessage( + `await Promise.reject(null)`, + `Uncaught (in promise) null` + ); + ok( + res.node, + "awaiting for Promise rejecting with null displays the expected error" + ); + + res = await executeAndWaitForErrorMessage( + `await Promise.reject(undefined)`, + `Uncaught (in promise) undefined` + ); + ok( + res.node, + "awaiting for Promise rejecting with undefined displays the expected error" + ); + + res = await executeAndWaitForErrorMessage( + `await Promise.reject(false)`, + `Uncaught (in promise) false` + ); + ok( + res.node, + "awaiting for Promise rejecting with false displays the expected error" + ); + + res = await executeAndWaitForErrorMessage( + `await Promise.reject(0)`, + `Uncaught (in promise) 0` + ); + ok( + res.node, + "awaiting for Promise rejecting with 0 displays the expected error" + ); + + res = await executeAndWaitForErrorMessage( + `await Promise.reject({foo: "bar"})`, + `Uncaught (in promise) Object { foo: "bar" }` + ); + ok( + res.node, + "awaiting for Promise rejecting with an object displays the expected error" + ); + + res = await executeAndWaitForErrorMessage( + `await Promise.reject(new Error("foo"))`, + `Uncaught (in promise) Error: foo` + ); + ok( + res.node, + "awaiting for Promise rejecting with an error object displays the expected error" + ); + + res = await executeAndWaitForErrorMessage( + `var err = new Error("foo"); + err.name = "CustomError"; + await Promise.reject(err); + `, + `Uncaught (in promise) CustomError: foo` + ); + ok( + res.node, + "awaiting for Promise rejecting with an error object with a name property displays the expected error" + ); + + res = await executeAndWaitForErrorMessage( + `await new Promise(() => a.b.c)`, + `ReferenceError: a is not defined` + ); + ok( + res.node, + "awaiting for a promise with a throwing function displays an error" + ); + + res = await executeAndWaitForErrorMessage( + `await new Promise(res => setTimeout(() => res(d.e.f), 250))`, + `ReferenceError: d is not defined` + ); + ok( + res.node, + "awaiting for a promise with a throwing function displays an error" + ); + + res = await executeAndWaitForErrorMessage( + `await new Promise(res => { throw "instant throw"; })`, + `Uncaught (in promise) instant throw` + ); + ok( + res.node, + "awaiting for a promise with a throwing function displays an error" + ); + + res = await executeAndWaitForErrorMessage( + `await new Promise(res => { throw new Error("instant error throw"); })`, + `Error: instant error throw` + ); + ok( + res.node, + "awaiting for a promise with a thrown Error displays an error message" + ); + + res = await executeAndWaitForErrorMessage( + `await new Promise(res => { setTimeout(() => { throw "throw in timeout"; }, 250) })`, + `Uncaught throw in timeout` + ); + ok( + res.node, + "awaiting for a promise with a throwing function displays an error" + ); + + res = await executeAndWaitForErrorMessage( + `await new Promise(res => { + setTimeout(() => { throw new Error("throw error in timeout"); }, 250) + })`, + `throw error in timeout` + ); + ok( + res.node, + "awaiting for a promise with a throwing function displays an error" + ); + + info("Check that we have the expected number of commands"); + const expectedInputsNumber = 16; + is( + hud.ui.outputNode.querySelectorAll(".message.command").length, + expectedInputsNumber, + "There is the expected number of commands messages" + ); + + info("Check that we have as many errors as commands"); + const expectedErrorsNumber = expectedInputsNumber; + is( + hud.ui.outputNode.querySelectorAll(".message.error").length, + expectedErrorsNumber, + "There is the expected number of error messages" + ); + + info("Check that there's no result message"); + is( + hud.ui.outputNode.querySelectorAll(".message.result").length, + 0, + "There is no result messages" + ); + + info("Check that malformed await expressions displays a meaningful error"); + res = await executeAndWaitForErrorMessage( + `await new Promise())`, + `SyntaxError: unexpected token: ')'` + ); + ok( + res.node, + "awaiting for a malformed expression displays a meaningful error" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_await_helper_dollar_underscore.js b/devtools/client/webconsole/test/browser/browser_jsterm_await_helper_dollar_underscore.js new file mode 100644 index 0000000000..512a7204b6 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_await_helper_dollar_underscore.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that $_ works as expected with top-level await expressions. + +"use strict"; +requestLongerTimeout(2); + +const TEST_URI = "data:text/html;charset=utf-8,top-level await + $_"; + +add_task(async function() { + // Enable await mapping. + await pushPref("devtools.debugger.features.map-await-expression", true); + const hud = await openNewTabAndConsole(TEST_URI); + + const executeAndWaitForResultMessage = (input, expectedOutput) => + executeAndWaitForMessage(hud, input, expectedOutput, ".result"); + + info("Evaluate a simple expression to populate $_"); + await executeAndWaitForResultMessage(`1 + 1`, `2`); + + await executeAndWaitForResultMessage(`$_ + 1`, `3`); + ok(true, "$_ works as expected"); + + info( + "Check that $_ does not get replaced until the top-level await is resolved" + ); + const onAwaitResultMessage = executeAndWaitForResultMessage( + `await new Promise(res => setTimeout(() => res([1,2,3, $_]), 1000))`, + `Array(4) [ 1, 2, 3, 4 ]` + ); + + await executeAndWaitForResultMessage(`$_ + 1`, `4`); + ok(true, "$_ was not impacted by the top-level await input"); + + await onAwaitResultMessage; + ok(true, "the top-level await result can use $_ in its returned value"); + + await executeAndWaitForResultMessage( + `await new Promise(res => setTimeout(() => res([...$_, 5]), 1000))`, + `Array(5) [ 1, 2, 3, 4, 5 ]` + ); + ok(true, "$_ is assigned with the result of the top-level await"); + + info("Check that awaiting for a rejecting promise does not re-assign $_"); + await executeAndWaitForMessage( + hud, + `x = await new Promise((resolve,reject) => + setTimeout(() => reject("await-" + "rej"), 500))`, + `await-rej`, + `.error` + ); + + await executeAndWaitForResultMessage(`$_`, `Array(5) [ 1, 2, 3, 4, 5 ]`); + ok(true, "$_ wasn't re-assigned"); + + info("Check that $_ gets the value of the last resolved await expression"); + const delays = [2000, 1000, 4000, 3000]; + const inputs = delays.map( + delay => `await new Promise( + r => setTimeout(() => r("await-concurrent-" + ${delay}), ${delay}))` + ); + + // Let's wait for the message that should be displayed last. + const onMessage = waitForMessage( + hud, + "await-concurrent-4000", + ".message.result" + ); + for (const input of inputs) { + execute(hud, input); + } + await onMessage; + + await executeAndWaitForResultMessage( + `"result: " + $_`, + `"result: await-concurrent-4000"` + ); + ok( + true, + "$_ was replaced with the last resolving top-level await evaluation result" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_await_paused.js b/devtools/client/webconsole/test/browser/browser_jsterm_await_paused.js new file mode 100644 index 0000000000..108282a8ba --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_await_paused.js @@ -0,0 +1,80 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that top-level await expression work as expected when debugger is paused. + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf-8,Web Console test top-level await when debugger paused`; + +add_task(async function() { + // Enable await mapping. + await pushPref("devtools.debugger.features.map-await-expression", true); + + // Force the split console to be closed. + await pushPref("devtools.toolbox.splitconsoleEnabled", false); + const hud = await openNewTabAndConsole(TEST_URI); + + const pauseExpression = `(() => { + var inPausedExpression = ["bar"]; + /* Will pause the script and open the debugger panel */ + debugger; + return "pauseExpression-res"; + })()`; + execute(hud, pauseExpression); + + // wait for the debugger to be opened and paused. + const target = await TargetFactory.forTab(gBrowser.selectedTab); + const toolbox = gDevTools.getToolbox(target); + + await waitFor(() => toolbox.getPanel("jsdebugger")); + const dbg = createDebuggerContext(toolbox); + await waitForPaused(dbg); + + await toolbox.openSplitConsole(); + + const awaitExpression = `await new Promise(res => { + const result = ["res", ...inPausedExpression]; + setTimeout(() => res(result), 1000); + })`; + + const onAwaitResultMessage = waitForMessage( + hud, + `[ "res", "bar" ]`, + ".message.result" + ); + execute(hud, awaitExpression); + + // We send an evaluation just after the await one to ensure the await evaluation was + // done. We can't await on the previous execution because it waits for the result to + // be send, which won't happen until we resume the debugger. + await executeAndWaitForMessage(hud, `"smoke"`, `"smoke"`, ".result"); + + // Give the engine some time to evaluate the await expression before resuming. + await waitForTick(); + + // Click on the resume button to not be paused anymore. + await resume(dbg); + + info("Wait for the paused expression result to be displayed"); + await waitFor(() => findMessage(hud, "pauseExpression-res", ".result")); + + await onAwaitResultMessage; + const messages = hud.ui.outputNode.querySelectorAll( + ".message.result .message-body" + ); + const messagesText = Array.from(messages).map(n => n.textContent); + const expectedMessages = [ + // Result of "smoke" + `"smoke"`, + // The result of pauseExpression (after smoke since pauseExpression iife was paused) + `"pauseExpression-res"`, + // Result of await + `Array [ "res", "bar" ]`, + ]; + is( + JSON.stringify(messagesText, null, 2), + JSON.stringify(expectedMessages, null, 2), + "The output contains the the expected messages, in the expected order" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_block_command.js b/devtools/client/webconsole/test/browser/browser_jsterm_block_command.js new file mode 100644 index 0000000000..63b3c9fa7e --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_block_command.js @@ -0,0 +1,93 @@ +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-block-action.html"; +const TIMEOUT = "TIMEOUT"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + ok(hud, "web console opened"); + + const filter = "test-block-action-style.css"; + const blockCommand = `:block ${filter}`; + const unblockCommand = `:unblock ${filter}`; + + info("Before blocking"); + await tryFetching(); + const resp1 = await waitFor(() => findMessage(hud, "successful")); + ok(resp1, "the request was not blocked"); + info(`Execute the :block command and try to do execute a network request`); + await executeAndWaitForMessage(hud, blockCommand, "are now blocked"); + await tryFetching(); + + const resp2 = await waitFor(() => findMessage(hud, "blocked")); + ok(resp2, "the request was blocked as expected"); + + info("Open netmonitor check the blocked filter is registered in its state"); + const { panelWin } = await openNetMonitor(); + const nmStore = panelWin.store; + nmStore.dispatch(panelWin.actions.toggleRequestBlockingPanel()); + //await waitForTime(1e7); + // wait until the blockedUrls property is populated + await waitFor( + () => nmStore.getState().requestBlocking.blockedUrls.length > 0 + ); + const netMonitorState1 = nmStore.getState(); + is( + netMonitorState1.requestBlocking.blockedUrls[0].url, + filter, + "blocked request shows up in netmonitor state" + ); + + info("Switch back to the console"); + await hud.toolbox.selectTool("webconsole"); + + // :unblock + await executeAndWaitForMessage(hud, unblockCommand, "Removed blocking"); + info("After unblocking"); + + const netMonitorState2 = nmStore.getState(); + is( + netMonitorState2.requestBlocking.blockedUrls.length, + 0, + "unblocked request should not be in netmonitor state" + ); + + await tryFetching(); + + const resp3 = await waitFor(() => findMessage(hud, "successful")); + ok(resp3, "the request was not blocked"); +}); + +async function tryFetching() { + await SpecialPowers.spawn(gBrowser.selectedBrowser, [TIMEOUT], async function( + timeoutStr + ) { + const win = content.wrappedJSObject; + const FETCH_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-block-action-style.css"; + const timeout = new Promise(res => + win.setTimeout(() => res(timeoutStr), 1000) + ); + const fetchPromise = win.fetch(FETCH_URI); + + try { + const resp = await Promise.race([fetchPromise, timeout]); + if (typeof resp === "object") { + // Request Promise + win.console.log("the request was successful"); + } else if (resp === timeoutStr) { + // Timeout + win.console.log("the request was blocked"); + } else { + win.console.error("Unkown response"); + } + } catch { + // NetworkError + win.console.log("the request was blocked"); + } + }); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_completion.js b/devtools/client/webconsole/test/browser/browser_jsterm_completion.js new file mode 100644 index 0000000000..ff093c898f --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_completion.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that code completion works properly. + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf8,<p>test code completion + <script> + foobar = true; + </script>`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + const { autocompletePopup } = jsterm; + + // Test typing 'docu'. + await setInputValueForAutocompletion(hud, "foob"); + is(getInputValue(hud), "foob", "'foob' completion (input.value)"); + checkInputCompletionValue(hud, "ar", "'foob' completion (completeNode)"); + is(autocompletePopup.items.length, 1, "autocomplete popup has 1 item"); + is(autocompletePopup.isOpen, false, "autocomplete popup is not open"); + + // Test typing 'docu' and press tab. + EventUtils.synthesizeKey("KEY_Tab"); + is(getInputValue(hud), "foobar", "'foob' tab completion"); + + checkInputCursorPosition( + hud, + "foobar".length, + "cursor is at the end of 'foobar'" + ); + is(getInputCompletionValue(hud).replace(/ /g, ""), "", "'foob' completed"); + + // Test typing 'window.Ob' and press tab. Just 'window.O' is + // ambiguous: could be window.Object, window.Option, etc. + await setInputValueForAutocompletion(hud, "window.Ob"); + EventUtils.synthesizeKey("KEY_Tab"); + is(getInputValue(hud), "window.Object", "'window.Ob' tab completion"); + + // Test typing 'document.getElem'. + const onPopupOpened = autocompletePopup.once("popup-opened"); + await setInputValueForAutocompletion(hud, "document.getElem"); + is(getInputValue(hud), "document.getElem", "'document.getElem' completion"); + checkInputCompletionValue(hud, "entById", "'document.getElem' completion"); + + // Test pressing key down. + await onPopupOpened; + EventUtils.synthesizeKey("KEY_ArrowDown"); + is(getInputValue(hud), "document.getElem", "'document.getElem' completion"); + checkInputCompletionValue( + hud, + "entsByClassName", + "'document.getElem' another tab completion" + ); + + // Test pressing key up. + EventUtils.synthesizeKey("KEY_ArrowUp"); + await waitFor(() => (getInputCompletionValue(hud) || "").includes("entById")); + is( + getInputValue(hud), + "document.getElem", + "'document.getElem' untab completion" + ); + checkInputCompletionValue(hud, "entById", "'document.getElem' completion"); + + await clearOutput(hud); + + await setInputValueForAutocompletion(hud, "docu"); + checkInputCompletionValue(hud, "ment", "'docu' completion"); + + let onAutocompletUpdated = jsterm.once("autocomplete-updated"); + EventUtils.synthesizeKey("KEY_Enter"); + await onAutocompletUpdated; + checkInputCompletionValue(hud, "", "clear completion on execute()"); + + // Test multi-line completion works. We can't use setInputValueForAutocompletion because + // it would trigger an evaluation (because of the new line, an Enter keypress is + // simulated). + onAutocompletUpdated = jsterm.once("autocomplete-updated"); + setInputValue(hud, "console.log('one');\n"); + EventUtils.sendString("consol"); + await onAutocompletUpdated; + checkInputCompletionValue(hud, "e", "multi-line completion"); + + // Test multi-line completion works even if there is text after the cursor + onAutocompletUpdated = jsterm.once("autocomplete-updated"); + setInputValue(hud, "{\n\n}"); + EventUtils.synthesizeKey("KEY_ArrowUp"); + EventUtils.sendString("console.g"); + await onAutocompletUpdated; + checkInputValueAndCursorPosition(hud, "{\nconsole.g|\n}"); + checkInputCompletionValue(hud, "roup", "multi-line completion"); + is(autocompletePopup.isOpen, true, "popup is opened"); + + // Test non-object autocompletion. + await setInputValueForAutocompletion(hud, "Object.name.sl"); + checkInputCompletionValue(hud, "ice", "non-object completion"); + + // Test string literal autocompletion. + await setInputValueForAutocompletion(hud, "'Asimov'.sl"); + checkInputCompletionValue(hud, "ice", "string literal completion"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_completion_bracket.js b/devtools/client/webconsole/test/browser/browser_jsterm_completion_bracket.js new file mode 100644 index 0000000000..5c066c9bec --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_completion_bracket.js @@ -0,0 +1,254 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that code completion works properly with `[` + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf8,<p>test [ completion. + <script> + window.testObject = Object.create(null, Object.getOwnPropertyDescriptors({ + bar: 0, + dataTest: 1, + "data-test": 2, + 'da"ta"test': 3, + "da\`ta\`test": 4, + "da'ta'test": 5, + "DATA-TEST": 6, + "DAT_\\\\a\\"'\`\${0}\\u0000\\b\\t\\n\\f\\r\\ude80\\ud83d\\ud83d\\ude80_TEST": 7, + })); + </script>`; + +add_task(async function() { + await pushPref("devtools.editor.autoclosebrackets", false); + const hud = await openNewTabAndConsole(TEST_URI); + await testInputs(hud, false); + await testCompletionTextUpdateOnPopupNavigate(hud, false); + await testAcceptCompletionExistingClosingBracket(hud); + + info("Test again with autoclosebracket set to true"); + await pushPref("devtools.editor.autoclosebrackets", true); + const hudAutoclose = await openNewTabAndConsole(TEST_URI); + await testInputs(hudAutoclose, true); + await testCompletionTextUpdateOnPopupNavigate(hudAutoclose, true); + await testAcceptCompletionExistingClosingBracket(hudAutoclose); +}); + +async function testInputs(hud, autocloseEnabled) { + const tests = [ + { + description: "Check that the popup is opened when typing `[`", + input: "window.testObject[", + expectedItems: [ + `"bar"`, + `"da'ta'test"`, + `"da\\"ta\\"test"`, + `"da\`ta\`test"`, + `"data-test"`, + `"dataTest"`, + `"DAT_\\\\a\\"'\`\${0}\\u0000\\b\\t\\n\\f\\r\\ude80\\ud83d🚀_TEST"`, + `"DATA-TEST"`, + ], + expectedCompletionText: autocloseEnabled ? "" : `"bar"]`, + expectedInputAfterCompletion: `window.testObject["bar"]`, + }, + { + description: "Test that the list can be filtered even without quote", + input: "window.testObject[d", + expectedItems: [ + `"da'ta'test"`, + `"da\\"ta\\"test"`, + `"da\`ta\`test"`, + `"data-test"`, + `"dataTest"`, + `"DAT_\\\\a\\"'\`\${0}\\u0000\\b\\t\\n\\f\\r\\ude80\\ud83d🚀_TEST"`, + `"DATA-TEST"`, + ], + expectedCompletionText: autocloseEnabled ? "" : `a'ta'test"]`, + expectedInputAfterCompletion: `window.testObject["da'ta'test"]`, + }, + { + description: "Test filtering with quote and string", + input: `window.testObject["d`, + expectedItems: [ + `"da'ta'test"`, + `"da\\"ta\\"test"`, + `"da\`ta\`test"`, + `"data-test"`, + `"dataTest"`, + `"DAT_\\\\a\\"'\`\${0}\\u0000\\b\\t\\n\\f\\r\\ude80\\ud83d🚀_TEST"`, + `"DATA-TEST"`, + ], + expectedCompletionText: autocloseEnabled ? "" : `a'ta'test"]`, + expectedInputAfterCompletion: `window.testObject["da'ta'test"]`, + }, + { + description: "Test filtering with simple quote and string", + input: `window.testObject['d`, + expectedItems: [ + `'da"ta"test'`, + `'da\\'ta\\'test'`, + `'da\`ta\`test'`, + `'data-test'`, + `'dataTest'`, + `'DAT_\\\\a"\\'\`\${0}\\u0000\\b\\t\\n\\f\\r\\ude80\\ud83d🚀_TEST'`, + `'DATA-TEST'`, + ], + expectedCompletionText: autocloseEnabled ? "" : `a"ta"test']`, + expectedInputAfterCompletion: `window.testObject['da"ta"test']`, + }, + { + description: "Test filtering with template literal and string", + input: "window.testObject[`d", + expectedItems: [ + "`da'ta'test`", + '`da"ta"test`', + "`da\\`ta\\`test`", + "`data-test`", + "`dataTest`", + "`DAT_\\\\a\"'\\`\\${0}\\u0000\\b\\t\\n\\f\\r\\ude80\\ud83d🚀_TEST`", + "`DATA-TEST`", + ], + expectedCompletionText: autocloseEnabled ? "" : "a'ta'test`]", + expectedInputAfterCompletion: "window.testObject[`da'ta'test`]", + }, + { + description: "Test that filtering is case insensitive", + input: "window.testObject[data-t", + expectedItems: [`"data-test"`, `"DATA-TEST"`], + expectedCompletionText: autocloseEnabled ? "" : `est"]`, + expectedInputAfterCompletion: `window.testObject["data-test"]`, + }, + { + description: + "Test that filtering without quote displays the popup when there's only 1 match", + input: "window.testObject[DATA-", + expectedItems: [`"DATA-TEST"`], + expectedCompletionText: autocloseEnabled ? "" : `TEST"]`, + expectedInputAfterCompletion: `window.testObject["DATA-TEST"]`, + }, + ]; + + for (const test of tests) { + await testInput(hud, test); + } +} + +async function testInput( + hud, + { + description, + input, + expectedItems, + expectedCompletionText, + expectedInputAfterCompletion, + } +) { + const { jsterm } = hud; + const { autocompletePopup } = jsterm; + + info(`${description} - test popup opening`); + const onPopUpOpen = autocompletePopup.once("popup-opened"); + EventUtils.sendString(input); + await onPopUpOpen; + + ok( + hasExactPopupLabels(autocompletePopup, expectedItems), + `${description} - popup has expected item, in expected order` + ); + checkInputCompletionValue( + hud, + expectedCompletionText, + `${description} - completeNode has expected value` + ); + + info(`${description} - test accepting completion`); + const onPopupClose = autocompletePopup.once("popup-closed"); + EventUtils.synthesizeKey("KEY_Tab"); + await onPopupClose; + checkInputValueAndCursorPosition( + hud, + expectedInputAfterCompletion + "|", + `${description} - input was completed as expected` + ); + checkInputCompletionValue(hud, "", `${description} - completeNode is empty`); + + setInputValue(hud, ""); +} + +async function testCompletionTextUpdateOnPopupNavigate(hud, autocloseEnabled) { + const { jsterm } = hud; + const { autocompletePopup } = jsterm; + + info( + "Test that navigating the popup list update the completionText as expected" + ); + const onPopUpOpen = autocompletePopup.once("popup-opened"); + const input = `window.testObject[data`; + EventUtils.sendString(input); + await onPopUpOpen; + + ok( + hasExactPopupLabels(autocompletePopup, [ + `"data-test"`, + `"dataTest"`, + `"DATA-TEST"`, + ]), + `popup has expected items, in expected order` + ); + checkInputCompletionValue( + hud, + autocloseEnabled ? "" : `-test"]`, + `completeNode has expected value` + ); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + checkInputCompletionValue( + hud, + autocloseEnabled ? "" : `Test"]`, + `completeNode has expected value` + ); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + checkInputCompletionValue( + hud, + autocloseEnabled ? "" : `-TEST"]`, + `completeNode has expected value` + ); + + const onPopupClose = autocompletePopup.once("popup-closed"); + EventUtils.synthesizeKey("KEY_Tab"); + await onPopupClose; + checkInputValueAndCursorPosition( + hud, + `window.testObject["DATA-TEST"]|`, + `input was completed as expected after navigating the popup` + ); +} + +async function testAcceptCompletionExistingClosingBracket(hud) { + const { jsterm } = hud; + const { autocompletePopup } = jsterm; + + info( + "Check that accepting completion when there's a closing bracket does not append " + + "another closing bracket" + ); + await setInputValueForAutocompletion(hud, "window.testObject[]", -1); + const onPopUpOpen = autocompletePopup.once("popup-opened"); + EventUtils.sendString(`"b`); + await onPopUpOpen; + ok( + hasExactPopupLabels(autocompletePopup, [`"bar"`]), + `popup has expected item` + ); + + const onPopupClose = autocompletePopup.once("popup-closed"); + EventUtils.synthesizeKey("KEY_Tab"); + await onPopupClose; + checkInputValueAndCursorPosition( + hud, + `window.testObject["bar"]|`, + `input was completed as expected, without adding a closing bracket` + ); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_completion_bracket_cached_results.js b/devtools/client/webconsole/test/browser/browser_jsterm_completion_bracket_cached_results.js new file mode 100644 index 0000000000..c880749ab2 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_completion_bracket_cached_results.js @@ -0,0 +1,130 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that code completion works properly with `[` and cached results + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf8,<p>test [ completion cached results. + <script> + window.testObject = Object.create(null, Object.getOwnPropertyDescriptors({ + bar: 0, + dataTest: 1, + "data-test": 2, + 'da"ta"test': 3, + "da\`ta\`test": 4, + "da'ta'test": 5, + "DATA-TEST": 6, + })); + </script>`; + +add_task(async function() { + await pushPref("devtools.editor.autoclosebrackets", false); + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + + info("Test that the autocomplete cache works with brackets"); + const { autocompletePopup } = jsterm; + + const tests = [ + { + description: "Test that it works if the user did not type a quote", + initialInput: `window.testObject[dat`, + expectedItems: [`"data-test"`, `"dataTest"`, `"DATA-TEST"`], + expectedCompletionText: `a-test"]`, + sequence: [ + { + char: "a", + expectedItems: [`"data-test"`, `"dataTest"`, `"DATA-TEST"`], + expectedCompletionText: `-test"]`, + }, + { + char: "-", + expectedItems: [`"data-test"`, `"DATA-TEST"`], + expectedCompletionText: `test"]`, + }, + { + char: "t", + expectedItems: [`"data-test"`, `"DATA-TEST"`], + expectedCompletionText: `est"]`, + }, + { + char: "e", + expectedItems: [`"data-test"`, `"DATA-TEST"`], + expectedCompletionText: `st"]`, + }, + ], + }, + { + description: "Test that it works if the user did type a quote", + initialInput: `window.testObject['dat`, + expectedItems: [`'data-test'`, `'dataTest'`, `'DATA-TEST'`], + expectedCompletionText: `a-test']`, + sequence: [ + { + char: "a", + expectedItems: [`'data-test'`, `'dataTest'`, `'DATA-TEST'`], + expectedCompletionText: `-test']`, + }, + { + char: "-", + expectedItems: [`'data-test'`, `'DATA-TEST'`], + expectedCompletionText: `test']`, + }, + { + char: "t", + expectedItems: [`'data-test'`, `'DATA-TEST'`], + expectedCompletionText: `est']`, + }, + { + char: "e", + expectedItems: [`'data-test'`, `'DATA-TEST'`], + expectedCompletionText: `st']`, + }, + ], + }, + ]; + + for (const test of tests) { + info(test.description); + + let onPopupUpdate = jsterm.once("autocomplete-updated"); + EventUtils.sendString(test.initialInput); + await onPopupUpdate; + + ok( + hasExactPopupLabels(autocompletePopup, test.expectedItems), + `popup has expected items, in expected order` + ); + + checkInputCompletionValue( + hud, + test.expectedCompletionText, + `completeNode has expected value` + ); + for (const { + char, + expectedItems, + expectedCompletionText, + } of test.sequence) { + onPopupUpdate = jsterm.once("autocomplete-updated"); + EventUtils.sendString(char); + await onPopupUpdate; + + ok( + hasExactPopupLabels(autocompletePopup, expectedItems), + `popup has expected items, in expected order` + ); + checkInputCompletionValue( + hud, + expectedCompletionText, + `completeNode has expected value` + ); + } + + const onPopupClose = autocompletePopup.once("popup-closed"); + EventUtils.synthesizeKey("KEY_Escape"); + await onPopupClose; + setInputValue(hud, ""); + } +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_completion_case_sensitivity.js b/devtools/client/webconsole/test/browser/browser_jsterm_completion_case_sensitivity.js new file mode 100644 index 0000000000..436bdf1365 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_completion_case_sensitivity.js @@ -0,0 +1,113 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that code completion works properly in regards to case sensitivity. + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf8,<p>test case-sensitivity completion. + <script> + fooBar = Object.create(null, Object.getOwnPropertyDescriptors({ + Foo: 1, + test: 2, + Test: 3, + TEST: 4, + })); + FooBar = true; + </script>`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + const { autocompletePopup } = jsterm; + + const checkInput = (expected, assertionInfo) => + checkInputValueAndCursorPosition(hud, expected, assertionInfo); + + info("Check that lowercased input is case-insensitive"); + await setInputValueForAutocompletion(hud, "foob"); + + ok( + hasExactPopupLabels(autocompletePopup, ["fooBar", "FooBar"]), + "popup has expected item, in expected order" + ); + + checkInputCompletionValue(hud, "ar", "completeNode has expected value"); + + info("Check that filtering the autocomplete cache is also case insensitive"); + let onAutoCompleteUpdated = jsterm.once("autocomplete-updated"); + // Send "a" to make the input "fooba" + EventUtils.sendString("a"); + await onAutoCompleteUpdated; + + checkInput("fooba|"); + ok( + hasExactPopupLabels(autocompletePopup, ["fooBar", "FooBar"]), + "popup cache filtering is also case-insensitive" + ); + checkInputCompletionValue(hud, "r", "completeNode has expected value"); + + info( + "Check that accepting the completion value will change the input casing" + ); + let onPopupClose = autocompletePopup.once("popup-closed"); + EventUtils.synthesizeKey("KEY_Tab"); + await onPopupClose; + checkInput("fooBar|", "The input was completed with the correct casing"); + checkInputCompletionValue(hud, "", "completeNode is empty"); + + info("Check that the popup is displayed with only 1 matching item"); + onAutoCompleteUpdated = jsterm.once("autocomplete-updated"); + EventUtils.sendString(".f"); + await onAutoCompleteUpdated; + ok(autocompletePopup.isOpen, "autocomplete popup is open"); + + // Here we want to match "Foo", and since the completion text will only be "oo", we want + // to display the popup so the user knows that we are matching "Foo" and not "foo". + checkInput("fooBar.f|"); + ok(true, "The popup was opened even if there's 1 item matching"); + ok( + hasExactPopupLabels(autocompletePopup, ["Foo"]), + "popup has expected item" + ); + checkInputCompletionValue(hud, "oo", "completeNode has expected value"); + + onPopupClose = autocompletePopup.once("popup-closed"); + EventUtils.synthesizeKey("KEY_Tab"); + await onPopupClose; + checkInput("fooBar.Foo|", "The input was completed with the correct casing"); + checkInputCompletionValue(hud, "", "completeNode is empty"); + + info("Check that Javascript keywords are displayed first"); + await setInputValueForAutocompletion(hud, "func"); + + ok( + hasExactPopupLabels(autocompletePopup, ["function", "Function"]), + "popup has expected item" + ); + checkInputCompletionValue(hud, "tion", "completeNode has expected value"); + + onPopupClose = autocompletePopup.once("popup-closed"); + EventUtils.synthesizeKey("KEY_Tab"); + await onPopupClose; + checkInput("function|", "The input was completed as expected"); + checkInputCompletionValue(hud, "", "completeNode is empty"); + + info("Check that filtering the cache works like on the server"); + await setInputValueForAutocompletion(hud, "fooBar."); + ok( + hasExactPopupLabels(autocompletePopup, ["test", "Foo", "Test", "TEST"]), + "popup has expected items" + ); + + onAutoCompleteUpdated = jsterm.once("autocomplete-updated"); + EventUtils.sendString("T"); + await onAutoCompleteUpdated; + ok( + hasExactPopupLabels(autocompletePopup, ["Test", "TEST"]), + "popup was filtered case-sensitively, as expected" + ); + + info("Close autocomplete popup"); + await closeAutocompletePopup(hud); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_completion_dollar_underscore.js b/devtools/client/webconsole/test/browser/browser_jsterm_completion_dollar_underscore.js new file mode 100644 index 0000000000..242c525236 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_completion_dollar_underscore.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that code completion works properly on $_. + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf8,<p>test code completion on $_`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + const { autocompletePopup } = jsterm; + + info( + "Test that there's no issue when trying to do an autocompletion without last " + + "evaluation result" + ); + await setInputValueForAutocompletion(hud, "$_."); + is(autocompletePopup.items.length, 0, "autocomplete popup has no items"); + is(autocompletePopup.isOpen, false, "autocomplete popup is not open"); + + info("Populate $_ by executing a command"); + await executeAndWaitForMessage( + hud, + `Object.create(null, Object.getOwnPropertyDescriptors({ + x: 1, + y: "hello" + }))`, + `Object { x: 1, y: "hello" }` + ); + + await setInputValueForAutocompletion(hud, "$_."); + checkInputCompletionValue(hud, "x", "'$_.' completion (completeNode)"); + ok( + hasExactPopupLabels(autocompletePopup, ["x", "y"]), + "autocomplete popup has expected items" + ); + is(autocompletePopup.isOpen, true, "autocomplete popup is open"); + + await setInputValueForAutocompletion(hud, "$_.x."); + is(autocompletePopup.isOpen, true, "autocomplete popup is open"); + ok( + hasPopupLabel(autocompletePopup, "toExponential"), + "autocomplete popup has expected items" + ); + + await setInputValueForAutocompletion(hud, "$_.y."); + is(autocompletePopup.isOpen, true, "autocomplete popup is open"); + ok( + hasPopupLabel(autocompletePopup, "trim"), + "autocomplete popup has expected items" + ); + + info("Close autocomplete popup"); + await closeAutocompletePopup(hud); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_completion_dollar_zero.js b/devtools/client/webconsole/test/browser/browser_jsterm_completion_dollar_zero.js new file mode 100644 index 0000000000..668ed1fe0e --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_completion_dollar_zero.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that code completion works properly on $0. + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf-8, +<head> + <title>$0 completion test</title> +</head> +<body> + <div> + <h1>$0 completion test</h1> + <p>This is some example text</p> + </div> +</body>`; + +add_task(async function() { + const toolbox = await openNewTabAndToolbox(TEST_URI, "inspector"); + const testActor = await getTestActor(toolbox); + await selectNodeWithPicker(toolbox, testActor, "h1"); + + info("Picker mode stopped, <h1> selected, now switching to the console"); + const hud = await openConsole(); + const { jsterm } = hud; + + await clearOutput(hud); + + const { autocompletePopup } = jsterm; + + await setInputValueForAutocompletion(hud, "$0."); + ok( + hasPopupLabel(autocompletePopup, "attributes"), + "autocomplete popup has expected items" + ); + is(autocompletePopup.isOpen, true, "autocomplete popup is open"); + + await setInputValueForAutocompletion(hud, "$0.attributes."); + is(autocompletePopup.isOpen, true, "autocomplete popup is open"); + ok( + hasPopupLabel(autocompletePopup, "getNamedItem"), + "autocomplete popup has expected items" + ); + + info("Close autocomplete popup"); + await closeAutocompletePopup(hud); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_completion_perfect_match.js b/devtools/client/webconsole/test/browser/browser_jsterm_completion_perfect_match.js new file mode 100644 index 0000000000..dcbc97a285 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_completion_perfect_match.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that code completion works properly in regards to case sensitivity. + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf8,<p>test completion perfect match. + <script> + x = Object.create(null, Object.getOwnPropertyDescriptors({ + foo: 1, + foO: 2, + fOo: 3, + fOO: 4, + })); + </script>`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + const { autocompletePopup } = jsterm; + + info("Check that filtering the cache works like on the server"); + await setInputValueForAutocompletion(hud, "x."); + ok( + hasExactPopupLabels(autocompletePopup, ["foo", "foO", "fOo", "fOO"]), + "popup has expected item, in expected order" + ); + + const onAutoCompleteUpdated = jsterm.once("autocomplete-updated"); + EventUtils.sendString("foO"); + await onAutoCompleteUpdated; + ok( + hasExactPopupLabels(autocompletePopup, ["foO", "foo", "fOo", "fOO"]), + "popup has expected item, in expected order" + ); + + info("Close autocomplete popup"); + await closeAutocompletePopup(hud); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_content_defined_helpers.js b/devtools/client/webconsole/test/browser/browser_jsterm_content_defined_helpers.js new file mode 100644 index 0000000000..64ee4577ac --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_content_defined_helpers.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that using helper functions in jsterm call the global content functions +// if they are defined. + +const PREFIX = "content-"; +const HELPERS = [ + "$_", + "$", + "$$", + "$0", + "$x", + "clear", + "clearHistory", + "copy", + "help", + "inspect", + "keys", + "screenshot", + "values", +]; + +// The page script sets a global function for each known helper (except print). +const TEST_URI = `data:text/html,<meta charset=utf8> + <script> + const helpers = ${JSON.stringify(HELPERS)}; + for (const helper of helpers) { + window[helper] = () => "${PREFIX}" + helper; + } + </script>`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + const { autocompletePopup } = jsterm; + + for (const helper of HELPERS) { + await setInputValueForAutocompletion(hud, helper); + const autocompleteItems = getAutocompletePopupLabels( + autocompletePopup + ).filter(l => l === helper); + is( + autocompleteItems.length, + 1, + `There's no duplicated "${helper}" item in the autocomplete popup` + ); + + await executeAndWaitForMessage( + hud, + `${helper}()`, + `"${PREFIX + helper}"`, + ".result" + ); + ok(true, `output is correct for ${helper}()`); + } +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_context_menu_labels.js b/devtools/client/webconsole/test/browser/browser_jsterm_context_menu_labels.js new file mode 100644 index 0000000000..819948b88d --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_context_menu_labels.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that context menu for CodeMirror is properly localized. + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf8,<p>test page</p>`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + + const target = await TargetFactory.forTab(gBrowser.selectedTab); + const toolbox = gDevTools.getToolbox(target); + + // Open context menu and wait until it's visible + const element = jsterm.node.querySelector(".CodeMirror-wrap"); + const menuPopup = await openTextBoxContextMenu(toolbox, element); + + // Check label of the 'undo' menu item. + const undoMenuItem = menuPopup.querySelector("#editmenu-undo"); + await waitUntil(() => !!undoMenuItem.getAttribute("label")); + + is( + undoMenuItem.getAttribute("label"), + "Undo", + "Undo is visible and localized" + ); +}); + +async function openTextBoxContextMenu(toolbox, element) { + const onConsoleMenuOpened = toolbox.once("menu-open"); + synthesizeContextMenuEvent(element); + await onConsoleMenuOpened; + return toolbox.getTextBoxContextMenu(); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_copy_command.js b/devtools/client/webconsole/test/browser/browser_jsterm_copy_command.js new file mode 100644 index 0000000000..b0099e7f5a --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_copy_command.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that the `copy` console helper works as intended. + +"use strict"; + +const text = + "Lorem ipsum dolor sit amet, consectetur adipisicing " + + "elit, sed do eiusmod tempor incididunt ut labore et dolore magna " + + "aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco " + + "laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure " + + "dolor in reprehenderit in voluptate velit esse cillum dolore eu " + + "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non " + + "proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + + new Date(); + +const id = "select-me"; +const TEST_URI = `data:text/html;charset=utf-8, +<body> + <div> + <h1>Testing copy command</h1> + <p>This is some example text</p> + <p id="${id}">${text}</p> + </div> + <div><p></p></div> +</body>`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const random = Math.random(); + const string = "Text: " + random; + const obj = { a: 1, b: "foo", c: random }; + + await testCopy(hud, random, random.toString()); + await testCopy(hud, JSON.stringify(string), string); + await testCopy(hud, obj.toSource(), JSON.stringify(obj, null, " ")); + + const outerHTML = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [id], + function(elementId) { + return content.document.getElementById(elementId).outerHTML; + } + ); + await testCopy(hud, `$("#${id}")`, outerHTML); +}); + +function testCopy(hud, stringToCopy, expectedResult) { + return waitForClipboardPromise(() => { + info(`Attempting to copy: "${stringToCopy}"`); + const command = `copy(${stringToCopy})`; + info(`Executing command: "${command}"`); + execute(hud, command); + }, expectedResult); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_ctrl_a_select_all.js b/devtools/client/webconsole/test/browser/browser_jsterm_ctrl_a_select_all.js new file mode 100644 index 0000000000..f54338b6ac --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_ctrl_a_select_all.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Map Control + A to Select All, In the web console input + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,Test console select all"; + +add_task(async function() { + // The TabContextMenu initializes its strings only on a focus or mouseover event. + // Calls focus event on the TabContextMenu early in the test. + gBrowser.selectedTab.focus(); + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + + setInputValue(hud, "Ignore These Four Words"); + + // Test select all with (cmd|control) + a. + EventUtils.synthesizeKey("a", { accelKey: true }); + + const inputLength = getSelectionTextLength(jsterm); + is(inputLength, getInputValue(hud).length, "Select all of input"); + + // (cmd|control) + e cannot be disabled on Linux so skip this section on that OS. + if (Services.appinfo.OS !== "Linux") { + // Test do nothing on Control + E. + setInputValue(hud, "Ignore These Four Words"); + setCursorAtStart(jsterm); + EventUtils.synthesizeKey("e", { accelKey: true }); + checkSelectionStart( + jsterm, + 0, + "control|cmd + e does not move to end of input" + ); + } +}); + +function getSelectionTextLength(jsterm) { + return jsterm.editor.getSelection().length; +} + +function setCursorAtStart(jsterm) { + jsterm.editor.setCursor({ line: 0, ch: 0 }); +} + +function checkSelectionStart(jsterm, expectedCursorIndex, assertionInfo) { + const [selection] = jsterm.editor.codeMirror.listSelections(); + const { head } = selection; + is(head.ch, expectedCursorIndex, assertionInfo); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_ctrl_key_nav.js b/devtools/client/webconsole/test/browser/browser_jsterm_ctrl_key_nav.js new file mode 100644 index 0000000000..46de5fd0f9 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_ctrl_key_nav.js @@ -0,0 +1,335 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test navigation of webconsole contents via ctrl-a, ctrl-e, ctrl-p, ctrl-n +// see https://bugzilla.mozilla.org/show_bug.cgi?id=804845 +// +// The shortcuts tested here have platform limitations: +// - ctrl-e does not work on windows, +// - ctrl-a, ctrl-p and ctrl-n only work on OSX +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf-8,Web Console test for " + + "bug 804845 and bug 619598"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + ok(!getInputValue(hud), "input is empty"); + checkInputCursorPosition(hud, 0, "Cursor is at the start of the input"); + + testSingleLineInputNavNoHistory(hud); + testMultiLineInputNavNoHistory(hud); + await testNavWithHistory(hud); +}); + +function testSingleLineInputNavNoHistory(hud) { + const checkInput = (expected, assertionInfo) => + checkInputValueAndCursorPosition(hud, expected, assertionInfo); + + // Single char input + EventUtils.sendString("1"); + checkInput("1|", "caret location after single char input"); + + // nav to start/end with ctrl-a and ctrl-e; + synthesizeLineStartKey(); + checkInput("|1", "caret location after single char input and ctrl-a"); + + synthesizeLineEndKey(); + checkInput("1|", "caret location after single char input and ctrl-e"); + + // Second char input + EventUtils.sendString("2"); + checkInput("12|", "caret location after second char input"); + + // nav to start/end with up/down keys; verify behaviour using ctrl-p/ctrl-n + EventUtils.synthesizeKey("KEY_ArrowUp"); + checkInput("|12", "caret location after two char input and KEY_ArrowUp"); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + checkInput("12|", "caret location after two char input and KEY_ArrowDown"); + + synthesizeLineStartKey(); + checkInput("|12", "move caret to beginning of 2 char input with ctrl-a"); + + synthesizeLineStartKey(); + checkInput("|12", "no change of caret location on repeat ctrl-a"); + + synthesizeLineUpKey(); + checkInput( + "|12", + "no change of caret location on ctrl-p from beginning of line" + ); + + synthesizeLineEndKey(); + checkInput("12|", "move caret to end of 2 char input with ctrl-e"); + + synthesizeLineEndKey(); + checkInput("12|", "no change of caret location on repeat ctrl-e"); + + synthesizeLineDownKey(); + checkInput("12|", "no change of caret location on ctrl-n from end of line"); + + synthesizeLineUpKey(); + checkInput("|12", "ctrl-p moves to start of line"); + + synthesizeLineDownKey(); + checkInput("12|", "ctrl-n moves to end of line"); +} + +function testMultiLineInputNavNoHistory(hud) { + const checkInput = (expected, assertionInfo) => + checkInputValueAndCursorPosition(hud, expected, assertionInfo); + + const lineValues = ["one", "2", "something longer", "", "", "three!"]; + setInputValue(hud, ""); + // simulate shift-return + for (const lineValue of lineValues) { + setInputValue(hud, getInputValue(hud) + lineValue); + EventUtils.synthesizeKey("KEY_Enter", { shiftKey: true }); + } + + checkInput( + `one +2 +something longer + + +three! +|`, + "caret at end of multiline input" + ); + + // Ok, test navigating within the multi-line string! + EventUtils.synthesizeKey("KEY_ArrowUp"); + checkInput( + `one +2 +something longer + + +|three! +`, + "up arrow from end of multiline" + ); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + checkInput( + `one +2 +something longer + + +three! +|`, + "down arrow from within multiline" + ); + + // navigate up through input lines + synthesizeLineUpKey(); + checkInput( + `one +2 +something longer + + +|three! +`, + "ctrl-p from end of multiline" + ); + + for (let i = 0; i < 5; i++) { + synthesizeLineUpKey(); + } + + checkInput( + `|one +2 +something longer + + +three! +`, + "reached start of input" + ); + + synthesizeLineUpKey(); + checkInput( + `|one +2 +something longer + + +three! +`, + "no change to multiline input on ctrl-p from beginning of multiline" + ); + + // navigate to end of first line + synthesizeLineEndKey(); + checkInput( + `one| +2 +something longer + + +three! +`, + "ctrl-e into multiline input" + ); + + synthesizeLineEndKey(); + checkInput( + `one| +2 +something longer + + +three! +`, + "repeat ctrl-e doesn't change caret position in multiline input" + ); + + synthesizeLineDownKey(); + synthesizeLineStartKey(); + checkInput( + `one +|2 +something longer + + +three! +` + ); + + synthesizeLineEndKey(); + synthesizeLineDownKey(); + synthesizeLineStartKey(); + checkInput( + `one +2 +|something longer + + +three! +` + ); +} + +async function testNavWithHistory(hud) { + const checkInput = (expected, assertionInfo) => + checkInputValueAndCursorPosition(hud, expected, assertionInfo); + + // NOTE: Tests does NOT currently define behaviour for ctrl-p/ctrl-n with + // caret placed _within_ single line input + const values = [ + "single line input", + "a longer single-line input to check caret repositioning", + "multi-line\ninput\nhere", + ]; + + // submit to history + for (const value of values) { + const onResult = waitForMessage(hud, "", ".result"); + setInputValue(hud, value); + EventUtils.synthesizeKey("KEY_Enter"); + await onResult; + } + + checkInput("|", "caret location at start of empty line"); + + synthesizeLineUpKey(); + checkInput( + "multi-line\ninput\nhere|", + "caret location at end of last history input" + ); + + synthesizeLineStartKey(); + checkInput( + "multi-line\ninput\n|here", + "caret location at beginning of last line of last history input" + ); + + synthesizeLineUpKey(); + checkInput( + "multi-line\n|input\nhere", + "caret location at beginning of second line of last history input" + ); + + synthesizeLineUpKey(); + checkInput( + "|multi-line\ninput\nhere", + "caret location at beginning of first line of last history input" + ); + + synthesizeLineUpKey(); + checkInput( + "a longer single-line input to check caret repositioning|", + "caret location at the end of second history input" + ); + + synthesizeLineUpKey(); + checkInput( + "single line input|", + "caret location at the end of first history input" + ); + + synthesizeLineUpKey(); + checkInput( + "|single line input", + "ctrl-p at beginning of history moves caret location to beginning of line" + ); + + synthesizeLineDownKey(); + checkInput( + "a longer single-line input to check caret repositioning|", + "caret location at the end of second history input" + ); + + synthesizeLineDownKey(); + checkInput( + "multi-line\ninput\nhere|", + "caret location at end of last history input" + ); + + synthesizeLineDownKey(); + checkInput("|", "ctrl-n at end of history updates to empty input"); + + // Simulate editing multi-line + const inputValue = "one\nlinebreak"; + setInputValue(hud, inputValue); + checkInput("one\nlinebreak|"); + + // Attempt nav within input + synthesizeLineUpKey(); + checkInput( + "one|\nlinebreak", + "ctrl-p from end of multi-line does not trigger history" + ); + + synthesizeLineStartKey(); + checkInput("|one\nlinebreak"); + + synthesizeLineUpKey(); + checkInput( + "multi-line\ninput\nhere|", + "ctrl-p from start of multi-line triggers history" + ); +} + +function synthesizeLineStartKey() { + EventUtils.synthesizeKey("a", { ctrlKey: true }); +} + +function synthesizeLineEndKey() { + EventUtils.synthesizeKey("e", { ctrlKey: true }); +} + +function synthesizeLineUpKey() { + EventUtils.synthesizeKey("p", { ctrlKey: true }); +} + +function synthesizeLineDownKey() { + EventUtils.synthesizeKey("n", { ctrlKey: true }); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_document_no_xray.js b/devtools/client/webconsole/test/browser/browser_jsterm_document_no_xray.js new file mode 100644 index 0000000000..44c7f08e0e --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_document_no_xray.js @@ -0,0 +1,19 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = "data:text/html,Test evaluating document"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + // check for occurrences of Object XRayWrapper, bug 604430 + const { node } = await executeAndWaitForMessage( + hud, + "document", + "HTMLDocument", + ".result" + ); + is(node.textContent.includes("xray"), false, "document - no XrayWrapper"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_eager_evaluation.js b/devtools/client/webconsole/test/browser/browser_jsterm_eager_evaluation.js new file mode 100644 index 0000000000..3159750437 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_eager_evaluation.js @@ -0,0 +1,351 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf-8, +<script> +let x = 3, y = 4; +function zzyzx() { + x = 10; +} +function zzyzx2() { + x = 10; +} +var obj = {propA: "A", propB: "B"}; +var array = [1, 2, 3]; +</script> +`; + +const EAGER_EVALUATION_PREF = "devtools.webconsole.input.eagerEvaluation"; + +// Basic testing of eager evaluation functionality. Expressions which can be +// eagerly evaluated should show their results, and expressions with side +// effects should not perform those side effects. +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + // Do an evaluation to populate $_ + await executeAndWaitForMessage( + hud, + "'result: ' + (x + y)", + "result: 7", + ".result" + ); + + setInputValue(hud, "x + y"); + await waitForEagerEvaluationResult(hud, "7"); + + setInputValue(hud, "x + y + undefined"); + await waitForEagerEvaluationResult(hud, "NaN"); + + setInputValue(hud, "1 - 1"); + await waitForEagerEvaluationResult(hud, "0"); + + setInputValue(hud, "!true"); + await waitForEagerEvaluationResult(hud, "false"); + + setInputValue(hud, `"ab".slice(0, 0)`); + await waitForEagerEvaluationResult(hud, `""`); + + setInputValue(hud, `JSON.parse("null")`); + await waitForEagerEvaluationResult(hud, "null"); + + setInputValue(hud, "-x / 0"); + await waitForEagerEvaluationResult(hud, "-Infinity"); + + setInputValue(hud, "x = 10"); + await waitForNoEagerEvaluationResult(hud); + + setInputValue(hud, "x + 1"); + await waitForEagerEvaluationResult(hud, "4"); + + setInputValue(hud, "zzyzx()"); + await waitForNoEagerEvaluationResult(hud); + + setInputValue(hud, "x + 2"); + await waitForEagerEvaluationResult(hud, "5"); + + setInputValue(hud, "x +"); + await waitForNoEagerEvaluationResult(hud); + + setInputValue(hud, "x + z"); + await waitForEagerEvaluationResult(hud, /ReferenceError/); + + setInputValue(hud, "var a = 5"); + await waitForNoEagerEvaluationResult(hud); + + setInputValue(hud, "x + a"); + await waitForEagerEvaluationResult(hud, /ReferenceError/); + + setInputValue(hud, '"foobar".slice(1, 5)'); + await waitForEagerEvaluationResult(hud, '"ooba"'); + + setInputValue(hud, '"foobar".toString()'); + await waitForEagerEvaluationResult(hud, '"foobar"'); + + setInputValue(hud, "(new Array()).push(3)"); + await waitForNoEagerEvaluationResult(hud); + + setInputValue(hud, "(new Uint32Array([1,2,3])).includes(2)"); + await waitForEagerEvaluationResult(hud, "true"); + + setInputValue(hud, "Math.round(3.2)"); + await waitForEagerEvaluationResult(hud, "3"); + + info("Check that $_ wasn't polluted by eager evaluations"); + setInputValue(hud, "$_"); + await waitForEagerEvaluationResult(hud, `"result: 7"`); + + setInputValue(hud, "'> ' + $_"); + await waitForEagerEvaluationResult(hud, `"> result: 7"`); + + info("Switch to editor mode"); + await toggleLayout(hud); + await waitForEagerEvaluationResult(hud, `"> result: 7"`); + ok(true, "eager evaluation is still displayed in editor mode"); + + setInputValue(hud, "4 + 7"); + await waitForEagerEvaluationResult(hud, "11"); + + setInputValue(hud, "typeof new Proxy({}, {})"); + await waitForEagerEvaluationResult(hud, `"object"`); + + setInputValue(hud, "typeof Proxy.revocable({}, {}).revoke"); + await waitForEagerEvaluationResult(hud, `"function"`); + + setInputValue(hud, "Reflect.apply(() => 1, null, [])"); + await waitForEagerEvaluationResult(hud, "1"); + setInputValue( + hud, + `Reflect.apply(() => { + globalThis.sideEffect = true; + return 2; + }, null, [])` + ); + await waitForNoEagerEvaluationResult(hud); + + setInputValue(hud, "Reflect.construct(Array, []).length"); + await waitForEagerEvaluationResult(hud, "0"); + setInputValue( + hud, + `Reflect.construct(function() { + globalThis.sideEffect = true; + }, [])` + ); + await waitForNoEagerEvaluationResult(hud); + + setInputValue(hud, "Reflect.defineProperty({}, 'a', {value: 1})"); + await waitForNoEagerEvaluationResult(hud); + + setInputValue(hud, "Reflect.deleteProperty({a: 1}, 'a')"); + await waitForNoEagerEvaluationResult(hud); + + setInputValue(hud, "Reflect.get({a: 1}, 'a')"); + await waitForEagerEvaluationResult(hud, "1"); + setInputValue(hud, "Reflect.get({get a(){return 2}, 'a')"); + await waitForNoEagerEvaluationResult(hud); + + setInputValue(hud, "Reflect.getOwnPropertyDescriptor({a: 1}, 'a').value"); + await waitForEagerEvaluationResult(hud, "1"); + setInputValue( + hud, + `Reflect.getOwnPropertyDescriptor( + new Proxy({ a: 2 }, { getOwnPropertyDescriptor() { + globalThis.sideEffect = true; + return { value: 2 }; + }}), + "a" + )` + ); + await waitForNoEagerEvaluationResult(hud); + + setInputValue(hud, "Reflect.getPrototypeOf({}) === Object.prototype"); + await waitForEagerEvaluationResult(hud, "true"); + setInputValue( + hud, + `Reflect.getPrototypeOf( + new Proxy({}, { getPrototypeOf() { + globalThis.sideEffect = true; + return null; + }}) + )` + ); + await waitForNoEagerEvaluationResult(hud); + + setInputValue(hud, "Reflect.has({a: 1}, 'a')"); + await waitForEagerEvaluationResult(hud, "true"); + setInputValue( + hud, + `Reflect.has( + new Proxy({ a: 2 }, { has() { + globalThis.sideEffect = true; + return true; + }}), "a" + )` + ); + await waitForNoEagerEvaluationResult(hud); + + setInputValue(hud, "Reflect.isExtensible({})"); + await waitForEagerEvaluationResult(hud, "true"); + setInputValue( + hud, + `Reflect.isExtensible( + new Proxy({}, { isExtensible() { + globalThis.sideEffect = true; + return true; + }}) + )` + ); + await waitForNoEagerEvaluationResult(hud); + + setInputValue(hud, "Reflect.ownKeys({a: 1})[0]"); + await waitForEagerEvaluationResult(hud, `"a"`); + setInputValue( + hud, + `Reflect.ownKeys( + new Proxy({}, { ownKeys() { + globalThis.sideEffect = true; + return ['a']; + }}) + )` + ); + await waitForNoEagerEvaluationResult(hud); + + setInputValue(hud, "Reflect.preventExtensions({})"); + await waitForNoEagerEvaluationResult(hud); + + setInputValue(hud, "Reflect.set({}, 'a', 1)"); + await waitForNoEagerEvaluationResult(hud); + + setInputValue(hud, "Reflect.setPrototypeOf({}, null)"); + await waitForNoEagerEvaluationResult(hud); + + setInputValue(hud, "[] instanceof Array"); + await waitForEagerEvaluationResult(hud, "true"); + + setInputValue(hud, "Int8Array.from({length: 1})[0]"); + await waitForEagerEvaluationResult(hud, "0"); + + setInputValue(hud, "Float64Array.of(1)[0]"); + await waitForEagerEvaluationResult(hud, "1"); + + setInputValue(hud, "array.fill()"); + await waitForNoEagerEvaluationResult(hud); + + setInputValue(hud, "array"); + await waitForEagerEvaluationResult(hud, "Array(3) [ 1, 2, 3 ]"); + + // go back to inline layout. + await toggleLayout(hud); +}); + +// Test that the currently selected autocomplete result is eagerly evaluated. +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + + const { autocompletePopup: popup } = jsterm; + + ok(!popup.isOpen, "popup is not open"); + let onPopupOpen = popup.once("popup-opened"); + EventUtils.sendString("zzy"); + await onPopupOpen; + + await waitForEagerEvaluationResult(hud, "function zzyzx()"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + await waitForEagerEvaluationResult(hud, "function zzyzx2()"); + + // works when the input isn't properly cased but matches an autocomplete item + setInputValue(hud, "o"); + onPopupOpen = popup.once("popup-opened"); + EventUtils.sendString("B"); + await waitForEagerEvaluationResult(hud, `Object { propA: "A", propB: "B" }`); + + // works when doing element access without quotes + setInputValue(hud, "obj[p"); + onPopupOpen = popup.once("popup-opened"); + EventUtils.sendString("RoP"); + await waitForEagerEvaluationResult(hud, `"A"`); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + await waitForEagerEvaluationResult(hud, `"B"`); + + // closing the autocomplete popup updates the eager evaluation result + let onPopupClose = popup.once("popup-closed"); + EventUtils.synthesizeKey("KEY_Escape"); + await onPopupClose; + await waitForNoEagerEvaluationResult(hud); + + info( + "Check that closing the popup by adding a space will update the instant eval result" + ); + await setInputValueForAutocompletion(hud, "x"); + await waitForEagerEvaluationResult(hud, "3"); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + // Navigates to the XMLDocument item in the popup + await waitForEagerEvaluationResult(hud, `function ()`); + + onPopupClose = popup.once("popup-closed"); + EventUtils.sendString(" "); + await waitForEagerEvaluationResult(hud, `3`); +}); + +// Test that the setting works as expected. +add_task(async function() { + // start with the pref off. + await pushPref(EAGER_EVALUATION_PREF, false); + const hud = await openNewTabAndConsole(TEST_URI); + + info("Check that the setting is disabled"); + checkConsoleSettingState( + hud, + ".webconsole-console-settings-menu-item-eager-evaluation", + false + ); + + // Wait for the autocomplete popup to be displayed so we know the eager evaluation could + // have occured. + const onPopupOpen = hud.jsterm.autocompletePopup.once("popup-opened"); + await setInputValueForAutocompletion(hud, "x + y"); + await onPopupOpen; + + is( + getEagerEvaluationElement(hud), + null, + "There's no eager evaluation element" + ); + hud.jsterm.autocompletePopup.hidePopup(); + + info("Turn on the eager evaluation"); + toggleConsoleSetting( + hud, + ".webconsole-console-settings-menu-item-eager-evaluation" + ); + await waitFor(() => getEagerEvaluationElement(hud)); + ok(true, "The eager evaluation element is now displayed"); + is( + Services.prefs.getBoolPref(EAGER_EVALUATION_PREF), + true, + "Pref was changed" + ); + + setInputValue(hud, "1 + 2"); + await waitForEagerEvaluationResult(hud, "3"); + ok(true, "Eager evaluation result is displayed"); + + info("Turn off the eager evaluation"); + toggleConsoleSetting( + hud, + ".webconsole-console-settings-menu-item-eager-evaluation" + ); + await waitFor(() => !getEagerEvaluationElement(hud)); + is( + Services.prefs.getBoolPref(EAGER_EVALUATION_PREF), + false, + "Pref was changed" + ); + ok(true, "Eager evaluation element is no longer displayed"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_eager_evaluation_element_highlight.js b/devtools/client/webconsole/test/browser/browser_jsterm_eager_evaluation_element_highlight.js new file mode 100644 index 0000000000..e4abc3c17d --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_eager_evaluation_element_highlight.js @@ -0,0 +1,75 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf8, +<h1 class="title">hello</h1> +<div id="mydiv">mydivtext</div> +<script> + x = Object.create(null, Object.getOwnPropertyDescriptors({ + a: document.querySelector("h1"), + b: document.querySelector("div"), + c: document.createElement("hr") + })); +</script>`; + +// Test that when the eager evaluation result is an element, it gets highlighted. +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm, toolbox } = hud; + const { autocompletePopup } = jsterm; + + const testActor = await getTestActor(toolbox); + const highlighter = toolbox.getHighlighter(); + let onHighlighterShown; + let onHighlighterHidden; + let data; + + ok(!autocompletePopup.isOpen, "popup is not open"); + const onPopupOpen = autocompletePopup.once("popup-opened"); + onHighlighterShown = highlighter.waitForHighlighterShown(); + EventUtils.sendString("x."); + await onPopupOpen; + + await waitForEagerEvaluationResult(hud, `<h1 class="title">`); + data = await onHighlighterShown; + is(data.nodeFront.displayName, "h1", "The correct node was highlighted"); + isVisible = await testActor.isHighlighting(); + is(isVisible, true, "Highlighter is displayed"); + + onHighlighterShown = highlighter.waitForHighlighterShown(); + EventUtils.synthesizeKey("KEY_ArrowDown"); + await waitForEagerEvaluationResult(hud, `<div id="mydiv">`); + data = await onHighlighterShown; + is(data.nodeFront.displayName, "div", "The correct node was highlighted"); + isVisible = await testActor.isHighlighting(); + is(isVisible, true, "Highlighter is displayed"); + + onHighlighterHidden = highlighter.waitForHighlighterHidden(); + EventUtils.synthesizeKey("KEY_ArrowDown"); + await waitForEagerEvaluationResult(hud, `<hr>`); + await onHighlighterHidden; + ok(true, "The highlighter isn't displayed on a non-connected element"); + + info("Test that text nodes are highlighted"); + onHighlighterShown = highlighter.waitForHighlighterShown(); + EventUtils.sendString("b.firstChild"); + await waitForEagerEvaluationResult(hud, `#text "mydivtext"`); + data = await onHighlighterShown; + is( + data.nodeFront.displayName, + "#text", + "The correct text node was highlighted" + ); + isVisible = await testActor.isHighlighting(); + is(isVisible, true, "Highlighter is displayed"); + + onHighlighterHidden = highlighter.waitForHighlighterHidden(); + EventUtils.synthesizeKey("KEY_Enter"); + await waitFor(() => findMessage(hud, `#text "mydivtext"`, ".result")); + await waitForNoEagerEvaluationResult(hud); + isVisible = await testActor.isHighlighting(); + is(isVisible, false, "Highlighter is closed after evaluating the expression"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_eager_evaluation_in_debugger_stackframe.js b/devtools/client/webconsole/test/browser/browser_jsterm_eager_evaluation_in_debugger_stackframe.js new file mode 100644 index 0000000000..afd7e5d112 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_eager_evaluation_in_debugger_stackframe.js @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Test that eager evaluation works as expected when paused in the debugger. + +const TEST_URI = `data:text/html;charset=utf-8, +<script> +var x = "global"; + +function pauseInDebugger(param) { + let x = "local"; + debugger; +} + +</script> +`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + const target = await TargetFactory.forTab(gBrowser.selectedTab); + const toolbox = gDevTools.getToolbox(target); + + setInputValue(hud, "x"); + await waitForEagerEvaluationResult(hud, `"global"`); + + info("Open Debugger"); + await openDebugger(); + const dbg = createDebuggerContext(toolbox); + + info("Pause in Debugger"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.pauseInDebugger("myParam"); + }); + await pauseDebugger(dbg); + + info("Opening Console"); + await toolbox.selectTool("webconsole"); + + info("Check that the parameter is eagerly evaluated as expected"); + setInputValue(hud, "param"); + await waitForEagerEvaluationResult(hud, `"myParam"`); + + setInputValue(hud, "x"); + await waitForEagerEvaluationResult(hud, `"local"`); + + await resume(dbg); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_eager_evaluation_on_webextension_target.js b/devtools/client/webconsole/test/browser/browser_jsterm_eager_evaluation_on_webextension_target.js new file mode 100644 index 0000000000..c6945afefe --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_eager_evaluation_on_webextension_target.js @@ -0,0 +1,83 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { DevToolsClient } = require("devtools/client/devtools-client"); +const { DevToolsServer } = require("devtools/server/devtools-server"); + +add_task(async function test_webextension_target_allowSource_on_eager_eval() { + const extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + background: function() { + this.browser.test.sendMessage("bg-ready"); + }, + }); + + await extension.startup(); + await extension.awaitMessage("bg-ready"); + + // Init debugger server. + DevToolsServer.init(); + DevToolsServer.registerAllActors(); + + // Create and connect a debugger client. + const transport = DevToolsServer.connectPipe(); + const client = new DevToolsClient(transport); + await client.connect(); + + // Get AddonTarget. + const addonDescriptor = await client.mainRoot.getAddon({ id: extension.id }); + ok(addonDescriptor, "webextension addon description has been found"); + const addonTarget = await addonDescriptor.getTarget(); + ok(addonTarget, "webextension target addon actor has been found"); + + // Open a toolbox window for the addon target. + const toolbox = await gDevTools.showToolbox( + addonTarget, + "webconsole", + Toolbox.HostType.WINDOW + ); + + await toolbox.selectTool("webconsole"); + + info("Start listening for console messages"); + SpecialPowers.registerConsoleListener(msg => { + if ( + msg.message && + msg.message.includes("Unexpected invalid url: debugger eager eval code") + ) { + ok( + false, + "webextension targetActor._allowSource should not log an error on debugger eager eval code" + ); + } + }); + registerCleanupFunction(() => { + SpecialPowers.postConsoleSentinel(); + }); + + const hud = toolbox.getPanel("webconsole").hud; + setInputValue(hud, `browser`); + + info("Wait for eager eval element"); + await TestUtils.waitForCondition(() => getEagerEvaluationElement(hud)); + + // The following step will force Firefox to list the source actors, one of those + // source actors is going to be the one related to the js code evaluated by the + // eager evaluation and it does make sure that WebExtensionTargetPrototype._allowSource + // is going to be called with the source actor with url "debugger eager eval code". + info("Select the debugger panel to force webextension actor to list sources"); + await toolbox.selectTool("jsdebugger"); + + // Wait for a bit so that the error message would have the time to be logged + // (and fail if the issue does regress again). + await wait(2000); + + await toolbox.destroy(); + await addonTarget.destroy(); + await client.close(); + + await extension.unload(); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_eager_evaluation_warnings.js b/devtools/client/webconsole/test/browser/browser_jsterm_eager_evaluation_warnings.js new file mode 100644 index 0000000000..ccc877ab41 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_eager_evaluation_warnings.js @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf-8,Test that eager evaluation can't log warnings in the output`; +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + setInputValue(hud, `document.getElementById("")`); + await waitForEagerEvaluationResult(hud, "null"); + + info("Wait for a bit so a warning message could be displayed"); + await wait(2000); + is( + findMessage(hud, "getElementById", ".warn"), + undefined, + "The eager evaluation did not triggered a warning message" + ); + + info("Sanity check for the warning message when the expression is evaluated"); + EventUtils.synthesizeKey("KEY_Enter"); + await waitFor(() => findMessage(hud, "getElementById", ".warn")); + ok(true, "Evaluation of the expression does trigger the warning message"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_editor.js b/devtools/client/webconsole/test/browser/browser_jsterm_editor.js new file mode 100644 index 0000000000..4ea70a852f --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_editor.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check that the editor is displayed as expected. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,<p>Test editor"; + +add_task(async function() { + await pushPref("devtools.webconsole.input.editor", false); + + const tab = await addTab(TEST_URI); + let hud = await openConsole(tab); + + info("Test that the editor mode is disabled when the pref is set to false"); + is( + isEditorModeEnabled(hud), + false, + "Editor is disabled when pref is set to false" + ); + const openEditorButton = getInlineOpenEditorButton(hud); + ok(openEditorButton, "button is rendered in the inline input"); + const rect = openEditorButton.getBoundingClientRect(); + ok(rect.width > 0 && rect.height > 0, "Button is visible"); + + await closeConsole(); + + info( + "Test that wrapper does have the jsterm-editor class when editor is enabled" + ); + await pushPref("devtools.webconsole.input.editor", true); + hud = await openConsole(tab); + is( + isEditorModeEnabled(hud), + true, + "Editor is enabled when pref is set to true" + ); + is(getInlineOpenEditorButton(hud), null, "Button is hidden in editor mode"); + + await toggleLayout(hud); + getInlineOpenEditorButton(hud).click(); + await waitFor(() => isEditorModeEnabled(hud)); + ok(true, "Editor is open when clicking on the button"); +}); + +function getInlineOpenEditorButton(hud) { + return hud.ui.outputNode.querySelector(".webconsole-input-openEditorButton"); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_editor_code_folding.js b/devtools/client/webconsole/test/browser/browser_jsterm_editor_code_folding.js new file mode 100644 index 0000000000..b3de92da5b --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_editor_code_folding.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests for code folding appears in editor mode, does not appear in inline mode, +// and that folded code does not remain folded when switched to inline mode. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=1581641 + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,Test JsTerm editor code folding"; + +add_task(async function() { + await pushPref("devtools.webconsole.input.editor", true); + + const hud = await openNewTabAndConsole(TEST_URI); + + info("Check that code folding gutter & arrow are rendered in editor mode"); + + const multilineExpression = `function() { + // Silence is golden + }`; + await setInputValue(hud, multilineExpression); + + ok( + await waitFor(() => getFoldArrowOpenElement(hud)), + "code folding gutter was rendered in editor mode" + ); + + const foldingArrow = getFoldArrowOpenElement(hud); + ok(foldingArrow, "code folding arrow was rendered in code folding gutter"); + is(getCodeLines(hud).length, 3, "There are 3 lines displayed"); + + info("Check that code folds when gutter marker clicked"); + EventUtils.synthesizeMouseAtCenter( + foldingArrow, + {}, + foldingArrow.ownerDocument.defaultView + ); + await waitFor(() => getCodeLines(hud).length === 1); + ok(true, "The code was folded, there's only one line displayed now"); + + info("Check that folded code is expanded when rendered inline"); + + await toggleLayout(hud); + + is( + getCodeLines(hud).length, + 3, + "folded code is expended when rendered in inline" + ); + + info( + "Check that code folding gutter is hidden when we switch to inline mode" + ); + ok( + !getFoldGutterElement(hud), + "code folding gutter is hidden when we switsch to inline mode" + ); +}); + +function getCodeLines(hud) { + return hud.ui.outputNode.querySelectorAll( + ".CodeMirror-code pre.CodeMirror-line" + ); +} + +function getFoldGutterElement(hud) { + return hud.ui.outputNode.querySelector(".CodeMirror-foldgutter"); +} + +function getFoldArrowOpenElement(hud) { + return hud.ui.outputNode.querySelector(".CodeMirror-foldgutter-open"); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_editor_disabled_history_nav_with_keyboard.js b/devtools/client/webconsole/test/browser/browser_jsterm_editor_disabled_history_nav_with_keyboard.js new file mode 100644 index 0000000000..3c11f0e463 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_editor_disabled_history_nav_with_keyboard.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that user input is not cleared when 'devtools.webconsole.input.editor' +// is set to true. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=1519313 + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf-8,Web Console test for bug 1519313"; + +add_task(async function() { + await pushPref("devtools.webconsole.input.editor", true); + const hud = await openNewTabAndConsole(TEST_URI); + + const testExpressions = [ + "`Mozilla 😍 Firefox`", + "`Firefox Devtools are awesome`", + "`2 + 2 = 5?`", + "`I'm running out of ideas...`", + "`🌑 🌒 🌓 🌔 🌕 🌖 🌗 🌘`", + "`🌪🌪 🐄 🐄 🏠 🐄 🐄 ⛈`", + "`🌈 🌈 🌈 🦄 🦄 🌈 🌈 🌈`", + "`Time to perform the test 🤪`", + ]; + + info("Executing a bunch of non-sense JS expression"); + for (const expression of testExpressions) { + // Wait until we get the result of the command. + await executeAndWaitForMessage(hud, expression, "", ".result"); + ok(true, `JS expression executed successfully: ${expression} `); + } + + info("Test that pressing ArrowUp does nothing"); + EventUtils.synthesizeKey("KEY_ArrowUp"); + is(getInputValue(hud), "", "Good! There is no text in the JS Editor"); + + info("Test that pressing multiple times ArrowUp does nothing"); + EventUtils.synthesizeKey("KEY_ArrowUp"); + EventUtils.synthesizeKey("KEY_ArrowUp"); + EventUtils.synthesizeKey("KEY_ArrowUp"); + is(getInputValue(hud), "", "Good! Again, there is no text in the JS Editor"); + + info( + "Move somewhere in the middle of the history using the navigation buttons and test again" + ); + const prevHistoryButton = getEditorToolbar(hud).querySelector( + ".webconsole-editor-toolbar-history-prevExpressionButton" + ); + info("Pressing 3 times the previous history button"); + prevHistoryButton.click(); + prevHistoryButton.click(); + prevHistoryButton.click(); + const jsExpression = testExpressions[testExpressions.length - 3]; + is( + getInputValue(hud), + jsExpression, + "Sweet! We are in the right position of the history" + ); + + info("Test again that pressing ArrowUp does nothing"); + EventUtils.synthesizeKey("KEY_ArrowUp"); + is( + getInputValue(hud), + jsExpression, + "OMG! We have some cows in the JS Editor!" + ); + + info("Test again that pressing multiple times ArrowUp does nothing"); + EventUtils.synthesizeKey("KEY_ArrowUp"); + EventUtils.synthesizeKey("KEY_ArrowUp"); + EventUtils.synthesizeKey("KEY_ArrowUp"); + is( + getInputValue(hud), + jsExpression, + "Awesome! The cows are still there in the JS Editor!" + ); + + info("Test that pressing ArrowDown does nothing"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + is( + getInputValue(hud), + jsExpression, + "Super! We still have the cows in the JS Editor!" + ); + + info("Test that pressing multiple times ArrowDown does nothing"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + is(getInputValue(hud), jsExpression, "And the cows are still there..."); +}); + +function getEditorToolbar(hud) { + return hud.ui.outputNode.querySelector(".webconsole-editor-toolbar"); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_editor_enter.js b/devtools/client/webconsole/test/browser/browser_jsterm_editor_enter.js new file mode 100644 index 0000000000..6583c22c80 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_editor_enter.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that hitting Ctrl (or Cmd on OSX) + Enter does execute the input +// and Enter does not when in editor mode. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=1519314 + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf-8,Web Console test for bug 1519314"; + +add_task(async function() { + await pushPref("devtools.webconsole.input.editor", true); + await performEditorEnabledTests(); +}); + +add_task(async function() { + await pushPref("devtools.webconsole.input.editor", false); + await performEditorDisabledTests(); +}); + +const first_expression = `x = 10`; +const second_expression = `x + 1`; + +/** + * Simulates typing of the two expressions above in the console + * for the two test cases below. + */ +function simulateConsoleInput() { + EventUtils.sendString(first_expression); + EventUtils.sendKey("return"); + EventUtils.sendString(second_expression); +} + +async function performEditorEnabledTests() { + const hud = await openNewTabAndConsole(TEST_URI); + + simulateConsoleInput(); + + is( + getInputValue(hud), + `${first_expression}\n${second_expression}`, + "text input after pressing the return key is present" + ); + + const { visibleMessages } = hud.ui.wrapper.getStore().getState().messages; + is( + visibleMessages.length, + 0, + "input expressions should not have been executed" + ); + + let onMessage = waitForMessage(hud, "11", ".result"); + EventUtils.synthesizeKey("KEY_Enter", { + [Services.appinfo.OS === "Darwin" ? "metaKey" : "ctrlKey"]: true, + }); + await onMessage; + ok(true, "Input was executed on Ctrl/Cmd + Enter"); + + setInputValue(hud, "function x() {"); + onMessage = waitForMessage(hud, "SyntaxError"); + EventUtils.synthesizeKey("KEY_Enter", { + [Services.appinfo.OS === "Darwin" ? "metaKey" : "ctrlKey"]: true, + }); + await onMessage; + ok(true, "The expression was evaluated, even if it wasn't well-formed"); +} + +async function performEditorDisabledTests() { + const hud = await openNewTabAndConsole(TEST_URI); + + simulateConsoleInput(); + // execute the 2nd expression which should have been entered but not executed + EventUtils.sendKey("return"); + + let msg = await waitFor(() => findMessage(hud, "10")); + ok(msg, "found evaluation result of 1st expression"); + + msg = await waitFor(() => findMessage(hud, "11")); + ok(msg, "found evaluation result of 2nd expression"); + + is(getInputValue(hud), "", "input line is cleared after execution"); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_editor_execute.js b/devtools/client/webconsole/test/browser/browser_jsterm_editor_execute.js new file mode 100644 index 0000000000..61e2ab93b4 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_editor_execute.js @@ -0,0 +1,21 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that user input is not cleared when 'devtools.webconsole.input.editor' +// is set to true. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=1519313 + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf-8,Web Console test for bug 1519313"; + +add_task(async function() { + await pushPref("devtools.webconsole.input.editor", true); + const hud = await openNewTabAndConsole(TEST_URI); + + const expression = `x = 10`; + setInputValue(hud, expression); + await executeAndWaitForMessage(hud, undefined, "", ".result"); + is(getInputValue(hud), expression, "input line is not cleared after submit"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_editor_execute_selection.js b/devtools/client/webconsole/test/browser/browser_jsterm_editor_execute_selection.js new file mode 100644 index 0000000000..70694353a0 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_editor_execute_selection.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the user can execute only the code that is selected in the input, in editor +// mode. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=1576563 + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf-8,Web Console test for executing input selection"; + +add_task(async function() { + await pushPref("devtools.webconsole.input.editor", true); + const hud = await openNewTabAndConsole(TEST_URI); + + const expression = `x = "first assignment";x; + x = "second assignment";x;`; + + info("Evaluate the whole expression"); + setInputValue(hud, expression); + + let onResultMessage = waitForMessage(hud, "second assignment", ".result"); + synthesizeKeyboardEvaluation(); + await onResultMessage; + ok(true, "The whole expression is evaluated when there's no selection"); + + info("Select the first line and evaluate"); + hud.ui.jsterm.editor.setSelection( + { line: 0, ch: 0 }, + { line: 0, ch: expression.split("\n")[0].length } + ); + onResultMessage = waitForMessage(hud, "first assignment", ".result"); + synthesizeKeyboardEvaluation(); + await onResultMessage; + ok(true, "Only the expression on the first line was evaluated"); + + info("Check that it also works when clicking on the Run button"); + onResultMessage = waitForMessage(hud, "first assignment", ".result"); + hud.ui.outputNode + .querySelector(".webconsole-editor-toolbar-executeButton") + .click(); + await onResultMessage; + ok( + true, + "Only the expression on the first line was evaluated when clicking the Run button" + ); + + info("Check that this is disabled in inline mode"); + await toggleLayout(hud); + hud.ui.jsterm.editor.setSelection( + { line: 0, ch: 0 }, + { line: 0, ch: expression.split("\n")[0].length } + ); + onResultMessage = waitForMessage(hud, "second assignment", ".result"); + synthesizeKeyboardEvaluation(); + await onResultMessage; + ok(true, "The whole expression was evaluated in inline mode"); +}); + +function synthesizeKeyboardEvaluation() { + EventUtils.synthesizeKey("KEY_Enter", { + [Services.appinfo.OS === "Darwin" ? "metaKey" : "ctrlKey"]: true, + }); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_editor_gutter.js b/devtools/client/webconsole/test/browser/browser_jsterm_editor_gutter.js new file mode 100644 index 0000000000..179dc9dd17 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_editor_gutter.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that CodeMirror's gutter in console input is displayed when +// 'devtools.webconsole.input.editor' is true. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=1519315 + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,Test JsTerm editor line gutters"; + +add_task(async function() { + await pushPref("devtools.webconsole.input.editor", true); + + const hud = await openNewTabAndConsole(TEST_URI); + + info("Check that the line numbers gutter is rendered when in editor layout"); + ok( + getLineNumbersGutterElement(hud), + "line numbers gutter is rendered on the input when in editor mode." + ); + + info( + "Check that the line numbers gutter is hidden we switch to the inline layout" + ); + await toggleLayout(hud); + ok( + !getLineNumbersGutterElement(hud), + "line numbers gutter is hidden on the input when in inline mode." + ); + + info( + "Check that the line numbers gutter is rendered again we switch back to editor" + ); + await toggleLayout(hud); + ok( + getLineNumbersGutterElement(hud), + "line numbers gutter is rendered again on the " + + " input when switching back to editor mode." + ); +}); + +function getLineNumbersGutterElement(hud) { + return hud.ui.outputNode.querySelector(".CodeMirror-linenumbers"); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_editor_onboarding.js b/devtools/client/webconsole/test/browser/browser_jsterm_editor_onboarding.js new file mode 100644 index 0000000000..b182fa44e5 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_editor_onboarding.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the onboarding UI is displayed when first displaying the editor mode, and +// that it can be permanentely dismissed. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=1558417 + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,Test onboarding UI"; +const EDITOR_UI_PREF = "devtools.webconsole.input.editor"; +const EDITOR_ONBOARDING_PREF = "devtools.webconsole.input.editorOnboarding"; + +add_task(async function() { + // Enable editor mode and force the onboarding pref to true so it's displayed. + await pushPref(EDITOR_UI_PREF, true); + await pushPref(EDITOR_ONBOARDING_PREF, true); + + let hud = await openNewTabAndConsole(TEST_URI); + + info("Check that the onboarding UI is displayed"); + const onboardingElement = getOnboardingEl(hud); + ok(onboardingElement, "The onboarding UI exists"); + + info("Check that the onboarding UI can be dismissed"); + const dismissButton = onboardingElement.querySelector( + ".editor-onboarding-dismiss-button" + ); + ok(dismissButton, "There's a dismiss button"); + dismissButton.click(); + + await waitFor(() => !getOnboardingEl(hud)); + ok(true, "The onboarding UI is hidden after clicking the dismiss button"); + + info("Check that the onboarding UI isn't displayed after a toolbox restart"); + await closeConsole(); + hud = await openConsole(); + is( + getOnboardingEl(hud), + null, + "The onboarding UI isn't displayed after a toolbox restart after being dismissed" + ); + + Services.prefs.clearUserPref(EDITOR_UI_PREF); + Services.prefs.clearUserPref(EDITOR_ONBOARDING_PREF); +}); + +function getOnboardingEl(hud) { + return hud.ui.outputNode.querySelector(".editor-onboarding"); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_editor_resize.js b/devtools/client/webconsole/test/browser/browser_jsterm_editor_resize.js new file mode 100644 index 0000000000..691702aafd --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_editor_resize.js @@ -0,0 +1,79 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the editor can be resized and that its width is persisted. + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf-8,Web Console test for editor resize"; + +add_task(async function() { + await pushPref("devtools.webconsole.input.editor", true); + await pushPref("devtools.webconsole.input.editorOnboarding", false); + + // Reset editorWidth pref so we have steady results when running multiple times. + await pushPref("devtools.webconsole.input.editorWidth", null); + + let hud = await openNewTabAndConsole(TEST_URI); + const getEditorEl = () => + hud.ui.outputNode.querySelector(".jsterm-input-container"); + const resizerEl = hud.ui.outputNode.querySelector(".editor-resizer"); + + const editorBoundingRect = getEditorEl().getBoundingClientRect(); + const delta = 100; + const originalWidth = editorBoundingRect.width; + const clientX = editorBoundingRect.right + delta; + await resize(resizerEl, clientX); + + const newWidth = Math.floor(originalWidth + delta); + is( + Math.floor(getEditorEl().getBoundingClientRect().width), + newWidth, + "The editor element was resized as expected" + ); + info("Close and re-open the console to check if editor width was persisted"); + await closeConsole(); + hud = await openConsole(); + + is( + Math.floor(getEditorEl().getBoundingClientRect().width), + newWidth, + "The editor element width was persisted" + ); + await toggleLayout(hud); + + ok(!getEditorEl().style.width, "The width isn't applied in in-line layout"); + + await toggleLayout(hud); + is( + getEditorEl().style.width, + `${newWidth}px`, + "The width is applied again when switching back to editor" + ); +}); + +async function resize(resizer, clientX) { + const doc = resizer.ownerDocument; + const win = doc.defaultView; + + info("Mouse down to start dragging"); + EventUtils.synthesizeMouseAtCenter( + resizer, + { button: 0, type: "mousedown" }, + win + ); + await waitFor(() => doc.querySelector(".dragging")); + + const event = new MouseEvent("mousemove", { clientX }); + resizer.dispatchEvent(event); + + info("Mouse up to stop resizing"); + EventUtils.synthesizeMouseAtCenter( + doc.body, + { button: 0, type: "mouseup" }, + win + ); + + await waitFor(() => !doc.querySelector(".dragging")); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_editor_reverse_search_button.js b/devtools/client/webconsole/test/browser/browser_jsterm_editor_reverse_search_button.js new file mode 100644 index 0000000000..a15ba71faf --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_editor_reverse_search_button.js @@ -0,0 +1,53 @@ +/* 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,Web Console test for bug 1567372"; + +add_task(async function() { + await pushPref("devtools.webconsole.input.editor", true); + + const hud = await openNewTabAndConsole(TEST_URI); + + info("Searching for `.webconsole-editor-toolbar`"); + const editorToolbar = hud.ui.outputNode.querySelector( + ".webconsole-editor-toolbar" + ); + + info("Searching for `.webconsole-editor-toolbar-reverseSearchButton`"); + const reverseSearchButton = editorToolbar.querySelector( + ".webconsole-editor-toolbar-reverseSearchButton" + ); + + const onReverseSearchUiOpen = waitFor( + () => getReverseSearchElement(hud) != null + ); + + info("Performing click on `.webconsole-editor-toolbar-reverseSearchButton`"); + reverseSearchButton.click(); + + await onReverseSearchUiOpen; + ok(true, "Reverse Search UI is open"); + + ok( + reverseSearchButton.classList.contains("checked"), + "Reverse Search Button is marked as checked" + ); + + const onReverseSearchUiClosed = waitFor( + () => getReverseSearchElement(hud) == null + ); + + info("Performing click on `.webconsole-editor-toolbar-reverseSearchButton`"); + reverseSearchButton.click(); + + await onReverseSearchUiClosed; + ok(true, "Reverse Search UI is closed"); + + ok( + !reverseSearchButton.classList.contains("checked"), + "Reverse Search Button is NOT marked as checked" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_editor_reverse_search_keyboard_navigation.js b/devtools/client/webconsole/test/browser/browser_jsterm_editor_reverse_search_keyboard_navigation.js new file mode 100644 index 0000000000..34f12500a8 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_editor_reverse_search_keyboard_navigation.js @@ -0,0 +1,123 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Ensure keyboard navigation works in editor mode and does +// not trigger reader mode (See 1682340). + +const TEST_URI = `http://example.com/browser/toolkit/components/reader/test/readerModeArticle.html`; +const isMacOS = AppConstants.platform === "macosx"; + +add_task(async function() { + await pushPref("devtools.webconsole.input.editor", true); + await pushPref("reader.parse-on-load.enabled", true); + // Disable eager evaluation to avoid intermittent failures due to pending + // requests to evaluateJSAsync. + await pushPref("devtools.webconsole.input.eagerEvaluation", false); + + const readerModeButtonEl = document.querySelector("#reader-mode-button"); + + const hud = await openNewTabAndConsole(TEST_URI); + await waitFor( + () => !readerModeButtonEl.hidden, + "wait for the reader mode button to be displayed" + ); + + const jstermHistory = [ + `document + .querySelectorAll("*") + .forEach(console.log)`, + `Dog = "Snoopy"`, + ]; + + const onLastMessage = waitForMessage(hud, `"Snoopy"`, ".result"); + for (const input of jstermHistory) { + execute(hud, input); + } + await onLastMessage; + await openReverseSearch(hud); + + // Wait for a bit so reader mode would have some time to initialize. + await wait(1000); + is( + readerModeButtonEl.getAttribute("readeractive"), + "", + "reader mode wasn't activated" + ); + + EventUtils.sendString("d"); + const infoElement = await waitFor(() => getReverseSearchInfoElement(hud)); + is( + infoElement.textContent, + "2 of 2 results", + "The reverse info has the expected text" + ); + + is(getInputValue(hud), jstermHistory[1], "JsTerm has the expected input"); + + await navigateResultsAndCheckState(hud, { + direction: "previous", + expectedInfoText: "1 of 2 results", + expectedJsTermInputValue: jstermHistory[0], + }); + + await navigateResultsAndCheckState(hud, { + direction: "next", + expectedInfoText: "2 of 2 results", + expectedJsTermInputValue: jstermHistory[1], + }); + + // Wait for a bit so reader mode would have some time to initialize. + await wait(1000); + is( + readerModeButtonEl.getAttribute("readeractive"), + "", + "reader mode still wasn't activated" + ); + + await closeToolbox(); +}); + +async function navigateResultsAndCheckState( + hud, + { direction, expectedInfoText, expectedJsTermInputValue } +) { + const onJsTermValueChanged = hud.jsterm.once("set-input-value"); + if (direction === "previous") { + triggerPreviousResultShortcut(); + } else { + triggerNextResultShortcut(); + } + await onJsTermValueChanged; + + is(getInputValue(hud), expectedJsTermInputValue, "JsTerm has expected value"); + + const infoElement = getReverseSearchInfoElement(hud); + is( + infoElement.textContent, + expectedInfoText, + "The reverse info has the expected text" + ); + is( + isReverseSearchInputFocused(hud), + true, + "reverse search input is still focused" + ); +} + +function triggerPreviousResultShortcut() { + if (isMacOS) { + EventUtils.synthesizeKey("r", { ctrlKey: true }); + } else { + EventUtils.synthesizeKey("VK_F9"); + } +} + +function triggerNextResultShortcut() { + if (isMacOS) { + EventUtils.synthesizeKey("s", { ctrlKey: true }); + } else { + EventUtils.synthesizeKey("VK_F9", { shiftKey: true }); + } +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_editor_toggle_keyboard_shortcut.js b/devtools/client/webconsole/test/browser/browser_jsterm_editor_toggle_keyboard_shortcut.js new file mode 100644 index 0000000000..8a77ce5174 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_editor_toggle_keyboard_shortcut.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that hitting Ctrl + B does toggle the editor mode. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=1519105 + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf-8,Test editor mode toggle keyboard shortcut"; +const EDITOR_PREF = "devtools.webconsole.input.editor"; + +// See Bug 1631529 +requestLongerTimeout(2); + +add_task(async function() { + // Start with the editor turned off + await pushPref(EDITOR_PREF, false); + let hud = await openNewTabAndConsole(TEST_URI); + + const INPUT_VALUE = "`hello`"; + setInputValue(hud, INPUT_VALUE); + + is(isEditorModeEnabled(hud), false, "The console isn't in editor mode"); + + info("Enable the editor mode"); + await toggleLayout(hud); + is(isEditorModeEnabled(hud), true, "Editor mode is enabled"); + is(getInputValue(hud), INPUT_VALUE, "The input value wasn't cleared"); + + info("Close the console and reopen it"); + // Wait for eager evaluation result so we don't have a pending call to the server. + await waitForEagerEvaluationResult(hud, `"hello"`); + await closeConsole(); + hud = await openConsole(); + is(isEditorModeEnabled(hud), true, "Editor mode is still enabled"); + setInputValue(hud, INPUT_VALUE); + + info("Disable the editor mode"); + await toggleLayout(hud); + is(isEditorModeEnabled(hud), false, "Editor was disabled"); + is(getInputValue(hud), INPUT_VALUE, "The input value wasn't cleared"); + + info("Enable the editor mode again"); + await toggleLayout(hud); + is(isEditorModeEnabled(hud), true, "Editor mode was enabled again"); + is(getInputValue(hud), INPUT_VALUE, "The input value wasn't cleared"); + + info("Close popup on switching editor modes"); + const popup = hud.jsterm.autocompletePopup; + await setInputValueForAutocompletion(hud, "a"); + ok(popup.isOpen, "Auto complete popup is shown"); + const onPopupClosed = popup.once("popup-closed"); + await toggleLayout(hud); + await onPopupClosed; + ok(!popup.isOpen, "Auto complete popup is hidden on switching editor modes."); + + // Wait for eager evaluation result so we don't have a pending call to the server. + await waitForEagerEvaluationResult(hud, /ReferenceError/); + + Services.prefs.clearUserPref(EDITOR_PREF); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_editor_toolbar.js b/devtools/client/webconsole/test/browser/browser_jsterm_editor_toolbar.js new file mode 100644 index 0000000000..269633c67a --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_editor_toolbar.js @@ -0,0 +1,180 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check that the editor toolbar works as expected when in editor mode. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,<p>Test editor toolbar"; + +add_task(async function() { + await pushPref("devtools.webconsole.input.editor", false); + + const tab = await addTab(TEST_URI); + let hud = await openConsole(tab); + + info("Test that the toolbar is not displayed when in editor mode"); + let toolbar = getEditorToolbar(hud); + is(toolbar, null, "The toolbar isn't displayed when not in editor mode"); + await closeToolbox(); + + await pushPref("devtools.webconsole.input.editor", true); + hud = await openConsole(tab); + + info("Test that the toolbar is displayed when in editor mode"); + toolbar = getEditorToolbar(hud); + ok(toolbar, "The toolbar is displayed when in editor mode"); + + info("Test that the toolbar has the expected items"); + const runButton = toolbar.querySelector( + ".webconsole-editor-toolbar-executeButton" + ); + is(runButton.textContent, "Run", "The button has the expected text"); + const keyShortcut = + (Services.appinfo.OS === "Darwin" ? "Cmd" : "Ctrl") + " + Enter"; + is( + runButton.getAttribute("title"), + `Run expression (${keyShortcut}). This won’t clear the input.`, + "The Run Button has the correct title" + ); + + info("Test that clicking on the Run button works as expected"); + + const jsTestStatememts = Object.entries({ + // input: output, + "`${1 + 1} = 2`": "2 = 2", + '`${"area" + 51} = aliens?`': "area51 = aliens?", + }); + + for (const [input, output] of jsTestStatememts) { + // Setting the input value. + setInputValue(hud, input); + runButton.click(); + await waitFor(() => findMessage(hud, input)); + await waitFor(() => findMessage(hud, output, ".message.result")); + ok(true, "The expression and its result are displayed in the output"); + ok( + isInputFocused(hud), + "input is still focused after clicking the Run button" + ); + } + // Clear JS Term beform testing history buttons + setInputValue(hud, ""); + + info("Test that clicking the previous expression button works as expected"); + const prevHistoryButton = toolbar.querySelector( + ".webconsole-editor-toolbar-history-prevExpressionButton" + ); + is( + prevHistoryButton.getAttribute("title"), + "Previous Expression", + "The Previous Expression Button has the correct title" + ); + for (const [input] of jsTestStatememts.slice().reverse()) { + prevHistoryButton.click(); + is( + getInputValue(hud), + input, + `The JS Terminal Editor has the correct previous expresion ${input}` + ); + } + + info("Test that clicking the next expression button works as expected"); + const nextHistoryButton = toolbar.querySelector( + ".webconsole-editor-toolbar-history-nextExpressionButton" + ); + is( + nextHistoryButton.getAttribute("title"), + "Next Expression", + "The Next Expression Button has the correct title" + ); + nextHistoryButton.click(); + const [nextHistoryJsStatement] = jsTestStatememts.slice(-1).pop(); + is( + getInputValue(hud), + nextHistoryJsStatement, + `The JS Terminal Editor has the correct next expresion ${nextHistoryJsStatement}` + ); + nextHistoryButton.click(); + is(getInputValue(hud), ""); + + info("Test that clicking the pretty print button works as expected"); + const expressionToPrettyPrint = [ + // [raw, prettified, prettifiedWithTab, prettifiedWith4Spaces] + ["fn=n=>n*n", "fn = n => n * n", "fn = n => n * n", "fn = n => n * n"], + [ + "{x:1, y:2}", + "{\n x: 1,\n y: 2\n}", + "{\n\tx: 1,\n\ty: 2\n}", + "{\n x: 1,\n y: 2\n}", + ], + [ + "async function test() {await new Promise(res => {})}", + "async function test() {\n await new Promise(res => {})\n}", + "async function test() {\n\tawait new Promise(res => {})\n}", + "async function test() {\n await new Promise(res => {})\n}", + ], + ]; + + const prettyPrintButton = toolbar.querySelector( + ".webconsole-editor-toolbar-prettyPrintButton" + ); + ok(prettyPrintButton, "The pretty print button is displayed in editor mode"); + for (const [ + input, + output, + outputWithTab, + outputWith4Spaces, + ] of expressionToPrettyPrint) { + // Setting the input value. + setInputValue(hud, input); + await pushPref("devtools.editor.tabsize", 2); + prettyPrintButton.click(); + is( + getInputValue(hud), + output, + `Pretty print works for expression ${input}` + ); + // Turn on indent with tab. + await pushPref("devtools.editor.expandtab", false); + prettyPrintButton.click(); + is( + getInputValue(hud), + outputWithTab, + `Pretty print works for expression ${input} when expandtab is false` + ); + await pushPref("devtools.editor.expandtab", true); + // Set indent size to 4. + await pushPref("devtools.editor.tabsize", 4); + prettyPrintButton.click(); + is( + getInputValue(hud), + outputWith4Spaces, + `Pretty print works for expression ${input} when tabsize is 4` + ); + await pushPref("devtools.editor.tabsize", 2); + ok( + isInputFocused(hud), + "input is still focused after clicking the pretty print button" + ); + } + + info("Test that clicking the close button works as expected"); + const closeButton = toolbar.querySelector( + ".webconsole-editor-toolbar-closeButton" + ); + const closeKeyShortcut = + (Services.appinfo.OS === "Darwin" ? "Cmd" : "Ctrl") + " + B"; + is( + closeButton.title, + `Switch back to inline mode (${closeKeyShortcut})`, + "Close button has expected title" + ); + closeButton.click(); + await waitFor(() => !isEditorModeEnabled(hud)); + ok(true, "Editor mode is disabled when clicking on the close button"); +}); + +function getEditorToolbar(hud) { + return hud.ui.outputNode.querySelector(".webconsole-editor-toolbar"); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_error_docs.js b/devtools/client/webconsole/test/browser/browser_jsterm_error_docs.js new file mode 100644 index 0000000000..3d6c07b3ee --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_error_docs.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = "data:text/html,Test error documentation"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + // Check that errors with entries in errordocs.js display links next to their messages. + const ErrorDocs = require("devtools/server/actors/errordocs"); + + const ErrorDocStatements = { + JSMSG_BAD_RADIX: "(42).toString(0);", + JSMSG_BAD_ARRAY_LENGTH: "([]).length = -1", + JSMSG_NEGATIVE_REPETITION_COUNT: "'abc'.repeat(-1);", + JSMSG_PRECISION_RANGE: "77.1234.toExponential(-1);", + }; + + for (const [errorMessageName, expression] of Object.entries( + ErrorDocStatements + )) { + const errorUrl = ErrorDocs.GetURL({ errorMessageName }); + const title = errorUrl.split("?")[0]; + + await clearOutput(hud); + + const { node } = await executeAndWaitForMessage( + hud, + expression, + "RangeError:" + ); + const learnMoreLink = node.querySelector(".learn-more-link"); + ok( + learnMoreLink, + `There is a [Learn More] link for "${errorMessageName}" error` + ); + is( + learnMoreLink.title, + title, + `The link has the expected "${title}" title` + ); + is( + learnMoreLink.href, + errorUrl, + `The link has the expected "${errorUrl}" href value` + ); + } +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_error_outside_valid_range.js b/devtools/client/webconsole/test/browser/browser_jsterm_error_outside_valid_range.js new file mode 100644 index 0000000000..6f0f48e94c --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_error_outside_valid_range.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Ensure that dom errors, with error numbers outside of the range +// of valid js.msg errors, don't cause crashes (See Bug 1270721). + +const TEST_URI = "data:text/html,Test error documentation"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + const text = + "TypeError: Request constructor: 'foo' (value of 'redirect' member of RequestInit) is not a valid value " + + "for enumeration RequestRedirect"; + await executeAndWaitForMessage( + hud, + "new Request('',{redirect:'foo'})", + text, + ".message.error" + ); + ok( + true, + "Error message displayed as expected, without crashing the console." + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector.js b/devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector.js new file mode 100644 index 0000000000..ac09419e09 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector.js @@ -0,0 +1,253 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const FILE_FOLDER = `browser/devtools/client/webconsole/test/browser`; +const TEST_URI = `http://example.com/${FILE_FOLDER}/test-console-evaluation-context-selector.html`; +const IFRAME_PATH = `${FILE_FOLDER}/test-console-evaluation-context-selector-child.html`; + +requestLongerTimeout(2); + +add_task(async function() { + await pushPref("devtools.contenttoolbox.webconsole.input.context", true); + + const hud = await openNewTabWithIframesAndConsole(TEST_URI, [ + `http://example.org/${IFRAME_PATH}?id=iframe-1`, + `http://mochi.test:8888/${IFRAME_PATH}?id=iframe-2`, + ]); + + const evaluationContextSelectorButton = hud.ui.outputNode.querySelector( + ".webconsole-evaluation-selector-button" + ); + + ok( + evaluationContextSelectorButton, + "The evaluation context selector is visible" + ); + is( + evaluationContextSelectorButton.innerText, + "Top", + "The button has the expected 'Top' text" + ); + is( + evaluationContextSelectorButton.classList.contains( + "webconsole-evaluation-selector-button-non-top" + ), + false, + "The non-top class isn't applied" + ); + + const topLevelDocumentMessage = await executeAndWaitForMessage( + hud, + "document.location", + "example.com", + ".result" + ); + + setInputValue(hud, "document.location.host"); + await waitForEagerEvaluationResult(hud, `"example.com"`); + + info("Check the context selector menu"); + const expectedTopItem = { + label: "Top", + tooltip: TEST_URI, + }; + const expectedSeparatorItem = { separator: true }; + const expectedFirstIframeItem = { + label: "iframe-1|example.org", + tooltip: `http://example.org/${IFRAME_PATH}?id=iframe-1`, + }; + const expectedSecondIframeItem = { + label: "iframe-2|mochi.test:8888", + tooltip: `http://mochi.test:8888/${IFRAME_PATH}?id=iframe-2`, + }; + + await checkContextSelectorMenu(hud, [ + { + ...expectedTopItem, + checked: true, + }, + expectedSeparatorItem, + { + ...expectedFirstIframeItem, + checked: false, + }, + { + ...expectedSecondIframeItem, + checked: false, + }, + ]); + + info("Select the first iframe"); + selectTargetInContextSelector(hud, expectedFirstIframeItem.label); + + await waitFor(() => + evaluationContextSelectorButton.innerText.includes("example.org") + ); + ok(true, "The context was set to the selected iframe document"); + is( + evaluationContextSelectorButton.classList.contains( + "webconsole-evaluation-selector-button-non-top" + ), + true, + "The non-top class is applied" + ); + + await waitForEagerEvaluationResult(hud, `"example.org"`); + ok(true, "The instant evaluation result is updated in the iframe context"); + + const iframe1DocumentMessage = await executeAndWaitForMessage( + hud, + "document.location", + "example.org", + ".result" + ); + setInputValue(hud, "document.location.host"); + + info("Select the second iframe in the context selector menu"); + await checkContextSelectorMenu(hud, [ + { + ...expectedTopItem, + checked: false, + }, + expectedSeparatorItem, + { + ...expectedFirstIframeItem, + checked: true, + }, + { + ...expectedSecondIframeItem, + checked: false, + }, + ]); + selectTargetInContextSelector(hud, expectedSecondIframeItem.label); + + await waitFor(() => + evaluationContextSelectorButton.innerText.includes("mochi.test") + ); + ok(true, "The context was set to the selected iframe document"); + is( + evaluationContextSelectorButton.classList.contains( + "webconsole-evaluation-selector-button-non-top" + ), + true, + "The non-top class is applied" + ); + + await waitForEagerEvaluationResult(hud, `"mochi.test:8888"`); + ok(true, "The instant evaluation result is updated in the iframe context"); + + const iframe2DocumentMessage = await executeAndWaitForMessage( + hud, + "document.location", + "mochi.test", + ".result" + ); + setInputValue(hud, "document.location.host"); + + info("Select the top frame in the context selector menu"); + await checkContextSelectorMenu(hud, [ + { + ...expectedTopItem, + checked: false, + }, + expectedSeparatorItem, + { + ...expectedFirstIframeItem, + checked: false, + }, + { + ...expectedSecondIframeItem, + checked: true, + }, + ]); + selectTargetInContextSelector(hud, expectedTopItem.label); + + await waitForEagerEvaluationResult(hud, `"example.com"`); + await waitFor(() => + evaluationContextSelectorButton.innerText.includes("Top") + ); + is( + evaluationContextSelectorButton.classList.contains( + "webconsole-evaluation-selector-button-non-top" + ), + false, + "The non-top class isn't applied" + ); + + info("Check that 'Store as global variable' selects the right context"); + await testStoreAsGlobalVariable( + hud, + iframe1DocumentMessage, + "temp0", + "example.org" + ); + await waitForEagerEvaluationResult( + hud, + `Location http://example.org/${IFRAME_PATH}?id=iframe-1` + ); + await waitFor(() => + evaluationContextSelectorButton.innerText.includes("example.org") + ); + ok(true, "The context was set to the selected iframe document"); + + await testStoreAsGlobalVariable( + hud, + iframe2DocumentMessage, + "temp0", + "mochi.test:8888" + ); + await waitForEagerEvaluationResult( + hud, + `Location http://mochi.test:8888/${IFRAME_PATH}?id=iframe-2` + ); + await waitFor(() => + evaluationContextSelectorButton.innerText.includes("mochi.test") + ); + ok(true, "The context was set to the selected iframe document"); + + await testStoreAsGlobalVariable( + hud, + topLevelDocumentMessage, + "temp0", + "example.com" + ); + await waitForEagerEvaluationResult(hud, `Location ${TEST_URI}`); + await waitFor(() => + evaluationContextSelectorButton.innerText.includes("Top") + ); + ok(true, "The context was set to the top document"); +}); + +async function testStoreAsGlobalVariable( + hud, + msg, + variableName, + expectedTextResult +) { + const menuPopup = await openContextMenu( + hud, + msg.node.querySelector(".objectBox") + ); + const storeMenuItem = menuPopup.querySelector("#console-menu-store"); + const onceInputSet = hud.jsterm.once("set-input-value"); + storeMenuItem.click(); + + info("Wait for console input to be updated with the temp variable"); + await onceInputSet; + + info("Wait for context menu to be hidden"); + await hideContextMenu(hud); + + is(getInputValue(hud), variableName, "Input was set"); + + await executeAndWaitForMessage( + hud, + `${variableName}`, + expectedTextResult, + ".result" + ); + ok(true, "Correct variable assigned into console."); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector_inspector.js b/devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector_inspector.js new file mode 100644 index 0000000000..486c22c876 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector_inspector.js @@ -0,0 +1,187 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Test that the evaluation context selector reacts as expected when performing some +// inspector actions (selecting a node, "use in console" context menu entry, …). + +const FILE_FOLDER = `browser/devtools/client/webconsole/test/browser`; +const TEST_URI = `http://example.com/${FILE_FOLDER}/test-console-evaluation-context-selector.html`; +const IFRAME_PATH = `${FILE_FOLDER}/test-console-evaluation-context-selector-child.html`; + +requestLongerTimeout(2); + +add_task(async function() { + await pushPref("devtools.contenttoolbox.webconsole.input.context", true); + + const hud = await openNewTabWithIframesAndConsole(TEST_URI, [ + `http://example.org/${IFRAME_PATH}?id=iframe-1`, + `http://mochi.test:8888/${IFRAME_PATH}?id=iframe-2`, + ]); + + const evaluationContextSelectorButton = hud.ui.outputNode.querySelector( + ".webconsole-evaluation-selector-button" + ); + + setInputValue(hud, "document.location.host"); + await waitForEagerEvaluationResult(hud, `"example.com"`); + + info("Go to the inspector panel"); + const inspector = await openInspector(); + + info("Expand all the nodes"); + await inspector.markup.expandAll(); + + info("Open the split console"); + await hud.toolbox.openSplitConsole(); + + info("Select the first iframe h2 element"); + await selectIframeContentElement(inspector, ".iframe-1", "h2"); + + await waitFor(() => + evaluationContextSelectorButton.innerText.includes("example.org") + ); + ok(true, "The context was set to the selected iframe document"); + + await waitForEagerEvaluationResult(hud, `"example.org"`); + ok(true, "The instant evaluation result is updated in the iframe context"); + + info("Select the second iframe h2 element"); + await selectIframeContentElement(inspector, ".iframe-2", "h2"); + + await waitFor(() => + evaluationContextSelectorButton.innerText.includes("mochi.test") + ); + ok(true, "The context was set to the selected iframe document"); + + await waitForEagerEvaluationResult(hud, `"mochi.test:8888"`); + ok(true, "The instant evaluation result is updated in the iframe context"); + + info("Select an element in the top document"); + const h1NodeFront = await inspector.walker.findNodeFront(["h1"]); + inspector.selection.setNodeFront(null); + inspector.selection.setNodeFront(h1NodeFront); + + await waitForEagerEvaluationResult(hud, `"example.com"`); + await waitFor(() => + evaluationContextSelectorButton.innerText.includes("Top") + ); + + info( + "Check that 'Use in console' works as expected for element in the first iframe" + ); + await testUseInConsole( + hud, + inspector, + ".iframe-1", + "h2", + "temp0", + `<h2 id="iframe-1">` + ); + await waitFor(() => + evaluationContextSelectorButton.innerText.includes("example.org") + ); + ok(true, "The context selector was updated"); + + info( + "Check that 'Use in console' works as expected for element in the second iframe" + ); + await testUseInConsole( + hud, + inspector, + ".iframe-2", + "h2", + "temp0", + `<h2 id="iframe-2">` + ); + await waitFor(() => + evaluationContextSelectorButton.innerText.includes("mochi.test:8888") + ); + ok(true, "The context selector was updated"); + + info( + "Check that 'Use in console' works as expected for element in the top frame" + ); + await testUseInConsole( + hud, + inspector, + ":root", + "h1", + "temp0", + `<h1 id="top-level">` + ); + await waitFor(() => + evaluationContextSelectorButton.innerText.includes("Top") + ); + ok(true, "The context selector was updated"); +}); + +async function selectIframeContentElement( + inspector, + iframeSelector, + iframeContentSelector +) { + inspector.selection.setNodeFront(null); + const iframeNodeFront = await inspector.walker.findNodeFront([ + iframeSelector, + ]); + const childrenNodeFront = await iframeNodeFront + .treeChildren()[0] + .walkerFront.findNodeFront([iframeContentSelector]); + inspector.selection.setNodeFront(childrenNodeFront); + return childrenNodeFront; +} + +async function testUseInConsole( + hud, + inspector, + iframeSelector, + iframeContentSelector, + variableName, + expectedTextResult +) { + const nodeFront = await selectIframeContentElement( + inspector, + iframeSelector, + iframeContentSelector + ); + const container = inspector.markup.getContainer(nodeFront); + + // Clear the input before clicking on "Use in Console" to workaround an bug + // with eager-evaluation, which will be skipped if the console input didn't + // change. See https://bugzilla.mozilla.org/show_bug.cgi?id=1668916#c1. + // TODO: Should be removed when Bug 1669151 is fixed. + setInputValue(hud, ""); + // Also need to wait in order to avoid batching. + await wait(100); + + const onConsoleReady = inspector.once("console-var-ready"); + const menu = inspector.markup.contextMenu._openMenu({ + target: container.tagLine, + }); + const useInConsoleItem = menu.items.find( + ({ id }) => id === "node-menu-useinconsole" + ); + useInConsoleItem.click(); + await onConsoleReady; + + menu.clear(); + + is( + getInputValue(hud), + variableName, + "A variable with the expected name was created" + ); + await waitForEagerEvaluationResult(hud, expectedTextResult); + ok(true, "The eager evaluation display the expected result"); + + await executeAndWaitForMessage( + hud, + variableName, + expectedTextResult, + ".result" + ); + ok(true, "the expected variable was created with the expected value."); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector_pause_in_debugger.js b/devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector_pause_in_debugger.js new file mode 100644 index 0000000000..ba9e2f3d64 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector_pause_in_debugger.js @@ -0,0 +1,122 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Check that when the debugger pauses in a frame which is in a different target, the +// context selector is updated, and evaluating in the console is done in the paused +// frame context. + +const TEST_URI = `${URL_ROOT_COM}test-console-evaluation-context-selector.html`; +const IFRAME_FILE = `test-console-evaluation-context-selector-child.html`; + +add_task(async function() { + await pushPref("devtools.contenttoolbox.webconsole.input.context", true); + + const tab = await addTab(TEST_URI); + + info("Create new iframes and add them to the page."); + await addIFrameAndWaitForLoad(`${URL_ROOT_ORG}${IFRAME_FILE}?id=iframe_org`); + await addIFrameAndWaitForLoad( + `${URL_ROOT_MOCHI_8888}${IFRAME_FILE}?id=iframe_mochi8888` + ); + + const toolbox = await openToolboxForTab(tab, "webconsole"); + + info("Open Debugger"); + await openDebugger(); + const dbg = createDebuggerContext(toolbox); + + info("Hit the debugger statement on first iframe"); + clickOnIframeStopMeButton(".iframe-1"); + + info("Wait for the debugger to pause"); + await waitForPaused(dbg); + + info("Open the split Console"); + await toolbox.openSplitConsole(); + const { hud } = toolbox.getPanel("webconsole"); + + const evaluationContextSelectorButton = hud.ui.outputNode.querySelector( + ".webconsole-evaluation-selector-button" + ); + await waitFor( + () => evaluationContextSelectorButton.innerText.includes("example.org"), + "The context selector wasn't updated" + ); + ok(true, "The context was set to the first iframe document"); + + // localVar is defined in the event listener, and was assigned the `document` value. + setInputValue(hud, "localVar"); + await waitForEagerEvaluationResult(hud, /example\.org/); + ok(true, "Instant evaluation has the expected result"); + + await keyboardExecuteAndWaitForMessage( + hud, + `localVar`, + "example.org", + ".result" + ); + ok(true, "Evaluation result is the expected one"); + + // Cleanup + await clearOutput(hud); + setInputValue(hud, ""); + + info("Resume the debugger"); + await resume(dbg); + + info("Hit the debugger statement on second iframe"); + clickOnIframeStopMeButton(".iframe-2"); + + info("Wait for the debugger to pause"); + await waitForPaused(dbg); + + await waitFor( + () => evaluationContextSelectorButton.innerText.includes("mochi.test"), + "The context selector wasn't updated" + ); + ok(true, "The context was set to the second iframe document"); + + // localVar is defined in the event listener, and was assigned the `document` value. + setInputValue(hud, "localVar"); + await waitForEagerEvaluationResult(hud, /mochi\.test/); + ok(true, "Instant evaluation has the expected result"); + + await keyboardExecuteAndWaitForMessage( + hud, + `localVar`, + "mochi.test", + ".result" + ); + ok(true, "Evaluation result is the expected one"); + + info("Resume the debugger"); + await resume(dbg); +}); + +async function addIFrameAndWaitForLoad(url) { + await SpecialPowers.spawn(gBrowser.selectedBrowser, [url], async innerUrl => { + const iframe = content.document.createElement("iframe"); + const iframeCount = content.document.querySelectorAll("iframe").length; + iframe.classList.add(`iframe-${iframeCount + 1}`); + content.document.body.append(iframe); + + const onLoadIframe = new Promise(resolve => { + iframe.addEventListener("load", resolve, { once: true }); + }); + + iframe.src = innerUrl; + await onLoadIframe; + }); +} + +function clickOnIframeStopMeButton(iframeClassName) { + SpecialPowers.spawn(gBrowser.selectedBrowser, [iframeClassName], cls => { + const iframe = content.document.querySelector(cls); + SpecialPowers.spawn(iframe, [], () => { + content.document.querySelector(".stop-me").click(); + }); + }); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector_targets_update.js b/devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector_targets_update.js new file mode 100644 index 0000000000..9026cb3fc9 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector_targets_update.js @@ -0,0 +1,151 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const FILE_FOLDER = `browser/devtools/client/webconsole/test/browser`; +const TEST_URI = `http://example.com/${FILE_FOLDER}/test-console-evaluation-context-selector.html`; +const IFRAME_PATH = `${FILE_FOLDER}/test-console-evaluation-context-selector-child.html`; + +// Test that when a target is destroyed, it does not appear in the list anymore (and +// the context is set to the top one if the destroyed target was selected). + +add_task(async function() { + await pushPref("devtools.contenttoolbox.webconsole.input.context", true); + + const hud = await openNewTabWithIframesAndConsole(TEST_URI, [ + `http://mochi.test:8888/${IFRAME_PATH}?id=iframe-1`, + ]); + + const evaluationContextSelectorButton = hud.ui.outputNode.querySelector( + ".webconsole-evaluation-selector-button" + ); + + is( + evaluationContextSelectorButton.innerText, + "Top", + "The button has the expected 'Top' text" + ); + + setInputValue(hud, "document.location.host"); + await waitForEagerEvaluationResult(hud, `"example.com"`); + + info("Check the context selector menu"); + const expectedTopItem = { + label: "Top", + tooltip: TEST_URI, + }; + const expectedSeparatorItem = { separator: true }; + const expectedFirstIframeItem = { + label: "iframe-1|mochi.test:8888", + tooltip: `http://mochi.test:8888/${IFRAME_PATH}?id=iframe-1`, + }; + + await checkContextSelectorMenu(hud, [ + { + ...expectedTopItem, + checked: true, + }, + expectedSeparatorItem, + { + ...expectedFirstIframeItem, + checked: false, + }, + ]); + + info("Add another iframe"); + ContentTask.spawn(gBrowser.selectedBrowser, [IFRAME_PATH], function(path) { + const iframe = content.document.createElement("iframe"); + iframe.src = `http://test1.example.org/${path}?id=iframe-2`; + content.document.body.append(iframe); + }); + + // Wait until the new iframe is rendered in the context selector. + await waitFor(() => getContextSelectorItems(hud).length === 4); + + const expectedSecondIframeItem = { + // The title is set in a script, but we don't update the context selector entry (it + // should be "iframe-2|test1.example.org:80"). + label: `http://test1.example.org/${IFRAME_PATH}?id=iframe-2`, + tooltip: `http://test1.example.org/${IFRAME_PATH}?id=iframe-2`, + }; + + await checkContextSelectorMenu(hud, [ + { + ...expectedTopItem, + checked: true, + }, + expectedSeparatorItem, + { + ...expectedSecondIframeItem, + checked: false, + }, + { + ...expectedFirstIframeItem, + checked: false, + }, + ]); + + info("Select the first iframe"); + selectTargetInContextSelector(hud, expectedFirstIframeItem.label); + + await waitFor(() => + evaluationContextSelectorButton.innerText.includes("mochi.test") + ); + await waitForEagerEvaluationResult(hud, `"mochi.test:8888"`); + ok(true, "The context was set to the selected iframe document"); + + info("Remove the first iframe from the content document"); + ContentTask.spawn(gBrowser.selectedBrowser, [], function() { + content.document.querySelector("iframe").remove(); + }); + + await waitFor(() => + evaluationContextSelectorButton.innerText.includes("Top") + ); + ok( + true, + "The context was set to Top frame after the selected iframe was removed" + ); + await waitForEagerEvaluationResult(hud, `"example.com"`); + ok(true, "Instant evaluation is done against the top frame context"); + + await checkContextSelectorMenu(hud, [ + { + ...expectedTopItem, + checked: true, + }, + expectedSeparatorItem, + { + ...expectedSecondIframeItem, + checked: false, + }, + ]); + + info("Select the remaining iframe"); + selectTargetInContextSelector(hud, expectedSecondIframeItem.label); + + await waitFor(() => + evaluationContextSelectorButton.innerText.includes("test1.example.org") + ); + await waitForEagerEvaluationResult(hud, `"test1.example.org"`); + ok(true, "The context was set to the selected iframe document"); + + info("Remove the second iframe from the content document"); + ContentTask.spawn(gBrowser.selectedBrowser, [], function() { + content.document.querySelector("iframe").remove(); + }); + + await waitFor( + () => + !hud.ui.outputNode.querySelector(".webconsole-evaluation-selector-button") + ); + ok( + true, + "The evaluation context selector is hidden after last iframe was removed" + ); + + await waitForEagerEvaluationResult(hud, `"example.com"`); + ok(true, "Instant evaluation is done against the top frame context"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_file_load_save_keyboard_shortcut.js b/devtools/client/webconsole/test/browser/browser_jsterm_file_load_save_keyboard_shortcut.js new file mode 100644 index 0000000000..8ea721c2eb --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_file_load_save_keyboard_shortcut.js @@ -0,0 +1,98 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that the keyboard shortcut for loading/saving from the console input work as expected. + +const TEST_URI = + "data:text/html;charset=utf-8,Test load/save keyboard shortcut"; +const { FileUtils } = ChromeUtils.import( + "resource://gre/modules/FileUtils.jsm" +); +const LOCAL_FILE_NAME = "snippet.js"; +const LOCAL_FILE_ORIGINAL_CONTENT = `"Hello from local file"`; +const LOCAL_FILE_NEW_CONTENT = `"Hello from console input"`; + +add_task(async function() { + info("Open the console"); + const hud = await openNewTabAndConsole(TEST_URI); + is(getInputValue(hud), "", "Input is empty after opening"); + + // create file to import first + info("Create the file to import"); + const { MockFilePicker } = SpecialPowers; + MockFilePicker.init(window); + MockFilePicker.returnValue = MockFilePicker.returnOK; + + const file = await createLocalFile(); + MockFilePicker.setFiles([file]); + + const onFilePickerShown = new Promise(resolve => { + MockFilePicker.showCallback = fp => { + resolve(fp); + }; + }); + + const isMacOS = Services.appinfo.OS === "Darwin"; + EventUtils.synthesizeKey("O", { + [isMacOS ? "metaKey" : "ctrlKey"]: true, + }); + + info("Wait for File Picker"); + await onFilePickerShown; + + await waitFor(() => getInputValue(hud) === LOCAL_FILE_ORIGINAL_CONTENT); + ok(true, "File was imported into console input"); + + info("Change the input content"); + await setInputValue(hud, LOCAL_FILE_NEW_CONTENT); + + const nsiFile = FileUtils.getFile("TmpD", [`console_input_${Date.now()}.js`]); + MockFilePicker.setFiles([nsiFile]); + + info("Save the input content"); + EventUtils.synthesizeKey("S", { + [isMacOS ? "metaKey" : "ctrlKey"]: true, + }); + + await waitFor(() => OS.File.exists(nsiFile.path)); + const buffer = await OS.File.read(nsiFile.path); + const fileContent = new TextDecoder().decode(buffer); + is( + fileContent, + LOCAL_FILE_NEW_CONTENT, + "Saved file has the expected content" + ); + MockFilePicker.reset(); +}); + +async function createLocalFile() { + const file = FileUtils.getFile("TmpD", [LOCAL_FILE_NAME]); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8)); + await writeInFile(LOCAL_FILE_ORIGINAL_CONTENT, file); + return file; +} + +function getUnicodeConverter() { + const className = "@mozilla.org/intl/scriptableunicodeconverter"; + const converter = Cc[className].createInstance( + Ci.nsIScriptableUnicodeConverter + ); + converter.charset = "UTF-8"; + return converter; +} + +function writeInFile(string, file) { + const inputStream = getUnicodeConverter().convertToInputStream(string); + const outputStream = FileUtils.openSafeFileOutputStream(file); + + return new Promise((resolve, reject) => { + NetUtil.asyncCopy(inputStream, outputStream, status => { + if (!Components.isSuccessCode(status)) { + reject(new Error("Could not save data to file.")); + } + resolve(); + }); + }); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_focus_reload.js b/devtools/client/webconsole/test/browser/browser_jsterm_focus_reload.js new file mode 100644 index 0000000000..00905ab208 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_focus_reload.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check that the console does not steal the focus when reloading a page, if the focus +// is on the content page. + +"use strict"; + +const TEST_URI = `data:text/html,<meta charset=utf8>Focus test`; + +add_task(async function() { + info("Testing that messages disappear on a refresh if logs aren't persisted"); + const hud = await openNewTabAndConsole(TEST_URI); + is(isInputFocused(hud), true, "JsTerm is focused when opening the console"); + + info("Put the focus on the content page"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => content.focus()); + await waitFor(() => isInputFocused(hud) === false); + + info( + "Reload the page to check that JsTerm does not steal the content page focus" + ); + await refreshTab(); + is( + isInputFocused(hud), + false, + "JsTerm is still unfocused after reloading the page" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_helper_clear.js b/devtools/client/webconsole/test/browser/browser_jsterm_helper_clear.js new file mode 100644 index 0000000000..f38aa4011b --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_helper_clear.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,Test <code>clear()</code> jsterm helper"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + const onMessage = waitForMessage(hud, "message"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.console.log("message"); + }); + await onMessage; + + const onCleared = waitFor( + () => hud.ui.outputNode.querySelector(".message") === null + ); + execute(hud, "clear()"); + await onCleared; + ok(true, "Console was cleared"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_helper_dollar.js b/devtools/client/webconsole/test/browser/browser_jsterm_helper_dollar.js new file mode 100644 index 0000000000..cafe115e9c --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_helper_dollar.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = `data:text/html, +<main> + <ul> + <li>First</li> + <li>Second</li> + </ul> + <aside>Sidebar</aside> +</main> +`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + let message = await executeAndWaitForMessage( + hud, + "$('main')", + "<main>", + ".result" + ); + ok(message, "`$('main')` worked"); + + message = await executeAndWaitForMessage( + hud, + "$('main > ul > li')", + "<li>", + ".result" + ); + ok(message, "`$('main > ul > li')` worked"); + + message = await executeAndWaitForMessage( + hud, + "$('main > ul > li').tagName", + "LI", + ".result" + ); + ok(message, "`$` result can be used right away"); + + message = await executeAndWaitForMessage(hud, "$('div')", "null", ".result"); + ok(message, "`$('div')` does return null"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_helper_dollar_dollar.js b/devtools/client/webconsole/test/browser/browser_jsterm_helper_dollar_dollar.js new file mode 100644 index 0000000000..c5d24c3dd4 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_helper_dollar_dollar.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = `data:text/html, +<main> + <ul> + <li>First</li> + <li>Second</li> + </ul> + <aside>Sidebar</aside> +</main> +`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + // Place the mouse on the top left corner to avoid triggering an highlighter request + // to the server. See Bug 1535082. + EventUtils.synthesizeMouse( + hud.ui.outputNode, + 0, + 0, + { type: "mousemove" }, + hud.iframeWindow + ); + + let message = await executeAndWaitForMessage( + hud, + "$$('main')", + "Array [ main ]", + ".result" + ); + ok(message, "`$$('main')` worked"); + + message = await executeAndWaitForMessage( + hud, + "$$('main > ul > li')", + "Array [ li, li ]", + ".result" + ); + ok(message, "`$$('main > ul > li')` worked"); + + message = await executeAndWaitForMessage( + hud, + "$$('main > ul > li').map(el => el.tagName).join(' - ')", + "LI - LI", + ".result" + ); + ok(message, "`$$` result can be used right away"); + + message = await executeAndWaitForMessage( + hud, + "$$('div')", + "Array []", + ".result" + ); + ok(message, "`$$('div')` returns an empty array"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_helper_dollar_x.js b/devtools/client/webconsole/test/browser/browser_jsterm_helper_dollar_x.js new file mode 100644 index 0000000000..8b25ee5fa9 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_helper_dollar_x.js @@ -0,0 +1,152 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = `data:text/html, +<main> + <ul> + <li>First</li> + <li>Second</li> + </ul> + <aside>Sidebar</aside> +</main> +`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + // Place the mouse on the top left corner to avoid triggering an highlighter request + // to the server. See Bug 1531572. + EventUtils.synthesizeMouse( + hud.ui.outputNode, + 0, + 0, + { type: "mousemove" }, + hud.iframeWindow + ); + + let message = await executeAndWaitForMessage( + hud, + "$x('.//li')", + "Array [ li, li ]" + ); + ok(message, "`$x` worked"); + + message = await executeAndWaitForMessage( + hud, + "$x('.//li', document.body)[0]", + "<li>" + ); + ok(message, "`$x()` result can be used right away"); + + message = await executeAndWaitForMessage( + hud, + "$x('count(.//li)', document.body, XPathResult.NUMBER_TYPE)", + "2" + ); + ok(message, "$x works as expected with XPathResult.NUMBER_TYPE"); + + message = await executeAndWaitForMessage( + hud, + "$x('count(.//li)', document.body, 'number')", + "2" + ); + ok(message, "$x works as expected number type"); + + message = await executeAndWaitForMessage( + hud, + "$x('.//li', document.body, XPathResult.STRING_TYPE)", + "First" + ); + ok(message, "$x works as expected with XPathResult.STRING_TYPE"); + + message = await executeAndWaitForMessage( + hud, + "$x('.//li', document.body, 'string')", + "First" + ); + ok(message, "$x works as expected with string type"); + + message = await executeAndWaitForMessage( + hud, + "$x('//li[not(@foo)]', document.body, XPathResult.BOOLEAN_TYPE)", + "true" + ); + ok(message, "$x works as expected with XPathResult.BOOLEAN_TYPE"); + + message = await executeAndWaitForMessage( + hud, + "$x('//li[not(@foo)]', document.body, 'bool')", + "true" + ); + ok(message, "$x works as expected with bool type"); + + message = await executeAndWaitForMessage( + hud, + "$x('.//li', document.body, XPathResult.UNORDERED_NODE_ITERATOR_TYPE)", + "Array [ li, li ]" + ); + ok( + message, + "$x works as expected with XPathResult.UNORDERED_NODE_ITERATOR_TYPE" + ); + + message = await executeAndWaitForMessage( + hud, + "$x('.//li', document.body, 'nodes')", + "Array [ li, li ]" + ); + ok(message, "$x works as expected with nodes type"); + + message = await executeAndWaitForMessage( + hud, + "$x('.//li', document.body, XPathResult.ORDERED_NODE_ITERATOR_TYPE)", + "Array [ li, li ]" + ); + ok( + message, + "$x works as expected with XPathResult.ORDERED_NODE_ITERATOR_TYPE" + ); + + message = await executeAndWaitForMessage( + hud, + "$x('.//li', document.body, XPathResult.ANY_UNORDERED_NODE_TYPE)", + "<li>" + ); + ok(message, "$x works as expected with XPathResult.ANY_UNORDERED_NODE_TYPE"); + + message = await executeAndWaitForMessage( + hud, + "$x('.//li', document.body, XPathResult.FIRST_ORDERED_NODE_TYPE)", + "<li>" + ); + ok(message, "$x works as expected with XPathResult.FIRST_ORDERED_NODE_TYPE"); + + message = await executeAndWaitForMessage( + hud, + "$x('.//li', document.body, 'node')", + "<li>" + ); + ok(message, "$x works as expected with node type"); + + message = await executeAndWaitForMessage( + hud, + "$x('.//li', document.body, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE)", + "Array [ li, li ]" + ); + ok( + message, + "$x works as expected with XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE" + ); + + message = await executeAndWaitForMessage( + hud, + "$x('.//li', document.body, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE)", + "Array [ li, li ]" + ); + ok( + message, + "$x works as expected with XPathResult.ORDERED_NODE_SNAPSHOT_TYPE" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_helper_help.js b/devtools/client/webconsole/test/browser/browser_jsterm_helper_help.js new file mode 100644 index 0000000000..a0b459744f --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_helper_help.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = "data:text/html,Test <code>help()</code> jsterm helper"; +const HELP_URL = "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + let openedLinks = 0; + const oldOpenLink = hud.openLink; + hud.openLink = url => { + if (url == HELP_URL) { + openedLinks++; + } + }; + + await clearOutput(hud); + execute(hud, "help()"); + execute(hud, "help"); + execute(hud, "?"); + // Wait for a simple message to be displayed so we know the different help commands + // were processed. + await executeAndWaitForMessage(hud, "smoke", "", ".result"); + + const messages = hud.ui.outputNode.querySelectorAll(".message"); + is(messages.length, 5, "There is the expected number of messages"); + const resultMessages = hud.ui.outputNode.querySelectorAll(".result"); + is( + resultMessages.length, + 1, + "There is no results shown for the help commands" + ); + + is(openedLinks, 3, "correct number of pages opened by the help calls"); + hud.openLink = oldOpenLink; +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_helper_keys_values.js b/devtools/client/webconsole/test/browser/browser_jsterm_helper_keys_values.js new file mode 100644 index 0000000000..c7a6fe4ac3 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_helper_keys_values.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = + "data:text/html,Test <code>keys()</code> & <code>values()</code> jsterm helper"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + let message = await executeAndWaitForMessage( + hud, + "keys({a: 2, b:1})", + `Array [ "a", "b" ]`, + ".result" + ); + ok(message, "`keys()` worked"); + + message = await executeAndWaitForMessage( + hud, + "values({a: 2, b:1})", + "Array [ 2, 1 ]", + ".result" + ); + ok(message, "`values()` worked"); + + message = await executeAndWaitForMessage( + hud, + "keys(window)", + "Array", + ".result" + ); + ok(message, "`keys(window)` worked"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_hide_when_devtools_chrome_enabled_false.js b/devtools/client/webconsole/test/browser/browser_jsterm_hide_when_devtools_chrome_enabled_false.js new file mode 100644 index 0000000000..e8f00e9e8c --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_hide_when_devtools_chrome_enabled_false.js @@ -0,0 +1,196 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Hide Browser Console JS input field if devtools.chrome.enabled is false. + * + * when devtools.chrome.enabled then: + * - browser console jsterm should be enabled + * - browser console object inspector properties should be set. + * - webconsole jsterm should be enabled + * - webconsole object inspector properties should be set. + * + * when devtools.chrome.enabled === false then + * - browser console jsterm should be disabled + * - browser console object inspector properties should be set (we used to not + * set them but there is no reason not to do so as the input is disabled). + * - webconsole jsterm should be enabled + * - webconsole object inspector properties should be set. + */ + +"use strict"; + +// Needed for slow platforms (See https://bugzilla.mozilla.org/show_bug.cgi?id=1506970) +requestLongerTimeout(2); + +add_task(async function() { + let browserConsole, webConsole, objInspector; + + // Setting editor mode for both webconsole and browser console as there are more + // elements to check. + await pushPref("devtools.webconsole.input.editor", true); + await pushPref("devtools.browserconsole.input.editor", true); + + // Needed for the execute() function below + await pushPref("security.allow_parent_unrestricted_js_loads", true); + + // We don't use `pushPref()` because we need to revert the same pref later + // in the test. + Services.prefs.setBoolPref("devtools.chrome.enabled", true); + + browserConsole = await BrowserConsoleManager.toggleBrowserConsole(); + objInspector = await logObject(browserConsole); + testInputRelatedElementsAreVisibile(browserConsole); + await testObjectInspectorPropertiesAreSet(objInspector); + + const browserTab = await addTab("data:text/html;charset=utf8,hello world"); + webConsole = await openConsole(browserTab); + objInspector = await logObject(webConsole); + testInputRelatedElementsAreVisibile(webConsole); + await testObjectInspectorPropertiesAreSet(objInspector); + + // Wait for the sourceMap worker target to be fully attached before closing the + // Browser Console, otherwise this could lead to failures as the target tries to attach + // while the connection is being destroyed. + await waitForSourceMapWorker(browserConsole); + await closeConsole(browserTab); + await safeCloseBrowserConsole(); + + Services.prefs.setBoolPref("devtools.chrome.enabled", false); + browserConsole = await BrowserConsoleManager.toggleBrowserConsole(); + objInspector = await logObject(browserConsole); + testInputRelatedElementsAreNotVisibile(browserConsole); + + webConsole = await openConsole(browserTab); + objInspector = await logObject(webConsole); + testInputRelatedElementsAreVisibile(webConsole); + await testObjectInspectorPropertiesAreSet(objInspector); + + info("Close webconsole and browser console"); + // Wait for the sourceMap worker target to be fully attached before closing the + // Browser Console, otherwise this could lead to failures as the target tries to attach + // while the connection is being destroyed. + await waitForSourceMapWorker(browserConsole); + await closeConsole(browserTab); + await safeCloseBrowserConsole(); +}); + +async function logObject(hud) { + const prop = "browser_console_hide_jsterm_test"; + const { node } = await executeAndWaitForMessage( + hud, + `new Object({ ${prop}: true })`, + prop, + ".result" + ); + return node.querySelector(".tree"); +} + +function getInputRelatedElements(hud) { + const { document } = hud.ui.window; + + return { + inputEl: document.querySelector(".jsterm-input-container"), + eagerEvaluationEl: document.querySelector(".eager-evaluation-result"), + editorResizerEl: document.querySelector(".editor-resizer"), + editorToolbarEl: document.querySelector(".webconsole-editor-toolbar"), + webConsoleAppEl: document.querySelector(".webconsole-app"), + }; +} + +function testInputRelatedElementsAreVisibile(hud) { + const { + inputEl, + eagerEvaluationEl, + editorResizerEl, + editorToolbarEl, + webConsoleAppEl, + } = getInputRelatedElements(hud); + + isnot(inputEl.style.display, "none", "input is visible"); + ok(eagerEvaluationEl, "eager evaluation result is in dom"); + ok(editorResizerEl, "editor resizer is in dom"); + ok(editorToolbarEl, "editor toolbar is in dom"); + ok( + webConsoleAppEl.classList.contains("jsterm-editor") && + webConsoleAppEl.classList.contains("eager-evaluation"), + "webconsole element has expected classes" + ); +} + +function testInputRelatedElementsAreNotVisibile(hud) { + const { + inputEl, + eagerEvaluationEl, + editorResizerEl, + editorToolbarEl, + webConsoleAppEl, + } = getInputRelatedElements(hud); + + is(inputEl, null, "input is not in dom"); + is(eagerEvaluationEl, null, "eager evaluation result is not in dom"); + is(editorResizerEl, null, "editor resizer is not in dom"); + is(editorToolbarEl, null, "editor toolbar is not in dom"); + is( + webConsoleAppEl.classList.contains("jsterm-editor") && + webConsoleAppEl.classList.contains("eager-evaluation"), + false, + "webconsole element does not have eager evaluation nor editor classes" + ); +} + +async function testObjectInspectorPropertiesAreSet(objInspector) { + const onMutation = waitForNodeMutation(objInspector, { + childList: true, + }); + + const arrow = objInspector.querySelector(".arrow"); + arrow.click(); + await onMutation; + + ok( + arrow.classList.contains("expanded"), + "The arrow of the root node of the tree is expanded after clicking on it" + ); + + const nameNode = objInspector.querySelector( + ".node:not(.lessen) .object-label" + ); + const container = nameNode.parentNode; + const name = nameNode.textContent; + const value = container.querySelector(".objectBox").textContent; + + is(name, "browser_console_hide_jsterm_test", "name is set correctly"); + is(value, "true", "value is set correctly"); +} + +const seenWorkerTargets = new Set(); +function waitForSourceMapWorker(hud) { + const { targetList } = hud; + // If Fission is not enabled for the Browser Console (e.g. in Beta at this moment), + // the target list won't watch for Worker targets, and as a result we won't have issues + // with pending connections to the server that we're observing when attaching the target. + const isFissionEnabledForBrowserConsole = Services.prefs.getBoolPref( + "devtools.browsertoolbox.fission", + false + ); + if (!isFissionEnabledForBrowserConsole) { + return Promise.resolve(); + } + + return new Promise(resolve => { + const onAvailable = ({ targetFront }) => { + if ( + targetFront.url.endsWith( + "devtools/client/shared/source-map/worker.js" + ) && + !seenWorkerTargets.has(targetFront) + ) { + seenWorkerTargets.add(targetFront); + targetList.unwatchTargets([targetList.TYPES.WORKER], onAvailable); + resolve(); + } + }; + targetList.watchTargets([targetList.TYPES.WORKER], onAvailable); + }); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_history.js b/devtools/client/webconsole/test/browser/browser_jsterm_history.js new file mode 100644 index 0000000000..89b5b9c914 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_history.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests the console history feature accessed via the up and down arrow keys. + +"use strict"; + +const TEST_URI = "data:text/html;charset=UTF-8,test"; +const COMMANDS = ["document", "window", "window.location"]; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + jsterm.focus(); + + for (const command of COMMANDS) { + info(`Executing command ${command}`); + await executeAndWaitForMessage(hud, command, "", ".result"); + } + + for (let x = COMMANDS.length - 1; x != -1; x--) { + EventUtils.synthesizeKey("KEY_ArrowUp"); + is(getInputValue(hud), COMMANDS[x], "check history previous idx:" + x); + } + + EventUtils.synthesizeKey("KEY_ArrowUp"); + is(getInputValue(hud), COMMANDS[0], "test that item is still index 0"); + + EventUtils.synthesizeKey("KEY_ArrowUp"); + is(getInputValue(hud), COMMANDS[0], "test that item is still still index 0"); + + for (let i = 1; i < COMMANDS.length; i++) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + is(getInputValue(hud), COMMANDS[i], "check history next idx:" + i); + } + + EventUtils.synthesizeKey("KEY_ArrowDown"); + is(getInputValue(hud), "", "check input is empty again"); + + // Simulate pressing Arrow_Down a few times and then if Arrow_Up shows + // the previous item from history again. + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + + is(getInputValue(hud), "", "check input is still empty"); + + const idxLast = COMMANDS.length - 1; + EventUtils.synthesizeKey("KEY_ArrowUp"); + is( + getInputValue(hud), + COMMANDS[idxLast], + "check history next idx:" + idxLast + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_history_arrow_keys.js b/devtools/client/webconsole/test/browser/browser_jsterm_history_arrow_keys.js new file mode 100644 index 0000000000..742ee77f6f --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_history_arrow_keys.js @@ -0,0 +1,174 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// See Bugs 594497 and 619598. + +const TEST_URI = + "data:text/html;charset=utf-8,Web Console test for " + + "bug 594497 and bug 619598"; + +const TEST_VALUES = [ + "document", + "window", + "document.body", + "document;\nwindow;\ndocument.body", + "document.location", +]; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + + const checkInput = (expected, assertionInfo) => + checkInputValueAndCursorPosition(hud, expected, assertionInfo); + + jsterm.focus(); + checkInput("|", "input is empty"); + + info("Execute each test value in the console"); + for (const value of TEST_VALUES) { + await executeAndWaitForMessage(hud, value, "", ".result"); + } + + EventUtils.synthesizeKey("KEY_ArrowUp"); + checkInput("document.location|", "↑: input #4 is correct"); + ok(inputHasNoSelection(jsterm)); + + EventUtils.synthesizeKey("KEY_ArrowUp"); + checkInput("document;\nwindow;\ndocument.body|", "↑: input #3 is correct"); + ok(inputHasNoSelection(jsterm)); + + info( + "Move cursor and ensure hitting arrow up twice won't navigate the history" + ); + EventUtils.synthesizeKey("KEY_ArrowLeft"); + EventUtils.synthesizeKey("KEY_ArrowLeft"); + checkInput("document;\nwindow;\ndocument.bo|dy"); + + EventUtils.synthesizeKey("KEY_ArrowUp"); + EventUtils.synthesizeKey("KEY_ArrowUp"); + + checkInput("document;|\nwindow;\ndocument.body", "↑↑: input #3 is correct"); + ok(inputHasNoSelection(jsterm)); + + EventUtils.synthesizeKey("KEY_ArrowUp"); + checkInput( + "|document;\nwindow;\ndocument.body", + "↑ again: input #3 is correct" + ); + ok(inputHasNoSelection(jsterm)); + + EventUtils.synthesizeKey("KEY_ArrowUp"); + checkInput("document.body|", "↑: input #2 is correct"); + + EventUtils.synthesizeKey("KEY_ArrowUp"); + checkInput("window|", "↑: input #1 is correct"); + + EventUtils.synthesizeKey("KEY_ArrowUp"); + checkInput("document|", "↑: input #0 is correct"); + ok(inputHasNoSelection(jsterm)); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + checkInput("window|", "↓: input #1 is correct"); + ok(inputHasNoSelection(jsterm)); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + checkInput("document.body|", "↓: input #2 is correct"); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + checkInput("document;\nwindow;\ndocument.body|", "↓: input #3 is correct"); + ok(inputHasNoSelection(jsterm)); + + setCursorAtPosition(hud, 2); + checkInput("do|cument;\nwindow;\ndocument.body"); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + checkInput("document;\nwindow;\ndo|cument.body", "↓↓: input #3 is correct"); + ok(inputHasNoSelection(jsterm)); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + checkInput( + "document;\nwindow;\ndocument.body|", + "↓ again: input #3 is correct" + ); + ok(inputHasNoSelection(jsterm)); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + checkInput("document.location|", "↓: input #4 is correct"); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + checkInput("|", "↓: input is empty"); + + info("Test that Cmd + ArrowDown/Up works as expected on OSX"); + if (Services.appinfo.OS === "Darwin") { + const option = { metaKey: true }; + EventUtils.synthesizeKey("KEY_ArrowUp", option); + checkInput("document.location|", "Cmd+↑ : input is correct"); + + EventUtils.synthesizeKey("KEY_ArrowUp", option); + checkInput( + "document;\nwindow;\ndocument.body|", + "Cmd+↑ : input is correct" + ); + + EventUtils.synthesizeKey("KEY_ArrowUp", option); + checkInput( + "|document;\nwindow;\ndocument.body", + "Cmd+↑ : cursor is moved to the beginning of the input" + ); + + EventUtils.synthesizeKey("KEY_ArrowUp", option); + checkInput("document.body|", "Cmd+↑: input is correct"); + + EventUtils.synthesizeKey("KEY_ArrowDown", option); + checkInput( + "document;\nwindow;\ndocument.body|", + "Cmd+↓ : input is correct" + ); + + EventUtils.synthesizeKey("KEY_ArrowUp", option); + checkInput( + "|document;\nwindow;\ndocument.body", + "Cmd+↑ : cursor is moved to the beginning of the input" + ); + + EventUtils.synthesizeKey("KEY_ArrowDown", option); + checkInput( + "document;\nwindow;\ndocument.body|", + "Cmd+↓ : cursor is moved to the end of the input" + ); + + EventUtils.synthesizeKey("KEY_ArrowDown", option); + checkInput("document.location|", "Cmd+↓ : input is correct"); + + EventUtils.synthesizeKey("KEY_ArrowDown", option); + checkInput("|", "Cmd+↓: input is empty"); + } +}); + +function setCursorAtPosition(hud, pos) { + const { editor } = hud.jsterm; + + let line = 0; + let ch = 0; + let currentPos = 0; + getInputValue(hud) + .split("\n") + .every(l => { + if (l.length < pos - currentPos) { + line++; + currentPos += l.length; + return true; + } + ch = pos - currentPos; + return false; + }); + return editor.setCursor({ line, ch }); +} + +function inputHasNoSelection(jsterm) { + return !jsterm.editor.getDoc().getSelection(); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_history_nav.js b/devtools/client/webconsole/test/browser/browser_jsterm_history_nav.js new file mode 100644 index 0000000000..a8d21150f3 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_history_nav.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// See Bug 660806. Check that history navigation with the UP/DOWN arrows does not trigger +// autocompletion. + +const TEST_URI = + "data:text/html;charset=utf-8,<p>bug 660806 - history " + + "navigation must not show the autocomplete popup"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + const popup = jsterm.autocompletePopup; + + // The autocomplete popup should never be displayed during the test. + const onShown = function() { + ok(false, "popup shown"); + }; + popup.on("popup-opened", onShown); + + await executeAndWaitForMessage( + hud, + `window.foobarBug660806 = { + 'location': 'value0', + 'locationbar': 'value1' + }`, + "", + ".result" + ); + ok(!popup.isOpen, "popup is not open"); + + // Let's add this expression in the input history. We don't use setInputValue since + // it _does_ trigger an autocompletion request in codeMirror JsTerm. + await executeAndWaitForMessage( + hud, + "window.foobarBug660806.location", + "", + ".result" + ); + + const onSetInputValue = jsterm.once("set-input-value"); + EventUtils.synthesizeKey("KEY_ArrowUp"); + await onSetInputValue; + + // We don't have an explicit event to wait for here, so we just wait for the next tick + // before checking the popup status. + await new Promise(executeSoon); + + is( + getInputValue(hud), + "window.foobarBug660806.location", + "input has expected value" + ); + + ok(!popup.isOpen, "popup is not open"); + popup.off("popup-opened", onShown); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_history_persist.js b/devtools/client/webconsole/test/browser/browser_jsterm_history_persist.js new file mode 100644 index 0000000000..3f4fc3bf8f --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_history_persist.js @@ -0,0 +1,170 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that console command input is persisted across toolbox loads. +// See Bug 943306. + +"use strict"; + +requestLongerTimeout(2); + +const TEST_URI = + "data:text/html;charset=utf-8,Web Console test for persisting history"; +const INPUT_HISTORY_COUNT = 10; + +const { + getHistoryEntries, +} = require("devtools/client/webconsole/selectors/history"); + +add_task(async function() { + info("Setting custom input history pref to " + INPUT_HISTORY_COUNT); + Services.prefs.setIntPref( + "devtools.webconsole.inputHistoryCount", + INPUT_HISTORY_COUNT + ); + + // First tab: run a bunch of commands and then make sure that you can + // navigate through their history. + const hud1 = await openNewTabAndConsole(TEST_URI); + let state1 = hud1.ui.wrapper.getStore().getState(); + is( + JSON.stringify(getHistoryEntries(state1)), + "[]", + "No history on first tab initially" + ); + await populateInputHistory(hud1); + + state1 = hud1.ui.wrapper.getStore().getState(); + is( + JSON.stringify(getHistoryEntries(state1)), + '["0","1","2","3","4","5","6","7","8","9"]', + "First tab has populated history" + ); + + // Second tab: Just make sure that you can navigate through the history + // generated by the first tab. + const hud2 = await openNewTabAndConsole(TEST_URI, false); + let state2 = hud2.ui.wrapper.getStore().getState(); + is( + JSON.stringify(getHistoryEntries(state2)), + '["0","1","2","3","4","5","6","7","8","9"]', + "Second tab has populated history" + ); + await testNavigatingHistoryInUI(hud2); + + state2 = hud2.ui.wrapper.getStore().getState(); + is( + JSON.stringify(getHistoryEntries(state2)), + '["0","1","2","3","4","5","6","7","8","9"]', + "An empty entry has been added in the second tab due to history perusal" + ); + is( + state2.history.originalUserValue, + "", + "An empty value has been stored as the current input value" + ); + + // Third tab: Should have the same history as first tab, but if we run a + // command, then the history of the first and second shouldn't be affected + const hud3 = await openNewTabAndConsole(TEST_URI, false); + let state3 = hud3.ui.wrapper.getStore().getState(); + + is( + JSON.stringify(getHistoryEntries(state3)), + '["0","1","2","3","4","5","6","7","8","9"]', + "Third tab has populated history" + ); + + // Set input value separately from execute so UP arrow accurately navigates + // history. + setInputValue(hud3, '"hello from third tab"'); + await executeAndWaitForMessage( + hud3, + '"hello from third tab"', + '"hello from third tab"', + ".result" + ); + + state1 = hud1.ui.wrapper.getStore().getState(); + is( + JSON.stringify(getHistoryEntries(state1)), + '["0","1","2","3","4","5","6","7","8","9"]', + "First tab history hasn't changed due to command in third tab" + ); + + state2 = hud2.ui.wrapper.getStore().getState(); + is( + JSON.stringify(getHistoryEntries(state2)), + '["0","1","2","3","4","5","6","7","8","9"]', + "Second tab history hasn't changed due to command in third tab" + ); + is( + state2.history.originalUserValue, + "", + "Current input value hasn't changed due to command in third tab" + ); + + state3 = hud3.ui.wrapper.getStore().getState(); + is( + JSON.stringify(getHistoryEntries(state3)), + '["1","2","3","4","5","6","7","8","9","\\"hello from third tab\\""]', + "Third tab has updated history (and purged the first result) after " + + "running a command" + ); + + // Fourth tab: Should have the latest command from the third tab, followed + // by the rest of the history from the first tab. + const hud4 = await openNewTabAndConsole(TEST_URI, false); + let state4 = hud4.ui.wrapper.getStore().getState(); + is( + JSON.stringify(getHistoryEntries(state4)), + '["1","2","3","4","5","6","7","8","9","\\"hello from third tab\\""]', + "Fourth tab has most recent history" + ); + + await hud4.ui.wrapper.dispatchClearHistory(); + state4 = hud4.ui.wrapper.getStore().getState(); + is( + JSON.stringify(getHistoryEntries(state4)), + "[]", + "Clearing history for a tab works" + ); + + const hud5 = await openNewTabAndConsole(TEST_URI, false); + const state5 = hud5.ui.wrapper.getStore().getState(); + is( + JSON.stringify(getHistoryEntries(state5)), + "[]", + "Clearing history carries over to a new tab" + ); + + info("Clearing custom input history pref"); + Services.prefs.clearUserPref("devtools.webconsole.inputHistoryCount"); +}); + +/** + * Populate the history by running the following commands: + * [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + */ +async function populateInputHistory(hud) { + for (let i = 0; i < INPUT_HISTORY_COUNT; i++) { + const input = i.toString(); + await executeAndWaitForMessage(hud, input, input, ".result"); + } +} + +/** + * Check pressing up results in history traversal like: + * [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] + */ +function testNavigatingHistoryInUI(hud) { + const { jsterm } = hud; + jsterm.focus(); + + // Count backwards from original input and make sure that pressing up + // restores this. + for (let i = INPUT_HISTORY_COUNT - 1; i >= 0; i--) { + EventUtils.synthesizeKey("KEY_ArrowUp"); + is(getInputValue(hud), i.toString(), "Pressing up restores last input"); + } +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_insert_tab_when_overflows_no_scroll.js b/devtools/client/webconsole/test/browser/browser_jsterm_insert_tab_when_overflows_no_scroll.js new file mode 100644 index 0000000000..09a7b78263 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_insert_tab_when_overflows_no_scroll.js @@ -0,0 +1,41 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Check that when the input overflows, inserting a tab doesn't not impact the +// scroll position. See Bug 1578283. + +"use strict"; + +const TEST_URI = "data:text/html,<meta charset=utf8>"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const cmScroller = hud.ui.outputNode.querySelector(".CodeMirror-scroll"); + + info("Fill in the input with a hundred lines to make it overflow"); + await setInputValue(hud, "x;\n".repeat(100)); + + ok(hasVerticalOverflow(cmScroller), "input overflows"); + + info("Put the cursor at the very beginning"); + hud.jsterm.editor.setCursor({ + line: 0, + ch: 0, + }); + is(cmScroller.scrollTop, 0, "input is scrolled all the way up"); + + info("Move the cursor one line down and hit Tab"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Tab"); + checkInputValueAndCursorPosition( + hud, + `x;\n\t|x;\n${"x;\n".repeat(98)}`, + "a tab char was added at the start of the second line after hitting Tab" + ); + is( + cmScroller.scrollTop, + 0, + "Scroll position wasn't affected by new char addition" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_inspect.js b/devtools/client/webconsole/test/browser/browser_jsterm_inspect.js new file mode 100644 index 0000000000..b296780fc8 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_inspect.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check that the inspect() jsterm helper function works. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,<p>test inspect() command"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + info("Test `inspect(window)`"); + // Add a global value so we can check it later. + await executeAndWaitForMessage( + hud, + "testProp = 'testValue'", + "testValue", + ".result" + ); + const { node: inspectWindowNode } = await executeAndWaitForMessage( + hud, + "inspect(window)", + "Window", + ".result" + ); + + const objectInspectors = [...inspectWindowNode.querySelectorAll(".tree")]; + is( + objectInspectors.length, + 1, + "There is the expected number of object inspectors" + ); + + const [windowOi] = objectInspectors; + let windowOiNodes = windowOi.querySelectorAll(".node"); + + // The tree can be collapsed since the properties are fetched asynchronously. + if (windowOiNodes.length === 1) { + // If this is the case, we wait for the properties to be fetched and displayed. + await waitForNodeMutation(windowOi, { + childList: true, + }); + windowOiNodes = windowOi.querySelectorAll(".node"); + } + + const propertiesNodes = [...windowOi.querySelectorAll(".object-label")]; + const testPropertyLabelNode = propertiesNodes.find( + el => el.textContent === "testProp" + ); + ok( + testPropertyLabelNode, + "The testProp property label is displayed as expected" + ); + + const testPropertyValueNode = testPropertyLabelNode + .closest(".node") + .querySelector(".objectBox"); + is( + testPropertyValueNode.textContent, + '"testValue"', + "The testProp property value is displayed as expected" + ); + + /* Check that a primitive value can be inspected, too */ + info("Test `inspect(1)`"); + execute(hud, "inspect(1)"); + + const inspectPrimitiveNode = await waitFor(() => + findInspectResultMessage(hud.ui.outputNode, 2) + ); + is( + parseInt(inspectPrimitiveNode.textContent, 10), + 1, + "The primitive is displayed as expected" + ); +}); + +function findInspectResultMessage(node, index) { + return node.querySelectorAll(".message.result")[index]; +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_inspect_panels.js b/devtools/client/webconsole/test/browser/browser_jsterm_inspect_panels.js new file mode 100644 index 0000000000..f8d0e45f21 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_inspect_panels.js @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check that the inspect() jsterm helper function works. + +"use strict"; + +const TEST_URI = + "https://example.com/browser/devtools/client/webconsole/test/browser/" + + "test-simple-function.html"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + await testInspectingElement(hud); + await testInspectingFunction(hud); +}); + +async function testInspectingElement(hud) { + info("Test `inspect(el)`"); + execute(hud, "inspect(document.querySelector('p'))"); + await waitForSelectedElementInInspector(hud.toolbox, "p"); + ok(true, "inspected element is now selected in the inspector"); + + info( + "Test that inspect selects the node in the inspector in the split console as well" + ); + const onSplitConsoleReady = hud.toolbox.once("split-console"); + EventUtils.sendKey("ESCAPE", hud.toolbox.win); + await onSplitConsoleReady; + + execute(hud, "inspect(document.querySelector('body'))"); + await waitForSelectedElementInInspector(hud.toolbox, "body"); + ok(true, "the inspected element is selected in the inspector"); +} + +async function testInspectingFunction(hud) { + info("Test `inspect(test)`"); + execute(hud, "inspect(test)"); + await waitFor(expectedSourceSelected("test-simple-function.js", 3)); + ok(true, "inspected function is now selected in the debugger"); + + info("Test `inspect(test_mangled)`"); + execute(hud, "inspect(test_mangled)"); + await waitFor( + expectedSourceSelected("test-mangled-function.js/originalSource-", 3) + ); + ok(true, "inspected source-mapped function is now selected in the debugger"); + + info("Test `inspect(test_bound)`"); + execute(hud, "inspect(test_bound)"); + await waitFor(expectedSourceSelected("test-simple-function.js", 7)); + ok(true, "inspected bound target function is now selected in the debugger"); + + function expectedSourceSelected(sourceFilename, sourceLine) { + return () => { + const dbg = hud.toolbox.getPanel("jsdebugger"); + if (!dbg) { + return false; + } + + const selectedLocation = dbg._selectors.getSelectedLocation( + dbg._getState() + ); + + if (!selectedLocation) { + return false; + } + + return ( + selectedLocation.sourceId.includes(sourceFilename) && + selectedLocation.line == sourceLine + ); + }; + } +} + +async function waitForSelectedElementInInspector(toolbox, displayName) { + return waitFor(() => { + const inspector = toolbox.getPanel("inspector"); + if (!inspector) { + return false; + } + + const selection = inspector.selection; + return ( + selection && + selection.nodeFront && + selection.nodeFront.displayName == displayName + ); + }); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_instance_of.js b/devtools/client/webconsole/test/browser/browser_jsterm_instance_of.js new file mode 100644 index 0000000000..7378d0548f --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_instance_of.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check instanceof correctness. See Bug 599940. +const TEST_URI = "data:text/html,Test <code>instanceof</code> evaluation"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + let message = await executeAndWaitForMessage( + hud, + "[] instanceof Array", + "true", + ".result" + ); + ok(message, "`instanceof Array` is correct"); + + message = await executeAndWaitForMessage( + hud, + "({}) instanceof Object", + "true", + ".result" + ); + ok(message, "`instanceof Object` is correct"); + + message = await executeAndWaitForMessage( + hud, + "({}) instanceof Array", + "false", + ".result" + ); + ok(message, "`instanceof Array` has expected result"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_middle_click_paste.js b/devtools/client/webconsole/test/browser/browser_jsterm_middle_click_paste.js new file mode 100644 index 0000000000..e96095be21 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_middle_click_paste.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that pasting clipboard content into input with middle-click works. + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf-8,Web Console test paste on middle-click`; + +add_task(async function() { + await pushPref("devtools.selfxss.count", 5); + + // Enable pasting with middle-click. + await pushPref("middlemouse.paste", true); + + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + + info("Set clipboard content"); + const clipboardContent = "test clipboard content"; + setClipboardText(clipboardContent); + + info("Middle-click on the console input"); + const node = jsterm.node; + + EventUtils.synthesizeMouse(node, 30, 10, { button: 1 }, hud.iframeWindow); + is( + getInputValue(hud), + clipboardContent, + "clipboard content was pasted in the console input" + ); +}); + +function setClipboardText(text) { + const helper = SpecialPowers.Cc[ + "@mozilla.org/widget/clipboardhelper;1" + ].getService(SpecialPowers.Ci.nsIClipboardHelper); + helper.copyString(text); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_multiline.js b/devtools/client/webconsole/test/browser/browser_jsterm_multiline.js new file mode 100644 index 0000000000..d005eacc11 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_multiline.js @@ -0,0 +1,65 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Tests that the console waits for more input instead of evaluating +// when valid, but incomplete, statements are present upon pressing enter +// -or- when the user ends a line with shift + enter. + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/test/browser/test-console.html"; + +const SHOULD_ENTER_MULTILINE = [ + { input: "function foo() {" }, + { input: "var a = 1," }, + { input: "var a = 1;", shiftKey: true }, + { input: "function foo() { }", shiftKey: true }, + { input: "function" }, + { input: "(x) =>" }, + { input: "let b = {" }, + { input: "let a = [" }, + { input: "{" }, + { input: "{ bob: 3343," }, + { input: "function x(y=" }, + { input: "Array.from(" }, + // shift + enter creates a new line despite parse errors + { input: "{2,}", shiftKey: true }, +]; +const SHOULD_EXECUTE = [ + { input: "function foo() { }" }, + { input: "var a = 1;" }, + { input: "function foo() { var a = 1; }" }, + { input: '"asdf"' }, + { input: "99 + 3" }, + { input: "1, 2, 3" }, + // errors + { input: "function f(x) { let y = 1, }" }, + { input: "function f(x=,) {" }, + { input: "{2,}" }, +]; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + for (const { input, shiftKey } of SHOULD_ENTER_MULTILINE) { + setInputValue(hud, input); + EventUtils.synthesizeKey("VK_RETURN", { shiftKey }); + + // We need to remove the spaces at the end of the input since code mirror do some + // automatic indent in some case. + const newValue = getInputValue(hud).replace(/ +$/g, ""); + is(newValue, input + "\n", "A new line was added"); + } + + for (const { input, shiftKey } of SHOULD_EXECUTE) { + setInputValue(hud, input); + const onMessage = waitForMessage(hud, "", ".result"); + EventUtils.synthesizeKey("VK_RETURN", { shiftKey }); + await onMessage; + + await waitFor(() => !getInputValue(hud)); + is(getInputValue(hud), "", "Input is cleared"); + } +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_no_input_and_tab_key_pressed.js b/devtools/client/webconsole/test/browser/browser_jsterm_no_input_and_tab_key_pressed.js new file mode 100644 index 0000000000..2b51e24163 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_no_input_and_tab_key_pressed.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// See Bug 583816. + +const TEST_URI = + "data:text/html,<meta charset=utf8>Testing jsterm with no input"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const jsterm = hud.jsterm; + + info("Check that hitting Tab when input is empty insert blur the input"); + jsterm.focus(); + setInputValue(hud, ""); + EventUtils.synthesizeKey("KEY_Tab"); + is(getInputValue(hud), "", "inputnode is empty - matched"); + ok(!isInputFocused(hud), "input isn't focused anymore"); + + info("Check that hitting Shift+Tab when input is empty blur the input"); + jsterm.focus(); + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + is(getInputValue(hud), "", "inputnode is empty - matched"); + ok(!isInputFocused(hud), "input isn't focused anymore"); + ok( + hasFocus( + hud.ui.outputNode.querySelector(".webconsole-input-openEditorButton") + ), + `The "Toggle Editor" button is now focused` + ); + + info("Check that hitting Shift+Tab again place the focus on the filter bar"); + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + ok( + hasFocus( + hud.ui.outputNode.querySelector( + ".webconsole-console-settings-menu-button" + ) + ), + `The "Console Settings" button is now focused` + ); + + info("Check that hitting Tab when input is not empty insert a tab"); + jsterm.focus(); + + const testString = "window.Bug583816"; + await setInputValueForAutocompletion(hud, testString, 0); + checkInputValueAndCursorPosition( + hud, + `|${testString}`, + "cursor is at the start of the input" + ); + + EventUtils.synthesizeKey("KEY_Tab"); + checkInputValueAndCursorPosition( + hud, + `\t|${testString}`, + "a tab char was added at the start of the input after hitting Tab" + ); + ok(isInputFocused(hud), "input is still focused"); + + info( + "Check that hitting Shift+Tab when input is not empty removed leading tabs" + ); + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + checkInputValueAndCursorPosition( + hud, + `|${testString}`, + "The tab char at the the start of the input was removed after hitting Shift+Tab" + ); + ok(isInputFocused(hud), "input is still focused"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_null_undefined.js b/devtools/client/webconsole/test/browser/browser_jsterm_null_undefined.js new file mode 100644 index 0000000000..8095c376d2 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_null_undefined.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,Test evaluating null and undefined"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + // Check that an evaluated null produces "null". See Bug 650780. + let message = await executeAndWaitForMessage(hud, "null", "null", ".result"); + ok(message, "`null` returned the expected value"); + + message = await executeAndWaitForMessage( + hud, + "undefined", + "undefined", + ".result" + ); + ok(message, "`undefined` returned the expected value"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_popup_close_on_tab_switch.js b/devtools/client/webconsole/test/browser/browser_jsterm_popup_close_on_tab_switch.js new file mode 100644 index 0000000000..4138e7d608 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_popup_close_on_tab_switch.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the autocomplete popup closes on switching tabs. See bug 900448. + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf-8,<p>bug 900448 - autocomplete " + + "popup closes on tab switch"; +const TEST_URI_NAVIGATE = + "data:text/html;charset=utf-8,<p>testing autocomplete closes"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const popup = hud.jsterm.autocompletePopup; + const popupShown = once(popup, "popup-opened"); + + setInputValue(hud, "sc"); + EventUtils.sendString("r"); + + await popupShown; + + await addTab(TEST_URI_NAVIGATE); + + ok(!popup.isOpen, "Popup closes on tab switch"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_clipboard.js b/devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_clipboard.js new file mode 100644 index 0000000000..f95bd03737 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_clipboard.js @@ -0,0 +1,175 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that screenshot command works properly + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test_jsterm_screenshot_command.html"; + +// on some machines, such as macOS, dpr is set to 2. This is expected behavior, however +// to keep tests consistant across OSs we are setting the dpr to 1 +const dpr = "--dpr 1"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + ok(hud, "web console opened"); + + await testClipboard(hud); + await testFullpageClipboard(hud); + await testSelectorClipboard(hud); + + // overflow + await createScrollbarOverflow(); + await testFullpageClipboardScrollbar(hud); +}); + +async function testClipboard(hud) { + const command = `:screenshot --clipboard ${dpr}`; + await executeScreenshotClipboardCommand(hud, command); + const contentSize = await getContentSize(); + const imgSize = await getImageSizeFromClipboard(); + + is( + imgSize.width, + contentSize.innerWidth, + "Clipboard: Image width matches window size" + ); + is( + imgSize.height, + contentSize.innerHeight, + "Clipboard: Image height matches window size" + ); +} + +async function testFullpageClipboard(hud) { + const command = `:screenshot --fullpage --clipboard ${dpr}`; + await executeScreenshotClipboardCommand(hud, command); + const contentSize = await getContentSize(); + const imgSize = await getImageSizeFromClipboard(); + + is( + imgSize.width, + contentSize.innerWidth + contentSize.scrollMaxX - contentSize.scrollMinX, + "Fullpage Clipboard: Image width matches page size" + ); + is( + imgSize.height, + contentSize.innerHeight + contentSize.scrollMaxY - contentSize.scrollMinY, + "Fullpage Clipboard: Image height matches page size" + ); +} + +async function testSelectorClipboard(hud) { + const command = `:screenshot --selector "img#testImage" --clipboard ${dpr}`; + await executeScreenshotClipboardCommand(hud, command); + + const imgSize1 = await getImageSizeFromClipboard(); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [imgSize1], function( + imgSize + ) { + const img = content.document.querySelector("#testImage"); + is( + imgSize.width, + img.clientWidth, + "Selector Clipboard: Image width matches element size" + ); + is( + imgSize.height, + img.clientHeight, + "Selector Clipboard: Image height matches element size" + ); + }); +} + +async function testFullpageClipboardScrollbar(hud) { + const command = `:screenshot --fullpage --clipboard ${dpr}`; + await executeScreenshotClipboardCommand(hud, command); + const contentSize = await getContentSize(); + const scrollbarSize = await getScrollbarSize(); + const imgSize = await getImageSizeFromClipboard(); + + is( + imgSize.width, + contentSize.innerWidth + + contentSize.scrollMaxX - + contentSize.scrollMinX - + scrollbarSize.width, + "Scroll Fullpage Clipboard: Image width matches page size minus scrollbar size" + ); + is( + imgSize.height, + contentSize.innerHeight + + contentSize.scrollMaxY - + contentSize.scrollMinY - + scrollbarSize.height, + "Scroll Fullpage Clipboard: Image height matches page size minus scrollbar size" + ); +} + +/** + * Executes the command string and returns a Promise that resolves when the message + * saying that the screenshot was copied to clipboard is rendered in the console. + * + * @param {WebConsole} hud + * @param {String} command + */ +function executeScreenshotClipboardCommand(hud, command) { + return executeAndWaitForMessage( + hud, + command, + "Screenshot copied to clipboard." + ); +} + +async function createScrollbarOverflow() { + // Trigger scrollbars by forcing document to overflow + // This only affects results on OSes with scrollbars that reduce document size + // (non-floating scrollbars). With default OS settings, this means Windows + // and Linux are affected, but Mac is not. For Mac to exhibit this behavior, + // change System Preferences -> General -> Show scroll bars to Always. + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.document.body.classList.add("overflow"); + }); +} + +async function getScrollbarSize() { + const scrollbarSize = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + function() { + const winUtils = content.windowUtils; + const scrollbarHeight = {}; + const scrollbarWidth = {}; + winUtils.getScrollbarSize(true, scrollbarWidth, scrollbarHeight); + return { + width: scrollbarWidth.value, + height: scrollbarHeight.value, + }; + } + ); + info(`Scrollbar size: ${scrollbarSize.width}x${scrollbarSize.height}`); + return scrollbarSize; +} + +async function getContentSize() { + const contentSize = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + function() { + return { + scrollMaxY: content.scrollMaxY, + scrollMaxX: content.scrollMaxX, + scrollMinY: content.scrollMinY, + scrollMinX: content.scrollMinX, + innerWidth: content.innerWidth, + innerHeight: content.innerHeight, + }; + } + ); + + info(`content size: ${contentSize.innerWidth}x${contentSize.innerHeight}`); + return contentSize; +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_file.js b/devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_file.js new file mode 100644 index 0000000000..c7463f0a93 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_file.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that screenshot command works properly + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test_jsterm_screenshot_command.html"; + +const { FileUtils } = ChromeUtils.import( + "resource://gre/modules/FileUtils.jsm" +); +// on some machines, such as macOS, dpr is set to 2. This is expected behavior, however +// to keep tests consistant across OSs we are setting the dpr to 1 +const dpr = "--dpr 1"; + +add_task(async function() { + await addTab(TEST_URI); + + const hud = await openConsole(); + ok(hud, "web console opened"); + + await testFile(hud); +}); + +async function testFile(hud) { + // Test capture to file + const file = FileUtils.getFile("TmpD", ["TestScreenshotFile.png"]); + const command = `:screenshot ${file.path} ${dpr}`; + await executeAndWaitForMessage(hud, command, `Saved to ${file.path}`); + + ok(file.exists(), "Screenshot file exists"); + + if (file.exists()) { + file.remove(false); + } +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_user.js b/devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_user.js new file mode 100644 index 0000000000..f8c68c554c --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_user.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that screenshot command works properly + +"use strict"; + +const TEST_URI = `data:text/html,<meta charset=utf8><script> + function screenshot() { + console.log("contextScreen"); + } +</script>`; + +add_task(async function() { + await addTab(TEST_URI); + + const hud = await openConsole(); + ok(hud, "web console opened"); + + await testCommand(hud); + await testUserScreenshotFunction(hud); +}); + +async function testCommand(hud) { + const command = `:screenshot --clipboard`; + await executeAndWaitForMessage( + hud, + command, + "Screenshot copied to clipboard." + ); + ok(true, ":screenshot was executed as expected"); + + const helpMessage = await executeAndWaitForMessage( + hud, + `:screenshot --help`, + "Save an image of the page" + ); + ok(helpMessage, ":screenshot --help was executed as expected"); + is( + helpMessage.node.innerText.match(/--\w+/g).join("\n"), + [ + "--clipboard", + "--delay", + "--dpr", + "--fullpage", + "--selector", + "--file", + "--filename", + ].join("\n"), + "Help message references the arguments of the screenshot command" + ); +} + +// if a user defines a screenshot, as is the case in the Test URI, the +// command should not overwrite the screenshot function +async function testUserScreenshotFunction(hud) { + const command = `screenshot()`; + await executeAndWaitForMessage(hud, command, "contextScreen"); + ok( + true, + "content screenshot function is not overidden and was executed as expected" + ); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_warnings.js b/devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_warnings.js new file mode 100644 index 0000000000..27c5d3ea55 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_warnings.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that screenshot command leads to the proper warning and error messages in the +// console when necessary. + +"use strict"; + +// The test times out on slow platforms (e.g. linux ccov) +requestLongerTimeout(2); + +// We create a very big page here in order to make the :screenshot command fail on +// purpose. +const TEST_URI = `data:text/html;charset=utf8, + <style> + body { margin:0; } + .big { width:20000px; height:20000px; } + .small { width:5px; height:5px; } + </style> + <div class="big"></div> + <div class="small"></div>`; + +add_task(async function() { + await addTab(TEST_URI); + + const hud = await openConsole(); + ok(hud, "web console opened"); + + await testTruncationWarning(hud); + await testDPRWarning(hud); +}); + +async function testTruncationWarning(hud) { + info("Check that large screenshots get cut off if necessary"); + + let onMessages = waitForMessages({ + hud, + messages: [ + { text: "Screenshot copied to clipboard." }, + { + text: + "The image was cut off to 10000×10000 as the resulting image was too large", + }, + ], + }); + // Note, we put the screenshot in the clipboard so we can easily measure the resulting + // image. We also pass --dpr 1 so we don't need to worry about different machines having + // different screen resolutions. + execute(hud, ":screenshot --clipboard --selector .big --dpr 1"); + await onMessages; + + let { width, height } = await getImageSizeFromClipboard(); + is(width, 10000, "The resulting image is 10000px wide"); + is(height, 10000, "The resulting image is 10000px high"); + + onMessages = waitForMessages({ + hud, + messages: [{ text: "Screenshot copied to clipboard." }], + }); + execute(hud, ":screenshot --clipboard --selector .small --dpr 1"); + await onMessages; + + ({ width, height } = await getImageSizeFromClipboard()); + is(width, 5, "The resulting image is 5px wide"); + is(height, 5, "The resulting image is 5px high"); +} + +async function testDPRWarning(hud) { + info("Check that DPR is reduced to 1 after failure"); + + const onMessages = waitForMessages({ + hud, + messages: [ + { text: "Screenshot copied to clipboard." }, + { + text: + "The image was cut off to 10000×10000 as the resulting image was too large", + }, + { + text: + "The device pixel ratio was reduced to 1 as the resulting image was too large", + }, + ], + }); + execute(hud, ":screenshot --clipboard --fullpage --dpr 1000"); + await onMessages; + + const { width, height } = await getImageSizeFromClipboard(); + is(width, 10000, "The resulting image is 10000px wide"); + is(height, 10000, "The resulting image is 10000px high"); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_selfxss.js b/devtools/client/webconsole/test/browser/browser_jsterm_selfxss.js new file mode 100644 index 0000000000..d1f7f00609 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_selfxss.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,<p>Test self-XSS protection</p>"; + +XPCOMUtils.defineLazyServiceGetter( + this, + "clipboardHelper", + "@mozilla.org/widget/clipboardhelper;1", + "nsIClipboardHelper" +); +const WebConsoleUtils = require("devtools/client/webconsole/utils").Utils; +const stringToCopy = "EvilCommand"; + +add_task(async function() { + await pushPref("devtools.chrome.enabled", false); + await pushPref("devtools.selfxss.count", 0); + const hud = await openNewTabAndConsole(TEST_URI); + const { ui } = hud; + const { document } = ui; + + info("Self-xss paste tests"); + WebConsoleUtils.usageCount = 0; + is(WebConsoleUtils.usageCount, 0, "Test for usage count getter"); + + // Input some commands to check if usage counting is working + for (let i = 0; i <= 3; i++) { + await executeAndWaitForMessage(hud, i.toString(), i, ".result"); + } + + is(WebConsoleUtils.usageCount, 4, "Usage count incremented"); + WebConsoleUtils.usageCount = 0; + + info(`Copy "${stringToCopy}" in clipboard`); + await waitForClipboardPromise( + () => clipboardHelper.copyString(stringToCopy), + stringToCopy + ); + goDoCommand("cmd_paste"); + + const notificationbox = document.getElementById("webconsole-notificationbox"); + const notification = notificationbox.querySelector(".notification"); + is( + notification.getAttribute("data-key"), + "selfxss-notification", + "Self-xss notification shown" + ); + is(getInputValue(hud), "", "Paste blocked by self-xss prevention"); + + // Allow pasting + const allowToken = "allow pasting"; + for (const char of allowToken) { + EventUtils.sendString(char); + } + + setInputValue(hud, ""); + goDoCommand("cmd_paste"); + is(getInputValue(hud), stringToCopy, "Paste works"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_syntax_highlight_output.js b/devtools/client/webconsole/test/browser/browser_jsterm_syntax_highlight_output.js new file mode 100644 index 0000000000..b5d2cf360d --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_jsterm_syntax_highlight_output.js @@ -0,0 +1,26 @@ +/* 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,Test syntax highlighted output"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + // Syntax highlighting is implemented with a Custom Element: + ok( + hud.iframeWindow.customElements.get("syntax-highlighted"), + "Custom Element exists" + ); + + // Check that we syntax highlight output to look like the inputed text. + // See Bug 1463669. + const onMessage = waitForMessage(hud, `var a = 'str';`); + execute(hud, "var a = 'str';"); + const message = await onMessage; + const highlighted = message.node.querySelectorAll("syntax-highlighted"); + const expectedMarkup = `<syntax-highlighted class="cm-s-mozilla"><span class="cm-keyword">var</span> <span class="cm-def">a</span> <span class="cm-operator">=</span> <span class="cm-string">'str'</span>;</syntax-highlighted>`; + is(highlighted.length, 1, "1 syntax highlighted tag"); + is(highlighted[0].outerHTML, expectedMarkup, "got expected html"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_toolbox_console_new_process.js b/devtools/client/webconsole/test/browser/browser_toolbox_console_new_process.js new file mode 100644 index 0000000000..5892226f76 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_toolbox_console_new_process.js @@ -0,0 +1,57 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Test that when the multiprocess browser toolbox is used, console messages +// from newly opened content processes appear. + +"use strict"; + +requestLongerTimeout(4); + +const TEST_URI = `data:text/html,<meta charset=utf8>console API calls<script> + console.log("Data Message"); +</script>`; + +const EXAMPLE_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-console.html"; + +/* global gToolbox */ +/* import-globals-from ../../../framework/browser-toolbox/test/helpers-browser-toolbox.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/framework/browser-toolbox/test/helpers-browser-toolbox.js", + this +); + +add_task(async function() { + await pushPref("devtools.browsertoolbox.fission", true); + // Needed for the invokeInTab() function below + await pushPref("security.allow_parent_unrestricted_js_loads", true); + + await addTab(TEST_URI); + const ToolboxTask = await initBrowserToolboxTask({ + enableContentMessages: true, + }); + await ToolboxTask.importFunctions({ findMessages, findMessage, waitUntil }); + + // Make sure the data: URL message appears in the OBT. + await ToolboxTask.spawn(null, async () => { + await gToolbox.selectTool("webconsole"); + const hud = gToolbox.getCurrentPanel().hud; + await waitUntil(() => findMessage(hud, "Data Message")); + }); + ok(true, "First message appeared in toolbox"); + + await addTab(EXAMPLE_URI); + invokeInTab("stringLog"); + + // Make sure the example.com message appears in the OBT. + await ToolboxTask.spawn(null, async () => { + const hud = gToolbox.getCurrentPanel().hud; + await waitUntil(() => findMessage(hud, "stringLog")); + }); + ok(true, "New message appeared in toolbox"); + + await ToolboxTask.destroy(); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_allow_mixedcontent_securityerrors.js b/devtools/client/webconsole/test/browser/browser_webconsole_allow_mixedcontent_securityerrors.js new file mode 100644 index 0000000000..ae7eb1d767 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_allow_mixedcontent_securityerrors.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// The test loads a web page with mixed active and display content +// on it while the "block mixed content" settings are _off_. +// It then checks that the loading mixed content warning messages +// are logged to the console and have the correct "Learn More" +// url appended to them. +// Bug 875456 - Log mixed content messages from the Mixed Content +// Blocker to the Security Pane in the Web Console + +"use strict"; + +const TEST_URI = + "https://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-mixedcontent-securityerrors.html"; +const LEARN_MORE_URI = + "https://developer.mozilla.org/docs/Web/Security/" + + "Mixed_content" + + DOCS_GA_PARAMS; + +add_task(async function() { + await Promise.all([ + pushPref("security.mixed_content.block_active_content", false), + pushPref("security.mixed_content.block_display_content", false), + pushPref("security.mixed_content.upgrade_display_content", false), + ]); + + const hud = await openNewTabAndConsole(TEST_URI); + + const activeContentText = + "Loading mixed (insecure) active content " + + "\u201chttp://example.com/\u201d on a secure page"; + const displayContentText = + "Loading mixed (insecure) display content " + + "\u201chttp://example.com/tests/image/test/mochitest/blue.png\u201d on a secure page"; + + const waitUntilWarningMessage = text => + waitFor(() => findMessage(hud, text, ".message.warn"), undefined, 100); + + const onMixedActiveContent = waitUntilWarningMessage(activeContentText); + const onMixedDisplayContent = waitUntilWarningMessage(displayContentText); + + await onMixedDisplayContent; + ok(true, "Mixed display content warning message is visible"); + + const mixedActiveContentMessage = await onMixedActiveContent; + ok(true, "Mixed active content warning message is visible"); + + const checkLink = ({ link, where, expectedLink, expectedTab }) => { + is(link, expectedLink, `Clicking the provided link opens ${link}`); + is(where, expectedTab, `Clicking the provided link opens in expected tab`); + }; + + info("Clicking on the Learn More link"); + const learnMoreLink = mixedActiveContentMessage.querySelector( + ".learn-more-link" + ); + const linkSimulation = await simulateLinkClick(learnMoreLink); + checkLink({ + ...linkSimulation, + expectedLink: LEARN_MORE_URI, + expectedTab: "tab", + }); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_async_stack.js b/devtools/client/webconsole/test/browser/browser_webconsole_async_stack.js new file mode 100644 index 0000000000..e7568254d1 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_async_stack.js @@ -0,0 +1,90 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check that async stacktraces are displayed as expected. + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf8,<script> +function timeout(cb, delay) { + setTimeout(cb, delay); +} + +function promiseThen(cb) { + Promise.resolve().then(cb); +} + +const onTimeout = () => { + console.trace("Trace message"); + console.error("console error message"); + throw new Error("Thrown error message"); +}; +const onPromiseThen = () => timeout(onTimeout, 1); +promiseThen(onPromiseThen); + +</script>`; + +add_task(async function() { + await pushPref("javascript.options.asyncstack_capture_debuggee_only", false); + const hud = await openNewTabAndConsole(TEST_URI); + + // Cached messages stacktrace are missing "promise callback" frames, so we reload + // the page to get "live" messages instead. See Bug 1604428. + await reloadPage(); + + const expectedFrames = [ + "onTimeout", + "(Async: setTimeout handler)", + "timeout", + "onPromiseThen", + "(Async: promise callback)", + "promiseThen", + "<anonymous>", + ].join("\n"); + + const traceMsgNode = await waitFor( + () => findMessage(hud, "Trace message", ".console-api.trace"), + "Wait for the trace message to be logged" + ); + let frames = await getSimplifiedStack(traceMsgNode); + is(frames, expectedFrames, "console.trace has expected frames"); + + const consoleErrorMsgNode = await waitFor( + () => findMessage(hud, "console error message", ".console-api.error"), + "Wait for the console error message to be logged" + ); + consoleErrorMsgNode.querySelector(".arrow").click(); + frames = await getSimplifiedStack(consoleErrorMsgNode); + is(frames, expectedFrames, "console.error has expected frames"); + + const errorMsgNode = await waitFor( + () => + findMessage( + hud, + "Uncaught Error: Thrown error message", + ".javascript.error" + ), + "Wait for the thrown error message to be logged" + ); + errorMsgNode.querySelector(".arrow").click(); + frames = await getSimplifiedStack(errorMsgNode); + is(frames, expectedFrames, "thrown error has expected frames"); +}); + +async function getSimplifiedStack(messageEl) { + const framesEl = await waitFor(() => { + const frames = messageEl.querySelectorAll( + ".message-body-wrapper > .stacktrace .frame" + ); + return frames.length > 0 ? frames : null; + }, "Couldn't find stacktrace"); + + return Array.from(framesEl) + .map(frameEl => + Array.from( + frameEl.querySelectorAll(".title,.location-async-cause") + ).map(el => el.textContent.trim()) + ) + .flat() + .join("\n"); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_batching.js b/devtools/client/webconsole/test/browser/browser_webconsole_batching.js new file mode 100644 index 0000000000..a52f1e861d --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_batching.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check adding console calls as batch keep the order of the message. + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-batching.html"; +const { l10n } = require("devtools/client/webconsole/utils/messages"); + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const messageNumber = 100; + await testSimpleBatchLogging(hud, messageNumber); + await testBatchLoggingAndClear(hud, messageNumber); +}); + +async function testSimpleBatchLogging(hud, messageNumber) { + await SpecialPowers.spawn(gBrowser.selectedBrowser, [messageNumber], function( + numMessages + ) { + content.wrappedJSObject.batchLog(numMessages); + }); + + for (let i = 0; i < messageNumber; i++) { + const node = await waitFor(() => findMessageAtIndex(hud, i, i)); + is( + node.textContent, + i.toString(), + `message at index "${i}" is the expected one` + ); + } +} + +async function testBatchLoggingAndClear(hud, messageNumber) { + await SpecialPowers.spawn(gBrowser.selectedBrowser, [messageNumber], function( + numMessages + ) { + content.wrappedJSObject.batchLogAndClear(numMessages); + }); + await waitFor(() => findMessage(hud, l10n.getStr("consoleCleared"))); + ok(true, "console cleared message is displayed"); + + // Passing the text argument as an empty string will returns all the message, + // whatever their content is. + const messages = findMessages(hud, ""); + is(messages.length, 1, "console was cleared as expected"); +} + +function findMessageAtIndex(hud, text, index) { + const selector = `.message:nth-of-type(${index + 1}) .message-body`; + return findMessage(hud, text, selector); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_block_mixedcontent_securityerrors.js b/devtools/client/webconsole/test/browser/browser_webconsole_block_mixedcontent_securityerrors.js new file mode 100644 index 0000000000..dd23b7ef41 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_block_mixedcontent_securityerrors.js @@ -0,0 +1,105 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// The test loads a web page with mixed active and display content +// on it while the "block mixed content" settings are _on_. +// It then checks that the blocked mixed content warning messages +// are logged to the console and have the correct "Learn More" +// url appended to them. After the first test finishes, it invokes +// a second test that overrides the mixed content blocker settings +// by clicking on the doorhanger shield and validates that the +// appropriate messages are logged to console. +// Bug 875456 - Log mixed content messages from the Mixed Content +// Blocker to the Security Pane in the Web Console. + +"use strict"; + +const TEST_URI = + "https://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-mixedcontent-securityerrors.html"; +const LEARN_MORE_URI = + "https://developer.mozilla.org/docs/Web/Security/Mixed_content" + + DOCS_GA_PARAMS; + +const blockedActiveContentText = + "Blocked loading mixed active content \u201chttp://example.com/\u201d"; +const blockedDisplayContentText = + "Blocked loading mixed display content " + + "\u201chttp://example.com/tests/image/test/mochitest/blue.png\u201d"; +const activeContentText = + "Loading mixed (insecure) active content " + + "\u201chttp://example.com/\u201d on a secure page"; +const displayContentText = + "Loading mixed (insecure) display content " + + "\u201chttp://example.com/tests/image/test/mochitest/blue.png\u201d on a " + + "secure page"; + +add_task(async function() { + await pushPrefEnv(); + + const hud = await openNewTabAndConsole(TEST_URI); + + const waitForErrorMessage = text => + waitFor(() => findMessage(hud, text, ".message.error"), undefined, 100); + + const onBlockedIframe = waitForErrorMessage(blockedActiveContentText); + const onBlockedImage = waitForErrorMessage(blockedDisplayContentText); + + await onBlockedImage; + ok(true, "Blocked mixed display content error message is visible"); + + const blockedMixedActiveContentMessage = await onBlockedIframe; + ok(true, "Blocked mixed active content error message is visible"); + + info("Clicking on the Learn More link"); + let learnMoreLink = blockedMixedActiveContentMessage.querySelector( + ".learn-more-link" + ); + let response = await simulateLinkClick(learnMoreLink); + is( + response.link, + LEARN_MORE_URI, + `Clicking the provided link opens ${response.link}` + ); + + info("Test disabling mixed content protection"); + + const { gIdentityHandler } = gBrowser.ownerGlobal; + ok( + gIdentityHandler._identityBox.classList.contains("mixedActiveBlocked"), + "Mixed Active Content state appeared on identity box" + ); + // Disabe mixed content protection. + gIdentityHandler.disableMixedContentProtection(); + + const waitForWarningMessage = text => + waitFor(() => findMessage(hud, text, ".message.warn"), undefined, 100); + + const onMixedActiveContent = waitForWarningMessage(activeContentText); + const onMixedDisplayContent = waitForWarningMessage(displayContentText); + + await onMixedDisplayContent; + ok(true, "Mixed display content warning message is visible"); + + const mixedActiveContentMessage = await onMixedActiveContent; + ok(true, "Mixed active content warning message is visible"); + + info("Clicking on the Learn More link"); + learnMoreLink = mixedActiveContentMessage.querySelector(".learn-more-link"); + response = await simulateLinkClick(learnMoreLink); + is( + response.link, + LEARN_MORE_URI, + `Clicking the provided link opens ${response.link}` + ); +}); + +function pushPrefEnv() { + const prefs = [ + ["security.mixed_content.block_active_content", true], + ["security.mixed_content.block_display_content", true], + ["security.mixed_content.upgrade_display_content", false], + ]; + + return Promise.all(prefs.map(([pref, value]) => pushPref(pref, value))); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_cached_messages.js b/devtools/client/webconsole/test/browser/browser_webconsole_cached_messages.js new file mode 100644 index 0000000000..e6a58aecd9 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_cached_messages.js @@ -0,0 +1,185 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test to see if the cached messages are displayed when the console UI is opened. + +"use strict"; + +// See Bug 1570524. +requestLongerTimeout(2); + +const TEST_URI = `data:text/html,<meta charset=utf8><h1>Test cached messages</h1> + <style> + h1 { + color: cssColorBug611032; + } + </style> + <script> + function logException() { + return new Promise(resolve => { + setTimeout(() => { + let foo = {}; + resolve(); + foo.unknown(); + }, 0); + }) + } + </script>`; + +add_task(async function() { + // On e10s, the exception is triggered in child process + // and is ignored by test harness + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } + // Enable CSS and XHR filters for the test. + await pushPref("devtools.webconsole.filter.css", true); + await pushPref("devtools.webconsole.filter.netxhr", true); + + await addTab(TEST_URI); + + info("Log different type of messages to fill the cache"); + await logMessages(); + + info("Open the console"); + let hud = await openConsole(); + + // We only start watching network requests when opening the toolbox. + await testMessagesVisibility(hud, false); + + info("Close the toolbox and reload the tab"); + await closeToolbox(); + await reloadPage(); + + info( + "Open the toolbox with the inspector selected, so we can get network messages" + ); + await openInspector(); + + info("Log different type of messages to fill the cache"); + await logMessages(); + + info("Select the console"); + hud = await openConsole(); + + await testMessagesVisibility(hud); + + info("Close the toolbox"); + await closeToolbox(); + + info("Open the console again"); + hud = await openConsole(); + // The network messages don't persist. + await testMessagesVisibility(hud, false); +}); + +async function logMessages() { + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() { + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + const wait = () => + new Promise(res => content.wrappedJSObject.setTimeout(res, 100)); + + content.wrappedJSObject.console.log("log Bazzle"); + await wait(); + + await content.wrappedJSObject.logException(); + await wait(); + + await content.wrappedJSObject.fetch( + "http://mochi.test:8888/browser/devtools/client/webconsole/test/browser/sjs_cors-test-server.sjs?1", + { mode: "cors" } + ); + await wait(); + + content.wrappedJSObject.console.error("error Bazzle"); + await wait(); + + await content.wrappedJSObject.logException(); + await wait(); + + await content.wrappedJSObject.fetch( + "http://mochi.test:8888/browser/devtools/client/webconsole/test/browser/sjs_cors-test-server.sjs?2" + ); + + content.wrappedJSObject.console.info("info Bazzle"); + await wait(); + }); +} + +async function testMessagesVisibility(hud, checkNetworkMessage = true) { + // wait for the last logged message to be displayed + await waitFor(() => findMessage(hud, "info Bazzle", ".message.info")); + + const messages = Array.from(hud.ui.outputNode.querySelectorAll(".message")); + const EXPECTED_MESSAGES = [ + { + text: "log Bazzle", + category: "log", + }, + { + text: "foo.unknown is not a function", + category: "error", + }, + { + text: "sjs_cors-test-server.sjs?1", + category: "network", + }, + { + text: "error Bazzle", + category: "error", + }, + { + text: "foo.unknown is not a function", + category: "error", + }, + { + text: "sjs_cors-test-server.sjs?2", + category: "network", + }, + { + text: "info Bazzle", + category: "info", + }, + ].filter(({ category }) => checkNetworkMessage || category != "network"); + + // Clone the original array so we can use it later + const expectedMessages = [...EXPECTED_MESSAGES]; + for (const message of messages) { + const [expectedMessage] = expectedMessages; + if ( + message.classList.contains(expectedMessage.category) && + message.textContent.includes(expectedMessage.text) + ) { + ok( + true, + `The ${expectedMessage.category} message "${expectedMessage.text}" is visible at the expected place` + ); + expectedMessages.shift(); + if (expectedMessages.length === 0) { + ok( + true, + "All the expected messages were found at the expected position" + ); + break; + } + } + } + + if (expectedMessages.length > 0) { + ok( + false, + `Some messages are not visible or not in the expected order. Expected to find: \n\n${EXPECTED_MESSAGES.map( + ({ text }) => text + ).join("\n")}\n\nGot: \n\n${messages + .map(message => `${message.querySelector(".message-body").textContent}`) + .join("\n")}` + ); + } + + // We can't assert the CSS warning position, so we only check that it's visible. + await waitFor( + () => findMessage(hud, "cssColorBug611032", ".message.warn.css"), + "Couldn't find the CSS warning message" + ); + ok(true, "css warning message is visible"); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_cached_messages_cross_domain_iframe.js b/devtools/client/webconsole/test/browser/browser_webconsole_cached_messages_cross_domain_iframe.js new file mode 100644 index 0000000000..51b50504ae --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_cached_messages_cross_domain_iframe.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test to see if retrieving cached messages in a page with a cross-domain iframe does +// not crash the console. + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-iframe-parent.html"; + +add_task(async function() { + // test-iframe-parent has an iframe pointing to http://mochi.test:8888/browser/devtools/client/webconsole/test/browser/test-iframe-child.html + info("Open the tab first"); + await addTab(TEST_URI); + + info("Evaluate an expression that will throw, so we'll have cached messages"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.document.querySelector("button").click(); + }); + + info("Then open the console, to retrieve cached messages"); + await openConsole(); + + // TODO: Make the test fail without the fix. + ok(true, "Everything is okay"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_cached_messages_no_duplicate.js b/devtools/client/webconsole/test/browser/browser_webconsole_cached_messages_no_duplicate.js new file mode 100644 index 0000000000..5f0659ebee --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_cached_messages_no_duplicate.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test to see if we don't get duplicated messages (cached and "live"). +// See Bug 1578138 for more information. + +"use strict"; + +// Log 1 message every 50ms, until we reach 50 messages. +const TEST_URI = `data:text/html,<meta charset=utf8><script> + var i = 0; + var intervalId = setInterval(() => { + if (i >= 50) { + clearInterval(intervalId); + intervalId = null; + return; + } + console.log("startup message " + (++i)); + }, 50); + </script>`; + +add_task(async function() { + info("Add a tab and open the console"); + const tab = await addTab(TEST_URI, { waitForLoad: false }); + const hud = await openConsole(tab); + + info("wait until all the messages are displayed"); + await waitFor( + () => findMessage(hud, "message 1") && findMessage(hud, "message 50") + ); + + is( + findMessages(hud, "startup message").length, + 50, + "We have the expected number of messages" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_certificate_messages.js b/devtools/client/webconsole/test/browser/browser_webconsole_certificate_messages.js new file mode 100644 index 0000000000..c4ba882582 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_certificate_messages.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that the Web Console shows weak crypto warnings (SHA-1 Certificate) + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf8,Web Console weak crypto warnings test"; +const TEST_URI_PATH = + "/browser/devtools/client/webconsole/test/" + + "browser/test-certificate-messages.html"; + +const SHA1_URL = "https://sha1ee.example.com" + TEST_URI_PATH; +const SHA256_URL = "https://sha256ee.example.com" + TEST_URI_PATH; +const TRIGGER_MSG = "If you haven't seen ssl warnings yet, you won't"; +const TLS_1_0_URL = "https://tls1.example.com" + TEST_URI_PATH; + +const TLS_expected_message = + "This site uses a deprecated version of TLS. " + + "Please upgrade to TLS 1.2 or 1.3."; + +registerCleanupFunction(function() { + // Set preferences back to their original values + Services.prefs.clearUserPref("security.tls.version.min"); + Services.prefs.clearUserPref("security.tls.version.max"); +}); + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + info("Test SHA1 warnings"); + let onContentLog = waitForMessage(hud, TRIGGER_MSG); + const onSha1Warning = waitForMessage(hud, "SHA-1"); + await navigateTo(SHA1_URL); + await Promise.all([onContentLog, onSha1Warning]); + + let { textContent } = hud.ui.outputNode; + ok( + !textContent.includes("SSL 3.0"), + "There is no warning message for SSL 3.0" + ); + ok(!textContent.includes("RC4"), "There is no warning message for RC4"); + + info("Test SSL warnings appropriately not present"); + onContentLog = waitForMessage(hud, TRIGGER_MSG); + await navigateTo(SHA256_URL); + await onContentLog; + + textContent = hud.ui.outputNode.textContent; + ok(!textContent.includes("SHA-1"), "There is no warning message for SHA-1"); + ok( + !textContent.includes("SSL 3.0"), + "There is no warning message for SSL 3.0" + ); + ok(!textContent.includes("RC4"), "There is no warning message for RC4"); + ok( + !textContent.includes(TLS_expected_message), + "There is not TLS warning message" + ); + + info("Test TLS warnings"); + // Run with all versions enabled for this test. + Services.prefs.setIntPref("security.tls.version.min", 1); + Services.prefs.setIntPref("security.tls.version.max", 4); + onContentLog = waitForMessage(hud, TRIGGER_MSG); + await navigateTo(TLS_1_0_URL); + await onContentLog; + + textContent = hud.ui.outputNode.textContent; + ok(textContent.includes(TLS_expected_message), "TLS warning message found"); + + Services.cache2.clear(); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_checkloaduri_errors.js b/devtools/client/webconsole/test/browser/browser_webconsole_checkloaduri_errors.js new file mode 100644 index 0000000000..cf9d9dccf2 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_checkloaduri_errors.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Ensure that same-origin errors are logged to the console. + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/test/browser/test-checkloaduri-failure.html"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + const targetURL = "file:///something-weird"; + const onErrorMessage = waitForMessage(hud, "may not load or link"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [targetURL], url => { + XPCNativeWrapper.unwrap(content).testImage(url); + }); + const message = await onErrorMessage; + const node = message.node; + ok( + node.classList.contains("error"), + "The message has the expected classname" + ); + ok( + node.textContent.includes(targetURL), + "The message is about the thing we were expecting" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_clear_cache.js b/devtools/client/webconsole/test/browser/browser_webconsole_clear_cache.js new file mode 100644 index 0000000000..980a6c187c --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_clear_cache.js @@ -0,0 +1,80 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Check that clearing the output also clears the console cache. + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf8,Test clear cache<script>abcdef</script>"; +const EXPECTED_REPORT = "ReferenceError: abcdef is not defined"; + +add_task(async function() { + const tab = await addTab(TEST_URI); + let hud = await openConsole(tab); + + const CACHED_MESSAGE = "CACHED_MESSAGE"; + await logTextToConsole(hud, CACHED_MESSAGE); + + info("Close and re-open the console"); + await closeToolbox(); + hud = await openConsole(tab); + + await waitFor(() => findMessage(hud, EXPECTED_REPORT)); + await waitFor(() => findMessage(hud, CACHED_MESSAGE)); + + info( + "Click the clear output button and wait until there's no messages in the output" + ); + hud.ui.window.document.querySelector(".devtools-clear-icon").click(); + await waitFor(() => findMessages(hud, "").length === 0); + + info("Close and re-open the console"); + await closeToolbox(); + hud = await openConsole(tab); + + info("Log a smoke message in order to know that the console is ready"); + await logTextToConsole(hud, "Smoke message"); + is( + findMessage(hud, CACHED_MESSAGE), + undefined, + "The cached message is not visible anymore" + ); + is( + findMessage(hud, EXPECTED_REPORT), + undefined, + "The cached error message is not visible anymore as well" + ); + + // Test that we also clear the cache when calling console.clear(). + const NEW_CACHED_MESSAGE = "NEW_CACHED_MESSAGE"; + await logTextToConsole(hud, NEW_CACHED_MESSAGE); + + info("Send a console.clear() from the content page"); + const onConsoleCleared = waitForMessage(hud, "Console was cleared"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.wrappedJSObject.console.clear(); + }); + await onConsoleCleared; + + info("Close and re-open the console"); + await closeToolbox(); + hud = await openConsole(tab); + + info("Log a smoke message in order to know that the console is ready"); + await logTextToConsole(hud, "Second smoke message"); + is( + findMessage(hud, NEW_CACHED_MESSAGE), + undefined, + "The new cached message is not visible anymore" + ); +}); + +function logTextToConsole(hud, text) { + const onMessage = waitForMessage(hud, text); + SpecialPowers.spawn(gBrowser.selectedBrowser, [text], function(str) { + content.wrappedJSObject.console.log(str); + }); + return onMessage; +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_click_function_to_mapped_source.js b/devtools/client/webconsole/test/browser/browser_webconsole_click_function_to_mapped_source.js new file mode 100644 index 0000000000..e92f4291d0 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_click_function_to_mapped_source.js @@ -0,0 +1,54 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that clicking on a function in a source-mapped file displays its +// original source in the debugger. See Bug 1433373. + +"use strict"; + +requestLongerTimeout(5); + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/" + + "test-click-function-to-mapped-source.html"; + +const TEST_ORIGINAL_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/" + + "test-click-function-to-source.js"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + info("Log a function"); + const onLoggedFunction = waitForMessage(hud, "function foo"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.foo(); + }); + const { node } = await onLoggedFunction; + const jumpIcon = node.querySelector(".jump-definition"); + ok(jumpIcon, "A jump to definition button is rendered, as expected"); + + info("Click on the jump to definition button."); + jumpIcon.click(); + + info("Wait for the Debugger panel to open."); + const toolbox = hud.toolbox; + await toolbox.getPanelWhenReady("jsdebugger"); + + const dbg = createDebuggerContext(toolbox); + await waitForSelectedSource(dbg, TEST_ORIGINAL_URI); + await waitForSelectedLocation(dbg, 9); + + const pendingLocation = dbg.selectors.getPendingSelectedLocation(); + const { url, line, column } = pendingLocation; + + is(url, TEST_ORIGINAL_URI, "Debugger is open at the expected file"); + is(line, 9, "Debugger is open at the expected line"); + // If we loaded the original file, we'd have column 12 for the function's + // start position, but 9 is correct for the location in the source map. + is(column, 9, "Debugger is open at the expected column"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_click_function_to_prettyprinted_source.js b/devtools/client/webconsole/test/browser/browser_webconsole_click_function_to_prettyprinted_source.js new file mode 100644 index 0000000000..0a67362d8b --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_click_function_to_prettyprinted_source.js @@ -0,0 +1,59 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that clicking on a function in a pretty-printed file displays its +// original source in the debugger. See Bug 1590824. + +"use strict"; + +requestLongerTimeout(5); + +const TEST_ROOT = + "http://example.com/browser/devtools/client/webconsole/test/browser/"; + +const TEST_URI = TEST_ROOT + "test-click-function-to-prettyprinted-source.html"; +const TEST_GENERATED_URI = + TEST_ROOT + "test-click-function-to-source.unmapped.min.js"; +const TEST_PRETTYPRINTED_URI = TEST_GENERATED_URI + ":formatted"; + +add_task(async function() { + await clearDebuggerPreferences(); + + info("Open the console"); + const hud = await openNewTabAndConsole(TEST_URI); + const toolbox = hud.toolbox; + + const onLoggedFunction = waitForMessage(hud, "function foo"); + invokeInTab("foo"); + const { node } = await onLoggedFunction; + const jumpIcon = node.querySelector(".jump-definition"); + ok(jumpIcon, "A jump to definition button is rendered, as expected"); + + info("Click on the jump to definition button"); + jumpIcon.click(); + await toolbox.getPanelWhenReady("jsdebugger"); + const dbg = createDebuggerContext(toolbox); + await waitForSelectedSource(dbg, TEST_GENERATED_URI); + + info("Pretty-print the minified source"); + clickElement(dbg, "prettyPrintButton"); + await waitForSelectedSource(dbg, TEST_PRETTYPRINTED_URI); + + info("Switch back to the console"); + await toolbox.selectTool("webconsole"); + info("Click on the jump to definition button a second time"); + jumpIcon.click(); + + info("Wait for the Debugger panel to open"); + await waitForSelectedSource(dbg, TEST_PRETTYPRINTED_URI); + await waitForSelectedLocation(dbg, 2); + + const location = dbg.selectors.getPendingSelectedLocation(); + // Pretty-printed source maps don't have column positions + const { url, line } = location; + + is(url, TEST_PRETTYPRINTED_URI, "Debugger is open at the expected file"); + is(line, 2, "Debugger is open at the expected line"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_click_function_to_source.js b/devtools/client/webconsole/test/browser/browser_webconsole_click_function_to_source.js new file mode 100644 index 0000000000..2830c58e8b --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_click_function_to_source.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that clicking on a function displays its source in the debugger. See Bug 1050691. + +"use strict"; + +requestLongerTimeout(5); + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/" + + "test-click-function-to-source.html"; + +const TEST_SCRIPT_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/" + + "test-click-function-to-source.js"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + info("Log a function"); + const onLoggedFunction = waitForMessage(hud, "function foo"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.foo(); + }); + const { node } = await onLoggedFunction; + const jumpIcon = node.querySelector(".jump-definition"); + ok(jumpIcon, "A jump to definition button is rendered, as expected"); + + info("Click on the jump to definition button."); + jumpIcon.click(); + + info("Wait for the Debugger panel to open."); + const toolbox = hud.toolbox; + await toolbox.getPanelWhenReady("jsdebugger"); + + const dbg = createDebuggerContext(toolbox); + await waitForSelectedSource(dbg, TEST_SCRIPT_URI); + + const pendingLocation = dbg.selectors.getPendingSelectedLocation(); + const { line, column } = pendingLocation; + is(line, 9, "Debugger is open at the expected line"); + is(column, 12, "Debugger is open at the expected column"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_clickable_urls.js b/devtools/client/webconsole/test/browser/browser_webconsole_clickable_urls.js new file mode 100644 index 0000000000..ccaede645d --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_clickable_urls.js @@ -0,0 +1,101 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// When strings containing URLs are entered into the webconsole, +// ensure that the output can be clicked to open those URLs. +// This test only check that clicking on a link works as expected, +// as the output is already tested in Reps (in Github). + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,Clickable URLS"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const currentTab = gBrowser.selectedTab; + + const firstURL = "http://example.com/"; + const secondURL = "http://example.com/?id=secondURL"; + SpecialPowers.spawn( + gBrowser.selectedBrowser, + [[firstURL, secondURL]], + urls => { + content.wrappedJSObject.console.log("Visit ", urls[0], " and ", urls[1]); + } + ); + + const node = await waitFor(() => findMessage(hud, firstURL)); + const [urlEl1, urlEl2] = Array.from(node.querySelectorAll("a.url")); + + let onTabLoaded = BrowserTestUtils.waitForNewTab(gBrowser, firstURL, true); + + info("Clicking on the first link"); + urlEl1.click(); + + let newTab = await onTabLoaded; + // We only need to check that newTab is truthy since + // BrowserTestUtils.waitForNewTab checks the URL. + ok(newTab, "The expected tab was opened."); + + info("Select the first tab again"); + gBrowser.selectedTab = currentTab; + + info("Ctrl/Cmd + Click on the second link"); + onTabLoaded = BrowserTestUtils.waitForNewTab(gBrowser, secondURL, true); + + const isMacOS = Services.appinfo.OS === "Darwin"; + await EventUtils.sendMouseEvent( + { + type: "click", + [isMacOS ? "metaKey" : "ctrlKey"]: true, + }, + urlEl2, + hud.ui.window + ); + + newTab = await onTabLoaded; + + ok(newTab, "The expected tab was opened."); + is( + newTab._tPos, + currentTab._tPos + 1, + "The new tab was opened in the position to the right of the current tab" + ); + is(gBrowser.selectedTab, currentTab, "The tab was opened in the background"); + + info( + "Test that Ctrl/Cmd + Click on a link in an array doesn't open the sidebar" + ); + const onMessage = waitForMessage(hud, "Visit"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [firstURL], url => { + content.wrappedJSObject.console.log([`Visit ${url}`]); + }); + const message = await onMessage; + const urlEl3 = message.node.querySelector("a.url"); + + onTabLoaded = BrowserTestUtils.waitForNewTab(gBrowser, firstURL, true); + + AccessibilityUtils.setEnv({ + // Focusable element is put back in focus order when its container row is in + // focused/active state. + nonNegativeTabIndexRule: false, + }); + await EventUtils.sendMouseEvent( + { + type: "click", + [isMacOS ? "metaKey" : "ctrlKey"]: true, + }, + urlEl3, + hud.ui.window + ); + AccessibilityUtils.resetEnv(); + await onTabLoaded; + + info("Log a message and wait for it to appear so we know the UI was updated"); + const onSmokeMessage = waitForMessage(hud, "smoke"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.wrappedJSObject.console.log("smoke"); + }); + await onSmokeMessage; + ok(!hud.ui.document.querySelector(".sidebar"), "Sidebar wasn't closed"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_close_groups_after_navigation.js b/devtools/client/webconsole/test/browser/browser_webconsole_close_groups_after_navigation.js new file mode 100644 index 0000000000..a231fa8179 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_close_groups_after_navigation.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=utf8,<script>console.group('hello')</script>`; + +add_task(async function() { + // Enable persist logs + await pushPref("devtools.webconsole.persistlog", true); + + const hud = await openNewTabAndConsole(TEST_URI); + + info("Refresh tab several times and check for correct message indentation"); + for (let i = 0; i < 5; i++) { + await refreshTabAndCheckIndent(hud); + } +}); + +async function refreshTabAndCheckIndent(hud) { + const onMessage = waitForMessage(hud, "hello"); + await refreshTab(); + const { node } = await onMessage; + + is( + node.querySelector(".indent").getAttribute("data-indent"), + "0", + "The message has the expected indent" + ); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_close_sidebar.js b/devtools/client/webconsole/test/browser/browser_webconsole_close_sidebar.js new file mode 100644 index 0000000000..bfafb89e71 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_close_sidebar.js @@ -0,0 +1,96 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the sidebar is hidden for all methods of closing it. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,"; + +add_task(async function() { + // Should be removed when sidebar work is complete + await pushPref("devtools.webconsole.sidebarToggle", true); + + const hud = await openNewTabAndConsole(TEST_URI); + await showSidebar(hud); + + info("Click the clear console button"); + const clearButton = hud.ui.document.querySelector(".devtools-button"); + clearButton.click(); + await waitFor(() => findMessages(hud, "").length == 0); + let sidebar = hud.ui.document.querySelector(".sidebar"); + ok(!sidebar, "Sidebar hidden after clear console button clicked"); + + await showSidebar(hud); + + info("Send a console.clear()"); + const onMessagesCleared = waitForMessage(hud, "Console was cleared"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.console.clear(); + }); + await onMessagesCleared; + sidebar = hud.ui.document.querySelector(".sidebar"); + ok(!sidebar, "Sidebar hidden after console.clear()"); + + await showSidebar(hud); + + info("Send ctrl-l to clear console"); + let clearShortcut; + if (Services.appinfo.OS === "Darwin") { + clearShortcut = WCUL10n.getStr("webconsole.clear.keyOSX"); + } else { + clearShortcut = WCUL10n.getStr("webconsole.clear.key"); + } + synthesizeKeyShortcut(clearShortcut); + await waitFor(() => findMessages(hud, "").length == 0); + sidebar = hud.ui.document.querySelector(".sidebar"); + ok(!sidebar, "Sidebar hidden after ctrl-l"); + + await showSidebar(hud); + + info("Click the close button"); + const closeButton = hud.ui.document.querySelector(".sidebar-close-button"); + const appNode = hud.ui.document.querySelector(".webconsole-app"); + let onSidebarShown = waitForNodeMutation(appNode, { childList: true }); + closeButton.click(); + await onSidebarShown; + sidebar = hud.ui.document.querySelector(".sidebar"); + ok(!sidebar, "Sidebar hidden after clicking on close button"); + + await showSidebar(hud); + + info("Send escape to hide sidebar"); + onSidebarShown = waitForNodeMutation(appNode, { childList: true }); + EventUtils.synthesizeKey("KEY_Escape"); + await onSidebarShown; + sidebar = hud.ui.document.querySelector(".sidebar"); + ok(!sidebar, "Sidebar hidden after sending esc"); + ok(isInputFocused(hud), "console input is focused after closing the sidebar"); +}); + +async function showSidebar(hud) { + const onMessage = waitForMessage(hud, "Object"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.console.log({ a: 1 }); + }); + await onMessage; + + const objectNode = hud.ui.outputNode.querySelector( + ".object-inspector .objectBox" + ); + const appNode = hud.ui.document.querySelector(".webconsole-app"); + const onSidebarShown = waitForNodeMutation(appNode, { childList: true }); + + const contextMenu = await openContextMenu(hud, objectNode); + const openInSidebar = contextMenu.querySelector("#console-menu-open-sidebar"); + openInSidebar.click(); + await onSidebarShown; + await hideContextMenu(hud); + + // Let's wait for the object inside the sidebar to be expanded. + await waitFor( + () => appNode.querySelectorAll(".sidebar .tree-node").length > 1, + null, + 100 + ); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_close_unfocused_window.js b/devtools/client/webconsole/test/browser/browser_webconsole_close_unfocused_window.js new file mode 100644 index 0000000000..8aa35f37a1 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_close_unfocused_window.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// See Bug 597103. Check that closing the console on an unfocused window does not trigger +// any error. + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-console.html"; + +add_task(async function() { + const tab1 = await addTab(TEST_URI, { window }); + + info("Open a second window"); + const win2 = await BrowserTestUtils.openNewBrowserWindow(); + + info("Add a test tab in the second window"); + const tab2 = await addTab(TEST_URI, { window: win2 }); + win2.gBrowser.selectedTab = tab2; + + info("Open console in tabs located in different windows"); + await openConsole(tab1); + await openConsole(tab2); + + info( + "Close toolboxes in tabs located in different windows, one of them not focused" + ); + await closeToolboxForTab(tab1); + await closeToolboxForTab(tab2); + + info("Close the second window"); + win2.close(); + + info("Close the test tab in the first window"); + window.gBrowser.removeTab(tab1); + + ok(true, "No error was triggered during the test"); +}); + +async function closeToolboxForTab(tab) { + const target = await TargetFactory.forTab(tab); + return gDevTools.closeToolbox(target); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_closing_after_completion.js b/devtools/client/webconsole/test/browser/browser_webconsole_closing_after_completion.js new file mode 100644 index 0000000000..145f0a7ad7 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_closing_after_completion.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests to ensure that errors don't appear when the console is closed while a +// completion is being performed. See Bug 580001. + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-console.html"; + +add_task(async function() { + const tab = await addTab(TEST_URI); + const browser = tab.linkedBrowser; + const hud = await openConsole(); + + // Fire a completion. + await setInputValueForAutocompletion(hud, "doc"); + + let errorWhileClosing = false; + function errorListener() { + errorWhileClosing = true; + } + + browser.addEventListener("error", errorListener); + const onToolboxDestroyed = gDevTools.once("toolbox-destroyed"); + + // Focus the jsterm and perform the keycombo to close the WebConsole. + hud.jsterm.focus(); + EventUtils.synthesizeKey("i", { + accelKey: true, + [Services.appinfo.OS == "Darwin" ? "altKey" : "shiftKey"]: true, + }); + + await onToolboxDestroyed; + + browser.removeEventListener("error", errorListener); + is(errorWhileClosing, false, "no error while closing the WebConsole"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_console_api_iframe.js b/devtools/client/webconsole/test/browser/browser_webconsole_console_api_iframe.js new file mode 100644 index 0000000000..f5e23a182b --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_console_api_iframe.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that Console API works with iframes. See Bug 613013. + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-console-api-iframe.html"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const loggedString = "iframe added"; + // Wait for the initial message to be displayed. + await waitFor(() => findMessage(hud, loggedString)); + ok(true, "The initial message is displayed in the console"); + // Create a promise for the message logged after the reload. + const onMessage = waitForMessage(hud, loggedString); + BrowserReload(); + await onMessage; + ok(true, "The message is also displayed after a page reload"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_console_dir.js b/devtools/client/webconsole/test/browser/browser_webconsole_console_dir.js new file mode 100644 index 0000000000..f041543487 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_console_dir.js @@ -0,0 +1,134 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check console.dir() calls. +const TEST_URI = "data:text/html;charset=utf8,<h1>test console.dir</h1>"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + logAllStoreChanges(hud); + + info("console.dir on an array"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.console.dir([1, 2, { a: "a", b: "b" }]); + }); + let dirMessageNode = await waitFor(() => + findConsoleDir(hud.ui.outputNode, 0) + ); + let objectInspectors = [...dirMessageNode.querySelectorAll(".tree")]; + is( + objectInspectors.length, + 1, + "There is the expected number of object inspectors" + ); + const [arrayOi] = objectInspectors; + let arrayOiNodes = arrayOi.querySelectorAll(".node"); + // The tree can be collapsed since the properties are fetched asynchronously. + if (arrayOiNodes.length === 1) { + // If this is the case, we wait for the properties to be fetched and displayed. + await waitForNodeMutation(arrayOi, { + childList: true, + }); + arrayOiNodes = arrayOi.querySelectorAll(".node"); + } + + // There are 6 nodes: the root, 1, 2, {a: "a", b: "b"}, length and the proto. + is( + arrayOiNodes.length, + 6, + "There is the expected number of nodes in the tree" + ); + let propertiesNodes = [...arrayOi.querySelectorAll(".object-label")].map( + el => el.textContent + ); + const arrayPropertiesNames = ["0", "1", "2", "length", "<prototype>"]; + is(JSON.stringify(propertiesNodes), JSON.stringify(arrayPropertiesNames)); + + info("console.dir on a long object"); + const obj = Array.from({ length: 100 }).reduce((res, _, i) => { + res["item-" + (i + 1).toString().padStart(3, "0")] = i + 1; + return res; + }, {}); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [obj], function(data) { + content.wrappedJSObject.console.dir(data); + }); + dirMessageNode = await waitFor(() => findConsoleDir(hud.ui.outputNode, 1)); + objectInspectors = [...dirMessageNode.querySelectorAll(".tree")]; + is( + objectInspectors.length, + 1, + "There is the expected number of object inspectors" + ); + const [objectOi] = objectInspectors; + let objectOiNodes = objectOi.querySelectorAll(".node"); + // The tree can be collapsed since the properties are fetched asynchronously. + if (objectOiNodes.length === 1) { + // If this is the case, we wait for the properties to be fetched and displayed. + await waitForNodeMutation(objectOi, { + childList: true, + }); + objectOiNodes = objectOi.querySelectorAll(".node"); + } + + // There are 102 nodes: the root, 100 "item-N" properties, and the proto. + is( + objectOiNodes.length, + 102, + "There is the expected number of nodes in the tree" + ); + const objectPropertiesNames = Object.getOwnPropertyNames(obj).map( + name => `"${name}"` + ); + objectPropertiesNames.push("<prototype>"); + propertiesNodes = [...objectOi.querySelectorAll(".object-label")].map( + el => el.textContent + ); + is(JSON.stringify(propertiesNodes), JSON.stringify(objectPropertiesNames)); + + info("console.dir on an error object"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + const err = new Error("myErrorMessage"); + err.myCustomProperty = "myCustomPropertyValue"; + content.wrappedJSObject.console.dir(err); + }); + dirMessageNode = await waitFor(() => findConsoleDir(hud.ui.outputNode, 2)); + objectInspectors = [...dirMessageNode.querySelectorAll(".tree")]; + is( + objectInspectors.length, + 1, + "There is the expected number of object inspectors" + ); + const [errorOi] = objectInspectors; + let errorOiNodes = errorOi.querySelectorAll(".node"); + // The tree can be collapsed since the properties are fetched asynchronously. + if (errorOiNodes.length === 1) { + // If this is the case, we wait for the properties to be fetched and displayed. + await waitForNodeMutation(errorOi, { + childList: true, + }); + errorOiNodes = errorOi.querySelectorAll(".node"); + } + + propertiesNodes = [...errorOi.querySelectorAll(".object-label")].map( + el => el.textContent + ); + is( + JSON.stringify(propertiesNodes), + JSON.stringify([ + "columnNumber", + "fileName", + "lineNumber", + "message", + "myCustomProperty", + "stack", + "<prototype>", + ]) + ); +}); + +function findConsoleDir(node, index) { + return node.querySelectorAll(".dir.message")[index]; +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_console_dir_uninspectable.js b/devtools/client/webconsole/test/browser/browser_webconsole_console_dir_uninspectable.js new file mode 100644 index 0000000000..255d9638be --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_console_dir_uninspectable.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Make sure that the Web Console output does not break after we try to call +// console.dir() for objects that are not inspectable. + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf8,test console.dir on uninspectable object"; +const FIRST_LOG_MESSAGE = "fooBug773466a"; +const SECOND_LOG_MESSAGE = "fooBug773466b"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + info("Logging a first message to make sure everything is working"); + await executeAndWaitForMessage( + hud, + `console.log("${FIRST_LOG_MESSAGE}")`, + FIRST_LOG_MESSAGE, + ".message.log" + ); + + info("console.dir on an uninspectable object"); + await executeAndWaitForMessage( + hud, + "console.dir(Object.create(null))", + "Object { }" + ); + + info("Logging a second message to make sure the console is not broken"); + const onLogMessage = waitForMessage(hud, SECOND_LOG_MESSAGE); + // Logging from content to make sure the console API is working. + SpecialPowers.spawn( + gBrowser.selectedBrowser, + [SECOND_LOG_MESSAGE], + string => { + content.console.log(string); + } + ); + await onLogMessage; + + ok( + true, + "The console.dir call on an uninspectable object did not break the console" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_console_error_expand_object.js b/devtools/client/webconsole/test/browser/browser_webconsole_console_error_expand_object.js new file mode 100644 index 0000000000..8811ab4109 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_console_error_expand_object.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check console.error calls with expandable object. +const TEST_URI = + "data:text/html;charset=utf8,<h1>test console.error with objects</h1>"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + const onMessagesLogged = waitForMessage(hud, "myError"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.console.error("myError", { a: "a", b: "b" }); + }); + const { node } = await onMessagesLogged; + + const objectInspectors = [...node.querySelectorAll(".tree")]; + is( + objectInspectors.length, + 1, + "There is the expected number of object inspectors" + ); + const [oi] = objectInspectors; + oi.querySelector(".node .arrow").click(); + await waitFor(() => oi.querySelectorAll(".node").length > 1); + ok(true, "The object can be expanded"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_console_group.js b/devtools/client/webconsole/test/browser/browser_webconsole_console_group.js new file mode 100644 index 0000000000..ec95615856 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_console_group.js @@ -0,0 +1,153 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check console.group, console.groupCollapsed and console.groupEnd calls +// behave as expected. + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-console-group.html"; +const { + INDENT_WIDTH, +} = require("devtools/client/webconsole/components/Output/MessageIndent"); + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const store = hud.ui.wrapper.getStore(); + logAllStoreChanges(hud); + + const onMessagesLogged = waitForMessage(hud, "log-6"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.doLog(); + }); + await onMessagesLogged; + + info("Test a group at root level"); + let node = findMessage(hud, "group-1"); + testClass(node, "startGroup"); + testIndent(node, 0); + await testGroupToggle({ + node, + store, + shouldBeOpen: true, + visibleMessageIdsAfterExpand: ["1", "2", "3", "4", "6", "8", "9", "12"], + visibleMessageIdsAfterCollapse: ["1", "8", "9", "12"], + }); + + info("Test a message in a 1 level deep group"); + node = findMessage(hud, "log-1"); + testClass(node, "log"); + testIndent(node, 1); + + info("Test a group in a 1 level deep group"); + node = findMessage(hud, "group-2"); + testClass(node, "startGroup"); + testIndent(node, 1); + await testGroupToggle({ + node, + store, + shouldBeOpen: true, + visibleMessageIdsAfterExpand: ["1", "2", "3", "4", "6", "8", "9", "12"], + visibleMessageIdsAfterCollapse: ["1", "2", "3", "6", "8", "9", "12"], + }); + + info("Test a message in a 2 level deep group"); + node = findMessage(hud, "log-2"); + testClass(node, "log"); + testIndent(node, 2); + + info( + "Test a message in a 1 level deep group, after closing a 2 level deep group" + ); + node = findMessage(hud, "log-3"); + testClass(node, "log"); + testIndent(node, 1); + + info("Test a message at root level, after closing all the groups"); + node = findMessage(hud, "log-4"); + testClass(node, "log"); + testIndent(node, 0); + + info("Test a collapsed group at root level"); + node = findMessage(hud, "group-3"); + testClass(node, "startGroupCollapsed"); + testIndent(node, 0); + await testGroupToggle({ + node, + store, + shouldBeOpen: false, + visibleMessageIdsAfterExpand: [ + "1", + "2", + "3", + "4", + "6", + "8", + "9", + "10", + "12", + ], + visibleMessageIdsAfterCollapse: ["1", "2", "3", "4", "6", "8", "9", "12"], + }); + + info("Test a message at root level, after closing a collapsed group"); + node = findMessage(hud, "log-6"); + testClass(node, "log"); + testIndent(node, 0); + const nodes = hud.ui.outputNode.querySelectorAll(".message"); + is(nodes.length, 8, "expected number of messages are displayed"); +}); + +function testClass(node, className) { + ok( + node.classList.contains(className), + `message has the expected "${className}" class` + ); +} + +function testIndent(node, indent) { + indent = `${indent * INDENT_WIDTH}px`; + is( + node.querySelector(".indent").style.width, + indent, + "message has the expected level of indentation" + ); +} + +async function testGroupToggle({ + node, + store, + shouldBeOpen, + visibleMessageIdsAfterExpand, + visibleMessageIdsAfterCollapse, +}) { + const toggleArrow = node.querySelector(".collapse-button"); + const isOpen = node2 => node2.classList.contains("open"); + const assertVisibleMessageIds = expanded => { + const visibleMessageIds = store.getState().messages.visibleMessages; + expanded + ? is( + visibleMessageIds.toString(), + visibleMessageIdsAfterExpand.toString() + ) + : is( + visibleMessageIds.toString(), + visibleMessageIdsAfterCollapse.toString() + ); + }; + + await waitFor(() => isOpen(node) === shouldBeOpen); + assertVisibleMessageIds(shouldBeOpen); + + toggleArrow.click(); + shouldBeOpen = !shouldBeOpen; + await waitFor(() => isOpen(node) === shouldBeOpen); + assertVisibleMessageIds(shouldBeOpen); + + toggleArrow.click(); + shouldBeOpen = !shouldBeOpen; + await waitFor(() => isOpen(node) === shouldBeOpen); + assertVisibleMessageIds(shouldBeOpen); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_console_group_open_no_scroll.js b/devtools/client/webconsole/test/browser/browser_webconsole_console_group_open_no_scroll.js new file mode 100644 index 0000000000..1e0def28f9 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_console_group_open_no_scroll.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that opening a group does not scroll the console output. + +const TEST_URI = `data:text/html,<meta charset=utf8><script> + Array.from({length: 100}, (_, i) => console.log("log-"+i)); + console.groupCollapsed("GROUP"); + console.log("in group"); +</script>`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const outputScroller = hud.ui.outputScroller; + + // Let's wait until the first message and the group are displayed. + await waitFor(() => findMessage(hud, "log-0")); + const groupMessage = await waitFor(() => findMessage(hud, "GROUP")); + + is(hasVerticalOverflow(outputScroller), true, "output node overflows"); + is( + isScrolledToBottom(outputScroller), + true, + "output node is scrolled to the bottom" + ); + + info("Expand the group"); + groupMessage.querySelector(".arrow").click(); + await waitFor(() => findMessage(hud, "in group")); + + is(hasVerticalOverflow(outputScroller), true, "output node overflows"); + is( + isScrolledToBottom(outputScroller), + false, + "output node isn't scrolled to the bottom anymore" + ); + + info("Scroll to bottom"); + outputScroller.scrollTop = outputScroller.scrollHeight; + is( + isScrolledToBottom(outputScroller), + true, + "output node is scrolled to the bottom" + ); + + info( + "Check that adding a message on an open group when scrolled to bottom scrolls " + + "to bottom" + ); + const onNewMessage = waitForMessage(hud, "new-message"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.console.group("GROUP-2"); + content.console.log("new-message"); + }); + await onNewMessage; + is( + isScrolledToBottom(outputScroller), + true, + "output node is scrolled to the bottom after adding message in group" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_console_logging_workers_api.js b/devtools/client/webconsole/test/browser/browser_webconsole_console_logging_workers_api.js new file mode 100644 index 0000000000..1bf1bf7601 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_console_logging_workers_api.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that the basic console.log() works for workers + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-console-workers.html"; + +add_task(async function() { + info("Run the test with worker events dispatched to main thread"); + await pushPref("dom.worker.console.dispatch_events_to_main_thread", true); + await testWorkerMessage(); + + info("Run the test with worker events NOT dispatched to main thread"); + await pushPref("dom.worker.console.dispatch_events_to_main_thread", false); + await testWorkerMessage(true); +}); + +async function testWorkerMessage(directConnectionToWorkerThread = false) { + const hud = await openNewTabAndConsole(TEST_URI); + + const cachedMessage = await waitFor(() => + findMessage(hud, "initial-message-from-worker") + ); + ok(true, "We get the cached message from the worker"); + + ok( + cachedMessage + .querySelector(".message-body") + .textContent.includes(`Object { foo: "bar" }`), + "The simple object is logged as expected" + ); + + if (directConnectionToWorkerThread) { + const scopeOi = cachedMessage.querySelector( + ".object-inspector:last-of-type" + ); + ok( + scopeOi.textContent.includes( + `DedicatedWorkerGlobalScope {`, + `The worker scope is logged as expected: ${scopeOi.textContent}` + ) + ); + } + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.wrappedJSObject.logFromWorker("live-message"); + }); + + const liveMessage = await waitFor(() => findMessage(hud, "log-from-worker")); + ok(true, "We get the cached message from the worker"); + + ok( + liveMessage + .querySelector(".message-body") + .textContent.includes(`live-message`), + "The message is logged as expected" + ); + + if (directConnectionToWorkerThread) { + const scopeOi = liveMessage.querySelector(".object-inspector:last-of-type"); + ok( + scopeOi.textContent.includes( + `DedicatedWorkerGlobalScope {`, + `The worker scope is logged as expected: ${scopeOi.textContent}` + ) + ); + + info("Check that Symbol are properly logged"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.wrappedJSObject.logFromWorker("live-message"); + }); + + const symbolMessage = await waitFor(() => + findMessage(hud, "Symbol(logged-symbol-from-worker)") + ); + ok(symbolMessage, "Symbol logged from worker is visible in the console"); + } + + const onMessagesCleared = hud.ui.once("messages-cleared"); + await clearOutput(hud); + await onMessagesCleared; +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_console_table.js b/devtools/client/webconsole/test/browser/browser_webconsole_console_table.js new file mode 100644 index 0000000000..8030dd3398 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_console_table.js @@ -0,0 +1,510 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check console.table calls with all the test cases shown +// in the MDN doc (https://developer.mozilla.org/en-US/docs/Web/API/Console/table) + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-console-table.html"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + function Person(firstName, lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + const holeyArray = []; + holeyArray[1] = "apples"; + holeyArray[3] = "oranges"; + holeyArray[6] = "bananas"; + + const testCases = [ + { + info: "Testing when data argument is an array", + input: ["apples", "oranges", "bananas"], + expected: { + columns: ["(index)", "Values"], + rows: [ + ["0", "apples"], + ["1", "oranges"], + ["2", "bananas"], + ], + }, + }, + { + info: "Testing when data argument is an holey array", + input: holeyArray, + expected: { + columns: ["(index)", "Values"], + rows: [ + ["0", ""], + ["1", "apples"], + ["2", ""], + ["3", "oranges"], + ["4", ""], + ["5", ""], + ["6", "bananas"], + ], + }, + }, + { + info: "Testing when data argument has holey array", + // eslint-disable-next-line no-sparse-arrays + input: [[1, , 2]], + expected: { + columns: ["(index)", "0", "1", "2"], + rows: [["0", "1", "", "2"]], + }, + }, + { + info: "Testing when data argument is an object", + input: new Person("John", "Smith"), + expected: { + columns: ["(index)", "Values"], + rows: [ + ["firstName", "John"], + ["lastName", "Smith"], + ], + }, + }, + { + info: "Testing when data argument is an array of arrays", + input: [ + ["Jane", "Doe"], + ["Emily", "Jones"], + ], + expected: { + columns: ["(index)", "0", "1"], + rows: [ + ["0", "Jane", "Doe"], + ["1", "Emily", "Jones"], + ], + }, + }, + { + info: "Testing when data argument is an array of objects", + input: [ + new Person("Jack", "Foo"), + new Person("Emma", "Bar"), + new Person("Michelle", "Rax"), + ], + expected: { + columns: ["(index)", "firstName", "lastName"], + rows: [ + ["0", "Jack", "Foo"], + ["1", "Emma", "Bar"], + ["2", "Michelle", "Rax"], + ], + }, + }, + { + info: + "Testing when data argument is an object whose properties are objects", + input: { + father: new Person("Darth", "Vader"), + daughter: new Person("Leia", "Organa"), + son: new Person("Luke", "Skywalker"), + }, + expected: { + columns: ["(index)", "firstName", "lastName"], + rows: [ + ["father", "Darth", "Vader"], + ["daughter", "Leia", "Organa"], + ["son", "Luke", "Skywalker"], + ], + }, + }, + { + info: "Testing when data argument is a Set", + input: new Set(["a", "b", "c"]), + expected: { + columns: ["(iteration index)", "Values"], + rows: [ + ["0", "a"], + ["1", "b"], + ["2", "c"], + ], + }, + }, + { + info: "Testing when data argument is a Map", + input: new Map([ + ["key-a", "value-a"], + ["key-b", "value-b"], + ]), + expected: { + columns: ["(iteration index)", "Key", "Values"], + rows: [ + ["0", "key-a", "value-a"], + ["1", "key-b", "value-b"], + ], + }, + }, + { + info: "Testing when data argument is a Int8Array", + input: new Int8Array([1, 2, 3, 4]), + expected: { + columns: ["(index)", "Values"], + rows: [ + ["0", "1"], + ["1", "2"], + ["2", "3"], + ["3", "4"], + ], + }, + }, + { + info: "Testing when data argument is a Uint8Array", + input: new Uint8Array([1, 2, 3, 4]), + expected: { + columns: ["(index)", "Values"], + rows: [ + ["0", "1"], + ["1", "2"], + ["2", "3"], + ["3", "4"], + ], + }, + }, + { + info: "Testing when data argument is a Int16Array", + input: new Int16Array([1, 2, 3, 4]), + expected: { + columns: ["(index)", "Values"], + rows: [ + ["0", "1"], + ["1", "2"], + ["2", "3"], + ["3", "4"], + ], + }, + }, + { + info: "Testing when data argument is a Uint16Array", + input: new Uint16Array([1, 2, 3, 4]), + expected: { + columns: ["(index)", "Values"], + rows: [ + ["0", "1"], + ["1", "2"], + ["2", "3"], + ["3", "4"], + ], + }, + }, + { + info: "Testing when data argument is a Int32Array", + input: new Int32Array([1, 2, 3, 4]), + expected: { + columns: ["(index)", "Values"], + rows: [ + ["0", "1"], + ["1", "2"], + ["2", "3"], + ["3", "4"], + ], + }, + }, + { + info: "Testing when data argument is a Uint32Array", + input: new Uint32Array([1, 2, 3, 4]), + expected: { + columns: ["(index)", "Values"], + rows: [ + ["0", "1"], + ["1", "2"], + ["2", "3"], + ["3", "4"], + ], + }, + }, + { + info: "Testing when data argument is a Float32Array", + input: new Float32Array([1, 2, 3, 4]), + expected: { + columns: ["(index)", "Values"], + rows: [ + ["0", "1"], + ["1", "2"], + ["2", "3"], + ["3", "4"], + ], + }, + }, + { + info: "Testing when data argument is a Float64Array", + input: new Float64Array([1, 2, 3, 4]), + expected: { + columns: ["(index)", "Values"], + rows: [ + ["0", "1"], + ["1", "2"], + ["2", "3"], + ["3", "4"], + ], + }, + }, + { + info: "Testing when data argument is a Uint8ClampedArray", + input: new Uint8ClampedArray([1, 2, 3, 4]), + expected: { + columns: ["(index)", "Values"], + rows: [ + ["0", "1"], + ["1", "2"], + ["2", "3"], + ["3", "4"], + ], + }, + }, + { + info: "Testing when data argument is a BigInt64Array", + // eslint-disable-next-line no-undef + input: new BigInt64Array([1n, 2n, 3n, 4n]), + expected: { + columns: ["(index)", "Values"], + rows: [ + ["0", "1n"], + ["1", "2n"], + ["2", "3n"], + ["3", "4n"], + ], + }, + }, + { + info: "Testing when data argument is a BigUint64Array", + // eslint-disable-next-line no-undef + input: new BigUint64Array([1n, 2n, 3n, 4n]), + expected: { + columns: ["(index)", "Values"], + rows: [ + ["0", "1n"], + ["1", "2n"], + ["2", "3n"], + ["3", "4n"], + ], + }, + }, + { + info: "Testing restricting the columns displayed", + input: [new Person("Sam", "Wright"), new Person("Elena", "Bartz")], + headers: ["firstName"], + expected: { + columns: ["(index)", "firstName"], + rows: [ + ["0", "Sam"], + ["1", "Elena"], + ], + }, + }, + { + info: "Testing nested object with falsy values", + input: [ + { a: null, b: false, c: undefined, d: 0 }, + { b: null, c: false, d: undefined, e: 0 }, + ], + expected: { + columns: ["(index)", "a", "b", "c", "d", "e"], + rows: [ + ["0", "null", "false", "undefined", "0", ""], + ["1", "", "null", "false", "undefined", "0"], + ], + }, + }, + { + info: "Testing invalid headers", + input: ["apples", "oranges", "bananas"], + headers: [[]], + expected: { + columns: ["(index)", "Values"], + rows: [ + ["0", "apples"], + ["1", "oranges"], + ["2", "bananas"], + ], + }, + }, + { + info: "Testing overflow-y", + input: Array.from({ length: 50 }, (_, i) => `item-${i}`), + expected: { + columns: ["(index)", "Values"], + rows: Array.from({ length: 50 }, (_, i) => [i.toString(), `item-${i}`]), + overflow: true, + }, + }, + { + info: "Testing table with expandable objects", + input: [{ a: { b: 34 } }], + expected: { + columns: ["(index)", "a"], + rows: [["0", "Object { b: 34 }"]], + }, + additionalTest: async function(node) { + info("Check that object in a cell can be expanded"); + const objectNode = node.querySelector(".tree .node"); + objectNode.click(); + await waitFor(() => node.querySelectorAll(".tree .node").length === 3); + const nodes = node.querySelectorAll(".tree .node"); + ok(nodes[1].textContent.includes("b: 34")); + ok(nodes[2].textContent.includes("<prototype>")); + }, + }, + { + info: "Testing max columns", + input: [ + Array.from({ length: 30 }).reduce((acc, _, i) => { + return { + ...acc, + ["item" + i]: i, + }; + }, {}), + ], + expected: { + // We show 21 columns at most + columns: [ + "(index)", + ...Array.from({ length: 20 }, (_, i) => `item${i}`), + ], + rows: [[0, ...Array.from({ length: 20 }, (_, i) => i)]], + }, + }, + { + info: "Testing performance entries", + input: "PERFORMANCE_ENTRIES", + headers: [ + "name", + "entryType", + "initiatorType", + "connectStart", + "connectEnd", + "fetchStart", + ], + expected: { + columns: [ + "(index)", + "initiatorType", + "fetchStart", + "connectStart", + "connectEnd", + "name", + "entryType", + ], + rows: [[0, "navigation", /\d+/, /\d+/, /\d+/, TEST_URI, "navigation"]], + }, + }, + ]; + + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [testCases.map(({ input, headers }) => ({ input, headers }))], + function(tests) { + tests.forEach(test => { + let { input, headers } = test; + if (input === "PERFORMANCE_ENTRIES") { + input = content.wrappedJSObject.performance.getEntriesByType( + "navigation" + ); + } + content.wrappedJSObject.doConsoleTable(input, headers); + }); + } + ); + const nodes = []; + for (const testCase of testCases) { + const node = await waitFor(() => + findConsoleTable(hud.ui.outputNode, testCases.indexOf(testCase)) + ); + nodes.push(node); + } + const consoleTableNodes = hud.ui.outputNode.querySelectorAll( + ".message .new-consoletable" + ); + is( + consoleTableNodes.length, + testCases.length, + "console has the expected number of consoleTable items" + ); + + for (const [index, testCase] of testCases.entries()) { + await testItem(testCase, nodes[index]); + } +}); + +async function testItem(testCase, node) { + info(testCase.info); + + const columns = Array.from(node.querySelectorAll("[role=columnheader]")); + const columnsNumber = columns.length; + const cells = Array.from(node.querySelectorAll("[role=gridcell]")); + + is( + JSON.stringify(columns.map(column => column.textContent)), + JSON.stringify(testCase.expected.columns), + `${testCase.info} | table has the expected columns` + ); + + // We don't really have rows since we are using a CSS grid in order to have a sticky + // header on the table. So we check the "rows" by dividing the number of cells by the + // number of columns. + is( + cells.length / columnsNumber, + testCase.expected.rows.length, + `${testCase.info} | table has the expected number of rows` + ); + + testCase.expected.rows.forEach((expectedRow, rowIndex) => { + const startIndex = rowIndex * columnsNumber; + // Slicing the cells array so we can get the current "row". + const rowCells = cells + .slice(startIndex, startIndex + columnsNumber) + .map(x => x.textContent); + + const isRegex = x => x && x.constructor.name === "RegExp"; + const hasRegExp = expectedRow.find(isRegex); + if (hasRegExp) { + is( + rowCells.length, + expectedRow.length, + `${testCase.info} | row ${rowIndex} has the expected number of cell` + ); + rowCells.forEach((cell, i) => { + const expected = expectedRow[i]; + const info = `${testCase.info} | row ${rowIndex} cell ${i} has the expected content`; + + if (isRegex(expected)) { + ok(expected.test(cell), info); + } else { + is(cell, `${expected}`, info); + } + }); + } else { + is( + rowCells.join(" | "), + expectedRow.join(" | "), + `${testCase.info} | row has the expected content` + ); + } + }); + + if (testCase.expected.overflow) { + ok(node.scrollHeight > node.clientHeight, "table overflows"); + ok(getComputedStyle(node).overflowY !== "hidden", "table can be scrolled"); + } + + if (typeof testCase.additionalTest === "function") { + await testCase.additionalTest(node); + } +} + +function findConsoleTable(node, index) { + const condition = node.querySelector( + `.message:nth-of-type(${index + 1}) .new-consoletable` + ); + return condition; +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_console_table_post_alterations.js b/devtools/client/webconsole/test/browser/browser_webconsole_console_table_post_alterations.js new file mode 100644 index 0000000000..cbbe4188bb --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_console_table_post_alterations.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that calling console.table on a variable which is modified after the +// console.table call only shows data for when the variable was logged. + +const TEST_URI = `data:text/html,Test console.table with modified variable`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + await ContentTask.spawn(gBrowser.selectedBrowser, null, () => { + const x = ["a", "b"]; + content.wrappedJSObject.console.table(x); + x.push("c"); + content.wrappedJSObject.console.table(x); + x.sort((a, b) => b - a); + content.wrappedJSObject.console.table(x); + }); + + const [table1, table2, table3] = await waitFor(() => { + const res = hud.ui.outputNode.querySelectorAll( + ".message .new-consoletable" + ); + if (res.length === 3) { + return res; + } + return null; + }); + + info("Check the rows of the first table"); + checkTable(table1, [ + [0, "a"], + [1, "b"], + ]); + + info("Check the rows of the table after adding an element to the array"); + checkTable(table2, [ + [0, "a"], + [1, "b"], + [2, "c"], + ]); + + info("Check the rows of the table after sorting the array"); + checkTable(table3, [ + [0, "c"], + [1, "b"], + [2, "a"], + ]); +}); + +function checkTable(node, expectedRows) { + const columns = Array.from(node.querySelectorAll("[role=columnheader]")); + const columnsNumber = columns.length; + const cells = Array.from(node.querySelectorAll("[role=gridcell]")); + + // We don't really have rows since we are using a CSS grid in order to have a sticky + // header on the table. So we check the "rows" by dividing the number of cells by the + // number of columns. + is( + cells.length / columnsNumber, + expectedRows.length, + "table has the expected number of rows" + ); + + expectedRows.forEach((expectedRow, rowIndex) => { + const startIndex = rowIndex * columnsNumber; + // Slicing the cells array so we can get the current "row". + const rowCells = cells.slice(startIndex, startIndex + columnsNumber); + is(rowCells.map(x => x.textContent).join(" | "), expectedRow.join(" | ")); + }); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_console_timeStamp.js b/devtools/client/webconsole/test/browser/browser_webconsole_console_timeStamp.js new file mode 100644 index 0000000000..a87ca73926 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_console_timeStamp.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that a console.timeStamp() does not print anything in the console + +"use strict"; + +const TEST_URI = "data:text/html,<meta charset=utf8>"; + +add_task(async function() { + // We open the console and an empty tab, as we only want to evaluate something. + const hud = await openNewTabAndConsole(TEST_URI); + // We execute `console.timeStamp('test')` from the console input. + execute(hud, "console.timeStamp('test')"); + info(`Checking size`); + await waitFor(() => findMessages(hud, "").length == 2); + const [first, second] = findMessages(hud, "").map(message => + message.textContent.trim() + ); + info(`Checking first message`); + is(first, "console.timeStamp('test')", "First message has expected text"); + info(`Checking second message`); + is(second, "undefined", "Second message has expected text"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_console_trace_distinct.js b/devtools/client/webconsole/test/browser/browser_webconsole_console_trace_distinct.js new file mode 100644 index 0000000000..00ef022b13 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_console_trace_distinct.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = `data:text/html,<meta charset=utf8><script> + var bar = () => myFunc(); + var rab = () => myFunc(); + var myFunc = () => console.trace(); + + bar();bar(); + rab();rab(); + </script>`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + await waitFor(() => findMessage(hud, "trace")); + ok(true, "console.trace() message is displayed in the console"); + const messages = findMessages(hud, "console.trace()"); + is(messages.length, 4, "There are 4 console.trace() messages"); + + info("Wait until the stacktraces are displayed"); + await waitFor(() => getFrames(hud).length === messages.length); + const [traceBar1, traceBar2, traceRab1, traceRab2] = getFrames(hud); + + const framesBar1 = getFramesTitleFromTrace(traceBar1); + is( + framesBar1.join(" - "), + "myFunc - bar - <anonymous>", + "First bar trace has the expected frames" + ); + + const framesBar2 = getFramesTitleFromTrace(traceBar2); + is( + framesBar2.join(" - "), + "myFunc - bar - <anonymous>", + "Second bar trace has the expected frames" + ); + + const framesRab1 = getFramesTitleFromTrace(traceRab1); + is( + framesRab1.join(" - "), + "myFunc - rab - <anonymous>", + "First rab trace has the expected frames" + ); + + const framesRab2 = getFramesTitleFromTrace(traceRab2); + is( + framesRab2.join(" - "), + "myFunc - rab - <anonymous>", + "Second rab trace has the expected frames" + ); +}); + +/** + * Get all the stacktrace `.frames` elements displayed in the console output. + * @returns {Array<HTMLElement>} + */ +function getFrames(hud) { + return Array.from(hud.ui.outputNode.querySelectorAll(".stacktrace .frames")); +} + +/** + * Given a stacktrace element, return an array of the frame names displayed in it. + * @param {HTMLElement} traceEl + * @returns {Array<String>} + */ +function getFramesTitleFromTrace(traceEl) { + return Array.from(traceEl.querySelectorAll(".frame .title")).map( + t => t.textContent + ); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_console_trace_duplicates.js b/devtools/client/webconsole/test/browser/browser_webconsole_console_trace_duplicates.js new file mode 100644 index 0000000000..508176c418 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_console_trace_duplicates.js @@ -0,0 +1,101 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/test/browser/" + + "test-console-trace-duplicates.html"; + +add_task(async function testTraceMessages() { + const hud = await openNewTabAndConsole(TEST_URI); + + const message = await waitFor(() => findMessage(hud, "foo1")); + // Wait until stacktrace is displayed. + await waitFor(() => !!message.querySelector(".frames")); + + is( + message.querySelector(".message-body").textContent, + "console.trace()", + "console.trace message body has expected text" + ); + is( + message.querySelector(".message-repeats").textContent, + "3", + "console.trace has the expected content for the repeat badge" + ); + + is( + message.querySelector(".frame-link-filename").textContent, + "test-console-trace-duplicates.html", + "message frame has expected text content" + ); + const [, line, column] = message + .querySelector(".frame-link-line") + .textContent.split(":"); + is(line, "20", "message frame has expected line"); + is(column, "11", "message frame has expected column"); + + const stack = message.querySelector(".stacktrace"); + ok(!!stack, "There's a stacktrace element"); + + const frames = Array.from(stack.querySelectorAll(".frame")); + checkStacktraceFrames(frames, [ + { + functionName: "foo3", + filename: TEST_URI, + line: 20, + }, + { + functionName: "foo2", + filename: TEST_URI, + line: 16, + }, + { + functionName: "foo1", + filename: TEST_URI, + line: 12, + }, + { + functionName: "<anonymous>", + filename: TEST_URI, + line: 23, + }, + ]); +}); + +/** + * Check stack info returned by getStackInfo(). + * + * @param {Object} stackInfo + * A stackInfo object returned by getStackInfo(). + * @param {Object} expected + * An object in the same format as the expected stackInfo object. + */ +function checkStacktraceFrames(frames, expectedFrames) { + is( + frames.length, + expectedFrames.length, + `There are ${frames.length} frames in the stacktrace` + ); + + frames.forEach((frameEl, i) => { + const expected = expectedFrames[i]; + + is( + frameEl.querySelector(".title").textContent, + expected.functionName, + `expected function name is displayed for frame #${i}` + ); + is( + frameEl.querySelector(".location .filename").textContent, + expected.filename, + `expected filename is displayed for frame #${i}` + ); + is( + frameEl.querySelector(".location .line").textContent, + `${expected.line}`, + `expected line is displayed for frame #${i}` + ); + }); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_copy_entire_message.js b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_copy_entire_message.js new file mode 100644 index 0000000000..132acff8f5 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_copy_entire_message.js @@ -0,0 +1,229 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const httpServer = createTestHTTPServer(); +httpServer.registerPathHandler(`/`, function(request, response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(` + <meta charset=utf8> + <h1>Test "copy message" context menu entry</h1> + <script type="text/javascript" src="test.js"></script>`); +}); + +httpServer.registerPathHandler("/test.js", function(request, response) { + response.setHeader("Content-Type", "application/javascript"); + response.write(` + window.logStuff = function() { + console.log("simple text message"); + function wrapper() { + console.log(new Error("error object")); + console.trace(); + for (let i = 0; i < 2; i++) console.log("repeated") + } + wrapper(); + }; + z.bar = "baz"; + `); +}); + +const TEST_URI = `http://localhost:${httpServer.identity.primaryPort}/`; + +// RegExp that validates copied text for log lines. +const LOG_FORMAT_WITH_TIMESTAMP = /^[\d:.]+ .+/; +const PREF_MESSAGE_TIMESTAMP = "devtools.webconsole.timestampMessages"; + +// Test the Copy menu item of the webconsole copies the expected clipboard text for +// different log messages. + +add_task(async function() { + await pushPref(PREF_MESSAGE_TIMESTAMP, true); + + const hud = await openNewTabAndConsole(TEST_URI); + + info("Call the log function defined in the test page"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.wrappedJSObject.logStuff(); + }); + + info("Test copy menu item with timestamp"); + await testMessagesCopy(hud, true); + + // Disable timestamp and wait until timestamp are not displayed anymore. + await toggleConsoleSetting( + hud, + ".webconsole-console-settings-menu-item-timestamps" + ); + await waitFor( + () => hud.ui.outputNode.querySelector(".message .timestamp") === null + ); + + info("Test copy menu item without timestamp"); + await testMessagesCopy(hud, false); +}); + +async function testMessagesCopy(hud, timestamp) { + const newLineString = "\n"; + + info("Test copy menu item for the simple log"); + let message = await waitFor(() => findMessage(hud, "simple text message")); + let clipboardText = await copyMessageContent(hud, message); + ok(true, "Clipboard text was found and saved"); + + info("Check copied text for simple log message"); + let lines = clipboardText.split(newLineString); + is(lines.length, 2, "There are 2 lines in the copied text"); + is(lines[1], "", "The last line is an empty new line"); + is( + lines[0], + `${ + timestamp ? getTimestampText(message) + " " : "" + }simple text message test.js:3:15`, + "Line of simple log message has expected text" + ); + if (timestamp) { + ok( + LOG_FORMAT_WITH_TIMESTAMP.test(lines[0]), + "Log line has the right format:\n" + lines[0] + ); + } + + info("Test copy menu item for the console.trace message"); + message = await waitFor(() => findMessage(hud, "console.trace")); + // Wait for the stacktrace to be rendered. + await waitFor(() => message.querySelector(".frames")); + clipboardText = await copyMessageContent(hud, message); + ok(true, "Clipboard text was found and saved"); + + info("Check copied text for the console.trace message"); + lines = clipboardText.split(newLineString); + is(lines.length, 4, "There are 4 lines in the copied text"); + is(lines[lines.length - 1], "", "The last line is an empty new line"); + is( + lines[0], + `${ + timestamp ? getTimestampText(message) + " " : "" + }console.trace() test.js:6:17`, + "Stacktrace first line has the expected text" + ); + if (timestamp) { + ok( + LOG_FORMAT_WITH_TIMESTAMP.test(lines[0]), + "Log line has the right format:\n" + lines[0] + ); + } + is( + lines[1], + ` wrapper ${TEST_URI}test.js:6`, + "Stacktrace first line has the expected text" + ); + is( + lines[2], + ` logStuff ${TEST_URI}test.js:9`, + "Stacktrace second line has the expected text" + ); + + info("Test copy menu item for the error message"); + message = await waitFor(() => findMessage(hud, "Error:")); + // Wait for the stacktrace to be rendered. + await waitFor(() => message.querySelector(".frames")); + clipboardText = await copyMessageContent(hud, message); + ok(true, "Clipboard text was found and saved"); + lines = clipboardText.split(newLineString); + is( + lines[0], + `${timestamp ? getTimestampText(message) + " " : ""}Error: error object`, + "Error object first line has expected text" + ); + if (timestamp) { + ok( + LOG_FORMAT_WITH_TIMESTAMP.test(lines[0]), + "Log line has the right format:\n" + lines[0] + ); + } + is( + lines[1], + ` wrapper ${TEST_URI}test.js:5`, + "Error Stacktrace first line has the expected text" + ); + is( + lines[2], + ` logStuff ${TEST_URI}test.js:9`, + "Error Stacktrace second line has the expected text" + ); + + info("Test copy menu item for the reference error message"); + message = await waitFor(() => findMessage(hud, "ReferenceError:")); + clipboardText = await copyMessageContent(hud, message); + ok(true, "Clipboard text was found and saved"); + lines = clipboardText.split(newLineString); + is( + lines[0], + (timestamp ? getTimestampText(message) + " " : "") + + "Uncaught ReferenceError: z is not defined", + "ReferenceError first line has expected text" + ); + if (timestamp) { + ok( + LOG_FORMAT_WITH_TIMESTAMP.test(lines[0]), + "Log line has the right format:\n" + lines[0] + ); + } + is( + lines[1], + ` <anonymous> ${TEST_URI}test.js:11`, + "ReferenceError second line has expected text" + ); + ok( + !!message.querySelector(".learn-more-link"), + "There is a Learn More link in the ReferenceError message" + ); + is( + clipboardText.toLowerCase().includes("Learn More"), + false, + "The Learn More text wasn't put in the clipboard" + ); + + message = await waitFor(() => findMessage(hud, "repeated 2")); + clipboardText = await copyMessageContent(hud, message); + ok(true, "Clipboard text was found and saved"); +} + +function getTimestampText(messageEl) { + return getSelectionTextFromElement(messageEl.querySelector(".timestamp")); +} + +/** + * Simple helper method to open the context menu on a given message, and click on the copy + * menu item. + */ +async function copyMessageContent(hud, messageEl) { + const menuPopup = await openContextMenu(hud, messageEl); + const copyMenuItem = menuPopup.querySelector("#console-menu-copy"); + ok(copyMenuItem, "copy menu item is enabled"); + + return waitForClipboardPromise( + () => copyMenuItem.click(), + data => data + ); +} + +/** + * Return the string representation, as if it was selected with the mouse and copied, + * using the Selection API. + * + * @param {HTMLElement} el + * @returns {String} the text representation of the element. + */ +function getSelectionTextFromElement(el) { + const doc = el.ownerDocument; + const win = doc.defaultView; + const range = doc.createRange(); + range.selectNode(el); + const selection = win.getSelection(); + selection.addRange(range); + const selectionText = selection.toString(); + selection.removeRange(range); + return selectionText; +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_copy_link_location.js b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_copy_link_location.js new file mode 100644 index 0000000000..6b1a1afa09 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_copy_link_location.js @@ -0,0 +1,99 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test the Copy Link Location menu item of the webconsole is displayed for network +// messages and copies the expected URL. + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-console.html?_date=" + + Date.now(); +const CONTEXT_MENU_ID = "#console-menu-copy-url"; + +add_task(async function() { + // Enable net messages in the console for this test. + await pushPref("devtools.webconsole.filter.net", true); + + const hud = await openNewTabAndConsole(TEST_URI); + await clearOutput(hud); + + info("Test Copy URL menu item for text log"); + + info("Logging a text message in the content window"); + const onLogMessage = waitForMessage(hud, "stringLog"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.wrappedJSObject.stringLog(); + }); + let message = await onLogMessage; + ok(message, "Text log found in the console"); + + info("Open and check the context menu for the logged text message"); + let menuPopup = await openContextMenu(hud, message.node); + + let copyURLItem = menuPopup.querySelector(CONTEXT_MENU_ID); + ok(!copyURLItem, "Copy URL menu item is hidden for a simple text message"); + + info("Open and check the context menu for the logged text message"); + const locationElement = message.node.querySelector( + ".frame-link-source-inner" + ); + menuPopup = await openContextMenu(hud, locationElement); + copyURLItem = menuPopup.querySelector(CONTEXT_MENU_ID); + ok(copyURLItem, "The Copy Link Location entry is displayed"); + + info("Click on Copy URL menu item and wait for clipboard to be updated"); + await waitForClipboardPromise(() => copyURLItem.click(), TEST_URI); + ok(true, "Expected text was copied to the clipboard."); + + await hideContextMenu(hud); + await clearOutput(hud); + + info("Test Copy URL menu item for network log"); + + info("Reload the content window to produce a network log"); + const onNetworkMessage = waitForMessage(hud, "test-console.html"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.wrappedJSObject.location.reload(); + }); + + message = await onNetworkMessage; + ok(message, "Network log found in the console"); + + info("Open and check the context menu for the logged network message"); + menuPopup = await openContextMenu(hud, message.node); + copyURLItem = menuPopup.querySelector(CONTEXT_MENU_ID); + ok(copyURLItem, "Copy url menu item is available in context menu"); + + info("Click on Copy URL menu item and wait for clipboard to be updated"); + await waitForClipboardPromise(() => copyURLItem.click(), TEST_URI); + ok(true, "Expected text was copied to the clipboard."); + + await hideContextMenu(hud); + await clearOutput(hud); + + info("Test Copy URL menu item from [Learn More] link"); + + info("Generate a Reference Error in the JS Console"); + message = await executeAndWaitForMessage( + hud, + "area51.aliens", + "ReferenceError:" + ); + ok(message, "Error log found in the console"); + + const learnMoreElement = message.node.querySelector(".learn-more-link"); + menuPopup = await openContextMenu(hud, learnMoreElement); + copyURLItem = menuPopup.querySelector(CONTEXT_MENU_ID); + ok(copyURLItem, "Copy url menu item is available in context menu"); + + info("Click on Copy URL menu item and wait for clipboard to be updated"); + await waitForClipboardPromise( + () => copyURLItem.click(), + learnMoreElement.href + ); + ok(true, "Expected text was copied to the clipboard."); + + await hideContextMenu(hud); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_copy_message_with_async_stacktrace.js b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_copy_message_with_async_stacktrace.js new file mode 100644 index 0000000000..bf978f9570 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_copy_message_with_async_stacktrace.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test the "Copy message" menu item copies the expected text to the clipboard +// for a message with a stacktrace containing async separators. + +"use strict"; + +const httpServer = createTestHTTPServer(); +httpServer.registerPathHandler(`/`, function(request, response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(`<script type="text/javascript" src="test.js"></script>`); +}); + +httpServer.registerPathHandler("/test.js", function(_, response) { + response.setHeader("Content-Type", "application/javascript"); + response.write(` + function resolveLater() { + return new Promise(function p(resolve) { + setTimeout(function timeout() { + Promise.resolve("blurp").then(function pthen(){ + console.trace("thenTrace"); + resolve(); + }) + }, 1); + }); + } + + async function waitForData() { + await resolveLater(); + } + `); +}); + +const TEST_URI = `http://localhost:${httpServer.identity.primaryPort}/`; + +add_task(async function() { + await pushPref("javascript.options.asyncstack_capture_debuggee_only", false); + const hud = await openNewTabAndConsole(TEST_URI); + + info("Call the log function defined in the test page"); + const onMessage = waitForMessage(hud, "thenTrace"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.wrappedJSObject.waitForData(); + }); + const message = await onMessage; + const messageEl = message.node; + await waitFor(() => messageEl.querySelector(".frames")); + + const clipboardText = await copyMessageContent(hud, messageEl); + ok(true, "Clipboard text was found and saved"); + + const newLineString = "\n"; + info("Check copied text for the console.trace message"); + const lines = clipboardText.split(newLineString); + + is( + JSON.stringify(lines, null, 2), + JSON.stringify( + [ + `console.trace() thenTrace test.js:6:21`, + ` pthen ${TEST_URI}test.js:6`, + ` (Async: promise callback)`, + ` timeout ${TEST_URI}test.js:5`, + ` (Async: setTimeout handler)`, + ` p ${TEST_URI}test.js:4`, + ` resolveLater ${TEST_URI}test.js:3`, + ` waitForData ${TEST_URI}test.js:14`, + ``, + ], + null, + 2 + ), + "Stacktrace was copied as expected" + ); +}); + +/** + * Simple helper method to open the context menu on a given message, and click on the copy + * menu item. + */ +async function copyMessageContent(hud, messageEl) { + const menuPopup = await openContextMenu(hud, messageEl); + const copyMenuItem = menuPopup.querySelector("#console-menu-copy"); + ok(copyMenuItem, "copy menu item is enabled"); + + return waitForClipboardPromise( + () => copyMenuItem.click(), + data => data + ); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_copy_message_with_framework_stacktrace.js b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_copy_message_with_framework_stacktrace.js new file mode 100644 index 0000000000..64d86fa4dd --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_copy_message_with_framework_stacktrace.js @@ -0,0 +1,129 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const httpServer = createTestHTTPServer(); +httpServer.registerPathHandler(`/`, function(request, response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(` + <meta charset=utf8> + <h1>Test "copy message" context menu entry on message with framework stacktrace</h1> + <script type="text/javascript" src="react.js"></script> + <script type="text/javascript" src="test.js"></script>`); +}); + +httpServer.registerPathHandler("/test.js", function(_, response) { + response.setHeader("Content-Type", "application/javascript"); + response.write(` + window.myFunc = () => wrapper(); + const wrapper = () => console.trace("wrapperTrace"); + `); +}); + +httpServer.registerPathHandler("/react.js", function(_, response) { + response.setHeader("Content-Type", "application/javascript"); + response.write(` + window.render = function() { + const renderFinal = () => window.myFunc(); + renderFinal(); + }; + `); +}); + +const TEST_URI = `http://localhost:${httpServer.identity.primaryPort}/`; + +// Test the Copy menu item of the webconsole copies the expected clipboard text for +// a message with a "framework" stacktrace (i.e. with grouped frames). + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + info("Call the log function defined in the test page"); + const onMessage = waitForMessage(hud, "wrapperTrace"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.wrappedJSObject.render(); + }); + const message = await onMessage; + const messageEl = message.node; + await waitFor(() => messageEl.querySelector(".frames")); + + let clipboardText = await copyMessageContent(hud, messageEl); + ok(true, "Clipboard text was found and saved"); + + const newLineString = "\n"; + info("Check copied text for the console.trace message"); + let lines = clipboardText.split(newLineString); + is(lines.length, 5, "Correct number of lines in the copied text"); + is(lines[lines.length - 1], "", "The last line is an empty new line"); + is( + lines[0], + `console.trace() wrapperTrace test.js:3:35`, + "Message first line has the expected text" + ); + is( + lines[1], + ` wrapper ${TEST_URI}test.js:3`, + "Stacktrace first line has the expected text" + ); + is( + lines[2], + ` myFunc ${TEST_URI}test.js:2`, + "Stacktrace second line has the expected text" + ); + is(lines[3], ` React 2`, "Stacktrace third line has the expected text"); + + info("Expand the React group"); + const getFrames = () => messageEl.querySelectorAll(".frame"); + const frames = getFrames().length; + messageEl.querySelector(".frames .group").click(); + // Let's wait until all React frames are displayed. + await waitFor(() => getFrames().length > frames); + + clipboardText = await copyMessageContent(hud, messageEl); + ok(true, "Clipboard text was found and saved"); + + info( + "Check copied text for the console.trace message with expanded React frames" + ); + lines = clipboardText.split(newLineString); + is(lines.length, 7, "Correct number of lines in the copied text"); + is(lines[lines.length - 1], "", "The last line is an empty new line"); + is( + lines[0], + `console.trace() wrapperTrace test.js:3:35`, + "Message first line has the expected text" + ); + is( + lines[1], + ` wrapper ${TEST_URI}test.js:3`, + "Stacktrace first line has the expected text" + ); + is( + lines[2], + ` myFunc ${TEST_URI}test.js:2`, + "Stacktrace second line has the expected text" + ); + is(lines[3], ` React 2`, "Stacktrace third line has the expected text"); + is( + lines[4], + ` renderFinal`, + "Stacktrace fourth line has the expected text" + ); + is(lines[5], ` render`, "Stacktrace fifth line has the expected text"); +}); + +/** + * Simple helper method to open the context menu on a given message, and click on the copy + * menu item. + */ +async function copyMessageContent(hud, messageEl) { + const menuPopup = await openContextMenu(hud, messageEl); + const copyMenuItem = menuPopup.querySelector("#console-menu-copy"); + ok(copyMenuItem, "copy menu item is enabled"); + + return waitForClipboardPromise( + () => copyMenuItem.click(), + data => data + ); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_copy_object.js b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_copy_object.js new file mode 100644 index 0000000000..95d2fa35a0 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_copy_object.js @@ -0,0 +1,145 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test the "Copy object" menu item of the webconsole is enabled only when +// clicking on messages that are associated with an object actor. + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf-8,<script> + window.bar = { baz: 1 }; + console.log("foo"); + console.log("foo", window.bar); + console.log(["foo", window.bar, 2]); + console.group("group"); + console.groupCollapsed("collapsed"); + console.groupEnd(); + console.log(532); + console.log(true); + console.log(false); + console.log(undefined); + console.log(null); +</script>`; +const copyObjectMenuItemId = "#console-menu-copy-object"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + const [msgWithText, msgWithObj, msgNested] = await waitFor(() => + findMessages(hud, "foo") + ); + ok( + msgWithText && msgWithObj && msgNested, + "Three messages should have appeared" + ); + + const [groupMsgObj] = await waitFor(() => + findMessages(hud, "group", ".message-body") + ); + const [collapsedGroupMsgObj] = await waitFor(() => + findMessages(hud, "collapsed", ".message-body") + ); + const [numberMsgObj] = await waitFor(() => + findMessages(hud, `532`, ".message-body") + ); + const [trueMsgObj] = await waitFor(() => + findMessages(hud, `true`, ".message-body") + ); + const [falseMsgObj] = await waitFor(() => + findMessages(hud, `false`, ".message-body") + ); + const [undefinedMsgObj] = await waitFor(() => + findMessages(hud, `undefined`, ".message-body") + ); + const [nullMsgObj] = await waitFor(() => + findMessages(hud, `null`, ".message-body") + ); + ok(nullMsgObj, "One message with null value should have appeared"); + + const text = msgWithText.querySelector(".objectBox-string"); + const objInMsgWithObj = msgWithObj.querySelector(".objectBox-object"); + const textInMsgWithObj = msgWithObj.querySelector(".objectBox-string"); + + // The third message has an object nested in an array, the array is therefore the top + // object, the object is the nested object. + const topObjInMsg = msgNested.querySelector(".objectBox-array"); + const nestedObjInMsg = msgNested.querySelector(".objectBox-object"); + + const consoleMessages = await waitFor(() => + findMessages(hud, 'console.log("foo");', ".message-location") + ); + await testCopyObjectMenuItemDisabled(hud, consoleMessages[0]); + + info(`Check "Copy object" is enabled for text only messages + thus copying the text`); + await testCopyObject(hud, text, `foo`, false); + + info(`Check "Copy object" is enabled for text in complex messages + thus copying the text`); + await testCopyObject(hud, textInMsgWithObj, `foo`, false); + + info("Check `Copy object` is enabled for objects in complex messages"); + await testCopyObject(hud, objInMsgWithObj, `{"baz":1}`, true); + + info("Check `Copy object` is enabled for top object in nested messages"); + await testCopyObject(hud, topObjInMsg, `["foo",{"baz":1},2]`, true); + + info("Check `Copy object` is enabled for nested object in nested messages"); + await testCopyObject(hud, nestedObjInMsg, `{"baz":1}`, true); + + info("Check `Copy object` is disabled on `console.group('group')` messages"); + await testCopyObjectMenuItemDisabled(hud, groupMsgObj); + + info(`Check "Copy object" is disabled in "console.groupCollapsed('collapsed')" + messages`); + await testCopyObjectMenuItemDisabled(hud, collapsedGroupMsgObj); + + // Check for primitive objects + info("Check `Copy object` is enabled for numbers"); + await testCopyObject(hud, numberMsgObj, `532`, false); + + info("Check `Copy object` is enabled for booleans"); + await testCopyObject(hud, trueMsgObj, `true`, false); + await testCopyObject(hud, falseMsgObj, `false`, false); + + info("Check `Copy object` is enabled for undefined and null"); + await testCopyObject(hud, undefinedMsgObj, `undefined`, false); + await testCopyObject(hud, nullMsgObj, `null`, false); +}); + +async function testCopyObject(hud, element, expectedMessage, objectInput) { + info("Check `Copy object` is enabled"); + let menuPopup = await openContextMenu(hud, element); + const copyObjectMenuItem = menuPopup.querySelector(copyObjectMenuItemId); + ok( + !copyObjectMenuItem.disabled, + "`Copy object` is enabled for object in complex message" + ); + + const validatorFn = data => { + const prettifiedMessage = prettyPrintMessage(expectedMessage, objectInput); + return data === prettifiedMessage; + }; + + info("Click on `Copy object`"); + await waitForClipboardPromise(() => copyObjectMenuItem.click(), validatorFn); + + info("`Copy object` by using the access-key O"); + menuPopup = await openContextMenu(hud, element); + await waitForClipboardPromise(() => synthesizeKeyShortcut("O"), validatorFn); +} + +async function testCopyObjectMenuItemDisabled(hud, element) { + const menuPopup = await openContextMenu(hud, element); + const copyObjectMenuItem = menuPopup.querySelector(copyObjectMenuItemId); + ok( + copyObjectMenuItem.disabled, + `"Copy object" is disabled for messages + with no variables/objects` + ); + await hideContextMenu(hud); +} + +function prettyPrintMessage(message, isObject) { + return isObject ? JSON.stringify(JSON.parse(message), null, 2) : message; +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_export_console_output.js b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_export_console_output.js new file mode 100644 index 0000000000..cc4831a83f --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_export_console_output.js @@ -0,0 +1,193 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const httpServer = createTestHTTPServer(); +httpServer.registerPathHandler(`/`, function(request, response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(` + <html> + <head> + <meta charset="utf-8"> + <script type="text/javascript" src="test.js"></script> + </head> + <body>Test "Export All" context menu entry</body> + </html>`); +}); + +httpServer.registerPathHandler("/test.js", function(request, response) { + response.setHeader("Content-Type", "application/javascript"); + response.write(` + window.logStuff = function() { + function wrapper() { + console.log("hello"); + console.log("myObject:", {a: 1}, "myArray:", ["b", "c"]); + console.log(new Error("error object")); + console.trace("myConsoleTrace"); + console.info("world", "!"); + } + wrapper(); + }; + `); +}); + +const TEST_URI = `http://localhost:${httpServer.identity.primaryPort}/`; + +const { MockFilePicker } = SpecialPowers; +MockFilePicker.init(window); +MockFilePicker.returnValue = MockFilePicker.returnOK; + +var { Cu } = require("chrome"); +var FileUtils = Cu.import("resource://gre/modules/FileUtils.jsm").FileUtils; + +// Test the export visible messages to clipboard of the webconsole copies the expected +// clipboard text for different log messages to find if everything is copied to clipboard. + +add_task(async function testExportToClipboard() { + const hud = await openNewTabAndConsole(TEST_URI); + await clearOutput(hud); + + info("Call the log function defined in the test page"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.logStuff(); + }); + + info("Test export to clipboard "); + // Let's wait until we have all the logged messages. + await waitFor(() => findMessages(hud, "").length === 5); + // And also until the stacktraces are rendered (there should be 2) + await waitFor( + () => hud.ui.outputNode.querySelectorAll(".frames").length === 2 + ); + + const message = findMessage(hud, "hello"); + const clipboardText = await exportAllToClipboard(hud, message); + ok(true, "Clipboard text was found and saved"); + + checkExportedText(clipboardText); +}); + +add_task(async function testExportToFile() { + const hud = await openNewTabAndConsole(TEST_URI); + await clearOutput(hud); + + info("Call the log function defined in the test page"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.logStuff(); + }); + + info("Test export to clipboard "); + // Let's wait until we have all the logged messages. + await waitFor(() => findMessages(hud, "").length === 5); + // And also until the stacktraces are rendered (there should be 2) + await waitFor( + () => hud.ui.outputNode.querySelectorAll(".frames").length === 2 + ); + + const message = findMessage(hud, "hello"); + const text = await exportAllToFile(hud, message); + checkExportedText(text); +}); + +function checkExportedText(text) { + // Here we should have: + // ------------------------------------------------------------------- + // hello test.js:4:17 + // ------------------------------------------------------------------- + // myObject: + // Object { a: 1 } + // myArray: + // Array [ "b", "c"] + // test.js:5:17 + // ------------------------------------------------------------------- + // Error: error object + // wrapper test.js:5 + // logStuff test.js:17 + // test.js:6:17 + // ------------------------------------------------------------------- + // console.trace() myConsoleTrace test.js:7:9 + // wrapper test.js:7 + // logStuff test.js:17 + // ------------------------------------------------------------------- + // world ! test.js:8:17 + // ------------------------------------------------------------------- + info("Check if all messages where exported as expected"); + const lines = text.split("\n").map(line => line.replace(/\r$/, "")); + + is(lines.length, 15, "There's 15 lines of text"); + is(lines[lines.length - 1], "", "Last line is empty"); + + info("Check simple text message"); + is(lines[0], "hello test.js:4:17", "Simple log has expected text"); + + info("Check multiple logged items message"); + is(lines[1], `myObject: `); + is(lines[2], `Object { a: 1 }`); + is(lines[3], ` myArray: `); + is(lines[4], `Array [ "b", "c" ]`); + is(lines[5], `test.js:5:17`); + + info("Check logged error object"); + is(lines[6], `Error: error object`); + is(lines[7], ` wrapper ${TEST_URI}test.js:6`); + is(lines[8], ` logStuff ${TEST_URI}test.js:10`); + is(lines[9], `test.js:6:17`); + + info("Check console.trace message"); + is(lines[10], `console.trace() myConsoleTrace test.js:7:17`); + is(lines[11], ` wrapper ${TEST_URI}test.js:7`); + is(lines[12], ` logStuff ${TEST_URI}test.js:10`); + + info("Check console.info message"); + is(lines[13], `world ! test.js:8:17`); +} + +async function exportAllToFile(hud, message) { + const menuPopup = await openContextMenuExportSubMenu(hud, message); + const exportFile = menuPopup.querySelector("#console-menu-export-file"); + ok(exportFile, "copy menu item is enabled"); + + const nsiFile = FileUtils.getFile("TmpD", [ + `export_console_${Date.now()}.log`, + ]); + MockFilePicker.setFiles([nsiFile]); + exportFile.click(); + info("Exporting to file"); + + // The file may not be ready yet. + await waitFor(() => OS.File.exists(nsiFile.path)); + const buffer = await OS.File.read(nsiFile.path); + return new TextDecoder().decode(buffer); +} + +/** + * Simple helper method to open the context menu on a given message, and click on the + * export visible messages to clipboard. + */ +async function exportAllToClipboard(hud, message) { + const menuPopup = await openContextMenuExportSubMenu(hud, message); + const exportClipboard = menuPopup.querySelector( + "#console-menu-export-clipboard" + ); + ok(exportClipboard, "copy menu item is enabled"); + + let clipboardText; + await waitForClipboardPromise( + () => exportClipboard.click(), + data => { + clipboardText = data; + return data; + } + ); + return clipboardText; +} + +async function openContextMenuExportSubMenu(hud, message) { + const menuPopup = await openContextMenu(hud, message); + const exportMenu = menuPopup.querySelector("#console-menu-export"); + + const view = exportMenu.ownerDocument.defaultView; + EventUtils.synthesizeMouseAtCenter(exportMenu, { type: "mousemove" }, view); + return menuPopup; +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_object_in_sidebar.js b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_object_in_sidebar.js new file mode 100644 index 0000000000..39f02af1e1 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_object_in_sidebar.js @@ -0,0 +1,160 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the "Open in sidebar" context menu entry is active for +// the correct objects and opens the sidebar when clicked. + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf8," + + `<script> + console.log({a:1}, 100, {b:1}, 'foo', false, null, undefined); + + var error = new Error("oh my"); + error.customProperty = {code: 500, message: "Internal Server Error"}; + error.name = "CustomServerError"; + console.info(error); + </script>`; + +add_task(async function() { + // Should be removed when sidebar work is complete + await pushPref("devtools.webconsole.sidebarToggle", true); + + const hud = await openNewTabAndConsole(TEST_URI); + + const message = await waitFor(() => findMessage(hud, "Object { a: 1 }")); + const [objectA, objectB] = message.querySelectorAll( + ".object-inspector .objectBox-object" + ); + const number = findMessage(hud, "100", ".objectBox"); + const string = findMessage(hud, "foo", ".objectBox"); + const bool = findMessage(hud, "false", ".objectBox"); + const nullMessage = findMessage(hud, "null", ".objectBox"); + const undefinedMsg = findMessage(hud, "undefined", ".objectBox"); + + info("Showing sidebar for {a:1}"); + await showSidebarWithContextMenu(hud, objectA, true); + + let sidebarContents = hud.ui.document.querySelector(".sidebar-contents"); + let objectInspector = sidebarContents.querySelector(".object-inspector"); + let oiNodes = objectInspector.querySelectorAll(".node"); + if (oiNodes.length === 1) { + // If this is the case, we wait for the properties to be fetched and displayed. + await waitForNodeMutation(objectInspector, { + childList: true, + }); + } + + let sidebarText = hud.ui.document.querySelector(".sidebar-contents") + .textContent; + ok(sidebarText.includes("a: 1"), "Sidebar is shown for {a:1}"); + + info("Showing sidebar for {a:1} again"); + await showSidebarWithContextMenu(hud, objectA, false); + ok( + hud.ui.document.querySelector(".sidebar"), + "Sidebar is still shown after clicking on same object" + ); + is( + hud.ui.document.querySelector(".sidebar-contents").textContent, + sidebarText, + "Sidebar is not updated after clicking on same object" + ); + + info("Showing sidebar for {b:1}"); + await showSidebarWithContextMenu(hud, objectB, false); + + sidebarContents = hud.ui.document.querySelector(".sidebar-contents"); + objectInspector = sidebarContents.querySelector(".object-inspector"); + oiNodes = objectInspector.querySelectorAll(".node"); + if (oiNodes.length === 1) { + // If this is the case, we wait for the properties to be fetched and displayed. + await waitForNodeMutation(objectInspector, { + childList: true, + }); + } + + isnot( + hud.ui.document.querySelector(".sidebar-contents").textContent, + sidebarText, + "Sidebar is updated for {b:1}" + ); + sidebarText = hud.ui.document.querySelector(".sidebar-contents").textContent; + + ok(sidebarText.includes("b: 1"), "Sidebar contents shown for {b:1}"); + + info("Showing sidebar for Error object"); + const errorMsg = findMessage(hud, "CustomServerError:"); + await showSidebarWithContextMenu(hud, errorMsg, false); + + sidebarContents = hud.ui.document.querySelector(".sidebar-contents"); + objectInspector = sidebarContents.querySelector(".object-inspector"); + oiNodes = objectInspector.querySelectorAll(".node"); + if (oiNodes.length === 1) { + // If this is the case, we wait for the properties to be fetched and displayed. + await waitForNodeMutation(objectInspector, { + childList: true, + }); + } + sidebarText = hud.ui.document.querySelector(".sidebar-contents").textContent; + is( + oiNodes[0].textContent, + "CustomServerError", + "First node has expected content" + ); + ok( + sidebarText.includes(`customProperty:`), + "Sidebar contents shown for the error object" + ); + + info("Checking context menu entry is disabled for number"); + const numberContextMenuEnabled = await isContextMenuEntryEnabled(hud, number); + ok(!numberContextMenuEnabled, "Context menu entry is disabled for number"); + + info("Checking context menu entry is disabled for string"); + const stringContextMenuEnabled = await isContextMenuEntryEnabled(hud, string); + ok(!stringContextMenuEnabled, "Context menu entry is disabled for string"); + + info("Checking context menu entry is disabled for bool"); + const boolContextMenuEnabled = await isContextMenuEntryEnabled(hud, bool); + ok(!boolContextMenuEnabled, "Context menu entry is disabled for bool"); + + info("Checking context menu entry is disabled for null message"); + const nullContextMenuEnabled = await isContextMenuEntryEnabled( + hud, + nullMessage + ); + ok(!nullContextMenuEnabled, "Context menu entry is disabled for nullMessage"); + + info("Checking context menu entry is disabled for undefined message"); + const undefinedContextMenuEnabled = await isContextMenuEntryEnabled( + hud, + undefinedMsg + ); + ok( + !undefinedContextMenuEnabled, + "Context menu entry is disabled for undefinedMsg" + ); +}); + +async function showSidebarWithContextMenu(hud, node, expectMutation) { + const appNode = hud.ui.document.querySelector(".webconsole-app"); + const onSidebarShown = waitForNodeMutation(appNode, { childList: true }); + + const contextMenu = await openContextMenu(hud, node); + const openInSidebar = contextMenu.querySelector("#console-menu-open-sidebar"); + openInSidebar.click(); + if (expectMutation) { + await onSidebarShown; + } + await hideContextMenu(hud); +} + +async function isContextMenuEntryEnabled(hud, node) { + const contextMenu = await openContextMenu(hud, node); + const openInSidebar = contextMenu.querySelector("#console-menu-open-sidebar"); + const enabled = !openInSidebar.attributes.disabled; + await hideContextMenu(hud); + return enabled; +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_open_url.js b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_open_url.js new file mode 100644 index 0000000000..2a9126dbdd --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_open_url.js @@ -0,0 +1,108 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the Open URL in new Tab menu item is displayed for links in text messages +// and network logs and that they work as expected. + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-console.html"; +const TEST_URI2 = "http://example.com/"; + +add_task(async function() { + // Enable net messages in the console for this test. + await pushPref("devtools.webconsole.filter.net", true); + + const hud = await openNewTabAndConsole(TEST_URI); + await clearOutput(hud); + + info("Test Open URL menu item for text log"); + + info("Logging a text message in the content window"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.wrappedJSObject.console.log("simple text message"); + }); + let message = await waitFor(() => findMessage(hud, "simple text message")); + ok(message, "Text log found in the console"); + + info("Open and check the context menu for the logged text message"); + let menuPopup = await openContextMenu(hud, message); + let openUrlItem = menuPopup.querySelector("#console-menu-open-url"); + ok(!openUrlItem, "Open URL menu item is not available"); + + await hideContextMenu(hud); + + info("Test Open URL menu item for a text message containing a link"); + await ContentTask.spawn(gBrowser.selectedBrowser, TEST_URI2, url => { + content.wrappedJSObject.console.log("Visit", url); + }); + + info("Open context menu for the link"); + message = await waitFor(() => findMessage(hud, "Visit")); + const urlNode = message.querySelector("a.url"); + menuPopup = await openContextMenu(hud, urlNode); + openUrlItem = menuPopup.querySelector("#console-menu-open-url"); + ok(openUrlItem, "Open URL menu item is available"); + + info("Click on Open URL menu item and wait for new tab to open"); + let currentTab = gBrowser.selectedTab; + const onTabLoaded = BrowserTestUtils.waitForNewTab(gBrowser, TEST_URI2, true); + openUrlItem.click(); + let newTab = await onTabLoaded; + ok(newTab, "The expected tab was opened."); + is( + newTab._tPos, + currentTab._tPos + 1, + "The new tab was opened in the position to the right of the current tab" + ); + is(gBrowser.selectedTab, currentTab, "The tab was opened in the background"); + + await clearOutput(hud); + + info("Test Open URL menu item for network log"); + + info("Reload the content window to produce a network log"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.wrappedJSObject.location.reload(); + }); + message = await waitFor(() => findMessage(hud, "test-console.html")); + ok(message, "Network log found in the console"); + + info("Open and check the context menu for the logged network message"); + menuPopup = await openContextMenu(hud, message); + openUrlItem = menuPopup.querySelector("#console-menu-open-url"); + ok(openUrlItem, "Open URL menu item is available"); + + currentTab = gBrowser.selectedTab; + const tabLoaded = listenToTabLoad(); + info("Click on Open URL menu item and wait for new tab to open"); + openUrlItem.click(); + await hideContextMenu(hud); + newTab = await tabLoaded; + const newTabHref = newTab.linkedBrowser.currentURI.spec; + is(newTabHref, TEST_URI, "Tab was opened with the expected URL"); + + info("Remove the new tab and select the previous tab back"); + gBrowser.removeTab(newTab); + gBrowser.selectedTab = currentTab; +}); + +/** + * Simple helper to wrap a tab load listener in a promise. + */ +function listenToTabLoad() { + return new Promise(resolve => { + gBrowser.tabContainer.addEventListener( + "TabOpen", + function(evt) { + const newTab = evt.target; + BrowserTestUtils.browserLoaded(newTab.linkedBrowser).then(() => + resolve(newTab) + ); + }, + { capture: true, once: true } + ); + }); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_reveal_in_inspector.js b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_reveal_in_inspector.js new file mode 100644 index 0000000000..ee28beca50 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_reveal_in_inspector.js @@ -0,0 +1,117 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test the "Reveal in Inspector" menu item of the webconsole is enabled only when +// clicking on HTML elements attached to the DOM. Also check that clicking the menu +// item or using the access-key Q does select the node in the inspector. + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf-8, + <!DOCTYPE html> + <html> + <body></body> + <script> + console.log("foo"); + console.log({hello: "world"}); + console.log(document.createElement("span")); + console.log(document.body.appendChild(document.createElement("div"))); + console.log(document.body.appendChild(document.createTextNode("test-text"))); + console.log(document.querySelectorAll('html')); + </script> + </html> +`; +const revealInInspectorMenuItemId = "#console-menu-open-node"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + const msgWithText = await waitFor(() => findMessage(hud, `foo`)); + const msgWithObj = await waitFor(() => findMessage(hud, `Object`)); + const nonDomEl = await waitFor(() => + findMessage(hud, `<span>`, ".objectBox-node") + ); + + const domEl = await waitFor(() => + findMessage(hud, `<div>`, ".objectBox-node") + ); + const domTextEl = await waitFor(() => + findMessage(hud, `test-text`, ".objectBox-textNode") + ); + const domElCollection = await waitFor(() => + findMessage(hud, `html`, ".objectBox-node") + ); + + info("Check `Reveal in Inspector` is not visible for strings"); + await testRevealInInspectorDisabled(hud, msgWithText); + + info("Check `Reveal in Inspector` is not visible for objects"); + await testRevealInInspectorDisabled(hud, msgWithObj); + + info("Check `Reveal in Inspector` is not visible for disconnected nodes"); + await testRevealInInspectorDisabled(hud, nonDomEl); + + info("Check `Reveal in Inspector` for a single connected node"); + await testRevealInInspector(hud, domEl, "div", false); + + info("Check `Reveal in Inspector` for a connected text element"); + await testRevealInInspector(hud, domTextEl, "#text", false); + + info("Check `Reveal in Inspector` for a collection of elements"); + await testRevealInInspector(hud, domElCollection, "html", false); + + info("`Reveal in Inspector` by using the access-key Q"); + await testRevealInInspector(hud, domEl, "div", true); +}); + +async function testRevealInInspector(hud, element, tag, accesskey) { + const toolbox = hud.toolbox; + + // Loading the inspector panel at first, to make it possible to listen for + // new node selections + await toolbox.loadTool("inspector"); + const inspector = toolbox.getPanel("inspector"); + + const menuPopup = await openContextMenu(hud, element); + const revealInInspectorMenuItem = menuPopup.querySelector( + revealInInspectorMenuItemId + ); + ok( + revealInInspectorMenuItem !== null, + "There is the `Reveal in Inspector` menu item" + ); + + const onInspectorSelected = toolbox.once("inspector-selected"); + const onInspectorUpdated = inspector.once("inspector-updated"); + const onNewNode = toolbox.selection.once("new-node-front"); + + if (accesskey) { + info("Clicking on `Reveal in Inspector` menu item"); + await revealInInspectorMenuItem.click(); + } else { + info("Using access-key Q to `Reveal in Inspector`"); + await synthesizeKeyShortcut("Q"); + } + + await onInspectorSelected; + await onInspectorUpdated; + const nodeFront = await onNewNode; + + ok(true, "Inspector selected and new node got selected"); + is(nodeFront.displayName, tag, "The expected node was selected"); + + await openConsole(); +} + +async function testRevealInInspectorDisabled(hud, element) { + info("Check 'Reveal in Inspector' is not in the menu"); + const menuPopup = await openContextMenu(hud, element); + const revealInInspectorMenuItem = menuPopup.querySelector( + revealInInspectorMenuItemId + ); + ok( + !revealInInspectorMenuItem, + `"Reveal in Inspector" is not available for messages with no HTML DOM elements` + ); + await hideContextMenu(hud); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_store_as_global.js b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_store_as_global.js new file mode 100644 index 0000000000..c7b424ee6a --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_store_as_global.js @@ -0,0 +1,117 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test the "Store as global variable" menu item of the webconsole is enabled only when +// clicking on messages that are associated with an object actor. + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf-8,<script> + window.bar = { baz: 1 }; + console.log("foo"); + console.log("foo", window.bar); + window.array = ["foo", window.bar, 2]; + console.log(window.array); + window.longString = "foo" + "a".repeat(1e4); + console.log(window.longString); + window.symbol = Symbol(); + console.log("foo", window.symbol); +</script>`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + const messages = await waitFor(() => findMessages(hud, "foo")); + is(messages.length, 5, "Five messages should have appeared"); + const [msgWithText, msgWithObj, msgNested, msgLongStr, msgSymbol] = messages; + let varIdx = 0; + + info("Check store as global variable is disabled for text only messages"); + await storeAsVariable(hud, msgWithText, "string"); + + info( + "Check store as global variable is disabled for text in complex messages" + ); + await storeAsVariable(hud, msgWithObj, "string"); + + info( + "Check store as global variable is enabled for objects in complex messages" + ); + await storeAsVariable(hud, msgWithObj, "object", varIdx++, "window.bar"); + + info( + "Check store as global variable is enabled for top object in nested messages" + ); + await storeAsVariable(hud, msgNested, "array", varIdx++, "window.array"); + + info( + "Check store as global variable is enabled for nested object in nested messages" + ); + await storeAsVariable(hud, msgNested, "object", varIdx++, "window.bar"); + + info("Check store as global variable is enabled for long strings"); + await storeAsVariable( + hud, + msgLongStr, + "string", + varIdx++, + "window.longString" + ); + + info("Check store as global variable is enabled for symbols"); + await storeAsVariable(hud, msgSymbol, "symbol", varIdx++, "window.symbol"); + + info( + "Check store as global variable is enabled for invisible-to-debugger objects" + ); + const onMessageInvisible = waitForMessage(hud, "foo"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + const obj = Cu.Sandbox(Cu.getObjectPrincipal(content), { + invisibleToDebugger: true, + }); + content.wrappedJSObject.invisibleToDebugger = obj; + content.console.log("foo", obj); + }); + const msgInvisible = (await onMessageInvisible).node; + await storeAsVariable( + hud, + msgInvisible, + "object", + varIdx++, + "window.invisibleToDebugger" + ); +}); + +async function storeAsVariable(hud, msg, type, varIdx, equalTo) { + const element = msg.querySelector(".objectBox-" + type); + const menuPopup = await openContextMenu(hud, element); + const storeMenuItem = menuPopup.querySelector("#console-menu-store"); + + if (varIdx == null) { + ok(storeMenuItem.disabled, "store as global variable is disabled"); + await hideContextMenu(hud); + return; + } + + ok(!storeMenuItem.disabled, "store as global variable is enabled"); + + info("Click on store as global variable"); + const onceInputSet = hud.jsterm.once("set-input-value"); + storeMenuItem.click(); + + info("Wait for console input to be updated with the temp variable"); + await onceInputSet; + + info("Wait for context menu to be hidden"); + await hideContextMenu(hud); + + is(getInputValue(hud), "temp" + varIdx, "Input was set"); + + await executeAndWaitForMessage( + hud, + `temp${varIdx} === ${equalTo}`, + true, + ".result" + ); + ok(true, "Correct variable assigned into console."); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_cors_errors.js b/devtools/client/webconsole/test/browser/browser_webconsole_cors_errors.js new file mode 100644 index 0000000000..9a41b290f4 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_cors_errors.js @@ -0,0 +1,236 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Ensure that the different CORS error are logged to the console with the appropriate +// "Learn more" link. + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/test/browser/test-network-request.html"; +const BASE_CORS_ERROR_URL = + "https://developer.mozilla.org/docs/Web/HTTP/CORS/Errors/"; +const BASE_CORS_ERROR_URL_PARAMS = new URLSearchParams({ + utm_source: "devtools", + utm_medium: "firefox-cors-errors", + utm_campaign: "default", +}); + +registerCleanupFunction(async function() { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +add_task(async function() { + await pushPref("devtools.webconsole.filter.netxhr", true); + + const hud = await openNewTabAndConsole(TEST_URI); + + let onCorsMessage; + let message; + + info(`Setting "content.cors.disable" to true to test CORSDisabled message`); + await pushPref("content.cors.disable", true); + onCorsMessage = waitForMessage(hud, "Reason: CORS disabled"); + makeFaultyCorsCall("CORSDisabled"); + message = await onCorsMessage; + await checkCorsMessage(message, "CORSDisabled"); + await pushPref("content.cors.disable", false); + + info("Test CORSPreflightDidNotSucceed"); + onCorsMessage = waitForMessage( + hud, + `CORS preflight response did not succeed` + ); + makeFaultyCorsCall("CORSPreflightDidNotSucceed"); + message = await onCorsMessage; + await checkCorsMessage(message, "CORSPreflightDidNotSucceed"); + + info("Test CORS did not succeed"); + onCorsMessage = waitForMessage(hud, "Reason: CORS request did not succeed"); + makeFaultyCorsCall("CORSDidNotSucceed"); + message = await onCorsMessage; + await checkCorsMessage(message, "CORSDidNotSucceed"); + + info("Test CORSExternalRedirectNotAllowed"); + onCorsMessage = waitForMessage( + hud, + "Reason: CORS request external redirect not allowed" + ); + makeFaultyCorsCall("CORSExternalRedirectNotAllowed"); + message = await onCorsMessage; + await checkCorsMessage(message, "CORSExternalRedirectNotAllowed"); + + info("Test CORSMissingAllowOrigin"); + onCorsMessage = waitForMessage( + hud, + `Reason: CORS header ${quote("Access-Control-Allow-Origin")} missing` + ); + makeFaultyCorsCall("CORSMissingAllowOrigin"); + message = await onCorsMessage; + await checkCorsMessage(message, "CORSMissingAllowOrigin"); + + info("Test CORSMultipleAllowOriginNotAllowed"); + onCorsMessage = waitForMessage( + hud, + `Reason: Multiple CORS header ${quote( + "Access-Control-Allow-Origin" + )} not allowed` + ); + makeFaultyCorsCall("CORSMultipleAllowOriginNotAllowed"); + message = await onCorsMessage; + await checkCorsMessage(message, "CORSMultipleAllowOriginNotAllowed"); + + info("Test CORSAllowOriginNotMatchingOrigin"); + onCorsMessage = waitForMessage( + hud, + `Reason: CORS header ` + + `${quote("Access-Control-Allow-Origin")} does not match ${quote( + "mochi.test" + )}` + ); + makeFaultyCorsCall("CORSAllowOriginNotMatchingOrigin"); + message = await onCorsMessage; + await checkCorsMessage(message, "CORSAllowOriginNotMatchingOrigin"); + + info("Test CORSNotSupportingCredentials"); + onCorsMessage = waitForMessage( + hud, + `Reason: Credential is not supported if the CORS ` + + `header ${quote("Access-Control-Allow-Origin")} is ${quote("*")}` + ); + makeFaultyCorsCall("CORSNotSupportingCredentials"); + message = await onCorsMessage; + await checkCorsMessage(message, "CORSNotSupportingCredentials"); + + info("Test CORSMethodNotFound"); + onCorsMessage = waitForMessage( + hud, + `Reason: Did not find method in CORS header ` + + `${quote("Access-Control-Allow-Methods")}` + ); + makeFaultyCorsCall("CORSMethodNotFound"); + message = await onCorsMessage; + await checkCorsMessage(message, "CORSMethodNotFound"); + + info("Test CORSMissingAllowCredentials"); + onCorsMessage = waitForMessage( + hud, + `Reason: expected ${quote("true")} in CORS ` + + `header ${quote("Access-Control-Allow-Credentials")}` + ); + makeFaultyCorsCall("CORSMissingAllowCredentials"); + message = await onCorsMessage; + await checkCorsMessage(message, "CORSMissingAllowCredentials"); + + info("Test CORSInvalidAllowMethod"); + onCorsMessage = waitForMessage( + hud, + `Reason: invalid token ${quote("xyz;")} in CORS ` + + `header ${quote("Access-Control-Allow-Methods")}` + ); + makeFaultyCorsCall("CORSInvalidAllowMethod"); + message = await onCorsMessage; + await checkCorsMessage(message, "CORSInvalidAllowMethod"); + + info("Test CORSInvalidAllowHeader"); + onCorsMessage = waitForMessage( + hud, + `Reason: invalid token ${quote("xyz;")} in CORS ` + + `header ${quote("Access-Control-Allow-Headers")}` + ); + makeFaultyCorsCall("CORSInvalidAllowHeader"); + message = await onCorsMessage; + await checkCorsMessage(message, "CORSInvalidAllowHeader"); + + info("Test CORSMissingAllowHeaderFromPreflight"); + onCorsMessage = waitForMessage( + hud, + `Reason: header ${quote("xyz")} is not allowed according to ` + + `header ${quote( + "Access-Control-Allow-Headers" + )} from CORS preflight response` + ); + makeFaultyCorsCall("CORSMissingAllowHeaderFromPreflight"); + message = await onCorsMessage; + await checkCorsMessage(message, "CORSMissingAllowHeaderFromPreflight"); + + // See Bug 1480671. + // XXX: how to make Origin to not be included in the request ? + // onCorsMessage = waitForMessage(hud, + // `Reason: CORS header ${quote("Origin")} cannot be added`); + // makeFaultyCorsCall("CORSOriginHeaderNotAdded"); + // message = await onCorsMessage; + // await checkCorsMessage(message, "CORSOriginHeaderNotAdded"); + + // See Bug 1480672. + // XXX: Failing with another error: Console message: Security Error: Content at + // http://example.com/browser/devtools/client/webconsole/test/browser/test-network-request.html + // may not load or link to file:///Users/nchevobbe/Projects/mozilla-central/devtools/client/webconsole/test/browser/sjs_cors-test-server.sjs. + // info("Test CORSRequestNotHttp"); + // onCorsMessage = waitForMessage(hud, "Reason: CORS request not http"); + // const dir = getChromeDir(getResolvedURI(gTestPath)); + // dir.append("sjs_cors-test-server.sjs"); + // makeFaultyCorsCall("CORSRequestNotHttp", Services.io.newFileURI(dir).spec); + // message = await onCorsMessage; + // await checkCorsMessage(message, "CORSRequestNotHttp"); +}); + +async function checkCorsMessage(message, category) { + const node = message.node; + ok( + node.classList.contains("error"), + "The cors message has the expected classname" + ); + const learnMoreLink = node.querySelector(".learn-more-link"); + ok(learnMoreLink, "There is a Learn more link displayed"); + const linkSimulation = await simulateLinkClick(learnMoreLink); + is( + linkSimulation.link, + getCategoryUrl(category), + "Click on the link opens the expected page" + ); +} + +function makeFaultyCorsCall(errorCategory, corsUrl) { + SpecialPowers.spawn( + gBrowser.selectedBrowser, + [[errorCategory, corsUrl]], + ([category, url]) => { + if (!url) { + const baseUrl = + "http://mochi.test:8888/browser/devtools/client/webconsole/test/browser"; + url = `${baseUrl}/sjs_cors-test-server.sjs?corsErrorCategory=${category}`; + } + + // Preflight request are not made for GET requests, so let's do a PUT. + const method = "PUT"; + const options = { method }; + if ( + category === "CORSNotSupportingCredentials" || + category === "CORSMissingAllowCredentials" + ) { + options.credentials = "include"; + } + + if (category === "CORSMissingAllowHeaderFromPreflight") { + options.headers = new content.Headers({ xyz: true }); + } + + content.fetch(url, options); + } + ); +} + +function quote(str) { + const openingQuote = String.fromCharCode(8216); + const closingQuote = String.fromCharCode(8217); + return `${openingQuote}${str}${closingQuote}`; +} + +function getCategoryUrl(category) { + return `${BASE_CORS_ERROR_URL}${category}?${BASE_CORS_ERROR_URL_PARAMS}`; +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_csp_ignore_reflected_xss_message.js b/devtools/client/webconsole/test/browser/browser_webconsole_csp_ignore_reflected_xss_message.js new file mode 100644 index 0000000000..7afe3c25f1 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_csp_ignore_reflected_xss_message.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that a file with an unsupported CSP directive ('reflected-xss filter') +// displays the appropriate message to the console. See Bug 1045902. + +"use strict"; + +const EXPECTED_RESULT = + "Not supporting directive \u2018reflected-xss\u2019. " + + "Directive and values will be ignored."; +const TEST_FILE = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test_console_csp_ignore_reflected_xss_message.html"; + +const TEST_URI = + "data:text/html;charset=utf8,Web Console CSP ignoring reflected XSS (bug 1045902)"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + await navigateTo(TEST_FILE); + + await waitFor(() => findMessage(hud, EXPECTED_RESULT, ".message.warn")); + ok( + true, + `CSP logs displayed in console when using "reflected-xss" directive` + ); + + Services.cache2.clear(); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_csp_violation.js b/devtools/client/webconsole/test/browser/browser_webconsole_csp_violation.js new file mode 100644 index 0000000000..17ab8d584e --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_csp_violation.js @@ -0,0 +1,116 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that the Web Console CSP messages for two META policies +// are correctly displayed. See Bug 1247459. + +"use strict"; + +add_task(async function() { + const TEST_URI = "data:text/html;charset=utf8,Web Console CSP violation test"; + const hud = await openNewTabAndConsole(TEST_URI); + await clearOutput(hud); + { + const TEST_VIOLATION = + "https://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-csp-violation.html"; + const CSP_VIOLATION_MSG = + "Content Security Policy: The page\u2019s settings " + + "blocked the loading of a resource at " + + "http://some.example.com/test.png (\u201cimg-src\u201d)."; + const onRepeatedMessage = waitForRepeatedMessage(hud, CSP_VIOLATION_MSG, 2); + await navigateTo(TEST_VIOLATION); + await onRepeatedMessage; + ok(true, "Received expected messages"); + } + await clearOutput(hud); + // Testing CSP Inline Violations + { + const TEST_VIOLATION = + "https://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-csp-violation-inline.html"; + const CSP_VIOLATION = + `Content Security Policy: The page’s settings blocked` + + ` the loading of a resource at inline (“style-src”).`; + const VIOLATION_LOCATION_HTML = "test-csp-violation-inline.html:18:1"; + const VIOLATION_LOCATION_JS = "test-csp-violation-inline.html:14:24"; + await navigateTo(TEST_VIOLATION); + // Triggering the Violation via HTML + let msg = await waitFor(() => findMessage(hud, CSP_VIOLATION)); + let locationNode = msg.querySelector(".message-location"); + info(`EXPECT ${VIOLATION_LOCATION_HTML} GOT: ${locationNode.textContent}`); + ok( + locationNode.textContent == VIOLATION_LOCATION_HTML, + "Printed the CSP Violation with HTML Context" + ); + // Triggering the Violation via JS + await clearOutput(hud); + msg = await executeAndWaitForMessage( + hud, + "window.violate()", + CSP_VIOLATION + ); + locationNode = msg.node.querySelector(".message-location"); + info(`EXPECT ${VIOLATION_LOCATION_JS} GOT: ${locationNode.textContent}`); + ok( + locationNode.textContent == VIOLATION_LOCATION_JS, + "Printed the CSP Violation with JS Context" + ); + } + await clearOutput(hud); + // Testing Base URI + { + const TEST_VIOLATION = + "https://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-csp-violation-base-uri.html"; + const CSP_VIOLATION = `Content Security Policy: The page’s settings blocked the loading of a resource at https://evil.com/ (“base-uri”).`; + const VIOLATION_LOCATION = "test-csp-violation-base-uri.html:15:24"; + await navigateTo(TEST_VIOLATION); + let msg = await waitFor(() => findMessage(hud, CSP_VIOLATION)); + ok(msg, "Base-URI validation was Printed"); + // Triggering the Violation via JS + await clearOutput(hud); + msg = await executeAndWaitForMessage( + hud, + "window.violate()", + CSP_VIOLATION + ); + const locationNode = msg.node.querySelector(".message-location"); + console.log(locationNode.textContent); + ok( + locationNode.textContent == VIOLATION_LOCATION, + "Base-URI validation was Printed with the Responsible JS Line" + ); + } + await clearOutput(hud); + // Testing CSP Form Action + { + const TEST_VIOLATION = + "https://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-csp-violation-form-action.html"; + const CSP_VIOLATION = `Content Security Policy: The page’s settings blocked the loading of a resource at https://evil.com/evil.com (“form-action”).`; + const VIOLATION_LOCATION = "test-csp-violation-form-action.html:14:39"; + + await navigateTo(TEST_VIOLATION); + const msg = await waitFor(() => findMessage(hud, CSP_VIOLATION)); + const locationNode = msg.querySelector(".message-location"); + info(`EXPECT ${VIOLATION_LOCATION} GOT: ${locationNode.textContent}`); + ok( + locationNode.textContent == VIOLATION_LOCATION, + "JS Line which Triggered the CSP-Form Action Violation was Printed" + ); + } + await clearOutput(hud); + // Testing CSP Frame Ancestors Directive + { + const TEST_VIOLATION = + "https://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-csp-violation-frame-ancestor-parent.html"; + const CSP_VIOLATION = + `Content Security Policy: The page’s settings blocked` + + ` the loading of a resource at ${TEST_VIOLATION} (“frame-ancestors”).`; + await navigateTo(TEST_VIOLATION); + const msg = await waitFor(() => findMessage(hud, CSP_VIOLATION)); + ok(msg, "Frame-Ancestors violation by html was printed"); + } +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_cspro.js b/devtools/client/webconsole/test/browser/browser_webconsole_cspro.js new file mode 100644 index 0000000000..56860a88f3 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_cspro.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* We are loading: +a script that is allowed by the CSP header but not by the CSPRO header +an image which is allowed by the CSPRO header but not by the CSP header. + +So we expect a warning (image has been blocked) and a report + (script should not load and was reported) + +The expected console messages in the constants CSP_VIOLATION_MSG and +CSP_REPORT_MSG are confirmed to be found in the console messages. + +See Bug 1010953. +*/ + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,Web Console CSP report only test"; +const TEST_VIOLATION = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-cspro.html"; +const CSP_VIOLATION_MSG = + "Content Security Policy: The page\u2019s settings blocked the loading of a resource " + + "at http://some.example.com/cspro.png (\u201cimg-src\u201d)."; +const CSP_REPORT_MSG = + "Content Security Policy: The page\u2019s settings observed the loading of a " + + "resource at http://some.example.com/cspro.js " + + "(\u201cscript-src\u201d). A CSP report is being sent."; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + const onCspViolationMessage = waitForMessage( + hud, + CSP_VIOLATION_MSG, + ".message.error" + ); + const onCspReportMessage = waitForMessage( + hud, + CSP_REPORT_MSG, + ".message.error" + ); + + info("Load a page with CSP warnings."); + await navigateTo(TEST_VIOLATION); + + await onCspViolationMessage; + await onCspReportMessage; + ok( + true, + "Confirmed that CSP and CSP-Report-Only log different messages to console" + ); + + await clearOutput(hud); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_css_error_impacted_elements.js b/devtools/client/webconsole/test/browser/browser_webconsole_css_error_impacted_elements.js new file mode 100644 index 0000000000..ec7221f206 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_css_error_impacted_elements.js @@ -0,0 +1,126 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Create a simple page for the iframe +const httpServer = createTestHTTPServer(); +httpServer.registerPathHandler(`/`, function(request, response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(` + <html> + <head> + <meta charset="utf-8"> + <style> + .subframe { + color: blouge; + } + </style> + </head> + <body class="subframe"> + <h1 class="subframe">Hello</h1> + <p class="subframe">sub-frame</p> + </body> + </html>`); +}); + +const TEST_URI = `data:text/html,<meta charset=utf8> + <style> + button { + cursor: unknownCursor; + } + </style> + <button id=1>Button 1</button> + <button id=2>Button 2</button> + <iframe src="http://localhost:${httpServer.identity.primaryPort}/"></iframe> + `; + +add_task(async function() { + // Enable CSS Warnings + await pushPref("devtools.webconsole.filter.css", true); + + const hud = await openNewTabAndConsole(TEST_URI); + const toolbox = hud.toolbox; + + // Load the inspector panel to make it possible to listen for new node selections + await toolbox.loadTool("inspector"); + const inspector = toolbox.getPanel("inspector"); + + info("Check the CSS warning message for the top level document"); + let messageNode = await waitFor(() => + findMessage(hud, "Error in parsing value for ‘cursor’", ".message.css") + ); + + info("Click on the expand arrow"); + messageNode.querySelector(".arrow").click(); + + await waitFor( + () => messageNode.querySelectorAll(".objectBox-node").length == 2 + ); + ok( + messageNode.textContent.includes("NodeList [ button#1, button#2 ]"), + "The message was expanded and shows the impacted elements" + ); + + let node = messageNode.querySelector(".objectBox-node"); + let openInInspectorIcon = node.querySelector(".open-inspector"); + ok(openInInspectorIcon !== null, "The is an open in inspector icon"); + + info( + "Clicking on the inspector icon and waiting for the inspector to be selected" + ); + let onInspectorSelected = toolbox.once("inspector-selected"); + let onInspectorUpdated = inspector.once("inspector-updated"); + let onNewNode = toolbox.selection.once("new-node-front"); + + openInInspectorIcon.click(); + + await onInspectorSelected; + await onInspectorUpdated; + let nodeFront = await onNewNode; + + ok(true, "Inspector selected and new node got selected"); + is(nodeFront.displayName, "button", "The expected node was selected"); + is(nodeFront.id, "1", "The expected node was selected"); + + info("Go back to the console"); + await toolbox.selectTool("webconsole"); + + info("Check the CSS warning message for the third-party iframe"); + messageNode = await waitFor(() => + findMessage(hud, "Error in parsing value for ‘color’", ".message.css") + ); + + info("Click on the expand arrow"); + messageNode.querySelector(".arrow").click(); + + await waitFor( + () => messageNode.querySelectorAll(".objectBox-node").length == 3 + ); + ok( + messageNode.textContent.includes( + "NodeList(3) [ body.subframe, h1.subframe, p.subframe ]" + ), + "The message was expanded and shows the impacted elements" + ); + node = messageNode.querySelectorAll(".objectBox-node")[2]; + openInInspectorIcon = node.querySelector(".open-inspector"); + ok(openInInspectorIcon !== null, "The is an open in inspector icon"); + + info( + "Clicking on the inspector icon and waiting for the inspector to be selected" + ); + onInspectorSelected = toolbox.once("inspector-selected"); + onInspectorUpdated = inspector.once("inspector-updated"); + onNewNode = toolbox.selection.once("new-node-front"); + + openInInspectorIcon.click(); + + await onInspectorSelected; + await onInspectorUpdated; + nodeFront = await onNewNode; + + ok(true, "Inspector selected and new node got selected"); + is(nodeFront.displayName, "p", "The expected node was selected"); + is(nodeFront.className, "subframe", "The expected node was selected"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_document_focus.js b/devtools/client/webconsole/test/browser/browser_webconsole_document_focus.js new file mode 100644 index 0000000000..1f8e7568e3 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_document_focus.js @@ -0,0 +1,99 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that focus is restored to content page after closing the console. See Bug 588342. +const TEST_URI = + "data:text/html;charset=utf-8,Test content focus after closing console"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + info("Focus after console is opened"); + ok(isInputFocused(hud), "input node is focused after console is opened"); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.onFocus = new Promise(resolve => { + content.addEventListener("focus", resolve, { once: true }); + }); + }); + + info("Closing console"); + await closeConsole(); + + const isFocused = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + async function() { + await content.onFocus; + return Services.focus.focusedWindow == content; + } + ); + ok(isFocused, "content document has focus after closing the console"); +}); + +add_task(async function testSeparateWindowToolbox() { + const hud = await openNewTabAndConsole(TEST_URI, true, "window"); + + info("Focus after console is opened"); + ok(isInputFocused(hud), "input node is focused after console is opened"); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.onFocus = new Promise(resolve => { + content.addEventListener("focus", resolve, { once: true }); + }); + }); + + info("Closing console"); + await closeConsole(); + + const isFocused = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + async function() { + await content.onFocus; + return Services.focus.focusedWindow == content; + } + ); + ok(isFocused, "content document has focus after closing the console"); +}); + +add_task(async function testSeparateWindowToolboxInactiveTab() { + await openNewTabAndConsole(TEST_URI, true, "window"); + + info("Focus after console is opened"); + const firstTab = gBrowser.selectedTab; + await addTab(`data:text/html,<meta charset=utf8>New tab XXX`); + + await SpecialPowers.spawn(firstTab.linkedBrowser, [], async () => { + // For some reason, there is no blur event fired on the document + await ContentTaskUtils.waitForCondition( + () => !content.browsingContext.isActive && !content.document.hasFocus(), + "Waiting for first tab to become inactive" + ); + content.onFocus = new Promise(resolve => { + content.addEventListener("focus", resolve, { once: true }); + }); + }); + + info("Closing console"); + await closeConsole(firstTab); + + const onFirstTabFocus = SpecialPowers.spawn( + firstTab.linkedBrowser, + [], + async function() { + await content.onFocus; + return "focused"; + } + ); + const timeoutRes = "time's out"; + const onTimeout = wait(2000).then(() => timeoutRes); + const res = await Promise.race([onFirstTabFocus, onTimeout]); + is( + res, + timeoutRes, + "original tab wasn't focused when closing the toolbox window" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_duplicate_errors.js b/devtools/client/webconsole/test/browser/browser_webconsole_duplicate_errors.js new file mode 100644 index 0000000000..556cdf75de --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_duplicate_errors.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that exceptions thrown by content don't show up twice in the Web +// Console. See Bug 582201. + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-duplicate-error.html"; + +add_task(async function() { + // On e10s, the exception is triggered in child process + // and is ignored by test harness + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } + const hud = await openNewTabAndConsole(TEST_URI); + + await waitFor(() => findMessage(hud, "fooDuplicateError1", ".message.error")); + + const errorMessages = hud.ui.outputNode.querySelectorAll(".message.error"); + is( + errorMessages.length, + 1, + "There's only one error message for fooDuplicateError1" + ); + is( + errorMessages[0].querySelector(".message-repeats"), + null, + "There is no repeat bubble on the error message" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_error_with_grouped_stack.js b/devtools/client/webconsole/test/browser/browser_webconsole_error_with_grouped_stack.js new file mode 100644 index 0000000000..a3f8bb90ce --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_error_with_grouped_stack.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check if an error with a stack containing grouped frames works as expected. + +"use strict"; + +const MESSAGE = "React Error"; +const TEST_URI = `data:text/html;charset=utf8,<script> + const x = new Error("${MESSAGE}"); + x.stack = "a@http://exampl.com:1:1\\n" + + "grouped@http://react.js:1:1\\n" + + "grouped2@http://react.js:1:1"; + console.error(x); +</script>`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + info("Wait for the error to be logged"); + const msgNode = await waitFor(() => findMessage(hud, MESSAGE)); + ok(!msgNode.classList.contains("open"), `Error logged not expanded`); + + const groupNode = await waitFor(() => msgNode.querySelector(".group")); + ok(groupNode, "The error object is logged as expected"); + + const onGroupExpanded = waitFor(() => + msgNode.querySelector(".frames-group.expanded") + ); + groupNode.click(); + await onGroupExpanded; + + ok(true, "The stacktrace group was expanded"); + is( + msgNode.querySelectorAll(".frame").length, + 3, + "Expected frames are displayed" + ); + ok( + !msgNode.classList.contains("open"), + `Error message is still not expanded` + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_error_with_longstring_stack.js b/devtools/client/webconsole/test/browser/browser_webconsole_error_with_longstring_stack.js new file mode 100644 index 0000000000..aa797c02f0 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_error_with_longstring_stack.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check if an error with a longString stack is displayed as expected. + +"use strict"; + +const MESSAGE = "Error with longString stack"; +const TEST_URI = `data:text/html;charset=utf8,<script> + const x = new Error("longString stack"); + x.stack = "s@http://exampl.com:1:1\\n".repeat(1000); + console.log("${MESSAGE}", x); +</script>`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + info("Wait for the error to be logged"); + const msgNode = await waitFor(() => findMessage(hud, MESSAGE)); + ok(msgNode, `Error logged`); + + const errorNode = msgNode.querySelector(".objectBox-stackTrace"); + ok(errorNode, "The error object is logged as expected"); + ok(errorNode.textContent.includes("longString stack")); + + info("Wait until the stacktrace gets rendered"); + const stackTraceElement = await waitFor(() => + errorNode.querySelector(".frames") + ); + + ok(stackTraceElement, "There's a stacktrace element"); + ok( + stackTraceElement.querySelectorAll(".frame .title").length > 0, + "Frames functions are displayed" + ); + ok( + stackTraceElement.querySelectorAll(".frame .location").length > 0, + "Frames location are displayed" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_error_with_unicode.js b/devtools/client/webconsole/test/browser/browser_webconsole_error_with_unicode.js new file mode 100644 index 0000000000..2f702c4f36 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_error_with_unicode.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check if an error with Unicode characters is reported correctly. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,<script>\u6e2c</script>"; +const EXPECTED_REPORT = "ReferenceError: \u6e2c is not defined"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + // On e10s, the exception is triggered in child process + // and is ignored by test harness + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } + + info("generate exception and wait for the message"); + + const msg = await waitFor(() => findMessage(hud, EXPECTED_REPORT)); + ok(msg, `Message found: "${EXPECTED_REPORT}"`); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_error_with_url.js b/devtools/client/webconsole/test/browser/browser_webconsole_error_with_url.js new file mode 100644 index 0000000000..bcc7e2bf14 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_error_with_url.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check if an error with Unicode characters is reported correctly. + +"use strict"; + +const longParam = "0".repeat(200); +const url1 = `https://example.com?v=${longParam}`; +const url2 = `https://example.org?v=${longParam}`; + +const TEST_URI = `data:text/html;charset=utf8,<script> + throw "Visit \u201c${url1}\u201d or \u201c${url2}\u201d to get more " + + "information on this error."; +</script>`; +const { ELLIPSIS } = require("devtools/shared/l10n"); + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + // On e10s, the exception is triggered in child process + // and is ignored by test harness + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } + + const getCroppedUrl = origin => { + const cropLimit = 120; + const half = cropLimit / 2; + const params = `?v=${"0".repeat( + half - origin.length - 3 + )}${ELLIPSIS}${"0".repeat(half)}`; + return `${origin}${params}`; + }; + + const EXPECTED_MESSAGE = `get more information on this error`; + + const msg = await waitFor(() => findMessage(hud, EXPECTED_MESSAGE)); + ok(msg, `Link in error message are cropped as expected`); + + const [comLink, orgLink] = Array.from(msg.querySelectorAll("a")); + is(comLink.getAttribute("href"), url1, "First link has expected url"); + is(comLink.getAttribute("title"), url1, "First link has expected tooltip"); + is( + comLink.textContent, + getCroppedUrl("https://example.com"), + "First link has expected text" + ); + + is(orgLink.getAttribute("href"), url2, "Second link has expected url"); + is(orgLink.getAttribute("title"), url2, "Second link has expected tooltip"); + is( + orgLink.textContent, + getCroppedUrl("https://example.org"), + "Second link has expected text" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_errors_after_page_reload.js b/devtools/client/webconsole/test/browser/browser_webconsole_errors_after_page_reload.js new file mode 100644 index 0000000000..1c2ca0682b --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_errors_after_page_reload.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that errors still show up in the Web Console after a page reload. +// See bug 580030: the error handler fails silently after page reload. +// https://bugzilla.mozilla.org/show_bug.cgi?id=580030 + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-error.html"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + info("Reload the content window"); + const onNavigate = hud.currentTarget.once("navigate"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.wrappedJSObject.location.reload(); + }); + await onNavigate; + info("Target navigated"); + + // On e10s, the exception is triggered in child process + // and is ignored by test harness + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } + + const onMessage = waitForMessage(hud, "fooBazBaz is not defined"); + BrowserTestUtils.synthesizeMouseAtCenter( + "button", + {}, + gBrowser.selectedBrowser + ); + await onMessage; + + ok(true, "Received the expected error message"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_eval_error.js b/devtools/client/webconsole/test/browser/browser_webconsole_eval_error.js new file mode 100644 index 0000000000..f4498590ed --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_eval_error.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that throwing uncaught errors while doing console evaluations shows the expected +// error message, with or without stack, in the console. + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-eval-error.html"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + execute(hud, "throwErrorObject()"); + await checkMessageStack(hud, "ThrowErrorObject", [6, 1]); + + execute(hud, "throwValue(40 + 2)"); + await checkMessageStack(hud, "42", [14, 10, 1]); + + await checkThrowingEvaluationWithStack(hud, `"bloop"`, "Uncaught bloop"); + await checkThrowingEvaluationWithStack(hud, `""`, "Uncaught <empty string>"); + await checkThrowingEvaluationWithStack(hud, `0`, "Uncaught 0"); + await checkThrowingEvaluationWithStack(hud, `null`, "Uncaught null"); + await checkThrowingEvaluationWithStack( + hud, + `undefined`, + "Uncaught undefined" + ); + await checkThrowingEvaluationWithStack(hud, `false`, "Uncaught false"); + + await checkThrowingEvaluationWithStack( + hud, + `new Error("watermelon")`, + "Uncaught Error: watermelon" + ); + + await checkThrowingEvaluationWithStack( + hud, + `(err = new Error("lettuce"), err.name = "VegetableError", err)`, + "Uncaught VegetableError: lettuce" + ); + + await checkThrowingEvaluationWithStack( + hud, + `{ fav: "eggplant" }`, + `Uncaught Object { fav: "eggplant" }` + ); + info("Check that object in errors can be expanded"); + const rejectedObjectMessage = findMessage(hud, "eggplant", ".error"); + const oi = rejectedObjectMessage.querySelector(".tree"); + ok(true, "The object was rendered in an ObjectInspector"); + + info("Expanding the object"); + const onOiExpanded = waitFor(() => { + return oi.querySelectorAll(".node").length === 3; + }); + oi.querySelector(".arrow").click(); + await onOiExpanded; + + ok( + oi.querySelector(".arrow").classList.contains("expanded"), + "Object expanded" + ); + + // The object inspector now looks like: + // {...} + // | fav: "eggplant" + // | <prototype>: Object { ... } + + const oiNodes = oi.querySelectorAll(".node"); + is(oiNodes.length, 3, "There is the expected number of nodes in the tree"); + + ok(oiNodes[0].textContent.includes(`{\u2026}`)); + ok(oiNodes[1].textContent.includes(`fav: "eggplant"`)); + ok(oiNodes[2].textContent.includes(`<prototype>: Object { \u2026 }`)); + + execute(hud, `1 + @`); + const messageNode = await waitFor(() => + findMessage(hud, "illegal character U+0040") + ); + is( + messageNode.querySelector(".frames"), + null, + "There's no stacktrace for a SyntaxError evaluation" + ); +}); + +function checkThrowingEvaluationWithStack(hud, expression, expectedMessage) { + execute( + hud, + ` + a = () => {throw ${expression}}; + b = () => a(); + c = () => b(); + d = () => c(); + d(); + ` + ); + return checkMessageStack(hud, expectedMessage, [2, 3, 4, 5, 6]); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_eval_in_debugger_stackframe.js b/devtools/client/webconsole/test/browser/browser_webconsole_eval_in_debugger_stackframe.js new file mode 100644 index 0000000000..69be9cd9ab --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_eval_in_debugger_stackframe.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that makes sure web console eval happens in the user-selected stackframe +// from the js debugger. + +"use strict"; +/* import-globals-from head.js*/ + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-eval-in-stackframe.html"; + +add_task(async function() { + // TODO: Remove this pref change when middleware for terminating requests + // when closing a panel is implemented + await pushPref("devtools.debugger.features.inline-preview", false); + + info("open the console"); + const hud = await openNewTabAndConsole(TEST_URI); + + info("Check `foo` value"); + await executeAndWaitForMessage(hud, "foo", "globalFooBug783499", ".result"); + ok(true, "|foo| value is correct"); + + info("Assign and check `foo2` value"); + await executeAndWaitForMessage( + hud, + "foo2 = 'newFoo'; window.foo2", + "newFoo", + ".result" + ); + ok(true, "'newFoo' is displayed after adding `foo2`"); + + info("Open the debugger and then select the console again"); + await openDebugger(); + const toolbox = hud.toolbox; + const dbg = createDebuggerContext(toolbox); + + await openConsole(); + + info("Check `foo + foo2` value"); + await executeAndWaitForMessage( + hud, + "foo + foo2", + "globalFooBug783499newFoo", + ".result" + ); + + info("Select the debugger again"); + await openDebugger(); + await pauseDebugger(dbg); + + const stackFrames = dbg.selectors.getCallStackFrames(); + + info("frames added, select the console again"); + await openConsole(); + + info("Check `foo + foo2` value when paused"); + await executeAndWaitForMessage( + hud, + "foo + foo2", + "globalFooBug783499foo2SecondCall", + ".result" + ); + ok(true, "`foo + foo2` from `secondCall()`"); + + info("select the debugger and select the frame (1)"); + await openDebugger(); + + await selectFrame(dbg, stackFrames[1]); + + await openConsole(); + + info("Check `foo + foo2 + foo3` value when paused on a given frame"); + await executeAndWaitForMessage( + hud, + "foo + foo2 + foo3", + "fooFirstCallnewFoofoo3FirstCall", + ".result" + ); + ok(true, "`foo + foo2 + foo3` from `firstCall()`"); + + await executeAndWaitForMessage( + hud, + "foo = 'abba'; foo3 = 'bug783499'; foo + foo3", + "abbabug783499", + ".result" + ); + ok(true, "`foo + foo3` updated in `firstCall()`"); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + is( + content.wrappedJSObject.foo, + "globalFooBug783499", + "`foo` in content window" + ); + is(content.wrappedJSObject.foo2, "newFoo", "`foo2` in content window"); + ok( + !content.wrappedJSObject.foo3, + "`foo3` was not added to the content window" + ); + }); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_eval_in_debugger_stackframe2.js b/devtools/client/webconsole/test/browser/browser_webconsole_eval_in_debugger_stackframe2.js new file mode 100644 index 0000000000..76d9d91cb8 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_eval_in_debugger_stackframe2.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test to make sure that web console commands can fire while paused at a +// breakpoint that was triggered from a JS call. Relies on asynchronous js +// evaluation over the protocol - see Bug 1088861. + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-eval-in-stackframe.html"; + +add_task(async function() { + // TODO: Remove this pref change when middleware for terminating requests + // when closing a panel is implemented + await pushPref("devtools.debugger.features.inline-preview", false); + + info("open the console"); + const hud = await openNewTabAndConsole(TEST_URI); + + info("open the debugger"); + await openDebugger(); + + const toolbox = hud.toolbox; + const dbg = createDebuggerContext(toolbox); + + // firstCall calls secondCall, which has a debugger statement, so we'll be paused. + const onFirstCallMessageReceived = waitForMessage(hud, "undefined"); + + const unresolvedSymbol = Symbol(); + let firstCallEvaluationResult = unresolvedSymbol; + onFirstCallMessageReceived.then(message => { + firstCallEvaluationResult = message; + }); + execute(hud, "firstCall()"); + + info("Waiting for a frame to be added"); + await waitForPaused(dbg); + + info("frames added, select the console again"); + await openConsole(); + + info("Executing basic command while paused"); + await executeAndWaitForMessage(hud, "1 + 2", "3", ".result"); + ok(true, "`1 + 2` was evaluated whith debugger paused"); + + info("Executing command using scoped variables while paused"); + await executeAndWaitForMessage( + hud, + "foo + foo2", + `"globalFooBug783499foo2SecondCall"`, + ".result" + ); + ok(true, "`foo + foo2` was evaluated as expected with debugger paused"); + + info( + "Checking the first command, which is the last to resolve since it paused" + ); + ok( + firstCallEvaluationResult === unresolvedSymbol, + "firstCall was not evaluated yet" + ); + + info("Resuming the thread"); + dbg.actions.resume(dbg.selectors.getThreadContext()); + + await onFirstCallMessageReceived; + ok( + firstCallEvaluationResult !== unresolvedSymbol, + "firstCall() returned correct value" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_eval_sources.js b/devtools/client/webconsole/test/browser/browser_webconsole_eval_sources.js new file mode 100644 index 0000000000..267192de25 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_eval_sources.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-eval-sources.html"; + +// Test that stack/message links in console API and error messages originating +// from eval code go to a source in the debugger. This should work even when the +// console is opened first. +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const target = await TargetFactory.forTab(gBrowser.selectedTab); + const toolbox = gDevTools.getToolbox(target); + + let messageNode = await waitFor(() => findMessage(hud, "BAR")); + await clickFirstStackElement(hud, messageNode, true); + + const dbg = toolbox.getPanel("jsdebugger"); + + is( + dbg._selectors.getSelectedSource(dbg._getState()).url, + null, + "expected source url" + ); + + await testOpenInDebugger(hud, toolbox, "FOO", false); + await testOpenInDebugger(hud, toolbox, "BAR", false); + + // Test that links in the API work when the eval source has a sourceURL property + // which is not considered to be a valid URL. + await testOpenInDebugger(hud, toolbox, "BAZ", false); + + // Test that stacks in console.trace() calls work. + messageNode = await waitFor(() => findMessage(hud, "TRACE")); + await clickFirstStackElement(hud, messageNode, false); + + is( + /my-foo.js/.test(dbg._selectors.getSelectedSource(dbg._getState()).url), + true, + "expected source url" + ); +}); + +async function clickFirstStackElement(hud, message, needsExpansion) { + if (needsExpansion) { + const button = message.querySelector(".collapse-button"); + ok(button, "has button"); + button.click(); + } + + let frame; + await waitUntil(() => { + frame = message.querySelector(".stacktrace .frame"); + return !!frame; + }); + + const onSourceOpenedInDebugger = once(hud, "source-in-debugger-opened"); + await EventUtils.sendMouseEvent({ type: "mousedown" }, frame); + await onSourceOpenedInDebugger; +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_execution_scope.js b/devtools/client/webconsole/test/browser/browser_webconsole_execution_scope.js new file mode 100644 index 0000000000..58c74eb0e9 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_execution_scope.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that commands run by the user are executed in content space. + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-console.html"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + await clearOutput(hud); + + const onInputMessage = waitForMessage( + hud, + "window.location.href;", + ".message.command" + ); + const onEvaluationResultMessage = waitForMessage( + hud, + TEST_URI, + ".message.result" + ); + execute(hud, "window.location.href;"); + + let message = await onInputMessage; + ok(message, "Input message is displayed with the expected class"); + + message = await onEvaluationResultMessage; + ok(message, "EvaluationResult message is displayed with the expected class"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_external_script_errors.js b/devtools/client/webconsole/test/browser/browser_webconsole_external_script_errors.js new file mode 100644 index 0000000000..ecadfef037 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_external_script_errors.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// See Bug 597136. + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/" + + "test-external-script-errors.html"; + +add_task(async function() { + // On e10s, the exception is triggered in child process + // and is ignored by test harness + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } + + const hud = await openNewTabAndConsole(TEST_URI); + + const onMessage = waitForMessage(hud, "bogus is not defined"); + BrowserTestUtils.synthesizeMouseAtCenter( + "button", + {}, + gBrowser.selectedBrowser + ); + await onMessage; + + ok(true, "Received the expected message"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_file_uri.js b/devtools/client/webconsole/test/browser/browser_webconsole_file_uri.js new file mode 100644 index 0000000000..3374fab441 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_file_uri.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// XXX Remove this when the file is migrated to the new frontend. +/* eslint-disable no-undef */ + +// See Bug 595223. + +const PREF = "devtools.webconsole.persistlog"; +const TEST_FILE = "test-network.html"; + +var hud; + +add_task(async function() { + Services.prefs.setBoolPref(PREF, true); + + const jar = getJar(getRootDirectory(gTestPath)); + const dir = jar + ? extractJarToTmp(jar) + : getChromeDir(getResolvedURI(gTestPath)); + + dir.append(TEST_FILE); + const uri = Services.io.newFileURI(dir); + + // Open tab with correct remote type so we don't switch processes when we load + // the file:// URI, otherwise we won't get the same web console. + const remoteType = E10SUtils.getRemoteTypeForURI( + uri.spec, + gMultiProcessBrowser, + gFissionBrowser + ); + await loadTab("about:blank", remoteType); + + hud = await openConsole(); + await clearOutput(hud); + + await navigateTo(uri.spec); + + await testMessages(); + + Services.prefs.clearUserPref(PREF); + hud = null; +}); + +function testMessages() { + return waitForMessages({ + webconsole: hud, + messages: [ + { + text: "running network console logging tests", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }, + { + text: "test-network.html", + category: CATEGORY_NETWORK, + severity: SEVERITY_LOG, + }, + { + text: "test-image.png", + category: CATEGORY_NETWORK, + severity: SEVERITY_LOG, + }, + { + text: "testscript.js", + category: CATEGORY_NETWORK, + severity: SEVERITY_LOG, + }, + ], + }); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_filter_buttons_overflow.js b/devtools/client/webconsole/test/browser/browser_webconsole_filter_buttons_overflow.js new file mode 100644 index 0000000000..325ee06ffa --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_filter_buttons_overflow.js @@ -0,0 +1,80 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test the locations of the filter buttons in the Webconsole's Filter Bar. + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-console.html"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const win = hud.browserWindow; + const initialWindowWidth = win.outerWidth; + + info( + "Check filter buttons are inline with filter input when window is large." + ); + resizeWindow(1500, win); + await waitForFilterBarLayout(hud, ".wide"); + ok(true, "The filter bar has the wide layout"); + + info("Check filter buttons overflow when window is small."); + resizeWindow(400, win); + await waitForFilterBarLayout(hud, ".narrow"); + ok(true, "The filter bar has the narrow layout"); + + info("Check that the filter bar layout changes when opening the sidebar"); + resizeWindow(750, win); + await waitForFilterBarLayout(hud, ".wide"); + const onMessage = waitForMessage(hud, "world"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.console.log({ hello: "world" }); + }); + const { node } = await onMessage; + const object = node.querySelector(".object-inspector .objectBox-object"); + info("Ctrl+click on an object to put it in the sidebar"); + const onSidebarShown = waitFor(() => + hud.ui.document.querySelector(".sidebar") + ); + AccessibilityUtils.setEnv({ + // Component that renders an object handles keyboard interactions on the + // container level. + mustHaveAccessibleRule: false, + }); + await EventUtils.sendMouseEvent( + { + type: "click", + [Services.appinfo.OS === "Darwin" ? "metaKey" : "ctrlKey"]: true, + }, + object, + hud.ui.window + ); + AccessibilityUtils.resetEnv(); + const sidebar = await onSidebarShown; + await waitForFilterBarLayout(hud, ".narrow"); + ok(true, "FilterBar layout changed when opening the sidebar"); + + info("Check that filter bar layout changes when closing the sidebar"); + sidebar.querySelector(".sidebar-close-button").click(); + await waitForFilterBarLayout(hud, ".wide"); + + info("Restore the original window size"); + await resizeWindow(initialWindowWidth, win); + + await closeTabAndToolbox(); +}); + +function resizeWindow(width, win) { + const onResize = once(win, "resize"); + win.resizeTo(width, win.outerHeight); + info("Wait for window resize event"); + return onResize; +} + +function waitForFilterBarLayout(hud, query) { + return waitFor(() => + hud.ui.outputNode.querySelector(`.webconsole-filteringbar-wrapper${query}`) + ); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_filter_by_input.js b/devtools/client/webconsole/test/browser/browser_webconsole_filter_by_input.js new file mode 100644 index 0000000000..d143684558 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_filter_by_input.js @@ -0,0 +1,294 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that the text filter box works to filter based on location. + +"use strict"; + +// In this test, we are trying to test the filtering functionality of the filter +// input. We test filtering not only for the contents of logs themseleves but +// also for the filenames. +// +// We simulate an HTML file which executes two Javascript files, one with an +// ASCII filename outputs some ASCII logs and the other one with a Unicode +// filename outputs some Unicode logs. + +const SEASON = { + english: "season", + chinese: "\u5b63", +}; +const SEASONS = [ + { + english: "spring", + chinese: "\u6625", + escapedChinese: "\\u6625", + }, + { + english: "summer", + chinese: "\u590f", + escapedChinese: "\\u590f", + }, + { + english: "autumn", + chinese: "\u79cb", + escapedChinese: "\\u79cb", + }, + { + english: "winter", + chinese: "\u51ac", + escapedChinese: "\\u51ac", + }, +]; + +// filenames +const HTML_FILENAME = `test.html`; +const JS_ASCII_FILENAME = `${SEASON.english}.js`; +const JS_UNICODE_FILENAME = `${SEASON.chinese}.js`; +const ENCODED_JS_UNICODE_FILENAME = encodeURIComponent(JS_UNICODE_FILENAME); + +// file contents +const HTML_CONSOLE_OUTPUT = `Test filtering ${SEASON.english} names.`; +const HTML_CONTENT = `<!DOCTYPE html> +<meta charset="utf-8"> +<title>Test filtering logs by filling keywords in the filter input.</title> +<script> +console.log("${HTML_CONSOLE_OUTPUT}"); +</script> +<script src="/${JS_ASCII_FILENAME}"></script> +<script src="/${ENCODED_JS_UNICODE_FILENAME}"></script>`; + +add_task(async function() { + const testUrl = createServerAndGetTestUrl(); + const hud = await openNewTabAndConsole(testUrl); + + // Let's wait for the last logged message of each file to be displayed in the + // output, in order to make sure all the logged messages have been displayed. + const lastSeason = SEASONS[SEASONS.length - 1]; + await waitFor( + () => + findMessage(hud, lastSeason.english) && + findMessage(hud, lastSeason.chinese) + ); + + // One external Javascript file outputs every season name in English, and the + // other Javascript file outputs every season name in Chinese. + // The HTML file outputs one line on its own. + // So the total number of all the logs is the doubled number of seasons plus + // one. + let visibleLogs = getVisibleLogs(hud); + is( + visibleLogs.length, + SEASONS.length * 2 + 1, + "the total number of all the logs before starting filtering" + ); + checkLogContent(visibleLogs[0], HTML_CONSOLE_OUTPUT, HTML_FILENAME); + for (let i = 0; i < SEASONS.length; i++) { + checkLogContent(visibleLogs[i + 1], SEASONS[i].english, JS_ASCII_FILENAME); + } + for (let i = 0; i < SEASONS.length; i++) { + checkLogContent( + visibleLogs[i + 1 + SEASONS.length], + SEASONS[i].chinese, + JS_UNICODE_FILENAME + ); + } + // checking the visibility of clear button, it should be visible only when + // there is text inside filter input box + await setFilterState(hud, { text: "" }); + is(getClearButton(hud).hidden, true, "Clear button is hidden"); + await setFilterState(hud, { text: JS_ASCII_FILENAME }); + is(getClearButton(hud).hidden, false, "Clear button is visible"); + + // All the logs outputted by the ASCII Javascript file are visible, the others + // are hidden. + await setFilterState(hud, { text: JS_ASCII_FILENAME }); + visibleLogs = getVisibleLogs(hud); + is( + visibleLogs.length, + SEASONS.length, + `the number of all the logs containing ${JS_ASCII_FILENAME}` + ); + for (let i = 0; i < SEASONS.length; i++) { + checkLogContent(visibleLogs[i], SEASONS[i].english, JS_ASCII_FILENAME); + } + + // Every season name in English is outputted once. + for (const curSeason of SEASONS) { + await setFilterState(hud, { text: curSeason.english }); + visibleLogs = getVisibleLogs(hud); + is( + visibleLogs.length, + 1, + `the number of all the logs containing ${curSeason.english}` + ); + checkLogContent(visibleLogs[0], curSeason.english, JS_ASCII_FILENAME); + } + + // All the logs outputted by the Unicode Javascript file are visible, the + // others are hidden. + await setFilterState(hud, { text: JS_UNICODE_FILENAME }); + visibleLogs = getVisibleLogs(hud); + is( + visibleLogs.length, + SEASONS.length, + `the number of all the logs containing ${JS_UNICODE_FILENAME}` + ); + for (let i = 0; i < SEASONS.length; i++) { + checkLogContent(visibleLogs[i], SEASONS[i].chinese, JS_UNICODE_FILENAME); + } + + // Every season name in Chinese is outputted once. + for (const curSeason of SEASONS) { + await setFilterState(hud, { text: curSeason.chinese }); + visibleLogs = getVisibleLogs(hud); + is( + visibleLogs.length, + 1, + `the number of all the logs containing ${curSeason.chinese}` + ); + checkLogContent(visibleLogs[0], curSeason.chinese, JS_UNICODE_FILENAME); + } + + // The filename of the ASCII Javascript file contains the English word season, + // so all the logs outputted by the file are visible, besides, the HTML + // outputs one line containing the English word season, so it is also visible. + // The other logs are hidden. So the number of all the visible logs is the + // season number plus one. + await setFilterState(hud, { text: SEASON.english }); + visibleLogs = getVisibleLogs(hud); + is( + visibleLogs.length, + SEASONS.length + 1, + `the number of all the logs containing ${SEASON.english}` + ); + checkLogContent(visibleLogs[0], HTML_CONSOLE_OUTPUT, HTML_FILENAME); + for (let i = 0; i < SEASONS.length; i++) { + checkLogContent(visibleLogs[i + 1], SEASONS[i].english, JS_ASCII_FILENAME); + } + + // The filename of the Unicode Javascript file contains the Chinese word + // season, so all the logs outputted by the file are visible. The other logs + // are hidden. So the number of all the visible logs is the season number. + await setFilterState(hud, { text: SEASON.chinese }); + visibleLogs = getVisibleLogs(hud); + is( + visibleLogs.length, + SEASONS.length, + `the number of all the logs containing ${SEASON.chinese}` + ); + for (let i = 0; i < SEASONS.length; i++) { + checkLogContent(visibleLogs[i], SEASONS[i].chinese, JS_UNICODE_FILENAME); + } + + // After clearing the text in the filter input box, all the logs are visible + // again. + await setFilterState(hud, { text: "" }); + checkAllMessagesAreVisible(hud); + + // clearing the text in the filter input box using clear button, so after which + // all logs will be visible again + await setFilterState(hud, { text: JS_ASCII_FILENAME }); + + info("Click the input clear button"); + clickClearButton(hud); + await waitFor(() => getClearButton(hud).hidden === true); + checkAllMessagesAreVisible(hud); +}); + +// Create an HTTP server to simulate a response for the a URL request and return +// the URL. +function createServerAndGetTestUrl() { + const httpServer = createTestHTTPServer(); + + httpServer.registerContentType("html", "text/html"); + httpServer.registerContentType("js", "application/javascript"); + + httpServer.registerPathHandler("/" + HTML_FILENAME, function( + request, + response + ) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(HTML_CONTENT); + }); + httpServer.registerPathHandler("/" + JS_ASCII_FILENAME, function( + request, + response + ) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/javascript", false); + let content = ""; + for (const curSeason of SEASONS) { + content += `console.log("${curSeason.english}");`; + } + response.write(content); + }); + httpServer.registerPathHandler("/" + ENCODED_JS_UNICODE_FILENAME, function( + request, + response + ) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/javascript", false); + let content = ""; + for (const curSeason of SEASONS) { + content += `console.log("${curSeason.escapedChinese}");`; + } + response.write(content); + }); + const port = httpServer.identity.primaryPort; + return `http://localhost:${port}/${HTML_FILENAME}`; +} + +function getClearButton(hud) { + return hud.ui.outputNode.querySelector( + ".devtools-searchbox .devtools-searchinput-clear" + ); +} + +function clickClearButton(hud) { + getClearButton(hud).click(); +} + +function getVisibleLogs(hud) { + const outputNode = hud.ui.outputNode; + return outputNode.querySelectorAll(".message"); +} + +function checkAllMessagesAreVisible(hud) { + const visibleLogs = getVisibleLogs(hud); + is( + visibleLogs.length, + SEASONS.length * 2 + 1, + "the total number of all the logs after clearing filtering" + ); + checkLogContent(visibleLogs[0], HTML_CONSOLE_OUTPUT, HTML_FILENAME); + for (let i = 0; i < SEASONS.length; i++) { + checkLogContent(visibleLogs[i + 1], SEASONS[i].english, JS_ASCII_FILENAME); + } + for (let i = 0; i < SEASONS.length; i++) { + checkLogContent( + visibleLogs[i + 1 + SEASONS.length], + SEASONS[i].chinese, + JS_UNICODE_FILENAME + ); + } +} +/** + * Check if the content of a log message is what we expect. + * + * @param Object node + * The node for the log message. + * @param String expectedMessageBody + * The string we expect to match the message body in the log message. + * @param String expectedFilename + * The string we expect to match the filename in the log message. + */ +function checkLogContent(node, expectedMessageBody, expectedFilename) { + const messageBody = node.querySelector(".message-body").textContent; + const location = node.querySelector(".message-location").textContent; + // The location detail contains the line number and the column number, let's + // strip them to get the filename. + const filename = location.split(":")[0]; + + is(messageBody, expectedMessageBody, "the expected message body"); + is(filename, expectedFilename, "the expected filename"); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_filter_by_regex_input.js b/devtools/client/webconsole/test/browser/browser_webconsole_filter_by_regex_input.js new file mode 100644 index 0000000000..c34836d5ad --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_filter_by_regex_input.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const MESSAGES = [ + "123-456-7890", + "foo@bar.com", + "http://abc.com/q?fizz=buzz&alpha=beta/", + "https://xyz.com/?path=/world", + "foooobaaaar", + "123 working", +]; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-console-filter-by-regex-input.html"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { outputNode } = hud.ui; + + await waitFor(() => findMessage(hud, MESSAGES[5]), null, 200); + + let filteredNodes; + + info("Filter out messages that begin with numbers"); + await setFilterInput(hud, "/^[0-9]/", MESSAGES[5]); + filteredNodes = outputNode.querySelectorAll(".message"); + checkFilteredMessages(filteredNodes, [MESSAGES[0], MESSAGES[5]], 2); + + info("Filter out messages that are phone numbers"); + await setFilterInput(hud, "/\\d{3}\\-\\d{3}\\-\\d{4}/", MESSAGES[0]); + filteredNodes = outputNode.querySelectorAll(".message"); + checkFilteredMessages(filteredNodes, [MESSAGES[0]], 1); + + info("Filter out messages that are an email address"); + await setFilterInput(hud, "/^\\w+@[a-zA-Z]+\\.[a-zA-Z]{2,3}$/", MESSAGES[1]); + filteredNodes = outputNode.querySelectorAll(".message"); + checkFilteredMessages(filteredNodes, [MESSAGES[1]], 1); + + info("Filter out messages that contain query strings"); + await setFilterInput(hud, "/\\?([^=&]+=[^=&]+&?)*\\//", MESSAGES[2]); + filteredNodes = outputNode.querySelectorAll(".message"); + checkFilteredMessages(filteredNodes, [MESSAGES[2]], 1); + + // If regex is invalid, do a normal text search instead + info("Filter messages using normal text search if regex is invalid"); + await setFilterInput(hud, "/?path=/", MESSAGES[3]); + filteredNodes = outputNode.querySelectorAll(".message"); + checkFilteredMessages(filteredNodes, [MESSAGES[3]], 1); + + info("Filter out messages not ending with numbers"); + await setFilterInput(hud, "/[^0-9]$/", MESSAGES[5]); + filteredNodes = outputNode.querySelectorAll(".message"); + checkFilteredMessages( + filteredNodes, + [MESSAGES[1], MESSAGES[2], MESSAGES[3], MESSAGES[4], MESSAGES[5]], + 5 + ); + + info("Filter out messages ending with numbers"); + await setFilterInput(hud, "-/[^0-9]$/", MESSAGES[0]); + filteredNodes = outputNode.querySelectorAll(".message"); + checkFilteredMessages(filteredNodes, [MESSAGES[0]], 1); +}); + +async function setFilterInput(hud, value, lastMessage) { + await setFilterState(hud, { text: value }); + await waitFor(() => findMessage(hud, lastMessage), null, 200); +} + +function checkFilteredMessages(filteredNodes, expectedMessages, expectedCount) { + is( + filteredNodes.length, + expectedCount, + `${expectedCount} messages should be displayed` + ); + + filteredNodes.forEach((node, id) => { + const messageBody = node.querySelector(".message-body").textContent; + ok(messageBody, expectedMessages[id]); + }); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_filter_groups.js b/devtools/client/webconsole/test/browser/browser_webconsole_filter_groups.js new file mode 100644 index 0000000000..865834d178 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_filter_groups.js @@ -0,0 +1,262 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests filters. + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-console-filter-groups.html"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + await waitFor(() => findMessage(hud, "[a]") && findMessage(hud, "[j]")); + + /* + * The output looks like the following: + * ▼[a] + * | [b] + * | [c] + * | ▼[d] + * | | [e] + * | [f] + * | [g] + * [h] + * [i] + * ▶︎[j] + * ▼[group] + * | ▼[subgroup] + * | | [subitem] + */ + + await setFilterState(hud, { + text: "[", + }); + + checkMessages( + hud, + ` + ▼[a] + | [b] + | [c] + | ▼[d] + | | [e] + | [f] + | [g] + [h] + [i] + ▶︎[j] + ▼[group] + | ▼[subgroup] + | | [subitem] + `, + `Got all expected messages when filtering on common character "["` + ); + + info("Check that filtering on a group substring shows all children"); + await setFilterState(hud, { + text: "[a]", + }); + + checkMessages( + hud, + ` + ▼[a] + | [b] + | [c] + | ▼[d] + | | [e] + | [f] + | [g] + `, + `Got all children of group when filtering on "[a]"` + ); + + info( + "Check that filtering on a group child substring shows the parent group message" + ); + await setFilterState(hud, { + text: "[b]", + }); + + checkMessages( + hud, + ` + ▼[a] + | [b] + `, + `Got matching message and parent group when filtering on "[b]"` + ); + + info( + "Check that filtering on a sub-group substring shows subgroup children and parent group" + ); + await setFilterState(hud, { + text: "[d]", + }); + + checkMessages( + hud, + ` + ▼[a] + | ▼[d] + | | [e] + `, + `Got matching message, subgroup children and parent group when filtering on "[d]"` + ); + + info("Check that filtering on a sub-group child shows all parent groups"); + await setFilterState(hud, { + text: "[e]", + }); + + checkMessages( + hud, + ` + ▼[a] + | ▼[d] + | | [e] + `, + `Got matching message and parent groups when filtering on "[e]"` + ); + + info( + "Check that filtering a message in a collapsed group shows the parent group" + ); + await setFilterState(hud, { + text: "[k]", + }); + + checkMessages( + hud, + ` + [j] + `, + `Got collapsed group when filtering on "[k]"` + ); + + info("Check that filtering a message not in a group hides all groups"); + await setFilterState(hud, { + text: "[h]", + }); + + checkMessages( + hud, + ` + [h] + `, + `Got only matching message when filtering on "[h]"` + ); + + await setFilterState(hud, { + text: "", + }); + + const groupA = findMessage(hud, "[a]"); + const groupJ = findMessage(hud, "[j]"); + + toggleGroup(groupA); + toggleGroup(groupJ); + + checkMessages( + hud, + `▶︎[a] + [h] + [i] + ▼[j] + | [k] + ▼[group] + | ▼[subgroup] + | | [subitem]`, + `Got matching messages after collapsing and expanding group messages` + ); + + info( + "Check that filtering on expanded groupCollapsed messages does not hide children" + ); + await setFilterState(hud, { + text: "[k]", + }); + + checkMessages( + hud, + ` + ▼[j] + | [k] + `, + `Got only matching message when filtering on "[k]"` + ); + + info( + "Check that filtering on collapsed group messages shows only the group parent" + ); + + await setFilterState(hud, { + text: "[e]", + }); + + checkMessages( + hud, + ` + [a] + `, + `Got only matching message when filtering on "[e]"` + ); + + info( + "Check that filtering on collapsed, nested group messages shows only expaded ancestor" + ); + + // We clear the filter so subgroup is visible and can be toggled + await setFilterState(hud, { + text: "", + }); + + const subGroup = findMessage(hud, "[subgroup]"); + toggleGroup(subGroup); + + await setFilterState(hud, { + text: "[subitem", + }); + + checkMessages( + hud, + ` + ▼[group] + | ▶︎[subgroup] + `, + `Got only visible ancestors when filtering on "[subitem"` + ); +}); + +/** + * + * @param {WebConsole} hud + * @param {String} expected + * @param {String} assertionMessage + */ +function checkMessages(hud, expected, assertionMessage) { + const expectedMessages = expected + .split("\n") + .filter(line => line.trim()) + .map(line => line.replace(/(▶︎|▼|\|)/g, "").trim()); + + const messages = Array.from( + hud.ui.outputNode.querySelectorAll(".message .message-body") + ).map(el => el.innerText.trim()); + + const formatMessages = arr => `\n${arr.join("\n")}\n`; + + is( + formatMessages(messages), + formatMessages(expectedMessages), + assertionMessage + ); +} + +function toggleGroup(node) { + const toggleArrow = node.querySelector(".collapse-button"); + toggleArrow.click(); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_filter_navigation_marker.js b/devtools/client/webconsole/test/browser/browser_webconsole_filter_navigation_marker.js new file mode 100644 index 0000000000..574bdad266 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_filter_navigation_marker.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that filters don't affect navigation markers. + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf-8, + <p>Web Console test for navigation marker filtering.</p> + <script>console.log("hello " + "world");</script>`; + +add_task(async function() { + // Enable persist log + await pushPref("devtools.webconsole.persistlog", true); + + const hud = await openNewTabAndConsole(TEST_URI); + await waitFor( + () => findMessage(hud, "hello world"), + "Wait for log message to be rendered" + ); + ok(true, "Log message rendered"); + + info("Reload the page"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.location.reload(); + }); + + // Wait for the navigation message to be displayed. + await waitFor( + () => findMessage(hud, "Navigated to"), + "Wait for navigation message to be rendered" + ); + + // Wait for 2 hellow world messages to be displayed. + await waitFor( + () => findMessages(hud, "hello world").length == 2, + "Wait for log message to be rendered after navigation" + ); + + info("disable all filters and set a text filter that doesn't match anything"); + await setFilterState(hud, { + error: false, + warn: false, + log: false, + info: false, + text: "qwqwqwqwqwqw", + }); + + await waitFor( + () => !findMessage(hud, "hello world"), + "Wait for the log messages to be hidden" + ); + ok( + findMessage(hud, "Navigated to"), + "The navigation marker is still visible" + ); + + info("Navigate to a different origin"); + let newUrl = `http://example.net/document-builder.sjs?html=HelloNet`; + await navigateTo(newUrl); + // Wait for the navigation message to be displayed. + await waitFor( + () => findMessage(hud, "Navigated to " + newUrl), + "Wait for example.net navigation message to be rendered" + ); + ok(true, "Navigation message for example.net was displayed as expected"); + + info("Navigate to another different origin"); + newUrl = `http://example.com/document-builder.sjs?html=HelloCom`; + await navigateTo(newUrl); + // Wait for the navigation message to be displayed. + await waitFor( + () => findMessage(hud, "Navigated to " + newUrl), + "Wait for example.com navigation message to be rendered" + ); + ok(true, "Navigation message for example.com was displayed as expected"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_filter_scroll.js b/devtools/client/webconsole/test/browser/browser_webconsole_filter_scroll.js new file mode 100644 index 0000000000..47dd804c96 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_filter_scroll.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf-8, + <p>Web Console test for scroll when filtering.</p> + <script> + for (let i = 0; i < 100; i++) { + console.log("init-" + i); + } + </script> +`; +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { ui } = hud; + const outputContainer = ui.outputNode.querySelector(".webconsole-output"); + + info("Console should be scrolled to bottom on initial load from page logs"); + await waitFor(() => findMessage(hud, "init-99")); + ok(hasVerticalOverflow(outputContainer), "There is a vertical overflow"); + ok( + isScrolledToBottom(outputContainer), + "The console is scrolled to the bottom" + ); + + info( + "Filter out some messages and check that the scroll position is not impacted" + ); + let onMessagesFiltered = waitFor( + () => !findMessage(hud, "init-1"), + null, + 200 + ); + await setFilterState(hud, { text: "init-9" }); + await onMessagesFiltered; + ok( + isScrolledToBottom(outputContainer), + "The console is still scrolled to the bottom after filtering" + ); + + info( + "Clear the text filter and check that the scroll position is not impacted" + ); + let onMessagesUnFiltered = waitFor( + () => findMessage(hud, "init-1"), + null, + 200 + ); + await setFilterState(hud, { text: "" }); + await onMessagesUnFiltered; + ok( + isScrolledToBottom(outputContainer), + "The console is still scrolled to the bottom after clearing the filter" + ); + + info("Scroll up"); + outputContainer.scrollTop = 0; + + await setFilterState(hud, { text: "init-9" }); + onMessagesFiltered = waitFor(() => !findMessage(hud, "init-1"), null, 200); + await onMessagesFiltered; + is( + outputContainer.scrollTop, + 0, + "The console is still scrolled to the top after filtering" + ); + + info( + "Clear the text filter and check that the scroll position is not impacted" + ); + onMessagesUnFiltered = waitFor(() => findMessage(hud, "init-1"), null, 200); + await setFilterState(hud, { text: "" }); + await onMessagesUnFiltered; + is( + outputContainer.scrollTop, + 0, + "The console is still scrolled to the top after clearing the filter" + ); +}); + +function hasVerticalOverflow(container) { + return container.scrollHeight > container.clientHeight; +} + +function isScrolledToBottom(container) { + if (!container.lastChild) { + return true; + } + const lastNodeHeight = container.lastChild.clientHeight; + return ( + container.scrollTop + container.clientHeight >= + container.scrollHeight - lastNodeHeight / 2 + ); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_filters.js b/devtools/client/webconsole/test/browser/browser_webconsole_filters.js new file mode 100644 index 0000000000..df30bb03af --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_filters.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests filters. + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-console-filters.html"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + const filterState = await getFilterState(hud); + + // Triggers network requests + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + const url = "./sjs_slow-response-test-server.sjs"; + // Set a smaller delay for the 300 to ensure we get it before the "error" responses. + content.fetch(`${url}?status=300&delay=100`); + content.fetch(`${url}?status=404&delay=500`); + content.fetch(`${url}?status=500&delay=500`); + }); + + // Wait for the messages + await waitFor(() => findMessage(hud, "status=404", ".network.error")); + await waitFor(() => findMessage(hud, "status=500", ".network.error")); + + // Check defaults. + + for (const category of ["error", "warn", "log", "info", "debug"]) { + const state = filterState[category]; + ok(state, `Filter button for ${category} is on by default`); + } + for (const category of ["css", "net", "netxhr"]) { + const state = filterState[category]; + ok(!state, `Filter button for ${category} is off by default`); + } + + // Check that messages are shown as expected. This depends on cached messages being + // shown. + is( + findMessages(hud, "").length, + 7, + "Messages of all levels shown when filters are on." + ); + + // Check that messages are not shown when their filter is turned off. + await setFilterState(hud, { + error: false, + }); + await waitFor(() => findMessages(hud, "").length == 4); + ok(true, "When a filter is turned off, its messages are not shown."); + + // Check that the ui settings were persisted. + await closeTabAndToolbox(); + await testFilterPersistence(); +}); + +function filterIsEnabled(button) { + return button.classList.contains("checked"); +} + +async function testFilterPersistence() { + const hud = await openNewTabAndConsole(TEST_URI); + const outputNode = hud.ui.outputNode; + const filterBar = await waitFor(() => { + return outputNode.querySelector(".webconsole-filterbar-secondary"); + }); + ok(filterBar, "Filter bar ui setting is persisted."); + // Check that the filter settings were persisted. + ok( + !filterIsEnabled(filterBar.querySelector("[data-category='error']")), + "Filter button setting is persisted" + ); + is( + findMessages(hud, "").length, + 4, + "testFilterPersistence: Messages of all levels but error shown." + ); + + await resetFilters(hud); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_filters_persist.js b/devtools/client/webconsole/test/browser/browser_webconsole_filters_persist.js new file mode 100644 index 0000000000..8d1a7b228f --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_filters_persist.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests all filters persist. + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-console-filters.html"; + +add_task(async function() { + let hud = await openNewTabAndConsole(TEST_URI); + + let filterButtons = await getFilterButtons(hud); + info("Disable all filters"); + filterButtons.forEach(filterButton => { + if (filterIsEnabled(filterButton)) { + filterButton.click(); + } + }); + + info("Close and re-open the console"); + await closeTabAndToolbox(); + hud = await openNewTabAndConsole(TEST_URI); + + info("Check that all filters are disabled, and enable them"); + filterButtons = await getFilterButtons(hud); + filterButtons.forEach(filterButton => { + ok(!filterIsEnabled(filterButton), "filter is disabled"); + filterButton.click(); + }); + + // Wait for the CSS warning to be displayed so we don't have a pending promise. + await waitFor(() => findMessage(hud, "Expected color but found ‘blouge’")); + + info("Close and re-open the console"); + await closeTabAndToolbox(); + hud = await openNewTabAndConsole(TEST_URI); + + info("Check that all filters are enabled"); + filterButtons = await getFilterButtons(hud); + filterButtons.forEach(filterButton => { + ok(filterIsEnabled(filterButton), "filter is enabled"); + }); + + // Wait for the CSS warning to be displayed so we don't have a pending promise. + await waitFor(() => findMessage(hud, "Expected color but found ‘blouge’")); + + // Check that the ui settings were persisted. + await closeTabAndToolbox(); +}); + +async function getFilterButtons(hud) { + const outputNode = hud.ui.outputNode; + + info("Wait for console filterbar to appear"); + const filterBar = await waitFor(() => { + return outputNode.querySelector(".webconsole-filterbar-secondary"); + }); + ok(filterBar, "Filter bar is shown when filter icon is clicked."); + + return filterBar.querySelectorAll(".devtools-togglebutton"); +} + +function filterIsEnabled(button) { + return button.getAttribute("aria-pressed") === "true"; +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_highlighter_console_helper.js b/devtools/client/webconsole/test/browser/browser_webconsole_highlighter_console_helper.js new file mode 100644 index 0000000000..6597e2dc97 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_highlighter_console_helper.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that the $0 console helper works as intended. See Bug 653531. + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf-8, +<head> + <title>Inspector Tree Selection Test</title> +</head> +<body> + <div> + <h1>Inspector Tree Selection Test</h1> + <p>This is some example text</p> + <p>${loremIpsum()}</p> + </div> + <div> + <p>${loremIpsum()}</p> + </div> +</body>`.replace("\n", ""); + +add_task(async function() { + const toolbox = await openNewTabAndToolbox(TEST_URI, "inspector"); + const testActor = await getTestActor(toolbox); + await selectNodeWithPicker(toolbox, testActor, "h1"); + + info("Picker mode stopped, <h1> selected, now switching to the console"); + const hud = await openConsole(); + + await clearOutput(hud); + + await executeAndWaitForMessage(hud, "$0", "<h1>", ".result"); + ok(true, "correct output for $0"); + + await clearOutput(hud); + + const newH1Content = "newH1Content"; + await executeAndWaitForMessage( + hud, + `$0.textContent = "${newH1Content}";$0`, + "<h1>", + ".result" + ); + + ok(true, "correct output for $0 after setting $0.textContent"); + const { textContent } = await testActor.getNodeInfo("h1"); + is(textContent, newH1Content, "node successfully updated"); +}); + +function loremIpsum() { + return `Lorem ipsum dolor sit amet, consectetur adipisicing +elit, sed do eiusmod tempor incididunt ut labore et dolore magna +aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco +laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure +dolor in reprehenderit in voluptate velit esse cillum dolore eu +fugiat nulla pariatur. Excepteur sint occaecat cupidatat non +proident, sunt in culpa qui officia deserunt mollit anim id est laborum.`.replace( + "\n", + "" + ); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_hsts_invalid-headers.js b/devtools/client/webconsole/test/browser/browser_webconsole_hsts_invalid-headers.js new file mode 100644 index 0000000000..2601639fa0 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_hsts_invalid-headers.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that errors about invalid HSTS security headers are logged to the web console. + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf-8,Web Console HSTS invalid header test"; +const SJS_URL = + "https://example.com/browser/devtools/client/webconsole/" + + "/test/browser/test_hsts-invalid-headers.sjs"; +const LEARN_MORE_URI = + "https://developer.mozilla.org/docs/Web/HTTP/Headers/" + + "Strict-Transport-Security" + + DOCS_GA_PARAMS; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + await navigateAndCheckWarningMessage( + { + url: SJS_URL + "?badSyntax", + name: "Could not parse header error displayed successfully", + text: + "Strict-Transport-Security: The site specified a header that could " + + "not be parsed successfully.", + }, + hud + ); + + await navigateAndCheckWarningMessage( + { + url: SJS_URL + "?noMaxAge", + name: "No max-age error displayed successfully", + text: + "Strict-Transport-Security: The site specified a header that did " + + "not include a \u2018max-age\u2019 directive.", + }, + hud + ); + + await navigateAndCheckWarningMessage( + { + url: SJS_URL + "?invalidIncludeSubDomains", + name: "Invalid includeSubDomains error displayed successfully", + text: + "Strict-Transport-Security: The site specified a header that " + + "included an invalid \u2018includeSubDomains\u2019 directive.", + }, + hud + ); + + await navigateAndCheckWarningMessage( + { + url: SJS_URL + "?invalidMaxAge", + name: "Invalid max-age error displayed successfully", + text: + "Strict-Transport-Security: The site specified a header that " + + "included an invalid \u2018max-age\u2019 directive.", + }, + hud + ); + + await navigateAndCheckWarningMessage( + { + url: SJS_URL + "?multipleIncludeSubDomains", + name: "Multiple includeSubDomains error displayed successfully", + text: + "Strict-Transport-Security: The site specified a header that " + + "included multiple \u2018includeSubDomains\u2019 directives.", + }, + hud + ); + + await navigateAndCheckWarningMessage( + { + url: SJS_URL + "?multipleMaxAge", + name: "Multiple max-age error displayed successfully", + text: + "Strict-Transport-Security: The site specified a header that " + + "included multiple \u2018max-age\u2019 directives.", + }, + hud + ); +}); + +async function navigateAndCheckWarningMessage({ url, name, text }, hud) { + await clearOutput(hud); + + const onMessage = waitForMessage(hud, text, ".message.warn"); + await navigateTo(url); + const { node } = await onMessage; + ok(node, name); + + const learnMoreNode = node.querySelector(".learn-more-link"); + ok(learnMoreNode, `There is a "Learn more" link`); + const navigationResponse = await simulateLinkClick(learnMoreNode); + is( + navigationResponse.link, + LEARN_MORE_URI, + "Click on the learn more link navigates the user to the expected url" + ); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_iframe_wrong_hud.js b/devtools/client/webconsole/test/browser/browser_webconsole_iframe_wrong_hud.js new file mode 100644 index 0000000000..808340f1ff --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_iframe_wrong_hud.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Ensure that iframes are not associated with the wrong hud. See Bug 593003. + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-iframe-wrong-hud.html"; + +const TEST_IFRAME_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-iframe-wrong-hud-iframe.html"; + +const TEST_DUMMY_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-console.html"; + +add_task(async function() { + await pushPref("devtools.webconsole.filter.net", true); + const tab1 = await addTab(TEST_URI); + const hud1 = await openConsole(tab1); + + const tab2 = await addTab(TEST_DUMMY_URI); + await openConsole(gBrowser.selectedTab); + + info("Reloading tab 1"); + await reloadTab(tab1); + + info("Waiting for messages"); + await waitFor(() => findMessage(hud1, TEST_IFRAME_URI, ".message.network")); + + const hud2 = await openConsole(tab2); + is( + findMessage(hud2, TEST_IFRAME_URI), + undefined, + "iframe network request is not displayed in tab2" + ); +}); + +function reloadTab(tab) { + const loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + tab.linkedBrowser.reload(); + return loaded; +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_in_line_layout.js b/devtools/client/webconsole/test/browser/browser_webconsole_in_line_layout.js new file mode 100644 index 0000000000..d86cf8fc28 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_in_line_layout.js @@ -0,0 +1,127 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the in-line layout works as expected + +const TEST_URI = + "data:text/html,<meta charset=utf8>Test in-line console layout"; + +const MINIMUM_MESSAGE_HEIGHT = 20; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { ui } = hud; + const { document } = ui; + const appNode = document.querySelector(".webconsole-app"); + const filterBarNode = appNode.querySelector( + ".webconsole-filteringbar-wrapper" + ); + const outputNode = appNode.querySelector(".webconsole-output"); + const inputNode = appNode.querySelector(".jsterm-input-container"); + const eagerNode = document.querySelector(".eager-evaluation-result"); + + // The app height is the sum of the filter bar, input, and eager evaluation + const calculateAppHeight = () => + filterBarNode.offsetHeight + + inputNode.offsetHeight + + eagerNode.offsetHeight; + + testLayout(appNode); + + is(outputNode.offsetHeight, 0, "output node has no height"); + is( + calculateAppHeight(), + appNode.offsetHeight, + "The entire height is taken by filter bar, input, and eager result" + ); + + info("Logging a message in the content window"); + const onLogMessage = waitForMessage(hud, "simple text message"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.wrappedJSObject.console.log("simple text message"); + }); + const logMessage = await onLogMessage; + testLayout(appNode); + is( + outputNode.clientHeight, + logMessage.node.clientHeight, + "Output node is only the height of the message it contains" + ); + + info("Logging multiple messages to make the output overflow"); + const onLastMessage = waitForMessage(hud, "message-100"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + for (let i = 1; i <= 100; i++) { + content.wrappedJSObject.console.log("message-" + i); + } + }); + await onLastMessage; + ok( + outputNode.scrollHeight > outputNode.clientHeight, + "Output node overflows" + ); + testLayout(appNode); + + info("Make sure setting a tall value in the input does not break the layout"); + setInputValue(hud, "multiline\n".repeat(200)); + is( + outputNode.clientHeight, + MINIMUM_MESSAGE_HEIGHT, + "One message is still visible in the output node" + ); + testLayout(appNode); + + const filterBarHeight = filterBarNode.clientHeight; + + info("Shrink the window so the filter buttons are put in a new line"); + const toolbox = hud.ui.wrapper.toolbox; + const hostWindow = toolbox.win.parent; + hostWindow.resizeTo(300, window.screen.availHeight); + await waitFor(() => + document.querySelector(".webconsole-filteringbar-wrapper.narrow") + ); + + ok(filterBarNode.clientHeight > filterBarHeight, "The filter bar is taller"); + testLayout(appNode); + + info("Expand the window so filter buttons aren't on their own line anymore"); + hostWindow.resizeTo(window.screen.availWidth, window.screen.availHeight); + await waitFor(() => + document.querySelector(".webconsole-filteringbar-wrapper.wide") + ); + testLayout(appNode); + + setInputValue(hud, ""); + testLayout(appNode); + + await clearOutput(hud); + testLayout(appNode); + is(outputNode.offsetHeight, 0, "output node has no height"); + is( + calculateAppHeight(), + appNode.offsetHeight, + "The entire height is taken by filter bar, input, and eager result" + ); +}); + +function testLayout(node) { + is( + node.offsetHeight, + node.scrollHeight, + "there's no scrollbar on the wrapper" + ); + ok( + node.offsetHeight <= node.ownerDocument.body.offsetHeight, + "console is not taller than document body" + ); + const childSumHeight = [...node.childNodes].reduce( + (height, n) => height + n.offsetHeight, + 0 + ); + ok( + node.offsetHeight >= childSumHeight, + "the sum of the height of wrapper child nodes is not taller than wrapper's one" + ); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_ineffective_iframe_sandbox_warning.js b/devtools/client/webconsole/test/browser/browser_webconsole_ineffective_iframe_sandbox_warning.js new file mode 100644 index 0000000000..178e507fcf --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_ineffective_iframe_sandbox_warning.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that warnings about ineffective iframe sandboxing are logged to the +// web console when necessary (and not otherwise). See Bug 752559. + +"use strict"; + +requestLongerTimeout(2); + +const TEST_PATH = + "http://example.com/browser/devtools/client/webconsole/" + "test/browser/"; +const TEST_URI_WARNING = `${TEST_PATH}test-ineffective-iframe-sandbox-warning0.html`; +const TEST_URI_NOWARNING = [ + `${TEST_PATH}test-ineffective-iframe-sandbox-warning1.html`, + `${TEST_PATH}test-ineffective-iframe-sandbox-warning2.html`, + `${TEST_PATH}test-ineffective-iframe-sandbox-warning3.html`, + `${TEST_PATH}test-ineffective-iframe-sandbox-warning4.html`, + `${TEST_PATH}test-ineffective-iframe-sandbox-warning5.html`, +]; + +const INEFFECTIVE_IFRAME_SANDBOXING_MSG = + "An iframe which has both " + + "allow-scripts and allow-same-origin for its sandbox attribute can remove " + + "its sandboxing."; +const SENTINEL_MSG = "testing ineffective sandboxing message"; + +add_task(async function() { + await testWarningMessageVisibility(TEST_URI_WARNING, true); + + for (const testUri of TEST_URI_NOWARNING) { + await testWarningMessageVisibility(testUri, false); + } +}); + +async function testWarningMessageVisibility(uri, visible) { + const hud = await openNewTabAndConsole(uri, true); + + const sentinel = SENTINEL_MSG + Date.now(); + const onSentinelMessage = waitForMessage(hud, sentinel); + + SpecialPowers.spawn(gBrowser.selectedBrowser, [sentinel], function(msg) { + content.console.log(msg); + }); + await onSentinelMessage; + + const warning = findMessage( + hud, + INEFFECTIVE_IFRAME_SANDBOXING_MSG, + ".message.warn" + ); + is( + !!warning, + visible, + `The warning message is${visible ? "" : " not"} visible on ${uri}` + ); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_init.js b/devtools/client/webconsole/test/browser/browser_webconsole_init.js new file mode 100644 index 0000000000..a3df830cdd --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_init.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-console.html"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { ui } = hud; + + ok(ui.jsterm, "jsterm exists"); + ok(ui.wrapper, "wrapper exists"); + + const receievedMessages = waitForMessages({ + hud, + messages: [{ text: "19" }], + }); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.doLogs(20); + }); + + await receievedMessages; + + const outputContainer = ui.outputNode.querySelector(".webconsole-output"); + is( + outputContainer.querySelectorAll(".message.console-api").length, + 20, + "Correct number of messages appear" + ); + is( + outputContainer.scrollWidth, + outputContainer.clientWidth, + "No horizontal overflow" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_input_field_focus_on_panel_select.js b/devtools/client/webconsole/test/browser/browser_webconsole_input_field_focus_on_panel_select.js new file mode 100644 index 0000000000..1a3b152846 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_input_field_focus_on_panel_select.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the JS input field is focused when the user switches back to the +// web console from other tools, see bug 891581. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,<p>Test console input focus"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + info("Focus after console is opened"); + ok(isInputFocused(hud), "input is focused after console is opened"); + + const filterInput = getFilterInput(hud); + filterInput.focus(); + ok(hasFocus(filterInput), "filter input should be focused"); + + is(isInputFocused(hud), false, "input node is not focused anymore"); + + info("Go to the inspector panel"); + await openInspector(); + + info("Go back to the console"); + await openConsole(); + + ok( + isInputFocused(hud), + "input is focused when coming from a different panel" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_input_focus.js b/devtools/client/webconsole/test/browser/browser_webconsole_input_focus.js new file mode 100644 index 0000000000..0d56856b8f --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_input_focus.js @@ -0,0 +1,90 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that the input field is focused when the console is opened. + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf-8,Test input focused + <script> + console.log("console message 1"); + </script>`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + info("Focus after console is opened"); + ok(isInputFocused(hud), "input node is focused after console is opened"); + + info("Set the input value and select the first line"); + const expression = `x = 10;x; + x = 20;x;`; + setInputValue(hud, expression); + hud.ui.jsterm.editor.setSelection( + { line: 0, ch: 0 }, + { line: 0, ch: expression.split("\n")[0].length } + ); + + await clearOutput(hud); + ok(isInputFocused(hud), "input node is focused after output is cleared"); + + info("Focus during message logging"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.console.log("console message 2"); + }); + const msg = await waitFor(() => findMessage(hud, "console message 2")); + ok(isInputFocused(hud), "input node is focused, first time"); + + // Checking that there's still a selection in the input + is( + hud.ui.jsterm.editor.getSelection(), + "x = 10;x;", + "editor has the expected selection" + ); + + info("Focus after clicking in the output area"); + await waitForBlurredInput(hud); + AccessibilityUtils.setEnv({ + actionCountRule: false, + focusableRule: false, + interactiveRule: false, + labelRule: false, + }); + await EventUtils.sendMouseEvent({ type: "click" }, msg); + AccessibilityUtils.resetEnv(); + ok(isInputFocused(hud), "input node is focused, second time"); + + is( + hud.ui.jsterm.editor.getSelection(), + "", + "input selection was removed when the input was blurred" + ); + + info("Setting a text selection and making sure a click does not re-focus"); + await waitForBlurredInput(hud); + const selection = hud.iframeWindow.getSelection(); + selection.selectAllChildren(msg.querySelector(".message-body")); + AccessibilityUtils.setEnv({ + actionCountRule: false, + focusableRule: false, + interactiveRule: false, + labelRule: false, + }); + await EventUtils.sendMouseEvent({ type: "click" }, msg); + AccessibilityUtils.resetEnv(); + ok(!isInputFocused(hud), "input node not focused after text is selected"); +}); + +function waitForBlurredInput(hud) { + const node = hud.jsterm.node; + return new Promise(resolve => { + const lostFocus = () => { + ok(!isInputFocused(hud), "input node is not focused"); + resolve(); + }; + node.addEventListener("focusout", lostFocus, { once: true }); + + // The 'focusout' event fires if we focus e.g. the filter box. + getFilterInput(hud).focus(); + }); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_insecure_passwords_about_blank_web_console_warning.js b/devtools/client/webconsole/test/browser/browser_webconsole_insecure_passwords_about_blank_web_console_warning.js new file mode 100644 index 0000000000..bf9c16c88e --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_insecure_passwords_about_blank_web_console_warning.js @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that errors about insecure passwords are logged to the web console. +// See Bug 762593. + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/" + + "test-insecure-passwords-about-blank-web-console-warning.html"; +const INSECURE_PASSWORD_MSG = + "Password fields present on an insecure (http://) iframe." + + " This is a security risk that allows user login credentials to be stolen."; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + await waitFor( + () => findMessage(hud, INSECURE_PASSWORD_MSG, ".message.warn"), + "", + 100 + ); + ok(true, "Insecure password error displayed successfully"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_insecure_passwords_web_console_warning.js b/devtools/client/webconsole/test/browser/browser_webconsole_insecure_passwords_web_console_warning.js new file mode 100644 index 0000000000..24be7459bc --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_insecure_passwords_web_console_warning.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that errors about insecure passwords are logged to the web console. +// See Bug 762593. + +"use strict"; + +const INSECURE_IFRAME_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-insecure-passwords-web-console-warning.html"; +const INSECURE_PASSWORD_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-iframe-insecure-form-action.html"; +const INSECURE_FORM_ACTION_URI = + "https://example.com/browser/devtools/client/" + + "webconsole/test/browser/test-iframe-insecure-form-action.html"; + +const STOLEN = + "This is a security risk that allows user login credentials to be stolen."; +const INSECURE_PASSWORD_MSG = + "Password fields present on an insecure (http://) page. " + STOLEN; +const INSECURE_FORM_ACTION_MSG = + "Password fields present in a form with an insecure (http://) form action. " + + STOLEN; +const INSECURE_IFRAME_MSG = + "Password fields present on an insecure (http://) iframe. " + STOLEN; +const INSECURE_PASSWORDS_URI = + "https://developer.mozilla.org/docs/Web/Security/Insecure_passwords" + + DOCS_GA_PARAMS; + +add_task(async function() { + await testUriWarningMessage(INSECURE_IFRAME_URI, INSECURE_IFRAME_MSG); + await testUriWarningMessage(INSECURE_PASSWORD_URI, INSECURE_PASSWORD_MSG); + await testUriWarningMessage( + INSECURE_FORM_ACTION_URI, + INSECURE_FORM_ACTION_MSG + ); +}); + +async function testUriWarningMessage(uri, warningMessage) { + const hud = await openNewTabAndConsole(uri); + const message = await waitFor(() => + findMessage(hud, warningMessage, ".message.warn") + ); + ok(message, "Warning message displayed successfully"); + await testLearnMoreLinkClick(message, INSECURE_PASSWORDS_URI); +} + +async function testLearnMoreLinkClick(message, expectedUri) { + const learnMoreLink = message.querySelector(".learn-more-link"); + ok(learnMoreLink, "There is a [Learn More] link"); + const { link } = await simulateLinkClick(learnMoreLink); + is( + link, + expectedUri, + "Click on [Learn More] link navigates user to " + expectedUri + ); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_inspect_cross_domain_object.js b/devtools/client/webconsole/test/browser/browser_webconsole_inspect_cross_domain_object.js new file mode 100644 index 0000000000..b08c5c6a47 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_inspect_cross_domain_object.js @@ -0,0 +1,128 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check that users can inspect objects logged from cross-domain iframes - +// bug 869003. + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-inspect-cross-domain-objects-top.html"; + +add_task(async function() { + requestLongerTimeout(2); + + // Bug 1518138: GC heuristics are broken for this test, so that the test + // ends up running out of memory. Try to work-around the problem by GCing + // before the test begins. + Cu.forceShrinkingGC(); + + let hud, node; + if (isFissionEnabled()) { + // When fission is enabled, we might miss the early message emitted while the target + // is being switched, so here we directly open the "real" test URI. See Bug 1614291. + hud = await openNewTabAndConsole(TEST_URI); + info("Wait for the 'foobar' message to be logged by the frame"); + node = await waitFor(() => findMessage(hud, "foobar")); + } else { + hud = await openNewTabAndConsole("data:text/html;charset=utf8,<p>hello"); + info( + "Navigate and wait for the 'foobar' message to be logged by the frame" + ); + const onMessage = waitForMessage(hud, "foobar"); + await navigateTo(TEST_URI); + ({ node } = await onMessage); + } + + const objectInspectors = [...node.querySelectorAll(".tree")]; + is( + objectInspectors.length, + 3, + "There is the expected number of object inspectors" + ); + + const [oi1, oi2, oi3] = objectInspectors; + + info("Expanding the first object inspector"); + await expandObjectInspector(oi1); + + // The first object inspector now looks like: + // ▼ {…} + // | bug: 869003 + // | hello: "world!" + // | ▶︎ <prototype>: Object { … } + + const oi1Nodes = oi1.querySelectorAll(".node"); + is(oi1Nodes.length, 4, "There is the expected number of nodes in the tree"); + ok(oi1.textContent.includes("bug: 869003"), "Expected content"); + ok(oi1.textContent.includes('hello: "world!"'), "Expected content"); + + info("Expanding the second object inspector"); + await expandObjectInspector(oi2); + + // The second object inspector now looks like: + // ▼ func() + // | arguments: null + // | bug: 869003 + // | caller: null + // | hello: "world!" + // | length: 1 + // | name: "func" + // | ▶︎ prototype: Object { … } + // | ▶︎ <prototype>: function () + + const oi2Nodes = oi2.querySelectorAll(".node"); + is(oi2Nodes.length, 9, "There is the expected number of nodes in the tree"); + ok(oi2.textContent.includes("arguments: null"), "Expected content"); + ok(oi2.textContent.includes("bug: 869003"), "Expected content"); + ok(oi2.textContent.includes("caller: null"), "Expected content"); + ok(oi2.textContent.includes('hello: "world!"'), "Expected content"); + ok(oi2.textContent.includes("length: 1"), "Expected content"); + ok(oi2.textContent.includes('name: "func"'), "Expected content"); + + info( + "Check that the logged element can be highlighted and clicked to jump to inspector" + ); + const toolbox = hud.toolbox; + // Loading the inspector panel at first, to make it possible to listen for + // new node selections + await toolbox.loadTool("inspector"); + const highlighter = toolbox.getHighlighter(); + + const elementNode = oi3.querySelector(".objectBox-node"); + ok(elementNode !== null, "Node was logged as expected"); + const view = node.ownerDocument.defaultView; + + info("Highlight the node by moving the cursor on it"); + const onNodeHighlight = highlighter.waitForHighlighterShown(); + + EventUtils.synthesizeMouseAtCenter(elementNode, { type: "mousemove" }, view); + + const { highlighter: activeHighlighter } = await onNodeHighlight; + ok(activeHighlighter, "Highlighter is displayed"); + // Move the mouse out of the node to prevent failure when test is run multiple times. + EventUtils.synthesizeMouseAtCenter(oi1, { type: "mousemove" }, view); + + const openInInspectorIcon = elementNode.querySelector(".open-inspector"); + ok(openInInspectorIcon !== null, "There is an open in inspector icon"); + + info( + "Clicking on the inspector icon and waiting for the inspector to be selected" + ); + const onNewNode = toolbox.selection.once("new-node-front"); + openInInspectorIcon.click(); + const inspectorSelectedNodeFront = await onNewNode; + + ok(true, "Inspector selected and new node got selected"); + is(inspectorSelectedNodeFront.id, "testEl", "The expected node was selected"); +}); + +function expandObjectInspector(oi) { + const onMutation = waitForNodeMutation(oi, { + childList: true, + }); + + oi.querySelector(".arrow").click(); + return onMutation; +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_keyboard_accessibility.js b/devtools/client/webconsole/test/browser/browser_webconsole_keyboard_accessibility.js new file mode 100644 index 0000000000..ec830addff --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_keyboard_accessibility.js @@ -0,0 +1,101 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check that basic keyboard shortcuts work in the web console. + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf-8,<p>Test keyboard accessibility</p> + <script> + for (let i = 1; i <= 100; i++) { + console.log("console message " + i); + } + </script> + `; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + info("Web Console opened"); + const outputScroller = hud.ui.outputScroller; + await waitFor( + () => findMessages(hud, "").length == 100, + "waiting for all the messages to be displayed", + 100, + 1000 + ); + + let currentPosition = outputScroller.scrollTop; + const bottom = currentPosition; + hud.jsterm.focus(); + // Page up. + EventUtils.synthesizeKey("KEY_PageUp"); + isnot( + outputScroller.scrollTop, + currentPosition, + "scroll position changed after page up" + ); + // Page down. + currentPosition = outputScroller.scrollTop; + EventUtils.synthesizeKey("KEY_PageDown"); + ok( + outputScroller.scrollTop > currentPosition, + "scroll position now at bottom" + ); + + // Home + EventUtils.synthesizeKey("KEY_Home"); + is(outputScroller.scrollTop, 0, "scroll position now at top"); + + // End + EventUtils.synthesizeKey("KEY_End"); + const scrollTop = outputScroller.scrollTop; + ok( + scrollTop > 0 && Math.abs(scrollTop - bottom) <= 5, + "scroll position now at bottom" + ); + + // Clear output + info("try ctrl-l to clear output"); + let clearShortcut; + if (Services.appinfo.OS === "Darwin") { + clearShortcut = WCUL10n.getStr("webconsole.clear.keyOSX"); + } else { + clearShortcut = WCUL10n.getStr("webconsole.clear.key"); + } + synthesizeKeyShortcut(clearShortcut); + await waitFor(() => findMessages(hud, "").length == 0); + ok(isInputFocused(hud), "console was cleared and input is focused"); + + if (Services.appinfo.OS === "Darwin") { + info("Log a new message from the content page"); + const onMessage = waitForMessage(hud, "another simple text message"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() { + content.console.log("another simple text message"); + }); + await onMessage; + + info("Send Cmd-K to clear console"); + synthesizeKeyShortcut(WCUL10n.getStr("webconsole.clear.alternativeKeyOSX")); + + await waitFor(() => findMessages(hud, "").length == 0); + ok( + isInputFocused(hud), + "console was cleared as expected with alternative shortcut" + ); + } + + // Focus filter + info("try ctrl-f to focus filter"); + synthesizeKeyShortcut(WCUL10n.getStr("webconsole.find.key")); + ok(!isInputFocused(hud), "input is not focused"); + ok(hasFocus(getFilterInput(hud)), "filter input is focused"); + + info("try ctrl-f when filter is already focused"); + synthesizeKeyShortcut(WCUL10n.getStr("webconsole.find.key")); + ok(!isInputFocused(hud), "input is not focused"); + is( + getFilterInput(hud), + outputScroller.ownerDocument.activeElement, + "filter input is focused" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_limit_multiline.js b/devtools/client/webconsole/test/browser/browser_webconsole_limit_multiline.js new file mode 100644 index 0000000000..3562b9ddf7 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_limit_multiline.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that the code with > 5 lines in multiline editor mode +// is collapsible +// Check Bug 1578212 for more info + +"use strict"; +const { ELLIPSIS } = require("devtools/shared/l10n"); + +const SMALL_EXPRESSION = `function fib(n) { + if (n <= 1) + return 1; + return fib(n-1) + fib(n-2); +}`; + +const LONG_EXPRESSION = `${SMALL_EXPRESSION} +fib(3);`; + +add_task(async function() { + const hud = await openNewTabAndConsole( + "data:text/html,<meta charset=utf8>Test multi-line commands expandability" + ); + info("Test that we don't slice messages with <= 5 lines"); + const message = await executeAndWaitForMessage( + hud, + SMALL_EXPRESSION, + "function fib" + ); + + is( + message.node.querySelector(".collapse-button"), + null, + "Collapse button does not exist" + ); + + info("Test messages with > 5 lines are sliced"); + + const messageExp = await executeAndWaitForMessage( + hud, + LONG_EXPRESSION, + "function fib" + ); + + const toggleArrow = messageExp.node.querySelector(".collapse-button"); + ok(toggleArrow, "Collapse button exists"); + // Check for elipsis + ok(messageExp.node.innerText.includes(ELLIPSIS), "Has ellipsis"); + + info("Test clicking the button expands/collapses the message"); + + const isOpen = node2 => node2.classList.contains("open"); + + toggleArrow.click(); // expand + await waitFor(() => isOpen(messageExp.node) === true); + + ok( + !messageExp.node.innerText.includes(ELLIPSIS), + "Opened message doesn't have ellipsis" + ); + is( + messageExp.node.innerText.trim().split("\n").length, + LONG_EXPRESSION.split("\n").length, + "Expanded code has same number of lines as original" + ); + + toggleArrow.click(); // expand + await waitFor(() => isOpen(messageExp.node) === false); + + is( + messageExp.node.innerText.trim().split("\n").length, + 5, + "Code is truncated & only 5 lines shown" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_location_debugger_link.js b/devtools/client/webconsole/test/browser/browser_webconsole_location_debugger_link.js new file mode 100644 index 0000000000..01d84d14b1 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_location_debugger_link.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that message source links for js errors and console API calls open in +// the jsdebugger when clicked. + +"use strict"; + +// There are shutdown issues for which multiple rejections are left uncaught. +// See bug 1018184 for resolving these issues. +PromiseTestUtils.allowMatchingRejectionsGlobally(/this\.worker is null/); + +requestLongerTimeout(2); + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-location-debugger-link.html"; + +add_task(async function() { + await pushPref("devtools.webconsole.filter.error", true); + await pushPref("devtools.webconsole.filter.log", true); + + // On e10s, the exception thrown in test-location-debugger-link-errors.js + // is triggered in child process and is ignored by test harness + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } + + const hud = await openNewTabAndConsole(TEST_URI); + const target = await TargetFactory.forTab(gBrowser.selectedTab); + const toolbox = gDevTools.getToolbox(target); + + await testOpenInDebugger(hud, toolbox, "document.bar"); + + info("Selecting the console again"); + await toolbox.selectTool("webconsole"); + await testOpenInDebugger(hud, toolbox, "Blah Blah"); + + // // check again the first node. + info("Selecting the console again"); + await toolbox.selectTool("webconsole"); + await testOpenInDebugger(hud, toolbox, "document.bar"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_location_logpoint_debugger_link.js b/devtools/client/webconsole/test/browser/browser_webconsole_location_logpoint_debugger_link.js new file mode 100644 index 0000000000..0fc4a6e053 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_location_logpoint_debugger_link.js @@ -0,0 +1,185 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test clicking locations of logpoint logs and errors will open corresponding +// conditional panels in the debugger. + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-location-debugger-link-logpoint.html"; + +add_task(async function() { + // On e10s, the exception thrown in test-location-debugger-link-errors.js + // is triggered in child process and is ignored by test harness + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } + + // Eliminate interference from "saved" breakpoints + // when running the test multiple times + await clearDebuggerPreferences(); + const hud = await openNewTabAndConsole(TEST_URI); + + info("Open the Debugger panel"); + await openDebugger(); + + const toolbox = hud.toolbox; + const dbg = createDebuggerContext(toolbox); + await selectSource(dbg, "test-location-debugger-link-logpoint-1.js"); + + info("Add a logpoint with an invalid expression"); + await setLogPoint(dbg, 7, "undefinedVariable"); + + info("Add a logpoint with a valid expression"); + await setLogPoint(dbg, 8, "`a is ${a}`"); + + await assertEditorLogpoint(dbg, 7, { hasLog: true }); + await assertEditorLogpoint(dbg, 8, { hasLog: true }); + + info("Close the file in the debugger"); + await closeTab(dbg, "test-location-debugger-link-logpoint-1.js"); + + info("Selecting the console"); + await toolbox.selectTool("webconsole"); + + info("Call the function"); + await invokeInTab("add"); + + info("Wait for two messages"); + await waitFor(() => findMessages(hud, "").length === 2); + + await testOpenInDebugger( + hud, + toolbox, + "undefinedVariable is not defined", + true, + false, + false, + "undefinedVariable" + ); + + info("Selecting the console again"); + await toolbox.selectTool("webconsole"); + await testOpenInDebugger( + hud, + toolbox, + "a is 1", + true, + false, + false, + "`a is ${a}`" + ); + + // Test clicking location of a removed logpoint, or a newly added breakpoint + // at an old logpoint's location will only highlight its line + info("Remove the logpoints"); + const source = await findSource( + dbg, + "test-location-debugger-link-logpoint-1.js" + ); + await removeBreakpoint(dbg, source.id, 7); + await removeBreakpoint(dbg, source.id, 8); + await addBreakpoint(dbg, "test-location-debugger-link-logpoint-1.js", 8); + + info("Selecting the console"); + await toolbox.selectTool("webconsole"); + await testOpenInDebugger( + hud, + toolbox, + "undefinedVariable is not defined", + true, + 8, + 10 + ); + + info("Selecting the console again"); + await toolbox.selectTool("webconsole"); + await testOpenInDebugger(hud, toolbox, "a is 1", true, 8, 10); +}); + +// Test clicking locations of logpoints from different files +add_task(async function() { + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } + + await clearDebuggerPreferences(); + const hud = await openNewTabAndConsole(TEST_URI); + + info("Open the Debugger panel"); + await openDebugger(); + + const toolbox = hud.toolbox; + const dbg = createDebuggerContext(toolbox); + + info("Add a logpoint to the first file"); + await selectSource(dbg, "test-location-debugger-link-logpoint-1.js"); + await setLogPoint(dbg, 8, "`a is ${a}`"); + + info("Add a logpoint to the second file"); + await selectSource(dbg, "test-location-debugger-link-logpoint-2.js"); + await setLogPoint(dbg, 8, "`c is ${c}`"); + + info("Selecting the console"); + await toolbox.selectTool("webconsole"); + + info("Call the function from the first file"); + await invokeInTab("add"); + + info("Wait for the first message"); + await waitFor(() => findMessages(hud, "").length === 1); + await testOpenInDebugger( + hud, + toolbox, + "a is 1", + true, + false, + false, + "`a is ${a}`" + ); + + info("Selecting the console again"); + await toolbox.selectTool("webconsole"); + + info("Call the function from the second file"); + await invokeInTab("subtract"); + + info("Wait for the second message"); + await waitFor(() => findMessages(hud, "").length === 2); + await testOpenInDebugger( + hud, + toolbox, + "c is 1", + true, + false, + false, + "`c is ${c}`" + ); +}); + +async function setLogPoint(dbg, index, expression) { + rightClickElement(dbg, "gutter", index); + selectContextMenuItem( + dbg, + `${selectors.addLogItem},${selectors.editLogItem}` + ); + const onBreakpointSet = waitForDispatch(dbg, "SET_BREAKPOINT"); + await typeInPanel(dbg, expression); + await onBreakpointSet; +} + +function getLineEl(dbg, line) { + const lines = dbg.win.document.querySelectorAll(".CodeMirror-code > div"); + return lines[line - 1]; +} + +function assertEditorLogpoint(dbg, line, { hasLog = false } = {}) { + const hasLogClass = getLineEl(dbg, line).classList.contains("has-log"); + + ok( + hasLogClass === hasLog, + `Breakpoint log ${hasLog ? "exists" : "does not exist"} on line ${line}` + ); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_location_styleeditor_link.js b/devtools/client/webconsole/test/browser/browser_webconsole_location_styleeditor_link.js new file mode 100644 index 0000000000..4d1c5699cd --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_location_styleeditor_link.js @@ -0,0 +1,106 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/" + + "test-location-styleeditor-link.html"; + +add_task(async function() { + await pushPref("devtools.webconsole.filter.css", true); + const hud = await openNewTabAndConsole(TEST_URI); + const target = await TargetFactory.forTab(gBrowser.selectedTab); + const toolbox = gDevTools.getToolbox(target); + + await testViewSource(hud, toolbox, "\u2018font-weight\u2019"); + + info("Selecting the console again"); + await toolbox.selectTool("webconsole"); + await testViewSource(hud, toolbox, "\u2018color\u2019"); + + info("Selecting the console again"); + await toolbox.selectTool("webconsole"); + await testViewSource(hud, toolbox, "\u2018display\u2019"); +}); + +async function testViewSource(hud, toolbox, text) { + info(`Testing message with text "${text}"`); + const messageNode = await waitFor( + () => findMessage(hud, text), + `couldn't find message containing "${text}"` + ); + const frameLinkNode = messageNode.querySelector( + ".message-location .frame-link" + ); + ok(frameLinkNode, "The message does have a location link"); + + const onStyleEditorSelected = toolbox.once("styleeditor-selected"); + + await EventUtils.sendMouseEvent( + { type: "click" }, + messageNode.querySelector(".frame-link-filename") + ); + + const panel = await onStyleEditorSelected; + ok( + true, + "The style editor is selected when clicking on the location element" + ); + + await onStyleEditorReady(panel); + + info("style editor window focused"); + const href = frameLinkNode.getAttribute("data-url"); + const line = frameLinkNode.getAttribute("data-line"); + const column = frameLinkNode.getAttribute("data-column"); + ok(line, "found source line"); + + const editor = getEditorForHref(panel.UI, href); + ok(editor, "found style editor for " + href); + await checkCursorPosition(panel.UI, editor, line - 1, column - 1); +} + +async function onStyleEditorReady(panel) { + const win = panel.panelWindow; + ok(win, "Style Editor Window is defined"); + ok(panel.UI, "Style Editor UI is defined"); + + info("Waiting the style editor to be focused"); + return new Promise(resolve => { + waitForFocus(function() { + resolve(); + }, win); + }); +} + +function getEditorForHref(styleEditorUI, href) { + let foundEditor = null; + for (const editor of styleEditorUI.editors) { + if (editor.styleSheet.href == href) { + foundEditor = editor; + break; + } + } + return foundEditor; +} + +async function checkCursorPosition(styleEditorUI, editor, line, column) { + info("wait for source editor to load"); + // Get out of the styleeditor-selected event loop. + await waitForTick(); + + // Get the updated line and column position if the CSS source was prettified. + const position = editor.translateCursorPosition(line, column); + line = position.line; + column = position.column; + + is(editor.sourceEditor.getCursor().line, line, "correct line is selected"); + is(editor.sourceEditor.getCursor().ch, column, "correct column is selected"); + is( + styleEditorUI.selectedStyleSheetIndex, + editor.styleSheet.styleSheetIndex, + "correct stylesheet is selected in the editor" + ); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_logErrorInPage.js b/devtools/client/webconsole/test/browser/browser_webconsole_logErrorInPage.js new file mode 100644 index 0000000000..193a87374e --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_logErrorInPage.js @@ -0,0 +1,19 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that we can log a message to the web console from the toolbox. + +const TEST_URI = "data:text/html;charset=utf-8,<p>test logErrorInPage"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const toolbox = hud.ui.wrapper.toolbox; + + toolbox.target.logErrorInPage("beware the octopus", "content javascript"); + + const node = await waitFor(() => findMessage(hud, "octopus")); + ok(node, "text is displayed in web console"); + ok(node.classList.contains("error"), "the log represents an error"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_logWarningInPage.js b/devtools/client/webconsole/test/browser/browser_webconsole_logWarningInPage.js new file mode 100644 index 0000000000..e7aec42b28 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_logWarningInPage.js @@ -0,0 +1,19 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that we can log warning message to the web console from the toolbox. + +const TEST_URI = "data:text/html;charset=utf-8,<p>test logErrorInPage"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const toolbox = hud.ui.wrapper.toolbox; + + toolbox.target.logWarningInPage("beware the octopus", "content javascript"); + + const node = await waitFor(() => findMessage(hud, "octopus")); + ok(node, "text is displayed in web console"); + ok(node.classList.contains("warn"), "the log represents a warning"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_loglimit.js b/devtools/client/webconsole/test/browser/browser_webconsole_loglimit.js new file mode 100644 index 0000000000..ddb86a0a92 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_loglimit.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that messages are properly updated when the log limit is reached. + +const TEST_URI = + "data:text/html;charset=utf-8,Web Console test for " + + "Old messages are removed after passing devtools.hud.loglimit"; + +add_task(async function() { + await pushPref("devtools.hud.loglimit", 140); + const hud = await openNewTabAndConsole(TEST_URI); + await clearOutput(hud); + + let onMessage = waitForMessage(hud, "test message [149]"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() { + for (let i = 0; i < 150; i++) { + content.console.log(`test message [${i}]`); + } + }); + await onMessage; + + ok(!findMessage(hud, "test message [0]"), "Message 0 has been pruned"); + ok(!findMessage(hud, "test message [9]"), "Message 9 has been pruned"); + ok(findMessage(hud, "test message [10]"), "Message 10 is still displayed"); + is( + findMessages(hud, "").length, + 140, + "Number of displayed messages is correct" + ); + + onMessage = waitForMessage(hud, "hello world"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() { + content.console.log("hello world"); + }); + await onMessage; + + ok(!findMessage(hud, "test message [10]"), "Message 10 has been pruned"); + ok(findMessage(hud, "test message [11]"), "Message 11 is still displayed"); + is( + findMessages(hud, "").length, + 140, + "Number of displayed messages is still correct" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_longstring.js b/devtools/client/webconsole/test/browser/browser_webconsole_longstring.js new file mode 100644 index 0000000000..9c3ba1c10e --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_longstring.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that very long strings can be expanded and collapsed, and do not hang the browser. + +"use strict"; + +const TEST_URI = "data:text/html,<meta charset=utf8>Test LongString hang"; + +const LONGSTRING = `foobar${"a".repeat( + 9000 +)}foobaz${"abbababazomglolztest".repeat(100)}boom!`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + info("Log a longString"); + const onMessage = waitForMessage(hud, LONGSTRING.slice(0, 50)); + SpecialPowers.spawn(gBrowser.selectedBrowser, [LONGSTRING], str => { + content.console.log(str); + }); + + const { node } = await onMessage; + const arrow = node.querySelector(".arrow"); + ok(arrow, "longString expand arrow is shown"); + + info("wait for long string expansion"); + const onLongStringFullTextDisplayed = waitFor(() => + findMessage(hud, LONGSTRING) + ); + arrow.click(); + await onLongStringFullTextDisplayed; + + ok(true, "The full text of the longString is displayed"); + + info("wait for long string collapse"); + const onLongStringCollapsed = waitFor(() => !findMessage(hud, LONGSTRING)); + arrow.click(); + await onLongStringCollapsed; + + ok(true, "The longString can be collapsed"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_longstring_getter.js b/devtools/client/webconsole/test/browser/browser_webconsole_longstring_getter.js new file mode 100644 index 0000000000..ff0a82b876 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_longstring_getter.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that getter properties that return long strings can be expanded. See Bug 1481833. + +"use strict"; + +const LONGSTRING = "a ".repeat(10000); +const TEST_URI = `data:text/html,Test expanding longString getter property + <svg> + <image xlink:href="data:image/png;base64,${LONGSTRING}"></image> + </svg> + <script> + console.dir("Test message", document.querySelector("svg image").href); + </script>`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + // Retrieve the logged message. + const message = await waitFor(() => findMessage(hud, "Test message")); + + // Wait until the SVGAnimatedString is expanded. + await waitFor(() => message.querySelectorAll(".arrow").length > 1); + + const arrow = message.querySelectorAll(".arrow")[1]; + ok(arrow, "longString expand arrow is shown"); + + info("wait for long string expansion"); + const onLongStringFullTextDisplayed = waitFor(() => + findMessage(hud, LONGSTRING) + ); + arrow.click(); + await onLongStringFullTextDisplayed; + + ok(true, "The full text of the longString is displayed"); + + info("wait for long string collapse"); + const onLongStringCollapsed = waitFor(() => !findMessage(hud, LONGSTRING)); + arrow.click(); + await onLongStringCollapsed; + + ok(true, "The longString can be collapsed"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_message_categories.js b/devtools/client/webconsole/test/browser/browser_webconsole_message_categories.js new file mode 100644 index 0000000000..f9054483f9 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_message_categories.js @@ -0,0 +1,149 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that messages are logged and observed with the correct category. See Bug 595934. +const { MESSAGE_CATEGORY } = require("devtools/shared/constants"); + +const TEST_URI = + "data:text/html;charset=utf-8,Web Console test for " + + "bug 595934 - message categories coverage."; +const TESTS_PATH = + "http://example.com/browser/devtools/client/webconsole/test/browser/"; +const TESTS = [ + { + // #0 + file: "test-message-categories-css-loader.html", + category: "CSS Loader", + matchString: "text/css", + }, + { + // #1 + file: "test-message-categories-imagemap.html", + category: "Layout: ImageMap", + matchString: 'shape="rect"', + }, + { + // #2 + file: "test-message-categories-html.html", + category: "HTML", + matchString: "multipart/form-data", + onload: function() { + SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() { + const form = content.document.querySelector("form"); + form.submit(); + }); + }, + }, + { + // #3 + file: "test-message-categories-workers.html", + category: "Web Worker", + matchString: "fooBarWorker", + }, + { + // #4 + file: "test-message-categories-malformedxml.xhtml", + category: "malformed-xml", + matchString: "no root element found", + }, + { + // #5 + file: "test-message-categories-svg.xhtml", + category: "SVG", + matchString: "fooBarSVG", + }, + { + // #6 + file: "test-message-categories-css-parser.html", + category: MESSAGE_CATEGORY.CSS_PARSER, + matchString: "foobarCssParser", + }, + { + // #7 + file: "test-message-categories-malformedxml-external.html", + category: "malformed-xml", + matchString: "</html>", + }, + { + // #8 + file: "test-message-categories-empty-getelementbyid.html", + category: "DOM", + matchString: "getElementById", + }, + { + // #9 + file: "test-message-categories-canvas-css.html", + category: MESSAGE_CATEGORY.CSS_PARSER, + matchString: "foobarCanvasCssParser", + }, + { + // #10 + file: "test-message-categories-image.html", + category: "Image", + matchString: "corrupt", + // This message is not displayed in the main console in e10s. Bug 1431731 + skipInE10s: true, + }, +]; + +add_task(async function() { + requestLongerTimeout(2); + + await pushPref("devtools.webconsole.filter.css", true); + await pushPref("devtools.webconsole.filter.net", true); + + const hud = await openNewTabAndConsole(TEST_URI); + for (let i = 0; i < TESTS.length; i++) { + const test = TESTS[i]; + info("Running test #" + i); + await runTest(test, hud); + } + + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +async function runTest(test, hud) { + const { file, category, matchString, onload, skipInE10s } = test; + + if (skipInE10s && Services.appinfo.browserTabsRemoteAutostart) { + return; + } + + const onMessageLogged = waitForMessage(hud, matchString); + + const onMessageObserved = new Promise(resolve => { + Services.console.registerListener(function listener(subject) { + if (!(subject instanceof Ci.nsIScriptError)) { + return; + } + + if (subject.category != category) { + return; + } + + ok(true, "Expected category [" + category + "] received in observer"); + Services.console.unregisterListener(listener); + resolve(); + }); + }); + + info("Load test file " + file); + await navigateTo(TESTS_PATH + file); + + // Call test specific callback if defined + if (onload) { + onload(); + } + + info("Wait for log message to be observed with the correct category"); + await onMessageObserved; + + info("Wait for log message to be displayed in the hud"); + await onMessageLogged; +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_mime_css_blocked.js b/devtools/client/webconsole/test/browser/browser_webconsole_mime_css_blocked.js new file mode 100644 index 0000000000..1bd9d90f8d --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_mime_css_blocked.js @@ -0,0 +1,16 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that non-CSS parser errors get displayed by default. + +"use strict"; + +const CSS_URI = "data:text/bogus,foo"; +const TEST_URI = `data:text/html,<!doctype html><link rel="stylesheet" href="${CSS_URI}">`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const MSG = `The stylesheet ${CSS_URI} was not loaded because its MIME type, “text/bogus”, is not “text/css”`; + await waitFor(() => findMessage(hud, MSG, ".message.error"), "", 100); + ok(true, "MIME type error displayed"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_multiple_windows_and_tabs.js b/devtools/client/webconsole/test/browser/browser_webconsole_multiple_windows_and_tabs.js new file mode 100644 index 0000000000..d3d4635ea8 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_multiple_windows_and_tabs.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that the Web Console doesn't leak when multiple tabs and windows are +// opened and then closed. See Bug 595350. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 595350"; + +add_task(async function() { + requestLongerTimeout(3); + // Bug 1518138: GC heuristics are broken for this test, so that the test + // ends up running out of memory. Try to work-around the problem by GCing + // before the test begins. + Cu.forceShrinkingGC(); + + const win1 = window; + + info("Add test tabs in first window"); + const tab1 = await addTab(TEST_URI, { window: win1 }); + const tab2 = await addTab(TEST_URI, { window: win1 }); + info("Test tabs added in first window"); + + info("Open a second window"); + const windowOpenedPromise = BrowserTestUtils.waitForNewWindow(); + const win2 = OpenBrowserWindow(); + await windowOpenedPromise; + + info("Add test tabs in second window"); + const tab3 = await addTab(TEST_URI, { window: win2 }); + const tab4 = await addTab(TEST_URI, { window: win2 }); + + info("Opening console in each test tab"); + const tabs = [tab1, tab2, tab3, tab4]; + for (const tab of tabs) { + // Open the console in tab${i}. + const hud = await openConsole(tab); + const browser = hud.currentTarget.localTab.linkedBrowser; + const message = "message for tab " + tabs.indexOf(tab); + + // Log a message in the newly opened console. + const onMessage = waitForMessage(hud, message); + await SpecialPowers.spawn(browser, [message], function(msg) { + content.console.log(msg); + }); + await onMessage; + + await hud.toolbox.sourceMapURLService.waitForSourcesLoading(); + } + + const onConsolesDestroyed = waitForNEvents("web-console-destroyed", 4); + + info("Close the second window"); + win2.close(); + + info("Close the test tabs in the first window"); + win1.gBrowser.removeTab(tab1); + win1.gBrowser.removeTab(tab2); + + info("Wait for 4 web-console-destroyed events"); + await onConsolesDestroyed; + + ok(true, "Received web-console-destroyed for each console opened"); +}); + +/** + * Wait for N events helper customized to work with Services.obs.add/removeObserver. + */ +function waitForNEvents(expectedTopic, times) { + return new Promise(resolve => { + let count = 0; + + function onEvent(subject, topic) { + if (topic !== expectedTopic) { + return; + } + + count++; + info(`Received ${expectedTopic} ${count} time(s).`); + if (count == times) { + resolve(); + } + } + + registerCleanupFunction(() => { + Services.obs.removeObserver(onEvent, expectedTopic); + }); + Services.obs.addObserver(onEvent, expectedTopic); + }); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_navigate_to_parse_error.js b/devtools/client/webconsole/test/browser/browser_webconsole_navigate_to_parse_error.js new file mode 100644 index 0000000000..53a214b164 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_navigate_to_parse_error.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that ensure CSP 'navigate-to' does not parse. +// Bug 1566149 + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf8,Web Console navigate-to parse error test"; +const TEST_VIOLATION = + "https://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-navigate-to-parse-error.html"; + +const CSP_VIOLATION_MSG = + "Content Security Policy: Couldn\u2019t process unknown directive \u2018navigate-to\u2019"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + await clearOutput(hud); + + const onCSPViolationMessage = waitForMessage(hud, CSP_VIOLATION_MSG); + await navigateTo(TEST_VIOLATION); + await onCSPViolationMessage; + ok(true, "Received expected violation message"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_network_attach.js b/devtools/client/webconsole/test/browser/browser_webconsole_network_attach.js new file mode 100644 index 0000000000..308d39d530 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_network_attach.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_FILE = "test-network-request.html"; +const TEST_PATH = + "http://example.com/browser/devtools/client/webconsole/" + "test/browser/"; +const TEST_URI = TEST_PATH + TEST_FILE; + +registerCleanupFunction(async function() { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +add_task(async function task() { + await pushPref("devtools.webconsole.filter.net", false); + await pushPref("devtools.webconsole.filter.netxhr", true); + await openNewTabAndToolbox(TEST_URI, "netmonitor"); + + const currentTab = gBrowser.selectedTab; + const target = await TargetFactory.forTab(currentTab); + const toolbox = gDevTools.getToolbox(target); + const panel = toolbox.getCurrentPanel().panelWin; + + const netReady = panel.api.once("NetMonitor:PayloadReady"); + + // Fire an XHR POST request. + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.testXhrGet(); + }); + + info("XHR executed"); + + await netReady; + + info("NetMonitor:PayloadReady received"); + + const { hud } = await toolbox.selectTool("webconsole"); + + const xhrUrl = TEST_PATH + "test-data.json"; + const messageNode = await waitFor(() => findMessage(hud, xhrUrl)); + const urlNode = messageNode.querySelector(".url"); + info("Network message found."); + + const onReady = hud.ui.once("network-request-payload-ready"); + + // Expand network log + urlNode.click(); + + await onReady; + + info("network-request-payload-ready received"); + + await testNetworkMessage(messageNode); + await waitForLazyRequests(toolbox); +}); + +async function testNetworkMessage(messageNode) { + const headersTab = messageNode.querySelector("#headers-tab"); + + ok(headersTab, "Headers tab is available"); + + // Headers tab should be selected by default, so just check its content. + await waitUntil(() => + messageNode.querySelector("#headers-panel .headers-overview") + ); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_network_exceptions.js b/devtools/client/webconsole/test/browser/browser_webconsole_network_exceptions.js new file mode 100644 index 0000000000..011ad5b7bc --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_network_exceptions.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that we report JS exceptions in event handlers coming from +// network requests, like onreadystate for XHR. See bug 618078. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 618078"; +const TEST_URI2 = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-network-exceptions.html"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + // On e10s, the exception is triggered in child process + // and is ignored by test harness + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } + + const onMessage = waitForMessage(hud, "bug618078exception"); + await navigateTo(TEST_URI2); + const { node } = await onMessage; + ok(true, "Network exception logged as expected."); + ok(node.classList.contains("error"), "Network exception is logged as error."); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_network_message_close_on_escape.js b/devtools/client/webconsole/test/browser/browser_webconsole_network_message_close_on_escape.js new file mode 100644 index 0000000000..5f4e92a2d9 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_network_message_close_on_escape.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_FILE = "test-network-request.html"; +const TEST_PATH = + "https://example.com/browser/devtools/client/webconsole/test/browser/"; +const TEST_URI = TEST_PATH + TEST_FILE; + +registerCleanupFunction(async function() { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +add_task(async function task() { + await pushPref("devtools.webconsole.filter.netxhr", true); + const hud = await openNewTabAndConsole(TEST_URI); + + const currentTab = gBrowser.selectedTab; + const target = await TargetFactory.forTab(currentTab); + const toolbox = gDevTools.getToolbox(target); + + const xhrUrl = TEST_PATH + "test-data.json"; + const onMessage = waitForMessage(hud, xhrUrl); + + // Fire an XHR POST request. + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.testXhrGet(); + }); + + const { node: messageNode } = await onMessage; + ok(messageNode, "Network message found."); + + // Expand network log + info("Click on XHR message to display network detail panel"); + messageNode.querySelector(".url").click(); + const headersTab = await waitFor(() => + messageNode.querySelector("#headers-tab") + ); + ok(headersTab, "Headers tab is available"); + + // Wait for all network RDP request to be finished and have updated the UI + await waitUntil(() => + messageNode.querySelector("#headers-panel .headers-overview") + ); + + info("Focus header tab and hit Escape"); + headersTab.focus(); + EventUtils.sendKey("ESCAPE", toolbox.win); + + await waitFor(() => !messageNode.querySelector(".network-info")); + ok(true, "The detail panel was closed on escape"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_network_message_ctrl_click.js b/devtools/client/webconsole/test/browser/browser_webconsole_network_message_ctrl_click.js new file mode 100644 index 0000000000..c269630002 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_network_message_ctrl_click.js @@ -0,0 +1,67 @@ +// Test that URL opens in a new tab when click while +// pressing CTR (or CMD in MacOS) as expected. + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-console.html"; + +add_task(async function() { + // Enable net messages in the console for this test. + await pushPref("devtools.webconsole.filter.net", true); + const isMacOS = Services.appinfo.OS === "Darwin"; + + // We open the console + const hud = await openNewTabAndConsole(TEST_URI); + + info("Reload the content window to produce a network log"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.wrappedJSObject.location.reload(); + }); + const message = await waitFor(() => findMessage(hud, "test-console.html")); + ok(message, "Network log found in the console"); + + const currentTab = gBrowser.selectedTab; + const tabLoaded = listenToTabLoad(); + + info("Cmd/Ctrl click on the message"); + const urlObject = message.querySelector(".url"); + + await EventUtils.sendMouseEvent( + { + type: "click", + [isMacOS ? "metaKey" : "ctrlKey"]: true, + }, + urlObject, + hud.ui.window + ); + + info("Opening the URL of the message on a new tab"); + const newTab = await tabLoaded; + const newTabHref = newTab.linkedBrowser.currentURI.spec; + + is(newTabHref, TEST_URI, "Tab was opened with the expected URL"); + + info("Remove the new tab and select the previous tab back"); + gBrowser.removeTab(newTab); + gBrowser.selectedTab = currentTab; +}); + +/** + * Simple helper to wrap a tab load listener in a promise. + */ +function listenToTabLoad() { + return new Promise(resolve => { + gBrowser.tabContainer.addEventListener( + "TabOpen", + function(evt) { + const newTab = evt.target; + BrowserTestUtils.browserLoaded(newTab.linkedBrowser).then(() => + resolve(newTab) + ); + }, + { capture: true, once: true } + ); + }); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_expand.js b/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_expand.js new file mode 100644 index 0000000000..1b78de7ef7 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_expand.js @@ -0,0 +1,356 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_FILE = "test-network-request.html"; +const TEST_PATH = + "https://example.com/browser/devtools/client/webconsole/" + "test/browser/"; +const TEST_URI = TEST_PATH + TEST_FILE; + +requestLongerTimeout(2); + +registerCleanupFunction(async function() { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +pushPref("devtools.webconsole.filter.net", false); +pushPref("devtools.webconsole.filter.netxhr", true); + +const tabs = [ + { + id: "headers", + testEmpty: testEmptyHeaders, + testContent: testHeaders, + }, + { + id: "cookies", + testEmpty: testEmptyCookies, + testContent: testCookies, + }, + { + id: "params", + testEmpty: testEmptyParams, + testContent: testParams, + }, + { + id: "response", + testEmpty: testEmptyResponse, + testContent: testResponse, + }, + { + id: "timings", + testEmpty: testEmptyTimings, + testContent: testTimings, + }, + { + id: "stack-trace", + testEmpty: testEmptyStackTrace, + testContent: testStackTrace, + }, + { + id: "security", + testEmpty: testEmptySecurity, + testContent: testSecurity, + }, +]; + +/** + * Main test for checking HTTP logs in the Console panel. + */ +add_task(async function task() { + const hud = await openNewTabAndConsole(TEST_URI); + + const currentTab = gBrowser.selectedTab; + const target = await TargetFactory.forTab(currentTab); + + // Execute XHR and expand it after all network + // update events are received. Consequently, + // check out content of all (HTTP details) tabs. + await openRequestAfterUpdates(target, hud); + + // Test proper UI update when request is opened. + // For every tab (with HTTP details): + // 1. Execute long-time request + // 2. Expand the net log before the request finishes (set default tab) + // 3. Check the default tab empty content + // 4. Wait till the request finishes + // 5. Check content of all tabs + for (const tab of tabs) { + await openRequestBeforeUpdates(target, hud, tab); + } +}); + +async function openRequestAfterUpdates(target, hud) { + const toolbox = gDevTools.getToolbox(target); + + const xhrUrl = TEST_PATH + "sjs_slow-response-test-server.sjs"; + const onMessage = waitForMessage(hud, xhrUrl); + const onRequestUpdates = waitForRequestUpdates(hud); + const onPayloadReady = waitForPayloadReady(hud); + + // Fire an XHR POST request. + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.testXhrPostSlowResponse(); + }); + + const { node: messageNode } = await onMessage; + ok(messageNode, "Network message found."); + + await onRequestUpdates; + + // Expand network log + await expandXhrMessage(messageNode); + + const toggleButtonNode = messageNode.querySelector(".sidebar-toggle"); + ok(!toggleButtonNode, "Sidebar toggle button shouldn't be shown"); + + await onPayloadReady; + await testNetworkMessage(toolbox, messageNode); +} + +async function openRequestBeforeUpdates(target, hud, tab) { + const toolbox = gDevTools.getToolbox(target); + + await clearOutput(hud); + + const xhrUrl = TEST_PATH + "sjs_slow-response-test-server.sjs"; + const onMessage = waitForMessage(hud, xhrUrl); + const onRequestUpdates = waitForRequestUpdates(hud); + const onPayloadReady = waitForPayloadReady(hud); + + // Fire an XHR POST request. + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.testXhrPostSlowResponse(); + }); + const { node: messageNode } = await onMessage; + ok(messageNode, "Network message found."); + + // Set the default panel. + const state = hud.ui.wrapper.getStore().getState(); + state.ui.networkMessageActiveTabId = tab.id; + + // Expand network log + await expandXhrMessage(messageNode); + + // Except the security tab. It isn't available till the + // "securityInfo" packet type is received, so doesn't + // fit this part of the test. + if (tab.id != "security") { + // Make sure the current tab is the expected one. + const currentTab = messageNode.querySelector(`#${tab.id}-tab`); + is( + currentTab.getAttribute("aria-selected"), + "true", + "The correct tab is selected" + ); + + // The tab should be empty now. + tab.testEmpty(messageNode); + } + + // Wait till all updates and payload are received. + await onRequestUpdates; + await onPayloadReady; + + // Test content of the default tab. + await tab.testContent(messageNode); + + // Test all tabs in the network log. + await testNetworkMessage(toolbox, messageNode); +} + +// Panel testing helpers + +async function testNetworkMessage(toolbox, messageNode) { + await testStatusInfo(messageNode); + await testHeaders(messageNode); + await testCookies(messageNode); + await testParams(messageNode); + await testResponse(messageNode); + await testTimings(messageNode); + await testStackTrace(messageNode); + await testSecurity(messageNode); + await waitForLazyRequests(toolbox); +} + +// Status Info + +function testStatusInfo(messageNode) { + const statusInfo = messageNode.querySelector(".status-info"); + ok(statusInfo, "Status info is not empty"); +} + +// Headers + +function testEmptyHeaders(messageNode) { + const emptyNotice = messageNode.querySelector("#headers-panel .empty-notice"); + ok(emptyNotice, "Headers tab is empty"); +} + +async function testHeaders(messageNode) { + const headersTab = messageNode.querySelector("#headers-tab"); + ok(headersTab, "Headers tab is available"); + + // Select Headers tab and check the content. + headersTab.click(); + await waitFor(() => + messageNode.querySelector("#headers-panel .headers-overview") + ); +} + +// Cookies + +function testEmptyCookies(messageNode) { + const emptyNotice = messageNode.querySelector("#cookies-panel .empty-notice"); + ok(emptyNotice, "Cookies tab is empty"); +} + +async function testCookies(messageNode) { + const cookiesTab = messageNode.querySelector("#cookies-tab"); + ok(cookiesTab, "Cookies tab is available"); + + // Select tab and check the content. + cookiesTab.click(); + await waitFor(() => + messageNode.querySelector("#cookies-panel .treeValueCell") + ); +} + +// Params + +function testEmptyParams(messageNode) { + const emptyNotice = messageNode.querySelector("#params-panel .empty-notice"); + ok(emptyNotice, "Params tab is empty"); +} + +async function testParams(messageNode) { + const paramsTab = messageNode.querySelector("#params-tab"); + ok(paramsTab, "Params tab is available"); + + // Select Params tab and check the content. CodeMirror initialization + // is delayed to prevent UI freeze, so wait for a little while. + paramsTab.click(); + const paramsPanel = messageNode.querySelector("#params-panel"); + await waitForSourceEditor(paramsPanel); + const paramsContent = messageNode.querySelector( + "#params-panel .panel-container .CodeMirror" + ); + ok(paramsContent, "Params content is available"); + ok( + paramsContent.textContent.includes("Hello world!"), + "Post body is correct" + ); +} + +// Response + +function testEmptyResponse(messageNode) { + const panel = messageNode.querySelector("#response-panel .tab-panel"); + is(panel.textContent, "", "Cookies tab is empty"); +} + +async function testResponse(messageNode) { + const responseTab = messageNode.querySelector("#response-tab"); + ok(responseTab, "Response tab is available"); + + // Select Response tab and check the content. CodeMirror initialization + // is delayed, so again wait for a little while. + responseTab.click(); + const responsePanel = messageNode.querySelector("#response-panel"); + await waitForSourceEditor(responsePanel); + const responseContent = messageNode.querySelector( + "#response-panel .editor-row-container .CodeMirror" + ); + ok(responseContent, "Response content is available"); + ok(responseContent.textContent, "Response text is available"); +} + +// Timings + +function testEmptyTimings(messageNode) { + const panel = messageNode.querySelector("#timings-panel .tab-panel"); + is(panel.textContent, "", "Timings tab is empty"); +} + +async function testTimings(messageNode) { + const timingsTab = messageNode.querySelector("#timings-tab"); + ok(timingsTab, "Timings tab is available"); + + // Select Timings tab and check the content. + timingsTab.click(); + await waitFor(() => + messageNode.querySelector( + "#timings-panel .timings-container .timings-label" + ) + ); + const timingsContent = messageNode.querySelector( + "#timings-panel .timings-container .timings-label" + ); + ok(timingsContent, "Timings content is available"); + ok(timingsContent.textContent, "Timings text is available"); +} + +// Stack Trace + +function testEmptyStackTrace(messageNode) { + const panel = messageNode.querySelector("#stack-trace-panel .stack-trace"); + is(panel.textContent, "", "StackTrace tab is empty"); +} + +async function testStackTrace(messageNode) { + const stackTraceTab = messageNode.querySelector("#stack-trace-tab"); + ok(stackTraceTab, "StackTrace tab is available"); + + // Select Timings tab and check the content. + stackTraceTab.click(); + await waitFor(() => + messageNode.querySelector("#stack-trace-panel .frame-link") + ); +} + +// Security + +function testEmptySecurity(messageNode) { + const panel = messageNode.querySelector("#security-panel .tab-panel"); + is(panel.textContent, "", "Security tab is empty"); +} + +async function testSecurity(messageNode) { + const securityTab = messageNode.querySelector("#security-tab"); + ok(securityTab, "Security tab is available"); + + // Select Timings tab and check the content. + securityTab.click(); + await waitFor(() => + messageNode.querySelector("#security-panel .treeTable .treeRow") + ); +} + +// Waiting helpers + +async function waitForPayloadReady(hud) { + return hud.ui.once("network-request-payload-ready"); +} + +async function waitForSourceEditor(panel) { + return waitUntil(() => { + return !!panel.querySelector(".CodeMirror"); + }); +} + +async function waitForRequestUpdates(hud) { + return hud.ui.once("network-messages-updated"); +} + +function expandXhrMessage(node) { + info( + "Click on XHR message and wait for the network detail panel to be displayed" + ); + node.querySelector(".url").click(); + return waitFor(() => node.querySelector(".network-info")); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_openinnet.js b/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_openinnet.js new file mode 100644 index 0000000000..6f672ed0f1 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_openinnet.js @@ -0,0 +1,112 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf8,Test that 'Open in Network Panel' " + + "context menu item opens the selected request in netmonitor panel."; + +const TEST_FILE = "test-network-request.html"; +const JSON_TEST_URL = "test-network-request.html"; +const TEST_PATH = + "http://example.com/browser/devtools/client/webconsole/test/browser/"; + +const NET_PREF = "devtools.webconsole.filter.net"; +const XHR_PREF = "devtools.webconsole.filter.netxhr"; + +Services.prefs.setBoolPref(NET_PREF, true); +Services.prefs.setBoolPref(XHR_PREF, true); + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref(NET_PREF); + Services.prefs.clearUserPref(XHR_PREF); + + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +add_task(async function task() { + const hud = await openNewTabAndConsole(TEST_URI); + + const currentTab = gBrowser.selectedTab; + const target = await TargetFactory.forTab(currentTab); + const toolbox = gDevTools.getToolbox(target); + + const documentUrl = TEST_PATH + TEST_FILE; + await navigateTo(documentUrl); + info("Document loaded."); + + await openMessageInNetmonitor(toolbox, hud, documentUrl); + + info( + "Wait for the netmonitor headers panel to appear as it spawn RDP requests" + ); + const netmonitor = toolbox.getCurrentPanel(); + await waitUntil(() => + netmonitor.panelWin.document.querySelector( + "#headers-panel .headers-overview" + ) + ); + + info( + "Wait for the event timings request which do not necessarily update the UI as timings may be undefined for cached requests" + ); + await waitForRequestData(netmonitor.panelWin.store, ["eventTimings"], 0); + + // Go back to console. + await toolbox.selectTool("webconsole"); + info("console panel open again."); + + // Fire an XHR request. + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() { + // Ensure XHR request is completed + await new Promise(resolve => content.wrappedJSObject.testXhrGet(resolve)); + }); + + const jsonUrl = TEST_PATH + JSON_TEST_URL; + await openMessageInNetmonitor(toolbox, hud, jsonUrl); + + info( + "Wait for the netmonitor headers panel to appear as it spawn RDP requests" + ); + await waitUntil(() => + netmonitor.panelWin.document.querySelector( + "#headers-panel .headers-overview" + ) + ); + + info( + "Wait for the event timings request which do not necessarily update the UI as timings may be undefined for cached requests" + ); + + // Hide the header panel to get the eventTimings + const { windowRequire } = netmonitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + info("Closing the header panel"); + await netmonitor.panelWin.store.dispatch(Actions.toggleNetworkDetails()); + + await waitForRequestData(netmonitor.panelWin.store, ["eventTimings"], 1); +}); + +const { + getSortedRequests, +} = require("devtools/client/netmonitor/src/selectors/index"); + +function waitForRequestData(store, fields, i) { + return waitUntil(() => { + const item = getSortedRequests(store.getState())[i]; + if (!item) { + return false; + } + for (const field of fields) { + if (!item[field]) { + return false; + } + } + return true; + }); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_resend_request.js b/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_resend_request.js new file mode 100644 index 0000000000..8708e1ec1b --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_resend_request.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf8,Test that 'Resend Request' context menu " + + "item resends the selected request and select it in netmonitor panel."; + +const TEST_FILE = "test-network-request.html"; +const TEST_PATH = + "http://example.com/browser/devtools/client/webconsole/test/browser/"; + +registerCleanupFunction(async function() { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +add_task(async function task() { + await pushPref("devtools.webconsole.filter.net", true); + + const hud = await openNewTabAndConsole(TEST_URI); + + const documentUrl = TEST_PATH + TEST_FILE; + await navigateTo(documentUrl); + info("Document loaded."); + + const message = await waitFor(() => findMessage(hud, documentUrl)); + + const menuPopup = await openContextMenu(hud, message); + const openResendRequestMenuItem = menuPopup.querySelector( + "#console-menu-resend-network-request" + ); + ok(openResendRequestMenuItem, "resend network request item is enabled"); + + // Wait for message containing the resent request url + openResendRequestMenuItem.click(); + await waitFor(() => findMessages(hud, documentUrl).length === 2); + + ok(true, "The resent request url is correct."); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_stacktrace_console_initiated_request.js b/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_stacktrace_console_initiated_request.js new file mode 100644 index 0000000000..38ad3a553f --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_stacktrace_console_initiated_request.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_FILE = "test-network-request.html"; +const TEST_PATH = + "https://example.com/browser/devtools/client/webconsole/" + "test/browser/"; +const TEST_URI = TEST_PATH + TEST_FILE; + +pushPref("devtools.webconsole.filter.netxhr", true); + +registerCleanupFunction(async function() { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +add_task(async function task() { + const hud = await openNewTabAndConsole(TEST_URI); + + const xhrUrl = TEST_PATH + "sjs_slow-response-test-server.sjs"; + + info("Fire an XHR POST request from the console."); + const { node: messageNode } = await executeAndWaitForMessage( + hud, + ` + xhrConsole = () => testXhrPostSlowResponse(); + xhrConsole(); + `, + xhrUrl + ); + + ok(messageNode, "Network message found."); + + info("Expand the network message"); + await expandXhrMessage(messageNode); + const stackTraceTab = messageNode.querySelector("#stack-trace-tab"); + ok(stackTraceTab, "StackTrace tab is available"); + + stackTraceTab.click(); + const selector = "#stack-trace-panel .frame-link"; + await waitFor(() => messageNode.querySelector(selector)); + const frames = [...messageNode.querySelectorAll(selector)]; + + is(frames.length, 4, "There's the expected frames"); + const functionNames = frames.map( + f => f.querySelector(".frame-link-function-display-name").textContent + ); + is( + functionNames.join("|"), + "makeXhr|testXhrPostSlowResponse|xhrConsole|<anonymous>", + "The stacktrace does not have devtools' internal frames" + ); +}); + +function expandXhrMessage(node) { + info( + "Click on XHR message and wait for the network detail panel to be displayed" + ); + node.querySelector(".url").click(); + return waitFor(() => node.querySelector("#stack-trace-tab")); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_status_code.js b/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_status_code.js new file mode 100644 index 0000000000..bb8ad37c9a --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_status_code.js @@ -0,0 +1,113 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_FILE = "test-network-request.html"; +const TEST_PATH = + "http://example.com/browser/devtools/client/webconsole/" + "test/browser/"; +const TEST_URI = TEST_PATH + TEST_FILE; + +const NET_PREF = "devtools.webconsole.filter.net"; +const XHR_PREF = "devtools.webconsole.filter.netxhr"; +const { l10n } = require("devtools/client/webconsole/utils/messages"); +const LEARN_MORE_URI = + "https://developer.mozilla.org/docs/Web/HTTP/Status/200" + GA_PARAMS; + +pushPref(NET_PREF, true); +pushPref(XHR_PREF, true); + +registerCleanupFunction(async function() { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +add_task(async function task() { + const hud = await openNewTabAndConsole(TEST_URI); + + const onNetworkMessageUpdate = hud.ui.once("network-messages-updated"); + + // Fire an XHR POST request. + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.testXhrPost(); + }); + + info("XHR executed"); + await onNetworkMessageUpdate; + + const xhrUrl = TEST_PATH + "test-data.json"; + const messageNode = await waitFor(() => findMessage(hud, xhrUrl)); + const statusCodeNode = messageNode.querySelector(".status-code"); + info("Network message found."); + + is( + statusCodeNode.title, + l10n.getStr("webConsoleMoreInfoLabel"), + "Status code has the expected tooltip" + ); + + const { + rightClickMouseEvent, + rightClickCtrlOrCmdKeyMouseEvent, + } = getMouseEvents(); + + const testCases = [ + { clickEvent: null, link: LEARN_MORE_URI, where: "tab" }, + { clickEvent: rightClickMouseEvent, link: null, where: null }, + { clickEvent: rightClickCtrlOrCmdKeyMouseEvent, link: null, where: null }, + ]; + + for (const testCase of testCases) { + info("Test case"); + const { clickEvent } = testCase; + const onConsoleMenuOpened = [ + rightClickMouseEvent, + rightClickCtrlOrCmdKeyMouseEvent, + ].includes(clickEvent) + ? hud.ui.wrapper.once("menu-open") + : null; + + const { link, where } = await simulateLinkClick( + statusCodeNode, + testCase.clickEvent + ); + is(link, testCase.link, `Clicking the provided link opens ${link}`); + is(where, testCase.where, `Link opened in correct tab`); + + if (onConsoleMenuOpened) { + info("Check if context menu is opened on right clicking the status-code"); + await onConsoleMenuOpened; + ok(true, "Console menu is opened"); + } + } + + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +function getMouseEvents() { + const isOSX = Services.appinfo.OS == "Darwin"; + + const rightClickMouseEvent = new MouseEvent("contextmenu", { + bubbles: true, + button: 2, + view: window, + }); + const rightClickCtrlOrCmdKeyMouseEvent = new MouseEvent("contextmenu", { + bubbles: true, + button: 2, + [isOSX ? "metaKey" : "ctrlKey"]: true, + view: window, + }); + + return { + rightClickMouseEvent, + rightClickCtrlOrCmdKeyMouseEvent, + }; +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_network_requests_from_chrome.js b/devtools/client/webconsole/test/browser/browser_webconsole_network_requests_from_chrome.js new file mode 100644 index 0000000000..ae5ebfd6c0 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_network_requests_from_chrome.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that network requests from chrome don't cause the Web Console to +// throw exceptions. See Bug 597136. + +"use strict"; + +const TEST_URI = "http://example.com/"; + +add_task(async function() { + // Start a listener on the console service. + let good = true; + const listener = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + observe: function(subject) { + if ( + subject instanceof Ci.nsIScriptError && + subject.category === "XPConnect JavaScript" && + subject.sourceName.includes("webconsole") + ) { + good = false; + } + }, + }; + Services.console.registerListener(listener); + + // trigger a lazy-load of the HUD Service + BrowserConsoleManager; + + await sendRequestFromChrome(); + + ok( + good, + "No exception was thrown when sending a network request from a chrome window" + ); + + Services.console.unregisterListener(listener); +}); + +function sendRequestFromChrome() { + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + + xhr.addEventListener( + "load", + () => { + window.setTimeout(resolve, 0); + }, + { once: true } + ); + + xhr.open("GET", TEST_URI, true); + xhr.send(null); + }); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_network_reset_filter.js b/devtools/client/webconsole/test/browser/browser_webconsole_network_reset_filter.js new file mode 100644 index 0000000000..affb4c7de0 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_network_reset_filter.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that network log messages bring up the network panel and select the +// right request even if it was previously filtered off. + +"use strict"; + +const TEST_PATH = + "http://example.com/browser/devtools/client/webconsole/" + "test/browser/"; +const TEST_URI = "data:text/html;charset=utf8,<p>test file URI"; + +add_task(async function() { + await pushPref("devtools.webconsole.filter.net", true); + + const toolbox = await openNewTabAndToolbox(TEST_URI, "webconsole"); + const hud = toolbox.getCurrentPanel().hud; + + const onMessages = waitForMessages({ + hud, + messages: [ + { text: "running network console logging tests" }, + { text: "test-network.html" }, + { text: "testscript.js" }, + ], + }); + + info("Wait for document to load"); + await navigateTo(TEST_PATH + "test-network.html"); + + info("Wait for expected messages to appear"); + await onMessages; + + const url = TEST_PATH + "testscript.js?foo"; + // The url as it appears in the webconsole, without the GET parameters + const shortUrl = TEST_PATH + "testscript.js"; + + info("Open the testscript.js request in the network monitor"); + await openMessageInNetmonitor(toolbox, hud, url, shortUrl); + + const netmonitor = toolbox.getCurrentPanel(); + + info( + "Wait for the netmonitor headers panel to appear as it spawn RDP requests" + ); + await waitUntil(() => + netmonitor.panelWin.document.querySelector( + "#headers-panel .headers-overview" + ) + ); + + info("Filter out the current request"); + const { store, windowRequire } = netmonitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.toggleRequestFilterType("js")); + + info("Select back the webconsole"); + await toolbox.selectTool("webconsole"); + is(toolbox.currentToolId, "webconsole", "Web console was selected"); + + info("Open the testscript.js request again in the network monitor"); + await openMessageInNetmonitor(toolbox, hud, url, shortUrl); + + info( + "Wait for the netmonitor headers panel to appear as it spawn RDP requests" + ); + await waitUntil(() => + netmonitor.panelWin.document.querySelector( + "#headers-panel .headers-overview" + ) + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_nodes_highlight.js b/devtools/client/webconsole/test/browser/browser_webconsole_nodes_highlight.js new file mode 100644 index 0000000000..40ca087e15 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_nodes_highlight.js @@ -0,0 +1,81 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Check hovering logged nodes highlight them in the content page. + +const HTML = ` + <!DOCTYPE html> + <html> + <body> + <h1>Node Highlight Test</h1> + </body> + <script> + function logNode(selector) { + console.log(document.querySelector(selector)); + } + </script> + </html> +`; +const TEST_URI = "data:text/html;charset=utf-8," + encodeURI(HTML); + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const toolbox = hud.toolbox; + + const testActor = await getTestActor(toolbox); + const highlighter = toolbox.getHighlighter(); + let onHighlighterShown; + let onHighlighterHidden; + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.wrappedJSObject.logNode("h1"); + }); + + const msg = await waitFor(() => findMessage(hud, "<h1>")); + const node = msg.querySelector(".objectBox-node"); + ok(node !== null, "Node was logged as expected"); + const view = node.ownerDocument.defaultView; + const nonHighlightEl = toolbox.doc.getElementById( + "toolbox-meatball-menu-button" + ); + + info("Highlight the node by moving the cursor on it"); + onHighlighterShown = highlighter.waitForHighlighterShown(); + + EventUtils.synthesizeMouseAtCenter(node, { type: "mousemove" }, view); + + const { nodeFront } = await onHighlighterShown; + is(nodeFront.displayName, "h1", "The correct node was highlighted"); + isVisible = await testActor.isHighlighting(); + ok(isVisible, "Highlighter is displayed"); + + info("Unhighlight the node by moving away from the node"); + onHighlighterHidden = highlighter.waitForHighlighterHidden(); + EventUtils.synthesizeMouseAtCenter( + nonHighlightEl, + { type: "mousemove" }, + view + ); + + await onHighlighterHidden; + ok(true, "node-unhighlight event was fired when moving away from the node"); + + info("Check we don't have zombie highlighters when briefly hovering a node"); + onHighlighterShown = highlighter.waitForHighlighterShown(); + onHighlighterHidden = highlighter.waitForHighlighterHidden(); + // Move hover the node and then right after move out. + EventUtils.synthesizeMouseAtCenter(node, { type: "mousemove" }, view); + EventUtils.synthesizeMouseAtCenter( + nonHighlightEl, + { type: "mousemove" }, + view + ); + await Promise.all([onHighlighterShown, onHighlighterHidden]); + ok(true, "The highlighter was removed"); + + isVisible = await testActor.isHighlighting(); + is(isVisible, false, "The highlighter is not displayed anymore"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_nodes_select.js b/devtools/client/webconsole/test/browser/browser_webconsole_nodes_select.js new file mode 100644 index 0000000000..cd93c8e9da --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_nodes_select.js @@ -0,0 +1,66 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Check clicking on open-in-inspector icon does select the node in the inspector. + +const HTML = ` + <!DOCTYPE html> + <html> + <body> + <h1>Select node in inspector test</h1> + </body> + <script> + function logNode(selector) { + console.log(document.querySelector(selector)); + } + </script> + </html> +`; +const TEST_URI = "data:text/html;charset=utf-8," + encodeURI(HTML); + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const toolbox = hud.toolbox; + + // Loading the inspector panel at first, to make it possible to listen for + // new node selections + await toolbox.loadTool("inspector"); + const inspector = toolbox.getPanel("inspector"); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.wrappedJSObject.logNode("h1"); + }); + + const msg = await waitFor(() => findMessage(hud, "<h1>")); + const node = msg.querySelector(".objectBox-node"); + ok(node !== null, "Node was logged as expected"); + + const openInInspectorIcon = node.querySelector(".open-inspector"); + ok(openInInspectorIcon !== null, "The is an open in inspector icon"); + + info( + "Clicking on the inspector icon and waiting for the " + + "inspector to be selected" + ); + const onInspectorSelected = toolbox.once("inspector-selected"); + const onInspectorUpdated = inspector.once("inspector-updated"); + const onNewNode = toolbox.selection.once("new-node-front"); + + openInInspectorIcon.click(); + + await onInspectorSelected; + await onInspectorUpdated; + const nodeFront = await onNewNode; + + ok(true, "Inspector selected and new node got selected"); + is(nodeFront.displayName, "h1", "The expected node was selected"); + + is( + msg.querySelector(".arrow").classList.contains("expanded"), + false, + "The object inspector wasn't expanded" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_non_javascript_mime_warning.js b/devtools/client/webconsole/test/browser/browser_webconsole_non_javascript_mime_warning.js new file mode 100644 index 0000000000..9401baf03a --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_non_javascript_mime_warning.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that <script> loads with non-JavaScript MIME types produce a warning. +// See Bug 1510223. + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/" + + "test-non-javascript-mime.html"; +const MIME_WARNING_MSG = + "The script from “http://example.com/browser/devtools/client/webconsole/test/browser/test-non-javascript-mime.js” was loaded even though its MIME type (“text/plain”) is not a valid JavaScript MIME type"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + await waitFor( + () => findMessage(hud, MIME_WARNING_MSG, ".message.warn"), + "", + 100 + ); + ok(true, "MIME type warning displayed"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_non_javascript_mime_worker_error.js b/devtools/client/webconsole/test/browser/browser_webconsole_non_javascript_mime_worker_error.js new file mode 100644 index 0000000000..9f38aeef84 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_non_javascript_mime_worker_error.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that importScripts loads inside a worker with a non-JavaScript +// MIME types produce an error and fail. +// See Bug 1514680. +// Also tests that `new Worker` with a non-JS MIME type fails. (Bug 1523706) + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/" + + "test-non-javascript-mime-worker.html"; + +const JS_URI = + "http://example.com/browser/devtools/client/webconsole/test/browser/test-non-javascript-mime.js"; +const MIME_ERROR_MSG1 = `Loading Worker from “${JS_URI}” was blocked because of a disallowed MIME type (“text/plain”).`; +const MIME_ERROR_MSG2 = `Loading script from “${JS_URI}” with importScripts() was blocked because of a disallowed MIME type (“text/plain”).`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + await waitFor( + () => findMessage(hud, MIME_ERROR_MSG1, ".message.error"), + "", + 100 + ); + await waitFor( + () => findMessage(hud, MIME_ERROR_MSG2, ".message.error"), + "", + 100 + ); + ok(true, "MIME type error displayed"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_object_ctrl_click.js b/devtools/client/webconsole/test/browser/browser_webconsole_object_ctrl_click.js new file mode 100644 index 0000000000..0b5728eeee --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_ctrl_click.js @@ -0,0 +1,122 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the ObjectInspector is rendered correctly in the sidebar. + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf8,<script> + console.log({ + a:1, + b:2, + c:[3] + }); + </script>`; + +add_task(async function() { + // Should be removed when sidebar work is complete + await pushPref("devtools.webconsole.sidebarToggle", true); + const isMacOS = Services.appinfo.OS === "Darwin"; + + const hud = await openNewTabAndConsole(TEST_URI); + + const message = findMessage(hud, "Object"); + const object = message.querySelector(".object-inspector .objectBox-object"); + + info("Ctrl+click on an object to put it in the sidebar"); + const onSidebarShown = waitFor(() => + hud.ui.document.querySelector(".sidebar") + ); + AccessibilityUtils.setEnv({ + // Component that renders an object handles keyboard interactions on the + // container level. + mustHaveAccessibleRule: false, + }); + await EventUtils.sendMouseEvent( + { + type: "click", + [isMacOS ? "metaKey" : "ctrlKey"]: true, + }, + object, + hud.ui.window + ); + AccessibilityUtils.resetEnv(); + await onSidebarShown; + ok(true, "sidebar is displayed after user Ctrl+clicked on it"); + + const sidebarContents = hud.ui.document.querySelector(".sidebar-contents"); + let objectInspectors = [...sidebarContents.querySelectorAll(".tree")]; + is( + objectInspectors.length, + 1, + "There is the expected number of object inspectors" + ); + let [objectInspector] = objectInspectors; + + // The object in the sidebar now should look like: + // ▼ { … } + // | a: 1 + // | b: 2 + // | ▶︎ c: Array [3] + // | ▶︎ <prototype>: Object { … } + await waitFor(() => objectInspector.querySelectorAll(".node").length === 5); + + let propertiesNodes = [ + ...objectInspector.querySelectorAll(".object-label"), + ].map(el => el.textContent); + let arrayPropertiesNames = ["a", "b", "c", "<prototype>"]; + is( + JSON.stringify(propertiesNodes), + JSON.stringify(arrayPropertiesNames), + "The expected nodes are displayed" + ); + + is( + message.querySelectorAll(".node").length, + 1, + "The message in the content panel wasn't expanded" + ); + + info( + "Expand the output message and Ctrl+click on the `c` property node to put it in the sidebar" + ); + message.querySelector(".node").click(); + const cNode = await waitFor(() => message.querySelectorAll(".node")[3]); + AccessibilityUtils.setEnv({ + // Component that renders an object handles keyboard interactions on the + // container level. + focusableRule: false, + interactiveRule: false, + labelRule: false, + }); + await EventUtils.sendMouseEvent( + { + type: "click", + [isMacOS ? "metaKey" : "ctrlKey"]: true, + }, + cNode, + hud.ui.window + ); + AccessibilityUtils.resetEnv(); + + objectInspectors = [...sidebarContents.querySelectorAll(".tree")]; + is(objectInspectors.length, 1, "There is still only one object inspector"); + [objectInspector] = objectInspectors; + + // The object in the sidebar now should look like: + // ▼ (1) […] + // | 0: 3 + // | length: 1 + // | ▶︎ <prototype>: Array [] + await waitFor(() => objectInspector.querySelectorAll(".node").length === 4); + + propertiesNodes = [...objectInspector.querySelectorAll(".object-label")].map( + el => el.textContent + ); + arrayPropertiesNames = ["0", "length", "<prototype>"]; + is( + JSON.stringify(propertiesNodes), + JSON.stringify(arrayPropertiesNames), + "The expected nodes are displayed" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_object_in_sidebar_keyboard_nav.js b/devtools/client/webconsole/test/browser/browser_webconsole_object_in_sidebar_keyboard_nav.js new file mode 100644 index 0000000000..470c184c15 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_in_sidebar_keyboard_nav.js @@ -0,0 +1,106 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the ObjectInspector in the sidebar can be navigated with the keyboard. + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf8, + <script> + console.log({ + a:1, + b:2, + c: Array.from({length: 100}, (_, i) => i) + }); + </script>`; +const { ELLIPSIS } = require("devtools/shared/l10n"); + +add_task(async function() { + // Should be removed when sidebar work is complete + await pushPref("devtools.webconsole.sidebarToggle", true); + + const hud = await openNewTabAndConsole(TEST_URI); + + const message = await waitFor(() => findMessage(hud, "Object")); + const object = message.querySelector(".object-inspector .objectBox-object"); + + const onSideBarVisible = waitFor(() => + hud.ui.document.querySelector(".sidebar-contents") + ); + + await openObjectInSidebar(hud, object); + const sidebarContents = await onSideBarVisible; + + const objectInspector = sidebarContents.querySelector(".object-inspector"); + ok(objectInspector, "The ObjectInspector is displayed"); + + // There are 5 nodes: the root, a, b, c, and proto. + await waitFor(() => objectInspector.querySelectorAll(".node").length === 5); + objectInspector.focus(); + + const [root, a, b, c] = objectInspector.querySelectorAll(".node"); + + ok(root.classList.contains("focused"), "The root node is focused"); + + await synthesizeKeyAndWaitForFocus("KEY_ArrowDown", a); + ok(true, "`a` node is focused"); + + await synthesizeKeyAndWaitForFocus("KEY_ArrowDown", b); + ok(true, "`b` node is focused"); + + await synthesizeKeyAndWaitForFocus("KEY_ArrowDown", c); + ok(true, "`c` node is focused"); + + EventUtils.synthesizeKey("KEY_ArrowRight"); + await waitFor(() => objectInspector.querySelectorAll(".node").length > 5); + ok(true, "`c` node is expanded"); + + const arrayNodes = objectInspector.querySelectorAll(`[aria-level="3"]`); + await synthesizeKeyAndWaitForFocus("KEY_ArrowDown", arrayNodes[0]); + ok(true, "First item of the `c` array is focused"); + + await synthesizeKeyAndWaitForFocus("KEY_ArrowLeft", c); + ok(true, "`c` node is focused again"); + + await synthesizeKeyAndWaitForFocus("KEY_ArrowUp", b); + ok(true, "`b` node is focused again"); + + info("Select another object in the console output"); + const onArrayMessage = waitForMessage(hud, "Array"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.wrappedJSObject.console.log([4, 5, 6]); + }); + + const arrayMessage = await onArrayMessage; + const array = arrayMessage.node.querySelector( + ".object-inspector .objectBox-array" + ); + await openObjectInSidebar(hud, array); + + await waitFor(() => + sidebarContents + .querySelector(".tree-node") + .textContent.includes(`(3) [${ELLIPSIS}]`) + ); + ok( + sidebarContents.querySelector(".tree-node").classList.contains("focused"), + "The root node of the new object in the sidebar is focused" + ); +}); + +async function openObjectInSidebar(hud, objectNode) { + const contextMenu = await openContextMenu(hud, objectNode); + const openInSidebarEntry = contextMenu.querySelector( + "#console-menu-open-sidebar" + ); + openInSidebarEntry.click(); + await hideContextMenu(hud); +} + +function synthesizeKeyAndWaitForFocus(keyStr, elementToBeFocused) { + const onFocusChanged = waitFor(() => + elementToBeFocused.classList.contains("focused") + ); + EventUtils.synthesizeKey(keyStr); + return onFocusChanged; +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector.js b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector.js new file mode 100644 index 0000000000..ac0ff2c5a4 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector.js @@ -0,0 +1,156 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check expanding/collapsing object inspector in the console. +const TEST_URI = "data:text/html;charset=utf8,<h1>test Object Inspector</h1>"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + logAllStoreChanges(hud); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.console.log("oi-test", [1, 2, { a: "a", b: "b" }], { + c: "c", + d: [3, 4], + length: 987, + }); + }); + + const node = await waitFor(() => findMessage(hud, "oi-test")); + const objectInspectors = [...node.querySelectorAll(".tree")]; + is( + objectInspectors.length, + 2, + "There is the expected number of object inspectors" + ); + + const [arrayOi, objectOi] = objectInspectors; + + info("Expanding the array object inspector"); + + let onArrayOiMutation = waitForNodeMutation(arrayOi, { + childList: true, + }); + + arrayOi.querySelector(".arrow").click(); + await onArrayOiMutation; + + ok( + arrayOi.querySelector(".arrow").classList.contains("expanded"), + "The arrow of the root node of the tree is expanded after clicking on it" + ); + + let arrayOiNodes = arrayOi.querySelectorAll(".node"); + + // The object inspector now looks like: + // ▼ […] + // | 0: 1 + // | 1: 2 + // | ▶︎ 2: {a: "a", b: "b"} + // | length: 3 + // | ▶︎ <prototype> + is( + arrayOiNodes.length, + 6, + "There is the expected number of nodes in the tree" + ); + + info("Expanding a leaf of the array object inspector"); + let arrayOiNestedObject = arrayOiNodes[3]; + onArrayOiMutation = waitForNodeMutation(arrayOi, { + childList: true, + }); + + arrayOiNestedObject.querySelector(".arrow").click(); + await onArrayOiMutation; + + ok( + arrayOiNestedObject.querySelector(".arrow").classList.contains("expanded"), + "The arrow of the root node of the tree is expanded after clicking on it" + ); + + arrayOiNodes = arrayOi.querySelectorAll(".node"); + + // The object inspector now looks like: + // ▼ […] + // | 0: 1 + // | 1: 2 + // | ▼ 2: {…} + // | | a: "a" + // | | b: "b" + // | | ▶︎ <prototype> + // | length: 3 + // | ▶︎ <prototype> + is( + arrayOiNodes.length, + 9, + "There is the expected number of nodes in the tree" + ); + + info("Collapsing the root"); + onArrayOiMutation = waitForNodeMutation(arrayOi, { + childList: true, + }); + arrayOi.querySelector(".arrow").click(); + + is( + arrayOi.querySelector(".arrow").classList.contains("expanded"), + false, + "The arrow of the root node of the tree is collapsed after clicking on it" + ); + + arrayOiNodes = arrayOi.querySelectorAll(".node"); + is(arrayOiNodes.length, 1, "Only the root node is visible"); + + info("Expanding the root again"); + onArrayOiMutation = waitForNodeMutation(arrayOi, { + childList: true, + }); + arrayOi.querySelector(".arrow").click(); + + ok( + arrayOi.querySelector(".arrow").classList.contains("expanded"), + "The arrow of the root node of the tree is expanded again after clicking on it" + ); + + arrayOiNodes = arrayOi.querySelectorAll(".node"); + arrayOiNestedObject = arrayOiNodes[3]; + ok( + arrayOiNestedObject.querySelector(".arrow").classList.contains("expanded"), + "The object tree is still expanded" + ); + + is( + arrayOiNodes.length, + 9, + "There is the expected number of nodes in the tree" + ); + + const onObjectOiMutation = waitForNodeMutation(objectOi, { + childList: true, + }); + + objectOi.querySelector(".arrow").click(); + await onObjectOiMutation; + + ok( + objectOi.querySelector(".arrow").classList.contains("expanded"), + "The arrow of the root node of the tree is expanded after clicking on it" + ); + + const objectOiNodes = objectOi.querySelectorAll(".node"); + // The object inspector now looks like: + // ▼ {…} + // | c: "c" + // | ▶︎ d: [3, 4] + // | length: 987 + // | ▶︎ <prototype> + is( + objectOiNodes.length, + 5, + "There is the expected number of nodes in the tree" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector__proto__.js b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector__proto__.js new file mode 100644 index 0000000000..abcd299f7b --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector__proto__.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check displaying object with __proto__ in the console. +const TEST_URI = + "data:text/html;charset=utf8,<h1>test Object Inspector __proto__</h1>"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + logAllStoreChanges(hud); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + const obj = Object.create(null); + // eslint-disable-next-line no-proto + obj.__proto__ = []; + content.wrappedJSObject.console.log("oi-test", obj); + }); + + const node = await waitFor(() => findMessage(hud, "oi-test")); + const objectInspector = node.querySelector(".tree"); + ok(objectInspector, "Object is printed in the console"); + + is( + objectInspector.textContent.trim(), + "Object { __proto__: [] }", + "Object is displayed as expected" + ); + + objectInspector.querySelector(".arrow").click(); + await waitFor(() => node.querySelectorAll(".tree-node").length === 2); + + const __proto__Node = node.querySelector(".tree-node:last-of-type"); + ok( + __proto__Node.textContent.includes("__proto__: Array []"), + "__proto__ node is displayed as expected" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_entries.js b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_entries.js new file mode 100644 index 0000000000..db3ce98775 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_entries.js @@ -0,0 +1,354 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check expanding/collapsing maps and sets in the console. +const TEST_URI = + "data:text/html;charset=utf8,<h1>Object Inspector on Maps & Sets</h1>"; +const { ELLIPSIS } = require("devtools/shared/l10n"); + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + logAllStoreChanges(hud); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.console.log( + "oi-entries-test", + new Map( + Array.from({ length: 2 }).map((el, i) => [{ key: i }, content.document]) + ), + new Map(Array.from({ length: 20 }).map((el, i) => [Symbol(i), i])), + new Map(Array.from({ length: 331 }).map((el, i) => [Symbol(i), i])), + new Set(Array.from({ length: 2 }).map((el, i) => ({ value: i }))), + new Set(Array.from({ length: 20 }).map((el, i) => i)), + new Set(Array.from({ length: 222 }).map((el, i) => i)) + ); + }); + + const node = await waitFor(() => findMessage(hud, "oi-entries-test")); + const objectInspectors = [...node.querySelectorAll(".tree")]; + is( + objectInspectors.length, + 6, + "There is the expected number of object inspectors" + ); + + const [ + smallMapOi, + mapOi, + largeMapOi, + smallSetOi, + setOi, + largeSetOi, + ] = objectInspectors; + + await testSmallMap(smallMapOi); + await testMap(mapOi); + await testLargeMap(largeMapOi); + await testSmallSet(smallSetOi); + await testSet(setOi); + await testLargeSet(largeSetOi); +}); + +async function testSmallMap(oi) { + info("Expanding the Map"); + let onMapOiMutation = waitForNodeMutation(oi, { + childList: true, + }); + + oi.querySelector(".arrow").click(); + await onMapOiMutation; + + ok( + oi.querySelector(".arrow").classList.contains("expanded"), + "The arrow of the node has the expected class after clicking on it" + ); + + let oiNodes = oi.querySelectorAll(".node"); + // There are 4 nodes: the root, size, entries and the proto. + is(oiNodes.length, 4, "There is the expected number of nodes in the tree"); + + info("Expanding the <entries> leaf of the map"); + const entriesNode = oiNodes[2]; + is( + entriesNode.textContent, + "<entries>", + "There is the expected <entries> node" + ); + onMapOiMutation = waitForNodeMutation(oi, { + childList: true, + }); + + entriesNode.querySelector(".arrow").click(); + await onMapOiMutation; + + oiNodes = oi.querySelectorAll(".node"); + // There are now 6 nodes, the 4 original ones, and the 2 entries. + is(oiNodes.length, 6, "There is the expected number of nodes in the tree"); + + info("Expand first entry"); + onMapOiMutation = waitForNodeMutation(oi, { + childList: true, + }); + oiNodes[3].querySelector(".arrow").click(); + await onMapOiMutation; + + oiNodes = oi.querySelectorAll(".node"); + /* + * ▼ Map (2) + * | size: 2 + * | ▼ <entries> + * | | ▼ 0: {…} -> HTMLDocument + * | | | ▶︎ <key>: Object {…} + * | | | ▶︎ <value>: HTMLDocument + * | | ▶︎ 1: {…} -> HTMLDocument + * | ▶︎ <prototype> + */ + is(oiNodes.length, 8, "There is the expected number of nodes in the tree"); + + info("Expand <key> for first entry"); + onMapOiMutation = waitForNodeMutation(oi, { + childList: true, + }); + oiNodes[4].querySelector(".arrow").click(); + await onMapOiMutation; + + oiNodes = oi.querySelectorAll(".node"); + /* + * ▼ Map (2) + * | size: 2 + * | ▼ <entries> + * | | ▼ 0: {…} -> HTMLDocument + * | | | ▼ <key>: Object {…} + * | | | | key: 0 + * | | | | ▶︎ <prototype> + * | | | ▶︎ <value>: HTMLDocument + * | | ▶︎ 1: {…} -> HTMLDocument + * | ▶︎ <prototype> + */ + is(oiNodes.length, 10, "There is the expected number of nodes in the tree"); + + info("Expand <value> for first entry"); + onMapOiMutation = waitForNodeMutation(oi, { + childList: true, + }); + oiNodes[7].querySelector(".arrow").click(); + await onMapOiMutation; + + oiNodes = oi.querySelectorAll(".node"); + ok(oiNodes.length > 10, "The document node was expanded"); +} + +async function testMap(oi) { + info("Expanding the Map"); + let onMapOiMutation = waitForNodeMutation(oi, { + childList: true, + }); + + oi.querySelector(".arrow").click(); + await onMapOiMutation; + + ok( + oi.querySelector(".arrow").classList.contains("expanded"), + "The arrow of the node has the expected class after clicking on it" + ); + + let oiNodes = oi.querySelectorAll(".node"); + // There are 4 nodes: the root, size, entries and the proto. + is(oiNodes.length, 4, "There is the expected number of nodes in the tree"); + + info("Expanding the <entries> leaf of the map"); + const entriesNode = oiNodes[2]; + is( + entriesNode.textContent, + "<entries>", + "There is the expected <entries> node" + ); + onMapOiMutation = waitForNodeMutation(oi, { + childList: true, + }); + + entriesNode.querySelector(".arrow").click(); + await onMapOiMutation; + + oiNodes = oi.querySelectorAll(".node"); + // There are now 24 nodes, the 4 original ones, and the 20 entries. + is(oiNodes.length, 24, "There is the expected number of nodes in the tree"); +} + +async function testLargeMap(oi) { + info("Expanding the large map"); + let onMapOiMutation = waitForNodeMutation(oi, { + childList: true, + }); + + oi.querySelector(".arrow").click(); + await onMapOiMutation; + + ok( + oi.querySelector(".arrow").classList.contains("expanded"), + "The arrow of the node has the expected class after clicking on it" + ); + + let oiNodes = oi.querySelectorAll(".node"); + // There are 4 nodes: the root, size, entries and the proto. + is(oiNodes.length, 4, "There is the expected number of nodes in the tree"); + + info("Expanding the <entries> leaf of the map"); + const entriesNode = oiNodes[2]; + is( + entriesNode.textContent, + "<entries>", + "There is the expected <entries> node" + ); + onMapOiMutation = waitForNodeMutation(oi, { + childList: true, + }); + + entriesNode.querySelector(".arrow").click(); + await onMapOiMutation; + + oiNodes = oi.querySelectorAll(".node"); + // There are now 8 nodes, the 4 original ones, and the 4 buckets. + is(oiNodes.length, 8, "There is the expected number of nodes in the tree"); + is(oiNodes[3].textContent, `[0${ELLIPSIS}99]`); + is(oiNodes[4].textContent, `[100${ELLIPSIS}199]`); + is(oiNodes[5].textContent, `[200${ELLIPSIS}299]`); + is(oiNodes[6].textContent, `[300${ELLIPSIS}330]`); +} + +async function testSmallSet(oi) { + info("Expanding the Set"); + let onMapOiMutation = waitForNodeMutation(oi, { + childList: true, + }); + + oi.querySelector(".arrow").click(); + await onMapOiMutation; + + ok( + oi.querySelector(".arrow").classList.contains("expanded"), + "The arrow of the node has the expected class after clicking on it" + ); + + let oiNodes = oi.querySelectorAll(".node"); + // There are 4 nodes: the root, size, entries and the proto. + is(oiNodes.length, 4, "There is the expected number of nodes in the tree"); + + info("Expanding the <entries> leaf of the map"); + const entriesNode = oiNodes[2]; + is( + entriesNode.textContent, + "<entries>", + "There is the expected <entries> node" + ); + onMapOiMutation = waitForNodeMutation(oi, { + childList: true, + }); + + entriesNode.querySelector(".arrow").click(); + await onMapOiMutation; + + oiNodes = oi.querySelectorAll(".node"); + // There are now 6 nodes, the 4 original ones, and the 2 entries. + is(oiNodes.length, 6, "There is the expected number of nodes in the tree"); + + info("Expand first entry"); + onMapOiMutation = waitForNodeMutation(oi, { + childList: true, + }); + oiNodes[3].querySelector(".arrow").click(); + await onMapOiMutation; + + oiNodes = oi.querySelectorAll(".node"); + /* + * ▼ Set (2) + * | size: 2 + * | ▼ <entries> + * | | ▼ 0: {…} + * | | | | value: 0 + * | | | ▶︎ <prototype> + * | | ▶︎ 1: {…} + * | ▶︎ <prototype> + */ + is(oiNodes.length, 8, "There is the expected number of nodes in the tree"); +} + +async function testSet(oi) { + info("Expanding the Set"); + let onSetOiMutation = waitForNodeMutation(oi, { + childList: true, + }); + + oi.querySelector(".arrow").click(); + await onSetOiMutation; + + ok( + oi.querySelector(".arrow").classList.contains("expanded"), + "The arrow of the node has the expected class after clicking on it" + ); + + let oiNodes = oi.querySelectorAll(".node"); + // There are 4 nodes: the root, size, entries and the proto. + is(oiNodes.length, 4, "There is the expected number of nodes in the tree"); + + info("Expanding the <entries> leaf of the Set"); + const entriesNode = oiNodes[2]; + is( + entriesNode.textContent, + "<entries>", + "There is the expected <entries> node" + ); + onSetOiMutation = waitForNodeMutation(oi, { + childList: true, + }); + + entriesNode.querySelector(".arrow").click(); + await onSetOiMutation; + + oiNodes = oi.querySelectorAll(".node"); + // There are now 24 nodes, the 4 original ones, and the 20 entries. + is(oiNodes.length, 24, "There is the expected number of nodes in the tree"); +} + +async function testLargeSet(oi) { + info("Expanding the large Set"); + let onSetOiMutation = waitForNodeMutation(oi, { + childList: true, + }); + + oi.querySelector(".arrow").click(); + await onSetOiMutation; + + ok( + oi.querySelector(".arrow").classList.contains("expanded"), + "The arrow of the node has the expected class after clicking on it" + ); + + let oiNodes = oi.querySelectorAll(".node"); + // There are 4 nodes: the root, size, entries and the proto. + is(oiNodes.length, 4, "There is the expected number of nodes in the tree"); + + info("Expanding the <entries> leaf of the Set"); + const entriesNode = oiNodes[2]; + is( + entriesNode.textContent, + "<entries>", + "There is the expected <entries> node" + ); + onSetOiMutation = waitForNodeMutation(oi, { + childList: true, + }); + + entriesNode.querySelector(".arrow").click(); + await onSetOiMutation; + + oiNodes = oi.querySelectorAll(".node"); + // There are now 7 nodes, the 4 original ones, and the 3 buckets. + is(oiNodes.length, 7, "There is the expected number of nodes in the tree"); + is(oiNodes[3].textContent, `[0${ELLIPSIS}99]`); + is(oiNodes[4].textContent, `[100${ELLIPSIS}199]`); + is(oiNodes[5].textContent, `[200${ELLIPSIS}221]`); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_getters.js b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_getters.js new file mode 100644 index 0000000000..c167b7d4ad --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_getters.js @@ -0,0 +1,662 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check evaluating and expanding getters in the console. +const TEST_URI = + "data:text/html;charset=utf8,<h1>Object Inspector on Getters</h1>"; +const { ELLIPSIS } = require("devtools/shared/l10n"); + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + const LONGSTRING = "ab ".repeat(1e5); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [LONGSTRING], function( + longString + ) { + content.wrappedJSObject.console.log( + "oi-test", + Object.create( + null, + Object.getOwnPropertyDescriptors({ + get myStringGetter() { + return "hello"; + }, + get myNumberGetter() { + return 123; + }, + get myUndefinedGetter() { + return undefined; + }, + get myNullGetter() { + return null; + }, + get myZeroGetter() { + return 0; + }, + get myEmptyStringGetter() { + return ""; + }, + get myFalseGetter() { + return false; + }, + get myTrueGetter() { + return true; + }, + get myObjectGetter() { + return { foo: "bar" }; + }, + get myArrayGetter() { + return Array.from({ length: 1000 }, (_, i) => i); + }, + get myMapGetter() { + return new Map([["foo", { bar: "baz" }]]); + }, + get myProxyGetter() { + const handler = { + get: function(target, name) { + return name in target ? target[name] : 37; + }, + }; + return new Proxy({ a: 1 }, handler); + }, + get myThrowingGetter() { + throw new Error("myError"); + }, + get myLongStringGetter() { + return longString; + }, + get ["hyphen-getter"]() { + return "---"; + }, + get [`"quoted-getter"`]() { + return "quoted"; + }, + get [`"'\``]() { + return "quoted2"; + }, + }) + ) + ); + }); + + const node = await waitFor(() => findMessage(hud, "oi-test")); + const oi = node.querySelector(".tree"); + + expandObjectInspectorNode(oi); + await waitFor(() => getObjectInspectorNodes(oi).length > 1); + + await testStringGetter(oi); + await testNumberGetter(oi); + await testUndefinedGetter(oi); + await testNullGetter(oi); + await testZeroGetter(oi); + await testEmptyStringGetter(oi); + await testFalseGetter(oi); + await testTrueGetter(oi); + await testObjectGetter(oi); + await testArrayGetter(oi); + await testMapGetter(oi); + await testProxyGetter(oi); + await testThrowingGetter(oi); + await testLongStringGetter(oi, LONGSTRING); + await testHypgenGetter(oi); + await testQuotedGetters(oi); +}); + +async function testStringGetter(oi) { + let node = findObjectInspectorNode(oi, "myStringGetter"); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); + const invokeButton = getObjectInspectorInvokeGetterButton(node); + ok(invokeButton, "There is an invoke button as expected"); + + invokeButton.click(); + await waitFor( + () => + !getObjectInspectorInvokeGetterButton( + findObjectInspectorNode(oi, "myStringGetter") + ) + ); + + node = findObjectInspectorNode(oi, "myStringGetter"); + ok( + node.textContent.includes(`myStringGetter: "hello"`), + "String getter now has the expected text content" + ); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); +} + +async function testNumberGetter(oi) { + let node = findObjectInspectorNode(oi, "myNumberGetter"); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); + const invokeButton = getObjectInspectorInvokeGetterButton(node); + ok(invokeButton, "There is an invoke button as expected"); + + invokeButton.click(); + await waitFor( + () => + !getObjectInspectorInvokeGetterButton( + findObjectInspectorNode(oi, "myNumberGetter") + ) + ); + + node = findObjectInspectorNode(oi, "myNumberGetter"); + ok( + node.textContent.includes(`myNumberGetter: 123`), + "Number getter now has the expected text content" + ); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); +} + +async function testUndefinedGetter(oi) { + let node = findObjectInspectorNode(oi, "myUndefinedGetter"); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); + const invokeButton = getObjectInspectorInvokeGetterButton(node); + ok(invokeButton, "There is an invoke button as expected"); + + invokeButton.click(); + await waitFor( + () => + !getObjectInspectorInvokeGetterButton( + findObjectInspectorNode(oi, "myUndefinedGetter") + ) + ); + + node = findObjectInspectorNode(oi, "myUndefinedGetter"); + ok( + node.textContent.includes(`myUndefinedGetter: undefined`), + "undefined getter now has the expected text content" + ); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); +} + +async function testNullGetter(oi) { + let node = findObjectInspectorNode(oi, "myNullGetter"); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); + const invokeButton = getObjectInspectorInvokeGetterButton(node); + ok(invokeButton, "There is an invoke button as expected"); + + invokeButton.click(); + await waitFor( + () => + !getObjectInspectorInvokeGetterButton( + findObjectInspectorNode(oi, "myNullGetter") + ) + ); + + node = findObjectInspectorNode(oi, "myNullGetter"); + ok( + node.textContent.includes(`myNullGetter: null`), + "null getter now has the expected text content" + ); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); +} + +async function testZeroGetter(oi) { + let node = findObjectInspectorNode(oi, "myZeroGetter"); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); + const invokeButton = getObjectInspectorInvokeGetterButton(node); + ok(invokeButton, "There is an invoke button as expected"); + + invokeButton.click(); + await waitFor( + () => + !getObjectInspectorInvokeGetterButton( + findObjectInspectorNode(oi, "myZeroGetter") + ) + ); + + node = findObjectInspectorNode(oi, "myZeroGetter"); + ok( + node.textContent.includes(`myZeroGetter: 0`), + "0 getter now has the expected text content" + ); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); +} + +async function testEmptyStringGetter(oi) { + let node = findObjectInspectorNode(oi, "myEmptyStringGetter"); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); + const invokeButton = getObjectInspectorInvokeGetterButton(node); + ok(invokeButton, "There is an invoke button as expected"); + + invokeButton.click(); + await waitFor( + () => + !getObjectInspectorInvokeGetterButton( + findObjectInspectorNode(oi, "myEmptyStringGetter") + ) + ); + + node = findObjectInspectorNode(oi, "myEmptyStringGetter"); + ok( + node.textContent.includes(`myEmptyStringGetter: ""`), + "empty string getter now has the expected text content" + ); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); +} + +async function testFalseGetter(oi) { + let node = findObjectInspectorNode(oi, "myFalseGetter"); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); + const invokeButton = getObjectInspectorInvokeGetterButton(node); + ok(invokeButton, "There is an invoke button as expected"); + + invokeButton.click(); + await waitFor( + () => + !getObjectInspectorInvokeGetterButton( + findObjectInspectorNode(oi, "myFalseGetter") + ) + ); + + node = findObjectInspectorNode(oi, "myFalseGetter"); + ok( + node.textContent.includes(`myFalseGetter: false`), + "false getter now has the expected text content" + ); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); +} + +async function testTrueGetter(oi) { + let node = findObjectInspectorNode(oi, "myTrueGetter"); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); + const invokeButton = getObjectInspectorInvokeGetterButton(node); + ok(invokeButton, "There is an invoke button as expected"); + + invokeButton.click(); + await waitFor( + () => + !getObjectInspectorInvokeGetterButton( + findObjectInspectorNode(oi, "myTrueGetter") + ) + ); + + node = findObjectInspectorNode(oi, "myTrueGetter"); + ok( + node.textContent.includes(`myTrueGetter: true`), + "false getter now has the expected text content" + ); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); +} + +async function testObjectGetter(oi) { + let node = findObjectInspectorNode(oi, "myObjectGetter"); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); + const invokeButton = getObjectInspectorInvokeGetterButton(node); + ok(invokeButton, "There is an invoke button as expected"); + + invokeButton.click(); + await waitFor( + () => + !getObjectInspectorInvokeGetterButton( + findObjectInspectorNode(oi, "myObjectGetter") + ) + ); + + node = findObjectInspectorNode(oi, "myObjectGetter"); + ok( + node.textContent.includes(`myObjectGetter: Object { foo: "bar" }`), + "object getter now has the expected text content" + ); + is(isObjectInspectorNodeExpandable(node), true, "The node can be expanded"); + + expandObjectInspectorNode(node); + await waitFor(() => getObjectInspectorChildrenNodes(node).length > 0); + checkChildren(node, [`foo: "bar"`, `<prototype>`]); +} + +async function testArrayGetter(oi) { + let node = findObjectInspectorNode(oi, "myArrayGetter"); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); + const invokeButton = getObjectInspectorInvokeGetterButton(node); + ok(invokeButton, "There is an invoke button as expected"); + + invokeButton.click(); + await waitFor( + () => + !getObjectInspectorInvokeGetterButton( + findObjectInspectorNode(oi, "myArrayGetter") + ) + ); + + node = findObjectInspectorNode(oi, "myArrayGetter"); + ok( + node.textContent.includes( + `myArrayGetter: Array(1000) [ 0, 1, 2, ${ELLIPSIS} ]` + ), + "Array getter now has the expected text content - " + ); + is(isObjectInspectorNodeExpandable(node), true, "The node can be expanded"); + + expandObjectInspectorNode(node); + await waitFor(() => getObjectInspectorChildrenNodes(node).length > 0); + const children = getObjectInspectorChildrenNodes(node); + + const firstBucket = children[0]; + ok(firstBucket.textContent.includes(`[0${ELLIPSIS}99]`), "Array has buckets"); + + is( + isObjectInspectorNodeExpandable(firstBucket), + true, + "The bucket can be expanded" + ); + expandObjectInspectorNode(firstBucket); + await waitFor(() => getObjectInspectorChildrenNodes(firstBucket).length > 0); + checkChildren( + firstBucket, + Array.from({ length: 100 }, (_, i) => `${i}: ${i}`) + ); +} + +async function testMapGetter(oi) { + let node = findObjectInspectorNode(oi, "myMapGetter"); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); + const invokeButton = getObjectInspectorInvokeGetterButton(node); + ok(invokeButton, "There is an invoke button as expected"); + + invokeButton.click(); + await waitFor( + () => + !getObjectInspectorInvokeGetterButton( + findObjectInspectorNode(oi, "myMapGetter") + ) + ); + + node = findObjectInspectorNode(oi, "myMapGetter"); + ok( + node.textContent.includes(`myMapGetter: Map`), + "map getter now has the expected text content" + ); + is(isObjectInspectorNodeExpandable(node), true, "The node can be expanded"); + + expandObjectInspectorNode(node); + await waitFor(() => getObjectInspectorChildrenNodes(node).length > 0); + checkChildren(node, [`size`, `<entries>`, `<prototype>`]); + + const entriesNode = findObjectInspectorNode(oi, "<entries>"); + expandObjectInspectorNode(entriesNode); + await waitFor(() => getObjectInspectorChildrenNodes(entriesNode).length > 0); + checkChildren(entriesNode, [`foo → Object { bar: "baz" }`]); + + const entryNode = getObjectInspectorChildrenNodes(entriesNode)[0]; + expandObjectInspectorNode(entryNode); + await waitFor(() => getObjectInspectorChildrenNodes(entryNode).length > 0); + checkChildren(entryNode, [`<key>: "foo"`, `<value>: Object { bar: "baz" }`]); +} + +async function testProxyGetter(oi) { + let node = findObjectInspectorNode(oi, "myProxyGetter"); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); + const invokeButton = getObjectInspectorInvokeGetterButton(node); + ok(invokeButton, "There is an invoke button as expected"); + + invokeButton.click(); + await waitFor( + () => + !getObjectInspectorInvokeGetterButton( + findObjectInspectorNode(oi, "myProxyGetter") + ) + ); + + node = findObjectInspectorNode(oi, "myProxyGetter"); + ok( + node.textContent.includes(`myProxyGetter: Proxy`), + "proxy getter now has the expected text content" + ); + is(isObjectInspectorNodeExpandable(node), true, "The node can be expanded"); + + expandObjectInspectorNode(node); + await waitFor(() => getObjectInspectorChildrenNodes(node).length > 0); + checkChildren(node, [`<target>`, `<handler>`]); + + const targetNode = findObjectInspectorNode(oi, "<target>"); + expandObjectInspectorNode(targetNode); + await waitFor(() => getObjectInspectorChildrenNodes(targetNode).length > 0); + checkChildren(targetNode, [`a: 1`, `<prototype>`]); + + const handlerNode = findObjectInspectorNode(oi, "<handler>"); + expandObjectInspectorNode(handlerNode); + await waitFor(() => getObjectInspectorChildrenNodes(handlerNode).length > 0); + checkChildren(handlerNode, [`get:`, `<prototype>`]); +} + +async function testThrowingGetter(oi) { + let node = findObjectInspectorNode(oi, "myThrowingGetter"); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); + const invokeButton = getObjectInspectorInvokeGetterButton(node); + ok(invokeButton, "There is an invoke button as expected"); + + invokeButton.click(); + await waitFor( + () => + !getObjectInspectorInvokeGetterButton( + findObjectInspectorNode(oi, "myThrowingGetter") + ) + ); + + node = findObjectInspectorNode(oi, "myThrowingGetter"); + ok( + node.textContent.includes(`myThrowingGetter: Error`), + "throwing getter does show the error" + ); + is(isObjectInspectorNodeExpandable(node), true, "The node can be expanded"); + + expandObjectInspectorNode(node); + await waitFor(() => getObjectInspectorChildrenNodes(node).length > 0); + checkChildren(node, [ + `columnNumber`, + `fileName`, + `lineNumber`, + `message`, + `stack`, + `<prototype>`, + ]); +} + +async function testLongStringGetter(oi, longString) { + const getLongStringNode = () => + findObjectInspectorNode(oi, "myLongStringGetter"); + const node = getLongStringNode(); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); + const invokeButton = getObjectInspectorInvokeGetterButton(node); + ok(invokeButton, "There is an invoke button as expected"); + + invokeButton.click(); + await waitFor(() => + getLongStringNode().textContent.includes(`myLongStringGetter: "ab ab`) + ); + ok(true, "longstring getter shows the initial text"); + is( + isObjectInspectorNodeExpandable(getLongStringNode()), + true, + "The node can be expanded" + ); + + expandObjectInspectorNode(getLongStringNode()); + await waitFor(() => + getLongStringNode().textContent.includes( + `myLongStringGetter: "${longString}"` + ) + ); + ok(true, "the longstring was expanded"); +} + +async function testHypgenGetter(oi) { + const findHyphenGetterNode = () => + findObjectInspectorNode(oi, `"hyphen-getter"`); + let node = findHyphenGetterNode(); + + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); + const invokeButton = getObjectInspectorInvokeGetterButton(node); + ok(invokeButton, "There is an invoke button as expected"); + + invokeButton.click(); + await waitFor( + () => !getObjectInspectorInvokeGetterButton(findHyphenGetterNode()) + ); + + node = findHyphenGetterNode(); + ok( + node.textContent.includes(`"hyphen-getter": "---"`), + "Node now has the expected text content" + ); + is( + isObjectInspectorNodeExpandable(node), + false, + "The node can't be expanded" + ); +} + +async function testQuotedGetters(oi) { + const nodes = [ + { + name: `"\\"quoted-getter\\""`, + expected: `"quoted"`, + expandable: false, + }, + { + name: `"\\"'\`"`, + expected: `"quoted2"`, + expandable: false, + }, + ]; + + for (const { name, expected, expandable } of nodes) { + await testGetter(oi, name, expected, expandable); + } +} + +async function testGetter(oi, propertyName, expectedResult, resultExpandable) { + info(`Check «${propertyName}» getter`); + const findNode = () => findObjectInspectorNode(oi, propertyName); + + let node = findNode(); + is( + isObjectInspectorNodeExpandable(node), + false, + `«${propertyName}» can't be expanded` + ); + getObjectInspectorInvokeGetterButton(node).click(); + await waitFor(() => !getObjectInspectorInvokeGetterButton(findNode())); + + node = findNode(); + ok( + node.textContent.includes(`${propertyName}: ${expectedResult}`), + `«${propertyName}» now has the expected text content («${expectedResult}»)` + ); + is( + isObjectInspectorNodeExpandable(node), + resultExpandable, + `«${propertyName}» ${resultExpandable ? "now can" : "can't"} be expanded` + ); +} + +function checkChildren(node, expectedChildren) { + const children = getObjectInspectorChildrenNodes(node); + is( + children.length, + expectedChildren.length, + "There is the expected number of children" + ); + children.forEach((child, index) => { + ok( + child.textContent.includes(expectedChildren[index]), + `Expected "${child.textContent}" to include "${expectedChildren[index]}"` + ); + }); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_getters_prototype.js b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_getters_prototype.js new file mode 100644 index 0000000000..058a6bbead --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_getters_prototype.js @@ -0,0 +1,121 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check evaluating getters on prototype nodes in the console. +const TEST_URI = + "data:text/html;charset=utf8,<h1>Object Inspector on Getters</h1>"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + class A { + constructor() { + this.myValue = "foo"; + } + get value() { + return `A-value:${this.myValue}`; + } + } + + class B extends A { + get value() { + return `B-value:${this.myValue}`; + } + } + + class C extends A { + constructor() { + super(); + this.myValue = "bar"; + } + } + + const a = new A(); + const b = new B(); + const c = new C(); + + const d = new C(); + d.myValue = "d"; + + content.wrappedJSObject.console.log("oi-test", a, b, c, d); + }); + + const node = await waitFor(() => findMessage(hud, "oi-test")); + const [a, b, c, d] = node.querySelectorAll(".tree"); + + await testObject(a, { + myValue: `myValue: "foo"`, + value: `value: "A-value:foo"`, + }); + + await testObject(b, { + myValue: `myValue: "foo"`, + value: `value: "B-value:foo"`, + }); + + await testObject(c, { + myValue: `myValue: "bar"`, + value: `value: "A-value:bar"`, + }); + + await testObject(d, { + myValue: `myValue: "d"`, + value: `value: "A-value:d"`, + }); +}); + +async function testObject(oi, { myValue, value }) { + expandObjectInspectorNode(oi.querySelector(".tree-node")); + const prototypeNode = await waitFor(() => + findObjectInspectorNode(oi, "<prototype>") + ); + let valueGetterNode = await getValueNode(prototypeNode); + + ok( + findObjectInspectorNode(oi, "myValue").textContent.includes(myValue), + `myValue has expected "${myValue}" content` + ); + + getObjectInspectorInvokeGetterButton(valueGetterNode).click(); + + await waitFor( + () => + !getObjectInspectorInvokeGetterButton( + findObjectInspectorNode(oi, "value") + ) + ); + valueGetterNode = findObjectInspectorNode(oi, "value"); + ok( + valueGetterNode.textContent.includes(value), + `Getter now has the expected "${value}" content` + ); +} + +async function getValueNode(prototypeNode) { + expandObjectInspectorNode(prototypeNode); + + await waitFor( + () => getObjectInspectorChildrenNodes(prototypeNode).length > 0 + ); + + const children = getObjectInspectorChildrenNodes(prototypeNode); + const valueNode = children.find( + child => child.querySelector(".object-label").textContent === "value" + ); + + if (valueNode) { + return valueNode; + } + + const childPrototypeNode = children.find( + child => child.querySelector(".object-label").textContent === "<prototype>" + ); + if (!childPrototypeNode) { + return null; + } + + return getValueNode(childPrototypeNode); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_getters_shadowed.js b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_getters_shadowed.js new file mode 100644 index 0000000000..4ef6a54637 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_getters_shadowed.js @@ -0,0 +1,80 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check evaluating shadowed getters in the console. +const TEST_URI = + "data:text/html;charset=utf8,<h1>Object Inspector on Getters</h1>"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + const a = { + getter: "[A]", + __proto__: { + get getter() { + return "[B]"; + }, + __proto__: { + get getter() { + return "[C]"; + }, + }, + }, + }; + const b = { + value: 1, + get getter() { + return `[A-${this.value}]`; + }, + __proto__: { + value: 2, + get getter() { + return `[B-${this.value}]`; + }, + }, + }; + content.wrappedJSObject.console.log("oi-test", a, b); + }); + + const node = await waitFor(() => findMessage(hud, "oi-test")); + const [a, b] = node.querySelectorAll(".tree"); + + await testObject(a, [null, "[B]", "[C]"]); + await testObject(b, ["[A-1]", "[B-1]"]); +}); + +async function testObject(oi, values) { + let node = oi.querySelector(".tree-node"); + for (const value of values) { + await expand(node); + if (value != null) { + const getter = findObjectInspectorNodeChild(node, "getter"); + await invokeGetter(getter); + ok( + getter.textContent.includes(`getter: "${value}"`), + `Getter now has the expected "${value}" content` + ); + } + node = findObjectInspectorNodeChild(node, "<prototype>"); + } +} + +function expand(node) { + expandObjectInspectorNode(node); + return waitFor(() => getObjectInspectorChildrenNodes(node).length > 0); +} + +function invokeGetter(node) { + getObjectInspectorInvokeGetterButton(node).click(); + return waitFor(() => !getObjectInspectorInvokeGetterButton(node)); +} + +function findObjectInspectorNodeChild(node, nodeLabel) { + return getObjectInspectorChildrenNodes(node).find(child => { + const label = child.querySelector(".object-label"); + return label && label.textContent === nodeLabel; + }); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_key_sorting.js b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_key_sorting.js new file mode 100644 index 0000000000..6d94ae0d37 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_key_sorting.js @@ -0,0 +1,136 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* Test case that ensures Array and other list types are not alphabetically sorted in the + * Object Inspector. + * + * The tested types are: + * - Array + * - NodeList + * - Object + * - Int8Array + * - Int16Array + * - Int32Array + * - Uint8Array + * - Uint16Array + * - Uint32Array + * - Uint8ClampedArray + * - Float32Array + * - Float64Array + */ + +const TEST_URI = `data:text/html;charset=utf-8, + <html> + <head> + <title>Test document for bug 977500</title> + </head> + <body> + <div></div> <div></div> <div></div> + <div></div> <div></div> <div></div> + <div></div> <div></div> <div></div> + <div></div> <div></div> <div></div> + </body> + </html>`; + +const typedArrayTypes = [ + "Int8Array", + "Int16Array", + "Int32Array", + "Uint8Array", + "Uint16Array", + "Uint32Array", + "Uint8ClampedArray", + "Float32Array", + "Float64Array", +]; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + // Array + await testKeyOrder(hud, "Array(0,1,2,3,4,5,6,7,8,9,10)", [ + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + ]); + // NodeList + await testKeyOrder(hud, "document.querySelectorAll('div')", [ + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + ]); + // Object + await testKeyOrder(hud, "Object({'hello':1, 1:5, 10:2, 4:2, 'abc':1})", [ + "1", + "4", + "10", + "abc", + "hello", + ]); + + // Typed arrays. + for (const type of typedArrayTypes) { + // size of 80 is enough to get 11 items on all ArrayTypes except for Float64Array. + const size = type === "Float64Array" ? 120 : 80; + await testKeyOrder(hud, `new ${type}(new ArrayBuffer(${size}))`, [ + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + ]); + } +}); + +async function testKeyOrder(hud, command, expectedKeys) { + info(`Testing command: ${command}`); + await clearOutput(hud); + + info( + "Wait for a new .result message with an object inspector to be displayed" + ); + const { node } = await executeAndWaitForMessage(hud, command, "", ".result"); + const oi = node.querySelector(".tree"); + + info("Expand object inspector"); + const onOiExpanded = waitFor(() => { + return oi.querySelectorAll(".node").length >= expectedKeys.length; + }); + oi.querySelector(".arrow").click(); + await onOiExpanded; + + const labelNodes = oi.querySelectorAll(".object-label"); + for (let i = 0; i < expectedKeys.length; i++) { + const key = expectedKeys[i]; + const labelNode = labelNodes[i]; + is( + labelNode.textContent, + key, + `Object inspector key is sorted as expected (${key})` + ); + } +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_local_session_storage.js b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_local_session_storage.js new file mode 100644 index 0000000000..58fc1f9ea8 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_local_session_storage.js @@ -0,0 +1,115 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check expanding/collapsing local and session storage in the console. +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-local-session-storage.html"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const messages = await logMessages(hud); + const objectInspectors = messages.map(node => node.querySelector(".tree")); + + is( + objectInspectors.length, + 2, + "There is the expected number of object inspectors" + ); + + await checkValues(objectInspectors[0], "localStorage"); + await checkValues(objectInspectors[1], "sessionStorage"); +}); + +async function logMessages(hud) { + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.console.log("localStorage", content.localStorage); + }); + const localStorageMsg = await waitFor(() => findMessage(hud, "localStorage")); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.console.log("sessionStorage", content.sessionStorage); + }); + const sessionStorageMsg = await waitFor(() => + findMessage(hud, "sessionStorage") + ); + + return [localStorageMsg, sessionStorageMsg]; +} + +async function checkValues(oi, storageType) { + info(`Expanding the ${storageType} object`); + let onMapOiMutation = waitForNodeMutation(oi, { + childList: true, + }); + + oi.querySelector(".arrow").click(); + await onMapOiMutation; + + ok( + oi.querySelector(".arrow").classList.contains("expanded"), + "The arrow of the node has the expected class after clicking on it" + ); + + let nodes = oi.querySelectorAll(".node"); + // There are 4 nodes: the root, size, entries and the proto. + is(nodes.length, 5, "There is the expected number of nodes in the tree"); + + info("Expanding the <entries> leaf of the map"); + const entriesNode = nodes[3]; + is( + entriesNode.textContent, + "<entries>", + "There is the expected <entries> node" + ); + onMapOiMutation = waitForNodeMutation(oi, { + childList: true, + }); + + entriesNode.querySelector(".arrow").click(); + await onMapOiMutation; + + nodes = oi.querySelectorAll(".node"); + // There are now 7 nodes, the 5 original ones, and the 2 entries. + is(nodes.length, 7, "There is the expected number of nodes in the tree"); + + const title = nodes[0].querySelector(".objectTitle").textContent; + const name1 = nodes[1].querySelector(".object-label").textContent; + const value1 = nodes[1].querySelector(".objectBox").textContent; + + const length = [...nodes[2].querySelectorAll(".object-label,.objectBox")].map( + node => node.textContent + ); + const key2 = [ + ...nodes[4].querySelectorAll(".object-label,.nodeName,.objectBox-string"), + ].map(node => node.textContent); + const key = [ + ...nodes[5].querySelectorAll(".object-label,.nodeName,.objectBox-string"), + ].map(node => node.textContent); + + is(title, "Storage", `${storageType} object has the expected title`); + is(length[0], "length", `${storageType} length property name is correct`); + is(length[1], "2", `${storageType} length property value is correct`); + is(key2[0], "0", `1st entry of ${storageType} entry has the correct index`); + is(key2[1], "key2", `1st entry of ${storageType} entry has the correct key`); + + const firstValue = storageType === "localStorage" ? `"value2"` : `"value4"`; + is(name1, "key2", "Name of short descriptor is correct"); + is(value1, firstValue, "Value of short descriptor is correct"); + is( + key2[2], + firstValue, + `1st entry of ${storageType} entry has the correct value` + ); + is(key[0], "1", `2nd entry of ${storageType} entry has the correct index`); + is(key[1], "key", `2nd entry of ${storageType} entry has the correct key`); + + const secondValue = storageType === "localStorage" ? `"value1"` : `"value3"`; + is( + key[2], + secondValue, + `2nd entry of ${storageType} entry has the correct value` + ); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_nested_promise.js b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_nested_promise.js new file mode 100644 index 0000000000..2337f6b733 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_nested_promise.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check evaluating and expanding promises in the console. +const TEST_URI = + "data:text/html;charset=utf8," + + "<h1>Object Inspector on deeply nested promises</h1>"; + +add_task(async function testExpandNestedPromise() { + const hud = await openNewTabAndConsole(TEST_URI); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + let nestedPromise = Promise.resolve({}); + for (let i = 0; i < 5; ++i) { + Object.setPrototypeOf(nestedPromise, null); + nestedPromise = Promise.resolve(nestedPromise); + } + content.wrappedJSObject.console.log("oi-test", nestedPromise); + }); + + const node = await waitFor(() => findMessage(hud, "oi-test")); + const oi = node.querySelector(".tree"); + const [promiseNode] = getObjectInspectorNodes(oi); + + expandObjectInspectorNode(promiseNode); + await waitFor(() => getObjectInspectorNodes(oi).length > 1); + checkChildren(promiseNode, [`<state>`, `<value>`]); + + const valueNode = findObjectInspectorNode(oi, "<value>"); + expandObjectInspectorNode(valueNode); + await waitFor(() => getObjectInspectorChildrenNodes(valueNode).length > 0); + checkChildren(valueNode, [`<state>`, `<value>`]); +}); + +add_task(async function testExpandCyclicPromise() { + const hud = await openNewTabAndConsole(TEST_URI); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + let resolve; + const cyclicPromise = new Promise(r => { + resolve = r; + }); + Object.setPrototypeOf(cyclicPromise, null); + const otherPromise = Promise.reject(cyclicPromise); + otherPromise.catch(() => {}); + Object.setPrototypeOf(otherPromise, null); + resolve(otherPromise); + content.wrappedJSObject.console.log("oi-test", cyclicPromise); + }); + + const node = await waitFor(() => findMessage(hud, "oi-test")); + const oi = node.querySelector(".tree"); + const [promiseNode] = getObjectInspectorNodes(oi); + + expandObjectInspectorNode(promiseNode); + await waitFor(() => getObjectInspectorNodes(oi).length > 1); + checkChildren(promiseNode, [`<state>`, `<value>`]); + + const valueNode = findObjectInspectorNode(oi, "<value>"); + expandObjectInspectorNode(valueNode); + await waitFor(() => getObjectInspectorChildrenNodes(valueNode).length > 0); + checkChildren(valueNode, [`<state>`, `<reason>`]); + + const reasonNode = findObjectInspectorNode(oi, "<reason>"); + expandObjectInspectorNode(reasonNode); + await waitFor(() => getObjectInspectorChildrenNodes(reasonNode).length > 0); + checkChildren(reasonNode, [`<state>`, `<value>`]); +}); + +function checkChildren(node, expectedChildren) { + const children = getObjectInspectorChildrenNodes(node); + is( + children.length, + expectedChildren.length, + "There is the expected number of children" + ); + children.forEach((child, index) => { + ok( + child.textContent.includes(expectedChildren[index]), + `Expected "${expectedChildren[index]}" child` + ); + }); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_nested_proxy.js b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_nested_proxy.js new file mode 100644 index 0000000000..66bdb97f89 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_nested_proxy.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check evaluating and expanding getters in the console. +const TEST_URI = + "data:text/html;charset=utf8," + + "<h1>Object Inspector on deeply nested proxies</h1>"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + let proxy = new Proxy({}, {}); + for (let i = 0; i < 1e5; ++i) { + proxy = new Proxy(proxy, proxy); + } + content.wrappedJSObject.console.log("oi-test", proxy); + }); + + const node = await waitFor(() => findMessage(hud, "oi-test")); + const oi = node.querySelector(".tree"); + const [proxyNode] = getObjectInspectorNodes(oi); + + expandObjectInspectorNode(proxyNode); + await waitFor(() => getObjectInspectorNodes(oi).length > 1); + checkChildren(proxyNode, [`<target>`, `<handler>`]); + + const targetNode = findObjectInspectorNode(oi, "<target>"); + expandObjectInspectorNode(targetNode); + await waitFor(() => getObjectInspectorChildrenNodes(targetNode).length > 0); + checkChildren(targetNode, [`<target>`, `<handler>`]); + + const handlerNode = findObjectInspectorNode(oi, "<handler>"); + expandObjectInspectorNode(handlerNode); + await waitFor(() => getObjectInspectorChildrenNodes(handlerNode).length > 0); + checkChildren(handlerNode, [`<target>`, `<handler>`]); +}); + +function checkChildren(node, expectedChildren) { + const children = getObjectInspectorChildrenNodes(node); + is( + children.length, + expectedChildren.length, + "There is the expected number of children" + ); + children.forEach((child, index) => { + ok( + child.textContent.includes(expectedChildren[index]), + `Expected "${expectedChildren[index]}" child` + ); + }); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_scroll.js b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_scroll.js new file mode 100644 index 0000000000..bf52faad82 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_scroll.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that expanding an objectInspector node doesn't alter the output scroll position. +const TEST_URI = "data:text/html;charset=utf8,test Object Inspector"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.console.log( + "oi-test", + content.wrappedJSObject.Math + ); + }); + + const node = await waitFor(() => findMessage(hud, "oi-test")); + const objectInspector = node.querySelector(".tree"); + + let onOiMutation = waitForNodeMutation(objectInspector, { + childList: true, + }); + + info("Expanding the object inspector"); + objectInspector.querySelector(".arrow").click(); + await onOiMutation; + + const nodes = objectInspector.querySelectorAll(".node"); + const lastNode = nodes[nodes.length - 1]; + + info("Scroll the last node of the ObjectInspector into view"); + lastNode.scrollIntoView(); + + const outputContainer = hud.ui.outputNode.querySelector(".webconsole-output"); + ok(hasVerticalOverflow(outputContainer), "There is a vertical overflow"); + const scrollTop = outputContainer.scrollTop; + + onOiMutation = waitForNodeMutation(objectInspector, { + childList: true, + }); + + info("Expand the last node"); + const view = lastNode.ownerDocument.defaultView; + EventUtils.synthesizeMouseAtCenter(lastNode, {}, view); + await onOiMutation; + + is( + scrollTop, + outputContainer.scrollTop, + "Scroll position did not changed when expanding a node" + ); +}); + +function hasVerticalOverflow(container) { + return container.scrollHeight > container.clientHeight; +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_selected_text.js b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_selected_text.js new file mode 100644 index 0000000000..ec29d01fe7 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_selected_text.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check expanding/collapsing object inspector in the console when text is selected. +const TEST_URI = "data:text/html;charset=utf8,<h1>test Object Inspector</h1>"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + const label = "oi-test"; + const onLoggedMessage = waitForMessage(hud, label); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [label], function(str) { + content.wrappedJSObject.console.log(str, [1, 2, 3]); + }); + const { node } = await onLoggedMessage; + + info(`Select the "Array" text`); + selectNode(hud, node.querySelector(".objectTitle")); + + info("Click on the arrow to expand the object"); + node.querySelector(".arrow").click(); + await waitFor(() => node.querySelectorAll(".tree-node").length > 1); + ok(true, "The array was expanded as expected"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_symbols.js b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_symbols.js new file mode 100644 index 0000000000..18a8658128 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_symbols.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check expanding/collapsing object with symbol properties in the console. +const TEST_URI = "data:text/html;charset=utf8,"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.console.log("oi-symbols-test", { + [Symbol()]: "first symbol", + [Symbol()]: "second symbol", + [Symbol()]: 0, + [Symbol()]: null, + [Symbol()]: false, + [Symbol()]: undefined, + [Symbol("named")]: "named symbol", + [Symbol("array")]: [1, 2, 3], + }); + }); + + const node = await waitFor(() => findMessage(hud, "oi-symbols-test")); + const objectInspectors = [...node.querySelectorAll(".tree")]; + is( + objectInspectors.length, + 1, + "There is the expected number of object inspectors" + ); + + const [oi] = objectInspectors; + + info("Expanding the Object"); + const onMapOiMutation = waitForNodeMutation(oi, { + childList: true, + }); + + oi.querySelector(".arrow").click(); + await onMapOiMutation; + + ok( + oi.querySelector(".arrow").classList.contains("expanded"), + "The arrow of the node has the expected class after clicking on it" + ); + + const oiNodes = oi.querySelectorAll(".node"); + // The object inspector should look like this: + /* + * ▼ { … } + * | Symbol(): "first symbol", + * | Symbol(): "second symbol", + * | Symbol(): 0, + * | Symbol(): null, + * | Symbol(): false, + * | Symbol(): undefined, + * | Symbol(named): "named symbol", + * | Symbol(array): Array(3) [ 1, 2, 3 ], + * | ▶︎ <prototype> + */ + is(oiNodes.length, 10, "There is the expected number of nodes in the tree"); + + is(oiNodes[1].textContent.trim(), `Symbol(): "first symbol"`); + is(oiNodes[2].textContent.trim(), `Symbol(): "second symbol"`); + is(oiNodes[3].textContent.trim(), `Symbol(): 0`); + is(oiNodes[4].textContent.trim(), `Symbol(): null`); + is(oiNodes[5].textContent.trim(), `Symbol(): false`); + is(oiNodes[6].textContent.trim(), `Symbol(): undefined`); + is(oiNodes[7].textContent.trim(), `Symbol(named): "named symbol"`); + is(oiNodes[8].textContent.trim(), `Symbol(array): Array(3) [ 1, 2, 3 ]`); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_while_debugging_and_inspecting.js b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_while_debugging_and_inspecting.js new file mode 100644 index 0000000000..cba19fa161 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_while_debugging_and_inspecting.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that makes sure web console eval works while the js debugger paused the +// page, and while the inspector is active. See bug 886137. + +"use strict"; + +const TEST_URI = + "https://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-eval-in-stackframe.html"; + +add_task(async function() { + // TODO: Remove this pref change when middleware for terminating requests + // when closing a panel is implemented + await pushPref("devtools.debugger.features.inline-preview", false); + + const hud = await openNewTabAndConsole(TEST_URI); + + info("Switch to the debugger"); + await openDebugger(); + + info("Switch to the inspector"); + const target = await TargetFactory.forTab(gBrowser.selectedTab); + await gDevTools.showToolbox(target, "inspector"); + + info("Call firstCall() and wait for the debugger statement to be reached."); + const toolbox = gDevTools.getToolbox(target); + const dbg = createDebuggerContext(toolbox); + await pauseDebugger(dbg); + + info("Switch back to the console"); + await gDevTools.showToolbox(target, "webconsole"); + + info("Test logging and inspecting objects while on a breakpoint."); + const message = await executeAndWaitForMessage( + hud, + "fooObj", + '{ testProp2: "testValue2" }', + ".result" + ); + + const objectInspectors = [...message.node.querySelectorAll(".tree")]; + is(objectInspectors.length, 1, "There should be one object inspector"); + + info("Expanding the array object inspector"); + const [oi] = objectInspectors; + const onOiExpanded = waitFor(() => { + return oi.querySelectorAll(".node").length === 3; + }); + oi.querySelector(".arrow").click(); + await onOiExpanded; + + ok( + oi.querySelector(".arrow").classList.contains("expanded"), + "Object inspector expanded" + ); + + // The object inspector now looks like: + // {...} + // | testProp2: "testValue2" + // | <prototype>: Object { ... } + + const oiNodes = oi.querySelectorAll(".node"); + is(oiNodes.length, 3, "There is the expected number of nodes in the tree"); + + ok(oiNodes[0].textContent.includes(`{\u2026}`)); + ok(oiNodes[1].textContent.includes(`testProp2: "testValue2"`)); + ok(oiNodes[2].textContent.includes(`<prototype>: Object { \u2026 }`)); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_observer_notifications.js b/devtools/client/webconsole/test/browser/browser_webconsole_observer_notifications.js new file mode 100644 index 0000000000..2eb8f7d8f5 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_observer_notifications.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf-8,<p>Web Console test for " + + "observer notifications"; + +let created = false; +let destroyed = false; + +add_task(async function() { + setupObserver(); + await openNewTabAndConsole(TEST_URI); + await waitFor(() => created); + + await closeTabAndToolbox(gBrowser.selectedTab); + await waitFor(() => destroyed); + + ok(true, "We received both created and destroyed events"); +}); + +function setupObserver() { + const observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + + observe: function observe(subject, topic) { + subject = subject.QueryInterface(Ci.nsISupportsString); + + switch (topic) { + case "web-console-created": + Services.obs.removeObserver(observer, "web-console-created"); + created = true; + break; + case "web-console-destroyed": + Services.obs.removeObserver(observer, "web-console-destroyed"); + destroyed = true; + break; + } + }, + }; + + Services.obs.addObserver(observer, "web-console-created"); + Services.obs.addObserver(observer, "web-console-destroyed"); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_optimized_out_vars.js b/devtools/client/webconsole/test/browser/browser_webconsole_optimized_out_vars.js new file mode 100644 index 0000000000..1edc360a65 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_optimized_out_vars.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check that inspecting an optimized out variable works when execution is +// paused. + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/" + + "test-closure-optimized-out.html"; + +add_task(async function() { + const breakpointLine = 18; + const hud = await openNewTabAndConsole(TEST_URI); + await openDebugger(); + + const toolbox = hud.toolbox; + const dbg = createDebuggerContext(toolbox); + + await selectSource(dbg, "test-closure-optimized-out.html"); + await addBreakpoint(dbg, "test-closure-optimized-out.html", breakpointLine); + + // Cause the debuggee to pause + await pauseDebugger(dbg); + + await toolbox.selectTool("webconsole"); + + // This is the meat of the test: evaluate the optimized out variable. + info("Waiting for optimized out message"); + await executeAndWaitForMessage(hud, "upvar", "optimized out", ".result"); + ok(true, "Optimized out message logged"); + + info("Open the debugger"); + await openDebugger(); + + info("Resume"); + await resume(dbg); + + info("Remove the breakpoint"); + const source = findSource(dbg, "test-closure-optimized-out.html"); + await removeBreakpoint(dbg, source.id, breakpointLine); +}); + +async function pauseDebugger(dbg) { + info("Waiting for debugger to pause"); + const onPaused = waitForPaused(dbg); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() { + const button = content.document.querySelector("button"); + button.click(); + }); + await onPaused; +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_output_copy.js b/devtools/client/webconsole/test/browser/browser_webconsole_output_copy.js new file mode 100644 index 0000000000..958c7fdc5e --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_output_copy.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test copy to clipboard on the console output. See Bug 587617. +const TEST_URI = "data:text/html,Test copy to clipboard on the console output"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + const smokeMessage = "Hello world!"; + const onMessage = waitForMessage(hud, smokeMessage); + SpecialPowers.spawn(gBrowser.selectedBrowser, [smokeMessage], function(msg) { + content.wrappedJSObject.console.log(msg); + }); + const { node } = await onMessage; + ok(true, "Message was logged"); + + const selection = selectNode(hud, node); + + const selectionString = selection.toString().trim(); + is( + selectionString, + smokeMessage, + `selection has expected "${smokeMessage}" value` + ); + + await waitForClipboardPromise( + () => { + // The focus is on the JsTerm, so we need to blur it for the copy comand to work. + node.ownerDocument.activeElement.blur(); + goDoCommand("cmd_copy"); + }, + data => { + return data.trim() === smokeMessage; + } + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_output_copy_newlines.js b/devtools/client/webconsole/test/browser/browser_webconsole_output_copy_newlines.js new file mode 100644 index 0000000000..27e20a45d1 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_output_copy_newlines.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that multiple messages are copied into the clipboard and that they are +// separated by new lines. See bug 916997. +const TEST_URI = + "data:text/html,<meta charset=utf8>" + + "Test copy multiple messages to clipboard"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + const messages = Array.from( + { length: 10 }, + (_, i) => `Message number ${i + 1}` + ); + const lastMessage = [...messages].pop(); + const onMessage = waitForMessage(hud, lastMessage); + SpecialPowers.spawn(gBrowser.selectedBrowser, [messages], msgs => { + msgs.forEach(msg => content.wrappedJSObject.console.log(msg)); + }); + const { node } = await onMessage; + ok(node, "Messages were logged"); + + // Select the whole output. + const output = node.closest(".webconsole-output"); + selectNode(hud, output); + + info( + "Wait for the clipboard to contain the text corresponding to all the messages" + ); + await waitForClipboardPromise( + () => { + // The focus is on the JsTerm, so we need to blur it for the copy comand to work. + output.ownerDocument.activeElement.blur(); + goDoCommand("cmd_copy"); + }, + data => data.trim() === messages.join("\n") + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_output_order.js b/devtools/client/webconsole/test/browser/browser_webconsole_output_order.js new file mode 100644 index 0000000000..b0e1fbd204 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_output_order.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that any output created from calls to the console API comes before the +// echoed JavaScript. + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-console.html"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + const evaluationResultMessage = await executeAndWaitForMessage( + hud, + `for (let i = 0; i < 5; i++) { console.log("item-" + i); }`, + "undefined", + ".result" + ); + + info("Wait for all the log messages to be displayed"); + // Console messages are batched by the Resource watcher API and might be rendered after + // the result message. + const logMessages = await waitFor(() => { + const messages = findMessages(hud, "item-", ".log.message:not(.command)"); + return messages.length === 5 ? messages : null; + }); + + const commandMessage = findMessage(hud, "", ".command"); + is( + commandMessage.nextElementSibling, + logMessages[0], + `the command message is followed by the first log message ( Got "${commandMessage.nextElementSibling.textContent}")` + ); + + for (let i = 0; i < logMessages.length; i++) { + ok( + logMessages[i].textContent.includes(`item-${i}`), + `The log message item-${i} is at the expected position ( Got "${logMessages[i].textContent}")` + ); + } + + is( + logMessages[logMessages.length - 1].nextElementSibling, + evaluationResultMessage.node, + "The evaluation result is after the last log message" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_output_trimmed.js b/devtools/client/webconsole/test/browser/browser_webconsole_output_trimmed.js new file mode 100644 index 0000000000..bed44de677 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_output_trimmed.js @@ -0,0 +1,109 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that we trim start and end whitespace in user input +// in the messages list + +"use strict"; + +const TEST_URI = `http://example.com/browser/devtools/client/webconsole/test/browser/test-console.html`; + +const TEST_ITEMS = [ + { + name: "Commands without whitespace are not affected by trimming", + command: "Math.PI==='3.14159'", + expected: "Math.PI==='3.14159'", + }, + { + name: "Trims whitespace before and after a command (single line case)", + command: "\t\t (window.o_O || {}) [' O_o '] ", + expected: "(window.o_O || {}) [' O_o ']", + }, + { + name: + "When trimming a whitespace before and after a command, " + + "it keeps indentation for each contentful line", + command: " \n \n 1,\n 2,\n 3\n \n ", + expected: " 1,\n 2,\n 3", + }, + { + name: + "When trimming a whitespace before and after a command, " + + "it keeps trailing whitespace for all lines except the last", + command: + "\n" + + " let numbers = [1,\n" + + " 2, \n" + + " 3];\n" + + " \n" + + " \n" + + " function addNumber() { \n" + + " numbers.push(numbers.length + 1);\n" + + " } \n" + + " ", + expected: + " let numbers = [1,\n" + + " 2, \n" + + " 3];\n" + + " \n" + + " \n" + + " function addNumber() { \n" + + " numbers.push(numbers.length + 1);\n" + + " }", + }, +]; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + // Check that expected output and actual trimmed output match + for (const { name, command, expected } of TEST_ITEMS) { + await clearOutput(hud); + await executeAndWaitForMessage(hud, command, "", ".result"); + + const result = await getActualDisplayedInput(hud); + + if (result === expected) { + ok(true, name); + } else { + ok(false, formatError(name, result, expected)); + } + } +}); + +/** + * Get the text content of the latest command logged in the console + * @param {WebConsole} hud: The webconsole + * @return {string|null} + */ +async function getActualDisplayedInput(hud) { + const message = Array.from( + hud.ui.outputNode.querySelectorAll(".message.command") + ).pop(); + + if (message) { + // Open the message if its collapsed + const toggleArrow = message.querySelector(".collapse-button"); + if (toggleArrow) { + toggleArrow.click(); + await waitFor(() => message.classList.contains("open") === true); + } + + return message.querySelector("syntax-highlighted").textContent; + } + + return null; +} + +/** + * Format a "Got vs Expected" error message on multiple lines, + * making whitespace more visible in console output. + */ +function formatError(name, result, expected) { + const quote = str => + typeof str === "string" + ? "> " + str.replace(/ /g, "\u{B7}").replace(/\n/g, "\n> ") + : str; + + return `${name}\nGot:\n${quote(result)}\nExpected:\n${quote(expected)}\n`; +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_persist.js b/devtools/client/webconsole/test/browser/browser_webconsole_persist.js new file mode 100644 index 0000000000..88b39db8f5 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_persist.js @@ -0,0 +1,87 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check that message persistence works - bug 705921 / bug 1307881 + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-console.html"; + +registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.webconsole.persistlog"); +}); + +add_task(async function() { + info("Testing that messages disappear on a refresh if logs aren't persisted"); + const hud = await openNewTabAndConsole(TEST_URI); + + const INITIAL_LOGS_NUMBER = 5; + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [INITIAL_LOGS_NUMBER], + count => { + content.wrappedJSObject.doLogs(count); + } + ); + await waitFor(() => findMessages(hud, "").length === INITIAL_LOGS_NUMBER); + ok(true, "Messages showed up initially"); + + const onReloaded = hud.ui.once("reloaded"); + await refreshTab(); + await onReloaded; + await waitFor(() => findMessages(hud, "").length === 0); + ok(true, "Messages disappeared"); + + await closeToolbox(); +}); + +add_task(async function() { + info("Testing that messages persist on a refresh if logs are persisted"); + + const hud = await openNewTabAndConsole(TEST_URI); + + await toggleConsoleSetting( + hud, + ".webconsole-console-settings-menu-item-persistentLogs" + ); + + const INITIAL_LOGS_NUMBER = 5; + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [INITIAL_LOGS_NUMBER], + count => { + content.wrappedJSObject.doLogs(count); + } + ); + await waitFor(() => findMessages(hud, "").length === INITIAL_LOGS_NUMBER); + ok(true, "Messages showed up initially"); + + const onNavigatedMessage = waitForMessage(hud, "Navigated to"); + const onReloaded = hud.ui.once("reloaded"); + refreshTab(); + await onNavigatedMessage; + await onReloaded; + + ok(true, "Navigation message appeared as expected"); + is( + findMessages(hud, "").length, + INITIAL_LOGS_NUMBER + 1, + "Messages logged before navigation are still visible" + ); + + const { + visibleMessages, + messagesById, + } = hud.ui.wrapper.getStore().getState().messages; + const [commandId, resultId] = visibleMessages; + const commandMessage = messagesById.get(commandId).timeStamp; + const resultMessage = messagesById.get(resultId).timeStamp; + + ok( + resultMessage > commandMessage && resultMessage < Date.now(), + "The result has a timestamp newer than the command and older than current time" + ); + await closeToolbox(); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_promise_rejected_object.js b/devtools/client/webconsole/test/browser/browser_webconsole_promise_rejected_object.js new file mode 100644 index 0000000000..f7f320d6dc --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_promise_rejected_object.js @@ -0,0 +1,127 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that rejected promise are reported to the console. + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf-8, + <script> + function createRejectedPromise(reason) { + new Promise(function promiseCb(_, reject) { + setTimeout(function setTimeoutCb(){ + reject(reason); + }, 0); + }); + } + + var err = new Error("carrot"); + err.name = "VeggieError"; + + const reasons = [ + "potato", + "", + 0, + false, + null, + undefined, + {fav: "eggplant"}, + ["cherry", "strawberry"], + new Error("spinach"), + err, + ]; + + reasons.forEach(function forEachCb(reason) { + createRejectedPromise(reason); + }); + </script>`; + +add_task(async function() { + await pushPref("javascript.options.asyncstack_capture_debuggee_only", false); + const hud = await openNewTabAndConsole(TEST_URI); + + const expectedErrors = [ + "Uncaught (in promise) potato", + "Uncaught (in promise) <empty string>", + "Uncaught (in promise) 0", + "Uncaught (in promise) false", + "Uncaught (in promise) null", + "Uncaught (in promise) undefined", + `Uncaught (in promise) Object { fav: "eggplant" }`, + `Uncaught (in promise) Array [ "cherry", "strawberry" ]`, + `Uncaught (in promise) Error: spinach`, + `Uncaught (in promise) VeggieError: carrot`, + ]; + + for (const expectedError of expectedErrors) { + const message = await waitFor( + () => findMessage(hud, expectedError, ".error"), + `Couldn't find «${expectedError}» message` + ); + ok(message, `Found «${expectedError}» message`); + + message.querySelector(".collapse-button").click(); + const framesEl = await waitFor(() => { + const frames = message.querySelectorAll( + ".message-body-wrapper > .stacktrace .frame" + ); + return frames.length > 0 ? frames : null; + }, "Couldn't find stacktrace"); + + const frames = Array.from(framesEl) + .map(frameEl => + Array.from( + frameEl.querySelectorAll(".title, .location-async-cause") + ).map(el => el.textContent.trim()) + ) + .flat(); + + is( + frames.join("\n"), + [ + "setTimeoutCb", + "(Async: setTimeout handler)", + "promiseCb", + "createRejectedPromise", + "forEachCb", + "forEach", + "<anonymous>", + ].join("\n"), + "Error message has expected frames" + ); + } + ok(true, "All expected messages were found"); + + info("Check that object in errors can be expanded"); + const rejectedObjectMessage = findMessage( + hud, + `Uncaught (in promise) Object { fav: "eggplant" }`, + ".error" + ); + const oi = rejectedObjectMessage.querySelector(".tree"); + ok(true, "The object was rendered in an ObjectInspector"); + + info("Expanding the object"); + const onOiExpanded = waitFor(() => { + return oi.querySelectorAll(".node").length === 3; + }); + oi.querySelector(".arrow").click(); + await onOiExpanded; + + ok( + oi.querySelector(".arrow").classList.contains("expanded"), + "Object expanded" + ); + + // The object inspector now looks like: + // {...} + // | fav: "eggplant" + // | <prototype>: Object { ... } + + const oiNodes = oi.querySelectorAll(".node"); + is(oiNodes.length, 3, "There is the expected number of nodes in the tree"); + + ok(oiNodes[0].textContent.includes(`{\u2026}`)); + ok(oiNodes[1].textContent.includes(`fav: "eggplant"`)); + ok(oiNodes[2].textContent.includes(`<prototype>: Object { \u2026 }`)); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_reopen_closed_tab.js b/devtools/client/webconsole/test/browser/browser_webconsole_reopen_closed_tab.js new file mode 100644 index 0000000000..4dcdb5815b --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_reopen_closed_tab.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// See Bug 597756. Check that errors are still displayed in the console after +// reloading a page. + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-reopen-closed-tab.html"; + +add_task(async function() { + // If we persist log, the test might be successful even if only the first + // error log is shown. + pushPref("devtools.webconsole.persistlog", false); + + info("Open console and refresh tab."); + + expectUncaughtExceptionNoE10s(); + let hud = await openNewTabAndConsole(TEST_URI); + await clearOutput(hud); + + expectUncaughtExceptionNoE10s(); + await refreshTab(); + await waitForError(hud); + + // Close and reopen + await closeConsole(); + + expectUncaughtExceptionNoE10s(); + gBrowser.removeCurrentTab(); + hud = await openNewTabAndConsole(TEST_URI); + + expectUncaughtExceptionNoE10s(); + await refreshTab(); + await waitForError(hud); +}); + +async function waitForError(hud) { + info("Wait for error message"); + await waitFor(() => findMessage(hud, "fooBug597756_error", ".message.error")); + ok(true, "error message displayed"); +} + +function expectUncaughtExceptionNoE10s() { + // On e10s, the exception is triggered in child process + // and is ignored by test harness + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_repeat_different_objects.js b/devtools/client/webconsole/test/browser/browser_webconsole_repeat_different_objects.js new file mode 100644 index 0000000000..23ab0e5fd5 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_repeat_different_objects.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that makes sure messages are not considered repeated when console.log() +// is invoked with different objects, see bug 865288. + +"use strict"; + +const TEST_URI = "data:text/html,Test repeated objects"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + const onMessages = waitForMessages({ + hud, + messages: [{ text: "abba" }, { text: "abba" }, { text: "abba" }], + }); + + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + for (let i = 0; i < 3; i++) { + const o = { id: "abba" }; + content.console.log("abba", o); + } + }); + + info("waiting for 3 console.log objects, with the exact same text content"); + const messages = await onMessages; + is(messages.length, 3, "There are 3 messages, as expected."); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_requestStorageAccess_errors.js b/devtools/client/webconsole/test/browser/browser_webconsole_requestStorageAccess_errors.js new file mode 100644 index 0000000000..45ba374469 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_requestStorageAccess_errors.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = + "https://example.com/browser/devtools/client/webconsole/test/browser/test-storageaccess-errors.html"; +const LEARN_MORE_URI = + "https://developer.mozilla.org/docs/Web/API/Document/requestStorageAccess" + + DOCS_GA_PARAMS; + +const { UrlClassifierTestUtils } = ChromeUtils.import( + "resource://testing-common/UrlClassifierTestUtils.jsm" +); + +UrlClassifierTestUtils.addTestTrackers(); +registerCleanupFunction(function() { + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + async function checkErrorMessage(text) { + const message = await waitFor( + () => findMessage(hud, text, ".message.error"), + undefined, + 100 + ); + ok(true, "Error message is visible: " + text); + + const checkLink = ({ link, where, expectedLink, expectedTab }) => { + is(link, expectedLink, `Clicking the provided link opens ${link}`); + is( + where, + expectedTab, + `Clicking the provided link opens in expected tab` + ); + }; + + info("Clicking on the Learn More link"); + const learnMoreLink = message.querySelector(".learn-more-link"); + const linkSimulation = await simulateLinkClick(learnMoreLink); + checkLink({ + ...linkSimulation, + expectedLink: LEARN_MORE_URI, + expectedTab: "tab", + }); + } + + const userGesture = + "document.requestStorageAccess() may only be requested from inside a short running user-generated event handler"; + const nested = + "document.requestStorageAccess() may not be called in a nested iframe."; + const nullPrincipal = + "document.requestStorageAccess() may not be called on a document with an opaque origin, such as a sandboxed iframe without allow-same-origin in its sandbox attribute."; + const sandboxed = + "document.requestStorageAccess() may not be called in a sandboxed iframe without allow-storage-access-by-user-activation in its sandbox attribute."; + + await checkErrorMessage(userGesture); + await checkErrorMessage(nullPrincipal); + await checkErrorMessage(nested); + await checkErrorMessage(sandboxed); + + await closeConsole(); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_responsive_design_mode.js b/devtools/client/webconsole/test/browser/browser_webconsole_responsive_design_mode.js new file mode 100644 index 0000000000..ed3472a51b --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_responsive_design_mode.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that messages are displayed in the console when RDM is enabled + +const TEST_URI = "data:text/html,<meta charset=utf8>Test logging in RDM"; + +const ResponsiveUIManager = require("devtools/client/responsive/manager"); +const message = require("devtools/client/responsive/utils/message"); + +add_task(async function() { + const tab = await addTab(TEST_URI); + + // Use a local file for the device list, otherwise the panel tries to reach an external + // URL, which makes the test fail. + await pushPref( + "devtools.devices.url", + "http://example.com/browser/devtools/client/responsive/test/browser/devices.json" + ); + + info("Open responsive design mode"); + const { toolWindow } = await ResponsiveUIManager.openIfNeeded( + tab.ownerGlobal, + tab, + { + trigger: "test", + } + ); + await message.wait(toolWindow, "post-init"); + + info("Log a message before the console is open"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.console.log("Cached message"); + }); + + info("Open the console"); + const hud = await openConsole(tab); + await waitFor( + () => findMessage(hud, "Cached message"), + "Cached message isn't displayed in the console output" + ); + ok(true, "Cached message is displayed in the console"); + + info("Log a message while the console is open"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.console.log("Live message"); + }); + + await waitFor( + () => findMessage(hud, "Live message"), + "Live message isn't displayed in the console output" + ); + ok(true, "Live message is displayed in the console"); + + info("Close responsive design mode"); + await ResponsiveUIManager.closeIfNeeded(tab.ownerGlobal, tab); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_reverse_search.js b/devtools/client/webconsole/test/browser/browser_webconsole_reverse_search.js new file mode 100644 index 0000000000..589c34d5e2 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_reverse_search.js @@ -0,0 +1,177 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Tests reverse search features. + +"use strict"; + +const TEST_URI = `data:text/html,<meta charset=utf8>Test reverse search`; +const isMacOS = AppConstants.platform === "macosx"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + const jstermHistory = [ + `document`, + `Dog = "Snoopy"`, + `document + .querySelectorAll("*") + .forEach(()=>{})`, + `document`, + `"a" + "😎"`, + ]; + + // We have to wait for the same message twice in order to wait for the evaluation line + // as well as the result line + const onLastMessage = waitForMessages({ + hud, + messages: [{ text: `"a" + "😎"` }, { text: `"a😎"` }], + }); + for (const input of jstermHistory) { + execute(hud, input); + } + await onLastMessage; + + const initialValue = "initialValue"; + setInputValue(hud, initialValue); + + info("Check that the reverse search toolbar as the expected initial state"); + let reverseSearchElement = await openReverseSearch(hud); + ok( + reverseSearchElement, + "Reverse search is displayed with a keyboard shortcut" + ); + ok( + !getReverseSearchInfoElement(hud), + "The result info element is not displayed by default" + ); + ok( + !reverseSearchElement.querySelector(".search-result-button-prev") && + !reverseSearchElement.querySelector(".search-result-button-next"), + "The results navigation buttons are not displayed by default" + ); + is( + getInputValue(hud), + initialValue, + "The jsterm value is not changed when opening reverse search" + ); + is(isReverseSearchInputFocused(hud), true, "reverse search input is focused"); + + EventUtils.sendString("d"); + let infoElement = await waitFor(() => getReverseSearchInfoElement(hud)); + is( + infoElement.textContent, + "3 of 3 results", + "The reverse info has the expected text " + + "— duplicated results (`document`) are coalesced" + ); + + const previousButton = reverseSearchElement.querySelector( + ".search-result-button-prev" + ); + const nextButton = reverseSearchElement.querySelector( + ".search-result-button-next" + ); + ok(previousButton, "Previous navigation button is now displayed"); + is( + previousButton.title, + `Previous result (${isMacOS ? "Ctrl + R" : "F9"})`, + "Previous navigation button has expected title" + ); + + ok(nextButton, "Next navigation button is now displayed"); + is( + nextButton.title, + `Next result (${isMacOS ? "Ctrl + S" : "Shift + F9"})`, + "Next navigation button has expected title" + ); + is(getInputValue(hud), "document", "JsTerm has the expected input"); + is( + hud.jsterm.autocompletePopup.isOpen, + false, + "Setting the input value did not trigger the autocompletion" + ); + is(isReverseSearchInputFocused(hud), true, "reverse search input is focused"); + + let onJsTermValueChanged = hud.jsterm.once("set-input-value"); + EventUtils.sendString("og"); + await onJsTermValueChanged; + is(getInputValue(hud), `Dog = "Snoopy"`, "JsTerm input was updated"); + is( + infoElement.textContent, + "1 result", + "The reverse info has the expected text" + ); + ok( + !reverseSearchElement.querySelector(".search-result-button-prev") && + !reverseSearchElement.querySelector(".search-result-button-next"), + "The results navigation buttons are not displayed when there's only one result" + ); + + info("Check that the UI and results are updated when typing in the input"); + onJsTermValueChanged = hud.jsterm.once("set-input-value"); + EventUtils.sendString("g"); + await waitFor(() => reverseSearchElement.classList.contains("no-result")); + is( + getInputValue(hud), + `Dog = "Snoopy"`, + "JsTerm input was not updated since there's no results" + ); + is( + infoElement.textContent, + "No results", + "The reverse info has the expected text" + ); + ok( + !reverseSearchElement.querySelector(".search-result-button-prev") && + !reverseSearchElement.querySelector(".search-result-button-next"), + "The results navigation buttons are not displayed when there's no result" + ); + + info("Check that Backspace updates the UI"); + EventUtils.synthesizeKey("KEY_Backspace"); + await waitFor(() => !reverseSearchElement.classList.contains("no-result")); + is( + infoElement.textContent, + "1 result", + "The reverse info has the expected text" + ); + is(getInputValue(hud), `Dog = "Snoopy"`, "JsTerm kept its value"); + + info("Check that Escape does not affect the jsterm value"); + EventUtils.synthesizeKey("KEY_Escape"); + await waitFor(() => !getReverseSearchElement(hud)); + is( + getInputValue(hud), + `Dog = "Snoopy"`, + "Closing the input did not changed the JsTerm value" + ); + is(isInputFocused(hud), true, "input is focused"); + + info("Check that the search works with emojis"); + reverseSearchElement = await openReverseSearch(hud); + onJsTermValueChanged = hud.jsterm.once("set-input-value"); + EventUtils.sendString("😎"); + infoElement = await waitFor(() => getReverseSearchInfoElement(hud)); + is( + infoElement.textContent, + "1 result", + "The reverse info has the expected text" + ); + + info("Check that Enter evaluates the JsTerm and closes the UI"); + // We have to wait for the same message twice in order to wait for the evaluation line + // as well as the result line + const onMessage = waitForMessages({ + hud, + messages: [{ text: `"a" + "😎"` }, { text: `"a😎"` }], + }); + const onReverseSearchClose = waitFor(() => !getReverseSearchElement(hud)); + EventUtils.synthesizeKey("KEY_Enter"); + await Promise.all([onMessage, onReverseSearchClose]); + ok( + true, + "Enter evaluates what's in the JsTerm and closes the reverse search UI" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_reverse_search_initial_value.js b/devtools/client/webconsole/test/browser/browser_webconsole_reverse_search_initial_value.js new file mode 100644 index 0000000000..3ad4fdd5f6 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_reverse_search_initial_value.js @@ -0,0 +1,98 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Tests reverse search features. + +"use strict"; + +const TEST_URI = `data:text/html,<meta charset=utf8>Test reverse search initial value`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { jsterm } = hud; + + const jstermHistory = [ + `Dog = "Snoopy"`, + `document + .querySelectorAll("*") + .forEach(() => {})`, + `document`, + `"😎"`, + ]; + + const onLastMessage = waitForMessage(hud, `"😎"`, ".result"); + for (const input of jstermHistory) { + execute(hud, input); + } + await onLastMessage; + + setInputValue(hud, "ado"); + + info(`Select 2 chars ("do") from the input`); + jsterm.editor.setSelection({ line: 0, ch: 1 }, { line: 0, ch: 3 }); + + info("Check that the reverse search toolbar as the expected initial state"); + let reverseSearchElement = await openReverseSearch(hud); + is( + reverseSearchElement.querySelector("input").value, + "do", + `Reverse search input has expected "do" value` + ); + is(isReverseSearchInputFocused(hud), true, "reverse search input is focused"); + ok( + reverseSearchElement, + "Reverse search is displayed with a keyboard shortcut" + ); + const infoElement = getReverseSearchInfoElement(hud); + is( + infoElement.textContent, + "3 of 3 results", + "The reverse info has the expected text" + ); + + const previousButton = reverseSearchElement.querySelector( + ".search-result-button-prev" + ); + const nextButton = reverseSearchElement.querySelector( + ".search-result-button-next" + ); + ok(previousButton, "Previous navigation button is displayed"); + ok(nextButton, "Next navigation button is displayed"); + + is(getInputValue(hud), "document", "JsTerm has the expected input"); + is( + jsterm.autocompletePopup.isOpen, + false, + "Setting the input value did not trigger the autocompletion" + ); + + const onJsTermValueChanged = jsterm.once("set-input-value"); + EventUtils.sendString("g"); + await onJsTermValueChanged; + is(getInputValue(hud), `Dog = "Snoopy"`, "JsTerm input was updated"); + is( + infoElement.textContent, + "1 result", + "The reverse info has the expected text" + ); + ok( + !reverseSearchElement.querySelector(".search-result-button-prev") && + !reverseSearchElement.querySelector(".search-result-button-next"), + "The results navigation buttons are not displayed when there's only one result" + ); + + info("Check that there's no initial value when no text is selected"); + EventUtils.synthesizeKey("KEY_Escape"); + await waitFor(() => !getReverseSearchElement(hud)); + + info( + "Check that opening the reverse search input is empty after opening it again" + ); + reverseSearchElement = await openReverseSearch(hud); + is( + reverseSearchElement.querySelector("input").value, + "", + "Reverse search input is empty" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_reverse_search_keyboard_navigation.js b/devtools/client/webconsole/test/browser/browser_webconsole_reverse_search_keyboard_navigation.js new file mode 100644 index 0000000000..24ab3a523b --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_reverse_search_keyboard_navigation.js @@ -0,0 +1,144 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Tests reverse search results keyboard navigation. + +"use strict"; + +const TEST_URI = `data:text/html,<meta charset=utf8>Test reverse search`; +const isMacOS = AppConstants.platform === "macosx"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + const jstermHistory = [ + `document`, + `document + .querySelectorAll("*") + .forEach(console.log)`, + `Dog = "Snoopy"`, + ]; + + const onLastMessage = waitForMessage(hud, `"Snoopy"`, ".result"); + for (const input of jstermHistory) { + execute(hud, input); + } + await onLastMessage; + + await openReverseSearch(hud); + EventUtils.sendString("d"); + const infoElement = await waitFor(() => getReverseSearchInfoElement(hud)); + is( + infoElement.textContent, + "3 of 3 results", + "The reverse info has the expected text" + ); + + is(getInputValue(hud), jstermHistory[2], "JsTerm has the expected input"); + is( + hud.jsterm.autocompletePopup.isOpen, + false, + "Setting the input value did not trigger the autocompletion" + ); + + await navigateResultsAndCheckState(hud, { + direction: "previous", + expectedInfoText: "2 of 3 results", + expectedJsTermInputValue: jstermHistory[1], + }); + + await navigateResultsAndCheckState(hud, { + direction: "previous", + expectedInfoText: "1 of 3 results", + expectedJsTermInputValue: jstermHistory[0], + }); + + info( + "Check that we go back to the last matching item if we were at the first" + ); + await navigateResultsAndCheckState(hud, { + direction: "previous", + expectedInfoText: "3 of 3 results", + expectedJsTermInputValue: jstermHistory[2], + }); + + await navigateResultsAndCheckState(hud, { + direction: "next", + expectedInfoText: "1 of 3 results", + expectedJsTermInputValue: jstermHistory[0], + }); + + await navigateResultsAndCheckState(hud, { + direction: "next", + expectedInfoText: "2 of 3 results", + expectedJsTermInputValue: jstermHistory[1], + }); + + await navigateResultsAndCheckState(hud, { + direction: "next", + expectedInfoText: "3 of 3 results", + expectedJsTermInputValue: jstermHistory[2], + }); + + info( + "Check that trying to navigate when there's only 1 result does not throw" + ); + EventUtils.sendString("og"); + await waitFor( + () => getReverseSearchInfoElement(hud).textContent === "1 result" + ); + triggerPreviousResultShortcut(); + triggerNextResultShortcut(); + + info("Check that trying to navigate when there's no result does not throw"); + EventUtils.sendString("g"); + await waitFor( + () => getReverseSearchInfoElement(hud).textContent === "No results" + ); + triggerPreviousResultShortcut(); + triggerNextResultShortcut(); +}); + +async function navigateResultsAndCheckState( + hud, + { direction, expectedInfoText, expectedJsTermInputValue } +) { + const onJsTermValueChanged = hud.jsterm.once("set-input-value"); + if (direction === "previous") { + triggerPreviousResultShortcut(); + } else { + triggerNextResultShortcut(); + } + await onJsTermValueChanged; + + is(getInputValue(hud), expectedJsTermInputValue, "JsTerm has expected value"); + + const infoElement = getReverseSearchInfoElement(hud); + is( + infoElement.textContent, + expectedInfoText, + "The reverse info has the expected text" + ); + is( + isReverseSearchInputFocused(hud), + true, + "reverse search input is still focused" + ); +} + +function triggerPreviousResultShortcut() { + if (isMacOS) { + EventUtils.synthesizeKey("r", { ctrlKey: true }); + } else { + EventUtils.synthesizeKey("VK_F9"); + } +} + +function triggerNextResultShortcut() { + if (isMacOS) { + EventUtils.synthesizeKey("s", { ctrlKey: true }); + } else { + EventUtils.synthesizeKey("VK_F9", { shiftKey: true }); + } +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_reverse_search_mouse_navigation.js b/devtools/client/webconsole/test/browser/browser_webconsole_reverse_search_mouse_navigation.js new file mode 100644 index 0000000000..e9e292f81f --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_reverse_search_mouse_navigation.js @@ -0,0 +1,139 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Tests reverse search results mouse navigation. + +"use strict"; + +const TEST_URI = `data:text/html,<meta charset=utf8>Test reverse search`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + const jstermHistory = [ + `document`, + `document + .querySelectorAll("span") + .forEach(console.log)`, + `Dog = "Snoopy"`, + ]; + + const onLastMessage = waitForMessage(hud, `"Snoopy"`); + for (const input of jstermHistory) { + execute(hud, input); + } + await onLastMessage; + + await openReverseSearch(hud); + EventUtils.sendString("d"); + const infoElement = await waitFor(() => getReverseSearchInfoElement(hud)); + is( + infoElement.textContent, + "3 of 3 results", + "The reverse info has the expected text" + ); + + is(getInputValue(hud), jstermHistory[2], "JsTerm has the expected input"); + is( + hud.jsterm.autocompletePopup.isOpen, + false, + "Setting the input value did not trigger the autocompletion" + ); + + await navigateResultsAndCheckState(hud, { + direction: "previous", + expectedInfoText: "2 of 3 results", + expectedJsTermInputValue: jstermHistory[1], + }); + + await navigateResultsAndCheckState(hud, { + direction: "previous", + expectedInfoText: "1 of 3 results", + expectedJsTermInputValue: jstermHistory[0], + }); + + info( + "Check that we go back to the last matching item if we were at the first" + ); + await navigateResultsAndCheckState(hud, { + direction: "previous", + expectedInfoText: "3 of 3 results", + expectedJsTermInputValue: jstermHistory[2], + }); + + await navigateResultsAndCheckState(hud, { + direction: "next", + expectedInfoText: "1 of 3 results", + expectedJsTermInputValue: jstermHistory[0], + }); + + await navigateResultsAndCheckState(hud, { + direction: "next", + expectedInfoText: "2 of 3 results", + expectedJsTermInputValue: jstermHistory[1], + }); + + await navigateResultsAndCheckState(hud, { + direction: "next", + expectedInfoText: "3 of 3 results", + expectedJsTermInputValue: jstermHistory[2], + }); +}); + +async function navigateResultsAndCheckState( + hud, + { direction, expectedInfoText, expectedJsTermInputValue } +) { + const onJsTermValueChanged = hud.jsterm.once("set-input-value"); + if (direction === "previous") { + clickPreviousButton(hud); + } else { + clickNextButton(hud); + } + await onJsTermValueChanged; + + is(getInputValue(hud), expectedJsTermInputValue, "JsTerm has expected value"); + + const infoElement = getReverseSearchInfoElement(hud); + is( + infoElement.textContent, + expectedInfoText, + "The reverse info has the expected text" + ); + is( + isReverseSearchInputFocused(hud), + true, + "reverse search input is still focused" + ); +} + +function clickPreviousButton(hud) { + const reverseSearchElement = getReverseSearchElement(hud); + if (!reverseSearchElement) { + return; + } + const button = reverseSearchElement.querySelector( + ".search-result-button-prev" + ); + if (!button) { + return; + } + + button.click(); +} + +function clickNextButton(hud) { + const reverseSearchElement = getReverseSearchElement(hud); + if (!reverseSearchElement) { + return; + } + const button = reverseSearchElement.querySelector( + ".search-result-button-next" + ); + if (!button) { + return; + } + + button.click(); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_reverse_search_toggle.js b/devtools/client/webconsole/test/browser/browser_webconsole_reverse_search_toggle.js new file mode 100644 index 0000000000..774c0d8a10 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_reverse_search_toggle.js @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Tests showing and hiding the reverse search UI. + +"use strict"; + +const TEST_URI = `data:text/html,<meta charset=utf8>Test reverse search toggle`; +const isMacOS = AppConstants.platform === "macosx"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + info("Close the reverse search UI with ESC"); + await openReverseSearch(hud); + let onReverseSearchUiClose = waitFor( + () => getReverseSearchElement(hud) === null + ); + EventUtils.sendKey("ESCAPE"); + await onReverseSearchUiClose; + ok(true, "Reverse search was closed with the Esc keyboard shortcut"); + + if (isMacOS) { + info("Close the reverse search UI with Ctrl + C on OSX"); + await openReverseSearch(hud); + onReverseSearchUiClose = waitFor( + () => getReverseSearchElement(hud) === null + ); + EventUtils.synthesizeKey("c", { ctrlKey: true }); + await onReverseSearchUiClose; + ok(true, "Reverse search was closed with the Ctrl + C keyboard shortcut"); + } + + info("Close the reverse search UI with the close button"); + const reverseSearchElement = await openReverseSearch(hud); + const closeButton = reverseSearchElement.querySelector( + ".reverse-search-close-button" + ); + ok(closeButton, "The close button is displayed"); + is( + closeButton.title, + `Close (Esc${isMacOS ? " | Ctrl + C" : ""})`, + "The close button has the expected tooltip" + ); + onReverseSearchUiClose = waitFor(() => getReverseSearchElement(hud) === null); + closeButton.click(); + await onReverseSearchUiClose; + ok(true, "Reverse search was closed by clicking on the close button"); + + info("Close the reverse search UI by clicking on the output"); + await openReverseSearch(hud); + hud.ui.outputNode.querySelector(".jsterm-input-container").click(); + ok(true, "Reverse search was closed by clicking in the output"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_same_origin_errors.js b/devtools/client/webconsole/test/browser/browser_webconsole_same_origin_errors.js new file mode 100644 index 0000000000..e5a50c3c9a --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_same_origin_errors.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Ensure that same-origin errors are logged to the console. + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/test/browser/test-same-origin-required-load.html"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + const targetURL = "http://example.org"; + const onErrorMessage = waitForMessage(hud, "may not load data"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [targetURL], url => { + XPCNativeWrapper.unwrap(content).testTrack(url); + }); + const message = await onErrorMessage; + const node = message.node; + ok( + node.classList.contains("error"), + "The message has the expected classname" + ); + ok( + node.textContent.includes(targetURL), + "The message is about the thing we were expecting" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_sandbox_update_after_navigation.js b/devtools/client/webconsole/test/browser/browser_webconsole_sandbox_update_after_navigation.js new file mode 100644 index 0000000000..c02ab9fe05 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_sandbox_update_after_navigation.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests if the JSTerm sandbox is updated when the user navigates from one +// domain to another, in order to avoid permission denied errors with a sandbox +// created for a different origin. See Bug 664688. + +"use strict"; + +const BASE_URI = + "browser/devtools/client/webconsole/test/browser/test-console.html"; +const TEST_URI1 = "http://example.com/" + BASE_URI; +const TEST_URI2 = "http://example.org/" + BASE_URI; + +add_task(async function() { + await pushPref("devtools.webconsole.persistlog", false); + + const hud = await openNewTabAndConsole(TEST_URI1); + + await executeAndWaitForMessage( + hud, + "window.location.href", + TEST_URI1, + ".result" + ); + + // load second url + await navigateTo(TEST_URI2); + + ok(!findMessage(hud, "Permission denied"), "no permission denied errors"); + + info("wait for window.location.href after page navigation"); + await clearOutput(hud); + await executeAndWaitForMessage( + hud, + "window.location.href", + TEST_URI2, + ".result" + ); + + ok(!findMessage(hud, "Permission denied"), "no permission denied errors"); + + // Navigation clears messages. Wait for that clear to happen before + // continuing the test or it might destroy messages we wait later on (Bug + // 1270234). + const promises = [hud.ui.once("messages-cleared")]; + if (isFissionEnabled() && isTargetSwitchingEnabled()) { + promises.push(hud.targetList.once("switched-target")); + } + + gBrowser.goBack(); + + info("Waiting for messages-cleared event due to navigation"); + await Promise.all(promises); + + info("Messages cleared after navigation; checking location"); + await executeAndWaitForMessage( + hud, + "window.location.href", + TEST_URI1, + ".result" + ); + + ok(!findMessage(hud, "Permission denied"), "no permission denied errors"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_script_errordoc_urls.js b/devtools/client/webconsole/test/browser/browser_webconsole_script_errordoc_urls.js new file mode 100644 index 0000000000..2ae6fc2f11 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_script_errordoc_urls.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Ensure that [Learn More] links appear alongside any errors listed +// in "errordocs.js". Note: this only tests script execution. + +"use strict"; + +const ErrorDocs = require("devtools/server/actors/errordocs"); +const TEST_URI = "data:text/html;charset=utf8,errordoc tests"; + +function makeURIData(script) { + return `data:text/html;charset=utf8,<script>${script}</script>`; +} + +const TestData = [ + { + jsmsg: "JSMSG_READ_ONLY", + script: + "'use strict'; (Object.freeze({name: 'Elsa', score: 157})).score = 0;", + isException: true, + expected: 'TypeError: "score" is read-only', + }, + { + jsmsg: "JSMSG_STMT_AFTER_RETURN", + script: "function a() { return; 1 + 1; };", + isException: false, + expected: "unreachable code after return statement", + }, +]; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + for (const data of TestData) { + await testScriptError(hud, data); + } +}); + +async function testScriptError(hud, testData) { + const isE10s = Services.appinfo.browserTabsRemoteAutostart; + if (testData.isException && !isE10s) { + expectUncaughtException(); + } + + await navigateTo(makeURIData(testData.script)); + + const msg = "the expected error message was displayed"; + info(`waiting for ${msg} to be displayed`); + await waitFor(() => findMessage(hud, testData.expected)); + ok(true, msg); + + // grab the most current error doc URL. + const urlObj = new URL( + ErrorDocs.GetURL({ errorMessageName: testData.jsmsg }) + ); + + // strip all params from the URL. + const url = `${urlObj.origin}${urlObj.pathname}`; + + // Gather all URLs displayed in the console. [Learn More] links have no href + // but have the URL in the title attribute. + const hrefs = new Set(); + for (const link of hud.ui.outputNode.querySelectorAll("a")) { + hrefs.add(link.title); + } + + ok(hrefs.has(url), `Expected a link to ${url}.`); + + await clearOutput(hud); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_scroll.js b/devtools/client/webconsole/test/browser/browser_webconsole_scroll.js new file mode 100644 index 0000000000..e3072a2f11 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_scroll.js @@ -0,0 +1,307 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf-8,<p>Web Console test for scroll.</p> + <script> + var a = () => b(); + var b = () => c(); + var c = () => console.trace("trace in C"); + + for (let i = 0; i < 100; i++) { + if (i % 10 === 0) { + c(); + } + console.log("init-" + i); + } + </script> +`; +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const { ui } = hud; + const outputContainer = ui.outputNode.querySelector(".webconsole-output"); + + info("Console should be scrolled to bottom on initial load from page logs"); + await waitFor(() => findMessage(hud, "init-99")); + ok(hasVerticalOverflow(outputContainer), "There is a vertical overflow"); + ok( + isScrolledToBottom(outputContainer), + "The console is scrolled to the bottom" + ); + + info("Wait until all stacktraces are rendered"); + await waitFor( + () => outputContainer.querySelectorAll(".frames").length === 10 + ); + ok( + isScrolledToBottom(outputContainer), + "The console is scrolled to the bottom" + ); + + await refreshTab(); + + info("Console should be scrolled to bottom after refresh from page logs"); + await waitFor(() => findMessage(hud, "init-99")); + ok(hasVerticalOverflow(outputContainer), "There is a vertical overflow"); + ok( + isScrolledToBottom(outputContainer), + "The console is scrolled to the bottom" + ); + + info("Wait until all stacktraces are rendered"); + await waitFor( + () => outputContainer.querySelectorAll(".frames").length === 10 + ); + ok( + isScrolledToBottom(outputContainer), + "The console is scrolled to the bottom" + ); + + info("Scroll up"); + outputContainer.scrollTop = 0; + + info("Add a console.trace message to check that the scroll isn't impacted"); + let onMessage = waitForMessage(hud, "trace in C"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.c(); + }); + let message = await onMessage; + ok(hasVerticalOverflow(outputContainer), "There is a vertical overflow"); + is(outputContainer.scrollTop, 0, "The console stayed scrolled to the top"); + + info("Wait until the stacktrace is rendered"); + await waitFor(() => message.node.querySelector(".frame")); + is(outputContainer.scrollTop, 0, "The console stayed scrolled to the top"); + + info("Evaluate a command to check that the console scrolls to the bottom"); + await executeAndWaitForMessage(hud, "21 + 21", "42", ".result"); + ok(hasVerticalOverflow(outputContainer), "There is a vertical overflow"); + ok( + isScrolledToBottom(outputContainer), + "The console is scrolled to the bottom" + ); + + info( + "Add a message to check that the console do scroll since we're at the bottom" + ); + onMessage = waitForMessage(hud, "scroll"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.console.log("scroll"); + }); + await onMessage; + ok(hasVerticalOverflow(outputContainer), "There is a vertical overflow"); + ok( + isScrolledToBottom(outputContainer), + "The console is scrolled to the bottom" + ); + + info( + "Evaluate an Error object to check that the console scrolls to the bottom" + ); + message = await executeAndWaitForMessage( + hud, + ` + x = new Error("myErrorObject"); + x.stack = "a@b/c.js:1:2\\nd@e/f.js:3:4"; + x;`, + "myErrorObject", + ".result" + ); + ok( + isScrolledToBottom(outputContainer), + "The console is scrolled to the bottom" + ); + + info( + "Wait until the stacktrace is rendered and check the console is scrolled" + ); + await waitFor(() => + message.node.querySelector(".objectBox-stackTrace .frame") + ); + ok( + isScrolledToBottom(outputContainer), + "The console is scrolled to the bottom" + ); + + info( + "Throw an Error object in a direct evaluation to check that the console scrolls to the bottom" + ); + message = await executeAndWaitForMessage( + hud, + ` + x = new Error("myEvaluatedThrownErrorObject"); + x.stack = "a@b/c.js:1:2\\nd@e/f.js:3:4"; + throw x; + `, + "Uncaught Error: myEvaluatedThrownErrorObject", + ".error" + ); + ok( + isScrolledToBottom(outputContainer), + "The console is scrolled to the bottom" + ); + + info( + "Wait until the stacktrace is rendered and check the console is scrolled" + ); + await waitFor(() => + message.node.querySelector(".objectBox-stackTrace .frame") + ); + ok( + isScrolledToBottom(outputContainer), + "The console is scrolled to the bottom" + ); + + info("Throw an Error object to check that the console scrolls to the bottom"); + message = await executeAndWaitForMessage( + hud, + ` + setTimeout(() => { + x = new Error("myThrownErrorObject"); + x.stack = "a@b/c.js:1:2\\nd@e/f.js:3:4"; + throw x + }, 10)`, + "Uncaught Error: myThrownErrorObject", + ".error" + ); + ok( + isScrolledToBottom(outputContainer), + "The console is scrolled to the bottom" + ); + + info( + "Wait until the stacktrace is rendered and check the console is scrolled" + ); + await waitFor(() => + message.node.querySelector(".objectBox-stackTrace .frame") + ); + ok( + isScrolledToBottom(outputContainer), + "The console is scrolled to the bottom" + ); + + info( + "Add a console.trace message to check that the console stays scrolled to bottom" + ); + onMessage = waitForMessage(hud, "trace in C"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.c(); + }); + message = await onMessage; + ok(hasVerticalOverflow(outputContainer), "There is a vertical overflow"); + ok( + isScrolledToBottom(outputContainer), + "The console is scrolled to the bottom" + ); + + info("Wait until the stacktrace is rendered"); + await waitFor(() => message.node.querySelector(".frame")); + ok( + isScrolledToBottom(outputContainer), + "The console is scrolled to the bottom" + ); + + info("Check that repeated messages don't prevent scroll to bottom"); + // We log a first message. + onMessage = waitForMessage(hud, "repeat"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.console.log("repeat"); + }); + message = await onMessage; + + // And a second one. We can't log them at the same time since we batch redux actions, + // and the message would already appear with the repeat badge, and the bug is + // only triggered when the badge is rendered after the initial message rendering. + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.console.log("repeat"); + }); + await waitFor(() => message.node.querySelector(".message-repeats")); + ok( + isScrolledToBottom(outputContainer), + "The console is still scrolled to the bottom when the repeat badge is added" + ); + + info( + "Check that adding a message after a repeated message scrolls to bottom" + ); + onMessage = waitForMessage(hud, "after repeat"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.console.log("after repeat"); + }); + message = await onMessage; + ok( + isScrolledToBottom(outputContainer), + "The console is scrolled to the bottom after a repeated message" + ); + + info( + "Check that switching between editor and inline mode keep the output scrolled to bottom" + ); + await toggleLayout(hud); + // Wait until the output is scrolled to the bottom. + await waitFor( + () => isScrolledToBottom(outputContainer), + "Output does not scroll to the bottom after switching to editor mode" + ); + ok( + true, + "The console is scrolled to the bottom after switching to editor mode" + ); + + // Switching back to inline mode + await toggleLayout(hud); + // Wait until the output is scrolled to the bottom. + await waitFor( + () => isScrolledToBottom(outputContainer), + "Output does not scroll to the bottom after switching back to inline mode" + ); + ok( + true, + "The console is scrolled to the bottom after switching back to inline mode" + ); + + info( + "Check that expanding a large object does not scroll the output to the bottom" + ); + // Clear the output so we only have the object + await clearOutput(hud); + // Evaluate an object with a hundred properties + const result = await executeAndWaitForMessage( + hud, + `Array.from({length: 100}, (_, i) => i) + .reduce( + (acc, item) => {acc["item-" + item] = item; return acc;}, + {} + )`, + "Object", + ".message.result" + ); + // Expand the object + result.node.querySelector(".arrow").click(); + // Wait until we have 102 nodes (the root node, 100 properties + <prototype>) + await waitFor(() => result.node.querySelectorAll(".node").length === 102); + // wait for a bit to give time to the resize observer callback to be triggered + await wait(500); + ok(hasVerticalOverflow(outputContainer), "The output does overflow"); + is( + isScrolledToBottom(outputContainer), + false, + "The output was not scrolled to the bottom" + ); +}); + +function hasVerticalOverflow(container) { + return container.scrollHeight > container.clientHeight; +} + +function isScrolledToBottom(container) { + if (!container.lastChild) { + return true; + } + const lastNodeHeight = container.lastChild.clientHeight; + return ( + container.scrollTop + container.clientHeight >= + container.scrollHeight - lastNodeHeight / 2 + ); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_select_all.js b/devtools/client/webconsole/test/browser/browser_webconsole_select_all.js new file mode 100644 index 0000000000..87314e71df --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_select_all.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the global Firefox "Select All" functionality (e.g. Edit > +// Select All) works properly in the Web Console. + +/* import-globals-from head.js */ + +const TEST_URI = "http://example.com/"; + +add_task(async function testSelectAll() { + const hud = await openNewTabAndConsole(TEST_URI); + await testSelectionWhenMovingBetweenBoxes(hud); + testBrowserMenuSelectAll(hud); + await testContextMenuSelectAll(hud); +}); + +async function testSelectionWhenMovingBetweenBoxes(hud) { + // Fill the console with some output. + await clearOutput(hud); + await executeAndWaitForMessage(hud, "1 + 2", "3", ".result"); + await executeAndWaitForMessage(hud, "3 + 4", "7", ".result"); + await executeAndWaitForMessage(hud, "5 + 6", "11", ".result"); +} + +function testBrowserMenuSelectAll(hud) { + const { ui } = hud; + const outputContainer = ui.outputNode.querySelector(".webconsole-output"); + + is( + outputContainer.childNodes.length, + 6, + "the output node contains the expected number of children" + ); + + // The focus is on the JsTerm, so we need to blur it for the copy comand to + // work. + outputContainer.ownerDocument.activeElement.blur(); + + // Test that the global Firefox "Select All" functionality (e.g. Edit > + // Select All) works properly in the Web Console. + goDoCommand("cmd_selectAll"); + + checkMessagesSelected(outputContainer); + hud.iframeWindow.getSelection().removeAllRanges(); +} + +// Test the context menu "Select All" (which has a different code path) works +// properly as well. +async function testContextMenuSelectAll(hud) { + const { ui } = hud; + const outputContainer = ui.outputNode.querySelector(".webconsole-output"); + const contextMenu = await openContextMenu(hud, outputContainer); + + const selectAllItem = contextMenu.querySelector("#console-menu-select"); + ok( + selectAllItem, + `the context menu on the output node has a "Select All" item` + ); + + outputContainer.focus(); + selectAllItem.click(); + + checkMessagesSelected(outputContainer); + hud.iframeWindow.getSelection().removeAllRanges(); +} + +function checkMessagesSelected(outputContainer) { + const selection = outputContainer.ownerDocument.getSelection(); + const messages = outputContainer.querySelectorAll(".message"); + + for (const message of messages) { + const selected = selection.containsNode(message); + ok(selected, `Node containing text "${message.textContent}" was selected`); + } +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_show_subresource_security_errors.js b/devtools/client/webconsole/test/browser/browser_webconsole_show_subresource_security_errors.js new file mode 100644 index 0000000000..04426373f7 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_show_subresource_security_errors.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Ensure non-toplevel security errors are displayed + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf-8,Web Console subresource STS warning test"; +const TEST_DOC = + "https://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-subresource-security-error.html"; +const SAMPLE_MSG = "specified a header that could not be parsed successfully."; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + await clearOutput(hud); + await navigateTo(TEST_DOC); + + await waitFor(() => findMessage(hud, SAMPLE_MSG, ".message.warn")); + + ok(true, "non-toplevel security warning message was displayed"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_shows_reqs_from_netmonitor.js b/devtools/client/webconsole/test/browser/browser_webconsole_shows_reqs_from_netmonitor.js new file mode 100644 index 0000000000..28a891cf83 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_shows_reqs_from_netmonitor.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=utf8,Test that the netmonitor " + + "displays requests that have been recorded in the " + + "web console, even if the netmonitor hadn't opened yet."; + +const TEST_FILE = "test-network-request.html"; +const TEST_PATH = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/" + + TEST_FILE; + +const NET_PREF = "devtools.webconsole.filter.net"; +Services.prefs.setBoolPref(NET_PREF, true); +registerCleanupFunction(async () => { + Services.prefs.clearUserPref(NET_PREF); + + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +add_task(async function task() { + // Make sure the filter to show all the requests is set + await pushPref("devtools.netmonitor.filters", '["all"]'); + + // Test that the request appears in the console. + const hud = await openNewTabAndConsole(TEST_URI); + const currentTab = gBrowser.selectedTab; + info("Web console is open"); + + const onMessageAdded = waitForMessages({ + hud, + messages: [ + { + text: TEST_PATH, + }, + ], + }); + + await navigateTo(TEST_PATH); + info("Document loaded."); + + await onMessageAdded; + info("Network message found."); + + // Test that the request appears in the network panel. + const target = await TargetFactory.forTab(currentTab); + const toolbox = await gDevTools.showToolbox(target, "netmonitor"); + info("Network panel is open."); + + await testNetmonitor(toolbox); +}); + +async function testNetmonitor(toolbox) { + const monitor = toolbox.getCurrentPanel(); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + await waitUntil(() => store.getState().requests.requests.length > 0); + // Lets also wait until all the event timings data requested + // has completed and the column is rendered. + await waitForDOM( + document, + ".request-list-item:first-child .requests-list-timings-total" + ); + + is( + store.getState().requests.requests.length, + 1, + "Network request appears in the network panel" + ); + + const item = getSortedRequests(store.getState())[0]; + is(item.method, "GET", "The attached method is correct."); + is(item.url, TEST_PATH, "The attached url is correct."); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_shows_reqs_in_netmonitor.js b/devtools/client/webconsole/test/browser/browser_webconsole_shows_reqs_in_netmonitor.js new file mode 100644 index 0000000000..518a51f6f9 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_shows_reqs_in_netmonitor.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf8,Test that the web console " + + "displays requests that have been recorded in the " + + "netmonitor, even if the console hadn't opened yet."; + +const TEST_FILE = "test-network-request.html"; +const TEST_PATH = + "http://example.com/browser/devtools/client/webconsole/test/browser/" + + TEST_FILE; + +const NET_PREF = "devtools.webconsole.filter.net"; +Services.prefs.setBoolPref(NET_PREF, true); +registerCleanupFunction(async () => { + Services.prefs.clearUserPref(NET_PREF); + + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +add_task(async function() { + const toolbox = await openNewTabAndToolbox(TEST_URI, "netmonitor"); + info("Network panel is open."); + + await navigateTo(TEST_PATH); + info("Document loaded."); + + // Test that the request appears in the network panel. + await testNetmonitor(toolbox); + + // Test that the request appears in the console. + const { hud } = await toolbox.selectTool("webconsole"); + info("Web console is open"); + + // We can't use `waitForMessages` here because the `new-messages` event + // can be emitted before we get the `hud`. + await waitFor(() => findMessage(hud, TEST_PATH)); + + ok(true, "The network message was found in the console"); +}); + +async function testNetmonitor(toolbox) { + const monitor = toolbox.getCurrentPanel(); + const { store, windowRequire } = monitor.panelWin; + const { getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + await waitFor(() => store.getState().requests.requests.length > 0); + + is( + store.getState().requests.requests.length, + 1, + "Network request appears in the network panel" + ); + + const item = getSortedRequests(store.getState())[0]; + is(item.method, "GET", "The request method is correct."); + is(item.url, TEST_PATH, "The request url is correct."); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_sidebar_object_expand_when_message_pruned.js b/devtools/client/webconsole/test/browser/browser_webconsole_sidebar_object_expand_when_message_pruned.js new file mode 100644 index 0000000000..3dc2b4103b --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_sidebar_object_expand_when_message_pruned.js @@ -0,0 +1,83 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Test that an object in the sidebar can still be expanded after the message where it was +// logged is pruned. + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf8," + + "<script>console.log({a:1,b:2,c:[3,4,5]});</script>"; + +add_task(async function() { + // Should be removed when sidebar work is complete (Bug 1447235) + await pushPref("devtools.webconsole.sidebarToggle", true); + // Set the loglimit to 1 so message gets pruned as soon as another message is displayed. + await pushPref("devtools.hud.loglimit", 1); + + const hud = await openNewTabAndConsole(TEST_URI); + + const message = await waitFor(() => findMessage(hud, "Object")); + const object = message.querySelector(".object-inspector .objectBox-object"); + + const sidebar = await showSidebarWithContextMenu(hud, object, true); + + const oi = sidebar.querySelector(".object-inspector"); + let oiNodes = oi.querySelectorAll(".node"); + if (oiNodes.length === 1) { + // If this is the case, we wait for the properties to be fetched and displayed. + await waitFor(() => oi.querySelectorAll(".node").length > 1); + oiNodes = oi.querySelectorAll(".node"); + } + + info("Log a message so the original one gets pruned"); + const messageText = "hello world"; + const onMessage = waitForMessage(hud, messageText); + SpecialPowers.spawn(gBrowser.selectedBrowser, [messageText], async function( + str + ) { + content.console.log(str); + }); + await onMessage; + + ok(!findMessage(hud, "Object"), "Message with object was pruned"); + + info("Expand the 'c' node in the sidebar"); + // Here's what the object in the sidebar looks like: + // ▼ {…} + // a: 1 + // b: 2 + // ▶︎ c: (3) […] + // ▶︎ <prototype>: {…} + const cNode = oiNodes[3]; + const onNodeExpanded = waitFor(() => oi.querySelectorAll(".node").length > 5); + cNode.click(); + await onNodeExpanded; + + // Here's what the object in the sidebar should look like: + // ▼ {…} + // a: 1 + // b: 2 + // ▼ c: (3) […] + // 0: 3 + // 1: 4 + // 2: 5 + // length: 3 + // ▶︎ <prototype>: [] + // ▶︎ <prototype>: {…} + is(oi.querySelectorAll(".node").length, 10, "The 'c' property was expanded"); +}); + +async function showSidebarWithContextMenu(hud, node) { + const appNode = hud.ui.document.querySelector(".webconsole-app"); + const onSidebarShown = waitFor(() => appNode.querySelector(".sidebar")); + + const contextMenu = await openContextMenu(hud, node); + const openInSidebar = contextMenu.querySelector("#console-menu-open-sidebar"); + openInSidebar.click(); + await onSidebarShown; + await hideContextMenu(hud); + return onSidebarShown; +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_sidebar_scroll.js b/devtools/client/webconsole/test/browser/browser_webconsole_sidebar_scroll.js new file mode 100644 index 0000000000..72faace0f5 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_sidebar_scroll.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the sidebar can be scrolled. + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf8,Test sidebar scroll`; + +add_task(async function() { + // Should be removed when sidebar work is complete + await pushPref("devtools.webconsole.sidebarToggle", true); + const isMacOS = Services.appinfo.OS === "Darwin"; + + const hud = await openNewTabAndConsole(TEST_URI); + + const onMessage = waitForMessage(hud, "Document"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.console.log(content.wrappedJSObject.document); + }); + + const { node } = await onMessage; + const object = node.querySelector(".object-inspector .node"); + + info("Ctrl+click on an object to put it in the sidebar"); + const onSidebarShown = waitFor(() => + hud.ui.document.querySelector(".sidebar") + ); + AccessibilityUtils.setEnv({ + // Component that renders a node handles keyboard interactions on the + // container level. + focusableRule: false, + interactiveRule: false, + labelRule: false, + }); + await EventUtils.sendMouseEvent( + { + type: "click", + [isMacOS ? "metaKey" : "ctrlKey"]: true, + }, + object, + hud.ui.window + ); + AccessibilityUtils.resetEnv(); + await onSidebarShown; + const sidebarContents = hud.ui.document.querySelector(".sidebar-contents"); + + // Let's wait until the object is fully expanded. + await waitFor(() => sidebarContents.querySelectorAll(".node").length > 1); + ok( + sidebarContents.scrollHeight > sidebarContents.clientHeight, + "Sidebar overflows" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_sourcemap_css.js b/devtools/client/webconsole/test/browser/browser_webconsole_sourcemap_css.js new file mode 100644 index 0000000000..b1f219ff32 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_sourcemap_css.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that a missing original source is reported. + +const CSS_URL = URL_ROOT + "source-mapped.css"; + +const PAGE_URL = `data:text/html, +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Empty test page to test source map and css</title> + </head> + + <link href="${CSS_URL}" rel="stylesheet" type="text/css"> + <body> + <div> + There should be a source-mapped CSS warning in the console. + </div> + </body> + +</html>`; + +add_task(async function() { + await pushPref("devtools.source-map.client-service.enabled", true); + await pushPref("devtools.webconsole.filter.css", true); + + const hud = await openNewTabAndConsole(PAGE_URL); + + info("Waiting for css warning"); + const node = await waitFor(() => findMessage(hud, "octopus")); + ok(!!node, "css warning seen"); + + info("Waiting for source map to be applied"); + const found = await waitFor(() => { + const frameLinkNode = node.querySelector(".message-location .frame-link"); + const url = frameLinkNode.getAttribute("data-url"); + return url.includes("scss"); + }); + + ok(found, "css warning is source mapped in web console"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_sourcemap_error.js b/devtools/client/webconsole/test/browser/browser_webconsole_sourcemap_error.js new file mode 100644 index 0000000000..62b24e4174 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_sourcemap_error.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that a missing source map is reported. + +const BASE = + "http://example.com/browser/devtools/client/webconsole/" + "test/browser/"; + +add_task(async function() { + for (const test of [ + "test-sourcemap-error-01.html", + "test-sourcemap-error-02.html", + ]) { + const hud = await openNewTabAndConsole(BASE + test); + + const node = await waitFor(() => findMessage(hud, "here")); + ok(node, "logged text is displayed in web console"); + + const node2 = await waitFor(() => findMessage(hud, "Source map error")); + ok(node2, "source map error is displayed in web console"); + } +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_sourcemap_invalid.js b/devtools/client/webconsole/test/browser/browser_webconsole_sourcemap_invalid.js new file mode 100644 index 0000000000..e0286f5878 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_sourcemap_invalid.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that an invalid source map is reported. + +const JS_URL = URL_ROOT + "code_bundle_invalidmap.js"; + +const PAGE_URL = `data:text/html, +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Empty test page to test source map with invalid source map</title> + </head> + + <body> + <script src="${JS_URL}"></script> + </body> + +</html>`; + +add_task(async function() { + await pushPref("devtools.source-map.client-service.enabled", true); + + const hud = await openNewTabAndConsole(PAGE_URL); + + const node = await waitFor(() => findMessage(hud, "Source map error")); + ok(node, "source map error is displayed in web console"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_sourcemap_nosource.js b/devtools/client/webconsole/test/browser/browser_webconsole_sourcemap_nosource.js new file mode 100644 index 0000000000..24316997c3 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_sourcemap_nosource.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that a missing original source is reported. + +const JS_URL = URL_ROOT + "code_bundle_nosource.js"; + +const PAGE_URL = `data:text/html, +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Empty test page to test source map with missing original source</title> + </head> + + <body> + <script src="${JS_URL}"></script> + </body> + +</html>`; + +add_task(async function() { + await pushPref("devtools.source-map.client-service.enabled", true); + + const hud = await openNewTabAndConsole(PAGE_URL); + const toolbox = hud.ui.wrapper.toolbox; + + info('Finding "here" message and waiting for source map to be applied'); + await waitFor(() => { + const node = findMessage(hud, "here"); + if (!node) { + return false; + } + const frameLinkNode = node.querySelector(".message-location .frame-link"); + const url = frameLinkNode.getAttribute("data-url"); + return url.includes("nosuchfile"); + }); + + await testOpenInDebugger(hud, toolbox, "here", true, false, false); + + info("Selecting the console again"); + await toolbox.selectTool("webconsole"); + + const node = await waitFor(() => findMessage(hud, "original source")); + ok(node, "source map error is displayed in web console"); + + ok( + !!node.querySelector(".learn-more-link"), + "source map error has learn more link" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_split.js b/devtools/client/webconsole/test/browser/browser_webconsole_split.js new file mode 100644 index 0000000000..9a92befcff --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_split.js @@ -0,0 +1,362 @@ +/* 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,Web Console test for splitting"; +const { LocalizationHelper } = require("devtools/shared/l10n"); +const L10N = new LocalizationHelper( + "devtools/client/locales/toolbox.properties" +); + +// Test is slow on Linux EC2 instances - Bug 962931 +requestLongerTimeout(4); + +add_task(async function() { + let toolbox; + await addTab(TEST_URI); + await testConsoleLoadOnDifferentPanel(); + await testKeyboardShortcuts(); + await checkAllTools(); + + info("Testing host types"); + checkHostType(Toolbox.HostType.BOTTOM); + await checkToolboxUI(); + await toolbox.switchHost(Toolbox.HostType.RIGHT); + checkHostType(Toolbox.HostType.RIGHT); + await checkToolboxUI(); + await toolbox.switchHost(Toolbox.HostType.WINDOW); + + // checkHostType, below, will open the meatball menu to read the "Split + // console" menu item label. However, if we've just opened a new window then + // on some platforms when we switch focus to the new window we might end up + // triggering the auto-close behavior on the menu popup. To avoid that, wait + // a moment before querying the menu. + await new Promise(resolve => requestIdleCallback(resolve)); + + checkHostType(Toolbox.HostType.WINDOW); + await checkToolboxUI(); + await toolbox.switchHost(Toolbox.HostType.BOTTOM); + + async function testConsoleLoadOnDifferentPanel() { + info("About to check console loads even when non-webconsole panel is open"); + + await openPanel("inspector"); + const webconsoleReady = toolbox.once("webconsole-ready"); + await toolbox.toggleSplitConsole(); + await webconsoleReady; + ok( + true, + "Webconsole has been triggered as loaded while another tool is active" + ); + } + + async function testKeyboardShortcuts() { + info("About to check that panel responds to ESCAPE keyboard shortcut"); + + const splitConsoleReady = toolbox.once("split-console"); + EventUtils.sendKey("ESCAPE", toolbox.win); + await splitConsoleReady; + ok(true, "Split console has been triggered via ESCAPE keypress"); + } + + async function checkAllTools() { + info("About to check split console with each panel individually."); + await openAndCheckPanel("jsdebugger"); + await openAndCheckPanel("inspector"); + await openAndCheckPanel("styleeditor"); + await openAndCheckPanel("performance"); + await openAndCheckPanel("netmonitor"); + + await checkWebconsolePanelOpened(); + } + + async function getCurrentUIState() { + const deck = toolbox.doc.querySelector("#toolbox-deck"); + const webconsolePanel = toolbox.webconsolePanel; + const splitter = toolbox.doc.querySelector("#toolbox-console-splitter"); + + const containerHeight = deck.parentNode.getBoundingClientRect().height; + const deckHeight = deck.getBoundingClientRect().height; + const webconsoleHeight = webconsolePanel.getBoundingClientRect().height; + const splitterVisibility = !splitter.getAttribute("hidden"); + // Splitter height will be 1px since the margin is negative. + const splitterHeight = splitterVisibility ? 1 : 0; + const openedConsolePanel = toolbox.currentToolId === "webconsole"; + const menuLabel = await getMenuLabel(toolbox); + + return { + deckHeight: deckHeight, + containerHeight: containerHeight, + webconsoleHeight: webconsoleHeight, + splitterVisibility: splitterVisibility, + splitterHeight: splitterHeight, + openedConsolePanel: openedConsolePanel, + menuLabel, + }; + } + + async function getMenuLabel() { + const button = toolbox.doc.getElementById("toolbox-meatball-menu-button"); + const onPopupShown = new Promise( + resolve => { + toolbox.doc.addEventListener("popupshown", () => resolve()); + }, + { once: true } + ); + info("Click on menu and wait for the popup to be visible"); + AccessibilityUtils.setEnv({ + // Toobox toolbar buttons are handled with arrow keys. + nonNegativeTabIndexRule: false, + }); + await EventUtils.sendMouseEvent({ type: "click" }, button); + AccessibilityUtils.resetEnv(); + await onPopupShown; + + const menuItem = toolbox.doc.getElementById( + "toolbox-meatball-menu-splitconsole" + ); + + // Return undefined if the menu item is not available + let label; + if (menuItem && menuItem.querySelector(".label")) { + label = + menuItem.querySelector(".label").textContent === + L10N.getStr("toolbox.meatballMenu.hideconsole.label") + ? "hide" + : "split"; + } + + // Wait for menu to close + const onPopupHide = new Promise(resolve => { + toolbox.doc.addEventListener( + "popuphidden", + () => { + resolve(label); + }, + { once: true } + ); + }); + info("Hit escape and wait for the popup to be closed"); + EventUtils.sendKey("ESCAPE", toolbox.win); + await onPopupHide; + + return label; + } + + async function checkWebconsolePanelOpened() { + info("About to check special cases when webconsole panel is open."); + + // Start with console split, so we can test for transition to main panel. + await toolbox.toggleSplitConsole(); + + let currentUIState = await getCurrentUIState(); + + ok( + currentUIState.splitterVisibility, + "Splitter is visible when console is split" + ); + ok( + currentUIState.deckHeight > 0, + "Deck has a height > 0 when console is split" + ); + ok( + currentUIState.webconsoleHeight > 0, + "Web console has a height > 0 when console is split" + ); + ok( + !currentUIState.openedConsolePanel, + "The console panel is not the current tool" + ); + is( + currentUIState.menuLabel, + "hide", + "The menu item indicates the console is split" + ); + + await openPanel("webconsole"); + currentUIState = await getCurrentUIState(); + + ok( + !currentUIState.splitterVisibility, + "Splitter is hidden when console is opened." + ); + is( + currentUIState.deckHeight, + 0, + "Deck has a height == 0 when console is opened." + ); + is( + currentUIState.webconsoleHeight, + currentUIState.containerHeight, + "Web console is full height." + ); + ok( + currentUIState.openedConsolePanel, + "The console panel is the current tool" + ); + is( + currentUIState.menuLabel, + undefined, + "The menu item is hidden when console is opened" + ); + + // Make sure splitting console does nothing while webconsole is opened + await toolbox.toggleSplitConsole(); + + currentUIState = await getCurrentUIState(); + + ok( + !currentUIState.splitterVisibility, + "Splitter is hidden when console is opened." + ); + is( + currentUIState.deckHeight, + 0, + "Deck has a height == 0 when console is opened." + ); + is( + currentUIState.webconsoleHeight, + currentUIState.containerHeight, + "Web console is full height." + ); + ok( + currentUIState.openedConsolePanel, + "The console panel is the current tool" + ); + is( + currentUIState.menuLabel, + undefined, + "The menu item is hidden when console is opened" + ); + + // Make sure that split state is saved after opening another panel + await openPanel("inspector"); + currentUIState = await getCurrentUIState(); + ok( + currentUIState.splitterVisibility, + "Splitter is visible when console is split" + ); + ok( + currentUIState.deckHeight > 0, + "Deck has a height > 0 when console is split" + ); + ok( + currentUIState.webconsoleHeight > 0, + "Web console has a height > 0 when console is split" + ); + ok( + !currentUIState.openedConsolePanel, + "The console panel is not the current tool" + ); + is( + currentUIState.menuLabel, + "hide", + "The menu item still indicates the console is split" + ); + + await toolbox.toggleSplitConsole(); + } + + async function checkToolboxUI() { + let currentUIState = await getCurrentUIState(); + + ok(!currentUIState.splitterVisibility, "Splitter is hidden by default"); + is( + currentUIState.deckHeight, + currentUIState.containerHeight, + "Deck has a height > 0 by default" + ); + is( + currentUIState.webconsoleHeight, + 0, + "Web console is collapsed by default" + ); + ok( + !currentUIState.openedConsolePanel, + "The console panel is not the current tool" + ); + is( + currentUIState.menuLabel, + "split", + "The menu item indicates the console is not split" + ); + + await toolbox.toggleSplitConsole(); + + currentUIState = await getCurrentUIState(); + + ok( + currentUIState.splitterVisibility, + "Splitter is visible when console is split" + ); + ok( + currentUIState.deckHeight > 0, + "Deck has a height > 0 when console is split" + ); + ok( + currentUIState.webconsoleHeight > 0, + "Web console has a height > 0 when console is split" + ); + is( + Math.round( + currentUIState.deckHeight + + currentUIState.webconsoleHeight + + currentUIState.splitterHeight + ), + currentUIState.containerHeight, + "Everything adds up to container height" + ); + ok( + !currentUIState.openedConsolePanel, + "The console panel is not the current tool" + ); + is( + currentUIState.menuLabel, + "hide", + "The menu item indicates the console is split" + ); + + await toolbox.toggleSplitConsole(); + + currentUIState = await getCurrentUIState(); + + ok(!currentUIState.splitterVisibility, "Splitter is hidden after toggling"); + is( + currentUIState.deckHeight, + currentUIState.containerHeight, + "Deck has a height > 0 after toggling" + ); + is( + currentUIState.webconsoleHeight, + 0, + "Web console is collapsed after toggling" + ); + ok( + !currentUIState.openedConsolePanel, + "The console panel is not the current tool" + ); + is( + currentUIState.menuLabel, + "split", + "The menu item indicates the console is not split" + ); + } + + async function openPanel(toolId) { + const target = await TargetFactory.forTab(gBrowser.selectedTab); + toolbox = await gDevTools.showToolbox(target, toolId); + } + + async function openAndCheckPanel(toolId) { + await openPanel(toolId); + await checkToolboxUI(toolbox.getCurrentPanel()); + } + + function checkHostType(hostType) { + is(toolbox.hostType, hostType, "host type is " + hostType); + + const pref = Services.prefs.getCharPref("devtools.toolbox.host"); + is(pref, hostType, "host pref is " + hostType); + } +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_split_close_button.js b/devtools/client/webconsole/test/browser/browser_webconsole_split_close_button.js new file mode 100644 index 0000000000..92a0137373 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_split_close_button.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf-8,<p>Web Console test for close button of " + + "split console"; + +add_task(async function() { + const toolbox = await openNewTabAndToolbox(TEST_URI, "inspector"); + + info("Check the split console toolbar has a close button."); + + const onSplitConsoleReady = toolbox.once("webconsole-ready"); + toolbox.toggleSplitConsole(); + await onSplitConsoleReady; + + let closeButton = getCloseButton(toolbox); + ok(closeButton, "The split console has close button."); + + info( + "Check we can reopen split console after closing split console by using " + + "the close button" + ); + + let onSplitConsoleChange = toolbox.once("split-console"); + closeButton.click(); + await onSplitConsoleChange; + ok(!toolbox.splitConsole, "The split console has been closed."); + + onSplitConsoleChange = toolbox.once("split-console"); + toolbox.toggleSplitConsole(); + await onSplitConsoleChange; + + ok(toolbox.splitConsole, "The split console has been displayed."); + closeButton = getCloseButton(toolbox); + ok(closeButton, "The split console has the close button after reopening."); + + info("Check the close button is not displayed on console panel."); + + await toolbox.selectTool("webconsole"); + closeButton = getCloseButton(toolbox); + ok(!closeButton, "The console panel should not have the close button."); + + info("The split console has the close button if back to the inspector."); + + await toolbox.selectTool("inspector"); + ok( + toolbox.splitConsole, + "The split console has been displayed with inspector." + ); + closeButton = getCloseButton(toolbox); + ok(closeButton, "The split console on the inspector has the close button."); +}); + +function getCloseButton(toolbox) { + const hud = toolbox.getPanel("webconsole").hud; + const doc = hud.ui.outputNode.ownerDocument; + return doc.getElementById("split-console-close-button"); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_split_escape_key.js b/devtools/client/webconsole/test/browser/browser_webconsole_split_escape_key.js new file mode 100644 index 0000000000..9e954e58e2 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_split_escape_key.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf-8,<p>Web Console test for splitting"; + +add_task(async function() { + info( + "Test various cases where the escape key should hide the split console." + ); + + const toolbox = await openNewTabAndToolbox(TEST_URI, "inspector"); + + info("Send ESCAPE key and wait for the split console to be displayed"); + + const onSplitConsoleReady = toolbox.once("webconsole-ready"); + toolbox.win.focus(); + EventUtils.sendKey("ESCAPE", toolbox.win); + await onSplitConsoleReady; + + const hud = toolbox.getPanel("webconsole").hud; + const jsterm = hud.jsterm; + ok(toolbox.splitConsole, "Split console is created."); + + info( + "Wait for the autocomplete to show suggestions for `document.location.`" + ); + const popup = jsterm.autocompletePopup; + const onPopupShown = popup.once("popup-opened"); + jsterm.focus(); + EventUtils.sendString("document.location."); + await onPopupShown; + + info( + "Send ESCAPE key and check that it only hides the autocomplete suggestions" + ); + + const onPopupClosed = popup.once("popup-closed"); + EventUtils.sendKey("ESCAPE", toolbox.win); + await onPopupClosed; + + ok(!popup.isOpen, "Auto complete popup is hidden."); + ok( + toolbox.splitConsole, + "Split console is open after hiding the autocomplete popup." + ); + + info("Send ESCAPE key again and check that now closes the splitconsole"); + const onSplitConsoleEvent = toolbox.once("split-console"); + EventUtils.sendKey("ESCAPE", toolbox.win); + await onSplitConsoleEvent; + + ok(!toolbox.splitConsole, "Split console is hidden."); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_split_focus.js b/devtools/client/webconsole/test/browser/browser_webconsole_split_focus.js new file mode 100644 index 0000000000..14d1912157 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_split_focus.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf-8,<p>Web Console test for splitting</p>"; + +add_task(async function() { + info( + "Test that the split console input is focused and restores the focus properly." + ); + + const toolbox = await openNewTabAndToolbox(TEST_URI, "inspector"); + ok(!toolbox.splitConsole, "Split console is hidden by default"); + + info("Focusing the search box before opening the split console"); + const inspector = toolbox.getPanel("inspector"); + inspector.searchBox.focus(); + + let activeElement = getActiveElement(inspector.panelDoc); + is(activeElement, inspector.searchBox, "Search box is focused"); + + await toolbox.openSplitConsole(); + + ok(toolbox.splitConsole, "Split console is now visible"); + + const { hud } = toolbox.getPanel("webconsole"); + ok(isInputFocused(hud), "Split console input is focused by default"); + + await toolbox.closeSplitConsole(); + + info( + "Making sure that the search box is refocused after closing the split console" + ); + activeElement = getActiveElement(inspector.panelDoc); + is(activeElement, inspector.searchBox, "Search box is focused"); +}); + +function getActiveElement(doc) { + let activeElement = doc.activeElement; + while (activeElement && activeElement.contentDocument) { + activeElement = activeElement.contentDocument.activeElement; + } + return activeElement; +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_split_persist.js b/devtools/client/webconsole/test/browser/browser_webconsole_split_persist.js new file mode 100644 index 0000000000..d8b24bf26b --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_split_persist.js @@ -0,0 +1,147 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the split console state is persisted. + +const { LocalizationHelper } = require("devtools/shared/l10n"); +const L10N = new LocalizationHelper( + "devtools/client/locales/toolbox.properties" +); + +const TEST_URI = + "data:text/html;charset=utf-8,<p>Web Console test for splitting</p>"; + +add_task(async function() { + info("Opening a tab while there is no user setting on split console pref"); + let toolbox = await openNewTabAndToolbox(TEST_URI, "inspector"); + ok(!toolbox.splitConsole, "Split console is hidden by default"); + ok( + !(await doesMenuSayHide(toolbox)), + "Split console menu item says split by default" + ); + + await toggleSplitConsoleWithEscape(toolbox); + ok(toolbox.splitConsole, "Split console is now visible."); + ok(await doesMenuSayHide(toolbox), "Split console menu item now says hide"); + ok(getVisiblePrefValue(), "Visibility pref is true"); + + is( + parseInt(getHeightPrefValue(), 10), + parseInt(toolbox.webconsolePanel.height, 10), + "Panel height matches the pref" + ); + toolbox.webconsolePanel.height = 200; + + await toolbox.destroy(); + + info( + "Opening a tab while there is a true user setting on split console pref" + ); + toolbox = await openNewTabAndToolbox(TEST_URI, "inspector"); + ok(toolbox.splitConsole, "Split console is visible by default."); + ok( + isInputFocused(toolbox.getPanel("webconsole").hud), + "Split console input is focused by default" + ); + ok( + await doesMenuSayHide(toolbox), + "Split console menu item initially says hide" + ); + is( + getHeightPrefValue(), + 200, + "Height is set based on panel height after closing" + ); + + toolbox.webconsolePanel.height = 1; + ok( + toolbox.webconsolePanel.clientHeight > 1, + "The actual height of the console is bound with a min height" + ); + + toolbox.webconsolePanel.height = 10000; + ok( + toolbox.webconsolePanel.clientHeight < 10000, + "The actual height of the console is bound with a max height" + ); + + await toggleSplitConsoleWithEscape(toolbox); + ok(!toolbox.splitConsole, "Split console is now hidden."); + ok( + !(await doesMenuSayHide(toolbox)), + "Split console menu item now says split" + ); + ok(!getVisiblePrefValue(), "Visibility pref is false"); + + await toolbox.destroy(); + + is( + getHeightPrefValue(), + 10000, + "Height is set based on panel height after closing" + ); + + info( + "Opening a tab while there is a false user setting on split " + + "console pref" + ); + toolbox = await openNewTabAndToolbox(TEST_URI, "inspector"); + + ok(!toolbox.splitConsole, "Split console is hidden by default."); + ok(!getVisiblePrefValue(), "Visibility pref is false"); + + await toolbox.destroy(); +}); + +function getVisiblePrefValue() { + return Services.prefs.getBoolPref("devtools.toolbox.splitconsoleEnabled"); +} + +function getHeightPrefValue() { + return Services.prefs.getIntPref("devtools.toolbox.splitconsoleHeight"); +} + +async function doesMenuSayHide(toolbox) { + const button = toolbox.doc.getElementById("toolbox-meatball-menu-button"); + await waitUntil( + () => toolbox.win.getComputedStyle(button).pointerEvents === "auto" + ); + return new Promise(resolve => { + EventUtils.synthesizeMouseAtCenter(button, {}, toolbox.win); + + toolbox.doc.addEventListener( + "popupshown", + () => { + const menuItem = toolbox.doc.getElementById( + "toolbox-meatball-menu-splitconsole" + ); + + const result = + menuItem && + menuItem.querySelector(".label") && + menuItem.querySelector(".label").textContent === + L10N.getStr("toolbox.meatballMenu.hideconsole.label"); + + toolbox.doc.addEventListener( + "popuphidden", + () => { + resolve(result); + }, + { once: true } + ); + EventUtils.synthesizeKey("KEY_Escape"); + }, + { once: true } + ); + }); +} + +function toggleSplitConsoleWithEscape(toolbox) { + const onceSplitConsole = toolbox.once("split-console"); + const toolboxWindow = toolbox.win; + toolboxWindow.focus(); + EventUtils.sendKey("ESCAPE", toolboxWindow); + return onceSplitConsole; +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_stacktrace_location_debugger_link.js b/devtools/client/webconsole/test/browser/browser_webconsole_stacktrace_location_debugger_link.js new file mode 100644 index 0000000000..cb937049d8 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_stacktrace_location_debugger_link.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that message source links for js errors and console API calls open in +// the jsdebugger when clicked. + +"use strict"; + +// There are shutdown issues for which multiple rejections are left uncaught. +// See bug 1018184 for resolving these issues. +PromiseTestUtils.allowMatchingRejectionsGlobally(/Component not initialized/); +PromiseTestUtils.allowMatchingRejectionsGlobally(/this\.worker is null/); + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/" + + "test-stacktrace-location-debugger-link.html"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const target = await TargetFactory.forTab(gBrowser.selectedTab); + const toolbox = gDevTools.getToolbox(target); + + await testOpenInDebugger(hud, toolbox, "console.trace()"); + await testOpenInDebugger(hud, toolbox, "myErrorObject"); +}); + +async function testOpenInDebugger(hud, toolbox, text) { + info(`Testing message with text "${text}"`); + const messageNode = await waitFor(() => findMessage(hud, text)); + const framesNode = await waitFor(() => messageNode.querySelector(".frames")); + + const frameNodes = framesNode.querySelectorAll(".frame"); + is( + frameNodes.length, + 3, + "The message does have the expected number of frames in the stacktrace" + ); + + for (const frameNode of frameNodes) { + await checkClickOnNode(hud, toolbox, frameNode); + + info("Selecting the console again"); + await toolbox.selectTool("webconsole"); + } +} + +async function checkClickOnNode(hud, toolbox, frameNode) { + info("checking click on node location"); + const onSourceInDebuggerOpened = once(hud, "source-in-debugger-opened"); + await EventUtils.sendMouseEvent( + { type: "mousedown" }, + frameNode.querySelector(".location") + ); + await onSourceInDebuggerOpened; + + const url = frameNode.querySelector(".filename").textContent; + const dbg = toolbox.getPanel("jsdebugger"); + is( + dbg._selectors.getSelectedSource(dbg._getState()).url, + url, + `Debugger is opened at expected source url (${url})` + ); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_stacktrace_mapped_location_debugger_link.js b/devtools/client/webconsole/test/browser/browser_webconsole_stacktrace_mapped_location_debugger_link.js new file mode 100644 index 0000000000..9d624bcecb --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_stacktrace_mapped_location_debugger_link.js @@ -0,0 +1,61 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that clicking on a location in a stacktrace for a source-mapped file displays its +// original source in the debugger. See Bug 1587839. + +"use strict"; + +requestLongerTimeout(2); + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/test/browser/" + + "test-console-stacktrace-mapped.html"; + +const TEST_ORIGINAL_FILENAME = "test-sourcemap-original.js"; + +const TEST_ORIGINAL_URI = + "http://example.com/browser/devtools/client/webconsole/test/browser/" + + TEST_ORIGINAL_FILENAME; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + info("Print a stacktrace"); + const onLoggedStacktrace = waitForMessage(hud, "console.trace"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.logTrace(); + }); + const { node } = await onLoggedStacktrace; + + info("Wait until the original frames are displayed"); + await waitFor(() => + Array.from(node.querySelectorAll(".stacktrace .filename")) + .map(frameEl => frameEl.textContent) + .includes(TEST_ORIGINAL_FILENAME) + ); + + info("Click on the frame."); + await EventUtils.sendMouseEvent( + { type: "mousedown" }, + node.querySelector(".stacktrace .location") + ); + + info("Wait for the Debugger panel to open."); + const toolbox = hud.toolbox; + await toolbox.getPanelWhenReady("jsdebugger"); + + const dbg = createDebuggerContext(toolbox); + + info("Wait for selected source"); + await waitForSelectedSource(dbg, TEST_ORIGINAL_URI); + await waitForSelectedLocation(dbg, 15); + + const pendingLocation = dbg.selectors.getPendingSelectedLocation(); + const { url, line } = pendingLocation; + + is(url, TEST_ORIGINAL_URI, "Debugger is open at the expected file"); + is(line, 15, "Debugger is open at the expected line"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_strict_mode_errors.js b/devtools/client/webconsole/test/browser/browser_webconsole_strict_mode_errors.js new file mode 100644 index 0000000000..e65e230414 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_strict_mode_errors.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check that "use strict" JS errors generate errors, not warnings. + +"use strict"; + +add_task(async function() { + const hud = await openNewTabAndConsole( + "data:text/html;charset=utf8,empty page" + ); + + await loadScriptURI("'use strict';var arguments;"); + await waitForError( + hud, + "SyntaxError: 'arguments' can't be defined or assigned to in strict mode code" + ); + + await loadScriptURI("'use strict';function f(a, a) {};"); + await waitForError(hud, "SyntaxError: duplicate formal argument a"); + + await loadScriptURI("'use strict';var o = {get p() {}};o.p = 1;"); + await waitForError(hud, 'TypeError: setting getter-only property "p"'); + + await loadScriptURI("'use strict';v = 1;"); + await waitForError( + hud, + "ReferenceError: assignment to undeclared variable v" + ); +}); + +async function waitForError(hud, text) { + await waitFor(() => findMessage(hud, text, ".message.error")); + ok(true, "Received expected error message"); +} + +function loadScriptURI(script) { + // On e10s, the exception is triggered in child process + // and is ignored by test harness + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } + const uri = "data:text/html;charset=utf8,<script>" + script + "</script>"; + return navigateTo(uri); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_string.js b/devtools/client/webconsole/test/browser/browser_webconsole_string.js new file mode 100644 index 0000000000..9b64fafa79 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_string.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/test/browser/test-console.html"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + info("Test that console.log with a string argument does not include quotes"); + let receivedMessages = waitForMessages({ + hud, + messages: [{ text: "stringLog" }], + }); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.stringLog(); + }); + await receivedMessages; + ok(true, "console.log result does not have quotes"); + + info( + "Test that console.log with empty string argument render <empty string>" + ); + receivedMessages = waitForMessages({ + hud, + messages: [{ text: "hello <empty string>" }], + }); + + await ContentTask.spawn(gBrowser.selectedBrowser, {}, function() { + const name = ""; + content.wrappedJSObject.console.log("hello", name); + }); + await receivedMessages; + ok(true, "console.log empty string argument renders as expected"); + + info( + "Test that log with object containing an empty string property renders as expected" + ); + receivedMessages = waitForMessages({ + hud, + messages: [{ text: `Object { a: "" }` }], + }); + + await ContentTask.spawn(gBrowser.selectedBrowser, {}, function() { + content.wrappedJSObject.console.log({ a: "" }); + }); + await receivedMessages; + ok(true, "object with empty string property renders as expected"); + + info("evaluating a string constant"); + let msg = await executeAndWaitForMessage( + hud, + '"string\\nconstant"', + "constant", + ".result" + ); + let body = msg.node.querySelector(".message-body"); + // On the other hand, a string constant result should be quoted, but + // newlines should be let through. + ok( + body.textContent.includes('"string\nconstant"'), + `found expected text - "${body.textContent}"` + ); + + info("evaluating an empty string constant"); + msg = await executeAndWaitForMessage(hud, '""', '""', ".result"); + body = msg.node.querySelector(".message-body"); + ok(body.textContent.includes('""'), `found expected text`); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_stubs_console_api.js b/devtools/client/webconsole/test/browser/browser_webconsole_stubs_console_api.js new file mode 100644 index 0000000000..a150ce17bc --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_stubs_console_api.js @@ -0,0 +1,334 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + STUBS_UPDATE_ENV, + createResourceWatcherForTab, + getStubFile, + getCleanedPacket, + getSerializedPacket, + writeStubsToFile, +} = require(`${CHROME_URL_ROOT}stub-generator-helpers`); + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html"; +const STUB_FILE = "consoleApi.js"; + +add_task(async function() { + const isStubsUpdate = env.get(STUBS_UPDATE_ENV) == "true"; + info(`${isStubsUpdate ? "Update" : "Check"} ${STUB_FILE}`); + + const generatedStubs = await generateConsoleApiStubs(); + + if (isStubsUpdate) { + await writeStubsToFile(env, STUB_FILE, generatedStubs); + ok(true, `${STUB_FILE} was updated`); + return; + } + const existingStubs = getStubFile(STUB_FILE); + const FAILURE_MSG = + "The consoleApi stubs file needs to be updated by running `" + + `mach test ${getCurrentTestFilePath()} --headless --setenv WEBCONSOLE_STUBS_UPDATE=true` + + "`"; + + if (generatedStubs.size !== existingStubs.rawPackets.size) { + ok(false, FAILURE_MSG); + return; + } + + let failed = false; + for (const [key, packet] of generatedStubs) { + const packetStr = getSerializedPacket(packet, { sortKeys: true }); + const existingPacketStr = getSerializedPacket( + existingStubs.rawPackets.get(key), + { sortKeys: true } + ); + + is(packetStr, existingPacketStr, `"${key}" packet has expected value`); + failed = failed || packetStr !== existingPacketStr; + } + + if (failed) { + ok(false, FAILURE_MSG); + } else { + ok(true, "Stubs are up to date"); + } + + await closeTabAndToolbox().catch(() => {}); +}); + +async function generateConsoleApiStubs() { + const stubs = new Map(); + + const tab = await addTab(TEST_URI); + const resourceWatcher = await createResourceWatcherForTab(tab); + + // The resource-watcher only supports a single call to watch/unwatch per + // instance, so we attach a unique watch callback, which will forward the + // resource to `handleConsoleMessage`, dynamically updated for each command. + let handleConsoleMessage = function() {}; + + const onConsoleMessage = resources => { + for (const resource of resources) { + handleConsoleMessage(resource); + } + }; + await resourceWatcher.watchResources( + [resourceWatcher.TYPES.CONSOLE_MESSAGE], + { + onAvailable: onConsoleMessage, + } + ); + + for (const { keys, code } of getCommands()) { + const received = new Promise(resolve => { + let i = 0; + handleConsoleMessage = async res => { + const callKey = keys[i]; + + stubs.set(callKey, getCleanedPacket(callKey, res)); + + if (++i === keys.length) { + resolve(); + } + }; + }); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [code], function( + subCode + ) { + const script = content.document.createElement("script"); + script.append( + content.document.createTextNode(`function triggerPacket() {${subCode}}`) + ); + content.document.body.append(script); + content.wrappedJSObject.triggerPacket(); + script.remove(); + }); + + await received; + } + + resourceWatcher.unwatchResources([resourceWatcher.TYPES.CONSOLE_MESSAGE], { + onAvailable: onConsoleMessage, + }); + + return stubs; +} + +function getCommands() { + const consoleApiCommands = [ + "console.log('foobar', 'test')", + "console.log(undefined)", + "console.warn('danger, will robinson!')", + "console.log(NaN)", + "console.log(null)", + "console.log('\u9f2c')", + "console.clear()", + "console.count('bar')", + "console.assert(false, {message: 'foobar'})", + "console.log('\xFA\u1E47\u0129\xE7\xF6d\xEA \u021B\u0115\u0219\u0165')", + "console.dirxml(window)", + "console.log('myarray', ['red', 'green', 'blue'])", + "console.log('myregex', /a.b.c/)", + "console.table(['red', 'green', 'blue']);", + "console.log('myobject', {red: 'redValue', green: 'greenValue', blue: 'blueValue'});", + "console.debug('debug message');", + "console.info('info message');", + "console.error('error message');", + ]; + + const consoleApi = consoleApiCommands.map(cmd => ({ + keys: [cmd], + code: cmd, + })); + + consoleApi.push( + { + keys: ["console.log('mymap')"], + code: ` + var map = new Map(); + map.set("key1", "value1"); + map.set("key2", "value2"); + console.log('mymap', map); + `, + }, + { + keys: ["console.log('myset')"], + code: ` + console.log('myset', new Set(["a", "b"])); + `, + }, + { + keys: ["console.trace()"], + code: ` + function testStacktraceFiltering() { + console.trace() + } + function foo() { + testStacktraceFiltering() + } + + foo() + `, + }, + { + keys: ["console.trace('bar', {'foo': 'bar'}, [1,2,3])"], + code: ` + function testStacktraceWithLog() { + console.trace('bar', {'foo': 'bar'}, [1,2,3]) + } + function foo() { + testStacktraceWithLog() + } + + foo() + `, + }, + { + keys: ['console.trace("%cHello%c|%cWorld")'], + code: ` + console.trace( + "%cHello%c|%cWorld", + "color:red", + "", + "color: blue" + ); + `, + }, + { + keys: [ + "console.time('bar')", + "timerAlreadyExists", + "console.timeLog('bar') - 1", + "console.timeLog('bar') - 2", + "console.timeEnd('bar')", + "timeEnd.timerDoesntExist", + "timeLog.timerDoesntExist", + ], + code: ` + console.time("bar"); + console.time("bar"); + console.timeLog("bar"); + console.timeLog("bar", "second call", {state: 1}); + console.timeEnd("bar"); + console.timeEnd("bar"); + console.timeLog("bar"); + `, + }, + { + keys: ["console.table('bar')"], + code: ` + console.table('bar'); + `, + }, + { + keys: ["console.table(['a', 'b', 'c'])"], + code: ` + console.table(['a', 'b', 'c']); + `, + }, + { + keys: ["console.group('bar')", "console.groupEnd('bar')"], + code: ` + console.group("bar"); + console.groupEnd(); + `, + }, + { + keys: ["console.groupCollapsed('foo')", "console.groupEnd('foo')"], + code: ` + console.groupCollapsed("foo"); + console.groupEnd(); + `, + }, + { + keys: ["console.group()", "console.groupEnd()"], + code: ` + console.group(); + console.groupEnd(); + `, + }, + { + keys: ["console.log(%cfoobar)"], + code: ` + console.log( + "%cfoo%cbar", + "color:blue; font-size:1.3em; background:url('http://example.com/test'); position:absolute; top:10px; ", + "color:red; line-height: 1.5; background:\\165rl('http://example.com/test')" + ); + `, + }, + { + keys: ['console.log("%cHello%c|%cWorld")'], + code: ` + console.log( + "%cHello%c|%cWorld", + "color:red", + "", + "color: blue" + ); + `, + }, + { + keys: ["console.group(%cfoo%cbar)", "console.groupEnd(%cfoo%cbar)"], + code: ` + console.group( + "%cfoo%cbar", + "color:blue;font-size:1.3em;background:url('http://example.com/test');position:absolute;top:10px", + "color:red;background:\\165rl('http://example.com/test')"); + console.groupEnd(); + `, + }, + { + keys: [ + "console.groupCollapsed(%cfoo%cbaz)", + "console.groupEnd(%cfoo%cbaz)", + ], + code: ` + console.groupCollapsed( + "%cfoo%cbaz", + "color:blue;font-size:1.3em;background:url('http://example.com/test');position:absolute;top:10px", + "color:red;background:\\165rl('http://example.com/test')"); + console.groupEnd(); + `, + }, + { + keys: ["console.dir({C, M, Y, K})"], + code: "console.dir({cyan: 'C', magenta: 'M', yellow: 'Y', black: 'K'});", + }, + { + keys: [ + "console.count | default: 1", + "console.count | default: 2", + "console.count | test counter: 1", + "console.count | test counter: 2", + "console.count | default: 3", + "console.count | clear", + "console.count | default: 4", + "console.count | test counter: 3", + "console.countReset | test counter: 0", + "console.countReset | counterDoesntExist", + ], + code: ` + console.count(); + console.count(); + console.count("test counter"); + console.count("test counter"); + console.count(); + console.clear(); + console.count(); + console.count("test counter"); + console.countReset("test counter"); + console.countReset("test counter"); + `, + }, + { + keys: ["console.log escaped characters"], + code: "console.log('hello \\nfrom \\rthe \\\"string world!')", + } + ); + return consoleApi; +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_stubs_css_message.js b/devtools/client/webconsole/test/browser/browser_webconsole_stubs_css_message.js new file mode 100644 index 0000000000..c511950280 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_stubs_css_message.js @@ -0,0 +1,123 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + STUBS_UPDATE_ENV, + createResourceWatcherForTab, + getCleanedPacket, + getStubFile, + writeStubsToFile, +} = require(`${CHROME_URL_ROOT}stub-generator-helpers`); + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/test/browser/stub-generators/test-css-message.html"; +const STUB_FILE = "cssMessage.js"; + +add_task(async function() { + const isStubsUpdate = env.get(STUBS_UPDATE_ENV) == "true"; + info(`${isStubsUpdate ? "Update" : "Check"} ${STUB_FILE}`); + + const generatedStubs = await generateCssMessageStubs(); + + if (isStubsUpdate) { + await writeStubsToFile(env, STUB_FILE, generatedStubs); + ok(true, `${STUB_FILE} was updated`); + return; + } + + const existingStubs = getStubFile(STUB_FILE); + const FAILURE_MSG = + "The cssMessage stubs file needs to be updated by running `" + + `mach test ${getCurrentTestFilePath()} --headless --setenv WEBCONSOLE_STUBS_UPDATE=true` + + "`"; + + if (generatedStubs.size !== existingStubs.stubPackets.size) { + ok(false, FAILURE_MSG); + return; + } + + let failed = false; + for (const [key, packet] of generatedStubs) { + const packetStr = JSON.stringify(packet, null, 2); + const existingPacketStr = JSON.stringify( + existingStubs.stubPackets.get(key), + null, + 2 + ); + is(packetStr, existingPacketStr, `"${key}" packet has expected value`); + failed = failed || packetStr !== existingPacketStr; + } + + if (failed) { + ok(false, FAILURE_MSG); + } else { + ok(true, "Stubs are up to date"); + } +}); + +async function generateCssMessageStubs() { + const stubs = new Map(); + + const tab = await addTab(TEST_URI); + const resourceWatcher = await createResourceWatcherForTab(tab); + + // The resource-watcher only supports a single call to watch/unwatch per + // instance, so we attach a unique watch callback, which will forward the + // resource to `handleErrorMessage`, dynamically updated for each command. + let handleCSSMessage = function() {}; + + const onCSSMessageAvailable = resources => { + for (const resource of resources) { + handleCSSMessage(resource); + } + }; + + await resourceWatcher.watchResources([resourceWatcher.TYPES.CSS_MESSAGE], { + onAvailable: onCSSMessageAvailable, + }); + + for (const code of getCommands()) { + const received = new Promise(resolve => { + handleCSSMessage = function(packet) { + const key = packet.pageError.errorMessage; + stubs.set(key, getCleanedPacket(key, packet)); + resolve(); + }; + }); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [code], function( + subCode + ) { + content.docShell.cssErrorReportingEnabled = true; + const style = content.document.createElement("style"); + style.append(content.document.createTextNode(subCode)); + content.document.body.append(style); + }); + + await received; + } + + resourceWatcher.unwatchResources([resourceWatcher.TYPES.CSS_MESSAGE], { + onAvailable: onCSSMessageAvailable, + }); + + await closeTabAndToolbox().catch(() => {}); + return stubs; +} + +function getCommands() { + return [ + ` + p { + such-unknown-property: wow; + } + `, + ` + p { + padding-top: invalid value; + } + `, + ]; +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_stubs_evaluation_result.js b/devtools/client/webconsole/test/browser/browser_webconsole_stubs_evaluation_result.js new file mode 100644 index 0000000000..57783b7af0 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_stubs_evaluation_result.js @@ -0,0 +1,120 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + STUBS_UPDATE_ENV, + getCleanedPacket, + getSerializedPacket, + getStubFile, + writeStubsToFile, +} = require(`${CHROME_URL_ROOT}stub-generator-helpers`); + +const TEST_URI = "data:text/html;charset=utf-8,stub generation"; +const STUB_FILE = "evaluationResult.js"; + +add_task(async function() { + const isStubsUpdate = env.get(STUBS_UPDATE_ENV) == "true"; + info(`${isStubsUpdate ? "Update" : "Check"} ${STUB_FILE}`); + + const generatedStubs = await generateEvaluationResultStubs(); + + if (isStubsUpdate) { + await writeStubsToFile(env, STUB_FILE, generatedStubs); + ok(true, `${STUB_FILE} was updated`); + return; + } + + const existingStubs = getStubFile(STUB_FILE); + const FAILURE_MSG = + "The evaluationResult stubs file needs to be updated by running `" + + `mach test ${getCurrentTestFilePath()} --headless --setenv WEBCONSOLE_STUBS_UPDATE=true` + + "`"; + + if (generatedStubs.size !== existingStubs.rawPackets.size) { + ok(false, FAILURE_MSG); + return; + } + + let failed = false; + for (const [key, packet] of generatedStubs) { + const packetStr = getSerializedPacket(packet, { sortKeys: true }); + const existingPacketStr = getSerializedPacket( + existingStubs.rawPackets.get(key), + { sortKeys: true } + ); + is(packetStr, existingPacketStr, `"${key}" packet has expected value`); + failed = failed || packetStr !== existingPacketStr; + } + + if (failed) { + ok(false, FAILURE_MSG); + } else { + ok(true, "Stubs are up to date"); + } + + await closeTabAndToolbox(); +}); + +async function generateEvaluationResultStubs() { + const stubs = new Map(); + const toolbox = await openNewTabAndToolbox(TEST_URI, "webconsole"); + const webConsoleFront = await toolbox.target.getFront("console"); + for (const [key, code] of getCommands()) { + const packet = await webConsoleFront.evaluateJSAsync(code); + stubs.set(key, getCleanedPacket(key, packet)); + } + + return stubs; +} + +function getCommands() { + const evaluationResultCommands = [ + "new Date(0)", + "asdf()", + "1 + @", + "inspect({a: 1})", + "undefined", + ]; + + const evaluationResult = new Map( + evaluationResultCommands.map(cmd => [cmd, cmd]) + ); + evaluationResult.set( + "longString message Error", + `throw new Error("Long error ".repeat(10000))` + ); + + evaluationResult.set(`eval throw ""`, `throw ""`); + evaluationResult.set(`eval throw "tomato"`, `throw "tomato"`); + evaluationResult.set(`eval throw false`, `throw false`); + evaluationResult.set(`eval throw 0`, `throw 0`); + evaluationResult.set(`eval throw null`, `throw null`); + evaluationResult.set(`eval throw undefined`, `throw undefined`); + evaluationResult.set(`eval throw Symbol`, `throw Symbol("potato")`); + evaluationResult.set(`eval throw Object`, `throw {vegetable: "cucumber"}`); + evaluationResult.set(`eval throw Error Object`, `throw new Error("pumpkin")`); + evaluationResult.set( + `eval throw Error Object with custom name`, + ` + var err = new Error("pineapple"); + err.name = "JuicyError"; + err.flavor = "delicious"; + throw err; + ` + ); + evaluationResult.set(`eval pending promise`, `new Promise(() => {})`); + evaluationResult.set(`eval Promise.resolve`, `Promise.resolve(123)`); + evaluationResult.set(`eval Promise.reject`, `Promise.reject("ouch")`); + evaluationResult.set( + `eval resolved promise`, + `Promise.resolve().then(() => 246)` + ); + evaluationResult.set( + `eval rejected promise`, + `Promise.resolve().then(() => a.b.c)` + ); + + return evaluationResult; +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_stubs_network_event.js b/devtools/client/webconsole/test/browser/browser_webconsole_stubs_network_event.js new file mode 100644 index 0000000000..8eeea50d23 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_stubs_network_event.js @@ -0,0 +1,216 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + createResourceWatcherForTab, + STUBS_UPDATE_ENV, + getStubFile, + getCleanedPacket, + writeStubsToFile, +} = require(`${CHROME_URL_ROOT}stub-generator-helpers`); + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/test/browser/stub-generators/test-network-event.html"; +const STUB_FILE = "networkEvent.js"; + +add_task(async function() { + const isStubsUpdate = env.get(STUBS_UPDATE_ENV) == "true"; + info(`${isStubsUpdate ? "Update" : "Check"} ${STUB_FILE}`); + + const generatedStubs = await generateNetworkEventStubs(); + + if (isStubsUpdate) { + await writeStubsToFile(env, STUB_FILE, generatedStubs, true); + ok(true, `${STUB_FILE} was updated`); + return; + } + + const existingStubs = getStubFile(STUB_FILE); + const FAILURE_MSG = + "The network event stubs file needs to be updated by running `" + + `mach test ${getCurrentTestFilePath()} --headless --setenv WEBCONSOLE_STUBS_UPDATE=true` + + "`"; + + if (generatedStubs.size !== existingStubs.stubPackets.size) { + ok(false, FAILURE_MSG); + return; + } + + let failed = false; + for (const [key, packet] of generatedStubs) { + const existingPacket = existingStubs.stubPackets.get(key); + const packetStr = JSON.stringify(packet, null, 2); + const existingPacketStr = JSON.stringify(existingPacket, null, 2); + is(packetStr, existingPacketStr, `"${key}" packet has expected value`); + failed = failed || packetStr !== existingPacketStr; + } + + if (failed) { + ok(false, FAILURE_MSG); + } else { + ok(true, "Stubs are up to date"); + } +}); + +async function generateNetworkEventStubs() { + const stubs = new Map(); + const tab = await addTab(TEST_URI); + const resourceWatcher = await createResourceWatcherForTab(tab); + const stacktraces = new Map(); + let addNetworkStub = function() {}; + let addNetworkUpdateStub = function() {}; + + const onAvailable = resources => { + for (const resource of resources) { + if (resource.resourceType == resourceWatcher.TYPES.NETWORK_EVENT) { + if (stacktraces.has(resource.channelId)) { + const { stacktraceAvailable, lastFrame } = stacktraces.get( + resource.channelId + ); + resource.cause.stacktraceAvailable = stacktraceAvailable; + resource.cause.lastFrame = lastFrame; + stacktraces.delete(resource.channelId); + } + addNetworkStub(resource); + continue; + } + if ( + resource.resourceType == resourceWatcher.TYPES.NETWORK_EVENT_STACKTRACE + ) { + stacktraces.set(resource.channelId, resource); + } + } + }; + const onUpdated = updates => { + for (const { resource } of updates) { + addNetworkUpdateStub(resource); + } + }; + + await resourceWatcher.watchResources( + [ + resourceWatcher.TYPES.NETWORK_EVENT_STACKTRACE, + resourceWatcher.TYPES.NETWORK_EVENT, + ], + { + onAvailable, + onUpdated, + } + ); + + for (const [key, code] of getCommands()) { + const networkEventDone = new Promise(resolve => { + addNetworkStub = resource => { + stubs.set(key, getCleanedPacket(key, getOrderedResource(resource))); + resolve(); + }; + }); + const networkEventUpdateDone = new Promise(resolve => { + addNetworkUpdateStub = resource => { + const updateKey = `${key} update`; + stubs.set(key, getCleanedPacket(key, getOrderedResource(resource))); + stubs.set( + updateKey, + // We cannot ensure the form of the resource, some properties + // might be in another order than in the original resource. + // Hand-picking only what we need should prevent this. + getCleanedPacket(updateKey, getOrderedResource(resource)) + ); + resolve(); + }; + }); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [code], function( + subCode + ) { + const script = content.document.createElement("script"); + script.append( + content.document.createTextNode(`function triggerPacket() {${subCode}}`) + ); + content.document.body.append(script); + content.wrappedJSObject.triggerPacket(); + script.remove(); + }); + await Promise.all([networkEventDone, networkEventUpdateDone]); + } + resourceWatcher.unwatchResources( + [ + resourceWatcher.TYPES.NETWORK_EVENT_STACKTRACE, + resourceWatcher.TYPES.NETWORK_EVENT, + ], + { + onAvailable, + onUpdated, + } + ); + return stubs; +} +// Ensures the order of the resource properties +function getOrderedResource(resource) { + return { + resourceType: resource.resourceType, + timeStamp: resource.timeStamp, + actor: resource.actor, + startedDateTime: resource.startedDateTime, + method: resource.method, + url: resource.url, + isXHR: resource.isXHR, + cause: resource.cause, + httpVersion: resource.httpVersion, + status: resource.status, + statusText: resource.statusText, + headersSize: resource.headersSize, + remoteAddress: resource.remoteAddress, + remotePort: resource.remotePort, + mimeType: resource.mimeType, + waitingTime: resource.waitingTime, + contentSize: resource.contentSize, + transferredSize: resource.transferredSize, + timings: resource.timings, + private: resource.private, + fromCache: resource.fromCache, + fromServiceWorker: resource.fromServiceWorker, + isThirdPartyTrackingResource: resource.isThirdPartyTrackingResource, + referrerPolicy: resource.referrerPolicy, + blockedReason: resource.blockedReason, + blockingExtension: resource.blockingExtension, + channelId: resource.channelId, + totalTime: resource.totalTime, + securityState: resource.securityState, + responseCache: resource.responseCache, + isRacing: resource.isRacing, + }; +} + +function getCommands() { + const networkEvent = new Map(); + + networkEvent.set( + "GET request", + ` +let i = document.createElement("img"); +i.src = "/inexistent.html"; +` + ); + + networkEvent.set( + "XHR GET request", + ` +const xhr = new XMLHttpRequest(); +xhr.open("GET", "/inexistent.html"); +xhr.send(); +` + ); + + networkEvent.set( + "XHR POST request", + ` +const xhr = new XMLHttpRequest(); +xhr.open("POST", "/inexistent.html"); +xhr.send(); +` + ); + return networkEvent; +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_stubs_page_error.js b/devtools/client/webconsole/test/browser/browser_webconsole_stubs_page_error.js new file mode 100644 index 0000000000..6b13a9c5eb --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_stubs_page_error.js @@ -0,0 +1,197 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + STUBS_UPDATE_ENV, + createResourceWatcherForTab, + getCleanedPacket, + getSerializedPacket, + getStubFile, + writeStubsToFile, +} = require(`${CHROME_URL_ROOT}stub-generator-helpers`); + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html"; +const STUB_FILE = "pageError.js"; + +add_task(async function() { + await pushPref("javascript.options.asyncstack_capture_debuggee_only", false); + + const isStubsUpdate = env.get(STUBS_UPDATE_ENV) == "true"; + info(`${isStubsUpdate ? "Update" : "Check"} ${STUB_FILE}`); + + const generatedStubs = await generatePageErrorStubs(); + + if (isStubsUpdate) { + await writeStubsToFile(env, STUB_FILE, generatedStubs); + ok(true, `${STUB_FILE} was updated`); + return; + } + + const existingStubs = getStubFile(STUB_FILE); + const FAILURE_MSG = + "The pageError stubs file needs to be updated by running `" + + `mach test ${getCurrentTestFilePath()} --headless --setenv WEBCONSOLE_STUBS_UPDATE=true` + + "`"; + + if (generatedStubs.size !== existingStubs.rawPackets.size) { + ok(false, FAILURE_MSG); + return; + } + + let failed = false; + for (const [key, packet] of generatedStubs) { + const packetStr = getSerializedPacket(packet, { sortKeys: true }); + const existingPacketStr = getSerializedPacket( + existingStubs.rawPackets.get(key), + { sortKeys: true } + ); + is(packetStr, existingPacketStr, `"${key}" packet has expected value`); + failed = failed || packetStr !== existingPacketStr; + } + + if (failed) { + ok(false, FAILURE_MSG); + } else { + ok(true, "Stubs are up to date"); + } + + await closeTabAndToolbox(); +}); + +async function generatePageErrorStubs() { + const stubs = new Map(); + + const tab = await addTab(TEST_URI); + const resourceWatcher = await createResourceWatcherForTab(tab); + + // The resource-watcher only supports a single call to watch/unwatch per + // instance, so we attach a unique watch callback, which will forward the + // resource to `handleErrorMessage`, dynamically updated for each command. + let handleErrorMessage = function() {}; + + const onErrorMessageAvailable = resources => { + for (const resource of resources) { + handleErrorMessage(resource); + } + }; + await resourceWatcher.watchResources([resourceWatcher.TYPES.ERROR_MESSAGE], { + onAvailable: onErrorMessageAvailable, + }); + + for (const [key, code] of getCommands()) { + const onPageError = new Promise(resolve => { + handleErrorMessage = packet => resolve(packet); + }); + + // On e10s, the exception is triggered in child process + // and is ignored by test harness + // expectUncaughtException should be called for each uncaught exception. + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } + + // Note: This needs to use ContentTask rather than SpecialPowers.spawn + // because the latter includes cross-process stack information. + await ContentTask.spawn(gBrowser.selectedBrowser, code, function(subCode) { + const script = content.document.createElement("script"); + script.append(content.document.createTextNode(subCode)); + content.document.body.append(script); + script.remove(); + }); + + const packet = await onPageError; + stubs.set(key, getCleanedPacket(key, packet)); + } + + return stubs; +} + +function getCommands() { + const pageError = new Map(); + + pageError.set( + "ReferenceError: asdf is not defined", + ` + function bar() { + asdf() + } + function foo() { + bar() + } + + foo() +` + ); + + pageError.set( + "SyntaxError: redeclaration of let a", + ` + let a, a; +` + ); + + pageError.set( + "TypeError longString message", + `throw new Error("Long error ".repeat(10000))` + ); + + const evilDomain = `https://evil.com/?`; + const badDomain = `https://not-so-evil.com/?`; + const paramLength = 200; + const longParam = "a".repeat(paramLength); + + const evilURL = `${evilDomain}${longParam}`; + const badURL = `${badDomain}${longParam}`; + + pageError.set( + `throw string with URL`, + `throw "“${evilURL}“ is evil and “${badURL}“ is not good either"` + ); + + pageError.set(`throw ""`, `throw ""`); + pageError.set(`throw "tomato"`, `throw "tomato"`); + pageError.set(`throw false`, `throw false`); + pageError.set(`throw 0`, `throw 0`); + pageError.set(`throw null`, `throw null`); + pageError.set(`throw undefined`, `throw undefined`); + pageError.set(`throw Symbol`, `throw Symbol("potato")`); + pageError.set(`throw Object`, `throw {vegetable: "cucumber"}`); + pageError.set(`throw Error Object`, `throw new Error("pumpkin")`); + pageError.set( + `throw Error Object with custom name`, + ` + var err = new Error("pineapple"); + err.name = "JuicyError"; + err.flavor = "delicious"; + throw err; + ` + ); + pageError.set(`Promise reject ""`, `Promise.reject("")`); + pageError.set(`Promise reject "tomato"`, `Promise.reject("tomato")`); + pageError.set(`Promise reject false`, `Promise.reject(false)`); + pageError.set(`Promise reject 0`, `Promise.reject(0)`); + pageError.set(`Promise reject null`, `Promise.reject(null)`); + pageError.set(`Promise reject undefined`, `Promise.reject(undefined)`); + pageError.set(`Promise reject Symbol`, `Promise.reject(Symbol("potato"))`); + pageError.set( + `Promise reject Object`, + `Promise.reject({vegetable: "cucumber"})` + ); + pageError.set( + `Promise reject Error Object`, + `Promise.reject(new Error("pumpkin"))` + ); + pageError.set( + `Promise reject Error Object with custom name`, + ` + var err = new Error("pineapple"); + err.name = "JuicyError"; + err.flavor = "delicious"; + Promise.reject(err); + ` + ); + return pageError; +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_stubs_platform_messages.js b/devtools/client/webconsole/test/browser/browser_webconsole_stubs_platform_messages.js new file mode 100644 index 0000000000..1861a870fa --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_stubs_platform_messages.js @@ -0,0 +1,117 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + STUBS_UPDATE_ENV, + createResourceWatcherForTarget, + getCleanedPacket, + getSerializedPacket, + getStubFile, + writeStubsToFile, +} = require(`${CHROME_URL_ROOT}stub-generator-helpers`); + +const STUB_FILE = "platformMessage.js"; + +add_task(async function() { + const isStubsUpdate = env.get(STUBS_UPDATE_ENV) == "true"; + info(`${isStubsUpdate ? "Update" : "Check"} ${STUB_FILE}`); + + const generatedStubs = await generatePlatformMessagesStubs(); + + if (isStubsUpdate) { + await writeStubsToFile(env, STUB_FILE, generatedStubs); + ok(true, `${STUB_FILE} was updated`); + return; + } + + const existingStubs = getStubFile(STUB_FILE); + + const FAILURE_MSG = + "The platformMessage stubs file needs to be updated by running `" + + `mach test ${getCurrentTestFilePath()} --headless --setenv WEBCONSOLE_STUBS_UPDATE=true` + + "`"; + + if (generatedStubs.size !== existingStubs.rawPackets.size) { + ok(false, FAILURE_MSG); + return; + } + + let failed = false; + for (const [key, packet] of generatedStubs) { + const packetStr = getSerializedPacket(packet, { sortKeys: true }); + const existingPacketStr = getSerializedPacket( + existingStubs.rawPackets.get(key), + { sortKeys: true } + ); + is(packetStr, existingPacketStr, `"${key}" packet has expected value`); + failed = failed || packetStr !== existingPacketStr; + } + + if (failed) { + ok(false, FAILURE_MSG); + } else { + ok(true, "Stubs are up to date"); + } +}); + +async function generatePlatformMessagesStubs() { + const stubs = new Map(); + + // Instantiate a minimal server + const { DevToolsClient } = require("devtools/client/devtools-client"); + const { DevToolsServer } = require("devtools/server/devtools-server"); + DevToolsServer.init(); + DevToolsServer.allowChromeProcess = true; + if (!DevToolsServer.createRootActor) { + DevToolsServer.registerAllActors(); + } + const transport = DevToolsServer.connectPipe(); + const client = new DevToolsClient(transport); + await client.connect(); + const mainProcessDescriptor = await client.mainRoot.getMainProcess(); + const target = await mainProcessDescriptor.getTarget(); + + const resourceWatcher = await createResourceWatcherForTarget(target); + + // The resource-watcher only supports a single call to watch/unwatch per + // instance, so we attach a unique watch callback, which will forward the + // resource to `handlePlatformMessage`, dynamically updated for each command. + let handlePlatformMessage = function() {}; + + const onPlatformMessageAvailable = resources => { + for (const resource of resources) { + handlePlatformMessage(resource); + } + }; + await resourceWatcher.watchResources( + [resourceWatcher.TYPES.PLATFORM_MESSAGE], + { + onAvailable: onPlatformMessageAvailable, + } + ); + + for (const [key, string] of getPlatformMessages()) { + const onPlatformMessage = new Promise(resolve => { + handlePlatformMessage = resolve; + }); + + Services.console.logStringMessage(string); + + const packet = await onPlatformMessage; + stubs.set(key, getCleanedPacket(key, packet)); + } + + resourceWatcher.targetList.destroy(); + await client.close(); + + return stubs; +} + +function getPlatformMessages() { + return new Map([ + ["platform-simple-message", "foobar test"], + ["platform-longString-message", `a\n${"a".repeat(20000)}`], + ]); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_telemetry_execute_js.js b/devtools/client/webconsole/test/browser/browser_webconsole_telemetry_execute_js.js new file mode 100644 index 0000000000..a64d129c18 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_telemetry_execute_js.js @@ -0,0 +1,95 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Tests that the console record the execute_js telemetry event with expected data +// when evaluating expressions. + +"use strict"; + +const TEST_URI = `data:text/html,<meta charset=utf8>Test execute_js telemetry event`; +const ALL_CHANNELS = Ci.nsITelemetry.DATASET_ALL_CHANNELS; + +add_task(async function() { + // Let's reset the counts. + Services.telemetry.clearEvents(); + + // Ensure no events have been logged + const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true); + ok(!snapshot.parent, "No events have been logged for the main process"); + + const hud = await openNewTabAndConsole(TEST_URI); + + info("Evaluate a single line"); + await keyboardExecuteAndWaitForMessage(hud, `"single line"`, "", ".result"); + + info("Evaluate another single line"); + await keyboardExecuteAndWaitForMessage(hud, `"single line 2"`, "", ".result"); + + info("Evaluate multiple lines"); + await keyboardExecuteAndWaitForMessage(hud, `"n"\n.trim()`, "", ".result"); + + info("Switch to editor mode"); + await toggleLayout(hud); + + info("Evaluate a single line in editor mode"); + await keyboardExecuteAndWaitForMessage(hud, `"single line 3"`, "", ".result"); + + info("Evaluate multiple lines in editor mode"); + await keyboardExecuteAndWaitForMessage( + hud, + `"y"\n.trim()\n.trim()`, + "", + ".result" + ); + + info("Evaluate multiple lines again in editor mode"); + await keyboardExecuteAndWaitForMessage(hud, `"x"\n.trim()`, "", ".result"); + + checkEventTelemetry([ + getTelemetryEventData({ lines: 1, input: "inline" }), + getTelemetryEventData({ lines: 1, input: "inline" }), + getTelemetryEventData({ lines: 2, input: "inline" }), + getTelemetryEventData({ lines: 1, input: "multiline" }), + getTelemetryEventData({ lines: 3, input: "multiline" }), + getTelemetryEventData({ lines: 2, input: "multiline" }), + ]); + + info("Switch back to inline mode"); + await toggleLayout(hud); +}); + +function checkEventTelemetry(expectedData) { + const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true); + const events = snapshot.parent.filter( + event => + event[1] === "devtools.main" && + event[2] === "execute_js" && + event[3] === "webconsole" && + event[4] === null + ); + + for (const [i, expected] of expectedData.entries()) { + const [timestamp, category, method, object, value, extra] = events[i]; + + // ignore timestamp + ok(timestamp > 0, "timestamp is greater than 0"); + is(category, expected.category, "'category' is correct"); + is(method, expected.method, "'method' is correct"); + is(object, expected.object, "'object' is correct"); + is(value, expected.value, "'value' is correct"); + is(parseInt(extra.lines, 10), expected.extra.lines, "'lines' is correct"); + is(extra.input, expected.extra.input, "'input' is correct"); + } +} + +function getTelemetryEventData(extra) { + return { + timestamp: null, + category: "devtools.main", + method: "execute_js", + object: "webconsole", + value: null, + extra, + }; +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_telemetry_filters_changed.js b/devtools/client/webconsole/test/browser/browser_webconsole_telemetry_filters_changed.js new file mode 100644 index 0000000000..d447be6fe2 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_telemetry_filters_changed.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests the filters_changed telemetry event. + +"use strict"; + +const TEST_URI = `data:text/html,<meta charset=utf8><script> + console.log("test message"); +</script>`; + +const ALL_CHANNELS = Ci.nsITelemetry.DATASET_ALL_CHANNELS; + +add_task(async function() { + // Let's reset the counts. + Services.telemetry.clearEvents(); + + // Ensure no events have been logged + const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true); + ok(!snapshot.parent, "No events have been logged for the main process"); + + const hud = await openNewTabAndConsole(TEST_URI); + + info("Click on the 'log' filter"); + await setFilterState(hud, { + log: false, + }); + + checkTelemetryEvent({ + trigger: "log", + active: "error,warn,info,debug", + inactive: "text,log,css,net,netxhr", + }); + + info("Click on the 'netxhr' filter"); + await setFilterState(hud, { + netxhr: true, + }); + + checkTelemetryEvent({ + trigger: "netxhr", + active: "error,warn,info,debug,netxhr", + inactive: "text,log,css,net", + }); + + info("Filter the output using the text filter input"); + await setFilterState(hud, { text: "no match" }); + + checkTelemetryEvent({ + trigger: "text", + active: "text,error,warn,info,debug,netxhr", + inactive: "log,css,net", + }); +}); + +function checkTelemetryEvent(expectedEvent) { + const events = getFiltersChangedEventsExtra(); + is(events.length, 1, "There was only 1 event logged"); + const [event] = events; + ok(event.session_id > 0, "There is a valid session_id in the logged event"); + const f = e => JSON.stringify(e, null, 2); + is( + f(event), + f({ + ...expectedEvent, + session_id: event.session_id, + }), + "The event has the expected data" + ); +} + +function getFiltersChangedEventsExtra() { + // Retrieve and clear telemetry events. + const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true); + + const filtersChangedEvents = snapshot.parent.filter( + event => + event[1] === "devtools.main" && + event[2] === "filters_changed" && + event[3] === "webconsole" + ); + + // Since we already know we have the correct event, we only return the `extra` field + // that was passed to it (which is event[5] here). + return filtersChangedEvents.map(event => event[5]); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_telemetry_js_errors.js b/devtools/client/webconsole/test/browser/browser_webconsole_telemetry_js_errors.js new file mode 100644 index 0000000000..190f8c8486 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_telemetry_js_errors.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests the DEVTOOLS_JAVASCRIPT_ERROR_DISPLAYED telemetry event. + +"use strict"; + +const TEST_URI = `data:text/html,<meta charset=utf8><script>document()</script>`; + +add_task(async function() { + startTelemetry(); + + const hud = await openNewTabAndConsole(TEST_URI); + + info( + "Check that the error message is logged in telemetry with the expected key" + ); + await waitFor(() => findMessage(hud, "is not a function")); + checkErrorDisplayedTelemetry("JSMSG_NOT_FUNCTION", 1); + + await refreshTab(); + + info("Reloading the page (and having the same error) increments the sum"); + await waitFor(() => findMessage(hud, "is not a function")); + checkErrorDisplayedTelemetry("JSMSG_NOT_FUNCTION", 2); + + info( + "Evaluating an expression resulting in the same error increments the sum" + ); + await executeAndWaitForMessage(hud, "window()", "window is not a function"); + checkErrorDisplayedTelemetry("JSMSG_NOT_FUNCTION", 3); + + info( + "Evaluating an expression resulting in another error is logged in telemetry" + ); + await executeAndWaitForMessage( + hud, + `"a".repeat(-1)`, + "repeat count must be non-negative" + ); + checkErrorDisplayedTelemetry("JSMSG_NEGATIVE_REPETITION_COUNT", 1); + + await executeAndWaitForMessage( + hud, + `"b".repeat(-1)`, + "repeat count must be non-negative" + ); + checkErrorDisplayedTelemetry("JSMSG_NEGATIVE_REPETITION_COUNT", 2); +}); + +function checkErrorDisplayedTelemetry(key, count) { + checkTelemetry( + "DEVTOOLS_JAVASCRIPT_ERROR_DISPLAYED", + key, + { 0: 0, 1: count, 2: 0 }, + "array" + ); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_telemetry_jump_to_definition.js b/devtools/client/webconsole/test/browser/browser_webconsole_telemetry_jump_to_definition.js new file mode 100644 index 0000000000..3f7e2bed7e --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_telemetry_jump_to_definition.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests the jump_to_definition telemetry event. + +"use strict"; + +const TEST_URI = `data:text/html,<meta charset=utf8><script> + function x(){} + console.log("test message", x); +</script>`; + +const ALL_CHANNELS = Ci.nsITelemetry.DATASET_ALL_CHANNELS; + +add_task(async function() { + // Let's reset the counts. + Services.telemetry.clearEvents(); + + // Ensure no events have been logged + const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true); + ok(!snapshot.parent, "No events have been logged for the main process"); + + const hud = await openNewTabAndConsole(TEST_URI); + + const message = await waitFor(() => findMessage(hud, "test message")); + info("Click on the 'jump to definition' button"); + const jumpIcon = message.querySelector(".jump-definition"); + jumpIcon.click(); + + const events = getJumpToDefinitionEventsExtra(); + is(events.length, 1, "There was 1 event logged"); + const [event] = events; + ok(event.session_id > 0, "There is a valid session_id in the logged event"); +}); + +function getJumpToDefinitionEventsExtra() { + // Retrieve and clear telemetry events. + const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true); + + const events = snapshot.parent.filter( + event => + event[1] === "devtools.main" && + event[2] === "jump_to_definition" && + event[3] === "webconsole" + ); + + // Since we already know we have the correct event, we only return the `extra` field + // that was passed to it (which is event[5] here). + return events.map(event => event[5]); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_telemetry_object_expanded.js b/devtools/client/webconsole/test/browser/browser_webconsole_telemetry_object_expanded.js new file mode 100644 index 0000000000..85a7d27c0b --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_telemetry_object_expanded.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests the object_expanded telemetry event. + +"use strict"; + +const TEST_URI = `data:text/html,<meta charset=utf8><script> + console.log("test message", [1,2,3]); +</script>`; + +const ALL_CHANNELS = Ci.nsITelemetry.DATASET_ALL_CHANNELS; + +add_task(async function() { + // Let's reset the counts. + Services.telemetry.clearEvents(); + + // Ensure no events have been logged + const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true); + ok(!snapshot.parent, "No events have been logged for the main process"); + + const hud = await openNewTabAndConsole(TEST_URI); + + const message = await waitFor(() => findMessage(hud, "test message")); + + info("Click on the arrow icon to expand the node"); + const arrowIcon = message.querySelector(".arrow"); + arrowIcon.click(); + + // let's wait until we have 2 arrows (i.e. the object was expanded) + await waitFor(() => message.querySelectorAll(".arrow").length === 2); + + let events = getObjectExpandedEventsExtra(); + is(events.length, 1, "There was 1 event logged"); + const [event] = events; + ok(event.session_id > 0, "There is a valid session_id in the logged event"); + + info("Click on the second arrow icon to expand the prototype node"); + const secondArrowIcon = message.querySelectorAll(".arrow")[1]; + secondArrowIcon.click(); + // let's wait until we have more than 2 arrows displayed, i.e. the prototype node was + // expanded. + await waitFor(() => message.querySelectorAll(".arrow").length > 2); + + events = getObjectExpandedEventsExtra(); + is(events.length, 1, "There was an event logged when expanding a child node"); + + info("Click the first arrow to collapse the object"); + arrowIcon.click(); + // Let's wait until there's only one arrow visible, i.e. the node is collapsed. + await waitFor(() => message.querySelectorAll(".arrow").length === 1); + + ok(!snapshot.parent, "There was no event logged when collapsing the node"); +}); + +function getObjectExpandedEventsExtra() { + // Retrieve and clear telemetry events. + const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true); + + const events = snapshot.parent.filter( + event => + event[1] === "devtools.main" && + event[2] === "object_expanded" && + event[3] === "webconsole" + ); + + // Since we already know we have the correct event, we only return the `extra` field + // that was passed to it (which is event[5] here). + return events.map(event => event[5]); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_telemetry_persist_toggle_changed.js b/devtools/client/webconsole/test/browser/browser_webconsole_telemetry_persist_toggle_changed.js new file mode 100644 index 0000000000..176f197058 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_telemetry_persist_toggle_changed.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests the log persistence telemetry event + +"use strict"; + +const { TelemetryTestUtils } = ChromeUtils.import( + "resource://testing-common/TelemetryTestUtils.jsm" +); + +const TEST_URI = `data:text/html,<meta charset=utf8><script> + console.log("test message"); +</script>`; + +add_task(async function() { + // Let's reset the counts. + Services.telemetry.clearEvents(); + + // Ensure no events have been logged + TelemetryTestUtils.assertNumberOfEvents(0); + + const hud = await openNewTabAndConsole(TEST_URI); + + // Toggle persistent logs - "true" + await toggleConsoleSetting( + hud, + ".webconsole-console-settings-menu-item-persistentLogs" + ); + await waitUntil( + () => hud.ui.wrapper.getStore().getState().ui.persistLogs === true + ); + + // Toggle persistent logs - "false" + await toggleConsoleSetting( + hud, + ".webconsole-console-settings-menu-item-persistentLogs" + ); + await waitUntil( + () => hud.ui.wrapper.getStore().getState().ui.persistLogs === false + ); + + const expectedEvents = [ + { + category: "devtools.main", + method: "persist_changed", + object: "webconsole", + value: "true", + }, + { + category: "devtools.main", + method: "persist_changed", + object: "webconsole", + value: "false", + }, + ]; + + const filter = { + category: "devtools.main", + method: "persist_changed", + object: "webconsole", + }; + + // Will compare filtered events to event list above + await TelemetryTestUtils.assertEvents(expectedEvents, filter); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_telemetry_reverse_search.js b/devtools/client/webconsole/test/browser/browser_webconsole_telemetry_reverse_search.js new file mode 100644 index 0000000000..7e039dcbb2 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_telemetry_reverse_search.js @@ -0,0 +1,171 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that the console records the reverse search telemetry event with expected data +// on open, navigate forward, navigate back and evaluate expression. + +"use strict"; + +const { TelemetryTestUtils } = ChromeUtils.import( + "resource://testing-common/TelemetryTestUtils.jsm" +); + +const TEST_URI = `data:text/html,<meta charset=utf8>Test reverse_search telemetry event`; +const ALL_CHANNELS = Ci.nsITelemetry.DATASET_ALL_CHANNELS; +const isMacOS = AppConstants.platform === "macosx"; + +add_task(async function() { + // Let's reset the counts. + Services.telemetry.clearEvents(); + + // Ensure no events have been logged + TelemetryTestUtils.assertNumberOfEvents(0); + + const hud = await openNewTabAndConsole(TEST_URI); + + info("Evaluate single line expressions"); + await keyboardExecuteAndWaitForMessage(hud, `"single line 1"`, "", ".result"); + await keyboardExecuteAndWaitForMessage(hud, `"single line 2"`, "", ".result"); + await keyboardExecuteAndWaitForMessage(hud, `"single line 3"`, "", ".result"); + + info("Open editor mode"); + await toggleLayout(hud); + + info("Open reverse search from editor mode"); + hud.ui.outputNode + .querySelector(".webconsole-editor-toolbar-reverseSearchButton") + .click(); + + info("Close reverse search"); + EventUtils.synthesizeKey("KEY_Escape"); + + info("Open reverse search using keyboard shortcut"); + await openReverseSearch(hud); + + info("Send keys to reverse search"); + EventUtils.sendString("sin"); + + info("Reverse search navigate next - keyboard"); + navigateReverseSearch("keyboard", "next", hud); + + info("Reverse search navigate previous - keyboard"); + navigateReverseSearch("keyboard", "previous", hud); + + info("Reverse search navigate next - mouse"); + navigateReverseSearch("mouse", "next", hud); + + info("Reverse search navigate previous - mouse"); + navigateReverseSearch("mouse", "previous", hud); + + info("Reverse search evaluate expression"); + const onMessage = waitForMessage(hud, "single line 3", ".result"); + EventUtils.synthesizeKey("KEY_Enter"); + await onMessage; + + info("Check reverse search telemetry"); + checkEventTelemetry([ + getTelemetryEventData("editor-toolbar-icon", { functionality: "open" }), + getTelemetryEventData("keyboard", { functionality: "open" }), + getTelemetryEventData("keyboard", { functionality: "navigate next" }), + getTelemetryEventData("keyboard", { functionality: "navigate previous" }), + getTelemetryEventData("click", { functionality: "navigate next" }), + getTelemetryEventData("click", { functionality: "navigate previous" }), + getTelemetryEventData(null, { functionality: "evaluate expression" }), + ]); + + info("Revert to inline layout"); + await toggleLayout(hud); +}); + +function triggerPreviousResultShortcut() { + if (isMacOS) { + EventUtils.synthesizeKey("r", { ctrlKey: true }); + } else { + EventUtils.synthesizeKey("VK_F9"); + } +} + +function triggerNextResultShortcut() { + if (isMacOS) { + EventUtils.synthesizeKey("s", { ctrlKey: true }); + } else { + EventUtils.synthesizeKey("VK_F9", { shiftKey: true }); + } +} + +function clickPreviousButton(hud) { + const reverseSearchElement = getReverseSearchElement(hud); + if (!reverseSearchElement) { + return; + } + const button = reverseSearchElement.querySelector( + ".search-result-button-prev" + ); + if (!button) { + return; + } + + button.click(); +} + +function clickNextButton(hud) { + const reverseSearchElement = getReverseSearchElement(hud); + if (!reverseSearchElement) { + return; + } + const button = reverseSearchElement.querySelector( + ".search-result-button-next" + ); + if (!button) { + return; + } + button.click(); +} + +function navigateReverseSearch(access, direction, hud) { + if (access == "keyboard") { + if (direction === "previous") { + triggerPreviousResultShortcut(); + } else { + triggerNextResultShortcut(); + } + } else if (access === "mouse") { + if (direction === "previous") { + clickPreviousButton(hud); + } else { + clickNextButton(hud); + } + } +} + +function getTelemetryEventData(value, extra) { + return { + timestamp: null, + category: "devtools.main", + method: "reverse_search", + object: "webconsole", + value, + extra, + }; +} + +function checkEventTelemetry(expectedData) { + const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true); + const events = snapshot.parent.filter(event => event[2] === "reverse_search"); + + for (const [i, expected] of expectedData.entries()) { + const [timestamp, category, method, object, value, extra] = events[i]; + + ok(timestamp > 0, "timestamp is greater than 0"); + is(category, expected.category, "'category' is correct"); + is(method, expected.method, "'method' is correct"); + is(object, expected.object, "'object' is correct"); + is(value, expected.value, "'value' is correct"); + is( + extra.functionality, + expected.extra.functionality, + "'functionality' is correct" + ); + ok(extra.session_id > 0, "'session_id' is correct"); + } +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_time_methods.js b/devtools/client/webconsole/test/browser/browser_webconsole_time_methods.js new file mode 100644 index 0000000000..53f51db420 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_time_methods.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that the Console API implements the time() and timeEnd() methods. + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-time-methods.html"; + +const TEST_URI2 = + "data:text/html;charset=utf-8,<script>" + + "console.timeEnd('bTimer');</script>"; + +const TEST_URI3 = + "data:text/html;charset=utf-8,<script>" + + "console.time('bTimer');console.log('smoke signal');</script>"; + +const TEST_URI4 = + "data:text/html;charset=utf-8," + + "<script>console.timeEnd('bTimer');</script>"; + +add_task(async function() { + // Calling console.time('aTimer') followed by console.timeEnd('aTimer') + // should result in the aTimer being ended, and a message like aTimer: 123ms + // printed to the console + const hud1 = await openNewTabAndConsole(TEST_URI); + + const aTimerCompleted = await waitFor(() => findMessage(hud1, "aTimer: ")); + ok( + aTimerCompleted.textContent.includes("- timer ended"), + "Calling " + "console.time('a') and console.timeEnd('a')ends the 'a' timer" + ); + + // Calling console.time('bTimer') in the current tab, opening a new tab + // and calling console.timeEnd('bTimer') in the new tab should not result in + // the bTimer in the initial tab being ended, but rather a warning message + // output to the console: Timer "bTimer" doesn't exist + const hud2 = await openNewTabAndConsole(TEST_URI2); + + const error1 = await waitFor(() => + findMessage(hud2, "bTimer", ".message.timeEnd.warn") + ); + ok( + error1, + "Timers with the same name but in separate tabs do not contain " + + "the same value" + ); + + // The next tests make sure that timers with the same name but in separate + // pages do not contain the same value. + await navigateTo(TEST_URI3); + + // The new console front-end does not display a message when timers are started, + // so there should not be a 'bTimer started' message on the output + + // We use this await to 'sync' until the message appears, as the console API + // guarantees us that the smoke signal will be printed after the message for + // console.time("bTimer") (if there were any) + await waitFor(() => findMessage(hud2, "smoke signal")); + + is( + findMessage(hud2, "bTimer started"), + undefined, + "No message is printed to " + "the console when the timer starts" + ); + + await clearOutput(hud2); + + // Calling console.time('bTimer') on a page, then navigating to another page + // and calling console.timeEnd('bTimer') on the new console front-end should + // result on a warning message: 'Timer "bTimer" does not exist', + // as the timers in different pages are not related + await navigateTo(TEST_URI4); + + const error2 = await waitFor(() => + findMessage(hud2, "bTimer", ".message.timeEnd.warn") + ); + ok( + error2, + "Timers with the same name but in separate pages do not contain " + + "the same value" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_timestamps.js b/devtools/client/webconsole/test/browser/browser_webconsole_timestamps.js new file mode 100644 index 0000000000..c45bb7bf1a --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_timestamps.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test for the message timestamps option: check if the preference toggles the +// display of messages in the console output. See bug 722267. + +"use strict"; + +const { PrefObserver } = require("devtools/client/shared/prefs"); + +const TEST_URI = `data:text/html;charset=utf-8, + Web Console test for bug 1307871 - preference for toggling timestamps in messages`; +const PREF_MESSAGE_TIMESTAMP = "devtools.webconsole.timestampMessages"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + info("Call the log function defined in the test page"); + const onMessage = waitForMessage(hud, "simple text message"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.wrappedJSObject.console.log("simple text message"); + }); + const message = await onMessage; + + const prefValue = Services.prefs.getBoolPref(PREF_MESSAGE_TIMESTAMP); + ok(!prefValue, "Messages should have no timestamp by default (pref check)"); + ok( + !message.node.querySelector(".timestamp"), + "Messages should have no timestamp by default (element check)" + ); + + const observer = new PrefObserver(""); + + info("Change Timestamp preference"); + const prefChanged = observer.once(PREF_MESSAGE_TIMESTAMP, () => {}); + + await toggleConsoleSetting( + hud, + ".webconsole-console-settings-menu-item-timestamps" + ); + + await prefChanged; + observer.destroy(); + + ok( + message.node.querySelector(".timestamp"), + "Messages should have timestamp" + ); + + Services.prefs.clearUserPref(PREF_MESSAGE_TIMESTAMP); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_trackingprotection_errors.js b/devtools/client/webconsole/test/browser/browser_webconsole_trackingprotection_errors.js new file mode 100644 index 0000000000..053e0401b8 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_trackingprotection_errors.js @@ -0,0 +1,266 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Load a page with tracking elements that get blocked and make sure that a +// 'learn more' link shows up in the webconsole. + +"use strict"; +requestLongerTimeout(2); + +const TEST_PATH = "browser/devtools/client/webconsole/test/browser/"; +const TEST_FILE = TEST_PATH + "test-trackingprotection-securityerrors.html"; +const TEST_FILE_THIRD_PARTY_ONLY = + TEST_PATH + "test-trackingprotection-securityerrors-thirdpartyonly.html"; +const TEST_URI = "https://example.com/" + TEST_FILE; +const TEST_URI_THIRD_PARTY_ONLY = + "https://example.com/" + TEST_FILE_THIRD_PARTY_ONLY; +const TRACKER_URL = "https://tracking.example.org/"; +const THIRD_PARTY_URL = "https://example.org/"; +const BLOCKED_URL = `\u201c${TRACKER_URL + + TEST_PATH + + "cookieSetter.html"}\u201d`; +const PARTITIONED_URL = `\u201c${THIRD_PARTY_URL + + TEST_PATH}cookieSetter.html\u201d`; + +const COOKIE_BEHAVIOR_PREF = "network.cookie.cookieBehavior"; +const COOKIE_BEHAVIORS = { + // reject all third-party cookies + REJECT_FOREIGN: 1, + // reject all cookies + REJECT: 2, + // reject third-party cookies unless the eTLD already has at least one cookie + LIMIT_FOREIGN: 3, + // reject trackers + REJECT_TRACKER: 4, + // dFPI - partitioned access to third-party cookies + PARTITION_FOREIGN: 5, +}; + +const { UrlClassifierTestUtils } = ChromeUtils.import( + "resource://testing-common/UrlClassifierTestUtils.jsm" +); + +registerCleanupFunction(async function() { + UrlClassifierTestUtils.cleanupTestTrackers(); + + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +pushPref("devtools.webconsole.groupWarningMessages", false); + +add_task(async function testContentBlockingMessage() { + await UrlClassifierTestUtils.addTestTrackers(); + + await pushPref("privacy.trackingprotection.enabled", true); + const hud = await openNewTabAndConsole(TRACKER_URL + TEST_FILE); + + info("Test content blocking message"); + const message = await waitFor(() => + findMessage( + hud, + `The resource at \u201chttps://tracking.example.com/\u201d was blocked because ` + + `content blocking is enabled` + ) + ); + + await testLearnMoreClickOpenNewTab( + message, + "https://developer.mozilla.org/Firefox/Privacy/Tracking_Protection" + + DOCS_GA_PARAMS + ); +}); + +add_task(async function testForeignCookieBlockedMessage() { + info("Test foreign cookie blocked message"); + // Bug 1518138: GC heuristics are broken for this test, so that the test + // ends up running out of memory. Try to work-around the problem by GCing + // before the test begins. + Cu.forceShrinkingGC(); + // We change the pref and open a new window to ensure it will be taken into account. + await pushPref(COOKIE_BEHAVIOR_PREF, COOKIE_BEHAVIORS.REJECT_FOREIGN); + const { hud, win } = await openNewWindowAndConsole(TEST_URI); + const message = await waitFor(() => + findMessage( + hud, + `Request to access cookie or storage on ${BLOCKED_URL} was blocked because we are ` + + `blocking all third-party storage access requests and content blocking is enabled` + ) + ); + await testLearnMoreClickOpenNewTab( + message, + getStorageErrorUrl("CookieBlockedForeign") + ); + // We explicitely destroy the toolbox in order to ensure waiting for its full destruction + // and avoid leak / pending requests + await hud.toolbox.destroy(); + win.close(); +}); + +add_task(async function testLimitForeignCookieBlockedMessage() { + info("Test unvisited eTLD foreign cookies blocked message"); + // Bug 1518138: GC heuristics are broken for this test, so that the test + // ends up running out of memory. Try to work-around the problem by GCing + // before the test begins. + Cu.forceShrinkingGC(); + // We change the pref and open a new window to ensure it will be taken into account. + await pushPref(COOKIE_BEHAVIOR_PREF, COOKIE_BEHAVIORS.LIMIT_FOREIGN); + const { hud, win } = await openNewWindowAndConsole(TEST_URI); + + const message = await waitFor( + () => + findMessage( + hud, + `Request to access cookie or storage on ${BLOCKED_URL} was blocked because we are ` + + `blocking all third-party storage access requests and content blocking is enabled` + ), + "Wait for 'blocking all third-party storage access' message", + 100 + ); + ok(true, "Third-party storage access blocked message was displayed"); + + info("Check that clicking on the Learn More link works as expected"); + await testLearnMoreClickOpenNewTab( + message, + getStorageErrorUrl("CookieBlockedForeign") + ); + // We explicitely destroy the toolbox in order to ensure waiting for its full destruction + // and avoid leak / pending requests + await hud.toolbox.destroy(); + win.close(); +}); + +add_task(async function testAllCookieBlockedMessage() { + info("Test all cookies blocked message"); + // We change the pref and open a new window to ensure it will be taken into account. + await pushPref(COOKIE_BEHAVIOR_PREF, COOKIE_BEHAVIORS.REJECT); + const { hud, win } = await openNewWindowAndConsole(TEST_URI); + + const message = await waitFor(() => + findMessage( + hud, + `Request to access cookie or storage on ${BLOCKED_URL} was blocked because we are ` + + `blocking all storage access requests` + ) + ); + await testLearnMoreClickOpenNewTab( + message, + getStorageErrorUrl("CookieBlockedAll") + ); + // We explicitely destroy the toolbox in order to ensure waiting for its full destruction + // and avoid leak / pending requests + await hud.toolbox.destroy(); + win.close(); +}); + +add_task(async function testTrackerCookieBlockedMessage() { + info("Test tracker cookie blocked message"); + // We change the pref and open a new window to ensure it will be taken into account. + await pushPref(COOKIE_BEHAVIOR_PREF, COOKIE_BEHAVIORS.REJECT_TRACKER); + const { hud, win } = await openNewWindowAndConsole(TEST_URI); + + const message = await waitFor(() => + findMessage( + hud, + `Request to access cookie or storage on ${BLOCKED_URL} was blocked because it came ` + + `from a tracker and content blocking is enabled` + ) + ); + await testLearnMoreClickOpenNewTab( + message, + getStorageErrorUrl("CookieBlockedTracker") + ); + // We explicitely destroy the toolbox in order to ensure waiting for its full destruction + // and avoid leak / pending requests + await hud.toolbox.destroy(); + win.close(); +}); + +add_task(async function testForeignCookiePartitionedMessage() { + info("Test tracker cookie blocked message"); + // We change the pref and open a new window to ensure it will be taken into account. + await pushPref(COOKIE_BEHAVIOR_PREF, COOKIE_BEHAVIORS.PARTITION_FOREIGN); + const { hud, win } = await openNewWindowAndConsole(TEST_URI_THIRD_PARTY_ONLY); + + const message = await waitFor(() => + findMessage( + hud, + `Partitioned cookie or storage access was provided to ${PARTITIONED_URL} because it is ` + + `loaded in the third-party context and storage partitioning is enabled.` + ) + ); + await testLearnMoreClickOpenNewTab( + message, + getStorageErrorUrl("CookiePartitionedForeign") + ); + // We explicitely destroy the toolbox in order to ensure waiting for its full destruction + // and avoid leak / pending requests + await hud.toolbox.destroy(); + win.close(); +}); + +add_task(async function testCookieBlockedByPermissionMessage() { + info("Test cookie blocked by permission message"); + // Turn off tracking protection and add a block permission on the URL. + await pushPref("privacy.trackingprotection.enabled", false); + const p = Services.scriptSecurityManager.createContentPrincipalFromOrigin( + TRACKER_URL + ); + Services.perms.addFromPrincipal( + p, + "cookie", + Ci.nsIPermissionManager.DENY_ACTION + ); + + const { hud, win } = await openNewWindowAndConsole(TEST_URI); + const message = await waitFor(() => + findMessage( + hud, + `Request to access cookies or ` + + `storage on ${BLOCKED_URL} was blocked because of custom cookie permission` + ) + ); + await testLearnMoreClickOpenNewTab( + message, + getStorageErrorUrl("CookieBlockedByPermission") + ); + // We explicitely destroy the toolbox in order to ensure waiting for its full destruction + // and avoid leak / pending requests + await hud.toolbox.destroy(); + win.close(); + + // Remove the custom permission. + Services.perms.removeFromPrincipal(p, "cookie"); +}); + +function getStorageErrorUrl(category) { + const BASE_STORAGE_ERROR_URL = + "https://developer.mozilla.org/docs/Mozilla/Firefox/" + + "Privacy/Storage_access_policy/Errors/"; + const STORAGE_ERROR_URL_PARAMS = new URLSearchParams({ + utm_source: "devtools", + utm_medium: "firefox-cookie-errors", + utm_campaign: "default", + }).toString(); + return `${BASE_STORAGE_ERROR_URL}${category}?${STORAGE_ERROR_URL_PARAMS}`; +} + +async function testLearnMoreClickOpenNewTab(message, expectedUrl) { + info("Clicking on the Learn More link"); + + const learnMoreLink = message.querySelector(".learn-more-link"); + const linkSimulation = await simulateLinkClick(learnMoreLink); + checkLink({ + ...linkSimulation, + expectedLink: expectedUrl, + expectedTab: "tab", + }); +} + +function checkLink({ link, where, expectedLink, expectedTab }) { + is(link, expectedLink, `Clicking the provided link opens ${link}`); + is(where, expectedTab, `Clicking the provided link opens in expected tab`); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_uncaught_exception.js b/devtools/client/webconsole/test/browser/browser_webconsole_uncaught_exception.js new file mode 100644 index 0000000000..6f824f4f7b --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_uncaught_exception.js @@ -0,0 +1,88 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that stack traces are shown when primitive values are thrown instead of +// error objects. + +"use strict"; + +const TEST_URI = `data:text/html,<meta charset=utf8>Test uncaught exception`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + await checkThrowingWithStack(hud, `"tomato"`, "Uncaught tomato"); + await checkThrowingWithStack(hud, `""`, "Uncaught <empty string>"); + await checkThrowingWithStack(hud, `42`, "Uncaught 42"); + await checkThrowingWithStack(hud, `0`, "Uncaught 0"); + await checkThrowingWithStack(hud, `null`, "Uncaught null"); + await checkThrowingWithStack(hud, `undefined`, "Uncaught undefined"); + await checkThrowingWithStack(hud, `false`, "Uncaught false"); + + await checkThrowingWithStack( + hud, + `new Error("watermelon")`, + "Uncaught Error: watermelon" + ); + + await checkThrowingWithStack( + hud, + `(err = new Error("lettuce"), err.name = "VegetableError", err)`, + "Uncaught VegetableError: lettuce" + ); + + await checkThrowingWithStack( + hud, + `{ fav: "eggplant" }`, + `Uncaught Object { fav: "eggplant" }` + ); + + info("Check that object in errors can be expanded"); + const rejectedObjectMessage = findMessage(hud, "eggplant", ".error"); + const oi = rejectedObjectMessage.querySelector(".tree"); + ok(true, "The object was rendered in an ObjectInspector"); + + info("Expanding the object"); + const onOiExpanded = waitFor(() => { + return oi.querySelectorAll(".node").length === 3; + }); + oi.querySelector(".arrow").click(); + await onOiExpanded; + + ok( + oi.querySelector(".arrow").classList.contains("expanded"), + "Object expanded" + ); + + // The object inspector now looks like: + // {...} + // | fav: "eggplant" + // | <prototype>: Object { ... } + + const oiNodes = oi.querySelectorAll(".node"); + is(oiNodes.length, 3, "There is the expected number of nodes in the tree"); + + ok(oiNodes[0].textContent.includes(`{\u2026}`)); + ok(oiNodes[1].textContent.includes(`fav: "eggplant"`)); + ok(oiNodes[2].textContent.includes(`<prototype>: Object { \u2026 }`)); +}); + +async function checkThrowingWithStack(hud, expression, expectedMessage) { + await SpecialPowers.spawn(gBrowser.selectedBrowser, [expression], function( + expr + ) { + const script = content.document.createElement("script"); + script.append( + content.document.createTextNode(` + a = () => {throw ${expr}}; + b = () => a(); + c = () => b(); + d = () => c(); + d(); + `) + ); + content.document.body.append(script); + script.remove(); + }); + return checkMessageStack(hud, expectedMessage, [2, 3, 4, 5, 6]); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_view_source.js b/devtools/client/webconsole/test/browser/browser_webconsole_view_source.js new file mode 100644 index 0000000000..b7fd5ebf4e --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_view_source.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that source URLs in the Web Console can be clicked to display the +// standard View Source window. As JS exceptions and console.log() messages always +// have their locations opened in Debugger, we need to test a security message in +// order to have it opened in the standard View Source window. + +"use strict"; + +const TEST_URI = + "https://example.com/browser/devtools/client/webconsole/" + + "test/browser/" + + "test-mixedcontent-securityerrors.html"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + info("console opened"); + + const msg = await waitFor(() => + findMessage(hud, "Blocked loading mixed active content") + ); + ok(msg, "error message"); + const locationNode = msg.querySelector( + ".message-location .frame-link-filename" + ); + ok(locationNode, "location node"); + + const onTabOpen = BrowserTestUtils.waitForNewTab(gBrowser, null, true); + + locationNode.click(); + await onTabOpen; + ok( + true, + "the view source tab was opened in response to clicking the location node" + ); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_visibility_messages.js b/devtools/client/webconsole/test/browser/browser_webconsole_visibility_messages.js new file mode 100644 index 0000000000..e375d4569b --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_visibility_messages.js @@ -0,0 +1,135 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Check messages logged when console not visible are displayed when +// the user show the console again. + +const HTML = ` + <!DOCTYPE html> + <html> + <body> + <h1>Test console visibility update</h1> + <script> + function log(str) { + console.log(str); + } + </script> + </body> + </html> +`; +const TEST_URI = "data:text/html;charset=utf-8," + encodeURI(HTML); +const MESSAGES_COUNT = 10; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const toolbox = hud.toolbox; + + info("Log one message in the console"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.wrappedJSObject.log("in-console log"); + }); + await waitFor(() => findMessage(hud, "in-console log")); + + info("select the inspector"); + await toolbox.selectTool("inspector"); + + info("Wait for console to be hidden"); + const { document } = hud.iframeWindow; + await waitFor(() => document.visibilityState == "hidden"); + + const onAllMessagesInStore = new Promise(done => { + const store = hud.ui.wrapper.getStore(); + store.subscribe(() => { + const messages = store.getState().messages.messagesById.size; + // Also consider the "in-console log" message + if (messages == MESSAGES_COUNT + 1) { + done(); + } + }); + }); + + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [[MESSAGES_COUNT]], + count => { + for (let i = 1; i <= count; i++) { + content.wrappedJSObject.log("in-inspector log " + i); + } + } + ); + + info("Waiting for all messages to be logged into the store"); + await onAllMessagesInStore; + + const inInspectorMessages = await findMessages(hud, "in-inspector"); + is( + inInspectorMessages.length, + 0, + "No messages from the inspector actually appear in the console" + ); + + info("select back the console"); + await toolbox.selectTool("webconsole"); + + info("And wait for all messages to be visible"); + const waitForMessagePromises = []; + for (let j = 1; j <= MESSAGES_COUNT; j++) { + waitForMessagePromises.push( + waitFor(() => findMessage(hud, "in-inspector log " + j)) + ); + } + + await Promise.all(waitForMessagePromises); + ok( + true, + "All the messages logged when the console was hidden were displayed." + ); +}); + +// Similar scenario, but with the split console on the inspector panel. +// Here, the messages should still be logged. +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const toolbox = hud.toolbox; + + info("Log one message in the console"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.wrappedJSObject.log("in-console log"); + }); + await waitFor(() => findMessage(hud, "in-console log")); + + info("select the inspector"); + await toolbox.selectTool("inspector"); + + info("Wait for console to be hidden"); + const { document } = hud.iframeWindow; + await waitFor(() => document.visibilityState == "hidden"); + + await toolbox.openSplitConsole(); + + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [[MESSAGES_COUNT]], + count => { + for (let i = 1; i <= count; i++) { + content.wrappedJSObject.log("in-inspector log " + i); + } + } + ); + + info("Wait for all messages to be visible in the split console"); + const waitForMessagePromises = []; + for (let j = 1; j <= MESSAGES_COUNT; j++) { + waitForMessagePromises.push( + waitFor(() => findMessage(hud, "in-inspector log " + j)) + ); + } + + await Promise.all(waitForMessagePromises); + ok(true, "All the messages logged when we are using the split console"); + + await toolbox.closeSplitConsole(); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_warn_about_replaced_api.js b/devtools/client/webconsole/test/browser/browser_webconsole_warn_about_replaced_api.js new file mode 100644 index 0000000000..0bca46fff0 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_warn_about_replaced_api.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI_REPLACED = + "data:text/html;charset=utf8,<script>console = {log: () => ''}</script>"; +const TEST_URI_NOT_REPLACED = + "data:text/html;charset=utf8,<script>console.log('foo')</script>"; + +add_task(async function() { + await pushPref("devtools.webconsole.timestampMessages", true); + await pushPref("devtools.webconsole.persistlog", true); + + let hud = await openNewTabAndConsole(TEST_URI_NOT_REPLACED); + + await testWarningNotPresent(hud); + await closeToolbox(); + + // Use BrowserTestUtils instead of navigateTo as there is no toolbox opened + const onBrowserLoaded = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser + ); + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI_REPLACED); + await onBrowserLoaded; + + const toolbox = await openToolboxForTab(gBrowser.selectedTab, "webconsole"); + hud = toolbox.getCurrentPanel().hud; + await testWarningPresent(hud); +}); + +async function testWarningNotPresent(hud) { + ok(!findMessage(hud, "logging API"), "no warning displayed"); + + // Bug 862024: make sure the warning doesn't show after page reload. + info( + "wait for the page to refresh and make sure the warning still isn't there" + ); + await refreshTab(); + await waitFor(() => { + // We need to wait for 3 messages because there are two logs, plus the + // navigation message since messages are persisted + return findMessages(hud, "foo").length === 3; + }); + + ok(!findMessage(hud, "logging API"), "no warning displayed"); +} + +async function testWarningPresent(hud) { + info("wait for the warning to show"); + await waitFor(() => findMessage(hud, "logging API")); + + info("reload the test page and wait for the warning to show"); + await refreshTab(); + await waitFor(() => { + return findMessages(hud, "logging API").length === 2; + }); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_warning_group_content_blocking.js b/devtools/client/webconsole/test/browser/browser_webconsole_warning_group_content_blocking.js new file mode 100644 index 0000000000..713f0ce34c --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_warning_group_content_blocking.js @@ -0,0 +1,245 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Load a page with tracking elements that get blocked and make sure that a +// 'learn more' link shows up in the webconsole. + +"use strict"; +requestLongerTimeout(2); + +const TEST_FILE = + "browser/devtools/client/webconsole/test/browser/test-warning-groups.html"; +const TEST_URI = "http://example.com/" + TEST_FILE; + +const TRACKER_URL = "http://tracking.example.com/"; +const IMG_FILE = + "browser/devtools/client/webconsole/test/browser/test-image.png"; +const TRACKER_IMG = "http://tracking.example.org/" + IMG_FILE; + +const CONTENT_BLOCKING_GROUP_LABEL = + "The resource at “<URL>” was blocked because content blocking is enabled."; + +const COOKIE_BEHAVIOR_PREF = "network.cookie.cookieBehavior"; +const COOKIE_BEHAVIORS = { + // reject all third-party cookies + REJECT_FOREIGN: 1, + // reject all cookies + REJECT: 2, + // reject third-party cookies unless the eTLD already has at least one cookie + LIMIT_FOREIGN: 3, + // reject trackers + REJECT_TRACKER: 4, +}; + +const { UrlClassifierTestUtils } = ChromeUtils.import( + "resource://testing-common/UrlClassifierTestUtils.jsm" +); +UrlClassifierTestUtils.addTestTrackers(); +registerCleanupFunction(function() { + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +pushPref("privacy.trackingprotection.enabled", true); +pushPref("devtools.webconsole.groupWarningMessages", true); + +async function cleanUp() { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +} + +add_task(cleanUp); + +add_task(async function testContentBlockingMessage() { + const { hud, tab, win } = await openNewWindowAndConsole( + "http://tracking.example.org/" + TEST_FILE + ); + const now = Date.now(); + + info("Test content blocking message"); + const message = + `The resource at \u201chttp://tracking.example.com/?1&${now}\u201d ` + + `was blocked because content blocking is enabled`; + const onContentBlockingWarningMessage = waitForMessage(hud, message, ".warn"); + emitContentBlockingMessage(tab, `${TRACKER_URL}?1&${now}`); + await onContentBlockingWarningMessage; + + ok(true, "The content blocking message was displayed"); + + info( + "Emit a new content blocking message to check that it causes a grouping" + ); + const onContentBlockingWarningGroupMessage = waitForMessage( + hud, + CONTENT_BLOCKING_GROUP_LABEL, + ".warn" + ); + emitContentBlockingMessage(tab, `${TRACKER_URL}?2&${now}`); + const { node } = await onContentBlockingWarningGroupMessage; + is( + node.querySelector(".warning-group-badge").textContent, + "2", + "The badge has the expected text" + ); + + checkConsoleOutputForWarningGroup(hud, [ + `▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL} 2`, + ]); + + info("Open the group"); + node.querySelector(".arrow").click(); + await waitFor(() => findMessage(hud, "http://tracking.example.com/?1")); + + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL} 2`, + `| The resource at \u201chttp://tracking.example.com/?1&${now}\u201d was blocked`, + `| The resource at \u201chttp://tracking.example.com/?2&${now}\u201d was blocked`, + ]); + await win.close(); +}); + +add_task(async function testForeignCookieBlockedMessage() { + info("Test foreign cookie blocked message"); + // We change the pref and open a new window to ensure it will be taken into account. + await pushPref(COOKIE_BEHAVIOR_PREF, COOKIE_BEHAVIORS.REJECT_FOREIGN); + await testStorageAccessBlockedGrouping( + "Request to access cookie or storage on " + + "“<URL>” was blocked because we are blocking all third-party storage access " + + "requests and content blocking is enabled." + ); +}); + +add_task(async function testLimitForeignCookieBlockedMessage() { + info("Test unvisited eTLD foreign cookies blocked message"); + // We change the pref and open a new window to ensure it will be taken into account. + await pushPref(COOKIE_BEHAVIOR_PREF, COOKIE_BEHAVIORS.LIMIT_FOREIGN); + await testStorageAccessBlockedGrouping( + "Request to access cookie or storage on " + + "“<URL>” was blocked because we are blocking all third-party storage access " + + "requests and content blocking is enabled." + ); +}); + +add_task(async function testAllCookieBlockedMessage() { + info("Test all cookies blocked message"); + // We change the pref and open a new window to ensure it will be taken into account. + await pushPref(COOKIE_BEHAVIOR_PREF, COOKIE_BEHAVIORS.REJECT); + await testStorageAccessBlockedGrouping( + "Request to access cookie or storage on " + + "“<URL>” was blocked because we are blocking all storage access requests." + ); +}); + +add_task(async function testTrackerCookieBlockedMessage() { + info("Test tracker cookie blocked message"); + // We change the pref and open a new window to ensure it will be taken into account. + await pushPref(COOKIE_BEHAVIOR_PREF, COOKIE_BEHAVIORS.REJECT_TRACKER); + await testStorageAccessBlockedGrouping( + "Request to access cookie or storage on " + + "“<URL>” was blocked because it came from a tracker and content blocking is " + + "enabled." + ); +}); + +add_task(async function testCookieBlockedByPermissionMessage() { + info("Test cookie blocked by permission message"); + // Turn off tracking protection and add a block permission on the URL. + await pushPref("privacy.trackingprotection.enabled", false); + const p = Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "http://tracking.example.org/" + ); + Services.perms.addFromPrincipal( + p, + "cookie", + Ci.nsIPermissionManager.DENY_ACTION + ); + + await testStorageAccessBlockedGrouping( + "Request to access cookies or storage on " + + "“<URL>” was blocked because of custom cookie permission." + ); + + // Remove the custom permission. + Services.perms.removeFromPrincipal(p, "cookie"); +}); + +add_task(cleanUp); + +/** + * Test that storage access blocked messages are grouped by emitting 2 messages. + * + * @param {String} groupLabel: The warning group label that should be created. + * It should contain "<URL>". + */ +async function testStorageAccessBlockedGrouping(groupLabel) { + const { hud, win, tab } = await openNewWindowAndConsole(TEST_URI); + const now = Date.now(); + + await clearOutput(hud); + + const getWarningMessage = url => groupLabel.replace("<URL>", url); + + const onStorageAccessBlockedMessage = waitForMessage( + hud, + getWarningMessage(`${TRACKER_IMG}?1&${now}`), + ".warn" + ); + emitStorageAccessBlockedMessage(tab, `${TRACKER_IMG}?1&${now}`); + await onStorageAccessBlockedMessage; + + info( + "Emit a new content blocking message to check that it causes a grouping" + ); + + const onContentBlockingWarningGroupMessage = waitForMessage( + hud, + groupLabel, + ".warn" + ); + emitStorageAccessBlockedMessage(tab, `${TRACKER_IMG}?2&${now}`); + const { node } = await onContentBlockingWarningGroupMessage; + is( + node.querySelector(".warning-group-badge").textContent, + "2", + "The badge has the expected text" + ); + + checkConsoleOutputForWarningGroup(hud, [`▶︎⚠ ${groupLabel} 2`]); + + info("Open the group"); + node.querySelector(".arrow").click(); + await waitFor(() => findMessage(hud, TRACKER_IMG)); + + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${groupLabel} 2`, + `| ${getWarningMessage(TRACKER_IMG + "?1&" + now)}`, + `| ${getWarningMessage(TRACKER_IMG + "?2&" + now)}`, + ]); + + await clearOutput(hud); + await win.close(); +} + +/** + * Emit a Content Blocking message. This is done by loading an iframe from an origin + * tagged as tracker. The image is loaded with a incremented counter query parameter + * each time so we can get the warning message. + */ +function emitContentBlockingMessage(tab, url) { + SpecialPowers.spawn(tab.linkedBrowser, [url], function(innerURL) { + content.wrappedJSObject.loadIframe(innerURL); + }); +} + +/** + * Emit a Storage blocked message. This is done by loading an image from an origin + * tagged as tracker. The image is loaded with a incremented counter query parameter + * each time so we can get the warning message. + */ +function emitStorageAccessBlockedMessage(tab, url) { + SpecialPowers.spawn(tab.linkedBrowser, [url], async function(innerURL) { + content.wrappedJSObject.loadImage(innerURL); + }); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_warning_group_cookies.js b/devtools/client/webconsole/test/browser/browser_webconsole_warning_group_cookies.js new file mode 100644 index 0000000000..40ae1d89ed --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_warning_group_cookies.js @@ -0,0 +1,155 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Load a page that generates cookie warning/info messages. See bug 1622306. + +"use strict"; +requestLongerTimeout(2); + +const TEST_FILE = + "browser/devtools/client/webconsole/test/browser/test-warning-groups.html"; + +pushPref("devtools.webconsole.groupWarningMessages", true); + +async function cleanUp() { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +} + +add_task(cleanUp); + +add_task(async function testSameSiteCookieMessage() { + const tests = [ + { + pref: true, + message1: + "Cookie “a” has “SameSite” policy set to “Lax” because it is missing a “SameSite” attribute, and “SameSite=Lax” is the default value for this attribute.", + typeMessage1: ".info", + groupLabel: + "Some cookies are misusing the “SameSite“ attribute, so it won’t work as expected", + message2: + "Cookie “b” has “SameSite” policy set to “Lax” because it is missing a “SameSite” attribute, and “SameSite=Lax” is the default value for this attribute.", + }, + { + pref: false, + groupLabel: + "Some cookies are misusing the recommended “SameSite“ attribute", + message1: + "Cookie “a” will be soon rejected because it has the “SameSite” attribute set to “None” or an invalid value, without the “secure” attribute.", + typeMessage1: ".warn", + message2: + "Cookie “b” will be soon rejected because it has the “SameSite” attribute set to “None” or an invalid value, without the “secure” attribute.", + }, + ]; + + for (const test of tests) { + info("LaxByDefault: " + test.pref); + await pushPref("network.cookie.sameSite.laxByDefault", test.pref); + + const { hud, tab, win } = await openNewWindowAndConsole( + "http://example.org/" + TEST_FILE + ); + + info("Test cookie messages"); + const onLaxMissingWarningMessage = waitForMessage( + hud, + test.message1, + test.typeMessage1 + ); + + SpecialPowers.spawn(tab.linkedBrowser, [], () => { + content.wrappedJSObject.createCookie("a=1"); + }); + + await onLaxMissingWarningMessage; + + ok(true, "The first message was displayed"); + + info("Emit a new cookie message to check that it causes a grouping"); + + const onCookieSameSiteWarningGroupMessage = waitForMessage( + hud, + test.groupLabel, + ".warn" + ); + + SpecialPowers.spawn(tab.linkedBrowser, [], () => { + content.wrappedJSObject.createCookie("b=1"); + }); + + const { node } = await onCookieSameSiteWarningGroupMessage; + is( + node.querySelector(".warning-group-badge").textContent, + "2", + "The badge has the expected text" + ); + + checkConsoleOutputForWarningGroup(hud, [`▶︎⚠ ${test.groupLabel} 2`]); + + info("Open the group"); + node.querySelector(".arrow").click(); + await waitFor(() => findMessage(hud, "SameSite")); + + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${test.groupLabel} 2`, + `| ${test.message1}`, + `| ${test.message2}`, + ]); + + await win.close(); + } +}); + +add_task(cleanUp); + +add_task(async function testInvalidSameSiteMessage() { + await pushPref("network.cookie.sameSite.laxByDefault", true); + + const groupLabel = + "Some cookies are misusing the “SameSite“ attribute, so it won’t work as expected"; + const message1 = + "Invalid “SameSite“ value for cookie “a”. The supported values are: “Lax“, “Strict“, “None“."; + const message2 = + "Cookie “a” has “SameSite” policy set to “Lax” because it is missing a “SameSite” attribute, and “SameSite=Lax” is the default value for this attribute."; + + const { hud, tab, win } = await openNewWindowAndConsole( + "http://example.org/" + TEST_FILE + ); + + info("Test cookie messages"); + + SpecialPowers.spawn(tab.linkedBrowser, [], () => { + content.wrappedJSObject.createCookie("a=1; sameSite=batman"); + }); + + const { node } = await waitForMessage(hud, groupLabel, ".warn"); + is( + node.querySelector(".warning-group-badge").textContent, + "2", + "The badge has the expected text" + ); + + checkConsoleOutputForWarningGroup(hud, [`▶︎⚠ ${groupLabel} 2`]); + + info("Open the group"); + node.querySelector(".arrow").click(); + await waitFor(() => findMessage(hud, "SameSite")); + + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${groupLabel} 2`, + `| ${message1}`, + `| ${message2}`, + ]); + + // Source map are being resolved in background and we might have + // pending request related to this service if we close the window + // immeditely. So just wait for these request to finish before proceeding. + await hud.toolbox.sourceMapURLService.waitForSourcesLoading(); + + await win.close(); +}); + +add_task(cleanUp); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_warning_group_multiples.js b/devtools/client/webconsole/test/browser/browser_webconsole_warning_group_multiples.js new file mode 100644 index 0000000000..4ad9f98e58 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_warning_group_multiples.js @@ -0,0 +1,313 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that warning messages can be grouped, per navigation and category, and that +// interacting with these groups works as expected. + +"use strict"; +requestLongerTimeout(2); + +const TEST_FILE = + "browser/devtools/client/webconsole/test/browser/test-warning-groups.html"; +const TEST_URI = "http://example.org/" + TEST_FILE; + +const TRACKER_URL = "http://tracking.example.com/"; +const FILE_PATH = + "browser/devtools/client/webconsole/test/browser/test-image.png"; +const CONTENT_BLOCKED_URL = TRACKER_URL + FILE_PATH; +const STORAGE_BLOCKED_URL = "http://example.com/" + FILE_PATH; + +const COOKIE_BEHAVIOR_PREF = "network.cookie.cookieBehavior"; +const COOKIE_BEHAVIORS_REJECT_FOREIGN = 1; + +const CONTENT_BLOCKED_GROUP_LABEL = + "The resource at “<URL>” was blocked because content blocking is enabled."; +const STORAGE_BLOCKED_GROUP_LABEL = + "Request to access cookie or storage on “<URL>” " + + "was blocked because we are blocking all third-party storage access requests and " + + "content blocking is enabled."; + +const { UrlClassifierTestUtils } = ChromeUtils.import( + "resource://testing-common/UrlClassifierTestUtils.jsm" +); +UrlClassifierTestUtils.addTestTrackers(); +registerCleanupFunction(function() { + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +pushPref("privacy.trackingprotection.enabled", true); +pushPref("devtools.webconsole.groupWarningMessages", true); + +add_task(async function testContentBlockingMessage() { + await pushPref(COOKIE_BEHAVIOR_PREF, COOKIE_BEHAVIORS_REJECT_FOREIGN); + await pushPref("devtools.webconsole.persistlog", true); + + const hud = await openNewTabAndConsole(TEST_URI); + + info( + "Log a tracking protection message to check a single message isn't grouped" + ); + let onContentBlockedMessage = waitForMessage( + hud, + CONTENT_BLOCKED_URL, + ".warn" + ); + emitContentBlockingMessage(hud); + let { node } = await onContentBlockedMessage; + is( + node.querySelector(".warning-indent"), + null, + "The message has the expected style" + ); + is( + node.querySelector(".indent").getAttribute("data-indent"), + "0", + "The message has the expected indent" + ); + + info("Log a simple message"); + await logString(hud, "simple message 1"); + + info( + "Log a second tracking protection message to check that it causes the grouping" + ); + let onContentBlockedWarningGroupMessage = waitForMessage( + hud, + CONTENT_BLOCKED_GROUP_LABEL, + ".warn" + ); + emitContentBlockingMessage(hud); + const { + node: contentBlockedWarningGroupNode, + } = await onContentBlockedWarningGroupMessage; + is( + contentBlockedWarningGroupNode.querySelector(".warning-group-badge") + .textContent, + "2", + "The badge has the expected text" + ); + + checkConsoleOutputForWarningGroup(hud, [ + `▶︎⚠ ${CONTENT_BLOCKED_GROUP_LABEL}`, + `simple message 1`, + ]); + + let onStorageBlockedWarningGroupMessage = waitForMessage( + hud, + STORAGE_BLOCKED_URL, + ".warn" + ); + + emitStorageAccessBlockedMessage(hud); + ({ node } = await onStorageBlockedWarningGroupMessage); + is( + node.querySelector(".warning-indent"), + null, + "The message has the expected style" + ); + is( + node.querySelector(".indent").getAttribute("data-indent"), + "0", + "The message has the expected indent" + ); + + info("Log a second simple message"); + await logString(hud, "simple message 2"); + + checkConsoleOutputForWarningGroup(hud, [ + `▶︎⚠ ${CONTENT_BLOCKED_GROUP_LABEL}`, + `simple message 1`, + `${STORAGE_BLOCKED_URL}`, + `simple message 2`, + ]); + + info( + "Log a second storage blocked message to check that it creates another group" + ); + onStorageBlockedWarningGroupMessage = waitForMessage( + hud, + STORAGE_BLOCKED_GROUP_LABEL, + ".warn" + ); + emitStorageAccessBlockedMessage(hud); + const { + node: storageBlockedWarningGroupNode, + } = await onStorageBlockedWarningGroupMessage; + is( + storageBlockedWarningGroupNode.querySelector(".warning-group-badge") + .textContent, + "2", + "The badge has the expected text" + ); + + info("Expand the second warning group"); + storageBlockedWarningGroupNode.querySelector(".arrow").click(); + await waitFor(() => findMessage(hud, STORAGE_BLOCKED_URL)); + + checkConsoleOutputForWarningGroup(hud, [ + `▶︎⚠ ${CONTENT_BLOCKED_GROUP_LABEL}`, + `simple message 1`, + `▼︎⚠ ${STORAGE_BLOCKED_GROUP_LABEL}`, + `| ${STORAGE_BLOCKED_URL}?3`, + `| ${STORAGE_BLOCKED_URL}?4`, + `simple message 2`, + ]); + + info( + "Add another storage blocked message to check it does go into the opened group" + ); + let onStorageBlockedMessage = waitForMessage( + hud, + STORAGE_BLOCKED_URL, + ".warn" + ); + emitStorageAccessBlockedMessage(hud); + await onStorageBlockedMessage; + + checkConsoleOutputForWarningGroup(hud, [ + `▶︎⚠ ${CONTENT_BLOCKED_GROUP_LABEL}`, + `simple message 1`, + `▼︎⚠ ${STORAGE_BLOCKED_GROUP_LABEL}`, + `| ${STORAGE_BLOCKED_URL}?3`, + `| ${STORAGE_BLOCKED_URL}?4`, + `| ${STORAGE_BLOCKED_URL}?5`, + `simple message 2`, + ]); + + info( + "Add a content blocked message to check the first group badge is updated" + ); + emitContentBlockingMessage(); + await waitForBadgeNumber(contentBlockedWarningGroupNode, 3); + + info("Expand the first warning group"); + contentBlockedWarningGroupNode.querySelector(".arrow").click(); + await waitFor(() => findMessage(hud, CONTENT_BLOCKED_URL)); + + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${CONTENT_BLOCKED_GROUP_LABEL}`, + `| ${CONTENT_BLOCKED_URL}?1`, + `| ${CONTENT_BLOCKED_URL}?2`, + `| ${CONTENT_BLOCKED_URL}?6`, + `simple message 1`, + `▼︎⚠ ${STORAGE_BLOCKED_GROUP_LABEL}`, + `| ${STORAGE_BLOCKED_URL}?3`, + `| ${STORAGE_BLOCKED_URL}?4`, + `| ${STORAGE_BLOCKED_URL}?5`, + `simple message 2`, + ]); + + info("Reload the page and wait for it to be ready"); + await reloadPage(); + + // Also wait for the navigation message to be displayed. + await waitFor(() => findMessage(hud, "Navigated to")); + + info("Add a storage blocked message and a content blocked one"); + onStorageBlockedMessage = waitForMessage(hud, STORAGE_BLOCKED_URL, ".warn"); + emitStorageAccessBlockedMessage(hud); + await onStorageBlockedMessage; + + onContentBlockedMessage = waitForMessage(hud, CONTENT_BLOCKED_URL, ".warn"); + emitContentBlockingMessage(hud); + await onContentBlockedMessage; + + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${CONTENT_BLOCKED_GROUP_LABEL}`, + `| ${CONTENT_BLOCKED_URL}?1`, + `| ${CONTENT_BLOCKED_URL}?2`, + `| ${CONTENT_BLOCKED_URL}?6`, + `simple message 1`, + `▼︎⚠ ${STORAGE_BLOCKED_GROUP_LABEL}`, + `| ${STORAGE_BLOCKED_URL}?3`, + `| ${STORAGE_BLOCKED_URL}?4`, + `| ${STORAGE_BLOCKED_URL}?5`, + `simple message 2`, + `Navigated to`, + `${STORAGE_BLOCKED_URL}?7`, + `${CONTENT_BLOCKED_URL}?8`, + ]); + + info( + "Add a storage blocked message and a content blocked one to create warningGroups" + ); + onStorageBlockedWarningGroupMessage = waitForMessage( + hud, + STORAGE_BLOCKED_GROUP_LABEL, + ".warn" + ); + emitStorageAccessBlockedMessage(); + await onStorageBlockedWarningGroupMessage; + + onContentBlockedWarningGroupMessage = waitForMessage( + hud, + CONTENT_BLOCKED_GROUP_LABEL, + ".warn" + ); + emitContentBlockingMessage(); + await onContentBlockedWarningGroupMessage; + + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${CONTENT_BLOCKED_GROUP_LABEL}`, + `| ${CONTENT_BLOCKED_URL}?1`, + `| ${CONTENT_BLOCKED_URL}?2`, + `| ${CONTENT_BLOCKED_URL}?6`, + `simple message 1`, + `▼︎⚠ ${STORAGE_BLOCKED_GROUP_LABEL}`, + `| ${STORAGE_BLOCKED_URL}?3`, + `| ${STORAGE_BLOCKED_URL}?4`, + `| ${STORAGE_BLOCKED_URL}?5`, + `simple message 2`, + `Navigated to`, + `▶︎⚠ ${STORAGE_BLOCKED_GROUP_LABEL}`, + `▶︎⚠ ${CONTENT_BLOCKED_GROUP_LABEL}`, + ]); +}); + +let cpt = 0; +const now = Date.now(); + +/** + * Emit a Content Blocking message. This is done by loading an image from an origin + * tagged as tracker. The image is loaded with a incremented counter query parameter + * each time so we can get the warning message. + */ +function emitContentBlockingMessage() { + const url = `${CONTENT_BLOCKED_URL}?${++cpt}-${now}`; + SpecialPowers.spawn(gBrowser.selectedBrowser, [url], function(innerURL) { + content.wrappedJSObject.loadImage(innerURL); + }); +} + +/** + * Emit a Storage blocked message. This is done by loading an image from a different + * origin, with the cookier permission rejecting foreign origin cookie access. + */ +function emitStorageAccessBlockedMessage() { + const url = `${STORAGE_BLOCKED_URL}?${++cpt}-${now}`; + SpecialPowers.spawn(gBrowser.selectedBrowser, [url], function(innerURL) { + content.wrappedJSObject.loadImage(innerURL); + }); +} + +/** + * Log a string from the content page. + * + * @param {WebConsole} hud + * @param {String} str + */ +function logString(hud, str) { + const onMessage = waitForMessage(hud, str); + SpecialPowers.spawn(gBrowser.selectedBrowser, [str], function(arg) { + content.console.log(arg); + }); + return onMessage; +} + +function waitForBadgeNumber(messageNode, expectedNumber) { + return waitFor( + () => + messageNode.querySelector(".warning-group-badge").textContent == + expectedNumber + ); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_warning_group_storage_isolation.js b/devtools/client/webconsole/test/browser/browser_webconsole_warning_group_storage_isolation.js new file mode 100644 index 0000000000..a14976bdf9 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_warning_group_storage_isolation.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Load a third-party page that sets a cookie and make sure a warning about +// partitioned storage access appears in the console. Also test that multiple +// such warnings are grouped together. + +"use strict"; +requestLongerTimeout(2); + +const TEST_PATH = "browser/devtools/client/webconsole/test/browser/"; +const TEST_FILE = TEST_PATH + "test-warning-groups.html"; +const TEST_URI = "http://example.com/" + TEST_FILE; + +const PARTITIONED_URL = + "https://example.org/" + TEST_PATH + "cookieSetter.html"; + +const STORAGE_ISOLATION_GROUP_LABEL = + `Partitioned cookie or storage access was provided to “<URL>” because it is ` + + `loaded in the third-party context and storage partitioning is enabled.`; + +const COOKIE_BEHAVIOR_PREF = "network.cookie.cookieBehavior"; +const COOKIE_BEHAVIOR_PARTITION_FOREIGN = 5; + +pushPref("devtools.webconsole.groupWarningMessages", true); + +async function cleanUp() { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +} + +add_task(async function testStorageIsolationMessage() { + await cleanUp(); + + await pushPref(COOKIE_BEHAVIOR_PREF, COOKIE_BEHAVIOR_PARTITION_FOREIGN); + const { hud, tab, win } = await openNewWindowAndConsole(TEST_URI); + const now = Date.now(); + + const getWarningMessage = url => + STORAGE_ISOLATION_GROUP_LABEL.replace("<URL>", url); + + info("Test storage isolation message"); + const url1 = `${PARTITIONED_URL}?1&${now}`; + const message = getWarningMessage(url1); + const onStorageIsolationWarningMessage = waitForMessage( + hud, + message, + ".warn" + ); + emitStorageIsolationMessage(tab, url1); + await onStorageIsolationWarningMessage; + + ok(true, "The storage isolation message was displayed"); + + info( + "Emit a new storage isolation message to check that it causes a grouping" + ); + const onStorageIsolationWarningGroupMessage = waitForMessage( + hud, + STORAGE_ISOLATION_GROUP_LABEL, + ".warn" + ); + const url2 = `${PARTITIONED_URL}?2&${now}`; + emitStorageIsolationMessage(tab, url2); + const { node } = await onStorageIsolationWarningGroupMessage; + is( + node.querySelector(".warning-group-badge").textContent, + "2", + "The badge has the expected text" + ); + + checkConsoleOutputForWarningGroup(hud, [ + `▶︎⚠ ${STORAGE_ISOLATION_GROUP_LABEL} 2`, + ]); + + info("Open the group"); + node.querySelector(".arrow").click(); + await waitFor(() => findMessage(hud, url1)); + + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${STORAGE_ISOLATION_GROUP_LABEL} 2`, + `| ${getWarningMessage(url1)}`, + `| ${getWarningMessage(url2)}`, + ]); + await win.close(); + + await cleanUp(); +}); + +/** + * Emit a Storage Isolation message. This is done by loading an iframe with a + * third-party resource. The iframe is loaded with a incremented counter query + * parameter each time so we can get the warning message. + */ +function emitStorageIsolationMessage(tab, url) { + SpecialPowers.spawn(tab.linkedBrowser, [url], function(innerURL) { + content.wrappedJSObject.loadIframe(innerURL); + }); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_warning_groups.js b/devtools/client/webconsole/test/browser/browser_webconsole_warning_groups.js new file mode 100644 index 0000000000..40a0578922 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_warning_groups.js @@ -0,0 +1,276 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that warning messages can be grouped, per navigation and category, and that +// interacting with these groups works as expected. + +"use strict"; +requestLongerTimeout(2); + +const TEST_FILE = + "browser/devtools/client/webconsole/test/browser/test-warning-groups.html"; +const TEST_URI = "http://example.org/" + TEST_FILE; + +const TRACKER_URL = "http://tracking.example.com/"; +const BLOCKED_URL = + TRACKER_URL + + "browser/devtools/client/webconsole/test/browser/test-image.png"; + +const { UrlClassifierTestUtils } = ChromeUtils.import( + "resource://testing-common/UrlClassifierTestUtils.jsm" +); +UrlClassifierTestUtils.addTestTrackers(); +registerCleanupFunction(function() { + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +pushPref("privacy.trackingprotection.enabled", true); +pushPref("devtools.webconsole.groupWarningMessages", true); + +const CONTENT_BLOCKING_GROUP_LABEL = + "The resource at “<URL>” was blocked because content blocking is enabled."; + +add_task(async function testContentBlockingMessage() { + // Enable groupWarning and persist log + await pushPref("devtools.webconsole.persistlog", true); + + const hud = await openNewTabAndConsole(TEST_URI); + + info( + "Log a tracking protection message to check a single message isn't grouped" + ); + let onContentBlockingWarningMessage = waitForMessage( + hud, + BLOCKED_URL, + ".warn" + ); + emitContentBlockedMessage(hud); + let { node } = await onContentBlockingWarningMessage; + is( + node.querySelector(".warning-indent"), + null, + "The message has the expected style" + ); + is( + node.querySelector(".indent").getAttribute("data-indent"), + "0", + "The message has the expected indent" + ); + + info("Log a simple message"); + await logString(hud, "simple message 1"); + + info( + "Log a second tracking protection message to check that it causes the grouping" + ); + let onContentBlockingWarningGroupMessage = waitForMessage( + hud, + CONTENT_BLOCKING_GROUP_LABEL, + ".warn" + ); + emitContentBlockedMessage(hud); + ({ node } = await onContentBlockingWarningGroupMessage); + is( + node.querySelector(".warning-group-badge").textContent, + "2", + "The badge has the expected text" + ); + + checkConsoleOutputForWarningGroup(hud, [ + `▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `simple message 1`, + ]); + + info("Log another simple message"); + await logString(hud, "simple message 2"); + + checkConsoleOutputForWarningGroup(hud, [ + `▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `simple message 1`, + `simple message 2`, + ]); + + info( + "Log a third tracking protection message to check that the badge updates" + ); + emitContentBlockedMessage(hud); + await waitFor( + () => node.querySelector(".warning-group-badge").textContent == "3" + ); + + checkConsoleOutputForWarningGroup(hud, [ + `▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `simple message 1`, + `simple message 2`, + ]); + + info("Open the group"); + node.querySelector(".arrow").click(); + await waitFor(() => findMessage(hud, BLOCKED_URL)); + + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `| ${BLOCKED_URL}?1`, + `| ${BLOCKED_URL}?2`, + `| ${BLOCKED_URL}?3`, + `simple message 1`, + `simple message 2`, + ]); + + info( + "Log a new tracking protection message to check it appears inside the group" + ); + onContentBlockingWarningMessage = waitForMessage(hud, BLOCKED_URL, ".warn"); + emitContentBlockedMessage(hud); + await onContentBlockingWarningMessage; + ok(true, "The new tracking protection message is displayed"); + + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `| ${BLOCKED_URL}?1`, + `| ${BLOCKED_URL}?2`, + `| ${BLOCKED_URL}?3`, + `| ${BLOCKED_URL}?4`, + `simple message 1`, + `simple message 2`, + ]); + + info("Reload the page and wait for it to be ready"); + await reloadPage(); + + // Also wait for the navigation message to be displayed. + await waitFor(() => findMessage(hud, "Navigated to")); + + info("Log a tracking protection message to check it is not grouped"); + onContentBlockingWarningMessage = waitForMessage(hud, BLOCKED_URL, ".warn"); + emitContentBlockedMessage(hud); + await onContentBlockingWarningMessage; + + await logString(hud, "simple message 3"); + + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `| ${BLOCKED_URL}?1`, + `| ${BLOCKED_URL}?2`, + `| ${BLOCKED_URL}?3`, + `| ${BLOCKED_URL}?4`, + `simple message 1`, + `simple message 2`, + "Navigated to", + `${BLOCKED_URL}?5`, + `simple message 3`, + ]); + + info( + "Log a second tracking protection message to check that it causes the grouping" + ); + onContentBlockingWarningGroupMessage = waitForMessage( + hud, + CONTENT_BLOCKING_GROUP_LABEL, + ".warn" + ); + emitContentBlockedMessage(hud); + ({ node } = await onContentBlockingWarningGroupMessage); + is( + node.querySelector(".warning-group-badge").textContent, + "2", + "The badge has the expected text" + ); + + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `| ${BLOCKED_URL}?1`, + `| ${BLOCKED_URL}?2`, + `| ${BLOCKED_URL}?3`, + `| ${BLOCKED_URL}?4`, + `simple message 1`, + `simple message 2`, + `Navigated to`, + `▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `simple message 3`, + ]); + + info("Check that opening this group works"); + node.querySelector(".arrow").click(); + await waitFor(() => findMessages(hud, BLOCKED_URL).length === 6); + + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `| ${BLOCKED_URL}?1`, + `| ${BLOCKED_URL}?2`, + `| ${BLOCKED_URL}?3`, + `| ${BLOCKED_URL}?4`, + `simple message 1`, + `simple message 2`, + `Navigated to`, + `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `| ${BLOCKED_URL}?5`, + `| ${BLOCKED_URL}?6`, + `simple message 3`, + ]); + + info("Check that closing this group works, and let the other one open"); + node.querySelector(".arrow").click(); + await waitFor(() => findMessages(hud, BLOCKED_URL).length === 4); + + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `| ${BLOCKED_URL}?1`, + `| ${BLOCKED_URL}?2`, + `| ${BLOCKED_URL}?3`, + `| ${BLOCKED_URL}?4`, + `simple message 1`, + `simple message 2`, + `Navigated to`, + `▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `simple message 3`, + ]); + + info( + "Log a third tracking protection message to check that the badge updates" + ); + emitContentBlockedMessage(hud); + await waitFor( + () => node.querySelector(".warning-group-badge").textContent == "3" + ); + + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `| ${BLOCKED_URL}?1`, + `| ${BLOCKED_URL}?2`, + `| ${BLOCKED_URL}?3`, + `| ${BLOCKED_URL}?4`, + `simple message 1`, + `simple message 2`, + `Navigated to`, + `▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `simple message 3`, + ]); +}); + +let cpt = 0; +/** + * Emit a Content Blocking message. This is done by loading an image from an origin + * tagged as tracker. The image is loaded with a incremented counter query parameter + * each time so we can get the warning message. + */ +function emitContentBlockedMessage() { + const url = `${BLOCKED_URL}?${++cpt}`; + SpecialPowers.spawn(gBrowser.selectedBrowser, [url], function(innerURL) { + content.wrappedJSObject.loadImage(innerURL); + }); +} + +/** + * Log a string from the content page. + * + * @param {WebConsole} hud + * @param {String} str + */ +function logString(hud, str) { + const onMessage = waitForMessage(hud, str); + SpecialPowers.spawn(gBrowser.selectedBrowser, [str], function(arg) { + content.console.log(arg); + }); + return onMessage; +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_warning_groups_filtering.js b/devtools/client/webconsole/test/browser/browser_webconsole_warning_groups_filtering.js new file mode 100644 index 0000000000..246ec2ca53 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_warning_groups_filtering.js @@ -0,0 +1,327 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that filtering the console output when there are warning groups works as expected. + +"use strict"; +requestLongerTimeout(2); + +const TEST_FILE = + "browser/devtools/client/webconsole/test/browser/test-warning-groups.html"; +const TEST_URI = "http://example.org/" + TEST_FILE; + +const TRACKER_URL = "http://tracking.example.com/"; +const BLOCKED_URL = + TRACKER_URL + + "browser/devtools/client/webconsole/test/browser/test-image.png"; + +const { UrlClassifierTestUtils } = ChromeUtils.import( + "resource://testing-common/UrlClassifierTestUtils.jsm" +); +UrlClassifierTestUtils.addTestTrackers(); +registerCleanupFunction(function() { + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +pushPref("privacy.trackingprotection.enabled", true); +pushPref("devtools.webconsole.groupWarningMessages", true); + +const CONTENT_BLOCKING_GROUP_LABEL = + "The resource at “<URL>” was blocked because content blocking is enabled."; + +add_task(async function testContentBlockingMessage() { + // Enable groupWarning and persist log + await pushPref("devtools.webconsole.persistlog", true); + + const hud = await openNewTabAndConsole(TEST_URI); + + info("Log a few content blocking messages and simple ones"); + let onContentBlockingWarningMessage = waitForMessage( + hud, + BLOCKED_URL, + ".warn" + ); + emitContentBlockedMessage(hud); + await onContentBlockingWarningMessage; + await logStrings(hud, "simple message A"); + let onContentBlockingWarningGroupMessage = waitForMessage( + hud, + CONTENT_BLOCKING_GROUP_LABEL, + ".warn" + ); + emitContentBlockedMessage(hud); + const warningGroupMessage1 = (await onContentBlockingWarningGroupMessage) + .node; + await logStrings(hud, "simple message B"); + emitContentBlockedMessage(hud); + await waitForBadgeNumber(warningGroupMessage1, "3"); + emitContentBlockedMessage(hud); + await waitForBadgeNumber(warningGroupMessage1, "4"); + + info("Reload the page and wait for it to be ready"); + await reloadPage(); + + // Wait for the navigation message to be displayed. + await waitFor(() => findMessage(hud, "Navigated to")); + + onContentBlockingWarningMessage = waitForMessage(hud, BLOCKED_URL, ".warn"); + emitContentBlockedMessage(hud); + await onContentBlockingWarningMessage; + await logStrings(hud, "simple message C"); + onContentBlockingWarningGroupMessage = waitForMessage( + hud, + CONTENT_BLOCKING_GROUP_LABEL, + ".warn" + ); + emitContentBlockedMessage(hud); + const warningGroupMessage2 = (await onContentBlockingWarningGroupMessage) + .node; + emitContentBlockedMessage(hud); + await waitForBadgeNumber(warningGroupMessage2, "3"); + + checkConsoleOutputForWarningGroup(hud, [ + `▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `simple message A #1`, + `simple message A #2`, + `simple message B #1`, + `simple message B #2`, + `Navigated to`, + `▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `simple message C #1`, + `simple message C #2`, + ]); + + info("Filter warnings"); + await setFilterState(hud, { warn: false }); + await waitFor(() => !findMessage(hud, CONTENT_BLOCKING_GROUP_LABEL)); + + checkConsoleOutputForWarningGroup(hud, [ + `simple message A #1`, + `simple message A #2`, + `simple message B #1`, + `simple message B #2`, + `Navigated to`, + `simple message C #1`, + `simple message C #2`, + ]); + + info("Display warning messages again"); + await setFilterState(hud, { warn: true }); + await waitFor(() => findMessage(hud, CONTENT_BLOCKING_GROUP_LABEL)); + + checkConsoleOutputForWarningGroup(hud, [ + `▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `simple message A #1`, + `simple message A #2`, + `simple message B #1`, + `simple message B #2`, + `Navigated to`, + `▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `simple message C #1`, + `simple message C #2`, + ]); + + info("Expand the first warning group"); + findMessages(hud, CONTENT_BLOCKING_GROUP_LABEL)[0] + .querySelector(".arrow") + .click(); + await waitFor(() => findMessage(hud, BLOCKED_URL)); + + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `| ${BLOCKED_URL}?1`, + `| ${BLOCKED_URL}?2`, + `| ${BLOCKED_URL}?3`, + `| ${BLOCKED_URL}?4`, + `simple message A #1`, + `simple message A #2`, + `simple message B #1`, + `simple message B #2`, + `Navigated to`, + `▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `simple message C #1`, + `simple message C #2`, + ]); + + info("Filter warnings"); + await setFilterState(hud, { warn: false }); + await waitFor(() => !findMessage(hud, CONTENT_BLOCKING_GROUP_LABEL)); + + checkConsoleOutputForWarningGroup(hud, [ + `simple message A #1`, + `simple message A #2`, + `simple message B #1`, + `simple message B #2`, + `Navigated to`, + `simple message C #1`, + `simple message C #2`, + ]); + + info("Display warning messages again"); + await setFilterState(hud, { warn: true }); + await waitFor(() => findMessage(hud, CONTENT_BLOCKING_GROUP_LABEL)); + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `| ${BLOCKED_URL}?1`, + `| ${BLOCKED_URL}?2`, + `| ${BLOCKED_URL}?3`, + `| ${BLOCKED_URL}?4`, + `simple message A #1`, + `simple message A #2`, + `simple message B #1`, + `simple message B #2`, + `Navigated to`, + `▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `simple message C #1`, + `simple message C #2`, + ]); + + info("Filter on warning group text"); + await setFilterState(hud, { text: CONTENT_BLOCKING_GROUP_LABEL }); + await waitFor(() => !findMessage(hud, "simple message")); + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `| ${BLOCKED_URL}?1`, + `| ${BLOCKED_URL}?2`, + `| ${BLOCKED_URL}?3`, + `| ${BLOCKED_URL}?4`, + `Navigated to`, + `▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + ]); + + info("Open the second warning group"); + findMessages(hud, CONTENT_BLOCKING_GROUP_LABEL)[1] + .querySelector(".arrow") + .click(); + await waitFor(() => findMessage(hud, "?6")); + + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `| ${BLOCKED_URL}?1`, + `| ${BLOCKED_URL}?2`, + `| ${BLOCKED_URL}?3`, + `| ${BLOCKED_URL}?4`, + `Navigated to`, + `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `| ${BLOCKED_URL}?5`, + `| ${BLOCKED_URL}?6`, + `| ${BLOCKED_URL}?7`, + ]); + + info("Filter on warning message text from a single warning group"); + await setFilterState(hud, { text: "/\\?(2|4)/" }); + await waitFor(() => !findMessage(hud, "?1")); + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `| ${BLOCKED_URL}?2`, + `| ${BLOCKED_URL}?4`, + `Navigated to`, + ]); + + info("Filter on warning message text from two warning groups"); + await setFilterState(hud, { text: "/\\?(3|6|7)/" }); + await waitFor(() => findMessage(hud, "?7")); + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `| ${BLOCKED_URL}?3`, + `Navigated to`, + `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `| ${BLOCKED_URL}?6`, + `| ${BLOCKED_URL}?7`, + ]); + + info("Clearing text filter"); + await setFilterState(hud, { text: "" }); + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `| ${BLOCKED_URL}?1`, + `| ${BLOCKED_URL}?2`, + `| ${BLOCKED_URL}?3`, + `| ${BLOCKED_URL}?4`, + `simple message A #1`, + `simple message A #2`, + `simple message B #1`, + `simple message B #2`, + `Navigated to`, + `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `| ${BLOCKED_URL}?5`, + `| ${BLOCKED_URL}?6`, + `| ${BLOCKED_URL}?7`, + `simple message C #1`, + `simple message C #2`, + ]); + + info("Filter warnings with two opened warning groups"); + await setFilterState(hud, { warn: false }); + await waitFor(() => !findMessage(hud, CONTENT_BLOCKING_GROUP_LABEL)); + checkConsoleOutputForWarningGroup(hud, [ + `simple message A #1`, + `simple message A #2`, + `simple message B #1`, + `simple message B #2`, + `Navigated to`, + `simple message C #1`, + `simple message C #2`, + ]); + + info("Display warning messages again with two opened warning groups"); + await setFilterState(hud, { warn: true }); + await waitFor(() => findMessage(hud, CONTENT_BLOCKING_GROUP_LABEL)); + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `| ${BLOCKED_URL}?1`, + `| ${BLOCKED_URL}?2`, + `| ${BLOCKED_URL}?3`, + `| ${BLOCKED_URL}?4`, + `simple message A #1`, + `simple message A #2`, + `simple message B #1`, + `simple message B #2`, + `Navigated to`, + `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `| ${BLOCKED_URL}?5`, + `| ${BLOCKED_URL}?6`, + `| ${BLOCKED_URL}?7`, + `simple message C #1`, + `simple message C #2`, + ]); +}); + +let cpt = 0; +/** + * Emit a Content Blocking message. This is done by loading an image from an origin + * tagged as tracker. The image is loaded with a incremented counter query parameter + * each time so we can get the warning message. + */ +function emitContentBlockedMessage(hud) { + const url = `${BLOCKED_URL}?${++cpt}`; + SpecialPowers.spawn(gBrowser.selectedBrowser, [url], function(innerURL) { + content.wrappedJSObject.loadImage(innerURL); + }); +} + +/** + * Log 2 string messages from the content page. This is done in order to increase the + * chance to have messages sharing the same timestamp (and making sure filtering and + * ordering still works fine). + * + * @param {WebConsole} hud + * @param {String} str + */ +function logStrings(hud, str) { + const onFirstMessage = waitForMessage(hud, `${str} #1`); + const onSecondMessage = waitForMessage(hud, `${str} #2`); + SpecialPowers.spawn(gBrowser.selectedBrowser, [str], function(arg) { + content.console.log(arg, "#1"); + content.console.log(arg, "#2"); + }); + return Promise.all([onFirstMessage, onSecondMessage]); +} + +function waitForBadgeNumber(message, expectedNumber) { + return waitFor( + () => + message.querySelector(".warning-group-badge").textContent == + expectedNumber + ); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_warning_groups_outside_console_group.js b/devtools/client/webconsole/test/browser/browser_webconsole_warning_groups_outside_console_group.js new file mode 100644 index 0000000000..428ca7b286 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_warning_groups_outside_console_group.js @@ -0,0 +1,212 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that warning groups are not created outside console.group. + +"use strict"; +requestLongerTimeout(2); + +const TEST_FILE = + "browser/devtools/client/webconsole/test/browser/test-warning-groups.html"; +const TEST_URI = "http://example.org/" + TEST_FILE; + +const TRACKER_URL = "http://tracking.example.com/"; +const BLOCKED_URL = + TRACKER_URL + + "browser/devtools/client/webconsole/test/browser/test-image.png"; + +const { UrlClassifierTestUtils } = ChromeUtils.import( + "resource://testing-common/UrlClassifierTestUtils.jsm" +); +UrlClassifierTestUtils.addTestTrackers(); +registerCleanupFunction(function() { + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +// Tracking protection preferences +pushPref("privacy.trackingprotection.enabled", true); + +const CONTENT_BLOCKING_GROUP_LABEL = + "The resource at “<URL>” was blocked because content blocking is enabled."; + +add_task(async function testContentBlockingMessage() { + // Enable groupWarning and persist log + await pushPref("devtools.webconsole.groupWarningMessages", true); + + const hud = await openNewTabAndConsole(TEST_URI); + + info("Log a console.group"); + const onGroupMessage = waitForMessage(hud, "myGroup"); + let onInGroupMessage = waitForMessage(hud, "log in group"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.console.group("myGroup"); + content.wrappedJSObject.console.log("log in group"); + }); + const { node: consoleGroupMessageNode } = await onGroupMessage; + await onInGroupMessage; + + checkConsoleOutputForWarningGroup(hud, [`▼ myGroup`, `| log in group`]); + + info( + "Log a tracking protection message to check a single message isn't grouped" + ); + const now = Date.now(); + let onContentBlockingWarningMessage = waitForMessage( + hud, + BLOCKED_URL, + ".warn" + ); + emitContentBlockedMessage(now); + await onContentBlockingWarningMessage; + + checkConsoleOutputForWarningGroup(hud, [ + `▼ myGroup`, + `| log in group`, + `| ${BLOCKED_URL}?${now}-1`, + ]); + + info("Collapse the console.group"); + consoleGroupMessageNode.querySelector(".arrow").click(); + await waitFor(() => !findMessage(hud, "log in group")); + + checkConsoleOutputForWarningGroup(hud, [`▶︎ myGroup`]); + + info("Expand the console.group"); + consoleGroupMessageNode.querySelector(".arrow").click(); + await waitFor(() => findMessage(hud, "log in group")); + + checkConsoleOutputForWarningGroup(hud, [ + `▼ myGroup`, + `| log in group`, + `| ${BLOCKED_URL}?${now}-1`, + ]); + + info( + "Log a second tracking protection message to check that it causes the grouping" + ); + const onContentBlockingWarningGroupMessage = waitForMessage( + hud, + CONTENT_BLOCKING_GROUP_LABEL, + ".warn" + ); + emitContentBlockedMessage(now); + const { node: warningGroupNode } = await onContentBlockingWarningGroupMessage; + + checkConsoleOutputForWarningGroup(hud, [ + `▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `▼ myGroup`, + `| log in group`, + ]); + + info("Open the warning group"); + warningGroupNode.querySelector(".arrow").click(); + await waitFor(() => findMessage(hud, BLOCKED_URL)); + + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `| ${BLOCKED_URL}?${now}-1`, + `| ${BLOCKED_URL}?${now}-2`, + `▼ myGroup`, + `| log in group`, + ]); + + info( + "Log a new tracking protection message to check it appears inside the group" + ); + onContentBlockingWarningMessage = waitForMessage(hud, BLOCKED_URL, ".warn"); + emitContentBlockedMessage(now); + await onContentBlockingWarningMessage; + ok(true, "The new tracking protection message is displayed"); + + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `| ${BLOCKED_URL}?${now}-1`, + `| ${BLOCKED_URL}?${now}-2`, + `| ${BLOCKED_URL}?${now}-3`, + `▼ myGroup`, + `| log in group`, + ]); + + info("Log a simple message to check if it goes into the console.group"); + onInGroupMessage = waitForMessage(hud, "log in group"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.console.log("second log in group"); + }); + await onInGroupMessage; + + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `| ${BLOCKED_URL}?${now}-1`, + `| ${BLOCKED_URL}?${now}-2`, + `| ${BLOCKED_URL}?${now}-3`, + `▼ myGroup`, + `| log in group`, + `| second log in group`, + ]); + + info("Collapse the console.group"); + consoleGroupMessageNode.querySelector(".arrow").click(); + await waitFor(() => !findMessage(hud, "log in group")); + + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `| ${BLOCKED_URL}?${now}-1`, + `| ${BLOCKED_URL}?${now}-2`, + `| ${BLOCKED_URL}?${now}-3`, + `▶︎ myGroup`, + ]); + + info("Close the warning group"); + warningGroupNode.querySelector(".arrow").click(); + await waitFor(() => !findMessage(hud, BLOCKED_URL)); + + checkConsoleOutputForWarningGroup(hud, [ + `▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `▶︎ myGroup`, + ]); + + info("Open the console group"); + consoleGroupMessageNode.querySelector(".arrow").click(); + await waitFor(() => findMessage(hud, "log in group")); + + checkConsoleOutputForWarningGroup(hud, [ + `▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `▼ myGroup`, + `| log in group`, + `| second log in group`, + ]); + + info("Collapse the console.group"); + consoleGroupMessageNode.querySelector(".arrow").click(); + await waitFor(() => !findMessage(hud, "log in group")); + + checkConsoleOutputForWarningGroup(hud, [ + `▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `▶︎ myGroup`, + ]); + + info("Open the warning group"); + warningGroupNode.querySelector(".arrow").click(); + await waitFor(() => findMessage(hud, BLOCKED_URL)); + + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `| ${BLOCKED_URL}?${now}-1`, + `| ${BLOCKED_URL}?${now}-2`, + `| ${BLOCKED_URL}?${now}-3`, + `▶︎ myGroup`, + ]); +}); + +let cpt = 0; +/** + * Emit a Content Blocking message. This is done by loading an image from an origin + * tagged as tracker. The image is loaded with a incremented counter query parameter + * each time so we can get the warning message. + */ +function emitContentBlockedMessage(prefix) { + const url = `${BLOCKED_URL}?${prefix}-${++cpt}`; + SpecialPowers.spawn(gBrowser.selectedBrowser, [url], function(innerURL) { + content.wrappedJSObject.loadImage(innerURL); + }); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_warning_groups_toggle.js b/devtools/client/webconsole/test/browser/browser_webconsole_warning_groups_toggle.js new file mode 100644 index 0000000000..f8757eee51 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_warning_groups_toggle.js @@ -0,0 +1,278 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that filtering the console output when there are warning groups works as expected. + +"use strict"; +requestLongerTimeout(2); + +const { PrefObserver } = require("devtools/client/shared/prefs"); + +const TEST_FILE = + "browser/devtools/client/webconsole/test/browser/test-warning-groups.html"; +const TEST_URI = "http://example.org/" + TEST_FILE; + +const TRACKER_URL = "http://tracking.example.com/"; +const BLOCKED_URL = + TRACKER_URL + + "browser/devtools/client/webconsole/test/browser/test-image.png"; +const WARNING_GROUP_PREF = "devtools.webconsole.groupWarningMessages"; + +const { UrlClassifierTestUtils } = ChromeUtils.import( + "resource://testing-common/UrlClassifierTestUtils.jsm" +); +UrlClassifierTestUtils.addTestTrackers(); +registerCleanupFunction(function() { + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +pushPref("privacy.trackingprotection.enabled", true); + +const CONTENT_BLOCKING_GROUP_LABEL = + "The resource at “<URL>” was blocked because content blocking is enabled."; + +add_task(async function testContentBlockingMessage() { + // Enable persist log + await pushPref("devtools.webconsole.persistlog", true); + + // Start with the warningGroup pref set to false. + await pushPref(WARNING_GROUP_PREF, false); + + const hud = await openNewTabAndConsole(TEST_URI); + + info("Log a few content blocking messages and simple ones"); + let onContentBlockingWarningMessage = waitForMessage( + hud, + `${BLOCKED_URL}?1`, + ".warn" + ); + emitContentBlockedMessage(hud); + await onContentBlockingWarningMessage; + await logString(hud, "simple message 1"); + + onContentBlockingWarningMessage = waitForMessage( + hud, + `${BLOCKED_URL}?2`, + ".warn" + ); + emitContentBlockedMessage(hud); + await onContentBlockingWarningMessage; + + onContentBlockingWarningMessage = waitForMessage( + hud, + `${BLOCKED_URL}?3`, + ".warn" + ); + emitContentBlockedMessage(hud); + await onContentBlockingWarningMessage; + + checkConsoleOutputForWarningGroup(hud, [ + `${BLOCKED_URL}?1`, + `simple message 1`, + `${BLOCKED_URL}?2`, + `${BLOCKED_URL}?3`, + ]); + + info("Enable the warningGroup feature pref and check warnings were grouped"); + await toggleWarningGroupPreference(hud); + let warningGroupMessage1 = await waitFor(() => + findMessage(hud, CONTENT_BLOCKING_GROUP_LABEL) + ); + is( + warningGroupMessage1.querySelector(".warning-group-badge").textContent, + "3", + "The badge has the expected text" + ); + + checkConsoleOutputForWarningGroup(hud, [ + `▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `simple message 1`, + ]); + + info("Add a new warning message and check it's placed in the closed group"); + emitContentBlockedMessage(hud); + await waitForBadgeNumber(warningGroupMessage1, "4"); + + info( + "Re-enable the warningGroup feature pref and check warnings are displayed" + ); + await toggleWarningGroupPreference(hud); + await waitFor(() => findMessage(hud, `${BLOCKED_URL}?4`)); + + // Warning messages are displayed at the expected positions. + checkConsoleOutputForWarningGroup(hud, [ + `${BLOCKED_URL}?1`, + `simple message 1`, + `${BLOCKED_URL}?2`, + `${BLOCKED_URL}?3`, + `${BLOCKED_URL}?4`, + ]); + + info("Re-disable the warningGroup feature pref"); + await toggleWarningGroupPreference(hud); + console.log("toggle successful"); + warningGroupMessage1 = await waitFor(() => + findMessage(hud, CONTENT_BLOCKING_GROUP_LABEL) + ); + + checkConsoleOutputForWarningGroup(hud, [ + `▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `simple message 1`, + ]); + + info("Expand the warning group"); + warningGroupMessage1.querySelector(".arrow").click(); + await waitFor(() => findMessage(hud, BLOCKED_URL)); + + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `| ${BLOCKED_URL}?1`, + `| ${BLOCKED_URL}?2`, + `| ${BLOCKED_URL}?3`, + `| ${BLOCKED_URL}?4`, + `simple message 1`, + ]); + + info("Reload the page and wait for it to be ready"); + await reloadPage(); + + // Wait for the navigation message to be displayed. + await waitFor(() => findMessage(hud, "Navigated to")); + + info("Disable the warningGroup feature pref again"); + await toggleWarningGroupPreference(hud); + + info("Add one warning message and one simple message"); + await waitFor(() => findMessage(hud, `${BLOCKED_URL}?4`)); + onContentBlockingWarningMessage = waitForMessage(hud, BLOCKED_URL, ".warn"); + emitContentBlockedMessage(hud); + await onContentBlockingWarningMessage; + await logString(hud, "simple message 2"); + + // nothing is grouped. + checkConsoleOutputForWarningGroup(hud, [ + `${BLOCKED_URL}?1`, + `simple message 1`, + `${BLOCKED_URL}?2`, + `${BLOCKED_URL}?3`, + `${BLOCKED_URL}?4`, + `Navigated to`, + `${BLOCKED_URL}?5`, + `simple message 2`, + ]); + + info( + "Enable the warningGroup feature pref to check that the group is still expanded" + ); + await toggleWarningGroupPreference(hud); + await waitFor(() => findMessage(hud, CONTENT_BLOCKING_GROUP_LABEL)); + + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `| ${BLOCKED_URL}?1`, + `| ${BLOCKED_URL}?2`, + `| ${BLOCKED_URL}?3`, + `| ${BLOCKED_URL}?4`, + `simple message 1`, + `Navigated to`, + `| ${BLOCKED_URL}?5`, + `simple message 2`, + ]); + + info( + "Add a second warning and check it's placed in the second, closed, group" + ); + const onContentBlockingWarningGroupMessage = waitForMessage( + hud, + CONTENT_BLOCKING_GROUP_LABEL, + ".warn" + ); + emitContentBlockedMessage(hud); + const warningGroupMessage2 = (await onContentBlockingWarningGroupMessage) + .node; + await waitForBadgeNumber(warningGroupMessage2, "2"); + + checkConsoleOutputForWarningGroup(hud, [ + `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `| ${BLOCKED_URL}?1`, + `| ${BLOCKED_URL}?2`, + `| ${BLOCKED_URL}?3`, + `| ${BLOCKED_URL}?4`, + `simple message 1`, + `Navigated to`, + `▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`, + `simple message 2`, + ]); + + info( + "Disable the warningGroup pref and check all warning messages are visible" + ); + await toggleWarningGroupPreference(hud); + await waitFor(() => findMessage(hud, `${BLOCKED_URL}?6`)); + + checkConsoleOutputForWarningGroup(hud, [ + `${BLOCKED_URL}?1`, + `simple message 1`, + `${BLOCKED_URL}?2`, + `${BLOCKED_URL}?3`, + `${BLOCKED_URL}?4`, + `Navigated to`, + `${BLOCKED_URL}?5`, + `simple message 2`, + `${BLOCKED_URL}?6`, + ]); + + // Clean the pref for the next tests. + Services.prefs.clearUserPref(WARNING_GROUP_PREF); +}); + +let cpt = 0; +/** + * Emit a Content Blocking message. This is done by loading an image from an origin + * tagged as tracker. The image is loaded with a incremented counter query parameter + * each time so we can get the warning message. + */ +function emitContentBlockedMessage(hud) { + const url = `${BLOCKED_URL}?${++cpt}`; + SpecialPowers.spawn(gBrowser.selectedBrowser, [url], function(innerURL) { + content.wrappedJSObject.loadImage(innerURL); + }); +} + +/** + * Log a string from the content page. + * + * @param {WebConsole} hud + * @param {String} str + */ +function logString(hud, str) { + const onMessage = waitForMessage(hud, str); + SpecialPowers.spawn(gBrowser.selectedBrowser, [str], function(arg) { + content.console.log(arg); + }); + return onMessage; +} + +function waitForBadgeNumber(message, expectedNumber) { + return waitFor( + () => + message.querySelector(".warning-group-badge").textContent == + expectedNumber + ); +} + +async function toggleWarningGroupPreference(hud) { + info("Open the settings panel"); + const observer = new PrefObserver(""); + + info("Change warning preference"); + const prefChanged = observer.once(WARNING_GROUP_PREF, () => {}); + + await toggleConsoleSetting( + hud, + ".webconsole-console-settings-menu-item-warning-groups" + ); + + await prefChanged; + observer.destroy(); +} diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_wasm_errors.js b/devtools/client/webconsole/test/browser/browser_webconsole_wasm_errors.js new file mode 100644 index 0000000000..c36490afdd --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_wasm_errors.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that WASM errors are reported to the console. + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf-8,Wasm errors`; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + const onCompileError = waitForMessage( + hud, + `Uncaught (in promise) CompileError: wasm validation error: at offset 0: failed to match magic number`, + ".error" + ); + execute(hud, `WebAssembly.instantiate(new Uint8Array())`); + await onCompileError; + ok(true, "The expected error message is displayed for CompileError"); + + const onLinkError = waitForMessage( + hud, + `Uncaught (in promise) LinkError: import object field 'f' is not a Function`, + ".error" + ); + execute( + hud, + `WebAssembly.instantiate( + new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,2,7,1,1,109,1,102,0,0]), + { m: { f: 3 } } + )` + ); + await onLinkError; + ok(true, "The expected error message is displayed for LinkError"); + + const onRuntimeError = waitForMessage( + hud, + "Uncaught RuntimeError: unreachable executed", + ".error" + ); + execute( + hud, + ` + const uintArray = new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,7,7,1,3,114,117,110,0,0,10,5,1,3,0,0,11]); + const module = new WebAssembly.Module(uintArray); + new WebAssembly.Instance(module).exports.run()` + ); + await onRuntimeError; + ok(true, "The expected error message is displayed for RuntimeError"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_websocket.js b/devtools/client/webconsole/test/browser/browser_webconsole_websocket.js new file mode 100644 index 0000000000..8d48a46a92 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_websocket.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that WebSocket connection failure messages are displayed. See Bug 603750. + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/test/browser/test-websocket.html"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + await waitFor( + () => findMessage(hud, "ws://0.0.0.0:81", ".error"), + "Did not find error message for ws://0.0.0.0:81 connection", + 500 + ); + await waitFor( + () => findMessage(hud, "ws://0.0.0.0:82", ".error"), + "Did not find error message for ws://0.0.0.0:82 connection", + 500 + ); + ok(true, "WebSocket error messages are displayed in the console"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_worker_error.js b/devtools/client/webconsole/test/browser/browser_webconsole_worker_error.js new file mode 100644 index 0000000000..2467a2df34 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_worker_error.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that throwing uncaught errors and primitive values in workers shows a +// stack in the console. + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-error-worker.html"; + +add_task(async function() { + await pushPref("javascript.options.asyncstack_capture_debuggee_only", false); + + const hud = await openNewTabAndConsole(TEST_URI); + + await checkMessageStack(hud, "hello", [13, 4, 3]); + await checkMessageStack(hud, "there", [16, 4, 3]); + await checkMessageStack(hud, "dom", [18, 4, 3]); + await checkMessageStack(hud, "worker2", [6, 3, 3]); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_worker_evaluate.js b/devtools/client/webconsole/test/browser/browser_webconsole_worker_evaluate.js new file mode 100644 index 0000000000..f85c2fd19b --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_worker_evaluate.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// When the debugger is paused in a worker thread, console evaluations should +// be performed in that worker's selected frame. + +"use strict"; + +const TEST_URI = + "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-evaluate-worker.html"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + + await openDebugger(); + const toolbox = hud.toolbox; + await waitFor(() => toolbox.store.getState().targets.targets.length == 2); + const dbg = createDebuggerContext(toolbox); + + execute(hud, "pauseInWorker(42)"); + + await waitForPaused(dbg); + await openConsole(); + + await executeAndWaitForMessage(hud, "data", "42", ".result"); + ok(true, "Evaluated console message in worker thread"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_worker_promise_error.js b/devtools/client/webconsole/test/browser/browser_webconsole_worker_promise_error.js new file mode 100644 index 0000000000..5871740aab --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_worker_promise_error.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that an uncaught promise rejection inside a Worker or Worklet +// is reported to the tabs' webconsole. + +"use strict"; + +const TEST_URI = + "https://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-worker-promise-error.html"; + +add_task(async function() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.audioworklet.enabled", true], + ["dom.worklet.enabled", true], + ], + }); + + const hud = await openNewTabAndConsole(TEST_URI); + await waitFor(() => + findMessage(hud, "uncaught exception: worker-error", ".message.error") + ); + + await waitFor(() => + findMessage(hud, "uncaught exception: worklet-error", ".message.error") + ); + + ok(true, "received error messages"); +}); diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_worklet_error.js b/devtools/client/webconsole/test/browser/browser_webconsole_worklet_error.js new file mode 100644 index 0000000000..82b57d4da5 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_worklet_error.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that syntax errors in worklet scripts show in the console and that +// throwing uncaught errors and primitive values in worklets shows a stack. + +"use strict"; + +const TEST_URI = + "https://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-error-worklet.html"; + +add_task(async function() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.audioworklet.enabled", true], + ["dom.worklet.enabled", true], + ], + }); + + const hud = await openNewTabAndConsole(TEST_URI); + + await waitFor(() => + findMessage(hud, "SyntaxError: duplicate formal argument") + ); + ok(true, "Received expected SyntaxError"); + await checkMessageStack(hud, "addModule", [18, 21]); + await checkMessageStack(hud, "process", [7, 12]); +}); diff --git a/devtools/client/webconsole/test/browser/code_bundle_invalidmap.js b/devtools/client/webconsole/test/browser/code_bundle_invalidmap.js new file mode 100644 index 0000000000..8076acd560 --- /dev/null +++ b/devtools/client/webconsole/test/browser/code_bundle_invalidmap.js @@ -0,0 +1,93 @@ +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Original source code for the cross-domain source map test. +// The generated file was made with +// webpack --devtool nosources-source-map code_nosource.js code_bundle_nosource.js +// ... and then the bundle was edited to change the source name. + + + +function f() { + console.log("here"); +} + +f(); + +// Avoid script GC. +window.f = f; + + +/***/ }) +/******/ ]); +//# sourceMappingURL=code_bundle_invalidmap.js.map diff --git a/devtools/client/webconsole/test/browser/code_bundle_invalidmap.js.map b/devtools/client/webconsole/test/browser/code_bundle_invalidmap.js.map new file mode 100644 index 0000000000..83aa54bcc5 --- /dev/null +++ b/devtools/client/webconsole/test/browser/code_bundle_invalidmap.js.map @@ -0,0 +1 @@ +!!!!!!!!!!!!!!!! not a source map !!!!!!!!!!!!!!!! diff --git a/devtools/client/webconsole/test/browser/code_bundle_nosource.js b/devtools/client/webconsole/test/browser/code_bundle_nosource.js new file mode 100644 index 0000000000..7762a82d91 --- /dev/null +++ b/devtools/client/webconsole/test/browser/code_bundle_nosource.js @@ -0,0 +1,93 @@ +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Original source code for the cross-domain source map test. +// The generated file was made with +// webpack --devtool nosources-source-map code_nosource.js code_bundle_nosource.js +// ... and then the bundle was edited to change the source name. + + + +function f() { + console.log("here"); +} + +f(); + +// Avoid script GC. +window.f = f; + + +/***/ }) +/******/ ]); +//# sourceMappingURL=code_bundle_nosource.js.map
\ No newline at end of file diff --git a/devtools/client/webconsole/test/browser/code_bundle_nosource.js.map b/devtools/client/webconsole/test/browser/code_bundle_nosource.js.map new file mode 100644 index 0000000000..f42b1a2fc3 --- /dev/null +++ b/devtools/client/webconsole/test/browser/code_bundle_nosource.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["webpack:///webpack/bootstrap 5f603779212cf1264c9b","nosuchfile.js"],"names":[],"mappings":";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAK;AACL;AACA;;AAEA;AACA;AACA;AACA,mCAA2B,0BAA0B,EAAE;AACvD,yCAAiC,eAAe;AAChD;AACA;AACA;;AAEA;AACA,8DAAsD,+DAA+D;;AAErH;AACA;;AAEA;AACA;;;;;;;;AC7DA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;;AAEA;;AAEA;AACA","file":"code_bundle_nosource.js","sourceRoot":""}
\ No newline at end of file diff --git a/devtools/client/webconsole/test/browser/code_nosource.js b/devtools/client/webconsole/test/browser/code_nosource.js new file mode 100644 index 0000000000..1234d5facf --- /dev/null +++ b/devtools/client/webconsole/test/browser/code_nosource.js @@ -0,0 +1,18 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Original source code for the cross-domain source map test. +// The generated file was made with +// webpack --devtool nosources-source-map code_nosource.js code_bundle_nosource.js +// ... and then the bundle was edited to change the source name. + +"use strict"; + +function f() { + console.log("here"); +} + +f(); + +// Avoid script GC. +window.f = f; diff --git a/devtools/client/webconsole/test/browser/cookieSetter.html b/devtools/client/webconsole/test/browser/cookieSetter.html new file mode 100644 index 0000000000..fe0ce181c9 --- /dev/null +++ b/devtools/client/webconsole/test/browser/cookieSetter.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<html> + <script> + "use strict"; + document.cookie = "name=value;SameSite=None;Secure"; + </script> +</html> diff --git a/devtools/client/webconsole/test/browser/head.js b/devtools/client/webconsole/test/browser/head.js new file mode 100644 index 0000000000..e48cad352e --- /dev/null +++ b/devtools/client/webconsole/test/browser/head.js @@ -0,0 +1,1900 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint no-unused-vars: [2, {"vars": "local"}] */ +/* import-globals-from ../../../shared/test/telemetry-test-helpers.js */ + +"use strict"; + +/* globals Task, openToolboxForTab, gBrowser */ + +// shared-head.js handles imports, constants, and utility functions +// Load the shared-head file first. +/* import-globals-from ../../../shared/test/shared-head.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js", + this +); + +// Import helpers for the new debugger +/* import-globals-from ../../../debugger/test/mochitest/helpers/context.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/debugger/test/mochitest/helpers/context.js", + this +); + +// Import helpers for the new debugger +/* import-globals-from ../../../debugger/test/mochitest/helpers.js*/ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/debugger/test/mochitest/helpers.js", + this +); + +var { + BrowserConsoleManager, +} = require("devtools/client/webconsole/browser-console-manager"); + +var WCUL10n = require("devtools/client/webconsole/utils/l10n"); +const DOCS_GA_PARAMS = `?${new URLSearchParams({ + utm_source: "mozilla", + utm_medium: "firefox-console-errors", + utm_campaign: "default", +})}`; +const GA_PARAMS = `?${new URLSearchParams({ + utm_source: "mozilla", + utm_medium: "devtools-webconsole", + utm_campaign: "default", +})}`; + +const wcActions = require("devtools/client/webconsole/actions/index"); + +registerCleanupFunction(async function() { + Services.prefs.clearUserPref("devtools.webconsole.ui.filterbar"); + + // Reset all filter prefs between tests. First flushPrefEnv in case one of the + // filter prefs has been pushed for the test + await SpecialPowers.flushPrefEnv(); + Services.prefs.getChildList("devtools.webconsole.filter").forEach(pref => { + Services.prefs.clearUserPref(pref); + }); +}); + +/** + * Add a new tab and open the toolbox in it, and select the webconsole. + * + * @param string url + * The URL for the tab to be opened. + * @param Boolean clearJstermHistory + * true (default) if the jsterm history should be cleared. + * @param String hostId (optional) + * The type of toolbox host to be used. + * @return Promise + * Resolves when the tab has been added, loaded and the toolbox has been opened. + * Resolves to the hud. + */ +async function openNewTabAndConsole(url, clearJstermHistory = true, hostId) { + const toolbox = await openNewTabAndToolbox(url, "webconsole", hostId); + const hud = toolbox.getCurrentPanel().hud; + + if (clearJstermHistory) { + // Clearing history that might have been set in previous tests. + await hud.ui.wrapper.dispatchClearHistory(); + } + + return hud; +} + +/** + * Add a new tab with iframes, open the toolbox in it, and select the webconsole. + * + * @param string url + * The URL for the tab to be opened. + * @param Arra<string> iframes + * An array of URLs that will be added to the top document. + * @return Promise + * Resolves when the tab has been added, loaded, iframes loaded, and the toolbox + * has been opened. Resolves to the hud. + */ +async function openNewTabWithIframesAndConsole(tabUrl, iframes) { + // We need to add the tab and the iframes before opening the console in case we want + // to handle remote frames (we don't support creating frames target when the toolbox + // is already open). + await addTab(tabUrl); + await ContentTask.spawn(gBrowser.selectedBrowser, iframes, async function( + urls + ) { + const iframesLoadPromises = urls.map((url, i) => { + const iframe = content.document.createElement("iframe"); + iframe.classList.add(`iframe-${i + 1}`); + const onLoadIframe = new Promise(resolve => { + iframe.addEventListener("load", resolve, { once: true }); + }); + content.document.body.append(iframe); + iframe.src = url; + return onLoadIframe; + }); + + await Promise.all(iframesLoadPromises); + }); + + return openConsole(); +} + +/** + * Open a new window with a tab,open the toolbox, and select the webconsole. + * + * @param string url + * The URL for the tab to be opened. + * @return Promise<{win, hud, tab}> + * Resolves when the tab has been added, loaded and the toolbox has been opened. + * Resolves to the toolbox. + */ +async function openNewWindowAndConsole(url) { + const win = await BrowserTestUtils.openNewBrowserWindow(); + const tab = await addTab(url, { window: win }); + win.gBrowser.selectedTab = tab; + const hud = await openConsole(tab); + return { win, hud, tab }; +} + +/** + * Subscribe to the store and log out stringinfied versions of messages. + * This is a helper function for debugging, to make is easier to see what + * happened during the test in the log. + * + * @param object hud + */ +function logAllStoreChanges(hud) { + const store = hud.ui.wrapper.getStore(); + // Adding logging each time the store is modified in order to check + // the store state in case of failure. + store.subscribe(() => { + const messages = [...store.getState().messages.messagesById.values()]; + const debugMessages = messages.map( + ({ id, type, parameters, messageText }) => { + return { id, type, parameters, messageText }; + } + ); + info( + "messages : " + + JSON.stringify(debugMessages, function(key, value) { + if (value && value.getGrip) { + return value.getGrip(); + } + return value; + }) + ); + }); +} + +/** + * Wait for messages in the web console output, resolving once they are received. + * + * @param object options + * - hud: the webconsole + * - messages: Array[Object]. An array of messages to match. + Current supported options: + * - text: Partial text match in .message-body + * - selector: {String} a selector that should match the message node. Defaults to + * ".message". + */ +function waitForMessages({ hud, messages, selector = ".message" }) { + return new Promise(resolve => { + const matchedMessages = []; + hud.ui.on("new-messages", function messagesReceived(newMessages) { + for (const message of messages) { + if (message.matched) { + continue; + } + + for (const newMessage of newMessages) { + const messageBody = newMessage.node.querySelector(`.message-body`); + if ( + messageBody && + newMessage.node.matches(selector) && + messageBody.textContent.includes(message.text) + ) { + matchedMessages.push(newMessage); + message.matched = true; + const messagesLeft = messages.length - matchedMessages.length; + info( + `Matched a message with text: "${message.text}", ` + + (messagesLeft > 0 + ? `still waiting for ${messagesLeft} messages.` + : `all messages received.`) + ); + break; + } + } + + if (matchedMessages.length === messages.length) { + hud.ui.off("new-messages", messagesReceived); + resolve(matchedMessages); + return; + } + } + }); + }); +} + +/** + * Wait for a message with the provided text and showing the provided repeat count. + * + * @param {Object} hud : the webconsole + * @param {String} text : text included in .message-body + * @param {Number} repeat : expected repeat count in .message-repeats + */ +function waitForRepeatedMessage(hud, text, repeat) { + return waitFor(() => { + // Wait for a message matching the provided text. + const node = findMessage(hud, text); + if (!node) { + return false; + } + + // Check if there is a repeat node with the expected count. + const repeatNode = node.querySelector(".message-repeats"); + if (repeatNode && parseInt(repeatNode.textContent, 10) === repeat) { + return node; + } + + return false; + }); +} + +/** + * Wait for a single message in the web console output, resolving once it is received. + * + * @param {Object} hud : the webconsole + * @param {String} text : text included in .message-body + * @param {String} selector : A selector that should match the message node. + */ +async function waitForMessage(hud, text, selector) { + const messages = await waitForMessages({ + hud, + messages: [{ text }], + selector, + }); + return messages[0]; +} + +/** + * Execute an input expression. + * + * @param {Object} hud : The webconsole. + * @param {String} input : The input expression to execute. + */ +function execute(hud, input) { + return hud.ui.wrapper.dispatchEvaluateExpression(input); +} + +/** + * Execute an input expression and wait for a message with the expected text (and an + * optional selector) to be displayed in the output. + * + * @param {Object} hud : The webconsole. + * @param {String} input : The input expression to execute. + * @param {String} matchingText : A string that should match the message body content. + * @param {String} selector : A selector that should match the message node. + */ +function executeAndWaitForMessage( + hud, + input, + matchingText, + selector = ".message" +) { + const onMessage = waitForMessage(hud, matchingText, selector); + execute(hud, input); + return onMessage; +} + +/** + * Set the input value, simulates the right keyboard event to evaluate it, depending on + * if the console is in editor mode or not, and wait for a message with the expected text + * (and an optional selector) to be displayed in the output. + * + * @param {Object} hud : The webconsole. + * @param {String} input : The input expression to execute. + * @param {String} matchingText : A string that should match the message body content. + * @param {String} selector : A selector that should match the message node. + */ +function keyboardExecuteAndWaitForMessage( + hud, + input, + matchingText, + selector = ".message" +) { + hud.jsterm.focus(); + setInputValue(hud, input); + const onMessage = waitForMessage(hud, matchingText, selector); + if (isEditorModeEnabled(hud)) { + EventUtils.synthesizeKey("KEY_Enter", { + [Services.appinfo.OS === "Darwin" ? "metaKey" : "ctrlKey"]: true, + }); + } else { + EventUtils.synthesizeKey("VK_RETURN"); + } + return onMessage; +} + +/** + * Find a message in the output. + * + * @param object hud + * The web console. + * @param string text + * A substring that can be found in the message. + * @param selector [optional] + * The selector to use in finding the message. + * @return {Node} the node corresponding the found message + */ +function findMessage(hud, text, selector = ".message") { + const elements = findMessages(hud, text, selector); + return elements.pop(); +} + +/** + * Find multiple messages in the output. + * + * @param object hud + * The web console. + * @param string text + * A substring that can be found in the message. + * @param selector [optional] + * The selector to use in finding the message. + */ +function findMessages(hud, text, selector = ".message") { + const messages = hud.ui.outputNode.querySelectorAll(selector); + const elements = Array.prototype.filter.call(messages, el => + el.textContent.includes(text) + ); + return elements; +} + +/** + * Simulate a context menu event on the provided element, and wait for the console context + * menu to open. Returns a promise that resolves the menu popup element. + * + * @param object hud + * The web console. + * @param element element + * The dom element on which the context menu event should be synthesized. + * @return promise + */ +async function openContextMenu(hud, element) { + const onConsoleMenuOpened = hud.ui.wrapper.once("menu-open"); + synthesizeContextMenuEvent(element); + await onConsoleMenuOpened; + return _getContextMenu(hud); +} + +/** + * Hide the webconsole context menu popup. Returns a promise that will resolve when the + * context menu popup is hidden or immediately if the popup can't be found. + * + * @param object hud + * The web console. + * @return promise + */ +function hideContextMenu(hud) { + const popup = _getContextMenu(hud); + if (!popup) { + return Promise.resolve(); + } + + const onPopupHidden = once(popup, "popuphidden"); + popup.hidePopup(); + return onPopupHidden; +} + +function _getContextMenu(hud) { + const toolbox = hud.toolbox; + const doc = toolbox ? toolbox.topWindow.document : hud.chromeWindow.document; + return doc.getElementById("webconsole-menu"); +} + +async function toggleConsoleSetting(hud, selector) { + const toolbox = hud.toolbox; + const doc = toolbox ? toolbox.doc : hud.chromeWindow.document; + + const menuItem = doc.querySelector(selector); + menuItem.click(); +} + +function getConsoleSettingElement(hud, selector) { + const toolbox = hud.toolbox; + const doc = toolbox ? toolbox.doc : hud.chromeWindow.document; + + return doc.querySelector(selector); +} + +function checkConsoleSettingState(hud, selector, enabled) { + const el = getConsoleSettingElement(hud, selector); + const checked = el.getAttribute("aria-checked") === "true"; + + if (enabled) { + ok(checked, "setting is enabled"); + } else { + ok(!checked, "setting is disabled"); + } +} + +/** + * Returns a promise that resolves when the node passed as an argument mutate + * according to the passed configuration. + * + * @param {Node} node - The node to observe mutations on. + * @param {Object} observeConfig - A configuration object for MutationObserver.observe. + * @returns {Promise} + */ +function waitForNodeMutation(node, observeConfig = {}) { + return new Promise(resolve => { + const observer = new MutationObserver(mutations => { + resolve(mutations); + observer.disconnect(); + }); + observer.observe(node, observeConfig); + }); +} + +/** + * Search for a given message. When found, simulate a click on the + * message's location, checking to make sure that the debugger opens + * the corresponding URL. If the message was generated by a logpoint, + * check if the corresponding logpoint editing panel is opened. + * + * @param {Object} hud + * The webconsole + * @param {Object} toolbox + * The toolbox + * @param {String} text + * The text to search for. This should be contained in the + * message. The searching is done with @see findMessage. + * @param {boolean} expectUrl + * Whether the URL in the opened source should match the link, or whether + * it is expected to be null. + * @param {boolean} expectLine + * It indicates if there is the need to check the line. + * @param {boolean} expectColumn + * It indicates if there is the need to check the column. + * @param {String} logPointExpr + * The logpoint expression + */ +async function testOpenInDebugger( + hud, + toolbox, + text, + expectUrl = true, + expectLine = true, + expectColumn = true, + logPointExpr = undefined +) { + info(`Finding message for open-in-debugger test; text is "${text}"`); + const messageNode = await waitFor(() => findMessage(hud, text)); + const frameLinkNode = messageNode.querySelector( + ".message-location .frame-link" + ); + ok(frameLinkNode, "The message does have a location link"); + await checkClickOnNode( + hud, + toolbox, + frameLinkNode, + expectUrl, + expectLine, + expectColumn, + logPointExpr + ); +} + +/** + * Helper function for testOpenInDebugger. + */ +async function checkClickOnNode( + hud, + toolbox, + frameLinkNode, + expectUrl, + expectLine, + expectColumn, + logPointExpr +) { + info("checking click on node location"); + + // If the debugger hasn't fully loaded yet and breakpoints are still being + // added when we click on the logpoint link, the logpoint panel might not + // render. Work around this for now, see bug 1592854. + await waitForTime(1000); + + const onSourceInDebuggerOpened = once(hud, "source-in-debugger-opened"); + + await EventUtils.sendMouseEvent( + { type: "click" }, + frameLinkNode.querySelector(".frame-link-filename") + ); + + await onSourceInDebuggerOpened; + + const dbg = toolbox.getPanel("jsdebugger"); + + // Wait for the source to finish loading, if it is pending. + await waitFor( + () => + !!dbg._selectors.getSelectedSource(dbg._getState()) && + !!dbg._selectors.getSelectedLocation(dbg._getState()) + ); + + if (expectUrl) { + const url = frameLinkNode.getAttribute("data-url"); + ok(url, `source url found ("${url}")`); + + is( + dbg._selectors.getSelectedSource(dbg._getState()).url, + url, + "expected source url" + ); + } + if (expectLine) { + const line = frameLinkNode.getAttribute("data-line"); + ok(line, `source line found ("${line}")`); + + is( + parseInt(dbg._selectors.getSelectedLocation(dbg._getState()).line, 10), + parseInt(line, 10), + "expected source line" + ); + } + if (expectColumn) { + const column = frameLinkNode.getAttribute("data-column"); + ok(column, `source column found ("${column}")`); + + is( + parseInt(dbg._selectors.getSelectedLocation(dbg._getState()).column, 10), + parseInt(column, 10), + "expected source column" + ); + } + + if (logPointExpr !== undefined && logPointExpr !== "") { + const inputEl = dbg.panelWin.document.activeElement; + is( + inputEl.tagName, + "TEXTAREA", + "The textarea of logpoint panel is focused" + ); + + const inputValue = inputEl.parentElement.parentElement.innerText.trim(); + is( + inputValue, + logPointExpr, + "The input in the open logpoint panel matches the logpoint expression" + ); + } +} + +/** + * Returns true if the give node is currently focused. + */ +function hasFocus(node) { + return ( + node.ownerDocument.activeElement == node && node.ownerDocument.hasFocus() + ); +} + +/** + * Get the value of the console input . + * + * @param {WebConsole} hud: The webconsole + * @returns {String}: The value of the console input. + */ +function getInputValue(hud) { + return hud.jsterm._getValue(); +} + +/** + * Set the value of the console input . + * + * @param {WebConsole} hud: The webconsole + * @param {String} value : The value to set the console input to. + */ +function setInputValue(hud, value) { + const onValueSet = hud.jsterm.once("set-input-value"); + hud.jsterm._setValue(value); + return onValueSet; +} + +/** + * Set the value of the console input and its caret position, and wait for the + * autocompletion to be updated. + * + * @param {WebConsole} hud: The webconsole + * @param {String} value : The value to set the jsterm to. + * @param {Integer} caretPosition : The index where to place the cursor. A negative + * number will place the caret at (value.length - offset) position. + * Default to value.length (caret set at the end). + * @returns {Promise} resolves when the jsterm is completed. + */ +async function setInputValueForAutocompletion( + hud, + value, + caretPosition = value.length +) { + const { jsterm } = hud; + + const initialPromises = []; + if (jsterm.autocompletePopup.isOpen) { + initialPromises.push(jsterm.autocompletePopup.once("popup-closed")); + } + setInputValue(hud, ""); + await Promise.all(initialPromises); + + // Wait for next tick. Tooltip tests sometimes fail to successively hide and + // show tooltips on Win32 debug. + await waitForTick(); + + jsterm.focus(); + + const updated = jsterm.once("autocomplete-updated"); + EventUtils.sendString(value, hud.iframeWindow); + await updated; + + // Wait for next tick. Tooltip tests sometimes fail to successively hide and + // show tooltips on Win32 debug. + await waitForTick(); + + if (caretPosition < 0) { + caretPosition = value.length + caretPosition; + } + + if (Number.isInteger(caretPosition)) { + jsterm.editor.setCursor(jsterm.editor.getPosition(caretPosition)); + } +} + +/** + * Set the value of the console input and wait for the confirm dialog to be displayed. + * + * @param {Toolbox} toolbox + * @param {WebConsole} hud + * @param {String} value : The value to set the jsterm to. + * Default to value.length (caret set at the end). + * @returns {Promise<HTMLElement>} resolves with dialog element when it is opened. + */ +async function setInputValueForGetterConfirmDialog(toolbox, hud, value) { + await setInputValueForAutocompletion(hud, value); + await waitFor(() => isConfirmDialogOpened(toolbox)); + ok(true, "The confirm dialog is displayed"); + return getConfirmDialog(toolbox); +} + +/** + * Checks if the console input has the expected completion value. + * + * @param {WebConsole} hud + * @param {String} expectedValue + * @param {String} assertionInfo: Description of the assertion passed to `is`. + */ +function checkInputCompletionValue(hud, expectedValue, assertionInfo) { + const completionValue = getInputCompletionValue(hud); + if (completionValue === null) { + ok(false, "Couldn't retrieve the completion value"); + } + + info(`Expects "${expectedValue}", is "${completionValue}"`); + is(completionValue, expectedValue, assertionInfo); +} + +/** + * Checks if the cursor on console input is at expected position. + * + * @param {WebConsole} hud + * @param {Integer} expectedCursorIndex + * @param {String} assertionInfo: Description of the assertion passed to `is`. + */ +function checkInputCursorPosition(hud, expectedCursorIndex, assertionInfo) { + const { jsterm } = hud; + is(jsterm.editor.getCursor().ch, expectedCursorIndex, assertionInfo); +} + +/** + * Checks the console input value and the cursor position given an expected string + * containing a "|" to indicate the expected cursor position. + * + * @param {WebConsole} hud + * @param {String} expectedStringWithCursor: + * String with a "|" to indicate the expected cursor position. + * For example, this is how you assert an empty value with the focus "|", + * and this indicates the value should be "test" and the cursor at the + * end of the input: "test|". + * @param {String} assertionInfo: Description of the assertion passed to `is`. + */ +function checkInputValueAndCursorPosition( + hud, + expectedStringWithCursor, + assertionInfo +) { + info(`Checking jsterm state: \n${expectedStringWithCursor}`); + if (!expectedStringWithCursor.includes("|")) { + ok( + false, + `expectedStringWithCursor must contain a "|" char to indicate cursor position` + ); + } + + const inputValue = expectedStringWithCursor.replace("|", ""); + const { jsterm } = hud; + + is(getInputValue(hud), inputValue, "console input has expected value"); + const lines = expectedStringWithCursor.split("\n"); + const lineWithCursor = lines.findIndex(line => line.includes("|")); + const { ch, line } = jsterm.editor.getCursor(); + is(line, lineWithCursor, assertionInfo + " - correct line"); + is(ch, lines[lineWithCursor].indexOf("|"), assertionInfo + " - correct ch"); +} + +/** + * Returns the console input completion value. + * + * @param {WebConsole} hud + * @returns {String} + */ +function getInputCompletionValue(hud) { + const { jsterm } = hud; + return jsterm.editor.getAutoCompletionText(); +} + +function closeAutocompletePopup(hud) { + const { jsterm } = hud; + + if (!jsterm.autocompletePopup.isOpen) { + return Promise.resolve(); + } + + const onPopupClosed = jsterm.autocompletePopup.once("popup-closed"); + const onAutocompleteUpdated = jsterm.once("autocomplete-updated"); + EventUtils.synthesizeKey("KEY_Escape"); + return Promise.all([onPopupClosed, onAutocompleteUpdated]); +} + +/** + * Returns a boolean indicating if the console input is focused. + * + * @param {WebConsole} hud + * @returns {Boolean} + */ +function isInputFocused(hud) { + const { jsterm } = hud; + const document = hud.ui.outputNode.ownerDocument; + const documentIsFocused = document.hasFocus(); + return documentIsFocused && jsterm.editor.hasFocus(); +} + +/** + * Open the JavaScript debugger. + * + * @param object options + * Options for opening the debugger: + * - tab: the tab you want to open the debugger for. + * @return object + * A promise that is resolved once the debugger opens, or rejected if + * the open fails. The resolution callback is given one argument, an + * object that holds the following properties: + * - target: the Target object for the Tab. + * - toolbox: the Toolbox instance. + * - panel: the jsdebugger panel instance. + */ +async function openDebugger(options = {}) { + if (!options.tab) { + options.tab = gBrowser.selectedTab; + } + + const target = await TargetFactory.forTab(options.tab); + let toolbox = gDevTools.getToolbox(target); + const dbgPanelAlreadyOpen = toolbox && toolbox.getPanel("jsdebugger"); + if (dbgPanelAlreadyOpen) { + await toolbox.selectTool("jsdebugger"); + + return { + target, + toolbox, + panel: toolbox.getCurrentPanel(), + }; + } + + toolbox = await gDevTools.showToolbox(target, "jsdebugger"); + const panel = toolbox.getCurrentPanel(); + + await toolbox.threadFront.getSources(); + + return { target, toolbox, panel }; +} + +async function openInspector(options = {}) { + if (!options.tab) { + options.tab = gBrowser.selectedTab; + } + + const target = await TargetFactory.forTab(options.tab); + const toolbox = await gDevTools.showToolbox(target, "inspector"); + + return toolbox.getCurrentPanel(); +} + +/** + * Open the netmonitor for the given tab, or the current one if none given. + * + * @param Element tab + * Optional tab element for which you want open the netmonitor. + * Defaults to current selected tab. + * @return Promise + * A promise that is resolved with the netmonitor panel once the netmonitor is open. + */ +async function openNetMonitor(tab) { + const target = await TargetFactory.forTab(tab || gBrowser.selectedTab); + let toolbox = await gDevTools.getToolbox(target); + if (!toolbox) { + toolbox = await gDevTools.showToolbox(target); + } + await toolbox.selectTool("netmonitor"); + return toolbox.getCurrentPanel(); +} + +/** + * Open the Web Console for the given tab, or the current one if none given. + * + * @param Element tab + * Optional tab element for which you want open the Web Console. + * Defaults to current selected tab. + * @return Promise + * A promise that is resolved with the console hud once the web console is open. + */ +async function openConsole(tab) { + const target = await TargetFactory.forTab(tab || gBrowser.selectedTab); + const toolbox = await gDevTools.showToolbox(target, "webconsole"); + return toolbox.getCurrentPanel().hud; +} + +/** + * Close the Web Console for the given tab. + * + * @param Element [tab] + * Optional tab element for which you want close the Web Console. + * Defaults to current selected tab. + * @return object + * A promise that is resolved once the web console is closed. + */ +async function closeConsole(tab = gBrowser.selectedTab) { + const target = await TargetFactory.forTab(tab); + const toolbox = gDevTools.getToolbox(target); + if (toolbox) { + await toolbox.destroy(); + } +} + +/** + * Fake clicking a link and return the URL we would have navigated to. + * This function should be used to check external links since we can't access + * network in tests. + * This can also be used to test that a click will not be fired. + * + * @param ElementNode element + * The <a> element we want to simulate click on. + * @param Object clickEventProps + * The custom properties which would be used to dispatch a click event + * @returns Promise + * A Promise that is resolved when the link click simulation occured or + * when the click is not dispatched. + * The promise resolves with an object that holds the following properties + * - link: url of the link or null(if event not fired) + * - where: "tab" if tab is active or "tabshifted" if tab is inactive + * or null(if event not fired) + */ +function simulateLinkClick(element, clickEventProps) { + return overrideOpenLink(() => { + if (clickEventProps) { + // Click on the link using the event properties. + element.dispatchEvent(clickEventProps); + } else { + // Click on the link. + element.click(); + } + }); +} + +/** + * Override the browserWindow open*Link function, executes the passed function and either + * wait for: + * - the link to be "opened" + * - 1s before timing out + * Then it puts back the original open*Link functions in browserWindow. + * + * @returns {Promise<Object>}: A promise resolving with an object of the following shape: + * - link: The link that was "opened" + * - where: If the link was opened in the background (null) or not ("tab"). + */ +function overrideOpenLink(fn) { + const browserWindow = Services.wm.getMostRecentWindow( + gDevTools.chromeWindowType + ); + + // Override LinkIn methods to prevent navigating. + const oldOpenTrustedLinkIn = browserWindow.openTrustedLinkIn; + const oldOpenWebLinkIn = browserWindow.openWebLinkIn; + + const onOpenLink = new Promise(resolve => { + const openLinkIn = function(link, where) { + browserWindow.openTrustedLinkIn = oldOpenTrustedLinkIn; + browserWindow.openWebLinkIn = oldOpenWebLinkIn; + resolve({ link: link, where }); + }; + browserWindow.openWebLinkIn = browserWindow.openTrustedLinkIn = openLinkIn; + fn(); + }); + + // Declare a timeout Promise that we can use to make sure openTrustedLinkIn or + // openWebLinkIn was not called. + let timeoutId; + const onTimeout = new Promise(function(resolve) { + timeoutId = setTimeout(() => { + browserWindow.openTrustedLinkIn = oldOpenTrustedLinkIn; + browserWindow.openWebLinkIn = oldOpenWebLinkIn; + timeoutId = null; + resolve({ link: null, where: null }); + }, 1000); + }); + + onOpenLink.then(() => { + if (timeoutId) { + clearTimeout(timeoutId); + } + }); + return Promise.race([onOpenLink, onTimeout]); +} + +/** + * Open a network request logged in the webconsole in the netmonitor panel. + * + * @param {Object} toolbox + * @param {Object} hud + * @param {String} url + * URL of the request as logged in the netmonitor. + * @param {String} urlInConsole + * (optional) Use if the logged URL in webconsole is different from the real URL. + */ +async function openMessageInNetmonitor(toolbox, hud, url, urlInConsole) { + // By default urlInConsole should be the same as the complete url. + urlInConsole = urlInConsole || url; + + const message = await waitFor(() => findMessage(hud, urlInConsole)); + + const onNetmonitorSelected = toolbox.once( + "netmonitor-selected", + (event, panel) => { + return panel; + } + ); + + const menuPopup = await openContextMenu(hud, message); + const openInNetMenuItem = menuPopup.querySelector( + "#console-menu-open-in-network-panel" + ); + ok(openInNetMenuItem, "open in network panel item is enabled"); + openInNetMenuItem.click(); + + const { panelWin } = await onNetmonitorSelected; + ok( + true, + "The netmonitor panel is selected when clicking on the network message" + ); + + const { store, windowRequire } = panelWin; + const nmActions = windowRequire( + "devtools/client/netmonitor/src/actions/index" + ); + const { getSelectedRequest } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(nmActions.batchEnable(false)); + + await waitFor(() => { + const selected = getSelectedRequest(store.getState()); + return selected && selected.url === url; + }, `network entry for the URL "${url}" wasn't found`); + + ok(true, "The attached url is correct."); + + info( + "Wait for the netmonitor headers panel to appear as it spawns RDP requests" + ); + await waitFor(() => + panelWin.document.querySelector("#headers-panel .headers-overview") + ); +} + +function selectNode(hud, node) { + const outputContainer = hud.ui.outputNode.querySelector(".webconsole-output"); + + // We must first blur the input or else we can't select anything. + outputContainer.ownerDocument.activeElement.blur(); + + const selection = outputContainer.ownerDocument.getSelection(); + const range = document.createRange(); + range.selectNodeContents(node); + selection.removeAllRanges(); + selection.addRange(range); + + return selection; +} + +async function waitForBrowserConsole() { + return new Promise(resolve => { + Services.obs.addObserver(function observer(subject) { + Services.obs.removeObserver(observer, "web-console-created"); + subject.QueryInterface(Ci.nsISupportsString); + + const hud = BrowserConsoleManager.getBrowserConsole(); + ok(hud, "browser console is open"); + is(subject.data, hud.hudId, "notification hudId is correct"); + + executeSoon(() => resolve(hud)); + }, "web-console-created"); + }); +} + +/** + * Get the state of a console filter. + * + * @param {Object} hud + */ +async function getFilterState(hud) { + const { outputNode } = hud.ui; + const filterBar = outputNode.querySelector(".webconsole-filterbar-secondary"); + const buttons = filterBar.querySelectorAll("button"); + const result = {}; + + for (const button of buttons) { + result[button.dataset.category] = + button.getAttribute("aria-pressed") === "true"; + } + + return result; +} + +/** + * Return the filter input element. + * + * @param {Object} hud + * @return {HTMLInputElement} + */ +function getFilterInput(hud) { + return hud.ui.outputNode.querySelector(".devtools-searchbox input"); +} + +/** + * Set the state of a console filter. + * + * @param {Object} hud + * @param {Object} settings + * Category settings in the following format: + * { + * error: true, + * warn: true, + * log: true, + * info: true, + * debug: true, + * css: false, + * netxhr: false, + * net: false, + * text: "" + * } + */ +async function setFilterState(hud, settings) { + const { outputNode } = hud.ui; + const filterBar = outputNode.querySelector(".webconsole-filterbar-secondary"); + + for (const category in settings) { + const value = settings[category]; + const button = filterBar.querySelector(`[data-category="${category}"]`); + + if (category === "text") { + const filterInput = getFilterInput(hud); + filterInput.focus(); + filterInput.select(); + const win = outputNode.ownerDocument.defaultView; + if (!value) { + EventUtils.synthesizeKey("KEY_Delete", {}, win); + } else { + EventUtils.sendString(value, win); + } + await waitFor(() => filterInput.value === value); + continue; + } + + if (!button) { + ok( + false, + `setFilterState() called with a category of ${category}, ` + + `which doesn't exist.` + ); + } + + info( + `Setting the ${category} category to ${value ? "checked" : "disabled"}` + ); + + const isPressed = button.getAttribute("aria-pressed"); + + if ((!value && isPressed === "true") || (value && isPressed !== "true")) { + button.click(); + + await waitFor(() => { + const pressed = button.getAttribute("aria-pressed"); + if (!value) { + return pressed === "false" || pressed === null; + } + return pressed === "true"; + }); + } + } +} + +/** + * Reset the filters at the end of a test that has changed them. This is + * important when using the `--verify` test option as when it is used you need + * to manually reset the filters. + * + * The css, netxhr and net filters are disabled by default. + * + * @param {Object} hud + */ +async function resetFilters(hud) { + info("Resetting filters to their default state"); + + const store = hud.ui.wrapper.getStore(); + store.dispatch(wcActions.filtersClear()); +} + +/** + * Open the reverse search input by simulating the appropriate keyboard shortcut. + * + * @param {Object} hud + * @returns {DOMNode} The reverse search dom node. + */ +async function openReverseSearch(hud) { + info("Open the reverse search UI with a keyboard shortcut"); + const onReverseSearchUiOpen = waitFor(() => getReverseSearchElement(hud)); + const isMacOS = AppConstants.platform === "macosx"; + if (isMacOS) { + EventUtils.synthesizeKey("r", { ctrlKey: true }); + } else { + EventUtils.synthesizeKey("VK_F9"); + } + + const element = await onReverseSearchUiOpen; + return element; +} + +function getReverseSearchElement(hud) { + const { outputNode } = hud.ui; + return outputNode.querySelector(".reverse-search"); +} + +function getReverseSearchInfoElement(hud) { + const reverseSearchElement = getReverseSearchElement(hud); + if (!reverseSearchElement) { + return null; + } + + return reverseSearchElement.querySelector(".reverse-search-info"); +} + +/** + * Returns a boolean indicating if the reverse search input is focused. + * + * @param {WebConsole} hud + * @returns {Boolean} + */ +function isReverseSearchInputFocused(hud) { + const { outputNode } = hud.ui; + const document = outputNode.ownerDocument; + const documentIsFocused = document.hasFocus(); + const reverseSearchInput = outputNode.querySelector(".reverse-search-input"); + + return document.activeElement == reverseSearchInput && documentIsFocused; +} + +function getEagerEvaluationElement(hud) { + return hud.ui.outputNode.querySelector(".eager-evaluation-result"); +} + +async function waitForEagerEvaluationResult(hud, text) { + await waitUntil(() => { + const elem = getEagerEvaluationElement(hud); + if (elem) { + if (text instanceof RegExp) { + return text.test(elem.innerText); + } + return elem.innerText == text; + } + return false; + }); + ok(true, `Got eager evaluation result ${text}`); +} + +// This just makes sure the eager evaluation result disappears. This will pass +// even for inputs which eventually have a result because nothing will be shown +// while the evaluation happens. Waiting here does make sure that a previous +// input was processed and sent down to the server for evaluating. +async function waitForNoEagerEvaluationResult(hud) { + await waitUntil(() => { + const elem = getEagerEvaluationElement(hud); + return elem && elem.innerText == ""; + }); + ok(true, `Eager evaluation result disappeared`); +} + +/** + * Selects a node in the inspector. + * + * @param {Object} toolbox + * @param {Object} testActor: A test actor registered on the target. Needed to click on + * the content element. + * @param {String} selector: The selector for the node we want to select. + */ +async function selectNodeWithPicker(toolbox, testActor, selector) { + const inspector = toolbox.getPanel("inspector"); + + const onPickerStarted = toolbox.nodePicker.once("picker-started"); + toolbox.nodePicker.start(); + await onPickerStarted; + + info( + `Picker mode started, now clicking on "${selector}" to select that node` + ); + const onPickerStopped = toolbox.nodePicker.once("picker-stopped"); + const onInspectorUpdated = inspector.once("inspector-updated"); + + testActor.synthesizeMouse({ + selector, + center: true, + options: {}, + }); + + await onPickerStopped; + await onInspectorUpdated; +} + +/** + * Clicks on the arrow of a single object inspector node if it exists. + * + * @param {HTMLElement} node: Object inspector node (.tree-node) + */ +function expandObjectInspectorNode(node) { + const arrow = getObjectInspectorNodeArrow(node); + if (!arrow) { + ok(false, "Node can't be expanded"); + return; + } + arrow.click(); +} + +/** + * Retrieve the arrow of a single object inspector node. + * + * @param {HTMLElement} node: Object inspector node (.tree-node) + * @return {HTMLElement|null} the arrow element + */ +function getObjectInspectorNodeArrow(node) { + return node.querySelector(".arrow"); +} + +/** + * Check if a single object inspector node is expandable. + * + * @param {HTMLElement} node: Object inspector node (.tree-node) + * @return {Boolean} true if the node can be expanded + */ +function isObjectInspectorNodeExpandable(node) { + return !!getObjectInspectorNodeArrow(node); +} + +/** + * Retrieve the nodes for a given object inspector element. + * + * @param {HTMLElement} oi: Object inspector element + * @return {NodeList} the object inspector nodes + */ +function getObjectInspectorNodes(oi) { + return oi.querySelectorAll(".tree-node"); +} + +/** + * Retrieve the "children" nodes for a given object inspector node. + * + * @param {HTMLElement} node: Object inspector node (.tree-node) + * @return {Array<HTMLElement>} the direct children (i.e. the ones that are one level + * deeper than the passed node) + */ +function getObjectInspectorChildrenNodes(node) { + const getLevel = n => parseInt(n.getAttribute("aria-level"), 10); + const level = getLevel(node); + const childLevel = level + 1; + const children = []; + + let currentNode = node; + while ( + currentNode.nextSibling && + getLevel(currentNode.nextSibling) === childLevel + ) { + currentNode = currentNode.nextSibling; + children.push(currentNode); + } + + return children; +} + +/** + * Retrieve the invoke getter button for a given object inspector node. + * + * @param {HTMLElement} node: Object inspector node (.tree-node) + * @return {HTMLElement|null} the invoke button element + */ +function getObjectInspectorInvokeGetterButton(node) { + return node.querySelector(".invoke-getter"); +} + +/** + * Retrieve the first node that match the passed node label, for a given object inspector + * element. + * + * @param {HTMLElement} oi: Object inspector element + * @param {String} nodeLabel: label of the searched node + * @return {HTMLElement|null} the Object inspector node with the matching label + */ +function findObjectInspectorNode(oi, nodeLabel) { + return [...oi.querySelectorAll(".tree-node")].find(node => { + const label = node.querySelector(".object-label"); + if (!label) { + return false; + } + return label.textContent === nodeLabel; + }); +} + +/** + * Return an array of the label of the autocomplete popup items. + * + * @param {AutocompletPopup} popup + * @returns {Array<String>} + */ +function getAutocompletePopupLabels(popup) { + return popup.getItems().map(item => item.label); +} + +/** + * Check if the retrieved list of autocomplete labels of the specific popup + * includes all of the expected labels. + * + * @param {AutocompletPopup} popup + * @param {Array<String>} expected the array of expected labels + */ +function hasExactPopupLabels(popup, expected) { + return hasPopupLabels(popup, expected, true); +} + +/** + * Check if the expected label is included in the list of autocomplete labels + * of the specific popup. + * + * @param {AutocompletPopup} popup + * @param {String} label the label to check + */ +function hasPopupLabel(popup, label) { + return hasPopupLabels(popup, [label]); +} + +/** + * Validate the expected labels against the autocomplete labels. + * + * @param {AutocompletPopup} popup + * @param {Array<String>} expectedLabels + * @param {Boolean} checkAll + */ +function hasPopupLabels(popup, expectedLabels, checkAll = false) { + const autocompleteLabels = getAutocompletePopupLabels(popup); + if (checkAll) { + return ( + autocompleteLabels.length === expectedLabels.length && + autocompleteLabels.every((autoLabel, idx) => { + return expectedLabels.indexOf(autoLabel) === idx; + }) + ); + } + return expectedLabels.every(expectedLabel => { + return autocompleteLabels.includes(expectedLabel); + }); +} + +/** + * Return the "Confirm Dialog" element. + * + * @param toolbox + * @returns {HTMLElement|null} + */ +function getConfirmDialog(toolbox) { + const { doc } = toolbox; + return doc.querySelector(".invoke-confirm"); +} + +/** + * Returns true if the Confirm Dialog is opened. + * @param toolbox + * @returns {Boolean} + */ +function isConfirmDialogOpened(toolbox) { + const tooltip = getConfirmDialog(toolbox); + if (!tooltip) { + return false; + } + + return tooltip.classList.contains("tooltip-visible"); +} + +async function selectFrame(dbg, frame) { + const onScopes = waitForDispatch(dbg, "ADD_SCOPES"); + await dbg.actions.selectFrame(dbg.selectors.getThreadContext(), frame); + await onScopes; +} + +async function pauseDebugger(dbg) { + info("Waiting for debugger to pause"); + const onPaused = waitForPaused(dbg); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.wrappedJSObject.firstCall(); + }).catch(() => {}); + await onPaused; +} + +/** + * Check that the passed HTMLElement vertically overflows. + * @param {HTMLElement} container + * @returns {Boolean} + */ +function hasVerticalOverflow(container) { + return container.scrollHeight > container.clientHeight; +} + +/** + * Check that the passed HTMLElement is scrolled to the bottom. + * @param {HTMLElement} container + * @returns {Boolean} + */ +function isScrolledToBottom(container) { + if (!container.lastChild) { + return true; + } + const lastNodeHeight = container.lastChild.clientHeight; + return ( + container.scrollTop + container.clientHeight >= + container.scrollHeight - lastNodeHeight / 2 + ); +} + +/** + * + * @param {WebConsole} hud + * @param {Array<String>} expectedMessages: An array of string representing the messages + * from the output. This can only be a part of the string of the + * message. + * Start the string with "▶︎⚠ " or "▼⚠ " to indicate that the + * message is a warningGroup (with respectively an open or + * collapsed arrow). + * Start the string with "|︎ " to indicate that the message is + * inside a group and should be indented. + */ +function checkConsoleOutputForWarningGroup(hud, expectedMessages) { + const messages = findMessages(hud, ""); + is( + messages.length, + expectedMessages.length, + "Got the expected number of messages" + ); + + const isInWarningGroup = index => { + const message = expectedMessages[index]; + if (!message.startsWith("|")) { + return false; + } + const groups = expectedMessages + .slice(0, index) + .reverse() + .filter(m => !m.startsWith("|")); + if (groups.length === 0) { + ok(false, "Unexpected structure: an indented message isn't in a group"); + } + + return groups[0].startsWith("▶︎⚠") || groups[0].startsWith("▼⚠"); + }; + + expectedMessages.forEach((expectedMessage, i) => { + const message = messages[i]; + info(`Checking "${expectedMessage}"`); + + // Collapsed Warning group + if (expectedMessage.startsWith("▶︎⚠")) { + is( + message.querySelector(".arrow").getAttribute("aria-expanded"), + "false", + "There's a collapsed arrow" + ); + is( + message.querySelector(".indent").getAttribute("data-indent"), + "0", + "The warningGroup has the expected indent" + ); + expectedMessage = expectedMessage.replace("▶︎⚠ ", ""); + } + + // Expanded Warning group + if (expectedMessage.startsWith("▼︎⚠")) { + is( + message.querySelector(".arrow").getAttribute("aria-expanded"), + "true", + "There's an expanded arrow" + ); + is( + message.querySelector(".indent").getAttribute("data-indent"), + "0", + "The warningGroup has the expected indent" + ); + expectedMessage = expectedMessage.replace("▼︎⚠ ", ""); + } + + // Collapsed console.group + if (expectedMessage.startsWith("▶︎")) { + is( + message.querySelector(".arrow").getAttribute("aria-expanded"), + "false", + "There's a collapsed arrow" + ); + expectedMessage = expectedMessage.replace("▶︎ ", ""); + } + + // Expanded console.group + if (expectedMessage.startsWith("▼")) { + is( + message.querySelector(".arrow").getAttribute("aria-expanded"), + "true", + "There's an expanded arrow" + ); + expectedMessage = expectedMessage.replace("▼ ", ""); + } + + // In-group message + if (expectedMessage.startsWith("|")) { + if (isInWarningGroup(i)) { + is( + message + .querySelector(".indent.warning-indent") + .getAttribute("data-indent"), + "1", + "The message has the expected indent" + ); + } + + expectedMessage = expectedMessage.replace("| ", ""); + } else { + is( + message.querySelector(".indent").getAttribute("data-indent"), + "0", + "The message has the expected indent" + ); + } + + ok( + message.textContent.trim().includes(expectedMessage.trim()), + `Message includes ` + + `the expected "${expectedMessage}" content - "${message.textContent.trim()}"` + ); + }); +} + +/** + * Check that there is a message with the specified text that has the specified + * stack information. Self-hosted frames are ignored. + * @param {WebConsole} hud + * @param {string} text + * message substring to look for + * @param {Array<number>} expectedFrameLines + * line numbers of the frames expected in the stack + */ +async function checkMessageStack(hud, text, expectedFrameLines) { + info(`Checking message stack for "${text}"`); + const msgNode = await waitFor( + () => findMessage(hud, text), + `Couln't find message including "${text}"` + ); + ok(!msgNode.classList.contains("open"), `Error logged not expanded`); + + const button = await waitFor( + () => msgNode.querySelector(".collapse-button"), + `Couldn't find the expand button on "${text}" message` + ); + button.click(); + + const framesNode = await waitFor( + () => msgNode.querySelector(".message-body-wrapper > .stacktrace .frames"), + `Couldn't find stacktrace frames on "${text}" message` + ); + const frameNodes = Array.from(framesNode.querySelectorAll(".frame")).filter( + el => { + const fileName = el.querySelector(".filename").textContent; + return ( + fileName !== "self-hosted" && + !fileName.startsWith("chrome:") && + !fileName.startsWith("resource:") + ); + } + ); + + for (let i = 0; i < frameNodes.length; i++) { + const frameNode = frameNodes[i]; + is( + frameNode.querySelector(".line").textContent, + expectedFrameLines[i].toString(), + `Found line ${expectedFrameLines[i]} for frame #${i}` + ); + } + + is( + frameNodes.length, + expectedFrameLines.length, + `Found ${frameNodes.length} frames` + ); +} + +/** + * Reload the content page. + * @returns {Promise} A promise that will return when the page is fully loaded (i.e., the + * `load` event was fired). + */ +function reloadPage() { + const onLoad = BrowserTestUtils.waitForContentEvent( + gBrowser.selectedBrowser, + "load", + true + ); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.location.reload(); + }); + return onLoad; +} + +/** + * Check if the editor mode is enabled (i.e. .webconsole-app has the expected class). + * + * @param {WebConsole} hud + * @returns {Boolean} + */ +function isEditorModeEnabled(hud) { + const { outputNode } = hud.ui; + const appNode = outputNode.querySelector(".webconsole-app"); + return appNode.classList.contains("jsterm-editor"); +} + +/** + * Toggle the layout between in-line and editor. + * + * @param {WebConsole} hud + * @returns {Promise} A promise that resolves once the layout change was rendered. + */ +function toggleLayout(hud) { + const isMacOS = Services.appinfo.OS === "Darwin"; + const enabled = isEditorModeEnabled(hud); + + EventUtils.synthesizeKey("b", { + [isMacOS ? "metaKey" : "ctrlKey"]: true, + }); + return waitFor(() => isEditorModeEnabled(hud) === !enabled); +} + +/** + * Wait until all lazily fetch requests in netmonitor get finished. + * Otherwise test will be shutdown too early and cause failure. + */ +async function waitForLazyRequests(toolbox) { + const { wrapper } = toolbox.getCurrentPanel().hud.ui; + return waitUntil(() => { + return ( + !wrapper.networkDataProvider.lazyRequestData.size && + // Make sure that batched request updates are all complete + // as they trigger late lazy data requests. + !wrapper.queuedRequestUpdates.length + ); + }); +} + +/** + * Clear the console output and wait for eventual object actors to be released. + * + * @param {WebConsole} hud + * @param {Object} An options object with the following properties: + * - {Boolean} keepStorage: true to prevent clearing the messages storage. + */ +async function clearOutput(hud, { keepStorage = false } = {}) { + const { ui } = hud; + const promises = [ui.once("messages-cleared")]; + + // If there's an object inspector, we need to wait for the actors to be released. + if (ui.outputNode.querySelector(".object-inspector")) { + promises.push(ui.once("fronts-released")); + } + + ui.clearOutput(!keepStorage); + await Promise.all(promises); +} + +/** + * Retrieve all the items of the context selector menu. + * + * @param {WebConsole} hud + * @return Array<Element> + */ +function getContextSelectorItems(hud) { + const toolbox = hud.toolbox; + const doc = toolbox ? toolbox.doc : hud.chromeWindow.document; + const list = doc.getElementById( + "webconsole-console-evaluation-context-selector-menu-list" + ); + return Array.from(list.querySelectorAll("li.menuitem button, hr")); +} + +/** + * Check that the evaluation context selector menu has the expected item, in the expected + * state. + * + * @param {WebConsole} hud + * @param {Array<Object>} expected: An array of object which can have the following shape: + * - {String} label: The label of the target + * - {String} tooltip: The tooltip of the target element in the menu + * - {Boolean} checked: if the target should be selected or not + * - {Boolean} separator: if the element is a simple separator + */ +function checkContextSelectorMenu(hud, expected) { + const items = getContextSelectorItems(hud); + + is( + items.length, + expected.length, + "The context selector menu has the expected number of items" + ); + + expected.forEach(({ label, tooltip, checked, separator }, i) => { + const el = items[i]; + + if (separator === true) { + is( + el.getAttribute("role"), + "menuseparator", + "The element is a separator" + ); + return; + } + + const elChecked = el.getAttribute("aria-checked") === "true"; + const elTooltip = el.getAttribute("title"); + const elLabel = el.querySelector(".label").innerText; + + is(elLabel, label, `The item has the expected label`); + is(elTooltip, tooltip, `Item "${label}" has the expected tooltip`); + is( + elChecked, + checked, + `Item "${label}" is ${checked ? "checked" : "unchecked"}` + ); + }); +} + +/** + * Select a target in the context selector. + * + * @param {WebConsole} hud + * @param {String} targetLabel: The label of the target to select. + */ +function selectTargetInContextSelector(hud, targetLabel) { + const items = getContextSelectorItems(hud); + const itemToSelect = items.find( + item => item.querySelector(".label")?.innerText === targetLabel + ); + if (!itemToSelect) { + ok(false, `Couldn't find target with "${targetLabel}" label`); + return; + } + + itemToSelect.click(); +} + +/** + * A helper that returns the size of the image that was just put into the clipboard by the + * :screenshot command. + * @return The {width, height} dimension object. + */ +async function getImageSizeFromClipboard() { + const clipid = Ci.nsIClipboard; + const clip = Cc["@mozilla.org/widget/clipboard;1"].getService(clipid); + const trans = Cc["@mozilla.org/widget/transferable;1"].createInstance( + Ci.nsITransferable + ); + const flavor = "image/png"; + trans.init(null); + trans.addDataFlavor(flavor); + + clip.getData(trans, clipid.kGlobalClipboard); + const data = {}; + trans.getTransferData(flavor, data); + + ok(data.value, "screenshot exists"); + + let image = data.value; + + // Due to the differences in how images could be stored in the clipboard the + // checks below are needed. The clipboard could already provide the image as + // byte streams or as image container. If it's not possible obtain a + // byte stream, the function throws. + + if (image instanceof Ci.imgIContainer) { + image = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .encodeImage(image, flavor); + } + + if (!(image instanceof Ci.nsIInputStream)) { + throw new Error("Unable to read image data"); + } + + const binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + binaryStream.setInputStream(image); + const available = binaryStream.available(); + const buffer = new ArrayBuffer(available); + is( + binaryStream.readArrayBuffer(available, buffer), + available, + "Read expected amount of data" + ); + + // We are going to load the image in the content page to measure its size. + // We don't want to insert the image directly in the browser's document + // (which is value of the global `document` here). Doing so might push the + // toolbox upwards, shrink the content page and fail the fullpage screenshot + // test. + return SpecialPowers.spawn(gBrowser.selectedBrowser, [buffer], async function( + _buffer + ) { + const img = content.document.createElement("img"); + const loaded = new Promise(r => { + img.addEventListener("load", r, { once: true }); + }); + + // Build a URL from the buffer passed to the ContentTask + const url = content.URL.createObjectURL( + new Blob([_buffer], { type: "image/png" }) + ); + + // Load the image + img.src = url; + content.document.documentElement.appendChild(img); + + info("Waiting for the clipboard image to load in the content page"); + await loaded; + + // Remove the image and revoke the URL. + img.remove(); + content.URL.revokeObjectURL(url); + + return { + width: img.width, + height: img.height, + }; + }); +} diff --git a/devtools/client/webconsole/test/browser/sjs_cors-test-server.sjs b/devtools/client/webconsole/test/browser/sjs_cors-test-server.sjs new file mode 100644 index 0000000000..b819463c8e --- /dev/null +++ b/devtools/client/webconsole/test/browser/sjs_cors-test-server.sjs @@ -0,0 +1,166 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function handleRequest(request, response) { + const params = new Map( + request.queryString + .replace("?", "") + .split("&") + .map(s => s.split("=")) + ); + + if (!params.has("corsErrorCategory")) { + response.setStatusLine(request.httpVersion, 200, "Och Aye"); + setCacheHeaders(response); + response.setHeader("Access-Control-Allow-Origin", "*", false); + response.setHeader("Access-Control-Allow-Headers", "content-type", false); + response.setHeader("Content-Type", "text/plain; charset=utf-8", false); + response.write("Access-Control-Allow-Origin: *"); + return; + } + + const category = params.get("corsErrorCategory"); + switch (category) { + case "CORSDidNotSucceed": + corsDidNotSucceed(request, response); + break; + case "CORSExternalRedirectNotAllowed": + corsExternalRedirectNotAllowed(request, response); + break; + case "CORSMissingAllowOrigin": + corsMissingAllowOrigin(request, response); + break; + case "CORSMultipleAllowOriginNotAllowed": + corsMultipleOriginNotAllowed(request, response); + break; + case "CORSAllowOriginNotMatchingOrigin": + corsAllowOriginNotMatchingOrigin(request, response); + break; + case "CORSNotSupportingCredentials": + corsNotSupportingCredentials(request, response); + break; + case "CORSMethodNotFound": + corsMethodNotFound(request, response); + break; + case "CORSMissingAllowCredentials": + corsMissingAllowCredentials(request, response); + break; + case "CORSPreflightDidNotSucceed": + corsPreflightDidNotSucceed(request, response); + break; + case "CORSInvalidAllowMethod": + corsInvalidAllowMethod(request, response); + break; + case "CORSInvalidAllowHeader": + corsInvalidAllowHeader(request, response); + break; + case "CORSMissingAllowHeaderFromPreflight": + corsMissingAllowHeaderFromPreflight(request, response); + break; + } +} + +function corsDidNotSucceed(request, response) { + setCacheHeaders(response); + response.setStatusLine(request.httpVersion, 301, "Moved Permanently"); + response.setHeader("Location", "http://example.com"); +} + +function corsExternalRedirectNotAllowed(request, response) { + response.setStatusLine(request.httpVersion, 301, "Moved Permanently"); + response.setHeader("Access-Control-Allow-Origin", "*", false); + response.setHeader("Access-Control-Allow-Headers", "content-type", false); + response.setHeader("Location", "http://redirect.test/"); +} + +function corsMissingAllowOrigin(request, response) { + setCacheHeaders(response); + response.setStatusLine(request.httpVersion, 200, "corsMissingAllowOrigin"); +} + +function corsMultipleOriginNotAllowed(request, response) { + // We can't set the same header twice with response.setHeader, so we need to seizePower + // and write the response manually. + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Access-Control-Allow-Origin: *\r\n"); + response.write("Access-Control-Allow-Origin: mochi.test\r\n"); + response.write("\r\n"); + response.finish(); + setCacheHeaders(response); +} + +function corsAllowOriginNotMatchingOrigin(request, response) { + response.setStatusLine( + request.httpVersion, + 200, + "corsAllowOriginNotMatchingOrigin" + ); + response.setHeader("Access-Control-Allow-Origin", "mochi.test"); +} + +function corsNotSupportingCredentials(request, response) { + response.setStatusLine( + request.httpVersion, + 200, + "corsNotSupportingCredentials" + ); + response.setHeader("Access-Control-Allow-Origin", "*"); +} + +function corsMethodNotFound(request, response) { + response.setStatusLine(request.httpVersion, 200, "corsMethodNotFound"); + response.setHeader("Access-Control-Allow-Origin", "*"); + // Will make the request fail since it is a "PUT". + response.setHeader("Access-Control-Allow-Methods", "POST"); +} + +function corsMissingAllowCredentials(request, response) { + response.setStatusLine( + request.httpVersion, + 200, + "corsMissingAllowCredentials" + ); + // Need to set an explicit origin (i.e. not "*") to make the request fail. + response.setHeader("Access-Control-Allow-Origin", "http://example.com"); +} + +function corsPreflightDidNotSucceed(request, response) { + const isPreflight = request.method == "OPTIONS"; + if (isPreflight) { + response.setStatusLine(request.httpVersion, 500, "Preflight fail"); + response.setHeader("Access-Control-Allow-Origin", "*"); + } +} + +function corsInvalidAllowMethod(request, response) { + response.setStatusLine(request.httpVersion, 200, "corsInvalidAllowMethod"); + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Methods", "xyz;"); +} + +function corsInvalidAllowHeader(request, response) { + response.setStatusLine(request.httpVersion, 200, "corsInvalidAllowHeader"); + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Methods", "PUT"); + response.setHeader("Access-Control-Allow-Headers", "xyz;"); +} + +function corsMissingAllowHeaderFromPreflight(request, response) { + response.setStatusLine( + request.httpVersion, + 200, + "corsMissingAllowHeaderFromPreflight" + ); + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Methods", "PUT"); +} + +function setCacheHeaders(response) { + response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + response.setHeader("Pragma", "no-cache"); + response.setHeader("Expires", "0"); +} diff --git a/devtools/client/webconsole/test/browser/sjs_slow-response-test-server.sjs b/devtools/client/webconsole/test/browser/sjs_slow-response-test-server.sjs new file mode 100644 index 0000000000..bcf85c5722 --- /dev/null +++ b/devtools/client/webconsole/test/browser/sjs_slow-response-test-server.sjs @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function handleRequest(request, response) { + response.processAsync(); + + const params = new Map( + request.queryString + .replace("?", "") + .split("&") + .map(s => s.split("=")) + ); + const delay = params.has("delay") ? params.get("delay") : 300; + const status = params.has("status") ? params.get("status") : 200; + + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback( + () => { + // to avoid garbage collection + timer = null; + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader( + "Set-Cookie", + "foo=bar; Max-Age=10; HttpOnly; SameSite=Lax", + true + ); + response.write("Some response data"); + response.finish(); + }, + delay, + Ci.nsITimer.TYPE_ONE_SHOT + ); +} diff --git a/devtools/client/webconsole/test/browser/source-mapped.css b/devtools/client/webconsole/test/browser/source-mapped.css new file mode 100644 index 0000000000..911a65bca2 --- /dev/null +++ b/devtools/client/webconsole/test/browser/source-mapped.css @@ -0,0 +1,6 @@ +body { + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent); } + body div { + color: octopus; } + +/*# sourceMappingURL=source-mapped.css.map */ diff --git a/devtools/client/webconsole/test/browser/source-mapped.css.map b/devtools/client/webconsole/test/browser/source-mapped.css.map new file mode 100644 index 0000000000..ade93953e2 --- /dev/null +++ b/devtools/client/webconsole/test/browser/source-mapped.css.map @@ -0,0 +1,7 @@ +{ +"version": 3, +"mappings": "AAAA,IAAK;EAKH,gBAAgB,EAAE,gLAAuK;EAJzL,QAAI;IACF,KAAK,EAAE,OAAO", +"sources": ["source-mapped.scss"], +"names": [], +"file": "source-mapped.css" +} diff --git a/devtools/client/webconsole/test/browser/source-mapped.scss b/devtools/client/webconsole/test/browser/source-mapped.scss new file mode 100644 index 0000000000..89b3ba36b2 --- /dev/null +++ b/devtools/client/webconsole/test/browser/source-mapped.scss @@ -0,0 +1,7 @@ +body { + div { + color: octopus; + } + + background-image: linear-gradient(45deg, rgba(255,255,255,0.2) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.2) 50%, rgba(255,255,255,0.2) 75%, transparent 75%, transparent); +} diff --git a/devtools/client/webconsole/test/browser/stub-generator-helpers.js b/devtools/client/webconsole/test/browser/stub-generator-helpers.js new file mode 100644 index 0000000000..0e24158a8a --- /dev/null +++ b/devtools/client/webconsole/test/browser/stub-generator-helpers.js @@ -0,0 +1,548 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ChromeUtils = require("ChromeUtils"); +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +const { + getAdHocFrontOrPrimitiveGrip, +} = require("devtools/client/fronts/object"); + +const CHROME_PREFIX = "chrome://mochitests/content/browser/"; +const STUBS_FOLDER = "devtools/client/webconsole/test/node/fixtures/stubs/"; +const STUBS_UPDATE_ENV = "WEBCONSOLE_STUBS_UPDATE"; + +async function createResourceWatcherForTab(tab) { + const { TargetFactory } = require("devtools/client/framework/target"); + const target = await TargetFactory.forTab(tab); + const resourceWatcher = await createResourceWatcherForTarget(target); + return resourceWatcher; +} + +async function createResourceWatcherForTarget(target) { + // Avoid mocha to try to load these module and fail while doing it when running node tests + const { + ResourceWatcher, + } = require("devtools/shared/resources/resource-watcher"); + const { TargetList } = require("devtools/shared/resources/target-list"); + + const targetList = new TargetList(target.client.mainRoot, target); + await targetList.startListening(); + return new ResourceWatcher(targetList); +} + +// eslint-disable-next-line complexity +function getCleanedPacket(key, packet) { + const { stubPackets } = require(CHROME_PREFIX + STUBS_FOLDER + "index"); + + // Strip escaped characters. + const safeKey = key + .replace(/\\n/g, "\n") + .replace(/\\r/g, "\r") + .replace(/\\\"/g, `\"`) + .replace(/\\\'/g, `\'`); + + cleanTimeStamp(packet); + // Remove the targetFront property that has a cyclical reference and that we don't need + // in our node tests. + delete packet.targetFront; + + if (!stubPackets.has(safeKey)) { + return packet; + } + + // If the stub already exist, we want to ignore irrelevant properties (actor, timer, …) + // that might changed and "pollute" the diff resulting from this stub generation. + const existingPacket = stubPackets.get(safeKey); + const res = Object.assign({}, packet, { + from: existingPacket.from, + }); + + if (res.innerWindowID) { + res.innerWindowID = existingPacket.innerWindowID; + } + + if (res.startedDateTime) { + res.startedDateTime = existingPacket.startedDateTime; + } + + if (res.actor) { + res.actor = existingPacket.actor; + } + + if (res.channelId) { + res.channelId = existingPacket.channelId; + } + + if (res.resultID) { + res.resultID = existingPacket.resultID; + } + + if (res.message) { + if (res.message.timer) { + // Clean timer properties on the message. + // Those properties are found on console.time, timeLog and timeEnd calls, + // and those time can vary, which is why we need to clean them. + if ("duration" in res.message.timer) { + res.message.timer.duration = existingPacket.message.timer.duration; + } + } + // Clean innerWindowId on the message prop. + if (existingPacket.message.innerWindowID) { + res.message.innerWindowID = existingPacket.message.innerWindowID; + } + + if (Array.isArray(res.message.arguments)) { + res.message.arguments = res.message.arguments.map((argument, i) => { + if (!argument || typeof argument !== "object") { + return argument; + } + + const newArgument = Object.assign({}, argument); + const existingArgument = existingPacket.message.arguments[i]; + + if (existingArgument && newArgument._grip) { + // Clean actor ids on each message.arguments item. + copyExistingActor(newArgument, existingArgument); + + // `window`'s properties count can vary from OS to OS, so we + // clean the `ownPropertyLength` property from the grip. + if (newArgument._grip.class === "Window") { + newArgument._grip.ownPropertyLength = + existingArgument._grip.ownPropertyLength; + } + } + return newArgument; + }); + } + + if (res.message.actor && existingPacket?.message?.actor) { + res.message.actor = existingPacket.message.actor; + } + + if (res.message.sourceId) { + res.message.sourceId = existingPacket.message.sourceId; + } + + if (Array.isArray(res.message.stacktrace)) { + res.message.stacktrace = res.message.stacktrace.map((frame, i) => { + const existingFrame = existingPacket.message.stacktrace[i]; + if (frame && existingFrame && frame.sourceId) { + frame.sourceId = existingFrame.sourceId; + } + return frame; + }); + } + } + + if (res?.exception?.actor && existingPacket.exception.actor) { + // Clean actor ids on evaluation exception + copyExistingActor(res.exception, existingPacket.exception); + } + + if (res.result && res.result._grip && existingPacket.result) { + // Clean actor ids on evaluation result messages. + copyExistingActor(res.result, existingPacket.result); + } + + if ( + res?.result?._grip?.preview?.ownProperties?.["<value>"]?.value && + existingPacket?.result?._grip?.preview?.ownProperties?.["<value>"]?.value + ) { + // Clean actor ids on evaluation promise result messages. + copyExistingActor( + res.result._grip.preview.ownProperties["<value>"].value, + existingPacket.result._grip.preview.ownProperties["<value>"].value + ); + } + + if ( + res?.result?._grip?.preview?.ownProperties?.["<reason>"]?.value && + existingPacket?.result?._grip?.preview?.ownProperties?.["<reason>"]?.value + ) { + // Clean actor ids on evaluation promise result messages. + copyExistingActor( + res.result._grip.preview.ownProperties["<reason>"].value, + existingPacket.result._grip.preview.ownProperties["<reason>"].value + ); + } + + if (res.exception && existingPacket.exception) { + // Clean actor ids on exception messages. + copyExistingActor(res.exception, existingPacket.exception); + + if ( + res.exception._grip && + res.exception._grip.preview && + existingPacket.exception._grip && + existingPacket.exception._grip.preview + ) { + if ( + typeof res.exception._grip.preview.message === "object" && + res.exception._grip.preview.message._grip.type === "longString" && + typeof existingPacket.exception._grip.preview.message === "object" && + existingPacket.exception._grip.preview.message._grip.type === + "longString" + ) { + copyExistingActor( + res.exception._grip.preview.message, + existingPacket.exception._grip.preview.message + ); + } + } + + if ( + typeof res.exceptionMessage === "object" && + res.exceptionMessage._grip && + res.exceptionMessage._grip.type === "longString" + ) { + copyExistingActor(res.exceptionMessage, existingPacket.exceptionMessage); + } + } + + if (res.eventActor) { + // Clean actor ids and startedDateTime on network messages. + res.eventActor.actor = existingPacket.actor; + res.eventActor.startedDateTime = existingPacket.startedDateTime; + } + + if (res.pageError) { + // Clean innerWindowID on pageError messages. + res.pageError.innerWindowID = existingPacket.pageError.innerWindowID; + + if ( + typeof res.pageError.errorMessage === "object" && + res.pageError.errorMessage._grip && + res.pageError.errorMessage._grip.type === "longString" + ) { + copyExistingActor( + res.pageError.errorMessage, + existingPacket.pageError.errorMessage + ); + } + + if ( + res.pageError.exception?._grip?.preview?.message?._grip && + existingPacket.pageError.exception?._grip?.preview?.message?._grip + ) { + copyExistingActor( + res.pageError.exception._grip.preview.message, + existingPacket.pageError.exception._grip.preview.message + ); + } + + if (res.pageError.exception && existingPacket.pageError.exception) { + copyExistingActor( + res.pageError.exception, + existingPacket.pageError.exception + ); + } + + if (res.pageError.sourceId) { + res.pageError.sourceId = existingPacket.pageError.sourceId; + } + + if ( + Array.isArray(res.pageError.stacktrace) && + Array.isArray(existingPacket.pageError.stacktrace) + ) { + res.pageError.stacktrace = res.pageError.stacktrace.map((frame, i) => { + const existingFrame = existingPacket.pageError.stacktrace[i]; + if (frame && existingFrame && frame.sourceId) { + frame.sourceId = existingFrame.sourceId; + } + return frame; + }); + } + } + + if (Array.isArray(res.exceptionStack)) { + res.exceptionStack = res.exceptionStack.map((frame, i) => { + const existingFrame = existingPacket.exceptionStack[i]; + if (frame && existingFrame && frame.sourceId) { + frame.sourceId = existingFrame.sourceId; + } + return frame; + }); + } + + if (res.frame && existingPacket.frame) { + res.frame.sourceId = existingPacket.frame.sourceId; + } + + if (res.packet) { + const override = {}; + const keys = ["totalTime", "from", "contentSize", "transferredSize"]; + keys.forEach(x => { + if (res.packet[x] !== undefined) { + override[x] = existingPacket.packet[key]; + } + }); + res.packet = Object.assign({}, res.packet, override); + } + + if (res.startedDateTime) { + res.startedDateTime = existingPacket.startedDateTime; + } + + if (res.totalTime && existingPacket.totalTime) { + res.totalTime = existingPacket.totalTime; + } + + if (res.securityState && existingPacket.securityState) { + res.securityState = existingPacket.securityState; + } + + if (res.actor && existingPacket.actor) { + res.actor = existingPacket.actor; + } + + if (res.waitingTime && existingPacket.waitingTime) { + res.waitingTime = existingPacket.waitingTime; + } + + if (res.helperResult) { + copyExistingActor( + res.helperResult.object, + existingPacket.helperResult.object + ); + } + return res; +} + +function cleanTimeStamp(packet) { + // We want to have the same timestamp for every stub, so they won't be re-sorted when + // adding them to the store. + const uniqueTimeStamp = 1572867483805; + // lowercased timestamp + if (packet.timestamp) { + packet.timestamp = uniqueTimeStamp; + } + + // camelcased timestamp + if (packet.timeStamp) { + packet.timeStamp = uniqueTimeStamp; + } + + if (packet.startTime) { + packet.startTime = uniqueTimeStamp; + } + + if (packet?.message?.timeStamp) { + packet.message.timeStamp = uniqueTimeStamp; + } + + if (packet?.result?._grip?.preview?.timestamp) { + packet.result._grip.preview.timestamp = uniqueTimeStamp; + } + + if (packet?.result?._grip?.promiseState?.creationTimestamp) { + packet.result._grip.promiseState.creationTimestamp = uniqueTimeStamp; + } + + if (packet?.exception?._grip?.preview?.timestamp) { + packet.exception._grip.preview.timestamp = uniqueTimeStamp; + } + + if (packet?.eventActor?.timeStamp) { + packet.eventActor.timeStamp = uniqueTimeStamp; + } + + if (packet?.pageError?.timeStamp) { + packet.pageError.timeStamp = uniqueTimeStamp; + } +} + +function copyExistingActor(a, b) { + if (!a || !b) { + return; + } + + if (a.actorID && b.actorID) { + a.actorID = b.actorID; + } + + if (a.actor && b.actor) { + a.actor = b.actor; + } + + if (a._grip && b._grip && a._grip.actor && b._grip.actor) { + a._grip.actor = b._grip.actor; + } +} + +/** + * Write stubs to a given file + * + * @param {Object} env + * @param {String} fileName: The file to write the stubs in. + * @param {Map} packets: A Map of the packets. + * @param {Boolean} isNetworkMessage: Is the packets are networkMessage packets + */ +async function writeStubsToFile(env, fileName, packets, isNetworkMessage) { + const mozRepo = env.get("MOZ_DEVELOPER_REPO_DIR"); + const filePath = `${mozRepo}/${STUBS_FOLDER + fileName}`; + + const serializedPackets = Array.from(packets.entries()).map( + ([key, packet]) => { + const stringifiedPacket = getSerializedPacket(packet); + return `rawPackets.set(\`${key}\`, ${stringifiedPacket});`; + } + ); + + const fileContent = `/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable max-len */ + +"use strict"; + +/* + * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. RUN TESTS IN FIXTURES/ TO UPDATE. + */ + +const { + parsePacketsWithFronts, +} = require("chrome://mochitests/content/browser/devtools/client/webconsole/test/browser/stub-generator-helpers"); +const { prepareMessage } = require("devtools/client/webconsole/utils/messages"); +const { + ConsoleMessage, + NetworkEventMessage, +} = require("devtools/client/webconsole/types"); + +const rawPackets = new Map(); +${serializedPackets.join("\n\n")} + + +const stubPackets = parsePacketsWithFronts(rawPackets); + +const stubPreparedMessages = new Map(); +for (const [key, packet] of Array.from(stubPackets.entries())) { + const transformedPacket = prepareMessage(${"packet"}, { + getNextId: () => "1", + }); + const message = ${ + isNetworkMessage + ? "NetworkEventMessage(transformedPacket);" + : "ConsoleMessage(transformedPacket);" + } + stubPreparedMessages.set(key, message); +} + +module.exports = { + rawPackets, + stubPreparedMessages, + stubPackets, +}; +`; + + await OS.File.writeAtomic(filePath, fileContent); +} + +function getStubFile(fileName) { + return require(CHROME_PREFIX + STUBS_FOLDER + fileName); +} + +function sortObjectKeys(obj) { + const isArray = Array.isArray(obj); + const isObject = Object.prototype.toString.call(obj) === "[object Object]"; + const isFront = obj?._grip; + + if (isObject && !isFront) { + // Reorder keys for objects, but skip fronts to avoid infinite recursion. + const sortedKeys = Object.keys(obj).sort((k1, k2) => k1.localeCompare(k2)); + const withSortedKeys = {}; + sortedKeys.forEach(k => { + withSortedKeys[k] = k !== "stacktrace" ? sortObjectKeys(obj[k]) : obj[k]; + }); + return withSortedKeys; + } else if (isArray) { + return obj.map(item => sortObjectKeys(item)); + } + return obj; +} + +/** + * @param {Object} packet + * The packet to serialize. + * @param {Object} + * - {Boolean} sortKeys: pass true to sort all keys alphabetically in the + * packet before serialization. For instance stub comparison should not + * fail if the order of properties changed. + */ +function getSerializedPacket(packet, { sortKeys = false } = {}) { + if (sortKeys) { + packet = sortObjectKeys(packet); + } + + return JSON.stringify( + packet, + function(_, value) { + // The message can have fronts that we need to serialize + if (value && value._grip) { + return { _grip: value._grip, actorID: value.actorID }; + } + + return value; + }, + 2 + ); +} + +/** + * + * @param {Map} rawPackets + */ +function parsePacketsWithFronts(rawPackets) { + const packets = new Map(); + for (const [key, packet] of rawPackets.entries()) { + const newPacket = parsePacketAndCreateFronts(packet); + packets.set(key, newPacket); + } + return packets; +} + +function parsePacketAndCreateFronts(packet) { + if (!packet) { + return packet; + } + if (Array.isArray(packet)) { + packet.forEach(parsePacketAndCreateFronts); + } + if (typeof packet === "object") { + for (const [key, value] of Object.entries(packet)) { + if (value?._grip) { + // The message of an error grip might be a longString. + if (value._grip?.preview?.message?._grip) { + value._grip.preview.message = value._grip.preview.message._grip; + } + + packet[key] = getAdHocFrontOrPrimitiveGrip(value._grip, { + conn: { + poolFor: () => {}, + addActorPool: () => {}, + getFrontByID: () => {}, + }, + manage: () => {}, + }); + } else { + packet[key] = parsePacketAndCreateFronts(value); + } + } + } + + return packet; +} + +module.exports = { + STUBS_UPDATE_ENV, + createResourceWatcherForTab, + createResourceWatcherForTarget, + getStubFile, + getCleanedPacket, + getSerializedPacket, + parsePacketsWithFronts, + parsePacketAndCreateFronts, + writeStubsToFile, +}; diff --git a/devtools/client/webconsole/test/browser/test-autocomplete-in-stackframe.html b/devtools/client/webconsole/test/browser/test-autocomplete-in-stackframe.html new file mode 100644 index 0000000000..433001d4ba --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-autocomplete-in-stackframe.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html dir="ltr" lang="en"> + <head> + <meta charset="utf8"> + <!-- + - Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ + --> + <title>Test for bug 842682 - use the debugger API for web console autocomplete</title> + <script> + /* eslint-disable */ + var foo1 = "globalFoo"; + + var foo1Obj = Object.assign(Object.create(null), { + prop1: "111", + prop2: { + prop21: "212121" + } + }); + + function firstCall() { + var foo2 = "fooFirstCall"; + + var foo2Obj = Object.assign(Object.create(null), { + prop1: Object.assign(Object.create(null), { + prop11: "111111" + }) + }); + + secondCall(); + } + + function secondCall() { + var foo3 = "fooSecondCall"; + + var foo3Obj = Object.assign(Object.create(null), { + prop1: Object.assign(Object.create(null), { + prop11: "313131" + }) + }); + + debugger; + } + </script> + </head> + <body> + <p>Hello world!</p> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-batching.html b/devtools/client/webconsole/test/browser/test-batching.html new file mode 100644 index 0000000000..8d5c9e1244 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-batching.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Webconsole batch console calls test page</title> + </head> + <body> + <p>batch console calls test page</p> + <script> + /* exported batchLog, batchLogAndClear */ + "use strict"; + + function batchLog(numMessages = 0) { + for (let i = 0; i < numMessages; i++) { + console.log(i); + } + } + + function batchLogAndClear(numMessages = 0) { + for (let i = 0; i < numMessages; i++) { + console.log(i); + if (i === numMessages - 1) { + console.clear(); + } + } + } + </script> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-block-action-style.css b/devtools/client/webconsole/test/browser/test-block-action-style.css new file mode 100644 index 0000000000..d224431f16 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-block-action-style.css @@ -0,0 +1,3 @@ +h1 { + color: red; +} diff --git a/devtools/client/webconsole/test/browser/test-block-action.html b/devtools/client/webconsole/test/browser/test-block-action.html new file mode 100644 index 0000000000..ea40c5e462 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-block-action.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>Test for bug 1546394 - :block command</title> + <link rel="stylesheet" href="test-block-action-style.css"> +</head> +<body> + <h1 id="heading">I won't be red for once.</h1> +</body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-bug_923281_console_log_filter.html b/devtools/client/webconsole/test/browser/test-bug_923281_console_log_filter.html new file mode 100644 index 0000000000..f2d650a5d9 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-bug_923281_console_log_filter.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf-8"> + <title>Console test</title> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + <script type="text/javascript" src="test-bug_923281_test1.js"></script> + <script type="text/javascript" src="test-bug_923281_test2.js"></script> + </head> + <body></body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-bug_923281_test1.js b/devtools/client/webconsole/test/browser/test-bug_923281_test1.js new file mode 100644 index 0000000000..40babb1c85 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-bug_923281_test1.js @@ -0,0 +1,7 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +console.log("Sample log."); +console.log("This log should be filtered when filtered for test2.js."); diff --git a/devtools/client/webconsole/test/browser/test-bug_923281_test2.js b/devtools/client/webconsole/test/browser/test-bug_923281_test2.js new file mode 100644 index 0000000000..ae91348d16 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-bug_923281_test2.js @@ -0,0 +1,6 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +console.log("This is a random text."); diff --git a/devtools/client/webconsole/test/browser/test-certificate-messages.html b/devtools/client/webconsole/test/browser/test-certificate-messages.html new file mode 100644 index 0000000000..234434e28a --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-certificate-messages.html @@ -0,0 +1,23 @@ +<!-- + Bug 1068949 - Log crypto warnings to the security pane in the webconsole +--> + +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf8"> + <title>Security warning test - no violations</title> + <!-- ensure no subresource errors so window re-use doesn't cause failures --> + <link rel="icon" href="data:;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC"> + <script> + "use strict"; + console.log("If you haven't seen ssl warnings yet, you won't"); + </script> + <!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ + --> + </head> + <body> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-checkloaduri-failure.html b/devtools/client/webconsole/test/browser/test-checkloaduri-failure.html new file mode 100644 index 0000000000..a242c549da --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-checkloaduri-failure.html @@ -0,0 +1,23 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <title>Test loads that fail checkLoadURI</title> + <script> + /* exported testImage */ + "use strict"; + + function testImage(url) { + const body = document.body; + const image = new Image(); + image.src = url; + body.append(image); + } + </script> + </head> + <body> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-click-function-to-mapped-source.html b/devtools/client/webconsole/test/browser/test-click-function-to-mapped-source.html new file mode 100644 index 0000000000..b2ce3e58c2 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-click-function-to-mapped-source.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf-8"> + <title>Click on function should point to source</title> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + <script type="text/javascript" src="test-click-function-to-source.min.js"></script> + </head> + <body></body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-click-function-to-prettyprinted-source.html b/devtools/client/webconsole/test/browser/test-click-function-to-prettyprinted-source.html new file mode 100644 index 0000000000..ef7d99dfdd --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-click-function-to-prettyprinted-source.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf-8"> + <title>Click on function should point to source</title> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + <script type="text/javascript" src="test-click-function-to-source.unmapped.min.js"></script> + </head> + <body></body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-click-function-to-source.html b/devtools/client/webconsole/test/browser/test-click-function-to-source.html new file mode 100644 index 0000000000..602836df08 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-click-function-to-source.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf-8"> + <title>Click on function should point to source</title> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + <script type="text/javascript" src="test-click-function-to-source.js"></script> + </head> + <body></body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-click-function-to-source.js b/devtools/client/webconsole/test/browser/test-click-function-to-source.js new file mode 100644 index 0000000000..92e1644f25 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-click-function-to-source.js @@ -0,0 +1,12 @@ +/* eslint-disable */ + +/** + * this + * is + * a + * function + */ +function foo() { + console.log(foo); +} + diff --git a/devtools/client/webconsole/test/browser/test-click-function-to-source.min.js b/devtools/client/webconsole/test/browser/test-click-function-to-source.min.js new file mode 100644 index 0000000000..b12ddee364 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-click-function-to-source.min.js @@ -0,0 +1,3 @@ +/* eslint-disable */ +function foo(){console.log(foo)} +//# sourceMappingURL=test-click-function-to-source.min.js.map diff --git a/devtools/client/webconsole/test/browser/test-click-function-to-source.min.js.map b/devtools/client/webconsole/test/browser/test-click-function-to-source.min.js.map new file mode 100644 index 0000000000..ee239bb640 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-click-function-to-source.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["test-click-function-to-source.js"],"names":["foo","console","log"],"mappings":";AAQA,SAASA,MACPC,QAAQC,IAAIF"}
\ No newline at end of file diff --git a/devtools/client/webconsole/test/browser/test-click-function-to-source.unmapped.min.js b/devtools/client/webconsole/test/browser/test-click-function-to-source.unmapped.min.js new file mode 100644 index 0000000000..1d6237b5eb --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-click-function-to-source.unmapped.min.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +function foo(){console.log(foo)} diff --git a/devtools/client/webconsole/test/browser/test-closure-optimized-out.html b/devtools/client/webconsole/test/browser/test-closure-optimized-out.html new file mode 100644 index 0000000000..28242ac07e --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-closure-optimized-out.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title>Debugger Test for Inspecting Optimized-Out Variables</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <script type="text/javascript"> + /* eslint-disable */ + window.addEventListener("load", function () { + function clickHandler(event) { + button.removeEventListener("click", clickHandler); + function outer(arg) { + let upvar = arg * 2; + // The inner lambda only aliases arg, so the frontend alias analysis decides + // that upvar is not aliased and is not in the CallObject. + return function () { + arg += 2; + }; + } + + let f = outer(42); + f(); + } + let button = document.querySelector("button"); + button.addEventListener("click", clickHandler); + }, {once: true}); + </script> + + </head> + <body> + <button>Click me!</button> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-console-api-iframe.html b/devtools/client/webconsole/test/browser/test-console-api-iframe.html new file mode 100644 index 0000000000..a5544e89b3 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-console-api-iframe.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Testing the console API after adding an iframe</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>test for bug 613013</p> + <script type="text/javascript"> + /* eslint-disable */ + (function () { + var iframe = document.createElement('iframe'); + iframe.src = 'data:text/html;charset=utf-8,little iframe'; + document.body.appendChild(iframe); + + console.log("iframe added"); + })(); + </script> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-console-api.html b/devtools/client/webconsole/test/browser/test-console-api.html new file mode 100644 index 0000000000..f0fb6e6fe8 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-console-api.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Stub generator</title> + </head> + <body> + <p>Stub generator</p> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-console-evaluation-context-selector-child.html b/devtools/client/webconsole/test/browser/test-console-evaluation-context-selector-child.html new file mode 100644 index 0000000000..88c42868e0 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-console-evaluation-context-selector-child.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + </head> + <body> + <h2>iframe</h2> + <button class="stop-me">Stop Me!</button> + <script> + "use strict"; + console.log("iframe", document); + var id = new URLSearchParams(document.location.search).get("id"); + document.querySelector("h2").id = id; + document.title = `${id}|${document.location.host}`; + document.addEventListener("click", function(e) { + // eslint-disable-next-line no-unused-vars + const localVar = document; + // eslint-disable-next-line no-debugger + debugger; + }); + </script> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-console-evaluation-context-selector.html b/devtools/client/webconsole/test/browser/test-console-evaluation-context-selector.html new file mode 100644 index 0000000000..5521648618 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-console-evaluation-context-selector.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Test evaluation context selector</title> + </head> + <body> + <h1 id="top-level">Test evaluation context selector</h1> + <script> + "use strict"; + console.log("top-level", document); + </script> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-console-filter-by-regex-input.html b/devtools/client/webconsole/test/browser/test-console-filter-by-regex-input.html new file mode 100644 index 0000000000..16d472d893 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-console-filter-by-regex-input.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test regex input.</title> + </head> + <body> + <p>Web Console test for filtering messages by regex input.</p> + <script> + "use strict"; + + console.log("123-456-7890"); + console.log("foo@bar.com"); + console.log("http://abc.com/q?fizz=buzz&alpha=beta/"); + console.log("https://xyz.com/?path=/world"); + console.log("foooobaaaar"); + console.log("123 working"); + </script> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-console-filter-groups.html b/devtools/client/webconsole/test/browser/test-console-filter-groups.html new file mode 100644 index 0000000000..d1135c7807 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-console-filter-groups.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Webconsole test filter groups page</title> + </head> + <body> + <p>Webconsole test filter groups page</p> + <script> + "use strict"; + + /* + * This is going to print the following: + * ▼[a] + * | [b] + * | [c] + * | ▼[d] + * | | [e] + * | [f] + * | [g] + * [h] + * [i] + * ▶︎[j] + * ▼[group] + * | ▼[subgroup] + * | | [subitem] + */ + console.group("[a]"); + console.log("[b]"); + console.info("[c]"); + console.group("[d]"); + console.error("[e]"); + console.groupEnd(); + console.warn("[f]"); + console.debug("[g]"); + console.groupEnd(); + console.log("[h]"); + console.log("[i]"); + console.groupCollapsed("[j]"); + console.log("[k]"); + console.groupEnd(); + console.group("[group]"); + console.group("[subgroup]"); + console.log("[subitem]"); + console.groupEnd(); + console.groupEnd(); + </script> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-console-filters.html b/devtools/client/webconsole/test/browser/test-console-filters.html new file mode 100644 index 0000000000..82ceddfaaa --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-console-filters.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Webconsole filters test page</title> + <style> + body { + color: blouge; + } + </style> + </head> + <body> + <p>Webconsole filters test page</p> + <script> + "use strict"; + + console.log("console log"); + console.warn("console warn"); + console.error("console error"); + console.info("console info"); + console.count("console debug"); + </script> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-console-group.html b/devtools/client/webconsole/test/browser/test-console-group.html new file mode 100644 index 0000000000..077cfd002a --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-console-group.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Webconsole console.group test page</title> + </head> + <body> + <p>console.group() & console.groupCollapsed() test page</p> + <script> + /* exported doLog */ + "use strict"; + + function doLog() { + console.group("group-1"); + console.log("log-1"); + console.group("group-2"); + console.log("log-2"); + console.groupEnd(); + console.log("log-3"); + console.groupEnd(); + console.log("log-4"); + console.groupCollapsed("group-3"); + console.log("log-5"); + console.groupEnd(); + console.log("log-6"); + } + </script> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-console-iframes.html b/devtools/client/webconsole/test/browser/test-console-iframes.html new file mode 100644 index 0000000000..e7c153a343 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-console-iframes.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <script> + /* eslint-disable */ + console.log("main file"); + </script> + </head> + <body> + <h1>iframe console test</h1> + <iframe src="test-iframe1.html"></iframe> + <iframe src="test-iframe2.html"></iframe> + <iframe src="test-iframe3.html"></iframe> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-console-stacktrace-mapped.html b/devtools/client/webconsole/test/browser/test-console-stacktrace-mapped.html new file mode 100644 index 0000000000..e047cd3f01 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-console-stacktrace-mapped.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf-8"> + <title>Click on stacktrace location should point to source</title> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + <script type="text/javascript" src="test-sourcemap.min.js"></script> + </head> + <body></body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-console-table.html b/devtools/client/webconsole/test/browser/test-console-table.html new file mode 100644 index 0000000000..88e9a4c07a --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-console-table.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Simple webconsole test page</title> + </head> + <body> + <p>console.table() test page</p> + <script> + /* exported doConsoleTable */ + "use strict"; + + function doConsoleTable(data, constrainedHeaders = null) { + if (constrainedHeaders) { + console.table(data, constrainedHeaders); + } else { + console.table(data); + } + } + </script> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-console-trace-duplicates.html b/devtools/client/webconsole/test/browser/test-console-trace-duplicates.html new file mode 100644 index 0000000000..e9e0c6e445 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-console-trace-duplicates.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Test for checking that same console.trace() calls are duplicated</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + +<script type="application/javascript"> +/* eslint-disable */ +function foo1() { + foo2(); +} + +function foo2() { + foo3(); +} + +function foo3() { + console.trace(); +} +for (let i = 0; i < 3; i++){ + foo1(); +} +</script> + </head> + <body> + <p>Test that same console.trace <b>are</b> repeated in the console</p> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-console-workers.html b/devtools/client/webconsole/test/browser/test-console-workers.html new file mode 100644 index 0000000000..fcc887a7e0 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-console-workers.html @@ -0,0 +1,28 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"><head> + <meta charset="utf-8"> + <title>Console test</title> + </head> + <body> + <script type="text/javascript"> + "use strict"; + + /* exported logFromWorker */ + + const worker = new Worker(`data:application/javascript, + console.log("initial-message-from-worker", {foo: "bar"}, globalThis); + + onmessage = function (e) { + console.log("log-from-worker", e.data.msg, globalThis); + console.log(Symbol("logged-symbol-from-worker")); + }; + `); + + function logFromWorker(msg) { + worker.postMessage({type: "log", msg}); + } + </script> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-console.html b/devtools/client/webconsole/test/browser/test-console.html new file mode 100644 index 0000000000..680426ae0e --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-console.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Simple webconsole test page</title> + </head> + <body> + <p>Simple webconsole test page</p> + <script> + /* exported doLogs, stringLog */ + "use strict"; + + function doLogs(num) { + num = num || 1; + for (let i = 0; i < num; i++) { + console.log(i); + } + } + + function stringLog() { + console.log("stringLog"); + } + </script> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-csp-violation-base-uri.html b/devtools/client/webconsole/test/browser/test-csp-violation-base-uri.html new file mode 100644 index 0000000000..9f6e975903 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-csp-violation-base-uri.html @@ -0,0 +1,18 @@ +<html> + <head> + <title>CSP Base-URI Violation Test </title> + <base href="https://evil.com/"> + </head> + <body> + <h1> Crashing the Base Element</h1> + </body> + <script> + "use strict"; + window.violate = ()=>{ + document.head.innerHTML = ""; + const b = document.createElement("base"); + b.href = "https://evil.com"; + document.head.append(b); + }; + </script> + </html> diff --git a/devtools/client/webconsole/test/browser/test-csp-violation-base-uri.html^headers^ b/devtools/client/webconsole/test/browser/test-csp-violation-base-uri.html^headers^ new file mode 100644 index 0000000000..3c02326419 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-csp-violation-base-uri.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: base-uri 'self'; diff --git a/devtools/client/webconsole/test/browser/test-csp-violation-form-action.html b/devtools/client/webconsole/test/browser/test-csp-violation-form-action.html new file mode 100644 index 0000000000..5620110415 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-csp-violation-form-action.html @@ -0,0 +1,16 @@ +<html> + <head> + <title>CSP Base-URI Violation Test </title> + <base href="https://evil.com/"> + </head> + <body> + <form action="evil.com" > + <input type="text" value="test" name="test" /> + <button type="submit">Submit Button</button> + </form> + </body> + <script> + "use strict"; + document.querySelector("form").submit(); + </script> + </html> diff --git a/devtools/client/webconsole/test/browser/test-csp-violation-form-action.html^headers^ b/devtools/client/webconsole/test/browser/test-csp-violation-form-action.html^headers^ new file mode 100644 index 0000000000..f9d93d65e2 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-csp-violation-form-action.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: form-action 'self'; diff --git a/devtools/client/webconsole/test/browser/test-csp-violation-frame-ancestor-child.html b/devtools/client/webconsole/test/browser/test-csp-violation-frame-ancestor-child.html new file mode 100644 index 0000000000..c090d519de --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-csp-violation-frame-ancestor-child.html @@ -0,0 +1,9 @@ +<html> + <head> + <title>CSP frame-ancestors Violation Test </title> + <base href="https://evil.com/"> + </head> + <body> + <h1> This Should not be Loadable</h1> + </body> + </html> diff --git a/devtools/client/webconsole/test/browser/test-csp-violation-frame-ancestor-child.html^headers^ b/devtools/client/webconsole/test/browser/test-csp-violation-frame-ancestor-child.html^headers^ new file mode 100644 index 0000000000..d86af2b05c --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-csp-violation-frame-ancestor-child.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: frame-ancestors 'none'; diff --git a/devtools/client/webconsole/test/browser/test-csp-violation-frame-ancestor-parent.html b/devtools/client/webconsole/test/browser/test-csp-violation-frame-ancestor-parent.html new file mode 100644 index 0000000000..65d8dfb20f --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-csp-violation-frame-ancestor-parent.html @@ -0,0 +1,21 @@ +<html> + <head> + <title>CSP frame-ancestors Violation Test + </title> + <base href="https://evil.com/"> + </head> + <body> + <iframe src="https://example.com/browser/devtools/client/webconsole/test/browser/test-csp-violation-frame-ancestor-child.html"></iframe> + </body> + <script> + "use strict"; + window.violate = () => { + const iframe = document.querySelector("iframe"); + const src = iframe.src; + iframe.src = ""; + requestAnimationFrame(() => { + iframe.src = src; + }); + }; + </script> +</html> diff --git a/devtools/client/webconsole/test/browser/test-csp-violation-frame-ancestor-parent.html^headers^ b/devtools/client/webconsole/test/browser/test-csp-violation-frame-ancestor-parent.html^headers^ new file mode 100644 index 0000000000..f9d93d65e2 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-csp-violation-frame-ancestor-parent.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: form-action 'self'; diff --git a/devtools/client/webconsole/test/browser/test-csp-violation-inline.html b/devtools/client/webconsole/test/browser/test-csp-violation-inline.html new file mode 100644 index 0000000000..b3234f787d --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-csp-violation-inline.html @@ -0,0 +1,21 @@ + <html> + <head> + <title>CSP Inline Violations Test</title> + + </head> + <body> + This Background should be neither Red nor Blue c: + </body> + <script> + "use strict"; + window.violate = () =>{ + const style = document.createElement("style"); + style.innerHTML = "body { background-color: red; }"; + document.head.appendChild(style); + }; + </script> + + <style> + background-color:blue; + </style> +</html> diff --git a/devtools/client/webconsole/test/browser/test-csp-violation-inline.html^headers^ b/devtools/client/webconsole/test/browser/test-csp-violation-inline.html^headers^ new file mode 100644 index 0000000000..f0b9c73af1 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-csp-violation-inline.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: style-src 'self'; diff --git a/devtools/client/webconsole/test/browser/test-csp-violation.html b/devtools/client/webconsole/test/browser/test-csp-violation.html new file mode 100644 index 0000000000..fdda4eb262 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-csp-violation.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta http-equiv="Content-Security-Policy" content="img-src https://example.com"></meta> + <meta http-equiv="Content-Security-Policy" content="img-src https://example.com"></meta> + <meta charset="UTF-8"> + <title>Test for Bug 1247459 - policy violations for header and META are displayed separately</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1247459">Mozilla Bug 1247459</a> +<img src="http://some.example.com/test.png"> +</body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-cspro.html b/devtools/client/webconsole/test/browser/test-cspro.html new file mode 100644 index 0000000000..d207b0949e --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-cspro.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="UTF-8"> + <title>Test for Bug 1010953 - Verify that CSP and CSPRO log different console +messages.</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1010953">Mozilla Bug 1010953</a> + + +<!-- this script file allowed by the CSP header (but not by the report-only header) --> +<script src="http://some.example.com/cspro.js"></script> + +<!-- this image allowed only be the CSP report-only header. --> +<img src="http://some.example.com/cspro.png"> +</body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-cspro.html^headers^ b/devtools/client/webconsole/test/browser/test-cspro.html^headers^ new file mode 100644 index 0000000000..03056e2cb3 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-cspro.html^headers^ @@ -0,0 +1,2 @@ +Content-Security-Policy: default-src 'self'; img-src 'self'; script-src some.example.com; +Content-Security-Policy-Report-Only: default-src 'self'; img-src some.example.com; script-src 'self'; report-uri https://example.com/ignored/;
\ No newline at end of file diff --git a/devtools/client/webconsole/test/browser/test-css-message.html b/devtools/client/webconsole/test/browser/test-css-message.html new file mode 100644 index 0000000000..f0fb6e6fe8 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-css-message.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Stub generator</title> + </head> + <body> + <p>Stub generator</p> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-cu-reporterror.js b/devtools/client/webconsole/test/browser/test-cu-reporterror.js new file mode 100644 index 0000000000..f769f73876 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-cu-reporterror.js @@ -0,0 +1,7 @@ +"use strict"; +function a() { + Cu.reportError( + "error thrown from test-cu-reporterror.js via Cu.reportError()" + ); +} +a(); diff --git a/devtools/client/webconsole/test/browser/test-data.json b/devtools/client/webconsole/test/browser/test-data.json new file mode 100644 index 0000000000..471d240b5d --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-data.json @@ -0,0 +1 @@ +{ id: "test JSON data", myArray: [ "foo", "bar", "baz", "biff" ] }
\ No newline at end of file diff --git a/devtools/client/webconsole/test/browser/test-data.json^headers^ b/devtools/client/webconsole/test/browser/test-data.json^headers^ new file mode 100644 index 0000000000..7b5e82d4b7 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-data.json^headers^ @@ -0,0 +1 @@ +Content-Type: application/json diff --git a/devtools/client/webconsole/test/browser/test-duplicate-error.html b/devtools/client/webconsole/test/browser/test-duplicate-error.html new file mode 100644 index 0000000000..60a0c984f4 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-duplicate-error.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf-8"> + <title>Console duplicate error test</title> + <!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ + + See https://bugzilla.mozilla.org/show_bug.cgi?id=582201 + --> + </head> + <body> + <h1>Heads Up Display - duplicate error test</h1> + + <script type="text/javascript"> + /* eslint-disable */ + fooDuplicateError1.bar(); + </script> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-dynamic-import.html b/devtools/client/webconsole/test/browser/test-dynamic-import.html new file mode 100644 index 0000000000..62420041b3 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-dynamic-import.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta charset="utf-8"> + <title>Test dynamic import usage in console</title> + </head> + <body> + <h1>Test dynamic import usage in console</h1> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-dynamic-import.js b/devtools/client/webconsole/test/browser/test-dynamic-import.js new file mode 100644 index 0000000000..bb3504a8e8 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-dynamic-import.js @@ -0,0 +1,12 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * @param {Number} numbers that will be summed. + * @returns {String} A string of the following form: `${arg1} + ${arg2} ${argn} = ${sum}` + */ +function sum(...args) { + return `${args.join(" + ")} = ${args.reduce((acc, i) => acc + i)}`; +} + +export { sum }; diff --git a/devtools/client/webconsole/test/browser/test-error-worker.html b/devtools/client/webconsole/test/browser/test-error-worker.html new file mode 100644 index 0000000000..07e63cfb00 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-error-worker.html @@ -0,0 +1,7 @@ +<script> +"use strict"; +var w = new Worker("test-error-worker.js"); +w.postMessage(1); +w.postMessage(2); +w.postMessage(3); +</script> diff --git a/devtools/client/webconsole/test/browser/test-error-worker.js b/devtools/client/webconsole/test/browser/test-error-worker.js new file mode 100644 index 0000000000..0dc46b4ec0 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-error-worker.js @@ -0,0 +1,20 @@ +"use strict"; + +self.addEventListener("message", function onMessage({ data }) { + return foo(data); +}); + +var w = new Worker("test-error-worker2.js"); +w.postMessage({}); + +function foo(data) { + switch (data) { + case 1: + throw new Error("hello"); + case 2: + /* eslint-disable */ + throw "there"; + case 3: + throw new DOMException("dom"); + } +} diff --git a/devtools/client/webconsole/test/browser/test-error-worker2.js b/devtools/client/webconsole/test/browser/test-error-worker2.js new file mode 100644 index 0000000000..61fe07c3c4 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-error-worker2.js @@ -0,0 +1,7 @@ +"use strict"; + +self.addEventListener("message", ({ data }) => foo(data)); + +function foo(data) { + throw new Error("worker2"); +} diff --git a/devtools/client/webconsole/test/browser/test-error-worklet.html b/devtools/client/webconsole/test/browser/test-error-worklet.html new file mode 100644 index 0000000000..249e184fe6 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-error-worklet.html @@ -0,0 +1,24 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Worklet error generator</title> + <!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ + --> +</head> +<script> +"use strict"; +const context = new AudioContext(); + +context.audioWorklet.addModule("test-syntaxerror-worklet.js").catch( + () => context.audioWorklet.addModule("test-error-worklet.js") +).then(() => { + const workletNode = new AudioWorkletNode(context, "error"); + const oscillator = new OscillatorNode(context); + oscillator.connect(workletNode); + oscillator.start(); +}); + +</script> +</html> diff --git a/devtools/client/webconsole/test/browser/test-error-worklet.js b/devtools/client/webconsole/test/browser/test-error-worklet.js new file mode 100644 index 0000000000..cca6667d19 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-error-worklet.js @@ -0,0 +1,21 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function throw_process() { + throw "process"; // eslint-disable-line no-throw-literal +} + +class ErrorProcessor extends AudioWorkletProcessor { + process() { + throw_process(); + } +} +registerProcessor("error", ErrorProcessor); + +function throw_error() { + throw new Error("addModule"); +} + +throw_error(); diff --git a/devtools/client/webconsole/test/browser/test-error.html b/devtools/client/webconsole/test/browser/test-error.html new file mode 100644 index 0000000000..a856bfab59 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-error.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf-8"> + <title>Console error test</title> + </head> + <body> + <h1>Heads Up Display - error test</h1> + <p><button>generate error</button></p> + + <script type="text/javascript"> + /* eslint-disable */ + var button = document.getElementsByTagName("button")[0]; + + button.addEventListener("click", function () { + fooBazBaz.bar(); + }, {once: true}); + </script> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-eval-error.html b/devtools/client/webconsole/test/browser/test-eval-error.html new file mode 100644 index 0000000000..ecc0fbb8cc --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-eval-error.html @@ -0,0 +1,16 @@ +<script> +/* eslint-disable no-unused-vars */ +"use strict"; + +function throwErrorObject(value) { + throw new Error("ThrowErrorObject"); +} + +function throwValue(value) { + otherFunction(value); +} + +function otherFunction(value) { + throw value; +} +</script> diff --git a/devtools/client/webconsole/test/browser/test-eval-in-stackframe.html b/devtools/client/webconsole/test/browser/test-eval-in-stackframe.html new file mode 100644 index 0000000000..3d9c4fd5c4 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-eval-in-stackframe.html @@ -0,0 +1,40 @@ +<!DOCTYPE HTML> +<html dir="ltr" lang="en"> + <head> + <meta charset="utf8"> + <!-- + - Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ + --> + <title>Test for bug 783499 - use the debugger API in the web console</title> + <script> + /* eslint-disable */ + var foo = "globalFooBug783499"; + var fooObj = { + testProp: "testValue", + }; + + function firstCall() + { + var foo = "fooFirstCall"; + var foo3 = "foo3FirstCall"; + secondCall(); + } + + function secondCall() + { + var foo2 = "foo2SecondCall"; + var fooObj = { + testProp2: "testValue2", + }; + var fooObj2 = { + testProp22: "testValue22", + }; + debugger; + } + </script> + </head> + <body> + <p>Hello world!</p> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-eval-sources.html b/devtools/client/webconsole/test/browser/test-eval-sources.html new file mode 100644 index 0000000000..aec86b42c4 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-eval-sources.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<meta charset=UTF-8> +<script> +/* eslint-disable */ +eval("window.foo = function() { console.log('FOO'); }"); +eval("window.bar = function() { throw new Error('BAR') };"); +eval(`window.baz = function() { + console.log('BAZ'); + console.trace('TRACE'); + } + //# sourceURL=my-foo.js`); + +foo(); +baz(); +bar(); + +</script> diff --git a/devtools/client/webconsole/test/browser/test-evaluate-worker.html b/devtools/client/webconsole/test/browser/test-evaluate-worker.html new file mode 100644 index 0000000000..2f424ec632 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-evaluate-worker.html @@ -0,0 +1,9 @@ +<script> +"use strict"; +var w = new Worker("test-evaluate-worker.js"); + +// eslint-disable-next-line no-unused-vars +function pauseInWorker(value) { + w.postMessage(value); +} +</script> diff --git a/devtools/client/webconsole/test/browser/test-evaluate-worker.js b/devtools/client/webconsole/test/browser/test-evaluate-worker.js new file mode 100644 index 0000000000..7d3ca22979 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-evaluate-worker.js @@ -0,0 +1,8 @@ +"use strict"; + +self.addEventListener("message", ({ data }) => foo(data)); + +function foo(data) { + // eslint-disable-next-line no-debugger + debugger; +} diff --git a/devtools/client/webconsole/test/browser/test-external-script-errors.html b/devtools/client/webconsole/test/browser/test-external-script-errors.html new file mode 100644 index 0000000000..a4d2b87a17 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-external-script-errors.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> +<!-- + ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * + * ***** END LICENSE BLOCK ***** + --> + <title>Test for bug 597136: external script errors</title> + </head> + <body> + <h1>Test for bug 597136: external script errors</h1> + <p><button onclick="f()">Click me</button</p> + + <script type="text/javascript" + src="test-external-script-errors.js"></script> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-external-script-errors.js b/devtools/client/webconsole/test/browser/test-external-script-errors.js new file mode 100644 index 0000000000..2a87f9a035 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-external-script-errors.js @@ -0,0 +1,9 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-disable */ + +function f() { + bogus.g(); +} + diff --git a/devtools/client/webconsole/test/browser/test-iframe-child.html b/devtools/client/webconsole/test/browser/test-iframe-child.html new file mode 100644 index 0000000000..af1a8e419e --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-iframe-child.html @@ -0,0 +1,12 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>test for bug 989025 - iframe child</title> + </head> + <body> + <p>test for bug 989025 - iframe child</p> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-iframe-insecure-form-action.html b/devtools/client/webconsole/test/browser/test-iframe-insecure-form-action.html new file mode 100644 index 0000000000..d14b5cdd7c --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-iframe-insecure-form-action.html @@ -0,0 +1,15 @@ +<!doctype html> +<html> + <head> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <h1>iframe 2</h1> + <p>This frame contains a password field inside a form with insecure action.</p> + <form action="http://test"> + <input type="password" name="pwd"> + </form> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-iframe-parent.html b/devtools/client/webconsole/test/browser/test-iframe-parent.html new file mode 100644 index 0000000000..6025dcf24c --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-iframe-parent.html @@ -0,0 +1,24 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>test for bug 989025 - iframe parent</title> + </head> + <body> + <p>test for bug 989025 - iframe parent</p> + <iframe src="http://mochi.test:8888/browser/devtools/client/webconsole/test/browser/test-iframe-child.html"></iframe> + <button>Throw Error</button> + <script> + "use strict"; + + /* exported throwError */ + function throwError() { + throwError.asdf(); + } + + document.querySelector("button").addEventListener("click", throwError); + </script> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-iframe-wrong-hud-iframe.html b/devtools/client/webconsole/test/browser/test-iframe-wrong-hud-iframe.html new file mode 100644 index 0000000000..ebf9c515fe --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-iframe-wrong-hud-iframe.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>WebConsole test: iframe associated to the wrong HUD</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>WebConsole test: iframe associated to the wrong HUD.</p> + <p>This is the iframe!</p> + </body> + </html> diff --git a/devtools/client/webconsole/test/browser/test-iframe-wrong-hud.html b/devtools/client/webconsole/test/browser/test-iframe-wrong-hud.html new file mode 100644 index 0000000000..cbb3f26d24 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-iframe-wrong-hud.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>WebConsole test: iframe associated to the wrong HUD</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>WebConsole test: iframe associated to the wrong HUD.</p> + <iframe src="http://example.com/browser/devtools/client/webconsole/test/browser/test-iframe-wrong-hud-iframe.html"></iframe> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-iframe1.html b/devtools/client/webconsole/test/browser/test-iframe1.html new file mode 100644 index 0000000000..14a6fec45c --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-iframe1.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <script> + /* eslint-disable */ + console.log("iframe 1"); + </script> + </head> + <body> + <h1>iframe 1</h1> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-iframe2.html b/devtools/client/webconsole/test/browser/test-iframe2.html new file mode 100644 index 0000000000..9266824886 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-iframe2.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <script> + /* eslint-disable */ + console.log("iframe 2"); + blah; + </script> + </head> + <body> + <h1>iframe 2</h1> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-iframe3.html b/devtools/client/webconsole/test/browser/test-iframe3.html new file mode 100644 index 0000000000..ce96f67d1f --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-iframe3.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <script> + /* eslint-disable */ + console.log("iframe 3"); + </script> + </head> + <body> + <h1>iframe 3</h1> + <iframe src="test-iframe1.html"></iframe> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-image.png b/devtools/client/webconsole/test/browser/test-image.png Binary files differnew file mode 100644 index 0000000000..769c636340 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-image.png diff --git a/devtools/client/webconsole/test/browser/test-image.png^headers^ b/devtools/client/webconsole/test/browser/test-image.png^headers^ new file mode 100644 index 0000000000..1a90bacd4b --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-image.png^headers^ @@ -0,0 +1 @@ +Set-Cookie: name=value diff --git a/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning-inner.html b/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning-inner.html new file mode 100644 index 0000000000..ccb363ed9e --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning-inner.html @@ -0,0 +1,13 @@ +<!doctype html> +<html> + <head> + <meta charset="utf8"> + <title>Bug 752559 - print warning to error console when iframe sandbox + is being used ineffectively</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>I am sandboxed and want to escape.</p> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning-nested1.html b/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning-nested1.html new file mode 100644 index 0000000000..2b3b8240b9 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning-nested1.html @@ -0,0 +1,13 @@ +<!doctype html> +<html> + <head> + <meta charset="utf8"> + <title>Bug 752559 - print warning to error console when iframe sandbox + is being used ineffectively</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <iframe src="http://www.example.com/browser/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning-inner.html"></iframe> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning-nested2.html b/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning-nested2.html new file mode 100644 index 0000000000..3545901a2c --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning-nested2.html @@ -0,0 +1,13 @@ +<!doctype html> +<html> + <head> + <meta charset="utf8"> + <title>Bug 752559 - print warning to error console when iframe sandbox + is being used ineffectively</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <iframe sandbox="allow-scripts allow-same-origin" src="http://www.example.com/browser/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning-inner.html"></iframe> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning0.html b/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning0.html new file mode 100644 index 0000000000..2d0b37dc6e --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning0.html @@ -0,0 +1,13 @@ +<!doctype html> +<html> + <head> + <meta charset="utf8"> + <title>Bug 752559 - print warning to error console when iframe sandbox + is being used ineffectively (allow-scripts, allow-same-origin)</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <iframe src="test-ineffective-iframe-sandbox-warning-inner.html" sandbox="allow-scripts allow-same-origin"></iframe> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning1.html b/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning1.html new file mode 100644 index 0000000000..f2cf80418f --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning1.html @@ -0,0 +1,13 @@ +<!doctype html> +<html> + <head> + <meta charset="utf8"> + <title>Bug 752559 - print warning to error console when iframe sandbox + is being used ineffectively (allow-scripts, no allow-same-origin)</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <iframe src="test-ineffective-iframe-sandbox-warning-inner.html" sandbox="allow-scripts"></iframe> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning2.html b/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning2.html new file mode 100644 index 0000000000..6053db1615 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning2.html @@ -0,0 +1,13 @@ +<!doctype html> +<html> + <head> + <meta charset="utf8"> + <title>Bug 752559 - print warning to error console when iframe sandbox + is being used ineffectively (no allow-scripts, allow-same-origin)</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <iframe src="test-ineffective-iframe-sandbox-warning-inner.html" sandbox="allow-same-origin"></iframe> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning3.html b/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning3.html new file mode 100644 index 0000000000..0080d5a3d3 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning3.html @@ -0,0 +1,13 @@ +<!doctype html> +<html> + <head> + <meta charset="utf8"> + <title>Bug 752559 - print warning to error console when iframe sandbox + is being used ineffectively (allow-scripts, allow-same-origin)</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <iframe sandbox="allow-scripts allow-same-origin" src="http://www.example.com/browser/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning-inner.html"></iframe> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning4.html b/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning4.html new file mode 100644 index 0000000000..4b968a4d75 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning4.html @@ -0,0 +1,13 @@ +<!doctype html> +<html> + <head> + <meta charset="utf8"> + <title>Bug 752559 - print warning to error console when iframe sandbox + is being used ineffectively (allow-scripts, allow-same-origin, nested)</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <iframe sandbox="allow-scripts allow-same-origin" src="http://www.example.com/browser/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning-nested1.html"></iframe> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning5.html b/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning5.html new file mode 100644 index 0000000000..35fe7c16ee --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning5.html @@ -0,0 +1,13 @@ +<!doctype html> +<html> + <head> + <meta charset="utf8"> + <title>Bug 752559 - print warning to error console when iframe sandbox + is being used ineffectively (nested, allow-scripts, allow-same-origin)</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <iframe src="http://www.example.com/browser/devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning-nested2.html"></iframe> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-insecure-frame.html b/devtools/client/webconsole/test/browser/test-insecure-frame.html new file mode 100644 index 0000000000..1f3662d773 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-insecure-frame.html @@ -0,0 +1,14 @@ +<!doctype html> +<html> + <head> + <meta + content="text/html;charset=UTF-8" http-equiv="Content-type"> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <h1>iframe 1</h1> + <p>This frame is served with an insecure password field.</p> + <iframe src="http://example.com/browser/devtools/client/webconsole/test/browser/test-iframe-insecure-form-action.html"></iframe> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-insecure-passwords-about-blank-web-console-warning.html b/devtools/client/webconsole/test/browser/test-insecure-passwords-about-blank-web-console-warning.html new file mode 100644 index 0000000000..89e4c14d78 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-insecure-passwords-about-blank-web-console-warning.html @@ -0,0 +1,29 @@ +<!doctype html> +<html> + <head> + <meta charset="utf8"> + <title>Bug 762593 - Add warning/error Message to Web Console when the + page includes Insecure Password fields</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + + <!-- This test tests the scenario where a javascript adds password fields to + an about:blank iframe inside an insecure web page. It ensures that + insecure password fields like those are detected and a warning is sent to + the web console. --> + </head> + <body> + <p>This insecure page is served with an about:blank iframe. A script then adds a + password field to it.</p> + <iframe id = "myiframe" width = "300" height="300" > + </iframe> + <script> + /* eslint-disable */ + var doc = window.document; + var myIframe = doc.getElementById("myiframe"); + myIframe.contentDocument.open(); + myIframe.contentDocument.write("<form><input type = 'password' name='pwd' value='test'> </form>"); + myIframe.contentDocument.close(); + </script> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-insecure-passwords-web-console-warning.html b/devtools/client/webconsole/test/browser/test-insecure-passwords-web-console-warning.html new file mode 100644 index 0000000000..06e203f748 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-insecure-passwords-web-console-warning.html @@ -0,0 +1,15 @@ +<!doctype html> +<html> + <head> + <meta charset="utf8"> + <title>Bug 762593 - Add warning/error Message to Web Console when the + page includes Insecure Password fields</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>This page is served with an iframe with insecure password field.</p> + <iframe src="test-insecure-frame.html"> + </iframe> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-inspect-cross-domain-objects-frame.html b/devtools/client/webconsole/test/browser/test-inspect-cross-domain-objects-frame.html new file mode 100644 index 0000000000..4665154a36 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-inspect-cross-domain-objects-frame.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 869003</title> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + <script type="text/javascript"> + /* eslint-disable */ + window.onload = function testConsoleLogging() + { + var obj1 = { hello: "world!", bug: 869003 }; + var obj2 = Object.assign(function func(arg){}, obj1); + var obj3 = document.getElementById("testEl"); + console.log("foobar", obj1, obj2, obj3); + }; + </script> + </head> + <body> + <p id="testEl">Make sure users can inspect objects from cross-domain iframes.</p> + <p>Iframe window.</p> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-inspect-cross-domain-objects-top.html b/devtools/client/webconsole/test/browser/test-inspect-cross-domain-objects-top.html new file mode 100644 index 0000000000..f1c8a4fe54 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-inspect-cross-domain-objects-top.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 869003</title> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>Make sure users can inspect objects from cross-domain iframes.</p> + <p>Top window.</p> + <iframe src="http://example.org/browser/devtools/client/webconsole/test/browser/test-inspect-cross-domain-objects-frame.html"></iframe> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-local-session-storage.html b/devtools/client/webconsole/test/browser/test-local-session-storage.html new file mode 100644 index 0000000000..c38f805cc4 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-local-session-storage.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8" /> + <title>localStorage and sessionStorage Test</title> + + <script> + "use strict"; + + /* eslint-disable no-unused-vars */ + + function init() { + // We use the value key to ensure we cover an issue with iterating through + // storage entries... please leave this key as "key" + localStorage.setItem("key", "value1"); + localStorage.setItem("key2", "value2"); + + // We use the value key to ensure we cover an issue with iterating through + // storage entries... please leave this key as "key" + sessionStorage.setItem("key", "value3"); + sessionStorage.setItem("key2", "value4"); + } + </script> +</head> +<body onload="init()"> + <h1>localStorage and sessionStorage Test</h1> +</body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-location-debugger-link-console-log.js b/devtools/client/webconsole/test/browser/test-location-debugger-link-console-log.js new file mode 100644 index 0000000000..14d6f8f366 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-location-debugger-link-console-log.js @@ -0,0 +1,10 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function onLoad123() { + console.log("Blah Blah"); +} + +window.addEventListener("load", onLoad123); diff --git a/devtools/client/webconsole/test/browser/test-location-debugger-link-errors.js b/devtools/client/webconsole/test/browser/test-location-debugger-link-errors.js new file mode 100644 index 0000000000..494cd23f60 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-location-debugger-link-errors.js @@ -0,0 +1,8 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +window.addEventListener("load", function() { + document.bar(); +}); diff --git a/devtools/client/webconsole/test/browser/test-location-debugger-link-logpoint-1.js b/devtools/client/webconsole/test/browser/test-location-debugger-link-logpoint-1.js new file mode 100644 index 0000000000..40e5461e2e --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-location-debugger-link-logpoint-1.js @@ -0,0 +1,13 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function add() { + const a = 1; + const b = 2; + + return a + b; +} + +add(); diff --git a/devtools/client/webconsole/test/browser/test-location-debugger-link-logpoint-2.js b/devtools/client/webconsole/test/browser/test-location-debugger-link-logpoint-2.js new file mode 100644 index 0000000000..27952587eb --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-location-debugger-link-logpoint-2.js @@ -0,0 +1,13 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function subtract() { + const c = 1; + const d = 2; + + return c - d; +} + +subtract(); diff --git a/devtools/client/webconsole/test/browser/test-location-debugger-link-logpoint.html b/devtools/client/webconsole/test/browser/test-location-debugger-link-logpoint.html new file mode 100644 index 0000000000..f87997954c --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-location-debugger-link-logpoint.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <title> + Web Console test for opening logpoint message links in Debugger + </title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <script + type="text/javascript" + src="test-location-debugger-link-logpoint-1.js" + ></script> + <script + type="text/javascript" + src="test-location-debugger-link-logpoint-2.js" + ></script> + </head> + <body> + <p>Web Console test for opening logpoint message links in Debugger.</p> + <button onclick="add()">Add</button> + <button onclick="subtract()">Subtract</button> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-location-debugger-link.html b/devtools/client/webconsole/test/browser/test-location-debugger-link.html new file mode 100644 index 0000000000..02aa90a12b --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-location-debugger-link.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for opening JS/Console call Links in Debugger</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <script type="text/javascript" src="test-location-debugger-link-errors.js"></script> + <script type="text/javascript" src="test-location-debugger-link-console-log.js"></script> + </head> + <body> + <p>Web Console test for opening JS/Console call Links in Debugger.</p> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-location-styleeditor-link-1.css b/devtools/client/webconsole/test/browser/test-location-styleeditor-link-1.css new file mode 100644 index 0000000000..647fddd511 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-location-styleeditor-link-1.css @@ -0,0 +1,9 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +body { + color: #0f0; + font-weight: green; +} diff --git a/devtools/client/webconsole/test/browser/test-location-styleeditor-link-2.css b/devtools/client/webconsole/test/browser/test-location-styleeditor-link-2.css new file mode 100644 index 0000000000..c24cb28a77 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-location-styleeditor-link-2.css @@ -0,0 +1,9 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +body { + color: #0fl; + font-weight: bold; +} diff --git a/devtools/client/webconsole/test/browser/test-location-styleeditor-link-minified.css b/devtools/client/webconsole/test/browser/test-location-styleeditor-link-minified.css new file mode 100644 index 0000000000..2c0c8a0fc8 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-location-styleeditor-link-minified.css @@ -0,0 +1,5 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +body{display:fake} diff --git a/devtools/client/webconsole/test/browser/test-location-styleeditor-link.html b/devtools/client/webconsole/test/browser/test-location-styleeditor-link.html new file mode 100644 index 0000000000..c964c5ec42 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-location-styleeditor-link.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for opening CSS Links in Style Editor</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <link rel="stylesheet" href="test-location-styleeditor-link-1.css"> + <link rel="stylesheet" href="test-location-styleeditor-link-2.css"> + <link rel="stylesheet" href="test-location-styleeditor-link-minified.css"> + </head> + <body> + <p>Web Console test for opening CSS Links in Style Editor.</p> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-mangled-function.js b/devtools/client/webconsole/test/browser/test-mangled-function.js new file mode 100644 index 0000000000..553c0e141d --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-mangled-function.js @@ -0,0 +1,2 @@ +"use strict";window.test_mangled = function() {console.log("simple mangled function");}; +//# sourceMappingURL=test-mangled-function.js.map diff --git a/devtools/client/webconsole/test/browser/test-mangled-function.js.map b/devtools/client/webconsole/test/browser/test-mangled-function.js.map new file mode 100644 index 0000000000..fbd5885f55 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-mangled-function.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["test-mangled-function.src.js"],"names":["window","test_mangled","console","log"],"mappings":"AAAA,aAEAA,OAAOC,aAAe,WACpBC,QAAQC,IAAI"}
\ No newline at end of file diff --git a/devtools/client/webconsole/test/browser/test-mangled-function.src.js b/devtools/client/webconsole/test/browser/test-mangled-function.src.js new file mode 100644 index 0000000000..d861301b5d --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-mangled-function.src.js @@ -0,0 +1,5 @@ +"use strict"; + +window.test_mangled = function() { + console.log("simple mangled function"); +}; diff --git a/devtools/client/webconsole/test/browser/test-message-categories-canvas-css.html b/devtools/client/webconsole/test/browser/test-message-categories-canvas-css.html new file mode 100644 index 0000000000..d0bb2da0e6 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-message-categories-canvas-css.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 595934 - category: CSS Parser (with + Canvas)</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <script type="text/javascript" + src="test-message-categories-canvas-css.js"></script> + </head> + <body> + <p>Web Console test for bug 595934 - category "CSS Parser" (with + Canvas).</p> + <p><canvas width="200" height="200">Canvas support is required!</canvas></p> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-message-categories-canvas-css.js b/devtools/client/webconsole/test/browser/test-message-categories-canvas-css.js new file mode 100644 index 0000000000..e126c5b59b --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-message-categories-canvas-css.js @@ -0,0 +1,10 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-disable */ + +window.addEventListener("DOMContentLoaded", function () { + var canvas = document.querySelector("canvas"); + var context = canvas.getContext("2d"); + context.strokeStyle = "foobarCanvasCssParser"; +}); diff --git a/devtools/client/webconsole/test/browser/test-message-categories-css-loader.css b/devtools/client/webconsole/test/browser/test-message-categories-css-loader.css new file mode 100644 index 0000000000..0a963b945a --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-message-categories-css-loader.css @@ -0,0 +1,9 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +body { + color: #0f0; + font-weight: bold; +} diff --git a/devtools/client/webconsole/test/browser/test-message-categories-css-loader.css^headers^ b/devtools/client/webconsole/test/browser/test-message-categories-css-loader.css^headers^ new file mode 100644 index 0000000000..e7be84a714 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-message-categories-css-loader.css^headers^ @@ -0,0 +1 @@ +Content-Type: image/png diff --git a/devtools/client/webconsole/test/browser/test-message-categories-css-loader.html b/devtools/client/webconsole/test/browser/test-message-categories-css-loader.html new file mode 100644 index 0000000000..0ecebd36f1 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-message-categories-css-loader.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 595934 - category: CSS Loader</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <link rel="stylesheet" href="test-message-categories-css-loader.css"> + </head> + <body> + <p>Web Console test for bug 595934 - category "CSS Loader".</p> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-message-categories-css-parser.css b/devtools/client/webconsole/test/browser/test-message-categories-css-parser.css new file mode 100644 index 0000000000..f6db823987 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-message-categories-css-parser.css @@ -0,0 +1,10 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +p { + color: #0f0; + foobarCssParser: failure; +} + diff --git a/devtools/client/webconsole/test/browser/test-message-categories-css-parser.html b/devtools/client/webconsole/test/browser/test-message-categories-css-parser.html new file mode 100644 index 0000000000..359165bd7a --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-message-categories-css-parser.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 595934 - category: CSS Parser</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <link rel="stylesheet" type="text/css" + href="test-message-categories-css-parser.css"> + </head> + <body> + <p>Web Console test for bug 595934 - category "CSS Parser".</p> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-message-categories-empty-getelementbyid.html b/devtools/client/webconsole/test/browser/test-message-categories-empty-getelementbyid.html new file mode 100644 index 0000000000..7f759a20c0 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-message-categories-empty-getelementbyid.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 595934 - category: DOM. + (empty getElementById())</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <script type="text/javascript" + src="test-message-categories-empty-getelementbyid.js"></script> + </head> + <body> + <p>Web Console test for bug 595934 - category "DOM" + (empty getElementById()).</p> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-message-categories-empty-getelementbyid.js b/devtools/client/webconsole/test/browser/test-message-categories-empty-getelementbyid.js new file mode 100644 index 0000000000..e861359d18 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-message-categories-empty-getelementbyid.js @@ -0,0 +1,8 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-disable */ + +window.addEventListener("load", function () { + document.getElementById(""); +}); diff --git a/devtools/client/webconsole/test/browser/test-message-categories-html.html b/devtools/client/webconsole/test/browser/test-message-categories-html.html new file mode 100644 index 0000000000..d9c09b5692 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-message-categories-html.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 595934 - category: HTML</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>Web Console test for bug 595934 - category "HTML".</p> + <form action="?" enctype="multipart/form-data"> + <p><label>Input <input type="text" value="test value"></label></p> + </form> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-message-categories-image.html b/devtools/client/webconsole/test/browser/test-message-categories-image.html new file mode 100644 index 0000000000..85592a7a45 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-message-categories-image.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 595934 - category: Image</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>Web Console test for bug 595934 - category Image.</p> + <p><img src="test-message-categories-image.jpg" alt="corrupted image"></p> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-message-categories-image.jpg b/devtools/client/webconsole/test/browser/test-message-categories-image.jpg Binary files differnew file mode 100644 index 0000000000..947e5f11ba --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-message-categories-image.jpg diff --git a/devtools/client/webconsole/test/browser/test-message-categories-imagemap.html b/devtools/client/webconsole/test/browser/test-message-categories-imagemap.html new file mode 100644 index 0000000000..a5c269d024 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-message-categories-imagemap.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 595934 - category: ImageMap</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>Web Console test for bug 595934 - category "ImageMap".</p> + <p><img src="test-image.png" usemap="#testMap" alt="Test image"></p> + <map name="testMap"> + <area shape="rect" coords="0,0,10,10,5" href="#" alt="Test area" /> + </map> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-message-categories-malformedxml-external.html b/devtools/client/webconsole/test/browser/test-message-categories-malformedxml-external.html new file mode 100644 index 0000000000..49b6783f2f --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-message-categories-malformedxml-external.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 595934 - category: malformed-xml. + (external file)</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <script type="text/javascript"> + /* eslint-disable */ + var req = new XMLHttpRequest(); + req.open("GET", "test-message-categories-malformedxml-external.xml", true); + req.send(null); + </script> + </head> + <body> + <p>Web Console test for bug 595934 - category "malformed-xml" + (external file).</p> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-message-categories-malformedxml-external.xml b/devtools/client/webconsole/test/browser/test-message-categories-malformedxml-external.xml new file mode 100644 index 0000000000..4812786f10 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-message-categories-malformedxml-external.xml @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>Web Console test for bug 595934 - category "malformed-xml".</p> + </body> diff --git a/devtools/client/webconsole/test/browser/test-message-categories-malformedxml.xhtml b/devtools/client/webconsole/test/browser/test-message-categories-malformedxml.xhtml new file mode 100644 index 0000000000..62689c567c --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-message-categories-malformedxml.xhtml @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> + <head> + <title>Web Console test for bug 595934 - category: malformed-xml</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>Web Console test for bug 595934 - category "malformed-xml".</p> + </body> diff --git a/devtools/client/webconsole/test/browser/test-message-categories-svg.xhtml b/devtools/client/webconsole/test/browser/test-message-categories-svg.xhtml new file mode 100644 index 0000000000..04c4227335 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-message-categories-svg.xhtml @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> + <head> + <title>Web Console test for bug 595934 - category: SVG</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>Web Console test for bug 595934 - category "SVG".</p> + <svg version="1.1" width="120" height="fooBarSVG" + xmlns="http://www.w3.org/2000/svg"> + <ellipse fill="#0f0" stroke="#000" cx="50%" + cy="50%" rx="50%" ry="50%" /> + </svg> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-message-categories-workers.html b/devtools/client/webconsole/test/browser/test-message-categories-workers.html new file mode 100644 index 0000000000..df49720e7a --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-message-categories-workers.html @@ -0,0 +1,18 @@ +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 595934 - category: DOM Worker + javascript</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p id="foobar">Web Console test for bug 595934 - category "DOM Worker + javascript".</p> + <script type="text/javascript"> + /* eslint-disable */ + var myWorker = new Worker("test-message-categories-workers.js"); + myWorker.postMessage("hello world"); + </script> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-message-categories-workers.js b/devtools/client/webconsole/test/browser/test-message-categories-workers.js new file mode 100644 index 0000000000..9fd2b86639 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-message-categories-workers.js @@ -0,0 +1,11 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* global fooBarWorker*/ +/* eslint-disable no-unused-vars*/ + +"use strict"; + +var onmessage = function() { + fooBarWorker(); +}; diff --git a/devtools/client/webconsole/test/browser/test-mixedcontent-securityerrors.html b/devtools/client/webconsole/test/browser/test-mixedcontent-securityerrors.html new file mode 100644 index 0000000000..cb8cfdaaf5 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-mixedcontent-securityerrors.html @@ -0,0 +1,21 @@ +<!-- + Bug 875456 - Log mixed content messages from the Mixed Content Blocker to the + Security Pane in the Web Console +--> + +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf8"> + <title>Mixed Content test - http on https</title> + <script src="testscript.js"></script> + <!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ + --> + </head> + <body> + <iframe src="http://example.com"></iframe> + <img src="http://example.com/tests/image/test/mochitest/blue.png"></img> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-navigate-to-parse-error.html b/devtools/client/webconsole/test/browser/test-navigate-to-parse-error.html new file mode 100644 index 0000000000..e806ea9498 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-navigate-to-parse-error.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta http-equiv="Content-Security-Policy" content="navigate-to https://example.com"></meta> + <meta charset="UTF-8"> + <title>Test for Bug 1566149 - Write test to ensure CSP 'navigate-to' does not parse</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1566149">Mozilla Bug 1566149</a> +</body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-nested-iframe-storageaccess-errors.html b/devtools/client/webconsole/test/browser/test-nested-iframe-storageaccess-errors.html new file mode 100644 index 0000000000..1b48c45e89 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-nested-iframe-storageaccess-errors.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf8"> + <title>requestStorageAccess test</title> + <!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ + --> + </head> + <body> + <iframe src="https://itisatracker.org/browser/devtools/client/webconsole/test/browser/test-storageaccess-errors.html"></iframe> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-network-event.html b/devtools/client/webconsole/test/browser/test-network-event.html new file mode 100644 index 0000000000..695d76c608 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-network-event.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Stub generator for network event</title> + </head> + <body> + <p>Stub generator for network event</p> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-network-exceptions.html b/devtools/client/webconsole/test/browser/test-network-exceptions.html new file mode 100644 index 0000000000..572c038b72 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-network-exceptions.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 618078 - exception in async network request + callback</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <script type="text/javascript"> + /* eslint-disable */ + var req = new XMLHttpRequest(); + req.open('GET', 'http://example.com', true); + req.onreadystatechange = function() { + if (req.readyState == 4) { + bug618078exception(); + } + }; + req.send(null); + </script> + </head> + <body> + <p>Web Console test for bug 618078 - exception in async network request + callback.</p> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-network-request.html b/devtools/client/webconsole/test/browser/test-network-request.html new file mode 100644 index 0000000000..e971ef3e61 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-network-request.html @@ -0,0 +1,50 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf-8"> + <title>Console HTTP test page</title> + <script type="text/javascript"> + /* exported testXhrGet, testXhrWarn, testXhrPost, testXhrPostSlowResponse */ + "use strict"; + + function makeXhr(method, url, requestBody, callback) { + const xmlhttp = new XMLHttpRequest(); + xmlhttp.open(method, url, true); + xmlhttp.onreadystatechange = function() { + if (callback && xmlhttp.readyState == 4) { + callback(); + } + }; + xmlhttp.send(requestBody); + } + + function testXhrGet(callback) { + makeXhr("get", "test-data.json", null, callback); + } + + function testXhrWarn(callback) { + makeXhr("get", "sjs_cors-test-server.sjs", null, callback); + } + + function testXhrPost(callback) { + makeXhr("post", "test-data.json", "Hello world!", callback); + } + + function testXhrPostSlowResponse(callback) { + makeXhr("post", "sjs_slow-response-test-server.sjs", "Hello world!", callback); + } + </script> + </head> + <body> + <h1>Heads Up Display HTTP Logging Testpage</h1> + <h2>This page is used to test the HTTP logging.</h2> + + <form action="test-network-request.html" method="post"> + <input name="name" type="text" value="foo bar"><br> + <input name="age" type="text" value="144"><br> + </form> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-network.html b/devtools/client/webconsole/test/browser/test-network.html new file mode 100644 index 0000000000..69d3422e32 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-network.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"><head> + <meta charset="utf-8"> + <title>Console network test</title> + <script src="testscript.js?foo"></script> + </head> + <body> + <h1>Heads Up Display Network Test Page</h1> + <img src="test-image.png"></img> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-non-javascript-mime-worker.html b/devtools/client/webconsole/test/browser/test-non-javascript-mime-worker.html new file mode 100644 index 0000000000..beed438a2f --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-non-javascript-mime-worker.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for script with non-JavaScript MIME type</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <script> + "use strict"; + + // Test new Worker + new Worker("http://example.com/browser/devtools/client/webconsole/test/browser/test-non-javascript-mime.js"); + + // Test importScripts + const source = `importScripts("http://example.com/browser/devtools/client/webconsole/" + + "test/browser/" + + "test-non-javascript-mime.js");`; + const url = URL.createObjectURL(new Blob([source], {type: "application/javascript"})); + new Worker(url); + </script> + </head> + <body> + <p>Web Console test for Worker and importScripts() inside Worker with non-JavaScript MIME type.</p> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-non-javascript-mime.html b/devtools/client/webconsole/test/browser/test-non-javascript-mime.html new file mode 100644 index 0000000000..50891983ec --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-non-javascript-mime.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for script with non-JavaScript MIME type</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <script type="text/javascript" src="test-non-javascript-mime.js"></script> + </head> + <body> + <p>Web Console test for script with non-JavaScript MIME type.</p> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-non-javascript-mime.js b/devtools/client/webconsole/test/browser/test-non-javascript-mime.js new file mode 100644 index 0000000000..4b9e4bb15c --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-non-javascript-mime.js @@ -0,0 +1 @@ +// Not empty. The ^headers^ file is important for this test. diff --git a/devtools/client/webconsole/test/browser/test-non-javascript-mime.js^headers^ b/devtools/client/webconsole/test/browser/test-non-javascript-mime.js^headers^ new file mode 100644 index 0000000000..7f77d67005 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-non-javascript-mime.js^headers^ @@ -0,0 +1 @@ +Content-Type: text/plain
\ No newline at end of file diff --git a/devtools/client/webconsole/test/browser/test-reopen-closed-tab.html b/devtools/client/webconsole/test/browser/test-reopen-closed-tab.html new file mode 100644 index 0000000000..b47c15692d --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-reopen-closed-tab.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf-8"> + <title>Bug 597756: test error logging after tab close and reopen</title> + <!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ + --> + </head> + <body> + <h1>Bug 597756: test error logging after tab close and reopen.</h1> + + <script type="text/javascript"> + /* eslint-disable */ + fooBug597756_error.bar(); + </script> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-same-origin-required-load.html b/devtools/client/webconsole/test/browser/test-same-origin-required-load.html new file mode 100644 index 0000000000..ba80eb956c --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-same-origin-required-load.html @@ -0,0 +1,26 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <title>Test loads that are required to be same-origin (no CORS involved)</title> + <script> + /* exported testTrack */ + "use strict"; + + function testTrack(url) { + const body = document.body; + const video = document.createElement("video"); + const track = document.createElement("track"); + track.src = url; + track.default = true; + video.append(track); + body.append(video); + } + </script> + </head> + <body> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-simple-function.html b/devtools/client/webconsole/test/browser/test-simple-function.html new file mode 100644 index 0000000000..76da4efef0 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-simple-function.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <script src="test-simple-function.js"></script> + <script src="test-mangled-function.js"></script> + </head> + <body> + <p>Test inspecting an element</p> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-simple-function.js b/devtools/client/webconsole/test/browser/test-simple-function.js new file mode 100644 index 0000000000..9b0d2f55e0 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-simple-function.js @@ -0,0 +1,11 @@ +"use strict"; + +window.test = function() { + console.log("simple function"); +}; + +window.test_bound_target = function() { + console.log("simple bound target function"); +}; + +window.test_bound = window.test_bound_target.bind(window); diff --git a/devtools/client/webconsole/test/browser/test-sourcemap-error-01.html b/devtools/client/webconsole/test/browser/test-sourcemap-error-01.html new file mode 100644 index 0000000000..95d2503d11 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-sourcemap-error-01.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Test that a missing source map is reported to the console</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <script type="text/javascript" src="test-sourcemap-error-01.js"></script> + </head> + <body> + <p>Web Console test for source map failure.</p> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-sourcemap-error-01.js b/devtools/client/webconsole/test/browser/test-sourcemap-error-01.js new file mode 100644 index 0000000000..74cc832a38 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-sourcemap-error-01.js @@ -0,0 +1,7 @@ +"use strict"; +window.qqz = function() { + console.log("here"); +}; +window.qqz(); +/* eslint-disable spaced-comment */ +//# sourceMappingURL=no-such-file.js.map diff --git a/devtools/client/webconsole/test/browser/test-sourcemap-error-02.html b/devtools/client/webconsole/test/browser/test-sourcemap-error-02.html new file mode 100644 index 0000000000..38cb62e01e --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-sourcemap-error-02.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Test that an invalid source map URL is reported to the console</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <script type="text/javascript" src="test-sourcemap-error-02.js"></script> + </head> + <body> + <p>Web Console test for source map failure.</p> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-sourcemap-error-02.js b/devtools/client/webconsole/test/browser/test-sourcemap-error-02.js new file mode 100644 index 0000000000..8b9b37913f --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-sourcemap-error-02.js @@ -0,0 +1,7 @@ +"use strict"; +window.qqz = function() { + console.log("here"); +}; +window.qqz(); +/* eslint-disable spaced-comment */ +//# sourceMappingURL=data:invalid diff --git a/devtools/client/webconsole/test/browser/test-sourcemap-original.js b/devtools/client/webconsole/test/browser/test-sourcemap-original.js new file mode 100644 index 0000000000..107d2a01b6 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-sourcemap-original.js @@ -0,0 +1,18 @@ +/* eslint-disable */ + +/** + * this + * is + * a + * function + */ +function logString(str) { + console.log(str); +} + +function logTrace() { + var logTraceInner = function() { + console.trace(); + }; + logTraceInner(); +} diff --git a/devtools/client/webconsole/test/browser/test-sourcemap.min.js b/devtools/client/webconsole/test/browser/test-sourcemap.min.js new file mode 100644 index 0000000000..957a85ffd2 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-sourcemap.min.js @@ -0,0 +1,3 @@ +/* eslint-disable */ +function logString(str){console.log(str)}function logTrace(){var logTraceInner=function(){console.trace()};logTraceInner()} +//# sourceMappingURL=test-sourcemap.min.js.map
\ No newline at end of file diff --git a/devtools/client/webconsole/test/browser/test-sourcemap.min.js.map b/devtools/client/webconsole/test/browser/test-sourcemap.min.js.map new file mode 100644 index 0000000000..a5e2c72199 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-sourcemap.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["test-sourcemap-original.js"],"names":["logString","str","console","log","logTrace","logTraceInner","trace"],"mappings":";AAQA,SAASA,UAAUC,KACjBC,QAAQC,IAAIF,KAGd,SAASG,WACP,IAAIC,cAAgB,WAClBH,QAAQI,SAEVD"}
\ No newline at end of file diff --git a/devtools/client/webconsole/test/browser/test-stacktrace-location-debugger-link.html b/devtools/client/webconsole/test/browser/test-stacktrace-location-debugger-link.html new file mode 100644 index 0000000000..a665974b49 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-stacktrace-location-debugger-link.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for opening console call stacktrace links in Debugger</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>Web Console test for opening console call stacktrace links in Debugger.</p> + <script type="text/javascript"> + "use strict"; + + function foo() { + bar(); + } + + function bar() { + console.log(new Error("myErrorObject")); + console.trace(); + } + + foo(); + </script> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-storageaccess-errors.html b/devtools/client/webconsole/test/browser/test-storageaccess-errors.html new file mode 100644 index 0000000000..2866991886 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-storageaccess-errors.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf8"> + <title>requestStorageAccess test</title> + <!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ + --> + </head> + <body> + <iframe id="ifr1"></iframe> + <iframe id="ifr2" sandbox="allow-scripts"></iframe> + <iframe id="ifr3" sandbox="allow-same-origin allow-scripts"></iframe> + <iframe id="ifr4"></iframe> + <script> + 'use strict'; + if (window.location.host == "itisatracker.org") { + document.requestStorageAccess(); + } else { + document.getElementById("ifr1").setAttribute("src", "https://itisatracker.org/browser/devtools/client/webconsole/test/browser/test-nested-iframe-storageaccess-errors.html"); + document.getElementById("ifr2").setAttribute("src", "https://itisatracker.org/browser/devtools/client/webconsole/test/browser/test-storageaccess-errors.html"); + document.getElementById("ifr3").setAttribute("src", "https://itisatracker.org/browser/devtools/client/webconsole/test/browser/test-storageaccess-errors.html"); + document.getElementById("ifr4").setAttribute("src", "https://itisatracker.org/browser/devtools/client/webconsole/test/browser/test-storageaccess-errors.html"); + } + </script> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-subresource-security-error.html b/devtools/client/webconsole/test/browser/test-subresource-security-error.html new file mode 100644 index 0000000000..6c33ebec41 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-subresource-security-error.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="UTF-8"> + <title>Bug 1092055 - Log console messages for non-top-level security errors</title> + <script src="test-subresource-security-error.js"></script> + <!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ + --> +</head> +<body> +Bug 1092055 - Log console messages for non-top-level security errors +</body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-subresource-security-error.js b/devtools/client/webconsole/test/browser/test-subresource-security-error.js new file mode 100644 index 0000000000..c7d5cec144 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-subresource-security-error.js @@ -0,0 +1,2 @@ +// It doesn't matter what this script does, but the broken HSTS header sent +// with it should result in warnings in the webconsole diff --git a/devtools/client/webconsole/test/browser/test-subresource-security-error.js^headers^ b/devtools/client/webconsole/test/browser/test-subresource-security-error.js^headers^ new file mode 100644 index 0000000000..f99377fc62 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-subresource-security-error.js^headers^ @@ -0,0 +1 @@ +Strict-Transport-Security: some complete nonsense diff --git a/devtools/client/webconsole/test/browser/test-syntaxerror-worklet.js b/devtools/client/webconsole/test/browser/test-syntaxerror-worklet.js new file mode 100644 index 0000000000..8b2ac63000 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-syntaxerror-worklet.js @@ -0,0 +1,6 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function f(a, a) {} diff --git a/devtools/client/webconsole/test/browser/test-time-methods.html b/devtools/client/webconsole/test/browser/test-time-methods.html new file mode 100644 index 0000000000..942065de57 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-time-methods.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <head> + <meta charset="utf-8"> + <title>Test for bug 658368: Expand console object with time and timeEnd + methods</title> + </head> + <body> + <h1>Test for bug 658368: Expand console object with time and timeEnd + methods</h1> + + <script type="text/javascript"> + /* eslint-disable */ + function foo() { + console.timeEnd("aTimer"); + } + console.time("aTimer"); + foo(); + console.time("bTimer"); + </script> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-trackingprotection-securityerrors-thirdpartyonly.html b/devtools/client/webconsole/test/browser/test-trackingprotection-securityerrors-thirdpartyonly.html new file mode 100644 index 0000000000..6cdcb575ef --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-trackingprotection-securityerrors-thirdpartyonly.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf8"> + </head> + <body> + <iframe src="https://example.org/browser/devtools/client/webconsole/test/browser/cookieSetter.html"></iframe> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-trackingprotection-securityerrors.html b/devtools/client/webconsole/test/browser/test-trackingprotection-securityerrors.html new file mode 100644 index 0000000000..fd96024ba8 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-trackingprotection-securityerrors.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf8"> + </head> + <body> + <iframe src="https://tracking.example.org/browser/devtools/client/webconsole/test/browser/cookieSetter.html"></iframe> + <iframe src="https://tracking.example.com/"></iframe> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-warning-groups.html b/devtools/client/webconsole/test/browser/test-warning-groups.html new file mode 100644 index 0000000000..64d2519ab5 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-warning-groups.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Warning groups webconsole test page</title> + </head> + <body> + <p>Warning groups webconsole test page</p> + <script> + "use strict"; + + /* exported loadImage, loadIframe, createCookie */ + function loadImage(src) { + const img = document.createElement("img"); + img.src = src; + img.alt = (new URL(src)).search; + img.title = src; + document.body.appendChild(img); + } + + function loadIframe(src) { + const iframe = document.createElement("iframe"); + iframe.src = src; + document.body.appendChild(iframe); + } + + function createCookie(cookie) { + document.cookie = cookie; + } + </script> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-websocket.html b/devtools/client/webconsole/test/browser/test-websocket.html new file mode 100644 index 0000000000..2b2a9ae63c --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-websocket.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for Web Socket errors</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>Web Console test for Web Socket errors.</p> + <iframe srcdoc="hello world!"></iframe> + <script type="text/javascript" src="test-websocket.js"></script> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/test-websocket.js b/devtools/client/webconsole/test/browser/test-websocket.js new file mode 100644 index 0000000000..975c0d9c66 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-websocket.js @@ -0,0 +1,18 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +window.addEventListener("load", function() { + const ws1 = new WebSocket("ws://0.0.0.0:81"); + ws1.onopen = function() { + ws1.send("test 1"); + ws1.close(); + }; + + const ws2 = new window.frames[0].WebSocket("ws://0.0.0.0:82"); + ws2.onopen = function() { + ws2.send("test 2"); + ws2.close(); + }; +}); diff --git a/devtools/client/webconsole/test/browser/test-worker-promise-error.html b/devtools/client/webconsole/test/browser/test-worker-promise-error.html new file mode 100644 index 0000000000..a89c89b07b --- /dev/null +++ b/devtools/client/webconsole/test/browser/test-worker-promise-error.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<html> +<head> + <title>Worker/Worklet promise error</title> +</head> +<body> +<script> +"use strict"; + +// Promise rejection in Worker (via async function) +const workerScript = ` +self.onmessage = async () => { + throw "worker-error" +} +`; +const workerScriptUrl = URL.createObjectURL(new Blob([workerScript])); +const worker = new Worker(workerScriptUrl); +worker.postMessage({}); + +// Promise rejection in Worklet (via async function) +const workletScript = ` +async function throw_process() { + throw "worklet-error"; +} + +class ErrorProcessor extends AudioWorkletProcessor { + process() { + throw_process(); + } +} +registerProcessor("error", ErrorProcessor); +`; +const workletScriptUrl = URL.createObjectURL(new Blob([workletScript])); +const context = new AudioContext(); +context.audioWorklet.addModule(workletScriptUrl).then(() => { + const workletNode = new AudioWorkletNode(context, "error"); + const oscillator = new OscillatorNode(context); + oscillator.connect(workletNode); + oscillator.start(); + oscillator.stop(); +}); +</script> +</body> +</html> diff --git a/devtools/client/webconsole/test/browser/test_console_csp_ignore_reflected_xss_message.html b/devtools/client/webconsole/test/browser/test_console_csp_ignore_reflected_xss_message.html new file mode 100644 index 0000000000..bf63601bf3 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test_console_csp_ignore_reflected_xss_message.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="UTF-8"> + <title>Bug 1045902 - CSP: Log console message for ‘reflected-xss’</title> +</head> +<body> +Bug 1045902 - CSP: Log console message for ‘reflected-xss’ +</body> +</html> diff --git a/devtools/client/webconsole/test/browser/test_console_csp_ignore_reflected_xss_message.html^headers^ b/devtools/client/webconsole/test/browser/test_console_csp_ignore_reflected_xss_message.html^headers^ new file mode 100644 index 0000000000..0b234f0e89 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test_console_csp_ignore_reflected_xss_message.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: reflected-xss filter; diff --git a/devtools/client/webconsole/test/browser/test_hsts-invalid-headers.sjs b/devtools/client/webconsole/test/browser/test_hsts-invalid-headers.sjs new file mode 100644 index 0000000000..9e3ea76240 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test_hsts-invalid-headers.sjs @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function handleRequest(request, response) +{ + response.setHeader("Content-Type", "text/plain; charset=utf-8", false); + + let issue; + switch (request.queryString) { + case "badSyntax": + response.setHeader("Strict-Transport-Security", "\""); + issue = "is not syntactically correct."; + break; + case "noMaxAge": + response.setHeader("Strict-Transport-Security", "max-age444"); + issue = "does not include a max-age directive."; + break; + case "invalidIncludeSubDomains": + response.setHeader("Strict-Transport-Security", "includeSubDomains=abc"); + issue = "includes an invalid includeSubDomains directive."; + break; + case "invalidMaxAge": + response.setHeader("Strict-Transport-Security", "max-age=abc"); + issue = "includes an invalid max-age directive."; + break; + case "multipleIncludeSubDomains": + response.setHeader("Strict-Transport-Security", + "includeSubDomains; includeSubDomains"); + issue = "includes multiple includeSubDomains directives."; + break; + case "multipleMaxAge": + response.setHeader("Strict-Transport-Security", + "max-age=444; max-age=999"); + issue = "includes multiple max-age directives."; + break; + } + + response.write("This page is served with a STS header that " + issue); +} diff --git a/devtools/client/webconsole/test/browser/test_jsterm_screenshot_command.html b/devtools/client/webconsole/test/browser/test_jsterm_screenshot_command.html new file mode 100644 index 0000000000..d869661887 --- /dev/null +++ b/devtools/client/webconsole/test/browser/test_jsterm_screenshot_command.html @@ -0,0 +1,18 @@ +<html> + <head> + <style> + img { + height: 100px; + width: 100px; + } + .overflow { + overflow: scroll; + height: 200%; + width: 200%; + } + </style> + </head> + <body> + <img id="testImage" ></img> + </body> +</html> diff --git a/devtools/client/webconsole/test/browser/testscript.js b/devtools/client/webconsole/test/browser/testscript.js new file mode 100644 index 0000000000..849b03d86e --- /dev/null +++ b/devtools/client/webconsole/test/browser/testscript.js @@ -0,0 +1,2 @@ +"use strict"; +console.log("running network console logging tests"); |