summaryrefslogtreecommitdiffstats
path: root/devtools/client/webconsole/test/browser
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /devtools/client/webconsole/test/browser
parentInitial commit. (diff)
downloadfirefox-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')
-rw-r--r--devtools/client/webconsole/test/browser/.eslintrc.js14
-rw-r--r--devtools/client/webconsole/test/browser/_browser_console.ini49
-rw-r--r--devtools/client/webconsole/test/browser/_jsterm.ini152
-rw-r--r--devtools/client/webconsole/test/browser/_webconsole.ini402
-rw-r--r--devtools/client/webconsole/test/browser/browser_console.js134
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_chrome_context_message.js49
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_clear_cache.js53
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_clear_closed_tab.js41
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_clear_method.js46
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_consolejsm_output.js136
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_content_getters.js585
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_content_longstring.js51
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_content_object.js78
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_content_object_context_menu.js71
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_content_object_in_sidebar.js144
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_context_menu_entries.js140
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_dead_objects.js41
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_devtools_loader_exception.js93
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_eager_eval.js45
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_error_source_click.js57
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_filters.js45
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_jsterm_await.js43
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_nsiconsolemessage.js83
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_open_or_focus.js52
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_restore.js42
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_webconsole_console_api_calls.js150
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_webconsole_ctrlw_close_tab.js64
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_webconsole_iframe_messages.js62
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_webconsole_private_browsing.js139
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_add_edited_input_to_history.js79
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete-properties-with-non-alphanumeric-names.js48
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_accept_no_scroll.js54
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_array_no_index.js38
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_arrow_keys.js237
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_await.js36
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_cached_results.js144
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_commands.js61
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_control_space.js58
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_crossdomain_iframe.js46
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_del_key.js40
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_disabled.js65
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_eager_evaluation.js37
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_escape_key.js50
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_expression_variables.js48
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_extraneous_closing_brackets.js20
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_getters_cache.js133
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_getters_cancel.js90
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_getters_confirm.js148
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_getters_learn_more_link.js62
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_helpers.js33
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_in_chrome_tab.js23
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_in_debugger_stackframe.js111
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_inside_text.js171
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_native_getters.js53
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_nav_and_tab_key.js134
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_null.js71
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_paste_undo.js66
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_race_on_enter.js169
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_return_key.js83
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_return_key_no_selection.js66
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_toggle.js77
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_width.js89
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_will_navigate.js48
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_await.js67
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_await_assignments.js85
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_await_concurrent.js48
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_await_concurrent_same_result.js39
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_await_dynamic_import.js38
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_await_error.js198
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_await_helper_dollar_underscore.js83
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_await_paused.js80
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_block_command.js93
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_completion.js104
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_completion_bracket.js254
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_completion_bracket_cached_results.js130
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_completion_case_sensitivity.js113
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_completion_dollar_underscore.js57
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_completion_dollar_zero.js48
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_completion_perfect_match.js40
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_content_defined_helpers.js59
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_context_menu_labels.js37
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_copy_command.js56
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_ctrl_a_select_all.js51
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_ctrl_key_nav.js335
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_document_no_xray.js19
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_eager_evaluation.js351
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_eager_evaluation_element_highlight.js75
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_eager_evaluation_in_debugger_stackframe.js51
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_eager_evaluation_on_webextension_target.js83
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_eager_evaluation_warnings.js26
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_editor.js49
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_editor_code_folding.js73
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_editor_disabled_history_nav_with_keyboard.js97
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_editor_enter.js84
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_editor_execute.js21
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_editor_execute_selection.js65
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_editor_gutter.js45
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_editor_onboarding.js50
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_editor_resize.js79
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_editor_reverse_search_button.js53
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_editor_reverse_search_keyboard_navigation.js123
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_editor_toggle_keyboard_shortcut.js62
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_editor_toolbar.js180
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_error_docs.js50
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_error_outside_valid_range.js27
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector.js253
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector_inspector.js187
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector_pause_in_debugger.js122
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector_targets_update.js151
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_file_load_save_keyboard_shortcut.js98
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_focus_reload.js29
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_helper_clear.js23
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_helper_dollar.js45
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_helper_dollar_dollar.js60
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_helper_dollar_x.js152
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_helper_help.js39
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_helper_keys_values.js35
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_hide_when_devtools_chrome_enabled_false.js196
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_history.js55
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_history_arrow_keys.js174
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_history_nav.js60
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_history_persist.js170
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_insert_tab_when_overflows_no_scroll.js41
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_inspect.js81
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_inspect_panels.js92
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_instance_of.js35
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_middle_click_paste.js39
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_multiline.js65
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_no_input_and_tab_key_pressed.js74
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_null_undefined.js22
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_popup_close_on_tab_switch.js27
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_clipboard.js175
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_file.js39
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_user.js63
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_warnings.js91
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_selfxss.js61
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_syntax_highlight_output.js26
-rw-r--r--devtools/client/webconsole/test/browser/browser_toolbox_console_new_process.js57
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_allow_mixedcontent_securityerrors.js65
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_async_stack.js90
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_batching.js55
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_block_mixedcontent_securityerrors.js105
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_cached_messages.js185
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_cached_messages_cross_domain_iframe.js28
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_cached_messages_no_duplicate.js37
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_certificate_messages.js74
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_checkloaduri_errors.js29
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_clear_cache.js80
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_click_function_to_mapped_source.js54
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_click_function_to_prettyprinted_source.js59
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_click_function_to_source.js46
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_clickable_urls.js101
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_close_groups_after_navigation.js29
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_close_sidebar.js96
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_close_unfocused_window.js45
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_closing_after_completion.js40
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_console_api_iframe.js23
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_console_dir.js134
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_console_dir_uninspectable.js48
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_console_error_expand_object.js29
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_console_group.js153
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_console_group_open_no_scroll.js63
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_console_logging_workers_api.js86
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_console_table.js510
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_console_table_post_alterations.js74
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_console_timeStamp.js24
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_console_trace_distinct.js72
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_console_trace_duplicates.js101
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_context_menu_copy_entire_message.js229
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_context_menu_copy_link_location.js99
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_context_menu_copy_message_with_async_stacktrace.js91
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_context_menu_copy_message_with_framework_stacktrace.js129
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_context_menu_copy_object.js145
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_context_menu_export_console_output.js193
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_context_menu_object_in_sidebar.js160
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_context_menu_open_url.js108
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_context_menu_reveal_in_inspector.js117
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_context_menu_store_as_global.js117
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_cors_errors.js236
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_csp_ignore_reflected_xss_message.js30
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_csp_violation.js116
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_cspro.js56
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_css_error_impacted_elements.js126
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_document_focus.js99
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_duplicate_errors.js34
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_error_with_grouped_stack.js43
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_error_with_longstring_stack.js40
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_error_with_unicode.js24
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_error_with_url.js57
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_errors_after_page_reload.js40
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_eval_error.js102
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_eval_in_debugger_stackframe.js104
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_eval_in_debugger_stackframe2.js73
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_eval_sources.js63
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_execution_scope.js33
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_external_script_errors.js31
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_file_uri.js73
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_filter_buttons_overflow.js80
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_filter_by_input.js294
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_filter_by_regex_input.js84
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_filter_groups.js262
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_filter_navigation_marker.js77
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_filter_scroll.js95
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_filters.js84
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_filters_persist.js68
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_highlighter_console_helper.js62
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_hsts_invalid-headers.js104
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_iframe_wrong_hud.js46
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_in_line_layout.js127
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_ineffective_iframe_sandbox_warning.js57
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_init.js39
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_input_field_focus_on_panel_select.js33
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_input_focus.js90
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_insecure_passwords_about_blank_web_console_warning.js25
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_insecure_passwords_web_console_warning.js59
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_inspect_cross_domain_object.js128
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_keyboard_accessibility.js101
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_limit_multiline.js75
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_location_debugger_link.js43
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_location_logpoint_debugger_link.js185
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_location_styleeditor_link.js106
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_logErrorInPage.js19
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_logWarningInPage.js19
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_loglimit.js47
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_longstring.js42
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_longstring_getter.js44
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_message_categories.js149
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_mime_css_blocked.js16
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_multiple_windows_and_tabs.js91
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_navigate_to_parse_error.js26
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_network_attach.js71
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_network_exceptions.js28
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_network_message_close_on_escape.js57
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_network_message_ctrl_click.js67
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_network_messages_expand.js356
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_network_messages_openinnet.js112
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_network_messages_resend_request.js44
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_network_messages_stacktrace_console_initiated_request.js65
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_network_messages_status_code.js113
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_network_requests_from_chrome.js56
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_network_reset_filter.js72
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_nodes_highlight.js81
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_nodes_select.js66
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_non_javascript_mime_warning.js24
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_non_javascript_mime_worker_error.js34
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_object_ctrl_click.js122
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_object_in_sidebar_keyboard_nav.js106
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_object_inspector.js156
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_object_inspector__proto__.js40
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_entries.js354
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_getters.js662
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_getters_prototype.js121
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_getters_shadowed.js80
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_key_sorting.js136
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_local_session_storage.js115
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_nested_promise.js85
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_nested_proxy.js54
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_scroll.js58
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_selected_text.js26
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_symbols.js72
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_while_debugging_and_inspecting.js70
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_observer_notifications.js46
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_optimized_out_vars.js54
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_output_copy.js39
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_output_copy_newlines.js42
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_output_order.js50
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_output_trimmed.js109
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_persist.js87
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_promise_rejected_object.js127
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_reopen_closed_tab.js52
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_repeat_different_objects.js29
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_requestStorageAccess_errors.js66
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_responsive_design_mode.js59
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_reverse_search.js177
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_reverse_search_initial_value.js98
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_reverse_search_keyboard_navigation.js144
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_reverse_search_mouse_navigation.js139
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_reverse_search_toggle.js55
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_same_origin_errors.js29
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_sandbox_update_after_navigation.js65
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_script_errordoc_urls.js71
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_scroll.js307
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_select_all.js78
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_show_subresource_security_errors.js23
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_shows_reqs_from_netmonitor.js89
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_shows_reqs_in_netmonitor.js67
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_sidebar_object_expand_when_message_pruned.js83
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_sidebar_scroll.js54
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_sourcemap_css.js46
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_sourcemap_error.js24
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_sourcemap_invalid.js32
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_sourcemap_nosource.js54
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_split.js362
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_split_close_button.js61
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_split_escape_key.js56
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_split_focus.js46
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_split_persist.js147
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_stacktrace_location_debugger_link.js64
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_stacktrace_mapped_location_debugger_link.js61
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_strict_mode_errors.js45
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_string.js72
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_stubs_console_api.js334
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_stubs_css_message.js123
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_stubs_evaluation_result.js120
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_stubs_network_event.js216
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_stubs_page_error.js197
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_stubs_platform_messages.js117
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_telemetry_execute_js.js95
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_telemetry_filters_changed.js86
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_telemetry_js_errors.js58
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_telemetry_jump_to_definition.js50
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_telemetry_object_expanded.js70
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_telemetry_persist_toggle_changed.js66
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_telemetry_reverse_search.js171
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_time_methods.js85
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_timestamps.js51
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_trackingprotection_errors.js266
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_uncaught_exception.js88
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_view_source.js37
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_visibility_messages.js135
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_warn_about_replaced_api.js58
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_warning_group_content_blocking.js245
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_warning_group_cookies.js155
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_warning_group_multiples.js313
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_warning_group_storage_isolation.js102
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_warning_groups.js276
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_warning_groups_filtering.js327
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_warning_groups_outside_console_group.js212
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_warning_groups_toggle.js278
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_wasm_errors.js51
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_websocket.js24
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_worker_error.js22
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_worker_evaluate.js28
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_worker_promise_error.js31
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_worklet_error.js29
-rw-r--r--devtools/client/webconsole/test/browser/code_bundle_invalidmap.js93
-rw-r--r--devtools/client/webconsole/test/browser/code_bundle_invalidmap.js.map1
-rw-r--r--devtools/client/webconsole/test/browser/code_bundle_nosource.js93
-rw-r--r--devtools/client/webconsole/test/browser/code_bundle_nosource.js.map1
-rw-r--r--devtools/client/webconsole/test/browser/code_nosource.js18
-rw-r--r--devtools/client/webconsole/test/browser/cookieSetter.html7
-rw-r--r--devtools/client/webconsole/test/browser/head.js1900
-rw-r--r--devtools/client/webconsole/test/browser/sjs_cors-test-server.sjs166
-rw-r--r--devtools/client/webconsole/test/browser/sjs_slow-response-test-server.sjs34
-rw-r--r--devtools/client/webconsole/test/browser/source-mapped.css6
-rw-r--r--devtools/client/webconsole/test/browser/source-mapped.css.map7
-rw-r--r--devtools/client/webconsole/test/browser/source-mapped.scss7
-rw-r--r--devtools/client/webconsole/test/browser/stub-generator-helpers.js548
-rw-r--r--devtools/client/webconsole/test/browser/test-autocomplete-in-stackframe.html49
-rw-r--r--devtools/client/webconsole/test/browser/test-batching.html29
-rw-r--r--devtools/client/webconsole/test/browser/test-block-action-style.css3
-rw-r--r--devtools/client/webconsole/test/browser/test-block-action.html11
-rw-r--r--devtools/client/webconsole/test/browser/test-bug_923281_console_log_filter.html12
-rw-r--r--devtools/client/webconsole/test/browser/test-bug_923281_test1.js7
-rw-r--r--devtools/client/webconsole/test/browser/test-bug_923281_test2.js6
-rw-r--r--devtools/client/webconsole/test/browser/test-certificate-messages.html23
-rw-r--r--devtools/client/webconsole/test/browser/test-checkloaduri-failure.html23
-rw-r--r--devtools/client/webconsole/test/browser/test-click-function-to-mapped-source.html11
-rw-r--r--devtools/client/webconsole/test/browser/test-click-function-to-prettyprinted-source.html11
-rw-r--r--devtools/client/webconsole/test/browser/test-click-function-to-source.html11
-rw-r--r--devtools/client/webconsole/test/browser/test-click-function-to-source.js12
-rw-r--r--devtools/client/webconsole/test/browser/test-click-function-to-source.min.js3
-rw-r--r--devtools/client/webconsole/test/browser/test-click-function-to-source.min.js.map1
-rw-r--r--devtools/client/webconsole/test/browser/test-click-function-to-source.unmapped.min.js2
-rw-r--r--devtools/client/webconsole/test/browser/test-closure-optimized-out.html34
-rw-r--r--devtools/client/webconsole/test/browser/test-console-api-iframe.html22
-rw-r--r--devtools/client/webconsole/test/browser/test-console-api.html10
-rw-r--r--devtools/client/webconsole/test/browser/test-console-evaluation-context-selector-child.html23
-rw-r--r--devtools/client/webconsole/test/browser/test-console-evaluation-context-selector.html14
-rw-r--r--devtools/client/webconsole/test/browser/test-console-filter-by-regex-input.html20
-rw-r--r--devtools/client/webconsole/test/browser/test-console-filter-groups.html49
-rw-r--r--devtools/client/webconsole/test/browser/test-console-filters.html24
-rw-r--r--devtools/client/webconsole/test/browser/test-console-group.html29
-rw-r--r--devtools/client/webconsole/test/browser/test-console-iframes.html16
-rw-r--r--devtools/client/webconsole/test/browser/test-console-stacktrace-mapped.html11
-rw-r--r--devtools/client/webconsole/test/browser/test-console-table.html22
-rw-r--r--devtools/client/webconsole/test/browser/test-console-trace-duplicates.html30
-rw-r--r--devtools/client/webconsole/test/browser/test-console-workers.html28
-rw-r--r--devtools/client/webconsole/test/browser/test-console.html25
-rw-r--r--devtools/client/webconsole/test/browser/test-csp-violation-base-uri.html18
-rw-r--r--devtools/client/webconsole/test/browser/test-csp-violation-base-uri.html^headers^1
-rw-r--r--devtools/client/webconsole/test/browser/test-csp-violation-form-action.html16
-rw-r--r--devtools/client/webconsole/test/browser/test-csp-violation-form-action.html^headers^1
-rw-r--r--devtools/client/webconsole/test/browser/test-csp-violation-frame-ancestor-child.html9
-rw-r--r--devtools/client/webconsole/test/browser/test-csp-violation-frame-ancestor-child.html^headers^1
-rw-r--r--devtools/client/webconsole/test/browser/test-csp-violation-frame-ancestor-parent.html21
-rw-r--r--devtools/client/webconsole/test/browser/test-csp-violation-frame-ancestor-parent.html^headers^1
-rw-r--r--devtools/client/webconsole/test/browser/test-csp-violation-inline.html21
-rw-r--r--devtools/client/webconsole/test/browser/test-csp-violation-inline.html^headers^1
-rw-r--r--devtools/client/webconsole/test/browser/test-csp-violation.html15
-rw-r--r--devtools/client/webconsole/test/browser/test-cspro.html20
-rw-r--r--devtools/client/webconsole/test/browser/test-cspro.html^headers^2
-rw-r--r--devtools/client/webconsole/test/browser/test-css-message.html10
-rw-r--r--devtools/client/webconsole/test/browser/test-cu-reporterror.js7
-rw-r--r--devtools/client/webconsole/test/browser/test-data.json1
-rw-r--r--devtools/client/webconsole/test/browser/test-data.json^headers^1
-rw-r--r--devtools/client/webconsole/test/browser/test-duplicate-error.html21
-rw-r--r--devtools/client/webconsole/test/browser/test-dynamic-import.html10
-rw-r--r--devtools/client/webconsole/test/browser/test-dynamic-import.js12
-rw-r--r--devtools/client/webconsole/test/browser/test-error-worker.html7
-rw-r--r--devtools/client/webconsole/test/browser/test-error-worker.js20
-rw-r--r--devtools/client/webconsole/test/browser/test-error-worker2.js7
-rw-r--r--devtools/client/webconsole/test/browser/test-error-worklet.html24
-rw-r--r--devtools/client/webconsole/test/browser/test-error-worklet.js21
-rw-r--r--devtools/client/webconsole/test/browser/test-error.html20
-rw-r--r--devtools/client/webconsole/test/browser/test-eval-error.html16
-rw-r--r--devtools/client/webconsole/test/browser/test-eval-in-stackframe.html40
-rw-r--r--devtools/client/webconsole/test/browser/test-eval-sources.html17
-rw-r--r--devtools/client/webconsole/test/browser/test-evaluate-worker.html9
-rw-r--r--devtools/client/webconsole/test/browser/test-evaluate-worker.js8
-rw-r--r--devtools/client/webconsole/test/browser/test-external-script-errors.html24
-rw-r--r--devtools/client/webconsole/test/browser/test-external-script-errors.js9
-rw-r--r--devtools/client/webconsole/test/browser/test-iframe-child.html12
-rw-r--r--devtools/client/webconsole/test/browser/test-iframe-insecure-form-action.html15
-rw-r--r--devtools/client/webconsole/test/browser/test-iframe-parent.html24
-rw-r--r--devtools/client/webconsole/test/browser/test-iframe-wrong-hud-iframe.html13
-rw-r--r--devtools/client/webconsole/test/browser/test-iframe-wrong-hud.html13
-rw-r--r--devtools/client/webconsole/test/browser/test-iframe1.html13
-rw-r--r--devtools/client/webconsole/test/browser/test-iframe2.html14
-rw-r--r--devtools/client/webconsole/test/browser/test-iframe3.html14
-rw-r--r--devtools/client/webconsole/test/browser/test-image.pngbin0 -> 580 bytes
-rw-r--r--devtools/client/webconsole/test/browser/test-image.png^headers^1
-rw-r--r--devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning-inner.html13
-rw-r--r--devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning-nested1.html13
-rw-r--r--devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning-nested2.html13
-rw-r--r--devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning0.html13
-rw-r--r--devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning1.html13
-rw-r--r--devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning2.html13
-rw-r--r--devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning3.html13
-rw-r--r--devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning4.html13
-rw-r--r--devtools/client/webconsole/test/browser/test-ineffective-iframe-sandbox-warning5.html13
-rw-r--r--devtools/client/webconsole/test/browser/test-insecure-frame.html14
-rw-r--r--devtools/client/webconsole/test/browser/test-insecure-passwords-about-blank-web-console-warning.html29
-rw-r--r--devtools/client/webconsole/test/browser/test-insecure-passwords-web-console-warning.html15
-rw-r--r--devtools/client/webconsole/test/browser/test-inspect-cross-domain-objects-frame.html23
-rw-r--r--devtools/client/webconsole/test/browser/test-inspect-cross-domain-objects-top.html14
-rw-r--r--devtools/client/webconsole/test/browser/test-local-session-storage.html28
-rw-r--r--devtools/client/webconsole/test/browser/test-location-debugger-link-console-log.js10
-rw-r--r--devtools/client/webconsole/test/browser/test-location-debugger-link-errors.js8
-rw-r--r--devtools/client/webconsole/test/browser/test-location-debugger-link-logpoint-1.js13
-rw-r--r--devtools/client/webconsole/test/browser/test-location-debugger-link-logpoint-2.js13
-rw-r--r--devtools/client/webconsole/test/browser/test-location-debugger-link-logpoint.html24
-rw-r--r--devtools/client/webconsole/test/browser/test-location-debugger-link.html14
-rw-r--r--devtools/client/webconsole/test/browser/test-location-styleeditor-link-1.css9
-rw-r--r--devtools/client/webconsole/test/browser/test-location-styleeditor-link-2.css9
-rw-r--r--devtools/client/webconsole/test/browser/test-location-styleeditor-link-minified.css5
-rw-r--r--devtools/client/webconsole/test/browser/test-location-styleeditor-link.html15
-rw-r--r--devtools/client/webconsole/test/browser/test-mangled-function.js2
-rw-r--r--devtools/client/webconsole/test/browser/test-mangled-function.js.map1
-rw-r--r--devtools/client/webconsole/test/browser/test-mangled-function.src.js5
-rw-r--r--devtools/client/webconsole/test/browser/test-message-categories-canvas-css.html17
-rw-r--r--devtools/client/webconsole/test/browser/test-message-categories-canvas-css.js10
-rw-r--r--devtools/client/webconsole/test/browser/test-message-categories-css-loader.css9
-rw-r--r--devtools/client/webconsole/test/browser/test-message-categories-css-loader.css^headers^1
-rw-r--r--devtools/client/webconsole/test/browser/test-message-categories-css-loader.html13
-rw-r--r--devtools/client/webconsole/test/browser/test-message-categories-css-parser.css10
-rw-r--r--devtools/client/webconsole/test/browser/test-message-categories-css-parser.html14
-rw-r--r--devtools/client/webconsole/test/browser/test-message-categories-empty-getelementbyid.html16
-rw-r--r--devtools/client/webconsole/test/browser/test-message-categories-empty-getelementbyid.js8
-rw-r--r--devtools/client/webconsole/test/browser/test-message-categories-html.html15
-rw-r--r--devtools/client/webconsole/test/browser/test-message-categories-image.html13
-rw-r--r--devtools/client/webconsole/test/browser/test-message-categories-image.jpgbin0 -> 2532 bytes
-rw-r--r--devtools/client/webconsole/test/browser/test-message-categories-imagemap.html16
-rw-r--r--devtools/client/webconsole/test/browser/test-message-categories-malformedxml-external.html20
-rw-r--r--devtools/client/webconsole/test/browser/test-message-categories-malformedxml-external.xml8
-rw-r--r--devtools/client/webconsole/test/browser/test-message-categories-malformedxml.xhtml10
-rw-r--r--devtools/client/webconsole/test/browser/test-message-categories-svg.xhtml16
-rw-r--r--devtools/client/webconsole/test/browser/test-message-categories-workers.html18
-rw-r--r--devtools/client/webconsole/test/browser/test-message-categories-workers.js11
-rw-r--r--devtools/client/webconsole/test/browser/test-mixedcontent-securityerrors.html21
-rw-r--r--devtools/client/webconsole/test/browser/test-navigate-to-parse-error.html13
-rw-r--r--devtools/client/webconsole/test/browser/test-nested-iframe-storageaccess-errors.html14
-rw-r--r--devtools/client/webconsole/test/browser/test-network-event.html13
-rw-r--r--devtools/client/webconsole/test/browser/test-network-exceptions.html25
-rw-r--r--devtools/client/webconsole/test/browser/test-network-request.html50
-rw-r--r--devtools/client/webconsole/test/browser/test-network.html11
-rw-r--r--devtools/client/webconsole/test/browser/test-non-javascript-mime-worker.html25
-rw-r--r--devtools/client/webconsole/test/browser/test-non-javascript-mime.html13
-rw-r--r--devtools/client/webconsole/test/browser/test-non-javascript-mime.js1
-rw-r--r--devtools/client/webconsole/test/browser/test-non-javascript-mime.js^headers^1
-rw-r--r--devtools/client/webconsole/test/browser/test-reopen-closed-tab.html19
-rw-r--r--devtools/client/webconsole/test/browser/test-same-origin-required-load.html26
-rw-r--r--devtools/client/webconsole/test/browser/test-simple-function.html10
-rw-r--r--devtools/client/webconsole/test/browser/test-simple-function.js11
-rw-r--r--devtools/client/webconsole/test/browser/test-sourcemap-error-01.html13
-rw-r--r--devtools/client/webconsole/test/browser/test-sourcemap-error-01.js7
-rw-r--r--devtools/client/webconsole/test/browser/test-sourcemap-error-02.html13
-rw-r--r--devtools/client/webconsole/test/browser/test-sourcemap-error-02.js7
-rw-r--r--devtools/client/webconsole/test/browser/test-sourcemap-original.js18
-rw-r--r--devtools/client/webconsole/test/browser/test-sourcemap.min.js3
-rw-r--r--devtools/client/webconsole/test/browser/test-sourcemap.min.js.map1
-rw-r--r--devtools/client/webconsole/test/browser/test-stacktrace-location-debugger-link.html26
-rw-r--r--devtools/client/webconsole/test/browser/test-storageaccess-errors.html28
-rw-r--r--devtools/client/webconsole/test/browser/test-subresource-security-error.html15
-rw-r--r--devtools/client/webconsole/test/browser/test-subresource-security-error.js2
-rw-r--r--devtools/client/webconsole/test/browser/test-subresource-security-error.js^headers^1
-rw-r--r--devtools/client/webconsole/test/browser/test-syntaxerror-worklet.js6
-rw-r--r--devtools/client/webconsole/test/browser/test-time-methods.html24
-rw-r--r--devtools/client/webconsole/test/browser/test-trackingprotection-securityerrors-thirdpartyonly.html12
-rw-r--r--devtools/client/webconsole/test/browser/test-trackingprotection-securityerrors.html13
-rw-r--r--devtools/client/webconsole/test/browser/test-warning-groups.html32
-rw-r--r--devtools/client/webconsole/test/browser/test-websocket.html14
-rw-r--r--devtools/client/webconsole/test/browser/test-websocket.js18
-rw-r--r--devtools/client/webconsole/test/browser/test-worker-promise-error.html44
-rw-r--r--devtools/client/webconsole/test/browser/test_console_csp_ignore_reflected_xss_message.html10
-rw-r--r--devtools/client/webconsole/test/browser/test_console_csp_ignore_reflected_xss_message.html^headers^1
-rw-r--r--devtools/client/webconsole/test/browser/test_hsts-invalid-headers.sjs39
-rw-r--r--devtools/client/webconsole/test/browser/test_jsterm_screenshot_command.html18
-rw-r--r--devtools/client/webconsole/test/browser/testscript.js2
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
new file mode 100644
index 0000000000..769c636340
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-image.png
Binary files differ
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
new file mode 100644
index 0000000000..947e5f11ba
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-message-categories-image.jpg
Binary files differ
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");