summaryrefslogtreecommitdiffstats
path: root/devtools/client/webconsole/test/browser
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/webconsole/test/browser')
-rw-r--r--devtools/client/webconsole/test/browser/_browser_console.ini69
-rw-r--r--devtools/client/webconsole/test/browser/_jsterm.ini162
-rw-r--r--devtools/client/webconsole/test/browser/_webconsole.ini480
-rw-r--r--devtools/client/webconsole/test/browser/browser_console.js315
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_and_breakpoints.js16
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_clear_cache.js48
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_clear_closed_tab.js39
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_clear_method.js33
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_consolejsm_output.js140
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_content_getters.js629
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_content_longstring.js56
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_content_object.js85
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_content_object_context_menu.js73
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_content_object_in_sidebar.js162
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_context_menu_entries.js151
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_context_menu_export_console_output.js53
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_dead_objects.js45
-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.js49
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_enable_network_monitoring.js116
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_error_source_click.js57
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_evaluation_context_selector.js203
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_filters.js46
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_ignore_debugger_statement.js34
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_jsterm_await.js40
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_many_toggles.js54
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_microtask.js67
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_modes.js248
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_nsiconsolemessage.js86
-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_screenshot.js107
-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.js61
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_webconsole_private_browsing.js181
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_webextension.js131
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_window_object_inheritance.js64
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_add_edited_input_to_history.js84
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete-properties-with-non-alphanumeric-names.js46
-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.js39
-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.js148
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_commands.js67
-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.js44
-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.js51
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_extraneous_closing_brackets.js21
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_getters_cache.js130
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_getters_cancel.js89
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_getters_confirm.js147
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_getters_learn_more_link.js56
-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.js133
-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_mapped_variables.js129
-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.js67
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_race_on_enter.js170
-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.js83
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_await_assignments.js77
-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.js43
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_await_error.js215
-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.js86
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_block_command.js103
-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.js47
-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.js58
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_context_menu_labels.js36
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_copy_command.js70
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_ctrl_a_select_all.js52
-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.js18
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_eager_evaluation.js371
-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.js50
-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.js74
-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.js69
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_editor_gutter.js46
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_editor_onboarding.js51
-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.js181
-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.js26
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector.js279
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector_iframe_picker.js124
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector_inspector.js180
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector_pause_in_debugger.js124
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector_targets_update.js242
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_file_load_save_keyboard_shortcut.js87
-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.js24
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_helper_dollar.js49
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_helper_dollar_dollar.js59
-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.js41
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_helper_keys_values.js28
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_hide_when_devtools_chrome_enabled_false.js161
-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_command.js43
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_history_nav.js58
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_history_persist.js169
-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.js80
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_inspect_panels.js101
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_instance_of.js33
-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.js18
-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.js182
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_file.js128
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_fixed_header.js68
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_selector.js142
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_user.js70
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_warnings.js98
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_selfxss.js63
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_syntax_highlight_output.js27
-rw-r--r--devtools/client/webconsole/test/browser/browser_toolbox_console_new_process.js69
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_allow_mixedcontent_securityerrors.js64
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_async_stack.js90
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_batching.js62
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_bidi_string_isolation.js96
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_block_mixedcontent_securityerrors.js106
-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_duplicate_after_target_switching.js46
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_cached_messages_no_duplicate.js39
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_certificate_messages.js42
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_checkloaduri_errors.js37
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_clear_cache.js86
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_click_function_to_mapped_source.js58
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_click_function_to_prettyprinted_source.js63
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_click_function_to_source.js50
-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.js33
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_close_sidebar.js100
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_close_unfocused_window.js40
-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.js135
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_console_dir_uninspectable.js53
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_console_error_expand_object.js33
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_console_group.js162
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_console_group_open_no_scroll.js67
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_console_logging_workers_api.js130
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_console_profile_unavailable.js26
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_console_table.js502
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_console_table_fallback.js40
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_console_table_post_alterations.js68
-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.js244
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_context_menu_copy_link_location.js101
-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.js132
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_context_menu_copy_object.js181
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_context_menu_export_console_output.js197
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_context_menu_object_in_sidebar.js182
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_context_menu_open_url.js110
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_context_menu_reveal_in_inspector.js143
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_context_menu_store_as_global.js121
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_cors_errors.js260
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_csp_ignore_reflected_xss_message.js34
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_csp_violation.js141
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_cspro.js57
-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_custom_formatters.js193
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_custom_formatters_errors.js199
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_deprecation_warning.js57
-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_enable_network_monitoring.js21
-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.js25
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_error_with_url.js62
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_errors_after_page_reload.js46
-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.js131
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_eval_in_debugger_stackframe2.js76
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_eval_sources.js74
-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.js77
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_filter_buttons_overflow.js83
-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.js106
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_filter_groups.js272
-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.js108
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_filters.js85
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_filters_persist.js72
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_highlighter_console_helper.js64
-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.js40
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_in_line_layout.js135
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_ineffective_iframe_sandbox_warning.js53
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_init.js37
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_input_field_focus_on_panel_select.js34
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_input_focus.js92
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_insecure_passwords_about_blank_web_console_warning.js22
-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.js130
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_keyboard_accessibility.js119
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_lenient_this_warning.js68
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_limit_multiline.js77
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_location_debugger_link.js51
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_location_logpoint_debugger_link.js187
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_location_styleeditor_link.js82
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_logErrorInPage.js20
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_logWarningInPage.js20
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_logging_exceptions.js38
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_loglimit.js86
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_logs_exceptions_order.js41
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_longstring.js49
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_longstring_getter.js48
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_message_categories.js167
-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.js92
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_navigate_to_parse_error.js30
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_network_attach.js72
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_network_exceptions.js29
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_network_message_close_on_escape.js56
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_network_message_ctrl_click.js68
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_network_messages_after_target_switching.js49
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_network_messages_expand.js261
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_network_messages_expand_before_updates.js316
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_network_messages_html_preview.js192
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_network_messages_openinnet.js111
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_network_messages_resend_request.js48
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_network_messages_stacktrace_console_initiated_request.js66
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_network_messages_status_code.js72
-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.js81
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_network_unicode.js35
-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.js20
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_non_javascript_mime_worker_error.js26
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_non_standard_doctype_errors.js126
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_object_ctrl_click.js125
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_object_in_sidebar_keyboard_nav.js105
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_object_inspector.js157
-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_array_getters.js94
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_entries.js722
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_getters.js664
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_getters_prototype.js119
-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.js117
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_nested_promise.js90
-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_private_properties.js306
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_scroll.js59
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_selected_text.js27
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_symbols.js74
-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.js40
-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.js49
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_output_trimmed.js109
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_persist.js290
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_promise_rejected_object.js126
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_record_tuple.js33
-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.js42
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_requestStorageAccess_errors.js132
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_responsive_design_mode.js43
-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.js37
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_sandbox_update_after_navigation.js59
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_script_errordoc_urls.js75
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_scroll.js407
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_select_all.js61
-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.js83
-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.js85
-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.js26
-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.js60
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_split.js365
-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.js143
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_stacktrace_location_debugger_link.js63
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_stacktrace_mapped_location_debugger_link.js65
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_strict_mode_errors.js48
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_string.js70
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_stubs_console_api.js343
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_stubs_css_message.js130
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_stubs_evaluation_result.js185
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_stubs_network_event.js233
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_stubs_page_error.js254
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_stubs_platform_messages.js107
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_telemetry_execute_js.js94
-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.js62
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_telemetry_jump_to_definition.js52
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_telemetry_object_expanded.js72
-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.js87
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_timestamps.js55
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_trackingprotection_errors.js268
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_uncaught_exception.js130
-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.js137
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_warn_about_replaced_api.js59
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_warning_group_content_blocking.js256
-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_csp.js29
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_warning_group_multiples.js326
-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.js286
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_warning_groups_filtering.js337
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_warning_groups_outside_console_group.js220
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_warning_groups_toggle.js284
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_wasm_errors.js51
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_webextension_promise_rejection.js37
-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.js30
-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.js1912
-rw-r--r--devtools/client/webconsole/test/browser/shared-head.js514
-rw-r--r--devtools/client/webconsole/test/browser/sjs_cors-test-server.sjs165
-rw-r--r--devtools/client/webconsole/test/browser/sjs_slow-response-test-server.sjs35
-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.js437
-rw-r--r--devtools/client/webconsole/test/browser/test-autocomplete-in-stackframe.html59
-rw-r--r--devtools/client/webconsole/test/browser/test-autocomplete-mapped.html15
-rw-r--r--devtools/client/webconsole/test/browser/test-autocomplete-mapped.js18
-rw-r--r--devtools/client/webconsole/test/browser/test-autocomplete-mapped.js.map1
-rw-r--r--devtools/client/webconsole/test/browser/test-autocomplete-mapped.src.js16
-rw-r--r--devtools/client/webconsole/test/browser/test-batching.html29
-rw-r--r--devtools/client/webconsole/test/browser/test-blank.html10
-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-custom-formatters-errors.html185
-rw-r--r--devtools/client/webconsole/test/browser/test-console-custom-formatters.html112
-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.html16
-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-logs-exceptions-order.html32
-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.html36
-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-event-handler.html8
-rw-r--r--devtools/client/webconsole/test/browser/test-csp-violation-event-handler.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-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.mjs12
-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.mjs21
-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.html53
-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.js7
-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.js8
-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.js6
-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-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-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-group-csp.html11
-rw-r--r--devtools/client/webconsole/test/browser/test-warning-group-csp.html^headers^1
-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-worker.js13
-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.sjs43
-rw-r--r--devtools/client/webconsole/test/browser/test_jsterm_screenshot_command.html87
-rw-r--r--devtools/client/webconsole/test/browser/testscript.js2
553 files changed, 43108 insertions, 0 deletions
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..4a459428f9
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/_browser_console.ini
@@ -0,0 +1,69 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+ shared-head.js
+ test-console-iframes.html
+ test-console.html
+ test-iframe1.html
+ test-iframe2.html
+ test-iframe3.html
+ test-image.png
+ test-image.png^headers^
+ test-worker.js
+ !/devtools/client/shared/test/shared-head.js
+ !/devtools/client/debugger/test/mochitest/shared-head.js
+ !/devtools/client/framework/browser-toolbox/test/helpers-browser-toolbox.js
+ !/devtools/client/shared/test/telemetry-test-helpers.js
+
+[browser_console.js]
+skip-if = http3 # Bug 1829298
+[browser_console_and_breakpoints.js]
+[browser_console_clear_cache.js]
+skip-if = tsan # Bug 1479876
+[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_context_menu_export_console_output.js]
+[browser_console_dead_objects.js]
+[browser_console_devtools_loader_exception.js]
+[browser_console_eager_eval.js]
+[browser_console_enable_network_monitoring.js]
+skip-if =
+ verify
+ http3 # Bug 1829298
+[browser_console_error_source_click.js]
+[browser_console_evaluation_context_selector.js]
+[browser_console_filters.js]
+[browser_console_ignore_debugger_statement.js]
+[browser_console_jsterm_await.js]
+[browser_console_many_toggles.js]
+skip-if = verify
+[browser_console_modes.js]
+[browser_console_nsiconsolemessage.js]
+[browser_console_open_or_focus.js]
+[browser_console_restore.js]
+skip-if = verify
+[browser_console_screenshot.js]
+[browser_console_webconsole_ctrlw_close_tab.js]
+[browser_console_webconsole_iframe_messages.js]
+[browser_console_webconsole_private_browsing.js]
+skip-if =
+ os == "mac" # Bug 1689000
+ os == "linux" && debug # Bug 1689000
+[browser_console_webextension.js]
+[browser_console_window_object_inheritance.js]
+[browser_toolbox_console_new_process.js]
+skip-if =
+ asan || debug || ccov # Bug 1591590
+ os == 'win' && bits == 64 && !debug # Bug 1591590
+[browser_console_microtask.js]
diff --git a/devtools/client/webconsole/test/browser/_jsterm.ini b/devtools/client/webconsole/test/browser/_jsterm.ini
new file mode 100644
index 0000000000..f82381cde2
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/_jsterm.ini
@@ -0,0 +1,162 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+ test-autocomplete-in-stackframe.html
+ test-autocomplete-mapped.html
+ test-autocomplete-mapped.js
+ test-autocomplete-mapped.js.map
+ test-autocomplete-mapped.src.js
+ 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.mjs
+ 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/shared-head.js
+ !/devtools/client/framework/browser-toolbox/test/helpers-browser-toolbox.js
+ !/devtools/client/shared/test/telemetry-test-helpers.js
+ !/devtools/client/shared/test/highlighter-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_mapped_variables.js]
+skip-if = http3 # Bug 1829298
+[browser_jsterm_autocomplete_native_getters.js]
+[browser_jsterm_autocomplete_nav_and_tab_key.js]
+[browser_jsterm_autocomplete_null.js]
+skip-if = tsan # bug 1778033
+[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_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_iframe_picker.js]
+[browser_jsterm_evaluation_context_selector_pause_in_debugger.js]
+[browser_jsterm_evaluation_context_selector_targets_update.js]
+skip-if = http3 # Bug 1829298
+[browser_jsterm_evaluation_context_selector_inspector.js]
+[browser_jsterm_evaluation_context_selector.js]
+[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_command.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_fixed_header.js]
+[browser_jsterm_screenshot_command_selector.js]
+[browser_jsterm_screenshot_command_warnings.js]
+skip-if =
+ os == "win" && os_version == "6.1" # Getting the clipboard image dimensions throws an exception
+ os == 'linux' && bits == 64 && !debug # Bug 1701439
+[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..79d460bc55
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/_webconsole.ini
@@ -0,0 +1,480 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+skip-if = asan # Frequent failures when opening tabs due to OOM issues, bug 1760260
+support-files =
+ code_bundle_invalidmap.js
+ code_bundle_invalidmap.js.map
+ code_bundle_nosource.js
+ code_bundle_nosource.js.map
+ cookieSetter.html
+ head.js
+ shared-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-blank.html
+ test-console-trace-duplicates.html
+ test-console-api-iframe.html
+ test-console-api.html
+ test-console-custom-formatters.html
+ test-console-custom-formatters-errors.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-event-handler.html
+ test-csp-violation-event-handler.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-logs-exceptions-order.html
+ test-console-stacktrace-mapped.html
+ test-console-table.html
+ test-console-workers.html
+ test-console.html
+ test-data.json
+ test-data.json^headers^
+ test-duplicate-error.html
+ test-dynamic-import.html
+ test-dynamic-import.mjs
+ test-error.html
+ test-error-worker.html
+ test-error-worker.js
+ test-error-worker2.js
+ test-error-worklet.html
+ test-error-worklet.mjs
+ 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-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-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-warning-group-csp.html
+ test-warning-group-csp.html^headers^
+ 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/shared-head.js
+ !/devtools/client/framework/browser-toolbox/test/helpers-browser-toolbox.js
+ !/devtools/client/shared/test/telemetry-test-helpers.js
+ !/devtools/client/shared/test/highlighter-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
+skip-if = http3 # Bug 1829298
+[browser_webconsole_async_stack.js]
+[browser_webconsole_batching.js]
+[browser_webconsole_bidi_string_isolation.js]
+[browser_webconsole_block_mixedcontent_securityerrors.js]
+tags = mcb
+skip-if = http3 # Bug 1829298
+[browser_webconsole_cached_messages_cross_domain_iframe.js]
+[browser_webconsole_cached_messages_duplicate_after_target_switching.js]
+https_first_disabled = true
+skip-if = http3 # Bug 1829298
+[browser_webconsole_cached_messages_no_duplicate.js]
+[browser_webconsole_cached_messages.js]
+[browser_webconsole_certificate_messages.js]
+skip-if = http3 # Bug 1829298
+[browser_webconsole_checkloaduri_errors.js]
+[browser_webconsole_clear_cache.js]
+[browser_webconsole_click_function_to_source.js]
+https_first_disabled = true
+skip-if = http3 # Bug 1829298
+[browser_webconsole_click_function_to_mapped_source.js]
+https_first_disabled = true
+skip-if = http3 # Bug 1829298
+[browser_webconsole_click_function_to_prettyprinted_source.js]
+https_first_disabled = true
+skip-if = http3 # Bug 1829298
+[browser_webconsole_clickable_urls.js]
+https_first_disabled = true
+skip-if = http3 # Bug 1829298
+[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]
+skip-if = tsan # Bug 1767724
+[browser_webconsole_console_profile_unavailable.js]
+[browser_webconsole_console_table_post_alterations.js]
+[browser_webconsole_console_table.js]
+https_first_disabled = true
+skip-if = http3 # Bug 1829298
+[browser_webconsole_console_timeStamp.js]
+[browser_webconsole_console_trace_distinct.js]
+[browser_webconsole_console_trace_duplicates.js]
+https_first_disabled = true
+skip-if = http3 # Bug 1829298
+[browser_webconsole_context_menu_export_console_output.js]
+[browser_webconsole_context_menu_copy_entire_message.js]
+[browser_webconsole_context_menu_copy_link_location.js]
+https_first_disabled = true
+skip-if =
+ os == "linux" # bug 1473120
+ http3 # Bug 1829298
+[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]
+skip-if =
+ apple_catalina # Bug 1713158
+[browser_webconsole_context_menu_object_in_sidebar.js]
+[browser_webconsole_context_menu_open_url.js]
+https_first_disabled = true
+skip-if = http3 # Bug 1829298
+[browser_webconsole_context_menu_store_as_global.js]
+skip-if =
+ apple_catalina # Bug 1713158
+[browser_webconsole_context_menu_reveal_in_inspector.js]
+[browser_webconsole_cors_errors.js]
+https_first_disabled = true
+skip-if = http3 # Bug 1829298
+[browser_webconsole_csp_ignore_reflected_xss_message.js]
+https_first_disabled = true
+skip-if = http3 # Bug 1829298
+[browser_webconsole_csp_violation.js]
+[browser_webconsole_cspro.js]
+https_first_disabled = true
+skip-if = http3 # Bug 1829298
+[browser_webconsole_css_error_impacted_elements.js]
+[browser_webconsole_custom_formatters.js]
+[browser_webconsole_custom_formatters_errors.js]
+[browser_webconsole_deprecation_warning.js]
+[browser_webconsole_document_focus.js]
+[browser_webconsole_duplicate_errors.js]
+[browser_webconsole_enable_network_monitoring.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" #Bug 1598205
+ !debug && 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]
+https_first_disabled = true
+skip-if = http3 # Bug 1829298
+[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]
+https_first_disabled = true
+skip-if = http3 # Bug 1829298
+[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]
+skip-if = http3 # Bug 1829298
+[browser_webconsole_insecure_passwords_web_console_warning.js]
+skip-if = http3 # Bug 1829298
+[browser_webconsole_inspect_cross_domain_object.js]
+[browser_webconsole_keyboard_accessibility.js]
+[browser_webconsole_lenient_this_warning.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_logging_exceptions.js]
+[browser_webconsole_loglimit.js]
+[browser_webconsole_logs_exceptions_order.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]
+skip-if =
+ win11_2009 # Bug 1798331
+[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]
+[browser_webconsole_network_messages_after_target_switching.js]
+[browser_webconsole_network_messages_expand_before_updates.js]
+skip-if =
+ os == "win" # Bug 1689101
+ os == "linux" # Bug 1689101
+[browser_webconsole_network_messages_expand.js]
+[browser_webconsole_network_messages_html_preview.js]
+[browser_webconsole_network_messages_openinnet.js]
+[browser_webconsole_network_messages_resend_request.js]
+[browser_webconsole_network_messages_stacktrace_console_initiated_request.js]
+[browser_webconsole_network_messages_status_code.js]
+[browser_webconsole_network_requests_from_chrome.js]
+skip-if = http3 # Bug 1829298
+[browser_webconsole_network_reset_filter.js]
+[browser_webconsole_network_unicode.js]
+[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_non_standard_doctype_errors.js]
+[browser_webconsole_object_ctrl_click.js]
+skip-if =
+ apple_catalina # Bug 1713158
+[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_array_getters.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_private_properties.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]
+skip-if = http3 # Bug 1829298
+[browser_webconsole_promise_rejected_object.js]
+[browser_webconsole_record_tuple.js]
+[browser_webconsole_reopen_closed_tab.js]
+[browser_webconsole_repeat_different_objects.js]
+[browser_webconsole_requestStorageAccess_errors.js]
+skip-if =
+ win10_2004 # Bug 1723573
+ http3 # Bug 1829298
+[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]
+[browser_webconsole_shows_reqs_in_netmonitor.js]
+[browser_webconsole_sidebar_object_expand_when_message_pruned.js]
+[browser_webconsole_sidebar_scroll.js]
+[browser_webconsole_sourcemap_css.js]
+skip-if = http3 # Bug 1829298
+[browser_webconsole_sourcemap_error.js]
+[browser_webconsole_sourcemap_invalid.js]
+[browser_webconsole_sourcemap_nosource.js]
+skip-if =
+ verify
+ http3 # Bug 1829298
+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]
+skip-if =
+ win10_2004 # Bug 1723573
+ win11_2009 # Bug 1798331
+ http3 # Bug 1829298
+[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]
+tags = trackingprotection
+skip-if = http3 # Bug 1829298
+[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 = http3 # Bug 1829298
+[browser_webconsole_warning_group_storage_isolation.js]
+skip-if = true # Bug 1765369
+[browser_webconsole_warning_group_cookies.js]
+skip-if = http3 # Bug 1829298
+[browser_webconsole_warning_group_csp.js]
+[browser_webconsole_warning_groups_filtering.js]
+[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_webextension_promise_rejection.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]
+[browser_webconsole_console_table_fallback.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..556d0c101a
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console.js
@@ -0,0 +1,315 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+const TEST_URI =
+ "http://example.com/browser/devtools/client/webconsole/" +
+ "test/browser/test-console.html?" +
+ Date.now();
+
+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() call in `testMessages`.
+ await pushPref("security.allow_parent_unrestricted_js_loads", true);
+ await pushPref("devtools.browserconsole.enableNetworkMonitoring", true);
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+
+ // Open a parent process tab to check it doesn't have impact
+ const aboutRobotsTab = await addTab("about:robots");
+ // And open the "actual" test tab
+ const tab = await addTab(TEST_URI);
+
+ await testMessages();
+
+ info("Close tab");
+ await removeTab(tab);
+ await removeTab(aboutRobotsTab);
+});
+
+async function testMessages() {
+ const opened = waitForBrowserConsole();
+ let hud = BrowserConsoleManager.getBrowserConsole();
+ ok(!hud, "browser console is not open");
+
+ // The test harness does override the global's console property to replace it with
+ // a Console.sys.mjs instance (https://searchfox.org/mozilla-central/rev/c5c002f81f08a73e04868e0c2bf0eb113f200b03/testing/mochitest/api.js#75-78)
+ // So here we reset the console property with the native console (which is luckily
+ // stored in `nativeConsole`).
+ const overriddenConsole = globalThis.console;
+ globalThis.console = globalThis.nativeConsole;
+
+ 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");
+
+ info("Check that we don't display the non-native console API warning");
+ // Wait a bit to let room for the message to be displayed
+ await wait(1000);
+ is(
+ await findMessageVirtualizedByType({
+ hud,
+ text: "The Web Console logging API",
+ typeSelector: ".warn",
+ }),
+ undefined,
+ "The message about disabled console API is not displayed"
+ );
+ // Set the overidden console back.
+ globalThis.console = overriddenConsole;
+
+ await clearOutput(hud);
+
+ await setFilterState(hud, {
+ netxhr: true,
+ css: true,
+ });
+
+ executeSoon(() => {
+ expectUncaughtException();
+ // eslint-disable-next-line no-undef
+ foobarException();
+ });
+
+ // Add a message from a chrome window.
+ hud.iframeWindow.console.log("message from chrome window");
+
+ // Spawn worker from a chrome window and log a message and an error
+ const workerCode = `console.log("message in parent worker");
+ throw new Error("error in parent worker");`;
+ const blob = new hud.iframeWindow.Blob([workerCode], {
+ type: "application/javascript",
+ });
+ const chromeSpawnedWorker = new hud.iframeWindow.Worker(
+ URL.createObjectURL(blob)
+ );
+
+ // Spawn Chrome worker from a chrome window and log a message
+ // It's important to use the browser console global so the message gets assigned
+ // a non-numeric innerID in Console.cpp
+ const browserConsoleGlobal = Cu.getGlobalForObject(hud);
+ const chromeWorker = new browserConsoleGlobal.ChromeWorker(
+ URL.createObjectURL(
+ new browserConsoleGlobal.Blob(
+ [`console.log("message in chrome worker")`],
+ {
+ type: "application/javascript",
+ }
+ )
+ )
+ );
+
+ const sandbox = new Cu.Sandbox(null, {
+ wantComponents: false,
+ wantGlobalProperties: ["URL", "URLSearchParams"],
+ });
+ const error = Cu.evalInSandbox(
+ `new Error("error from nuked globals");`,
+ sandbox
+ );
+ console.error(error);
+ Cu.nukeSandbox(sandbox);
+
+ const componentsException = new Components.Exception("Components.Exception");
+ console.error(componentsException);
+
+ // Check privileged error message from a content process
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ (async function () {
+ throw new Error("privileged content process error message");
+ })();
+ });
+
+ // Add a message from a content window.
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.wrappedJSObject.console.log("message from content window");
+ content.wrappedJSObject.throwError("error from content window");
+
+ content.testWorker = new content.Worker("./test-worker.js");
+ content.testWorker.postMessage({
+ type: "log",
+ message: "message in content worker",
+ });
+ content.testWorker.postMessage({
+ type: "error",
+ message: "error in content worker",
+ });
+ });
+
+ // Test eval.
+ execute(hud, "`Parent Process Location: ${document.location.href}`");
+
+ // Test eval frame script
+ gBrowser.selectedBrowser.messageManager.loadFrameScript(
+ `data:application/javascript,console.log("framescript-message")`,
+ false
+ );
+
+ // 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");
+
+ // Check messages logged with Services.console.logMessage
+ const scriptErrorMessage = Cc["@mozilla.org/scripterror;1"].createInstance(
+ Ci.nsIScriptError
+ );
+ scriptErrorMessage.initWithWindowID(
+ "Error from Services.console.logMessage",
+ gBrowser.currentURI.prePath,
+ null,
+ 0,
+ 0,
+ Ci.nsIScriptError.warningFlag,
+ // platform-specific category to test case for Bug 1770160
+ "chrome javascript",
+ gBrowser.selectedBrowser.innerWindowID
+ );
+ Services.console.logMessage(scriptErrorMessage);
+
+ // Check messages logged in content with Log.sys.mjs
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ const { Log } = ChromeUtils.importESModule(
+ "resource://gre/modules/Log.sys.mjs"
+ );
+ const logger = Log.repository.getLogger("TEST_LOGGER_" + Date.now());
+ logger.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));
+ logger.level = Log.Level.Info;
+ logger.info("Log.sys.mjs content process messsage");
+ });
+
+ // Check CSS warnings in parent process
+ await execute(hud, `document.body.style.backgroundColor = "rainbow"`);
+
+ // Wait enough so any duplicated message would have the time to be rendered
+ await wait(1000);
+
+ await checkUniqueMessageExists(
+ hud,
+ "message from chrome window",
+ ".console-api"
+ );
+ await checkUniqueMessageExists(hud, "error from nuked globals", ".error");
+ await checkUniqueMessageExists(
+ hud,
+ "privileged content process error message",
+ ".error"
+ );
+ await checkUniqueMessageExists(
+ hud,
+ "message from content window",
+ ".console-api"
+ );
+ await checkUniqueMessageExists(hud, "error from content window", ".error");
+ await checkUniqueMessageExists(
+ hud,
+ `"Parent Process Location: chrome://browser/content/browser.xhtml"`,
+ ".result"
+ );
+ await checkUniqueMessageExists(hud, "framescript-message", ".console-api");
+ await checkUniqueMessageExists(
+ hud,
+ "Error from Services.console.logMessage",
+ ".warn"
+ );
+ await checkUniqueMessageExists(hud, "foobarException", ".error");
+ await checkUniqueMessageExists(hud, "test-console.html", ".network");
+ await checkUniqueMessageExists(hud, "404.html", ".network");
+ await checkUniqueMessageExists(hud, "test-image.png", ".network");
+ await checkUniqueMessageExists(
+ hud,
+ "Log.sys.mjs content process messsage",
+ ".console-api"
+ );
+ await checkUniqueMessageExists(
+ hud,
+ "message in content worker",
+ ".console-api"
+ );
+ await checkUniqueMessageExists(hud, "error in content worker", ".error");
+ await checkUniqueMessageExists(
+ hud,
+ "message in parent worker",
+ ".console-api"
+ );
+ await checkUniqueMessageExists(hud, "error in parent worker", ".error");
+ await checkUniqueMessageExists(
+ hud,
+ "message in chrome worker",
+ ".console-api"
+ );
+ await checkUniqueMessageExists(
+ hud,
+ "Expected color but found ‘rainbow’",
+ ".warn"
+ );
+ await checkUniqueMessageExists(
+ hud,
+ "Expected color but found ‘bled’",
+ ".warn"
+ );
+
+ await checkComponentExceptionMessage(hud, componentsException);
+
+ await resetFilters(hud);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.testWorker.terminate();
+ delete content.testWorker;
+ });
+ chromeSpawnedWorker.terminate();
+ chromeWorker.terminate();
+ info("Close the Browser Console");
+ await safeCloseBrowserConsole();
+}
+
+async function checkComponentExceptionMessage(hud, exception) {
+ const msgNode = await checkUniqueMessageExists(
+ hud,
+ "Components.Exception",
+ ".error"
+ );
+ const framesNode = await waitFor(() => msgNode.querySelector(".pane.frames"));
+ ok(framesNode, "The Components.Exception stack is displayed right away");
+
+ const frameNodes = framesNode.querySelectorAll(".frame");
+ ok(frameNodes.length > 1, "Got at least one frame in the stack");
+ is(
+ frameNodes[0].querySelector(".line").textContent,
+ String(exception.lineNumber),
+ "The stack displayed by default refers to Components.Exception passed as argument"
+ );
+
+ const [, line] = msgNode
+ .querySelector(".frame-link-line")
+ .textContent.split(":");
+ is(
+ line,
+ String(exception.lineNumber + 1),
+ "The link on the top right refers to the console.error callsite"
+ );
+}
diff --git a/devtools/client/webconsole/test/browser/browser_console_and_breakpoints.js b/devtools/client/webconsole/test/browser/browser_console_and_breakpoints.js
new file mode 100644
index 0000000000..6ff072863b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_and_breakpoints.js
@@ -0,0 +1,16 @@
+/* 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/. */
+
+// Verify that breakpoints don't impact the browser console
+
+"use strict";
+
+add_task(async function () {
+ await BrowserConsoleManager.toggleBrowserConsole();
+
+ // Bug 1687657, if the thread actor is attached or set up for breakpoints,
+ // the test will freeze here, by the browser console's thread actor.
+ // eslint-disable-next-line no-debugger
+ debugger;
+});
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..8fc536e67a
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_clear_cache.js
@@ -0,0 +1,48 @@
+/* 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,<!DOCTYPE html>Test browser console clear cache";
+
+add_task(async function () {
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+
+ await addTab(TEST_URI);
+ let hud = await BrowserConsoleManager.toggleBrowserConsole();
+
+ const CACHED_MESSAGE = "CACHED_MESSAGE";
+ await logTextInContentAndWaitForMessage(hud, CACHED_MESSAGE);
+
+ info("Click the clear output button");
+ const onBrowserConsoleOutputCleared = waitFor(
+ () => !findConsoleAPIMessage(hud, CACHED_MESSAGE)
+ );
+ hud.ui.window.document.querySelector(".devtools-clear-icon").click();
+ await onBrowserConsoleOutputCleared;
+ ok(true, "Message was cleared");
+
+ 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 logTextInContentAndWaitForMessage(hud, "Smoke message");
+ is(
+ findConsoleAPIMessage(hud, CACHED_MESSAGE),
+ undefined,
+ "The cached message is not visible anymore"
+ );
+});
+
+function logTextInContentAndWaitForMessage(hud, text) {
+ const onMessage = waitForMessageByType(hud, text, ".console-api");
+ 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..1473153ba6
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_clear_closed_tab.js
@@ -0,0 +1,39 @@
+/* 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.browsertoolbox.scope", "everything");
+
+ // 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(() => findConsoleAPIMessage(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(() => !findConsoleAPIMessage(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..e0601f6ec5
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_clear_method.js
@@ -0,0 +1,33 @@
+/* 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,<!DOCTYPE html><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 waitForMessageByType(hud, "msg", ".console-api");
+
+ info("Send a console.clear() from the content page");
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ content.wrappedJSObject.console.clear();
+ });
+ await waitForMessageByType(hud, "Console was cleared", ".console-api");
+
+ 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");
+});
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..e80aac6b83
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_consolejsm_output.js
@@ -0,0 +1,140 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that Console.sys.mjs 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.importESModule(
+ "resource://gre/modules/Console.sys.mjs"
+ );
+ 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.sys.mjs 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.importESModule(
+ "resource://gre/modules/Console.sys.mjs"
+ );
+ 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.importESModule(
+ "resource://gre/modules/Console.sys.mjs"
+ );
+
+ 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(() => findConsoleAPIMessage(hud, msg));
+ ok(message, `"${msg}" was logged`);
+}
+
+async function checkMessageHidden(hud, msg) {
+ info(`Checking "${msg}" was not logged`);
+ await waitFor(() => findConsoleAPIMessage(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..9c2b801461
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_content_getters.js
@@ -0,0 +1,629 @@
+/* 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,<!DOCTYPE html><h1>Object Inspector on Getters</h1>";
+const { ELLIPSIS } = require("resource://devtools/shared/l10n.js");
+
+add_task(async function () {
+ // Show the content messages
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+
+ 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) {
+ const obj = 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(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;
+ },
+ })
+ );
+ Object.defineProperty(obj, "MyPrint", { get: content.print });
+ Object.defineProperty(obj, "MyElement", { get: content.Element });
+ Object.defineProperty(obj, "MySetAttribute", {
+ get: content.Element.prototype.setAttribute,
+ });
+ Object.defineProperty(obj, "MySetClassName", {
+ get: Object.getOwnPropertyDescriptor(
+ content.Element.prototype,
+ "className"
+ ).set,
+ });
+
+ content.wrappedJSObject.console.log("oi-test", obj);
+ }
+ );
+
+ const node = await waitFor(() => findConsoleAPIMessage(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 testUnsafeGetters(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);
+ 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);
+ 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);
+ 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);
+ checkChildren(node, [`size`, `<entries>`, `<prototype>`]);
+
+ const entriesNode = findObjectInspectorNode(oi, "<entries>");
+ expandObjectInspectorNode(entriesNode);
+ await waitFor(() => !!getObjectInspectorChildrenNodes(entriesNode).length);
+ checkChildren(entriesNode, [`foo → Object { bar: "baz" }`]);
+
+ const entryNode = getObjectInspectorChildrenNodes(entriesNode)[0];
+ expandObjectInspectorNode(entryNode);
+ await waitFor(() => !!getObjectInspectorChildrenNodes(entryNode).length);
+ 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);
+ checkChildren(node, [`<target>`, `<handler>`]);
+
+ const targetNode = findObjectInspectorNode(oi, "<target>");
+ expandObjectInspectorNode(targetNode);
+ await waitFor(() => !!getObjectInspectorChildrenNodes(targetNode).length);
+ checkChildren(targetNode, [`a: 1`, `<prototype>`]);
+
+ const handlerNode = findObjectInspectorNode(oi, "<handler>");
+ expandObjectInspectorNode(handlerNode);
+ await waitFor(() => !!getObjectInspectorChildrenNodes(handlerNode).length);
+ 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);
+ 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 testUnsafeGetters(oi) {
+ const props = [
+ [
+ "MyPrint",
+ "MyPrint: TypeError: 'print' called on an object that does not implement interface Window.",
+ ],
+ ["MyElement", "MyElement: TypeError: Illegal constructor."],
+ [
+ "MySetAttribute",
+ "MySetAttribute: TypeError: 'setAttribute' called on an object that does not implement interface Element.",
+ ],
+ [
+ "MySetClassName",
+ "MySetClassName: TypeError: 'set className' called on an object that does not implement interface Element.",
+ ],
+ ];
+
+ for (const [name, text] of props) {
+ const getNode = () => findObjectInspectorNode(oi, name);
+ is(
+ isObjectInspectorNodeExpandable(getNode()),
+ false,
+ `The ${name} node can't be expanded`
+ );
+ const invokeButton = getObjectInspectorInvokeGetterButton(getNode());
+ ok(invokeButton, `There is an invoke button for ${name} as expected`);
+
+ invokeButton.click();
+ await waitFor(() => getNode().textContent.includes(text));
+ ok(true, `${name} getter shows the error message ${text}`);
+ }
+}
+
+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..99f97b38ee
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_content_longstring.js
@@ -0,0 +1,56 @@
+/* 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,<!DOCTYPE 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.browsertoolbox.scope", "everything");
+
+ await addTab(TEST_URI);
+
+ info("Open the Browser Console");
+ const hud = await BrowserConsoleManager.toggleBrowserConsole();
+
+ info("Log a longString");
+ const onMessage = waitForMessageByType(
+ hud,
+ LONGSTRING.slice(0, 50),
+ ".console-api"
+ );
+ 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(() =>
+ findConsoleAPIMessage(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(
+ () => !findConsoleAPIMessage(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..fce1e1a324
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_content_object.js
@@ -0,0 +1,85 @@
+/* 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,<!DOCTYPE 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.browsertoolbox.scope", "everything");
+
+ 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(() =>
+ findConsoleAPIMessage(
+ 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(() =>
+ findConsoleAPIMessage(
+ 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:
+ // ▼ Object { contentObject: "YAY!", deep: (1) […] }
+ // | 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('Object { contentObject: "YAY!", deep: (1) […] }')
+ );
+ ok(contentObjectProp.textContent.includes(`contentObject: "YAY!"`));
+ ok(deepProp.textContent.includes(`deep: Array [ "yes!" ]`));
+ ok(prototypeProp.textContent.includes(`<prototype>`));
+
+ // The object inspector now looks like:
+ // ▼ Object { contentObject: "YAY!", deep: (1) […] }
+ // | 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..7f2135c929
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_content_object_context_menu.js
@@ -0,0 +1,73 @@
+/* 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,<!DOCTYPE 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.browsertoolbox.scope", "everything");
+
+ 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(() =>
+ findConsoleAPIMessage(
+ 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:
+ // ▼ Object { contentObject: "YAY!", deep: (1) […] }
+ // | 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.querySelectorAll(".node")[2].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..289fc56a6f
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_content_object_in_sidebar.js
@@ -0,0 +1,162 @@
+/* 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,<!DOCTYPE html><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.browsertoolbox.scope", "everything");
+
+ await addTab(TEST_URI);
+
+ info("Open the Browser Console");
+ const hud = await BrowserConsoleManager.toggleBrowserConsole();
+
+ const message = await waitFor(() => findConsoleAPIMessage(hud, "foo"));
+ const [objectA, objectB] = message.querySelectorAll(
+ ".object-inspector .objectBox-object"
+ );
+ const number = findMessagePartByType(hud, {
+ text: "100",
+ typeSelector: ".console-api",
+ partSelector: ".objectBox",
+ });
+ const string = findMessagePartByType(hud, {
+ text: "foo",
+ typeSelector: ".console-api",
+ partSelector: ".objectBox",
+ });
+ const bool = findMessagePartByType(hud, {
+ text: "false",
+ typeSelector: ".console-api",
+ partSelector: ".objectBox",
+ });
+ const nullMessage = findMessagePartByType(hud, {
+ text: "null",
+ typeSelector: ".console-api",
+ partSelector: ".objectBox",
+ });
+ const undefinedMsg = findMessagePartByType(hud, {
+ text: "undefined",
+ typeSelector: ".console-api",
+ partSelector: ".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..1f1c011997
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_context_menu_entries.js
@@ -0,0 +1,151 @@
+/* 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.browsertoolbox.scope", "everything");
+ // 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();
+
+ // Network monitoring is turned off by default in the browser console
+ info("Turn on network monitoring");
+ await toggleNetworkMonitoringConsoleSetting(hud, true);
+
+ info("Reload the content window to produce a network log");
+ const onNetworkMessage = waitForMessageByType(
+ hud,
+ "test-console.html",
+ ".network"
+ );
+ 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-export-clipboard (M)",
+ "#console-menu-export-file (F)",
+ ]);
+ 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 = waitForMessageByType(
+ hud,
+ "simple text message",
+ ".console-api"
+ );
+ 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-export-clipboard (M)",
+ "#console-menu-export-file (F)",
+ ]);
+ 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);
+ await toggleNetworkMonitoringConsoleSetting(hud, false);
+});
+
+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_context_menu_export_console_output.js b/devtools/client/webconsole/test/browser/browser_console_context_menu_export_console_output.js
new file mode 100644
index 0000000000..bd5d2740be
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_context_menu_export_console_output.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,<!DOCTYPE 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.browsertoolbox.scope", "everything");
+
+ await addTab(TEST_URI);
+
+ info("Open the Browser Console");
+ const hud = await BrowserConsoleManager.toggleBrowserConsole();
+
+ info("Wait until the content object is displayed");
+ const message = await waitFor(() =>
+ findConsoleAPIMessage(
+ hud,
+ `Object { contentObject: "YAY!", deep: (2) […] }`
+ )
+ );
+ ok(true, "Content object is displayed in the Browser Console");
+ // Clear clipboard content.
+ SpecialPowers.clipboardCopyString("");
+
+ const menuPopup = await openContextMenu(hud, message);
+ const exportClipboard = menuPopup.querySelector(
+ "#console-menu-export-clipboard"
+ );
+ ok(exportClipboard, "copy menu item is enabled");
+
+ const clipboardText = await waitForClipboardPromise(
+ () => exportClipboard.click(),
+ data => data.includes("YAY")
+ );
+ menuPopup.hidePopup();
+
+ ok(true, "Clipboard text was found and saved");
+ // We're only checking that the export did work.
+ // browser_webconsole_context_menu_export_console_output.js covers the feature in
+ // greater detail.
+ ok(
+ clipboardText.includes(`Object { contentObject: "YAY!", deep: (2) […] }`),
+ "Message was exported to clipboard"
+ );
+});
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..69e019c062
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_dead_objects.js
@@ -0,0 +1,45 @@
+/* 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 executeAndWaitForResultMessage(hud, "nukedSandbox", "DeadObject");
+ const msg = await executeAndWaitForErrorMessage(
+ 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 executeAndWaitForResultMessage(
+ 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..6d263bee79
--- /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,<!DOCTYPE html><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);
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+
+ 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(() =>
+ findErrorMessage(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 { targetCommand } = bcHud.commands;
+ // 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 = new Promise(resolve => {
+ const onAvailable = ({ targetFront }) => {
+ if (targetFront.url.includes("view-source:")) {
+ targetCommand.unwatchTargets({
+ types: [targetCommand.TYPES.FRAME],
+ onAvailable,
+ });
+ resolve();
+ }
+ };
+ targetCommand.watchTargets({
+ types: [targetCommand.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..74e8c5661b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_eager_eval.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/. */
+
+"use strict";
+
+// Check evaluating eager-evaluation values.
+const TEST_URI = "data:text/html;charset=utf8,<!DOCTYPE html>";
+
+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 executeAndWaitForResultMessage(
+ hud,
+ `globalThis.eagerLoader = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs");`,
+ `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 executeAndWaitForResultMessage(
+ hud,
+ `delete globalThis.eagerLoader;`,
+ `true`
+ );
+}
diff --git a/devtools/client/webconsole/test/browser/browser_console_enable_network_monitoring.js b/devtools/client/webconsole/test/browser/browser_console_enable_network_monitoring.js
new file mode 100644
index 0000000000..e0477a5da7
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_enable_network_monitoring.js
@@ -0,0 +1,116 @@
+/* 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 enabling/disabling network monitoring work in the browser console.
+"use strict";
+
+const TEST_IMAGE =
+ "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-image.png";
+
+requestLongerTimeout(10);
+
+// Test that "Enable Network Monitoring" work as expected in the browser
+// console
+add_task(async function testEnableNetworkMonitoringInBrowserConsole() {
+ const hud = await BrowserConsoleManager.toggleBrowserConsole();
+ const enableNetworkMonitoringSelector =
+ ".webconsole-console-settings-menu-item-enableNetworkMonitoring";
+
+ info("Set the focus on the Browser Console");
+ hud.iframeWindow.focus();
+
+ await setFilterState(hud, {
+ netxhr: true,
+ net: true,
+ });
+
+ info("Check that the 'Enable Network Monitoring' setting is off by default");
+ await checkConsoleSettingState(hud, enableNetworkMonitoringSelector, false);
+
+ await fetch(TEST_IMAGE);
+
+ await checkNoMessageExists(hud, "test-image.png", ".message.network");
+
+ info("Turn on network monitoring");
+ await toggleNetworkMonitoringConsoleSetting(hud, true);
+
+ let onMessageLogged = waitForMessageByType(
+ hud,
+ "test-image.png?id=1",
+ ".message.network"
+ );
+ await fetch(TEST_IMAGE + "?id=1");
+ await onMessageLogged;
+
+ info("Turn off network monitoring");
+ await toggleNetworkMonitoringConsoleSetting(hud, false);
+
+ await fetch(TEST_IMAGE + "?id=2");
+
+ await checkNoMessageExists(hud, "test-image.png?id=2", ".message.network");
+
+ info("Turn on network monitoring again");
+ await toggleNetworkMonitoringConsoleSetting(hud, true);
+
+ onMessageLogged = waitForMessageByType(
+ hud,
+ "test-image.png?id=3",
+ ".message.network"
+ );
+ await fetch(TEST_IMAGE + "?id=3");
+ await onMessageLogged;
+
+ info(
+ "Test that the 'Enable Network Monitoring' setting is persisted across browser console reopens "
+ );
+
+ info("Close the browser console");
+ await safeCloseBrowserConsole({ clearOutput: true });
+ await BrowserConsoleManager.closeBrowserConsole();
+
+ info("Reopen the browser console");
+ const hud2 = await BrowserConsoleManager.toggleBrowserConsole();
+ hud2.iframeWindow.focus();
+
+ info("Check that the 'Enable Network Monitoring' setting is on");
+ await checkConsoleSettingState(hud2, enableNetworkMonitoringSelector, true);
+
+ onMessageLogged = waitForMessageByType(
+ hud2,
+ "test-image.png?id=4",
+ ".message.network"
+ );
+ await fetch(TEST_IMAGE + "?id=4");
+ await onMessageLogged;
+
+ info("Clear and close the Browser Console");
+ // Reset the network monitoring setting to off
+ await toggleNetworkMonitoringConsoleSetting(hud2, false);
+ await safeCloseBrowserConsole({ clearOutput: true });
+});
+
+/**
+ * Check that a message is not logged.
+ *
+ * @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.
+ */
+async function checkNoMessageExists(hud, msg, selector) {
+ info(`Checking that "${msg}" was not logged`);
+ let messages;
+ try {
+ messages = await waitFor(async () => {
+ const msgs = await findMessagesVirtualized({ hud, text: msg, selector });
+ return msgs.length ? msgs : null;
+ });
+ ok(!messages.length, `"${msg}" was logged once`);
+ } catch (e) {
+ ok(true, `Message "${msg}" wasn't logged\n`);
+ }
+}
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..41bab8c989
--- /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,<!DOCTYPE html><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.browsertoolbox.scope", "everything");
+ 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(
+ () => findErrorMessage(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_evaluation_context_selector.js b/devtools/client/webconsole/test/browser/browser_console_evaluation_context_selector.js
new file mode 100644
index 0000000000..98faa3a92a
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_evaluation_context_selector.js
@@ -0,0 +1,203 @@
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function () {
+ await pushPref("devtools.webconsole.input.context", true);
+ await pushPref("devtools.chrome.enabled", true);
+ await pushPref("devtools.every-frame-target.enabled", true);
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+
+ const hud = await BrowserConsoleManager.toggleBrowserConsole();
+
+ const evaluationContextSelectorButton = await waitFor(() =>
+ 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("checked"),
+ false,
+ "The checked class isn't applied"
+ );
+
+ await executeAndWaitForResultMessage(
+ hud,
+ "document.location",
+ "chrome://browser/content/browser.xhtml"
+ );
+ ok(true, "The evaluation was done in the top context");
+
+ setInputValue(hud, "document.location.host");
+ await waitForEagerEvaluationResult(hud, `"browser"`);
+
+ info("Check the context selector menu");
+ checkContextSelectorMenuItemAt(hud, 0, {
+ label: "Top",
+ tooltip: "chrome://browser/content/browser.xhtml",
+ checked: true,
+ });
+ checkContextSelectorMenuItemAt(hud, 1, {
+ separator: true,
+ });
+
+ info(
+ "Add a tab with a worker and check both the document and the worker are displayed in the context selector"
+ );
+ const documentFile = "test-evaluate-worker.html";
+ const documentWithWorkerUrl =
+ "https://example.com/browser/devtools/client/webconsole/test/browser/" +
+ documentFile;
+ const tab = await addTab(documentWithWorkerUrl);
+
+ const documentIndex = await waitFor(() => {
+ const i = getContextSelectorItems(hud).findIndex(el =>
+ el.querySelector(".label")?.innerText?.endsWith(documentFile)
+ );
+ return i == -1 ? null : i;
+ });
+
+ info("Select the document target");
+ selectTargetInContextSelector(hud, documentWithWorkerUrl);
+
+ await waitFor(() =>
+ evaluationContextSelectorButton.innerText.includes(documentFile)
+ );
+ ok(true, "The context was set to the selected document");
+ is(
+ evaluationContextSelectorButton.classList.contains("checked"),
+ true,
+ "The checked class is applied"
+ );
+
+ checkContextSelectorMenuItemAt(hud, documentIndex, {
+ label: documentWithWorkerUrl,
+ tooltip: documentWithWorkerUrl,
+ checked: true,
+ });
+
+ await waitForEagerEvaluationResult(hud, `"example.com"`);
+ ok(true, "The instant evaluation result is updated in the document context");
+
+ await executeAndWaitForResultMessage(
+ hud,
+ "document.location",
+ documentWithWorkerUrl
+ );
+ ok(true, "The evaluation is done in the document context");
+
+ info("Check that autocomplete is done in the tab document context");
+ await setInputValueForAutocompletion(hud, "p");
+ // `pauseInWorker` is defined in test-evaluate-worker.html
+ ok(
+ getAutocompletePopupLabels(hud.jsterm.autocompletePopup).includes(
+ "pauseInWorker"
+ ),
+ "autocomplete happened in the tab document context"
+ );
+
+ // set input text so we can watch for instant evaluation result update
+ setInputValue(hud, "globalThis.location.href");
+ await waitForEagerEvaluationResult(hud, `"${documentWithWorkerUrl}"`);
+
+ info("Select the worker target");
+ const workerFile = "test-evaluate-worker.js";
+ const workerUrl =
+ "https://example.com/browser/devtools/client/webconsole/test/browser/" +
+ workerFile;
+ // Wait for the worker target to be displayed
+ await waitFor(() =>
+ getContextSelectorItems(hud).find(el =>
+ el.querySelector(".label")?.innerText?.endsWith(workerFile)
+ )
+ );
+ selectTargetInContextSelector(hud, workerFile);
+
+ await waitFor(() =>
+ evaluationContextSelectorButton.innerText.includes(workerFile)
+ );
+ ok(true, "The context was set to the selected worker");
+
+ await waitForEagerEvaluationResult(hud, `"${workerUrl}"`);
+ ok(true, "The instant evaluation result is updated in the worker context");
+
+ const workerIndex = await waitFor(() => {
+ const i = getContextSelectorItems(hud).findIndex(el =>
+ el.querySelector(".label")?.innerText?.endsWith(workerFile)
+ );
+ return i == -1 ? null : i;
+ });
+ checkContextSelectorMenuItemAt(hud, workerIndex, {
+ label: workerFile,
+ tooltip: workerFile,
+ checked: true,
+ });
+
+ await executeAndWaitForResultMessage(
+ hud,
+ "globalThis.location",
+ `WorkerLocation`
+ );
+ ok(true, "The evaluation is done in the worker context");
+
+ info("Check that autocomplete is done in the worker context");
+ await setInputValueForAutocompletion(hud, "f");
+ // `foo` is defined in test-evaluate-worker.js
+ ok(
+ getAutocompletePopupLabels(hud.jsterm.autocompletePopup).includes("foo"),
+ "autocomplete happened in the worker context"
+ );
+
+ // set input text so we can watch for instant evaluation result update
+ setInputValue(hud, "document.location.host");
+ await waitForEagerEvaluationResult(
+ hud,
+ `ReferenceError: document is not defined`
+ );
+
+ info(
+ "Remove the tab and make sure both the document and worker target are removed from the context selector"
+ );
+ await removeTab(tab);
+
+ await waitFor(() => evaluationContextSelectorButton.innerText == "Top");
+ ok(true, "The context is set back to Top");
+
+ checkContextSelectorMenuItemAt(hud, 0, {
+ label: "Top",
+ tooltip: "chrome://browser/content/browser.xhtml",
+ checked: true,
+ });
+
+ is(
+ getContextSelectorItems(hud).every(el => {
+ const label = el.querySelector(".label")?.innerText;
+ return (
+ !label ||
+ (label !== "test-evaluate-worker.html" && label !== workerFile)
+ );
+ }),
+ true,
+ "the document and worker targets were removed"
+ );
+
+ await waitForEagerEvaluationResult(hud, `"browser"`);
+ ok(true, "The instant evaluation was done in the top context");
+
+ await executeAndWaitForResultMessage(
+ hud,
+ "document.location",
+ "chrome://browser/content/browser.xhtml"
+ );
+ ok(true, "The evaluation was done in the top context");
+});
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..5f79aa1acf
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_filters.js
@@ -0,0 +1,46 @@
+/* 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,<!DOCTYPE html><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_ignore_debugger_statement.js b/devtools/client/webconsole/test/browser/browser_console_ignore_debugger_statement.js
new file mode 100644
index 0000000000..7ed84f2b08
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_ignore_debugger_statement.js
@@ -0,0 +1,34 @@
+/* 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 debugger statements are ignored in the browser console.
+
+"use strict";
+
+const URI_WITH_DEBUGGER_STATEMENT = `data:text/html,<!DOCTYPE html>
+ <meta charset=utf8>
+ browser console ignore debugger statement
+ <script>
+ debugger;
+ console.log("after debugger statement");
+ </script>`;
+
+add_task(async function () {
+ // Show the content messages
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+
+ info("Open the Browser Console");
+ const hud = await BrowserConsoleManager.toggleBrowserConsole();
+
+ info("Add tab with a script containing debugger statement");
+ const onMessage = waitForMessageByType(
+ hud,
+ `after debugger statement`,
+ ".console-api"
+ );
+ await addTab(URI_WITH_DEBUGGER_STATEMENT);
+ await onMessage;
+
+ ok(true, "The debugger statement was ignored");
+});
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..501f7dcffd
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_jsterm_await.js
@@ -0,0 +1,40 @@
+/* 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,<!DOCTYPE html>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();
+
+ info("Evaluate a top-level await expression");
+ const simpleAwait = `await new Promise(r => setTimeout(() => r(["await1"]), 500))`;
+ await executeAndWaitForResultMessage(hud, 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_many_toggles.js b/devtools/client/webconsole/test/browser/browser_console_many_toggles.js
new file mode 100644
index 0000000000..21bbde1d14
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_many_toggles.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we safely close and reopen the Browser Console.
+
+"use strict";
+
+add_task(async function () {
+ // Enable the multiprocess mode as it is more likely to break on startup
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+
+ const promises = [];
+ for (let i = 0; i < 5; i++) {
+ info("Open the Browser Console");
+ promises.push(BrowserConsoleManager.toggleBrowserConsole());
+
+ // Use different pause time between opening and closing
+ await wait(i * 100);
+
+ info("Close the Browser Console");
+ promises.push(BrowserConsoleManager.closeBrowserConsole());
+
+ // Use different pause time between opening and closing
+ await wait(i * 100);
+ }
+
+ info("Wait for all opening/closing promises");
+ // Ignore any exception here, we expect some as we are racing opening versus destruction
+ await Promise.allSettled(promises);
+
+ // The browser console may end up being opened or closed because of usage of "toggle"
+ // Ensure having a console opened to verify it works
+ let hud = BrowserConsoleManager.getBrowserConsole();
+ if (!hud) {
+ info("Reopen the browser console a last time");
+ hud = await BrowserConsoleManager.toggleBrowserConsole();
+ }
+
+ info("Log a message and ensure it is visible and the console mostly works");
+ console.log("message from mochitest");
+ await checkUniqueMessageExists(hud, "message from mochitest", ".console-api");
+
+ // Clear the messages in order to be able to run this test more than once
+ // and clear the message we just logged
+ await clearOutput(hud);
+
+ info("Ensure closing the Browser Console at the end of the test");
+ await BrowserConsoleManager.closeBrowserConsole();
+
+ ok(
+ !BrowserConsoleManager.getBrowserConsole(),
+ "No browser console opened at the end of test"
+ );
+});
diff --git a/devtools/client/webconsole/test/browser/browser_console_microtask.js b/devtools/client/webconsole/test/browser/browser_console_microtask.js
new file mode 100644
index 0000000000..5f6103c678
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_microtask.js
@@ -0,0 +1,67 @@
+/* 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/. */
+
+// The console input should be evaluated with microtask level != 0.
+
+"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();
+
+ hud.iframeWindow.focus();
+ execute(
+ hud,
+ `
+{
+ let logCount = 0;
+
+ queueMicrotask(() => {
+ console.log("#" + logCount + ": in microtask");
+ logCount++;
+ });
+
+ console.log("#" + logCount + ": before createXULElement");
+ logCount++;
+
+ // This shouldn't perform microtask checkpoint.
+ document.createXULElement("browser");
+
+ console.log("#" + logCount + ": after createXULElement");
+ logCount++;
+}
+`
+ );
+
+ const beforeCreateXUL = await waitFor(() =>
+ findConsoleAPIMessage(hud, "before createXULElement")
+ );
+ const afterCreateXUL = await waitFor(() =>
+ findConsoleAPIMessage(hud, "after createXULElement")
+ );
+ const inMicroTask = await waitFor(() =>
+ findConsoleAPIMessage(hud, "in microtask")
+ );
+
+ const getMessageIndex = msg => {
+ const text = msg.textContent;
+ return text.match(/^#(\d+)/)[1];
+ };
+
+ is(
+ getMessageIndex(beforeCreateXUL),
+ "0",
+ "before createXULElement log is displayed first"
+ );
+ is(
+ getMessageIndex(afterCreateXUL),
+ "1",
+ "after createXULElement log is displayed second"
+ );
+ is(getMessageIndex(inMicroTask), "2", "in microtask log is displayed last");
+
+ ok(true, "Expected messages are displayed in the browser console");
+});
diff --git a/devtools/client/webconsole/test/browser/browser_console_modes.js b/devtools/client/webconsole/test/browser/browser_console_modes.js
new file mode 100644
index 0000000000..796d5d1aef
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_modes.js
@@ -0,0 +1,248 @@
+/* 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 messages from different contexts appears in the Browser Console and that their
+// visibility can be controlled through the UI (either with the ChromeDebugToolbar mode whe
+// Fission is enabled, or through the "Show Content Messages" setting when it's not).
+
+"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,<!DOCTYPE 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>`;
+
+// Test can be a bit long
+requestLongerTimeout(2);
+
+add_task(async function () {
+ // Show the content messages
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+ // Show context selector
+ await pushPref("devtools.chrome.enabled", true);
+ await pushPref("devtools.webconsole.input.context", true);
+
+ // Open the WebConsole on the tab to check changing mode won't focus the tab
+ await openNewTabAndConsole(TEST_URI);
+
+ // Open the Browser Console
+ const hud = await BrowserConsoleManager.toggleBrowserConsole();
+ // Set a filter to have predictable results, otherwise we may get messages from Firefox
+ // polluting the test.
+ await setFilterState(hud, { text: FILTER_PREFIX });
+
+ const chromeDebugToolbar = hud.ui.document.querySelector(
+ ".chrome-debug-toolbar"
+ );
+ ok(
+ !!chromeDebugToolbar,
+ "ChromeDebugToolbar is displayed when the Browser Console has fission support"
+ );
+ is(
+ hud.chromeWindow.document.title,
+ "Multiprocess Browser Console",
+ "Browser Console window has expected title"
+ );
+
+ const evaluationContextSelectorButton = await waitFor(() =>
+ hud.ui.outputNode.querySelector(".webconsole-evaluation-selector-button")
+ );
+
+ info("Select the content process target");
+ const pid =
+ gBrowser.selectedTab.linkedBrowser.browsingContext.currentWindowGlobal
+ .osPid;
+ getContextSelectorItems(hud)
+ .find(item =>
+ item.querySelector(".label")?.innerText?.startsWith(`(pid ${pid})`)
+ )
+ .click();
+
+ await waitFor(() =>
+ evaluationContextSelectorButton.classList.contains("checked")
+ );
+
+ // We can't directly throw in the script as it would be treated as an evaluation result
+ // and wouldn't be hidden when switching modes.
+ // Here we use an async-iife in which we throw so this will trigger the proper error
+ // reporting path.
+ await executeAndWaitForResultMessage(
+ hud,
+ `(async function(){
+ throw new Error("${FILTER_PREFIX}Content error")
+ })();
+ 21+21`,
+ 42
+ );
+
+ await waitFor(() => findErrorMessage(hud, "Content error"));
+ ok(true, "Error message from content process is displayed");
+
+ // Emit an error message from the parent process
+ executeSoon(() => {
+ expectUncaughtException();
+ throw new Error(`${FILTER_PREFIX}Parent error`);
+ });
+
+ await waitFor(() => findErrorMessage(hud, "Parent error"));
+ ok(true, "Parent process message is displayed");
+
+ const suffix = ` 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()`,
+ ];
+
+ info("wait for all the messages to be displayed");
+ await waitFor(
+ () =>
+ expectedMessages.every(
+ expectedMessage =>
+ findMessagePartsByType(hud, {
+ text: expectedMessage,
+ typeSelector: ".console-api",
+ partSelector: ".message-body",
+ }).length == 1
+ ),
+ "wait for all the messages to be displayed",
+ 100
+ );
+ ok(true, "Expected messages are displayed in the browser console");
+
+ const tableMessage = findConsoleAPIMessage(hud, "console.table()", ".table");
+
+ const table = await waitFor(() =>
+ tableMessage.querySelector(".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("Set Browser Console Mode to parent process only");
+ chromeDebugToolbar
+ .querySelector(
+ `.chrome-debug-toolbar input[name="chrome-debug-mode"][value="parent-process"]`
+ )
+ .click();
+ info("Wait for content messages to be hidden");
+ await waitFor(() => !findConsoleAPIMessage(hud, contentArgs.log));
+
+ for (const expectedMessage of expectedMessages) {
+ ok(
+ !findConsoleAPIMessage(hud, expectedMessage),
+ `"${expectedMessage}" is hidden`
+ );
+ }
+
+ is(
+ hud.chromeWindow.document.title,
+ "Parent process Browser Console",
+ "Browser Console window title was updated"
+ );
+
+ ok(hud.iframeWindow.document.hasFocus(), "Browser Console is still focused");
+
+ await waitFor(
+ () => !evaluationContextSelectorButton.classList.contains("checked")
+ );
+ ok(true, "Changing mode did reset the context selector");
+ ok(
+ findMessageByType(hud, "21+21", ".command"),
+ "The evaluation message is still displayed"
+ );
+ ok(
+ findEvaluationResultMessage(hud, `42`),
+ "The evaluation result is still displayed"
+ );
+
+ info(
+ "Check that message from parent process is still visible in the Browser Console"
+ );
+ ok(
+ !!findErrorMessage(hud, "Parent error"),
+ "Parent process message is still displayed"
+ );
+
+ info("Set Browser Console Mode to Multiprocess");
+ chromeDebugToolbar
+ .querySelector(
+ `.chrome-debug-toolbar input[name="chrome-debug-mode"][value="everything"]`
+ )
+ .click();
+
+ info("Wait for content messages to be displayed");
+ await waitFor(() =>
+ expectedMessages.every(expectedMessage =>
+ findConsoleAPIMessage(hud, expectedMessage)
+ )
+ );
+
+ for (const expectedMessage of expectedMessages) {
+ // Search into the message body as the message location could have some of the
+ // searched text.
+ is(
+ findMessagePartsByType(hud, {
+ text: expectedMessage,
+ typeSelector: ".console-api",
+ partSelector: ".message-body",
+ }).length,
+ 1,
+ `"${expectedMessage}" is visible`
+ );
+ }
+
+ is(
+ findErrorMessages(hud, "Content error").length,
+ 1,
+ "error message from content process is only displayed once"
+ );
+
+ is(
+ hud.chromeWindow.document.title,
+ "Multiprocess Browser Console",
+ "Browser Console window title was updated again"
+ );
+
+ info("Clear and close the Browser Console");
+ await safeCloseBrowserConsole({ clearOutput: true });
+});
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..a1f1b85669
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_nsiconsolemessage.js
@@ -0,0 +1,86 @@
+/* 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,<!DOCTYPE html>
+<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 = waitForMessageByType(hud, text, ".console-api");
+ 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(() =>
+ findConsoleAPIMessage(hud, "cachedBrowserConsoleMessage")
+ );
+ Services.console.logStringMessage("liveBrowserConsoleMessage2");
+ await waitFor(() => findConsoleAPIMessage(hud, "liveBrowserConsoleMessage2"));
+
+ const msg = await waitFor(() =>
+ findConsoleAPIMessage(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(
+ () =>
+ findConsoleAPIMessages(hud, "cachedBrowserConsoleMessage").length === 0
+ );
+ await waitFor(
+ () => findConsoleAPIMessages(hud, "liveBrowserConsoleMessage").length === 0
+ );
+ await waitFor(
+ () => findConsoleAPIMessages(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..679d426dc3
--- /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("resource://devtools/client/definitions.js");
+
+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(() => findConsoleAPIMessage(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..5941c744fc
--- /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_screenshot.js b/devtools/client/webconsole/test/browser/browser_console_screenshot.js
new file mode 100644
index 0000000000..b22a17e847
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_screenshot.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that :screenshot command works properly in the Browser Console
+
+"use strict";
+
+const COLOR_DIV_1 = "rgb(255, 0, 0)";
+const COLOR_DIV_2 = "rgb(0, 200, 0)";
+const COLOR_DIV_3 = "rgb(0, 0, 150)";
+const COLOR_DIV_4 = "rgb(100, 0, 100)";
+
+const TEST_URI = `data:text/html,<!DOCTYPE html><meta charset=utf8>
+ <style>
+ body {
+ margin: 0;
+ height: 100vh;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ grid-template-rows: 1fr 1fr;
+ }
+ div:nth-child(1) { background-color: ${COLOR_DIV_1}; }
+ div:nth-child(2) { background-color: ${COLOR_DIV_2}; }
+ div:nth-child(3) { background-color: ${COLOR_DIV_3}; }
+ div:nth-child(4) { background-color: ${COLOR_DIV_4}; }
+ </style>
+ <body>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ </body>`;
+
+add_task(async function () {
+ await addTab(TEST_URI);
+ const hud = await BrowserConsoleManager.toggleBrowserConsole();
+
+ info("Execute :screenshot");
+ const file = FileUtils.getFile("TmpD", ["TestScreenshotFile.png"]);
+ // 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 command = `:screenshot ${file.path} --dpr 1`;
+
+ await executeAndWaitForMessageByType(
+ hud,
+ command,
+ `Saved to ${file.path}`,
+ ".console-api"
+ );
+
+ info("Create an image using the downloaded file as source");
+ const image = new Image();
+ image.src = PathUtils.toFileURI(file.path);
+ await once(image, "load");
+
+ info(
+ "Retrieve the position of the elements relatively to the browser viewport"
+ );
+ const bodyBounds = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async () => {
+ return content.document.body.getBoxQuadsFromWindowOrigin()[0].getBounds();
+ }
+ );
+
+ const center = [
+ bodyBounds.left + bodyBounds.width / 2,
+ bodyBounds.top + bodyBounds.height / 2,
+ ];
+
+ info(
+ "Check that the divs of the content page were rendered on the screenshot"
+ );
+ checkImageColorAt({
+ image,
+ x: center[0] - 50,
+ y: center[1] - 50,
+ expectedColor: COLOR_DIV_1,
+ label: "The screenshot did render the first div of the content page",
+ });
+ checkImageColorAt({
+ image,
+ x: center[0] + 50,
+ y: center[1] - 50,
+ expectedColor: COLOR_DIV_2,
+ label: "The screenshot did render the second div of the content page",
+ });
+ checkImageColorAt({
+ image,
+ x: center[0] - 50,
+ y: center[1] + 50,
+ expectedColor: COLOR_DIV_3,
+ label: "The screenshot did render the third div of the content page",
+ });
+ checkImageColorAt({
+ image,
+ x: center[0] + 50,
+ y: center[1] + 50,
+ expectedColor: COLOR_DIV_4,
+ label: "The screenshot did render the fourth div of the content page",
+ });
+
+ info("Remove the downloaded screenshot file and cleanup downloads");
+ await IOUtils.remove(file.path);
+ await resetDownloads();
+});
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..f4fe78a073
--- /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,<!DOCTYPE html><title>bug871156</title>\n" +
+ "<p>hello world";
+ const firstTab = gBrowser.selectedTab;
+
+ let hud = await openNewTabAndConsole(TEST_URI);
+
+ const toolbox = await gDevTools.getToolboxForTab(gBrowser.selectedTab);
+
+ 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.commands.targetCommand);
+ 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..e6ed5388ce
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_webconsole_iframe_messages.js
@@ -0,0 +1,61 @@
+/* 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", ".console-api"],
+ ["blah", ".error"],
+ ["iframe 2", ".console-api"],
+ ["iframe 3", ".console-api"],
+];
+
+// This log comes from test-iframe1.html, which is included from test-console-iframes.html
+// __and__ from test-iframe3.html as well, so we should see it twice.
+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");
+
+ // Show the content messages
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+ hud = await BrowserConsoleManager.toggleBrowserConsole();
+ ok(hud, "browser console opened");
+ await testMessages(hud);
+
+ // clear the browser console.
+ await clearOutput(hud);
+ await waitForTick();
+ await safeCloseBrowserConsole();
+});
+
+async function testMessages(hud) {
+ for (const [message, selector] of expectedMessages) {
+ info(`checking that the message "${message}" exists`);
+ await waitFor(() => findMessageByType(hud, message, selector));
+ }
+
+ ok(true, "Found expected unique messages");
+
+ await waitFor(
+ () => findConsoleAPIMessages(hud, expectedDupedMessage).length == 2
+ );
+ ok(true, `${expectedDupedMessage} is present twice`);
+}
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..0fed8c03c5
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_webconsole_private_browsing.js
@@ -0,0 +1,181 @@
+/* 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,<!DOCTYPE html>Not private";
+const PRIVATE_TEST_URI = `data:text/html;charset=utf8,<!DOCTYPE html>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.browsertoolbox.scope", "everything");
+
+ const publicTab = 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 = waitForMessageByType(
+ hud,
+ PRIVATE_MESSAGE,
+ ".console-api"
+ );
+ const onErrorMessage = waitForMessageByType(hud, PRIVATE_EXCEPTION, ".error");
+ logPrivateMessages(privateBrowser.selectedBrowser);
+
+ await onLogMessage;
+ await onErrorMessage;
+ ok(true, "Messages are displayed as expected");
+
+ info("Check that commands executed in private windows aren't put in history");
+ const privateCommand = `"command in private window"`;
+ await executeAndWaitForResultMessage(hud, privateCommand, "");
+
+ const publicHud = await openConsole(publicTab);
+ const historyMessage = await executeAndWaitForMessageByType(
+ publicHud,
+ ":history",
+ "",
+ ".simpleTable"
+ );
+
+ ok(
+ Array.from(
+ historyMessage.node.querySelectorAll("tr td:last-of-type")
+ ).every(td => td.textContent !== privateCommand),
+ "command from private window wasn't added to the history"
+ );
+ await closeConsole(publicTab);
+
+ info("test cached messages");
+ await closeConsole(privateTab);
+ info("web console closed");
+ hud = await openConsole(privateTab);
+ ok(hud, "web console reopened");
+
+ await waitFor(() => findConsoleAPIMessage(hud, PRIVATE_MESSAGE));
+ await waitFor(() => findErrorMessage(hud, PRIVATE_EXCEPTION));
+ 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();
+
+ // Add a non-private message to the console.
+ const onBrowserConsoleNonPrivateMessage = waitForMessageByType(
+ hud,
+ NON_PRIVATE_MESSAGE,
+ ".console-api"
+ );
+ SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [NON_PRIVATE_MESSAGE],
+ function (msg) {
+ content.console.log(msg);
+ }
+ );
+ await onBrowserConsoleNonPrivateMessage;
+
+ info(
+ "Check that cached messages from private tabs are not displayed in the browser console"
+ );
+ // We do the check at this moment, after we received the "live" message, so the browser
+ // console would have displayed any cached messages by now.
+ assertNoPrivateMessages(hud);
+
+ const onBrowserConsolePrivateLogMessage = waitForMessageByType(
+ hud,
+ PRIVATE_MESSAGE,
+ ".console-api"
+ );
+ const onBrowserConsolePrivateErrorMessage = waitForMessageByType(
+ hud,
+ PRIVATE_EXCEPTION,
+ ".error"
+ );
+ logPrivateMessages(privateBrowser.selectedBrowser);
+
+ info("Wait for private log message");
+ await onBrowserConsolePrivateLogMessage;
+ info("Wait for private error message");
+ 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(
+ findConsoleAPIMessage(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);
+
+ info("close the browser console again");
+ await safeCloseBrowserConsole();
+});
+
+function logPrivateMessages(browser) {
+ SpecialPowers.spawn(browser, [], () => content.wrappedJSObject.logMessages());
+}
+
+function assertNoPrivateMessages(hud) {
+ is(
+ findConsoleAPIMessage(hud, PRIVATE_MESSAGE, ":not(.error)")?.textContent,
+ undefined,
+ "no console message displayed"
+ );
+ is(
+ findErrorMessage(hud, PRIVATE_EXCEPTION)?.textContent,
+ undefined,
+ "no exception displayed"
+ );
+}
diff --git a/devtools/client/webconsole/test/browser/browser_console_webextension.js b/devtools/client/webconsole/test/browser/browser_console_webextension.js
new file mode 100644
index 0000000000..b11a56bf27
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_webextension.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that messages from WebExtension are logged in the Browser Console.
+
+"use strict";
+
+const TEST_URI =
+ "https://example.com/browser/devtools/client/webconsole/" +
+ "test/browser/test-console.html?" +
+ Date.now();
+
+add_task(async function () {
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+ await addTab(TEST_URI);
+
+ await testWebExtensionMessages(false);
+ await testWebExtensionMessages(true);
+});
+
+async function testWebExtensionMessages(
+ createWebExtensionBeforeOpeningBrowserConsole = false
+) {
+ let extension;
+ if (createWebExtensionBeforeOpeningBrowserConsole) {
+ extension = await loadExtension();
+ }
+ const hud = await BrowserConsoleManager.toggleBrowserConsole();
+ if (!createWebExtensionBeforeOpeningBrowserConsole) {
+ extension = await loadExtension();
+ }
+
+ // TODO: Re-enable this (See Bug 1699050).
+ /*
+ // Trigger the messages logged when opening the popup.
+ const { AppUiTestDelegate } = ChromeUtils.importESModule(
+ "resource://testing-common/AppUiTestDelegate.sys.mjs"
+ );
+ const onPopupReady = extension.awaitMessage(`popup-ready`);
+ await AppUiTestDelegate.clickBrowserAction(window, extension.id);
+ // Ensure the popup script ran before going further
+ AppUiTestDelegate.awaitExtensionPanel(window, extension.id);
+ await onPopupReady;
+ */
+
+ // Wait enough so any duplicated message would have the time to be rendered
+ await wait(1000);
+
+ await checkUniqueMessageExists(
+ hud,
+ "content console API message",
+ ".console-api"
+ );
+ await checkUniqueMessageExists(
+ hud,
+ "background console API message",
+ ".console-api"
+ );
+
+ await checkUniqueMessageExists(hud, "content error", ".error");
+ await checkUniqueMessageExists(hud, "background error", ".error");
+
+ // TODO: Re-enable those checks (See Bug 1699050).
+ // await checkUniqueMessageExists(hud, "popup console API message", ".console-api");
+ // await checkUniqueMessageExists(hud, "popup error", ".error");
+
+ await clearOutput(hud);
+
+ info("Close the Browser Console");
+ await safeCloseBrowserConsole();
+
+ await extension.unload();
+}
+
+async function loadExtension() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ background: { scripts: ["background.js"] },
+
+ browser_action: {
+ default_popup: "popup.html",
+ },
+
+ content_scripts: [
+ {
+ matches: [TEST_URI],
+ js: ["content-script.js"],
+ },
+ ],
+ },
+ useAddonManager: "temporary",
+
+ files: {
+ "background.js": function () {
+ console.log("background console API message");
+ throw new Error("background error");
+ },
+
+ "popup.html": `
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>Popup</body>
+ <script src="popup.js"></script>
+ </html>`,
+
+ "popup.js": function () {
+ console.log("popup console API message");
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(() => {
+ throw new Error("popup error");
+ }, 5);
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(() => {
+ // eslint-disable-next-line no-undef
+ browser.test.sendMessage(`popup-ready`);
+ }, 10);
+ },
+
+ "content-script.js": function () {
+ console.log("content console API message");
+ throw new Error("content error");
+ },
+ },
+ });
+ await extension.startup();
+ return extension;
+}
diff --git a/devtools/client/webconsole/test/browser/browser_console_window_object_inheritance.js b/devtools/client/webconsole/test/browser/browser_console_window_object_inheritance.js
new file mode 100644
index 0000000000..2356f5e951
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_window_object_inheritance.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ await addTab("about:blank");
+
+ info(`Open browser console`);
+ const hud = await BrowserConsoleManager.openBrowserConsoleOrFocus();
+
+ info(`Clear existing messages`);
+ const onMessagesCleared = hud.ui.once("messages-cleared");
+ await clearOutput(hud);
+ await onMessagesCleared;
+
+ info(`Create a DOM window object`);
+ await hud.commands.scriptCommand.execute(`
+ globalThis.myBrowser = Services.appShell.createWindowlessBrowser();
+ globalThis.myWindow = myBrowser.document.defaultView;
+ `);
+
+ info(`Check objects inheriting from a DOM window`);
+ async function check(input, expected, name) {
+ const msg = await executeAndWaitForResultMessage(hud, input, "");
+ is(msg.node.querySelector(".message-body").textContent, expected, name);
+ }
+ await check("Object.create(myWindow)", "Object { }", "Empty object");
+ await check(
+ "Object.create(myWindow, { location: { value: 1, enumerable: true } })",
+ "Object { location: 1 }",
+ "Object with 'location' property"
+ );
+ await check(
+ `Object.create(myWindow, {
+ location: {
+ get() {
+ console.error("pwned!");
+ return { href: "Oops" };
+ },
+ enumerable: true,
+ },
+ })`,
+ "Object { location: Getter }",
+ "Object with 'location' unsafe getter"
+ );
+
+ info(`Check that no error was logged`);
+ // wait a bit so potential errors can be printed
+ await wait(1000);
+ const error = findErrorMessage(hud, "", ":not(.network)");
+ if (error) {
+ ok(false, `Got error ${JSON.stringify(error.textContent)}`);
+ } else {
+ ok(true, "No error was logged");
+ }
+
+ info(`Cleanup`);
+ await hud.commands.scriptCommand.execute(`
+ myBrowser.close();
+ delete globalThis.myBrowser;
+ delete globalThis.myWindow;
+ `);
+});
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..8b9660f9e2
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_add_edited_input_to_history.js
@@ -0,0 +1,84 @@
+/* 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,<!DOCTYPE html>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(() => findEvaluationResultMessage(hud, "first item"));
+ 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(() => findEvaluationResultMessage(hud, "second item"));
+
+ 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(
+ () => findEvaluationResultMessages(hud, "second item").length == 2
+ );
+
+ setInputValue(hud, '"second item" ');
+ EventUtils.synthesizeKey("KEY_Enter");
+ await waitFor(
+ () => findEvaluationResultMessages(hud, "second item").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..509e01a065
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete-properties-with-non-alphanumeric-names.js
@@ -0,0 +1,46 @@
+/* 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,<!DOCTYPE html>test autocompletion with $ or _`;
+
+add_task(async function () {
+ const hud = await openNewTabAndConsole(TEST_URI);
+
+ await executeAndWaitForResultMessage(
+ hud,
+ "var testObject = {$$aaab: '', $$aaac: ''}",
+ ""
+ );
+
+ // 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 executeAndWaitForResultMessage(
+ hud,
+ "let foobar = {a: ''}; const blargh = {a: 1};",
+ ""
+ );
+ 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..ad557e5310
--- /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,<!DOCTYPE html>
+ <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..e880229b9d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_array_no_index.js
@@ -0,0 +1,39 @@
+/* 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,
+<!DOCTYPE html>
+<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..b6cdd5db17
--- /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,<!DOCTYPE html><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..217f2f35e6
--- /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,<!DOCTYPE html>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..2aac0dee1e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_cached_results.js
@@ -0,0 +1,148 @@
+/* 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,<!DOCTYPE html><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");
+ let result = await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.wrappedJSObject.x.docfoobar = "added";
+ return content.wrappedJSObject.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"
+ );
+
+ result = await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.wrappedJSObject.x.docfoobar = "added";
+ delete content.wrappedJSObject.x.docfoobar;
+ return typeof content.wrappedJSObject.x.docfoobar;
+ });
+ is(result, "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, "'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");
+ result = await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.wrappedJSObject.x.docfoobar = "added";
+ return content.wrappedJSObject.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..d86a52a0a1
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_commands.js
@@ -0,0 +1,67 @@
+/* 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,<!DOCTYPE html>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",
+ ":history",
+ ":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..e5543eed5f
--- /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,<!DOCTYPE html>
+<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..018faac310
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_crossdomain_iframe.js
@@ -0,0 +1,44 @@
+/* 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 =
+ "https://example.com/browser/devtools/client/webconsole/" +
+ "test/browser/test-iframe-parent.html";
+
+add_task(async function () {
+ const hud = await openNewTabAndConsole(TEST_URI);
+
+ await executeAndWaitForResultMessage(hud, "document.title", "iframe parent");
+ 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 = waitForMessageByType(
+ hud,
+ "Permission denied",
+ ".error"
+ );
+ 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 executeAndWaitForResultMessage(
+ hud,
+ "window.location",
+ "test-iframe-parent.html"
+ );
+ 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..27a277f2e1
--- /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,<!DOCTYPE html>
+<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..e150695e46
--- /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,<!DOCTYPE html>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, "'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..9b4f500d55
--- /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,<!DOCTYPE html>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.of(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..c565c2b575
--- /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,<!DOCTYPE html>
+<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..737248aaf8
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_expression_variables.js
@@ -0,0 +1,51 @@
+/* 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,<!DOCTYPE html>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 {
+ #secret
+ #getSecret() {}
+ }
+ 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..a9fb1d5e0f
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_extraneous_closing_brackets.js
@@ -0,0 +1,21 @@
+/* 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,<!DOCTYPE html>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..ad96dca55b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_getters_cache.js
@@ -0,0 +1,130 @@
+/* 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,<!DOCTYPE html>
+<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 toolbox = await gDevTools.getToolboxForTab(gBrowser.selectedTab);
+
+ 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");
+ await reloadBrowser();
+ info("tab reloaded, waiting for the popup to close");
+ await onPopupClose;
+
+ 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..3aa5853749
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_getters_cancel.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 confirm dialog can be closed with different actions.
+
+const TEST_URI = `data:text/html;charset=utf-8,<!DOCTYPE html>
+<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 toolbox = await gDevTools.getToolboxForTab(gBrowser.selectedTab);
+
+ 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(() => findEvaluationResultMessage(hud, "3"));
+ 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..a343517547
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_getters_confirm.js
@@ -0,0 +1,147 @@
+/* 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,<!DOCTYPE html>
+<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 toolbox = await gDevTools.getToolboxForTab(gBrowser.selectedTab);
+
+ 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..d33cfd2a17
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_getters_learn_more_link.js
@@ -0,0 +1,56 @@
+/* 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,<!DOCTYPE html>
+<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 DOC_URL =
+ "https://firefox-source-docs.mozilla.org/devtools-user/web_console/invoke_getters_from_autocomplete/";
+
+add_task(async function () {
+ const hud = await openNewTabAndConsole(TEST_URI);
+ const toolbox = await gDevTools.getToolboxForTab(gBrowser.selectedTab);
+
+ 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 { link } = await simulateLinkClick(learnMoreEl);
+ is(link, DOC_URL, `Click on "Learn More" link navigates user to ${DOC_URL}`);
+
+ 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..bd75fe4534
--- /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,<!DOCTYPE html><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..a091abdb63
--- /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..520a78e2d1
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_in_debugger_stackframe.js
@@ -0,0 +1,133 @@
+/* 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";
+
+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 toolbox = await gDevTools.getToolboxForTab(gBrowser.selectedTab);
+
+ 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, "'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, "method", "foo1Obj completion");
+ ok(
+ hasExactPopupLabels(popup, ["method", "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 'this.' gives 'method', 'prop1', and 'prop2', not global variables, since we are paused in
+ // `foo1Obj.method()` (called by `secondCall`)
+ await jstermComplete("this.");
+ ok(
+ hasExactPopupLabels(popup, ["method", "prop1", "prop2"]),
+ `"this." gave the expected suggestions`
+ );
+
+ await selectFrame(dbg, stackFrames[1]);
+
+ // Test if 'foo' gives 'foo3' and 'foo1' but not 'foo2', since we are now in the `secondCall`
+ // frame (called by `firstCall`, which we call in `pauseDebugger`).
+ await jstermComplete("foo");
+ ok(
+ hasExactPopupLabels(popup, ["foo1", "foo1Obj", "foo3", "foo3Obj"]),
+ `"foo." gave the expected suggestions`
+ );
+
+ // Test that 'shadowed.' autocompletes properties from the local variable named "shadowed".
+ await jstermComplete("shadowed.");
+ ok(
+ hasExactPopupLabels(popup, ["bar"]),
+ `"shadowed." gave the expected suggestions`
+ );
+
+ await openDebugger();
+
+ // Select the frame for the `firstCall` function.
+ await selectFrame(dbg, stackFrames[2]);
+
+ 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 that 'shadowed.' autocompletes properties from the global variable named "shadowed".
+ await jstermComplete("shadowed.");
+ ok(
+ hasExactPopupLabels(popup, ["foo"]),
+ `"shadowed." 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..6baf1d4201
--- /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,<!DOCTYPE html>
+<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_mapped_variables.js b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_mapped_variables.js
new file mode 100644
index 0000000000..5309cb4c6c
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_mapped_variables.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that makes sure source mapped variables appear in autocompletion
+// on an equal footing with variables from the generated source.
+
+"use strict";
+
+const TEST_URI =
+ "http://example.com/browser/devtools/client/webconsole/" +
+ "test/browser/test-autocomplete-mapped.html";
+
+add_task(async function () {
+ const hud = await openNewTabAndConsole(TEST_URI);
+ const { jsterm } = hud;
+ const { autocompletePopup: popup } = jsterm;
+ const toolbox = await gDevTools.getToolboxForTab(gBrowser.selectedTab);
+
+ info("Opening Debugger and enabling map scopes");
+ await openDebugger();
+ const dbg = createDebuggerContext(toolbox);
+ dbg.actions.toggleMapScopes();
+
+ info("Waiting for pause");
+ // This calls firstCall() on the content page and waits for pause. (firstCall
+ // has a debugger statement)
+ await pauseDebugger(dbg);
+
+ await toolbox.selectTool("webconsole");
+ await setInputValueForAutocompletion(hud, "valu");
+ ok(
+ hasExactPopupLabels(popup, ["value", "valueOf", "values"]),
+ "Autocomplete popup displays original variable name"
+ );
+
+ await setInputValueForAutocompletion(hud, "temp");
+ ok(
+ hasExactPopupLabels(popup, ["temp", "temp2"]),
+ "Autocomplete popup displays original variable name when entering a complete variable name"
+ );
+
+ await setInputValueForAutocompletion(hud, "t");
+ ok(
+ hasPopupLabel(popup, "t"),
+ "Autocomplete popup displays generated variable name"
+ );
+
+ await setInputValueForAutocompletion(hud, "value.to");
+ ok(
+ hasPopupLabel(popup, "toString"),
+ "Autocomplete popup displays properties of original variable"
+ );
+
+ await setInputValueForAutocompletion(hud, "imported.imp");
+ ok(
+ hasPopupLabel(popup, "importResult"),
+ "Autocomplete popup displays properties of multi-part variable"
+ );
+
+ let tooltip = await setInputValueForGetterConfirmDialog(
+ toolbox,
+ hud,
+ "getter."
+ );
+ let labelEl = tooltip.querySelector(".confirm-label");
+ is(
+ labelEl.textContent,
+ "Invoke getter getter to retrieve the property list?",
+ "Dialog has expected text content"
+ );
+
+ info(
+ "Check that getter confirmation on a variable that maps to two getters invokes both getters"
+ );
+ let onPopUpOpen = popup.once("popup-opened");
+ EventUtils.synthesizeKey("KEY_Tab");
+ await onPopUpOpen;
+ ok(popup.isOpen, "popup is open after Tab");
+ ok(hasPopupLabel(popup, "getterResult"), "popup has expected items");
+
+ info(
+ "Check that the getter confirmation dialog shows the original variable name"
+ );
+ tooltip = await setInputValueForGetterConfirmDialog(
+ toolbox,
+ hud,
+ "localWithGetter.value."
+ );
+ labelEl = tooltip.querySelector(".confirm-label");
+ is(
+ labelEl.textContent,
+ "Invoke getter localWithGetter.value to retrieve the property list?",
+ "Dialog has expected text content"
+ );
+
+ info(
+ "Check that hitting Tab does invoke the getter and return its properties"
+ );
+ onPopUpOpen = popup.once("popup-opened");
+ EventUtils.synthesizeKey("KEY_Tab");
+ await onPopUpOpen;
+ ok(popup.isOpen, "popup is open after Tab");
+ ok(hasPopupLabel(popup, "then"), "popup has expected items");
+ info("got popup items: " + JSON.stringify(getAutocompletePopupLabels(popup)));
+
+ info(
+ "Check that authorizing an original getter applies to the generated getter"
+ );
+ await setInputValueForAutocompletion(hud, "o.value.");
+ ok(hasPopupLabel(popup, "then"), "popup has expected items");
+
+ await setInputValueForAutocompletion(hud, "(temp + temp2).");
+ ok(
+ hasPopupLabel(popup, "toFixed"),
+ "Autocomplete popup displays properties of eagerly evaluated value"
+ );
+ info("got popup items: " + JSON.stringify(getAutocompletePopupLabels(popup)));
+
+ info("Disabling map scopes");
+ dbg.actions.toggleMapScopes();
+ await setInputValueForAutocompletion(hud, "tem");
+ const autocompleteLabels = getAutocompletePopupLabels(popup);
+ ok(
+ !autocompleteLabels.includes("temp"),
+ "Autocomplete popup does not display mapped variables when mapping is disabled"
+ );
+
+ await resume(dbg);
+});
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..88d56b275d
--- /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,<!DOCTYPE html>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..4db7d18cb0
--- /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,<!DOCTYPE html>
+<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..a98a2495c4
--- /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`);
+ // we're using the browser console so we can check for error messages that would be
+ // caused by console code.
+ 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 commands directly as we don't want to impact the UI state.
+ await hud.commands.scriptCommand.execute("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(() => findErrorMessage(hud, "", ":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.commands.scriptCommand.execute("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..14ef4aa177
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_paste_undo.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=utf-8,<!DOCTYPE html><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..655a1f41af
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_race_on_enter.js
@@ -0,0 +1,170 @@
+/* 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,<!DOCTYPE html><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 = waitForMessageByType(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 = waitForMessageByType(hud, "docx is not defined", ".error");
+ 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..fba3da5781
--- /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,<!DOCTYPE html>
+<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..5bebf850dc
--- /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,<!DOCTYPE html>
+<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("resource://devtools/client/webconsole/selectors/history.js");
+
+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 = waitForMessageByType(hud, "hello world", ".result");
+ 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..79cdf4fb7a
--- /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,<!DOCTYPE html>`;
+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..cf294610db
--- /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,<!DOCTYPE html>
+<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..b3fa1115f2
--- /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,<!DOCTYPE html>
+<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 reloadBrowser();
+ 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..0e8b894ce6
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_await.js
@@ -0,0 +1,83 @@
+/* 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,<!DOCTYPE html>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);
+
+ info("Evaluate a top-level await expression");
+ const simpleAwait = `await new Promise(r => setTimeout(() => r(["await1"]), 500))`;
+ await executeAndWaitForResultMessage(hud, 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, mutableMessagesById } = hud.ui.wrapper
+ .getStore()
+ .getState().messages;
+ const [commandId, resultId] = visibleMessages;
+ const delta =
+ mutableMessagesById.get(resultId).timeStamp -
+ mutableMessagesById.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(
+ hud,
+ `x = await new Promise(r => setTimeout(() => r("await2"), 500))`,
+ `await2`
+ );
+
+ let message = await executeAndWaitForResultMessage(
+ hud,
+ `"-" + 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(
+ hud,
+ `new Promise(r => setTimeout(() => r(1), 1000))`,
+ `Promise {`
+ );
+ ok(message, "Promise are displayed as expected");
+
+ info("Check that then getters aren't called twice");
+ message = await executeAndWaitForResultMessage(
+ hud,
+ // It's important to keep the last statement of the expression as it covers the original issue.
+ // We could execute another expression to get `object.called`, but since we get a preview
+ // of the object with an accurate `called` value, this is enough.
+ `
+ var obj = {
+ called: 0,
+ get then(){
+ this.called++
+ }
+ };
+ await obj`,
+ `Object { called: 1, then: Getter }`
+ );
+});
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..6eb1ee53ed
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_await_assignments.js
@@ -0,0 +1,77 @@
+/* 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,<!DOCTYPE html>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 executeAndWaitForResultMessage(
+ hud,
+ `let bazA = await new Promise(r => setTimeout(() => r("local-bazA"), 10))`,
+ "local-bazA"
+ );
+ await checkVariable(hud, "bazA");
+
+ info(
+ "Check that declaring a const variable does not create a global property"
+ );
+ await executeAndWaitForResultMessage(
+ hud,
+ `const bazB = await new Promise(r => setTimeout(() => r("local-bazB"), 10))`,
+ "local-bazB"
+ );
+ await checkVariable(hud, "bazB");
+
+ info("Check that complex variable declarations work as expected");
+ await executeAndWaitForResultMessage(
+ 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"]
+ }
+ }
+ });`,
+ ""
+ );
+ 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 executeAndWaitForResultMessage(hud, `window.${varName}`, `undefined`);
+ ok(true, `The ${varName} assignment did not create a global variable`);
+ await executeAndWaitForResultMessage(hud, varName, `"local-${varName}"`);
+ 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..8cd3978d2d
--- /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,<!DOCTYPE html>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 = waitForMessageByType(
+ hud,
+ "await-concurrent-9000",
+ ".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..a1a6a0394b
--- /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,<!DOCTYPE html>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(
+ () => findEvaluationResultMessages(hud, "foo").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..fae858c8bb
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_await_dynamic_import.js
@@ -0,0 +1,43 @@
+/* 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);
+
+ info("Evaluate an expression with a dynamic import");
+ let importAwaitExpression = `
+ var {sum} = await import("./test-dynamic-import.mjs");
+ sum(1, 2, 3);
+ `;
+ await executeAndWaitForResultMessage(
+ hud,
+ 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.mjs");
+ sum(2, 3, 4);
+ `;
+ await executeAndWaitForResultMessage(
+ hud,
+ 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..94fb671768
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_await_error.js
@@ -0,0 +1,215 @@
+/* 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,<!DOCTYPE html>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);
+
+ info("Check that awaiting for a rejecting promise displays an error");
+ let res = await executeAndWaitForErrorMessage(
+ hud,
+ `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(
+ hud,
+ `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(
+ hud,
+ `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(
+ hud,
+ `await Promise.reject(null)`,
+ `Uncaught (in promise) null`
+ );
+ ok(
+ res.node,
+ "awaiting for Promise rejecting with null displays the expected error"
+ );
+
+ res = await executeAndWaitForErrorMessage(
+ hud,
+ `await Promise.reject(undefined)`,
+ `Uncaught (in promise) undefined`
+ );
+ ok(
+ res.node,
+ "awaiting for Promise rejecting with undefined displays the expected error"
+ );
+
+ res = await executeAndWaitForErrorMessage(
+ hud,
+ `await Promise.reject(false)`,
+ `Uncaught (in promise) false`
+ );
+ ok(
+ res.node,
+ "awaiting for Promise rejecting with false displays the expected error"
+ );
+
+ res = await executeAndWaitForErrorMessage(
+ hud,
+ `await Promise.reject(0)`,
+ `Uncaught (in promise) 0`
+ );
+ ok(
+ res.node,
+ "awaiting for Promise rejecting with 0 displays the expected error"
+ );
+
+ res = await executeAndWaitForErrorMessage(
+ hud,
+ `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(
+ hud,
+ `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(
+ hud,
+ `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(
+ hud,
+ `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(
+ hud,
+ `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(
+ hud,
+ `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(
+ hud,
+ `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(
+ hud,
+ `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(
+ hud,
+ `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(
+ (await findMessagesVirtualizedByType({ hud, typeSelector: ".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(
+ (await findMessagesVirtualizedByType({ hud, typeSelector: ".error" }))
+ .length,
+ expectedErrorsNumber,
+ "There is the expected number of error messages"
+ );
+
+ info("Check that there's no result message");
+ is(
+ (await findMessagesVirtualizedByType({ hud, typeSelector: ".result" }))
+ .length,
+ 0,
+ "There is no result messages"
+ );
+
+ info("Check that malformed await expressions displays a meaningful error");
+ res = await executeAndWaitForErrorMessage(
+ hud,
+ `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..d947e169d1
--- /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,<!DOCTYPE html>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);
+
+ info("Evaluate a simple expression to populate $_");
+ await executeAndWaitForResultMessage(hud, `1 + 1`, `2`);
+
+ await executeAndWaitForResultMessage(hud, `$_ + 1`, `3`);
+ ok(true, "$_ works as expected");
+
+ info(
+ "Check that $_ does not get replaced until the top-level await is resolved"
+ );
+ const onAwaitResultMessage = executeAndWaitForResultMessage(
+ hud,
+ `await new Promise(res => setTimeout(() => res([1,2,3, $_]), 1000))`,
+ `Array(4) [ 1, 2, 3, 4 ]`
+ );
+
+ await executeAndWaitForResultMessage(hud, `$_ + 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(
+ hud,
+ `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 executeAndWaitForErrorMessage(
+ hud,
+ `x = await new Promise((resolve,reject) =>
+ setTimeout(() => reject("await-" + "rej"), 500))`,
+ `await-rej`
+ );
+
+ await executeAndWaitForResultMessage(hud, `$_`, `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 = waitForMessageByType(
+ hud,
+ "await-concurrent-4000",
+ ".result"
+ );
+ for (const input of inputs) {
+ execute(hud, input);
+ }
+ await onMessage;
+
+ await executeAndWaitForResultMessage(
+ hud,
+ `"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..ef1604f9c7
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_await_paused.js
@@ -0,0 +1,86 @@
+/* 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,<!DOCTYPE html>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 toolbox = await gDevTools.getToolboxForTab(gBrowser.selectedTab);
+
+ 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), 2000);
+ console.log("awaitExpression executed");
+ })`;
+
+ const onAwaitResultMessage = waitForMessageByType(
+ hud,
+ `[ "res", "bar" ]`,
+ ".result"
+ );
+ const onAwaitExpressionExecuted = waitForMessageByType(
+ hud,
+ "awaitExpression executed",
+ ".console-api"
+ );
+ 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 executeAndWaitForResultMessage(hud, `"smoke"`, `"smoke"`);
+
+ // Give the engine some time to evaluate the await expression before resuming.
+ // Otherwise the awaitExpression may be evaluate while the thread is already resumed!
+ await onAwaitExpressionExecuted;
+
+ // 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(() => findEvaluationResultMessage(hud, "pauseExpression-res"));
+
+ 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" ]`,
+ ];
+ Assert.deepEqual(
+ messagesText,
+ expectedMessages,
+ "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..c774f9ae6e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_block_command.js
@@ -0,0 +1,103 @@
+"use strict";
+
+const TEST_URI =
+ "https://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(() => findConsoleAPIMessage(hud, "successful"));
+ ok(resp1, "the request was not blocked");
+ info(`Execute the :block command and try to do execute a network request`);
+ await executeAndWaitForMessageByType(
+ hud,
+ blockCommand,
+ "are now blocked",
+ ".console-api"
+ );
+ await tryFetching();
+
+ const resp2 = await waitFor(() => findConsoleAPIMessage(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);
+ 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 executeAndWaitForMessageByType(
+ hud,
+ unblockCommand,
+ "Removed blocking",
+ ".console-api"
+ );
+ 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(() => findConsoleAPIMessage(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 =
+ "https://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..4327daa9ce
--- /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,<!DOCTYPE html><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..7704230d6e
--- /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,<!DOCTYPE html><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..ecca77f071
--- /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,<!DOCTYPE html><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..58190cb333
--- /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,<!DOCTYPE html><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..29c8d338e6
--- /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,<!DOCTYPE html><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 executeAndWaitForResultMessage(
+ 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..911287cc71
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_completion_dollar_zero.js
@@ -0,0 +1,47 @@
+/* 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,<!DOCTYPE html>
+<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");
+ await selectNodeWithPicker(toolbox, "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..45a9148892
--- /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,<!DOCTYPE html><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..b4c4504c84
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_content_defined_helpers.js
@@ -0,0 +1,58 @@
+/* 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,<!DOCTYPE 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 executeAndWaitForResultMessage(
+ hud,
+ `${helper}()`,
+ `"${PREFIX + helper}"`
+ );
+ 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..7ed8148bae
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_context_menu_labels.js
@@ -0,0 +1,36 @@
+/* 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,<!DOCTYPE html><p>test page</p>`;
+
+add_task(async function () {
+ const hud = await openNewTabAndConsole(TEST_URI);
+ const { jsterm } = hud;
+
+ const toolbox = await gDevTools.getToolboxForTab(gBrowser.selectedTab);
+
+ // 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..5e8b684c41
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_copy_command.js
@@ -0,0 +1,70 @@
+/* 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,<!DOCTYPE html>
+<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);
+});
+
+add_task(async function () {
+ const hud = await openNewTabAndConsole(TEST_URI);
+ await executeAndWaitForErrorMessage(
+ hud,
+ "var a = {}; a.b = a; copy(a);",
+ "`copy` command failed, object can’t be stringified: TypeError: cyclic object value"
+ );
+});
+
+function testCopy(hud, stringToCopy, expectedResult) {
+ return waitForClipboardPromise(async () => {
+ info(`Attempting to copy: "${stringToCopy}"`);
+ const command = `copy(${stringToCopy})`;
+ info(`Executing command: "${command}"`);
+ await executeAndWaitForMessageByType(
+ hud,
+ command,
+ "String was copied to clipboard",
+ ".console-api"
+ );
+ }, 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..2e8caa6345
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_ctrl_a_select_all.js
@@ -0,0 +1,52 @@
+/* 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,<!DOCTYPE html>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..5db3e2a68f
--- /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,<!DOCTYPE html>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 = waitForMessageByType(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..8f6e6bda3e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_document_no_xray.js
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html,<!DOCTYPE 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 executeAndWaitForResultMessage(
+ hud,
+ "document",
+ "HTMLDocument"
+ );
+ 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..e65a0c5f05
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_eager_evaluation.js
@@ -0,0 +1,371 @@
+/* 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,<!DOCTYPE html>
+<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 executeAndWaitForResultMessage(
+ hud,
+ "'result: ' + (x + y)",
+ "result: 7"
+ );
+
+ 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");
+
+ // go back to inline layout.
+ await toggleLayout(hud);
+
+ 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 ]");
+
+ info("Check that top-level await expression are not evaluated");
+ setInputValue(hud, "await 1; 2 + 3;");
+ await waitForNoEagerEvaluationResult(hud);
+ ok(true, "instant evaluation is disabled for top-level await expressions");
+});
+
+// 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");
+
+ // reset the preference
+ await pushPref(EAGER_EVALUATION_PREF, true);
+});
+
+// Test that the console instant evaluation is updated on page navigation
+add_task(async function () {
+ const start_uri = "data:text/html, Start uri";
+ const new_uri = "data:text/html, Test console refresh instant value";
+ const hud = await openNewTabAndConsole(start_uri);
+
+ setInputValue(hud, "globalThis.location.href");
+ await waitForEagerEvaluationResult(hud, `"${start_uri}"`);
+
+ await navigateTo(new_uri);
+ await waitForEagerEvaluationResult(hud, `"${new_uri}"`);
+});
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..94f7c92920
--- /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,<!DOCTYPE html>
+<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 highlighterTestFront = await getHighlighterTestFront(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 highlighterTestFront.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 highlighterTestFront.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 highlighterTestFront.isHighlighting();
+ is(isVisible, true, "Highlighter is displayed");
+
+ onHighlighterHidden = highlighter.waitForHighlighterHidden();
+ EventUtils.synthesizeKey("KEY_Enter");
+ await waitFor(() => findEvaluationResultMessage(hud, `#text "mydivtext"`));
+ await waitForNoEagerEvaluationResult(hud);
+ isVisible = await highlighterTestFront.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..47d09b9312
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_eager_evaluation_in_debugger_stackframe.js
@@ -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/. */
+
+"use strict";
+
+// Test that eager evaluation works as expected when paused in the debugger.
+
+const TEST_URI = `data:text/html;charset=utf-8,<!DOCTYPE html>
+<script>
+var x = "global";
+
+function pauseInDebugger(param) {
+ let x = "local";
+ debugger;
+}
+
+</script>
+`;
+
+add_task(async function () {
+ const hud = await openNewTabAndConsole(TEST_URI);
+
+ const toolbox = await gDevTools.getToolboxForTab(gBrowser.selectedTab);
+
+ 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_warnings.js b/devtools/client/webconsole/test/browser/browser_jsterm_eager_evaluation_warnings.js
new file mode 100644
index 0000000000..446024c7b9
--- /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,<!DOCTYPE html>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(
+ findWarningMessage(hud, "getElementById"),
+ 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(() => findWarningMessage(hud, "getElementById"));
+ 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..a47f570186
--- /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,<!DOCTYPE html><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..e902a7e761
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_editor_code_folding.js
@@ -0,0 +1,74 @@
+/* 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,<!DOCTYPE html>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..ecbdbc92d7
--- /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,<!DOCTYPE html>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 executeAndWaitForResultMessage(hud, expression, "");
+ 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..8d353959f7
--- /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,<!DOCTYPE html>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 = waitForMessageByType(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 = waitForMessageByType(hud, "SyntaxError", ".error");
+ 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(() => findEvaluationResultMessage(hud, "10"));
+ ok(msg, "found evaluation result of 1st expression");
+
+ msg = await waitFor(() => findEvaluationResultMessage(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..2cede4b69f
--- /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,<!DOCTYPE html>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 executeAndWaitForResultMessage(hud, undefined, "");
+ 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..8e46247f4a
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_editor_execute_selection.js
@@ -0,0 +1,69 @@
+/* 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,<!DOCTYPE html>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 = waitForMessageByType(
+ 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 = waitForMessageByType(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 = waitForMessageByType(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 = waitForMessageByType(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..e233cb4cbc
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_editor_gutter.js
@@ -0,0 +1,46 @@
+/* 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,<!DOCTYPE html>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..48410a14df
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_editor_onboarding.js
@@ -0,0 +1,51 @@
+/* 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,<!DOCTYPE html>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..db9557bf04
--- /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,<!DOCTYPE html>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..080b0d17e3
--- /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,<!DOCTYPE html>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..0b9b828ce3
--- /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 = waitForMessageByType(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..714f9d90f4
--- /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,<!DOCTYPE html>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..d804005074
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_editor_toolbar.js
@@ -0,0 +1,181 @@
+/* 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,<!DOCTYPE html><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(() => findMessageByType(hud, input, ".command"));
+ await waitFor(() => findEvaluationResultMessage(hud, output));
+ 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..0e37ad27fb
--- /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,<!DOCTYPE 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("resource://devtools/server/actors/errordocs.js");
+
+ 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 executeAndWaitForErrorMessage(
+ 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..46a0d74529
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_error_outside_valid_range.js
@@ -0,0 +1,26 @@
+/* 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,<!DOCTYPE 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 executeAndWaitForErrorMessage(
+ hud,
+ "new Request('',{redirect:'foo'})",
+ text
+ );
+ 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..71cc06345e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector.js
@@ -0,0 +1,279 @@
+/* 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 = `https://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.webconsole.input.context", true);
+
+ const hud = await openNewTabWithIframesAndConsole(TEST_URI, [
+ `https://example.org/${IFRAME_PATH}?id=iframe-1`,
+ `https://example.net/${IFRAME_PATH}?id=iframe-2`,
+ ]);
+
+ const evaluationContextSelectorButton = hud.ui.outputNode.querySelector(
+ ".webconsole-evaluation-selector-button"
+ );
+
+ if (!isFissionEnabled() && !isEveryFrameTargetEnabled()) {
+ is(
+ evaluationContextSelectorButton,
+ null,
+ "context selector is only displayed when Fission or EFT is enabled"
+ );
+ return;
+ }
+
+ ok(
+ evaluationContextSelectorButton,
+ "The evaluation context selector is visible"
+ );
+ is(
+ evaluationContextSelectorButton.innerText,
+ "Top",
+ "The button has the expected 'Top' text"
+ );
+ is(
+ evaluationContextSelectorButton.classList.contains("checked"),
+ false,
+ "The checked class isn't applied"
+ );
+
+ const topLevelDocumentMessage = await executeAndWaitForResultMessage(
+ hud,
+ "document.location",
+ "example.com"
+ );
+
+ 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: `https://example.org/${IFRAME_PATH}?id=iframe-1`,
+ };
+ const expectedSecondIframeItem = {
+ label: "iframe-2|example.net",
+ tooltip: `https://example.net/${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("checked"),
+ true,
+ "The checked class is applied"
+ );
+
+ await waitForEagerEvaluationResult(hud, `"example.org"`);
+ ok(true, "The instant evaluation result is updated in the iframe context");
+
+ const iframe1DocumentMessage = await executeAndWaitForResultMessage(
+ hud,
+ "document.location",
+ "example.org"
+ );
+ 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("example.net")
+ );
+ ok(true, "The context was set to the selected iframe document");
+ is(
+ evaluationContextSelectorButton.classList.contains("checked"),
+ true,
+ "The checked class is applied"
+ );
+
+ await waitForEagerEvaluationResult(hud, `"example.net"`);
+ ok(true, "The instant evaluation result is updated in the iframe context");
+
+ const iframe2DocumentMessage = await executeAndWaitForResultMessage(
+ hud,
+ "document.location",
+ "example.net"
+ );
+ 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("checked"),
+ false,
+ "The checked 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 https://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",
+ "example.net"
+ );
+ await waitForEagerEvaluationResult(
+ hud,
+ `Location https://example.net/${IFRAME_PATH}?id=iframe-2`
+ );
+ await waitFor(() =>
+ evaluationContextSelectorButton.innerText.includes("example.net")
+ );
+ 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");
+
+ info("Check that autocomplete data are cleared when changing context");
+ await setInputValueForAutocompletion(hud, "foo");
+ ok(
+ hasExactPopupLabels(hud.jsterm.autocompletePopup, ["foobar", "foobaz"]),
+ "autocomplete has expected items from top level document"
+ );
+ checkInputCompletionValue(hud, "bar", `completeNode has expected value`);
+
+ info("Select iframe document");
+ // We need to hide the popup to be able to select the target in the context selector.
+ // Don't use `closeAutocompletePopup` as it uses the Escape key, which explicitely hides
+ // the completion node.
+ const onPopupHidden = hud.jsterm.autocompletePopup.once("popuphidden");
+ hud.jsterm.autocompletePopup.hidePopup();
+ onPopupHidden;
+
+ selectTargetInContextSelector(hud, expectedSecondIframeItem.label);
+ await waitFor(() => getInputCompletionValue(hud) === "");
+ ok(true, `completeNode was cleared`);
+
+ const updated = hud.jsterm.once("autocomplete-updated");
+ EventUtils.sendString("b", hud.iframeWindow);
+ await updated;
+
+ ok(
+ hasExactPopupLabels(hud.jsterm.autocompletePopup, []),
+ "autocomplete data was cleared"
+ );
+});
+
+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 executeAndWaitForResultMessage(
+ hud,
+ `${variableName}`,
+ expectedTextResult
+ );
+ ok(true, "Correct variable assigned into console.");
+}
diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector_iframe_picker.js b/devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector_iframe_picker.js
new file mode 100644
index 0000000000..215495ee53
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector_iframe_picker.js
@@ -0,0 +1,124 @@
+/* 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 using the Toolbox
+// iframe picker.
+
+const TEST_URI = `https://example.com/document-builder.sjs?html=${encodeURIComponent(`
+ <html>
+ <h1>example.com</h1>
+ <iframe src="https://example.org/document-builder.sjs?html=example.org"></iframe>
+ <iframe src="https://example.net/document-builder.sjs?html=example.net"></iframe>
+ </html>
+`)}`;
+
+add_task(async function () {
+ // Enable the context selector and the frames button.
+ await pushPref("devtools.webconsole.input.context", true);
+ await pushPref("devtools.command-button-frames.enabled", true);
+
+ const hud = await openNewTabAndConsole(TEST_URI);
+
+ info("Wait until the iframe picker button is displayed");
+ try {
+ await waitFor(() => getFramesButton(hud.toolbox));
+ ok(
+ !isFissionEnabled() || isEveryFrameTargetEnabled(),
+ "iframe picker should only display remote frames when EFT is enabled"
+ );
+ } catch (e) {
+ if (isFissionEnabled() && !isEveryFrameTargetEnabled()) {
+ ok(true, "iframe picker displays remote frames only when EFT is enabled");
+ return;
+ }
+ throw e;
+ }
+
+ const evaluationContextSelectorButton = hud.ui.outputNode.querySelector(
+ ".webconsole-evaluation-selector-button"
+ );
+ await executeAndWaitForResultMessage(
+ hud,
+ "document.location.host",
+ `"example.com"`
+ );
+ ok(true, "The expression was evaluated in the example.com document.");
+
+ info("Select the example.org iframe");
+ selectFrameInIframePicker(hud.toolbox, "https://example.org");
+ try {
+ await waitFor(() =>
+ evaluationContextSelectorButton.innerText.includes("example.org")
+ );
+ if (!isEveryFrameTargetEnabled()) {
+ todo(
+ true,
+ "context selector should only reacts to iframe picker when EFT is enabled"
+ );
+ return;
+ }
+ } catch (e) {
+ if (!isEveryFrameTargetEnabled()) {
+ todo(
+ false,
+ "context selector only reacts to iframe picker when EFT is enabled"
+ );
+ return;
+ }
+ throw e;
+ }
+ ok(true, "The context was set to the example.org document");
+
+ await executeAndWaitForResultMessage(
+ hud,
+ "document.location.host",
+ `"example.org"`
+ );
+ ok(true, "The expression was evaluated in the example.org document.");
+
+ info("Select the example.net iframe");
+ selectFrameInIframePicker(hud.toolbox, "https://example.net");
+ await waitFor(() =>
+ evaluationContextSelectorButton.innerText.includes("example.net")
+ );
+ ok(true, "The context was set to the example.net document");
+
+ await executeAndWaitForResultMessage(
+ hud,
+ "document.location.host",
+ `"example.net"`
+ );
+ ok(true, "The expression was evaluated in the example.net document.");
+
+ info("Select the Top frame");
+ selectFrameInIframePicker(hud.toolbox, "https://example.com");
+ await waitFor(() =>
+ evaluationContextSelectorButton.innerText.includes("Top")
+ );
+ ok(true, "The context was set to the example.com document");
+
+ await executeAndWaitForResultMessage(
+ hud,
+ "document.location.host",
+ `"example.com"`
+ );
+ ok(true, "The expression was evaluated in the example.com document.");
+});
+
+function getFramesButton(toolbox) {
+ return toolbox.doc.getElementById("command-button-frames");
+}
+
+function selectFrameInIframePicker(toolbox, host) {
+ const commandItem = Array.from(
+ toolbox.doc.querySelectorAll("#toolbox-frame-menu .command .label")
+ ).find(label => label.textContent.startsWith(host));
+ if (!commandItem) {
+ throw new Error(`Couldn't find any frame starting with "${host}"`);
+ }
+
+ commandItem.click();
+}
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..11ffd22fd6
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector_inspector.js
@@ -0,0 +1,180 @@
+/* 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 = `https://example.com/${FILE_FOLDER}/test-console-evaluation-context-selector.html`;
+const IFRAME_PATH = `${FILE_FOLDER}/test-console-evaluation-context-selector-child.html`;
+
+// Import helpers for the inspector
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
+ this
+);
+
+requestLongerTimeout(2);
+
+add_task(async function () {
+ await pushPref("devtools.webconsole.input.context", true);
+
+ const hud = await openNewTabWithIframesAndConsole(TEST_URI, [
+ `https://example.org/${IFRAME_PATH}?id=iframe-1`,
+ `https://example.net/${IFRAME_PATH}?id=iframe-2`,
+ ]);
+
+ const evaluationContextSelectorButton = hud.ui.outputNode.querySelector(
+ ".webconsole-evaluation-selector-button"
+ );
+
+ if (!isFissionEnabled() && !isEveryFrameTargetEnabled()) {
+ is(
+ evaluationContextSelectorButton,
+ null,
+ "context selector is only displayed when Fission or EFT is enabled"
+ );
+ return;
+ }
+
+ setInputValue(hud, "document.location.host");
+ await waitForEagerEvaluationResult(hud, `"example.com"`);
+
+ info("Go to the inspector panel");
+ const inspector = await hud.toolbox.selectTool("inspector");
+
+ 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 selectNodeInFrames([".iframe-1", "h2"], inspector);
+
+ 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 top document via the context selector");
+ // This should take the lead over the currently selected element in the inspector
+ selectTargetInContextSelector(hud, "Top");
+
+ await waitForEagerEvaluationResult(hud, `"example.com"`);
+ await waitFor(() =>
+ evaluationContextSelectorButton.innerText.includes("Top")
+ );
+
+ info("Select the second iframe h2 element");
+ await selectNodeInFrames([".iframe-2", "h2"], inspector);
+
+ await waitFor(() =>
+ evaluationContextSelectorButton.innerText.includes("example.net")
+ );
+ ok(true, "The context was set to the selected iframe document");
+
+ await waitForEagerEvaluationResult(hud, `"example.net"`);
+ ok(true, "The instant evaluation result is updated in the iframe context");
+
+ info("Select an element in the top document");
+ await selectNodeInFrames(["h1"], inspector);
+
+ 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("example.net")
+ );
+ 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,
+ ["h1"],
+ "temp0",
+ `<h1 id="top-level">`
+ );
+ await waitFor(() =>
+ evaluationContextSelectorButton.innerText.includes("Top")
+ );
+ ok(true, "The context selector was updated");
+});
+
+async function testUseInConsole(
+ hud,
+ inspector,
+ selectors,
+ variableName,
+ expectedTextResult
+) {
+ const nodeFront = await selectNodeInFrames(selectors, inspector);
+ 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 executeAndWaitForResultMessage(hud, variableName, expectedTextResult);
+ 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..f94e3ff6eb
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector_pause_in_debugger.js
@@ -0,0 +1,124 @@
+/* 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_SSL}test-console-evaluation-context-selector.html`;
+const IFRAME_FILE = `test-console-evaluation-context-selector-child.html`;
+
+add_task(async function () {
+ await pushPref("devtools.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_SSL}${IFRAME_FILE}?id=iframe_org`
+ );
+ await addIFrameAndWaitForLoad(
+ `${URL_ROOT_NET_SSL}${IFRAME_FILE}?id=iframe_net`
+ );
+
+ 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"
+ );
+
+ if (!isFissionEnabled() && !isEveryFrameTargetEnabled()) {
+ is(
+ evaluationContextSelectorButton,
+ null,
+ "context selector is only displayed when Fission or EFT is enabled"
+ );
+ return;
+ }
+
+ 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 keyboardExecuteAndWaitForResultMessage(hud, `localVar`, "example.org");
+ 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("example.net"),
+ "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, /example\.net/);
+ ok(true, "Instant evaluation has the expected result");
+
+ await keyboardExecuteAndWaitForResultMessage(hud, `localVar`, "example.net");
+ 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..40c75a8e24
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector_targets_update.js
@@ -0,0 +1,242 @@
+/* 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 = `https://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.popups.debug", true);
+ await pushPref("devtools.webconsole.input.context", true);
+
+ const hud = await openNewTabWithIframesAndConsole(TEST_URI, [
+ `https://example.net/${IFRAME_PATH}?id=iframe-1`,
+ ]);
+
+ const evaluationContextSelectorButton = hud.ui.outputNode.querySelector(
+ ".webconsole-evaluation-selector-button"
+ );
+
+ if (!isFissionEnabled() && !isEveryFrameTargetEnabled()) {
+ is(
+ evaluationContextSelectorButton,
+ null,
+ "context selector is only displayed when Fission or EFT is enabled"
+ );
+ return;
+ }
+
+ 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|example.net",
+ tooltip: `https://example.net/${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 = `https://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(() => {
+ const items = getContextSelectorItems(hud);
+ return (
+ items.length === 4 &&
+ items.some(el =>
+ el
+ .querySelector(".label")
+ ?.textContent.includes("iframe-2|test1.example.org")
+ )
+ );
+ });
+
+ const expectedSecondIframeItem = {
+ label: `iframe-2|test1.example.org`,
+ tooltip: `https://test1.example.org/${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.net")
+ );
+ await waitForEagerEvaluationResult(hud, `"example.net"`);
+ 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");
+
+ info("Open a popup");
+ const originalTab = gBrowser.selectedTab;
+ let onSwitchedHost = hud.toolbox.once("host-changed");
+ await ContentTask.spawn(
+ gBrowser.selectedBrowser,
+ [IFRAME_PATH],
+ function (path) {
+ content.open(`https://test2.example.org/${path}?id=popup`);
+ }
+ );
+ await onSwitchedHost;
+
+ // Wait until the popup is rendered in the context selector
+ // and that it is automatically switched to (aria-checked==true).
+ await waitFor(() => {
+ try {
+ const items = getContextSelectorItems(hud);
+ return (
+ items.length === 3 &&
+ items.some(
+ el =>
+ el
+ .querySelector(".label")
+ ?.textContent.includes("popup|test2.example.org") &&
+ el.getAttribute("aria-checked") === "true"
+ )
+ );
+ } catch (e) {
+ // The context list may be wiped while updating and getContextSelectorItems will throw
+ }
+ return false;
+ });
+
+ const expectedPopupItem = {
+ label: `popup|test2.example.org`,
+ tooltip: `https://test2.example.org/${IFRAME_PATH}?id=popup`,
+ };
+
+ await checkContextSelectorMenu(hud, [
+ {
+ ...expectedTopItem,
+ checked: false,
+ },
+ expectedSeparatorItem,
+ {
+ ...expectedPopupItem,
+ checked: true,
+ },
+ ]);
+
+ await waitForEagerEvaluationResult(hud, `"test2.example.org"`);
+ ok(true, "The context was set to the popup document");
+
+ info("Open a second popup and reload the original tab");
+ onSwitchedHost = hud.toolbox.once("host-changed");
+ await ContentTask.spawn(
+ originalTab.linkedBrowser,
+ [IFRAME_PATH],
+ function (path) {
+ content.open(`https://test2.example.org/${path}?id=popup2`);
+ }
+ );
+ await onSwitchedHost;
+
+ // Reloading the tab while having two popups opened used to
+ // generate exception in the context selector component
+ await BrowserTestUtils.reloadTab(originalTab);
+
+ ok(
+ !hud.ui.document.querySelector(".app-error-panel"),
+ "The web console did not crash"
+ );
+});
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..89e4841af9
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_file_load_save_keyboard_shortcut.js
@@ -0,0 +1,87 @@
+/* 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,<!DOCTYPE html>Test load/save keyboard shortcut";
+
+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(() => IOUtils.exists(nsiFile.path));
+ const buffer = await IOUtils.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 writeInFile(string, file) {
+ const inputStream = getInputStream(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..5bafa3943d
--- /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,<!DOCTYPE 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 reloadBrowser();
+ 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..70c52f3848
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_helper_clear.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI =
+ "data:text/html,<!DOCTYPE html>Test <code>clear()</code> jsterm helper";
+
+add_task(async function () {
+ const hud = await openNewTabAndConsole(TEST_URI);
+
+ const onMessage = waitForMessageByType(hud, "message", ".console-api");
+ 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..88bedca17b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_helper_dollar.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = `data:text/html,<!DOCTYPE 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 executeAndWaitForResultMessage(
+ hud,
+ "$('main')",
+ "<main>"
+ );
+ ok(message, "`$('main')` worked");
+
+ message = await executeAndWaitForResultMessage(
+ hud,
+ "$('main > ul > li')",
+ "<li>"
+ );
+ ok(message, "`$('main > ul > li')` worked");
+
+ message = await executeAndWaitForResultMessage(
+ hud,
+ "$('main > ul > li').tagName",
+ "LI"
+ );
+ ok(message, "`$` result can be used right away");
+
+ message = await executeAndWaitForResultMessage(hud, "$('div')", "null");
+ ok(message, "`$('div')` does return null");
+
+ message = await executeAndWaitForErrorMessage(
+ hud,
+ "$(':foo')",
+ "':foo' is not a valid selector"
+ );
+ ok(message, "`$(':foo')` returns an error message");
+});
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..b38cd8b0c7
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_helper_dollar_dollar.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = `data:text/html,<!DOCTYPE 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 executeAndWaitForResultMessage(
+ hud,
+ "$$('main')",
+ "Array [ main ]"
+ );
+ ok(message, "`$$('main')` worked");
+
+ message = await executeAndWaitForResultMessage(
+ hud,
+ "$$('main > ul > li')",
+ "Array [ li, li ]"
+ );
+ ok(message, "`$$('main > ul > li')` worked");
+
+ message = await executeAndWaitForResultMessage(
+ hud,
+ "$$('main > ul > li').map(el => el.tagName).join(' - ')",
+ "LI - LI"
+ );
+ ok(message, "`$$` result can be used right away");
+
+ message = await executeAndWaitForResultMessage(hud, "$$('div')", "Array []");
+ ok(message, "`$$('div')` returns an empty array");
+
+ message = await executeAndWaitForErrorMessage(
+ hud,
+ "$$(':foo')",
+ "':foo' is not a valid selector"
+ );
+ ok(message, "`$$(':foo')` returns an error message");
+});
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..49480104d1
--- /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,<!DOCTYPE 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 executeAndWaitForResultMessage(
+ hud,
+ "$x('.//li')",
+ "Array [ li, li ]"
+ );
+ ok(message, "`$x` worked");
+
+ message = await executeAndWaitForResultMessage(
+ hud,
+ "$x('.//li', document.body)[0]",
+ "<li>"
+ );
+ ok(message, "`$x()` result can be used right away");
+
+ message = await executeAndWaitForResultMessage(
+ hud,
+ "$x('count(.//li)', document.body, XPathResult.NUMBER_TYPE)",
+ "2"
+ );
+ ok(message, "$x works as expected with XPathResult.NUMBER_TYPE");
+
+ message = await executeAndWaitForResultMessage(
+ hud,
+ "$x('count(.//li)', document.body, 'number')",
+ "2"
+ );
+ ok(message, "$x works as expected number type");
+
+ message = await executeAndWaitForResultMessage(
+ hud,
+ "$x('.//li', document.body, XPathResult.STRING_TYPE)",
+ "First"
+ );
+ ok(message, "$x works as expected with XPathResult.STRING_TYPE");
+
+ message = await executeAndWaitForResultMessage(
+ hud,
+ "$x('.//li', document.body, 'string')",
+ "First"
+ );
+ ok(message, "$x works as expected with string type");
+
+ message = await executeAndWaitForResultMessage(
+ hud,
+ "$x('//li[not(@foo)]', document.body, XPathResult.BOOLEAN_TYPE)",
+ "true"
+ );
+ ok(message, "$x works as expected with XPathResult.BOOLEAN_TYPE");
+
+ message = await executeAndWaitForResultMessage(
+ hud,
+ "$x('//li[not(@foo)]', document.body, 'bool')",
+ "true"
+ );
+ ok(message, "$x works as expected with bool type");
+
+ message = await executeAndWaitForResultMessage(
+ 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 executeAndWaitForResultMessage(
+ hud,
+ "$x('.//li', document.body, 'nodes')",
+ "Array [ li, li ]"
+ );
+ ok(message, "$x works as expected with nodes type");
+
+ message = await executeAndWaitForResultMessage(
+ 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 executeAndWaitForResultMessage(
+ 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 executeAndWaitForResultMessage(
+ 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 executeAndWaitForResultMessage(
+ hud,
+ "$x('.//li', document.body, 'node')",
+ "<li>"
+ );
+ ok(message, "$x works as expected with node type");
+
+ message = await executeAndWaitForResultMessage(
+ 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 executeAndWaitForResultMessage(
+ 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..e8835088c8
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_helper_help.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI =
+ "data:text/html,<!DOCTYPE html>Test <code>help()</code> jsterm helper";
+const HELP_URL =
+ "https://firefox-source-docs.mozilla.org/devtools-user/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 executeAndWaitForResultMessage(hud, "smoke", "");
+
+ 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..ed06c181ce
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_helper_keys_values.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI =
+ "data:text/html,<!DOCTYPE html>Test <code>keys()</code> & <code>values()</code> jsterm helper";
+
+add_task(async function () {
+ const hud = await openNewTabAndConsole(TEST_URI);
+
+ let message = await executeAndWaitForResultMessage(
+ hud,
+ "keys({a: 2, b:1})",
+ `Array [ "a", "b" ]`
+ );
+ ok(message, "`keys()` worked");
+
+ message = await executeAndWaitForResultMessage(
+ hud,
+ "values({a: 2, b:1})",
+ "Array [ 2, 1 ]"
+ );
+ ok(message, "`values()` worked");
+
+ message = await executeAndWaitForResultMessage(hud, "keys(window)", "Array");
+ 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..6f872400a3
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_hide_when_devtools_chrome_enabled_false.js
@@ -0,0 +1,161 @@
+/* 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);
+
+ // Enable Multiprocess Browser Console
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+
+ // 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,<!DOCTYPE html>hello world"
+ );
+ webConsole = await openConsole(browserTab);
+ objInspector = await logObject(webConsole);
+ testInputRelatedElementsAreVisibile(webConsole);
+ await testObjectInspectorPropertiesAreSet(objInspector);
+
+ 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");
+ await closeConsole(browserTab);
+ await safeCloseBrowserConsole();
+});
+
+async function logObject(hud) {
+ const prop = "browser_console_hide_jsterm_test";
+ const { node } = await executeAndWaitForResultMessage(
+ hud,
+ `new Object({ ${prop}: true })`,
+ prop
+ );
+ 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");
+}
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..b523f96c9d
--- /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,<!DOCTYPE html>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 executeAndWaitForResultMessage(hud, command, "");
+ }
+
+ 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..766ed36514
--- /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,<!DOCTYPE html>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 executeAndWaitForResultMessage(hud, value, "");
+ }
+
+ 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_command.js b/devtools/client/webconsole/test/browser/browser_jsterm_history_command.js
new file mode 100644
index 0000000000..979236a5a8
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_history_command.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests if the command history shows a table with the content we expected.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=UTF-8,<!DOCTYPE html>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 executeAndWaitForResultMessage(hud, command, "");
+ }
+
+ info(`Executing command :history`);
+ await executeAndWaitForMessageByType(hud, ":history", "", ".simpleTable");
+ const historyTableRows = hud.ui.outputNode.querySelectorAll(
+ ".message.simpleTable tbody tr"
+ );
+
+ const expectedCommands = [...COMMANDS, ":history"];
+
+ for (let i = 0; i < historyTableRows.length; i++) {
+ const cells = historyTableRows[i].querySelectorAll("td");
+
+ is(
+ cells[0].textContent,
+ String(i),
+ "Check the value of the column (index)"
+ );
+ is(
+ cells[1].textContent,
+ expectedCommands[i],
+ "Check if the value of the column Expressions is the value expected"
+ );
+ }
+});
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..0d9fd467f8
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_history_nav.js
@@ -0,0 +1,58 @@
+/* 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,<!DOCTYPE html><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 executeAndWaitForResultMessage(
+ hud,
+ `window.foobarBug660806 = {
+ 'location': 'value0',
+ 'locationbar': 'value1'
+ }`,
+ ""
+ );
+ 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 executeAndWaitForResultMessage(
+ hud,
+ "window.foobarBug660806.location",
+ ""
+ );
+
+ 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..a8eb2c4169
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_history_persist.js
@@ -0,0 +1,169 @@
+/* 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,<!DOCTYPE html>Web Console test for persisting history";
+const INPUT_HISTORY_COUNT = 10;
+
+const {
+ getHistoryEntries,
+} = require("resource://devtools/client/webconsole/selectors/history.js");
+
+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 executeAndWaitForResultMessage(
+ hud3,
+ '"hello from third tab"',
+ '"hello from third tab"'
+ );
+
+ 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 executeAndWaitForResultMessage(hud, input, input);
+ }
+}
+
+/**
+ * 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..6a5ab75a50
--- /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,<!DOCTYPE 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..07e5980ac7
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_inspect.js
@@ -0,0 +1,80 @@
+/* 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,<!DOCTYPE html><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 executeAndWaitForResultMessage(
+ hud,
+ "testProp = 'testValue'",
+ "testValue"
+ );
+ const { node: inspectWindowNode } = await executeAndWaitForResultMessage(
+ hud,
+ "inspect(window)",
+ "Window"
+ );
+
+ 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..a3520e05d0
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_inspect_panels.js
@@ -0,0 +1,101 @@
+/* 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", 3, true));
+ 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,
+ isOriginalSource
+ ) {
+ return () => {
+ const dbg = hud.toolbox.getPanel("jsdebugger");
+ if (!dbg) {
+ return false;
+ }
+
+ const selectedLocation = dbg._selectors.getSelectedLocation(
+ dbg._getState()
+ );
+
+ if (!selectedLocation) {
+ return false;
+ }
+
+ if (
+ isOriginalSource &&
+ !selectedLocation.sourceId.includes("/originalSource-")
+ ) {
+ 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..1ec14a421c
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_instance_of.js
@@ -0,0 +1,33 @@
+/* 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,<!DOCTYPE html>Test <code>instanceof</code> evaluation";
+
+add_task(async function () {
+ const hud = await openNewTabAndConsole(TEST_URI);
+
+ let message = await executeAndWaitForResultMessage(
+ hud,
+ "[] instanceof Array",
+ "true"
+ );
+ ok(message, "`instanceof Array` is correct");
+
+ message = await executeAndWaitForResultMessage(
+ hud,
+ "({}) instanceof Object",
+ "true"
+ );
+ ok(message, "`instanceof Object` is correct");
+
+ message = await executeAndWaitForResultMessage(
+ hud,
+ "({}) instanceof Array",
+ "false"
+ );
+ 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..7dbcec34e9
--- /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,<!DOCTYPE html>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..cfd103f5e5
--- /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 = waitForMessageByType(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..cb22502145
--- /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,<!DOCTYPE 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..957495e399
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_null_undefined.js
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI =
+ "data:text/html,<!DOCTYPE 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 executeAndWaitForResultMessage(hud, "null", "null");
+ ok(message, "`null` returned the expected value");
+
+ message = await executeAndWaitForResultMessage(hud, "undefined", "undefined");
+ 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..4b654494bb
--- /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,<!DOCTYPE html><p>bug 900448 - autocomplete " +
+ "popup closes on tab switch";
+const TEST_URI_NAVIGATE =
+ "data:text/html;charset=utf-8,<!DOCTYPE html><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..7bed7a39f3
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_clipboard.js
@@ -0,0 +1,182 @@
+/* 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);
+ 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) {
+ info("Test taking a fullpage image that overflows");
+ await createScrollbarOverflow();
+
+ const command = `:screenshot --fullpage --clipboard ${dpr}`;
+ await executeScreenshotClipboardCommand(hud, command);
+ const contentSize = await getContentSize();
+ const imgSize = await getImageSizeFromClipboard();
+
+ const scrollbarSize = await getScrollbarSize();
+ 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 executeAndWaitForMessageByType(
+ hud,
+ command,
+ "Screenshot copied to clipboard.",
+ ".console-api"
+ );
+}
+
+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");
+ return content.windowUtils.flushLayoutWithoutThrottledAnimations();
+ });
+
+ // Let's wait for next tick so scrollbars have the time to be rendered
+ await waitForTick();
+}
+
+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..6401e3c16e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_file.js
@@ -0,0 +1,128 @@
+/* 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);
+
+ info("wait for the iframes to be loaded");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.querySelectorAll(".loaded-iframe").length == 2
+ );
+ });
+
+ info("Test :screenshot to file");
+ const file = FileUtils.getFile("TmpD", ["TestScreenshotFile.png"]);
+ const command = `:screenshot ${file.path} ${dpr}`;
+ await executeAndWaitForMessageByType(
+ hud,
+ command,
+ `Saved to ${file.path}`,
+ ".console-api"
+ );
+
+ const fileExists = file.exists();
+ if (!fileExists) {
+ throw new Error(`${file.path} does not exist`);
+ }
+
+ ok(fileExists, `Screenshot was saved to ${file.path}`);
+
+ info("Create an image using the downloaded file as source");
+ const image = new Image();
+ image.src = PathUtils.toFileURI(file.path);
+ await once(image, "load");
+
+ // The page has the following structure
+ // +--------------------------------------------------+
+ // | Fixed header [50px tall, red] |
+ // +--------------------------------------------------+
+ // | Same-origin iframe [50px tall, rgb(255, 255, 0)] |
+ // +--------------------------------------------------+
+ // | Remote iframe [50px tall, rgb(0, 255, 255)] |
+ // +--------------------------------------------------+
+ // | Image |
+ // | 100px |
+ // | |
+ // +---------+
+
+ info("Check that the header is rendered in the screenshot");
+ checkImageColorAt({
+ image,
+ y: 0,
+ expectedColor: `rgb(255, 0, 0)`,
+ label:
+ "The top-left corner has the expected red color, matching the header element",
+ });
+
+ info("Check that the same-origin iframe is rendered in the screenshot");
+ checkImageColorAt({
+ image,
+ y: 60,
+ expectedColor: `rgb(255, 255, 0)`,
+ label: "The same-origin iframe is rendered properly in the screenshot",
+ });
+
+ info("Check that the remote iframe is rendered in the screenshot");
+ checkImageColorAt({
+ image,
+ y: 110,
+ expectedColor: `rgb(0, 255, 255)`,
+ label: "The remote iframe is rendered properly in the screenshot",
+ });
+
+ info("Test :screenshot to file default filename");
+ const message = await executeAndWaitForMessageByType(
+ hud,
+ `:screenshot ${dpr}`,
+ `Saved to`,
+ ".console-api"
+ );
+ const date = new Date();
+ const monthString = (date.getMonth() + 1).toString().padStart(2, "0");
+ const dayString = date.getDate().toString().padStart(2, "0");
+ const expectedDateString = `${date.getFullYear()}-${monthString}-${dayString}`;
+
+ let screenshotDir;
+ try {
+ // This will throw if there is not a screenshot directory set for the platform
+ screenshotDir = Services.dirsvc.get("Scrnshts", Ci.nsIFile).path;
+ } catch (e) {
+ const { Downloads } = ChromeUtils.importESModule(
+ "resource://gre/modules/Downloads.sys.mjs"
+ );
+ screenshotDir = await Downloads.getPreferredDownloadsDirectory();
+ }
+
+ const { renderedDate, filePath } =
+ /Saved to (?<filePath>.*Screen Shot (?<renderedDate>\d{4}-\d{2}-\d{2}) at \d{2}.\d{2}.\d{2}\.png)/.exec(
+ message.node.textContent
+ ).groups;
+ is(
+ renderedDate,
+ expectedDateString,
+ `Screenshot file has expected default name (full message: ${message.node.textContent})`
+ );
+ is(
+ filePath.startsWith(screenshotDir),
+ true,
+ `Screenshot file is saved in default directory`
+ );
+
+ info("Remove the downloaded screenshot files and cleanup downloads");
+ await IOUtils.remove(file.path);
+ await IOUtils.remove(filePath);
+ await resetDownloads();
+});
diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_fixed_header.js b/devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_fixed_header.js
new file mode 100644
index 0000000000..ce1f667b01
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_fixed_header.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that fullpage screenshot command works properly with fixed elements
+
+"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);
+
+ info("Scroll in the content page");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ // Overflow the page
+ content.document.body.classList.add("overflow");
+ content.wrappedJSObject.scrollTo(200, 350);
+ });
+
+ info("Execute :screenshot --fullpage");
+ const file = FileUtils.getFile("TmpD", ["TestScreenshotFile.png"]);
+ const command = `:screenshot ${file.path} ${dpr} --fullpage`;
+ // `-fullpage` is appended at the end of the provided filename
+ const actualFilePath = file.path.replace(".png", "-fullpage.png");
+ await executeAndWaitForMessageByType(
+ hud,
+ command,
+ `Saved to ${file.path.replace(".png", "-fullpage.png")}`,
+ ".console-api"
+ );
+
+ info("Create an image using the downloaded file as source");
+ const image = new Image();
+ image.src = PathUtils.toFileURI(actualFilePath);
+ await once(image, "load");
+
+ info("Check that the fixed element is rendered at the expected position");
+ checkImageColorAt({
+ image,
+ x: 0,
+ y: 0,
+ expectedColor: `rgb(255, 0, 0)`,
+ label:
+ "The top-left corner has the expected red color, matching the header element",
+ });
+
+ const scrollPosition = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async () => {
+ return [content.wrappedJSObject.scrollX, content.wrappedJSObject.scrollY];
+ }
+ );
+ is(
+ scrollPosition.join("|"),
+ "200|350",
+ "The page still has the same scroll positions as before taking the screenshot"
+ );
+
+ info("Remove the downloaded screenshot file and cleanup downloads");
+ await IOUtils.remove(actualFilePath);
+ await resetDownloads();
+});
diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_selector.js b/devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_selector.js
new file mode 100644
index 0000000000..9e2a545014
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_selector.js
@@ -0,0 +1,142 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that screenshot command works properly with the --selector arg
+
+"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 () {
+ await pushPref("devtools.webconsole.input.context", true);
+
+ const hud = await openNewTabAndConsole(TEST_URI);
+
+ info("wait for the iframes to be loaded");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.querySelectorAll(".loaded-iframe").length == 2
+ );
+ });
+
+ info("Test :screenshot --selector iframe");
+ const sameOriginIframeScreenshotFile = FileUtils.getFile("TmpD", [
+ "TestScreenshotFile-same-origin-iframe.png",
+ ]);
+ await executeAndWaitForMessageByType(
+ hud,
+ `:screenshot --selector #same-origin-iframe ${sameOriginIframeScreenshotFile.path} ${dpr}`,
+ `Saved to ${sameOriginIframeScreenshotFile.path}`,
+ ".console-api"
+ );
+
+ let fileExists = sameOriginIframeScreenshotFile.exists();
+ if (!fileExists) {
+ throw new Error(`${sameOriginIframeScreenshotFile.path} does not exist`);
+ }
+
+ ok(
+ fileExists,
+ `Screenshot was saved to ${sameOriginIframeScreenshotFile.path}`
+ );
+
+ info("Create an image using the downloaded file as source");
+ let image = new Image();
+ image.src = PathUtils.toFileURI(sameOriginIframeScreenshotFile.path);
+ await once(image, "load");
+
+ info("Check that the node was rendered as expected in the screenshot");
+ checkImageColorAt({
+ image,
+ y: 0,
+ expectedColor: `rgb(255, 255, 0)`,
+ label:
+ "The top-left corner has the expected color, matching the same-origin iframe",
+ });
+
+ // Remove the downloaded screenshot file and cleanup downloads
+ await IOUtils.remove(sameOriginIframeScreenshotFile.path);
+ await resetDownloads();
+
+ info("Check using :screenshot --selector in a remote-iframe context");
+ // Select the remote iframe in the context selector
+ const evaluationContextSelectorButton = hud.ui.outputNode.querySelector(
+ ".webconsole-evaluation-selector-button"
+ );
+
+ if (!isFissionEnabled() && !isEveryFrameTargetEnabled()) {
+ is(
+ evaluationContextSelectorButton,
+ null,
+ "context selector is only displayed when Fission or EFT is enabled"
+ );
+ return;
+ }
+
+ const remoteIframeUrl = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async () => {
+ return content.document.querySelector("#remote-iframe").src;
+ }
+ );
+ selectTargetInContextSelector(hud, remoteIframeUrl);
+ await waitFor(() =>
+ evaluationContextSelectorButton.innerText.includes("example.org")
+ );
+
+ const remoteIframeSpanScreenshot = FileUtils.getFile("TmpD", [
+ "TestScreenshotFile-remote-iframe.png",
+ ]);
+ await executeAndWaitForMessageByType(
+ hud,
+ `:screenshot --selector span ${remoteIframeSpanScreenshot.path} ${dpr}`,
+ `Saved to ${remoteIframeSpanScreenshot.path}`,
+ ".console-api"
+ );
+
+ fileExists = remoteIframeSpanScreenshot.exists();
+ if (!fileExists) {
+ throw new Error(`${remoteIframeSpanScreenshot.path} does not exist`);
+ }
+
+ ok(fileExists, `Screenshot was saved to ${remoteIframeSpanScreenshot.path}`);
+
+ info("Create an image using the downloaded file as source");
+ image = new Image();
+ image.src = PathUtils.toFileURI(remoteIframeSpanScreenshot.path);
+ await once(image, "load");
+
+ info("Check that the node was rendered as expected in the screenshot");
+ checkImageColorAt({
+ image,
+ y: 0,
+ expectedColor: `rgb(0, 100, 0)`,
+ label:
+ "The top-left corner has the expected color, matching the span inside the iframe",
+ });
+
+ info(
+ "Check that using a selector that doesn't match any element displays a warning in console"
+ );
+ await executeAndWaitForMessageByType(
+ hud,
+ `:screenshot --selector #this-element-does-not-exist`,
+ `The ‘#this-element-does-not-exist’ selector does not match any element on the page.`,
+ ".warn"
+ );
+ ok(
+ true,
+ "A warning message is emitted when the passed selector doesn't match any element"
+ );
+
+ // Remove the downloaded screenshot file and cleanup downloads
+ await IOUtils.remove(remoteIframeSpanScreenshot.path);
+ await resetDownloads();
+});
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..2fcf4248a1
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_user.js
@@ -0,0 +1,70 @@
+/* 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,<!DOCTYPE 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 executeAndWaitForMessageByType(
+ hud,
+ command,
+ "Screenshot copied to clipboard.",
+ ".console-api"
+ );
+ ok(true, ":screenshot was executed as expected");
+
+ const helpMessage = await executeAndWaitForMessageByType(
+ hud,
+ `:screenshot --help`,
+ "Save an image of the page",
+ ".console-api"
+ );
+ 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 executeAndWaitForMessageByType(
+ hud,
+ command,
+ "contextScreen",
+ ".console-api"
+ );
+ 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..7c4b2252b1
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_warnings.js
@@ -0,0 +1,98 @@
+/* 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,<!DOCTYPE html>
+ <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 = waitForMessagesByType({
+ hud,
+ messages: [
+ {
+ text: "Screenshot copied to clipboard.",
+ typeSelector: ".console-api",
+ },
+ {
+ text: "The image was cut off to 10000×10000 as the resulting image was too large",
+ typeSelector: ".console-api",
+ },
+ ],
+ });
+ // 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 = waitForMessageByType(
+ hud,
+ "Screenshot copied to clipboard.",
+ ".console-api"
+ );
+ 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 = waitForMessagesByType({
+ hud,
+ messages: [
+ {
+ text: "Screenshot copied to clipboard.",
+ typeSelector: ".console-api",
+ },
+ {
+ text: "The image was cut off to 10000×10000 as the resulting image was too large",
+ typeSelector: ".console-api",
+ },
+ {
+ text: "The device pixel ratio was reduced to 1 as the resulting image was too large",
+ typeSelector: ".console-api",
+ },
+ ],
+ });
+ 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..f35b0e624d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_selfxss.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 =
+ "data:text/html;charset=utf-8,<!DOCTYPE html><p>Test self-XSS protection</p>";
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "clipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1",
+ "nsIClipboardHelper"
+);
+const WebConsoleUtils =
+ require("resource://devtools/client/webconsole/utils.js").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 executeAndWaitForResultMessage(hud, i.toString(), i);
+ }
+
+ 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..62b8b11613
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_syntax_highlight_output.js
@@ -0,0 +1,27 @@
+/* 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,<!DOCTYPE html>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 = waitForMessageByType(hud, `var a = 'str';`, ".command");
+ 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..47f156f8c4
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_toolbox_console_new_process.js
@@ -0,0 +1,69 @@
+/* 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,<!DOCTYPE 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 */
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/framework/browser-toolbox/test/helpers-browser-toolbox.js",
+ this
+);
+
+add_task(async function () {
+ // Needed for the invokeInTab() function below
+ await pushPref("security.allow_parent_unrestricted_js_loads", true);
+
+ await addTab(TEST_URI);
+ const ToolboxTask = await initBrowserToolboxTask();
+ await ToolboxTask.importFunctions({
+ findMessagesVirtualized,
+ findMessageVirtualizedByType,
+ 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(() =>
+ findMessageVirtualizedByType({
+ hud,
+ text: "Data Message",
+ typeSelector: ".console-api",
+ })
+ );
+ });
+ 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(() =>
+ findMessageVirtualizedByType({
+ hud,
+ text: "stringLog",
+ typeSelector: ".console-api",
+ })
+ );
+ });
+ 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..fa907a6748
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_allow_mixedcontent_securityerrors.js
@@ -0,0 +1,64 @@
+/* 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(() => findWarningMessage(hud, text), 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..c8c638facd
--- /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,<!DOCTYPE html><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(
+ () => findConsoleAPIMessage(hud, "Trace message", ".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(
+ () => findConsoleAPIMessage(hud, "console error message", ".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(
+ () =>
+ findErrorMessage(
+ hud,
+ "Uncaught Error: Thrown error message",
+ ".javascript"
+ ),
+ "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 ? 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..8c49a003a0
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_batching.js
@@ -0,0 +1,62 @@
+/* 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("resource://devtools/client/webconsole/utils/messages.js");
+
+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);
+ }
+ );
+ const allMessages = await waitFor(async () => {
+ const msgs = await findAllMessagesVirtualized(hud);
+ if (msgs.length == messageNumber) {
+ return msgs;
+ }
+ return null;
+ });
+ for (let i = 0; i < messageNumber; i++) {
+ const node = allMessages[i].querySelector(".message-body");
+ 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(() =>
+ findConsoleAPIMessage(hud, l10n.getStr("consoleCleared"))
+ );
+ ok(true, "console cleared message is displayed");
+
+ const messages = findAllMessages(hud);
+ is(messages.length, 1, "console was cleared as expected");
+}
diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_bidi_string_isolation.js b/devtools/client/webconsole/test/browser/browser_webconsole_bidi_string_isolation.js
new file mode 100644
index 0000000000..8884bba2d6
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_bidi_string_isolation.js
@@ -0,0 +1,96 @@
+/* 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,<!DOCTYPE html>Bidi strings";
+const rtlOverride = "\u202e";
+
+add_task(async function () {
+ const hud = await openNewTabAndConsole(TEST_URI);
+ const browser = gBrowser.selectedBrowser;
+
+ /* eslint-disable-next-line no-shadow */
+ await SpecialPowers.spawn(browser, [rtlOverride], rtlOverride => {
+ const { console } = content.wrappedJSObject;
+
+ console.log(Symbol(rtlOverride + "msg01"));
+ console.log([rtlOverride + "msg02"]);
+ console.log({ p: rtlOverride + "msg03" });
+ console.log({ [rtlOverride + "msg04"]: null });
+ console.log(new Set([rtlOverride + "msg05"]));
+ console.log(new Map([[rtlOverride + "msg06", null]]));
+ console.log(new Map([[null, rtlOverride + "msg07"]]));
+
+ const parser = content.document.createElement("div");
+ // eslint-disable-next-line no-unsanitized/property
+ parser.innerHTML = `
+ <div data-test="${rtlOverride}msg08"></div>
+ <div data-${rtlOverride}="msg09"></div>
+ <div-${rtlOverride} msg10></div-${rtlOverride}>
+ `;
+ for (const child of parser.children) {
+ console.log(child);
+ }
+ });
+
+ const texts = [
+ `Symbol("${rtlOverride}msg01")`,
+ `Array [ "${rtlOverride}msg02" ]`,
+ `Object { p: "${rtlOverride}msg03" }`,
+ `Object { "${rtlOverride}msg04": null }`,
+ `Set [ "${rtlOverride}msg05" ]`,
+ `Map { "${rtlOverride}msg06" → null }`,
+ `Map { null → "${rtlOverride}msg07" }`,
+ `<div data-test="${rtlOverride}msg08">`,
+ `<div data-${rtlOverride}="msg09">`,
+ `<div-${rtlOverride} msg10="">`,
+ ];
+ for (let i = 0; i < texts.length; ++i) {
+ const msgId = "msg" + String(i + 1).padStart(2, "0");
+ const message = await waitFor(() => findConsoleAPIMessage(hud, msgId));
+ const objectBox = message.querySelector(".objectBox");
+ is(objectBox.textContent, texts[i], "Should have all the relevant text");
+ checkRects(objectBox);
+ }
+});
+
+function getBoundingClientRect(node) {
+ if (node.nodeType === Node.ELEMENT_NODE) {
+ return node.getBoundingClientRect();
+ }
+ // There is no Node.getBoundingClientRect, use a Range instead.
+ const range = document.createRange();
+ range.selectNode(node);
+ return range.getBoundingClientRect();
+}
+
+/**
+ * The console prints data build from external strings. They can contain
+ * characters that change the directionality of the text. For example, RTL
+ * characters will flow right to left. However, this should be isolated to
+ * prevent one string from mangling how another one is rendered.
+ * This function uses getBoundingClientRect() to check that the nodes, as a
+ * whole, flow LTR (even if the characters in the node flow RTL).
+ * The bidi algorithm happens at layout time, so we need to check the rects,
+ * DOM operations like textContent would be useless.
+ */
+function checkRects(node, parentRect = getBoundingClientRect(node)) {
+ let prevRect;
+ for (const child of node.childNodes) {
+ const rect = getBoundingClientRect(child);
+ ok(rect.x >= parentRect.x, "Rect should start inside parent");
+ ok(
+ rect.x + rect.width <= parentRect.x + parentRect.width,
+ "Rect should end inside parent"
+ );
+ if (prevRect) {
+ ok(
+ rect.x >= prevRect.x + prevRect.width,
+ "Rect should start after previous one"
+ );
+ }
+ prevRect = rect;
+ checkRects(child, rect);
+ }
+}
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..2dcffdfcf3
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_block_mixedcontent_securityerrors.js
@@ -0,0 +1,106 @@
+/* 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(() => findErrorMessage(hud, text), 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(() => findWarningMessage(hud, text), 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}`
+ );
+
+ gIdentityHandler.enableMixedContentProtectionNoReload();
+});
+
+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..5909d2e824
--- /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,<!DOCTYPE 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(() => findConsoleAPIMessage(hud, "info Bazzle", ".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) {
+ 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(
+ () => findWarningMessage(hud, "cssColorBug611032", ".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..157d5825ef
--- /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_duplicate_after_target_switching.js b/devtools/client/webconsole/test/browser/browser_webconsole_cached_messages_duplicate_after_target_switching.js
new file mode 100644
index 0000000000..9848baf3a5
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_cached_messages_duplicate_after_target_switching.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_ORG = `https://example.org/document-builder.sjs?html=<meta charset=utf8></meta>
+<script>
+ console.log("early message on org page");
+</script><body>`;
+const TEST_URI_COM = TEST_URI_ORG.replace(/org/g, "com");
+
+add_task(async function () {
+ info("Add a tab and open the console");
+ const tab = await addTab("about:robots");
+ const hud = await openConsole(tab);
+
+ {
+ await navigateTo(TEST_URI_ORG);
+
+ // Wait for some time in order to let a chance to have duplicated message
+ // and catch such regression
+ await wait(1000);
+
+ info("wait until the ORG message is displayed");
+ await checkUniqueMessageExists(
+ hud,
+ "early message on org page",
+ ".console-api"
+ );
+ }
+
+ {
+ await navigateTo(TEST_URI_COM);
+
+ // Wait for some time in order to let a chance to have duplicated message
+ // and catch such regression
+ await wait(1000);
+
+ info("wait until the COM message is displayed");
+ await checkUniqueMessageExists(
+ hud,
+ "early message on com page",
+ ".console-api"
+ );
+ }
+});
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..b7f8ee431b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_cached_messages_no_duplicate.js
@@ -0,0 +1,39 @@
+/* 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,<!DOCTYPE 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(
+ () =>
+ findConsoleAPIMessage(hud, "message 1") &&
+ findConsoleAPIMessage(hud, "message 50")
+ );
+
+ is(
+ (await findAllMessagesVirtualized(hud)).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..05c060b8e2
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_certificate_messages.js
@@ -0,0 +1,42 @@
+/* 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,<!DOCTYPE html>Web Console weak crypto warnings test";
+const TEST_URI_PATH =
+ "/browser/devtools/client/webconsole/test/" +
+ "browser/test-certificate-messages.html";
+
+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 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);
+ const onContentLog = waitForMessageByType(hud, TRIGGER_MSG, ".console-api");
+ await navigateTo(TLS_1_0_URL);
+ await onContentLog;
+
+ const 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..19b48ac18e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_checkloaduri_errors.js
@@ -0,0 +1,37 @@
+/* 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.
+
+// XPCNativeWrapper is not defined globally in ESLint as it may be going away.
+// See bug 1481337.
+/* global XPCNativeWrapper */
+
+"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 = waitForMessageByType(
+ hud,
+ "may not load or link",
+ ".error"
+ );
+ 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..114c82923b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_clear_cache.js
@@ -0,0 +1,86 @@
+/* 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,<!DOCTYPE html>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(() => findErrorMessage(hud, EXPECTED_REPORT));
+ await waitFor(() => findConsoleAPIMessage(hud, CACHED_MESSAGE));
+
+ info(
+ "Click the clear output button and wait until there's no messages in the output"
+ );
+ let onMessagesCacheCleared = hud.ui.once("messages-cache-cleared");
+ hud.ui.window.document.querySelector(".devtools-clear-icon").click();
+ await onMessagesCacheCleared;
+
+ 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(
+ findConsoleAPIMessage(hud, CACHED_MESSAGE),
+ undefined,
+ "The cached message is not visible anymore"
+ );
+ is(
+ findErrorMessage(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");
+ onMessagesCacheCleared = hud.ui.once("messages-cache-cleared");
+ const onConsoleCleared = waitForMessageByType(
+ hud,
+ "Console was cleared",
+ ".console-api"
+ );
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.wrappedJSObject.console.clear();
+ });
+ await Promise.all([onConsoleCleared, onMessagesCacheCleared]);
+
+ 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(
+ findConsoleAPIMessage(hud, NEW_CACHED_MESSAGE),
+ undefined,
+ "The new cached message is not visible anymore"
+ );
+});
+
+function logTextToConsole(hud, text) {
+ const onMessage = waitForMessageByType(hud, text, ".console-api");
+ 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..14ed0b6fcd
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_click_function_to_mapped_source.js
@@ -0,0 +1,58 @@
+/* -*- 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 = waitForMessageByType(
+ hud,
+ "function foo",
+ ".console-api"
+ );
+ 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..0389cd657a
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_click_function_to_prettyprinted_source.js
@@ -0,0 +1,63 @@
+/* -*- 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 = waitForMessageByType(
+ hud,
+ "function foo",
+ ".console-api"
+ );
+ 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..9227e3b209
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_click_function_to_source.js
@@ -0,0 +1,50 @@
+/* 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 = waitForMessageByType(
+ hud,
+ "function foo",
+ ".console-api"
+ );
+ 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..7d7ef290b8
--- /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,<!DOCTYPE html>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(() => findConsoleAPIMessage(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";
+ 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 = waitForMessageByType(hud, "Visit", ".console-api");
+ 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,
+ });
+ 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 = waitForMessageByType(hud, "smoke", ".console-api");
+ 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..b0ce7c3206
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_close_groups_after_navigation.js
@@ -0,0 +1,33 @@
+/* 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,<!DOCTYPE html><script>console.group('hello')</script>`;
+
+add_task(async function () {
+ // Enable persist logs
+ await pushPref("devtools.webconsole.persistlog", true);
+
+ info(
+ "Open the console and wait for the console.group message to be rendered"
+ );
+ const hud = await openNewTabAndConsole(TEST_URI);
+ await waitFor(() => findConsoleAPIMessage(hud, "hello", ".startGroup"));
+
+ info("Refresh tab several times and check for correct message indentation");
+ for (let i = 0; i < 5; i++) {
+ await reloadBrowserAndCheckIndent(hud);
+ }
+});
+
+async function reloadBrowserAndCheckIndent(hud) {
+ const onMessage = waitForMessageByType(hud, "hello", ".startGroup");
+ await reloadBrowser();
+ const { node } = await onMessage;
+
+ is(
+ node.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..559bb94696
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_close_sidebar.js
@@ -0,0 +1,100 @@
+/* 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,<!DOCTYPE html>";
+
+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(() => !findAllMessages(hud).length);
+ 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 = waitForMessageByType(
+ hud,
+ "Console was cleared",
+ ".console-api"
+ );
+ 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(() => !findAllMessages(hud).length);
+ 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 = waitForMessageByType(hud, "Object", ".console-api");
+ 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..097e58b77f
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_close_unfocused_window.js
@@ -0,0 +1,40 @@
+/* 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 gDevTools.closeToolboxForTab(tab1);
+ await gDevTools.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");
+});
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..58f9a1755b
--- /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..9670312e85
--- /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(() => findConsoleAPIMessage(hud, loggedString));
+ ok(true, "The initial message is displayed in the console");
+ // Create a promise for the message logged after the reload.
+ const onMessage = waitForMessageByType(hud, loggedString, ".console-api");
+ 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..d221988dc1
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_console_dir.js
@@ -0,0 +1,135 @@
+/* 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,<!DOCTYPE html><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..a55d0886f3
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_console_dir_uninspectable.js
@@ -0,0 +1,53 @@
+/* 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,<!DOCTYPE html>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 executeAndWaitForMessageByType(
+ hud,
+ `console.log("${FIRST_LOG_MESSAGE}")`,
+ FIRST_LOG_MESSAGE,
+ ".console-api"
+ );
+
+ info("console.dir on an uninspectable object");
+ await executeAndWaitForMessageByType(
+ hud,
+ "console.dir(Object.create(null))",
+ "Object { }",
+ ".console-api"
+ );
+
+ info("Logging a second message to make sure the console is not broken");
+ const onLogMessage = waitForMessageByType(
+ hud,
+ SECOND_LOG_MESSAGE,
+ ".console-api"
+ );
+ // 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..56a797115d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_console_error_expand_object.js
@@ -0,0 +1,33 @@
+/* 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,<!DOCTYPE html><h1>test console.error with objects</h1>";
+
+add_task(async function () {
+ const hud = await openNewTabAndConsole(TEST_URI);
+
+ const onMessagesLogged = waitForMessageByType(
+ hud,
+ "myError",
+ ".console-api.error"
+ );
+ 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..e1cfa436ae
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_console_group.js
@@ -0,0 +1,162 @@
+/* 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("resource://devtools/client/webconsole/components/Output/MessageIndent.js");
+
+add_task(async function () {
+ const hud = await openNewTabAndConsole(TEST_URI);
+ const store = hud.ui.wrapper.getStore();
+ logAllStoreChanges(hud);
+
+ const onMessagesLogged = waitForMessageByType(hud, "log-6", ".console-api");
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ content.wrappedJSObject.doLog();
+ });
+ await onMessagesLogged;
+
+ info("Test a group at root level");
+ let node = findConsoleAPIMessage(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 = findConsoleAPIMessage(hud, "log-1");
+ testClass(node, "log");
+ testIndent(node, 1);
+
+ info("Test a group in a 1 level deep group");
+ node = findConsoleAPIMessage(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 = findConsoleAPIMessage(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 = findConsoleAPIMessage(hud, "log-3");
+ testClass(node, "log");
+ testIndent(node, 1);
+
+ info("Test a message at root level, after closing all the groups");
+ node = findConsoleAPIMessage(hud, "log-4");
+ testClass(node, "log");
+ testIndent(node, 0);
+
+ info("Test a collapsed group at root level");
+ node = findConsoleAPIMessage(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 = findConsoleAPIMessage(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) {
+ if (indent == 0) {
+ is(
+ node.querySelector(".indent"),
+ null,
+ "message doesn't have any indentation"
+ );
+ return;
+ }
+
+ 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..c60a7a6303
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_console_group_open_no_scroll.js
@@ -0,0 +1,67 @@
+/* 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,<!DOCTYPE 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(() => findConsoleAPIMessage(hud, "log-0"));
+ const groupMessage = await waitFor(() => findConsoleAPIMessage(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(() => findConsoleAPIMessage(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;
+ await new Promise(r =>
+ window.requestAnimationFrame(() => TestUtils.executeSoon(r))
+ );
+
+ 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 = waitForMessageByType(hud, "new-message", ".console-api");
+ 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..79ae3b5971
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_console_logging_workers_api.js
@@ -0,0 +1,130 @@
+/* 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) {
+ await addTab(TEST_URI);
+ // Open the debugger first as it can cause some message to be duplicated (See Bug 1778852)
+ await openDebugger();
+
+ info("Open the console");
+ const hud = await openConsole();
+
+ const cachedMessage = await waitFor(() =>
+ findConsoleAPIMessage(hud, "initial-message-from-worker")
+ );
+ is(
+ findConsoleAPIMessages(hud, "initial-message-from-worker").length,
+ 1,
+ "We get a single 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(() =>
+ findConsoleAPIMessage(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(() =>
+ findConsoleAPIMessage(hud, 'Symbol("logged-symbol-from-worker")')
+ );
+ ok(symbolMessage, "Symbol logged from worker is visible in the console");
+ }
+
+ info("Click on the clear button and wait for messages to be removed");
+ const onMessagesCacheCleared = hud.ui.once("messages-cache-cleared");
+ hud.ui.window.document.querySelector(".devtools-clear-icon").click();
+ await waitFor(
+ () =>
+ !findConsoleAPIMessage(hud, "initial-message-from-worker") &&
+ !findConsoleAPIMessage(hud, "log-from-worker")
+ );
+ await onMessagesCacheCleared;
+ ok(true, "Messages were removed");
+
+ info("Close and reopen the console to check messages were cleared properly");
+ await closeConsole();
+ const toolbox = await openToolboxForTab(gBrowser.selectedTab, "webconsole");
+ const newHud = toolbox.getCurrentPanel().hud;
+
+ info(
+ "Log a message and wait for it to appear so older messages would have been displayed"
+ );
+ const onSmokeMessage = waitForMessageByType(newHud, "smoke", ".console-api");
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.wrappedJSObject.console.log("smoke");
+ });
+ await onSmokeMessage;
+
+ is(
+ findConsoleAPIMessage(newHud, "initial-message-from-worker"),
+ undefined,
+ "Message cache was cleared"
+ );
+ is(
+ findConsoleAPIMessage(newHud, "log-from-worker"),
+ undefined,
+ "Live message were cleared as well"
+ );
+ await closeTabAndToolbox();
+}
diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_console_profile_unavailable.js b/devtools/client/webconsole/test/browser/browser_webconsole_console_profile_unavailable.js
new file mode 100644
index 0000000000..c40e1b9cac
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_console_profile_unavailable.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check console.profile() shows a warning with the new performance panel.
+const TEST_URI =
+ "data:text/html;charset=utf8,<!DOCTYPE html><h1>test console.profile</h1>";
+
+const EXPECTED_WARNING =
+ "console.profile is not compatible with the new Performance recorder";
+
+add_task(async function consoleProfileWarningWithNewPerfPanel() {
+ const hud = await openNewTabAndConsole(TEST_URI);
+
+ info("Use console.profile in the content page");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ content.wrappedJSObject.console.profile();
+ });
+
+ await waitFor(
+ () => findWarningMessage(hud, EXPECTED_WARNING),
+ "Wait until the warning about console.profile is displayed"
+ );
+ ok(true, "The expected warning was displayed.");
+});
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..5b78a1a4e9
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_console_table.js
@@ -0,0 +1,502 @@
+/* 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 }"]],
+ },
+ async additionalTest(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 messages = await waitFor(async () => {
+ const msgs = await findAllMessagesVirtualized(hud);
+ if (msgs.length === testCases.length) {
+ return msgs;
+ }
+ return null;
+ });
+ for (const [index, testCase] of testCases.entries()) {
+ // Refresh the reference to the message, as it may have been scrolled out of existence.
+ const node = await findMessageVirtualizedById({
+ hud,
+ messageId: messages[index].getAttribute("data-message-id"),
+ });
+ await testItem(testCase, node.querySelector(".consoletable"));
+ }
+});
+
+async function testItem(testCase, tableNode) {
+ info(testCase.info);
+
+ const ths = Array.from(tableNode.querySelectorAll("th"));
+ const trs = Array.from(tableNode.querySelectorAll("tbody tr"));
+
+ is(
+ JSON.stringify(ths.map(column => column.textContent)),
+ JSON.stringify(testCase.expected.columns),
+ `${testCase.info} | table has the expected columns`
+ );
+
+ is(
+ trs.length,
+ testCase.expected.rows.length,
+ `${testCase.info} | table has the expected number of rows`
+ );
+
+ testCase.expected.rows.forEach((expectedRow, rowIndex) => {
+ const rowCells = Array.from(trs[rowIndex].querySelectorAll("td")).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(
+ tableNode.isConnected,
+ "Node must be connected to test overflow. It is likely scrolled out of view."
+ );
+ const tableWrapperNode = tableNode.closest(".consoletable-wrapper");
+ ok(
+ tableWrapperNode.scrollHeight > tableWrapperNode.clientHeight,
+ testCase.info + " table overflows"
+ );
+ ok(
+ getComputedStyle(tableWrapperNode).overflowY !== "hidden",
+ "table can be scrolled"
+ );
+ }
+
+ if (typeof testCase.additionalTest === "function") {
+ await testCase.additionalTest(tableNode);
+ }
+}
diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_console_table_fallback.js b/devtools/client/webconsole/test/browser/browser_webconsole_console_table_fallback.js
new file mode 100644
index 0000000000..a511e8af77
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_console_table_fallback.js
@@ -0,0 +1,40 @@
+/* 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/. */
+
+// console.table fallback to console.log for unsupported parameters.
+
+"use strict";
+
+const tests = [
+ [`console.table(10, 20, 30, 40, 50)`, `10 20 30 40 50`],
+ [`console.table(1.2, 3.4, 5.6)`, `1.2 3.4 5.6`],
+ [`console.table(10n, 20n, 30n)`, `10n 20n 30n`],
+ [`console.table(true, false)`, `true false`],
+ [`console.table("foo", "bar", "baz")`, `foo bar baz`],
+ [`console.table(null, undefined, null)`, `null undefined null`],
+ [`console.table(undefined, null, undefined)`, `undefined null undefined`],
+ [`console.table(Symbol.iterator)`, `Symbol(Symbol.iterator)`],
+ [`console.table(/pattern/i)`, `/pattern/i`],
+ [`console.table(function f() {})`, `function f()`],
+];
+
+add_task(async function () {
+ const TEST_URI = "data:text/html,<!DOCTYPE html><meta charset=utf8>";
+
+ const hud = await openNewTabAndConsole(TEST_URI);
+
+ for (const [input, output] of tests) {
+ execute(hud, input);
+ const message = await waitFor(
+ () => findConsoleAPIMessage(hud, output),
+ `Waiting for output for ${input}`
+ );
+
+ is(
+ message.querySelector(".message-body").textContent,
+ output,
+ `Expected messages are displayed for ${input}`
+ );
+ }
+});
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..2f45120427
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_console_table_post_alterations.js
@@ -0,0 +1,68 @@
+/* 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,<!DOCTYPE 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) => {
+ if (a < b) {
+ return 1;
+ }
+ if (a > b) {
+ return -1;
+ }
+ return 0;
+ });
+ content.wrappedJSObject.console.table(x);
+ });
+
+ const [table1, table2, table3] = await waitFor(() => {
+ const res = hud.ui.outputNode.querySelectorAll(".message .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 rows = Array.from(node.querySelectorAll("tbody tr"));
+ is(rows.length, expectedRows.length, "table has the expected number of rows");
+
+ expectedRows.forEach((expectedRow, rowIndex) => {
+ const rowCells = Array.from(rows[rowIndex].querySelectorAll("td"));
+ 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..158e10b7a4
--- /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,<!DOCTYPE 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(() => findAllMessages(hud).length == 2);
+ const [first, second] = findAllMessages(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..15d87c4bbc
--- /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,<!DOCTYPE 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(() => findConsoleAPIMessage(hud, "trace"));
+ ok(true, "console.trace() message is displayed in the console");
+ const messages = findConsoleAPIMessages(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..42f89ab69c
--- /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(() => findConsoleAPIMessage(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..b4f076ec67
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_copy_entire_message.js
@@ -0,0 +1,244 @@
+/* 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")
+ console.log(document.location + "?" + "z".repeat(100))
+ }
+ 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(() =>
+ findConsoleAPIMessage(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(() => findConsoleAPIMessage(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:10`,
+ "Stacktrace second line has the expected text"
+ );
+
+ info("Test copy menu item for the error message");
+ message = await waitFor(() => findConsoleAPIMessage(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:10`,
+ "Error Stacktrace second line has the expected text"
+ );
+
+ info("Test copy menu item for the reference error message");
+ message = await waitFor(() => findErrorMessage(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:12`,
+ "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(() => findConsoleAPIMessage(hud, "repeated 2"));
+ clipboardText = await copyMessageContent(hud, message);
+ ok(true, "Clipboard text was found and saved");
+
+ info("Test copy menu item for the message with the cropped URL");
+ message = await waitFor(() => findConsoleAPIMessage(hud, "z".repeat(100)));
+ ok(!!message.querySelector("a.cropped-url"), "URL is cropped");
+ clipboardText = await copyMessageContent(hud, message);
+ ok(
+ clipboardText.startsWith(TEST_URI) + "?" + "z".repeat(100),
+ "Full URL was copied to clipboard"
+ );
+}
+
+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");
+
+ const text = await waitForClipboardPromise(
+ () => copyMenuItem.click(),
+ data => data
+ );
+
+ menuPopup.hidePopup();
+ return text;
+}
+
+/**
+ * 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..229470edcd
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_copy_link_location.js
@@ -0,0 +1,101 @@
+/* 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 = waitForMessageByType(hud, "stringLog", ".console-api");
+ 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");
+ 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 = waitForMessageByType(
+ hud,
+ "test-console.html",
+ ".network"
+ );
+ 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 executeAndWaitForErrorMessage(
+ 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..a004a32d67
--- /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 = waitForMessageByType(hud, "thenTrace", ".console-api");
+ 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..66649020fb
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_copy_message_with_framework_stacktrace.js
@@ -0,0 +1,132 @@
+/* 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 = waitForMessageByType(hud, "wrapperTrace", ".console-api");
+ 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");
+
+ const text = await waitForClipboardPromise(
+ () => copyMenuItem.click(),
+ data => data
+ );
+
+ menuPopup.hidePopup();
+ return text;
+}
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..7956cb6154
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_copy_object.js
@@ -0,0 +1,181 @@
+/* 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,<!DOCTYPE html><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(() =>
+ findConsoleAPIMessages(hud, "foo")
+ );
+ ok(
+ msgWithText && msgWithObj && msgNested,
+ "Three messages should have appeared"
+ );
+
+ const [groupMsgObj] = await waitFor(() =>
+ findMessagePartsByType(hud, {
+ text: "group",
+ typeSelector: ".console-api",
+ partSelector: ".message-body",
+ })
+ );
+ const [collapsedGroupMsgObj] = await waitFor(() =>
+ findMessagePartsByType(hud, {
+ text: "collapsed",
+ typeSelector: ".console-api",
+ partSelector: ".message-body",
+ })
+ );
+ const [numberMsgObj] = await waitFor(() =>
+ findMessagePartsByType(hud, {
+ text: `532`,
+ typeSelector: ".console-api",
+ partSelector: ".message-body",
+ })
+ );
+ const [trueMsgObj] = await waitFor(() =>
+ findMessagePartsByType(hud, {
+ text: `true`,
+ typeSelector: ".console-api",
+ partSelector: ".message-body",
+ })
+ );
+ const [falseMsgObj] = await waitFor(() =>
+ findMessagePartsByType(hud, {
+ text: `false`,
+ typeSelector: ".console-api",
+ partSelector: ".message-body",
+ })
+ );
+ const [undefinedMsgObj] = await waitFor(() =>
+ findMessagePartsByType(hud, {
+ text: `undefined`,
+ typeSelector: ".console-api",
+ partSelector: ".message-body",
+ })
+ );
+ const [nullMsgObj] = await waitFor(() =>
+ findMessagePartsByType(hud, {
+ text: `null`,
+ typeSelector: ".console-api",
+ partSelector: ".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(() =>
+ findMessagePartsByType(hud, {
+ text: 'console.log("foo");',
+ typeSelector: ".console-api",
+ partSelector: ".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");
+ const menuPopup = await openContextMenu(hud, element);
+ const copyObjectMenuItem = menuPopup.querySelector(copyObjectMenuItemId);
+ ok(
+ !copyObjectMenuItem.disabled,
+ "`Copy object` is enabled for object in complex message"
+ );
+ is(
+ copyObjectMenuItem.getAttribute("accesskey"),
+ "o",
+ "`Copy object` has the right accesskey"
+ );
+
+ const validatorFn = data => {
+ const prettifiedMessage = prettyPrintMessage(expectedMessage, objectInput);
+ return data === prettifiedMessage;
+ };
+
+ info("Activate item `Copy object`");
+ await waitForClipboardPromise(
+ () => menuPopup.activateItem(copyObjectMenuItem),
+ 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..19ae6e2a3f
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_export_console_output.js
@@ -0,0 +1,197 @@
+/* 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", "!");
+ /* add enough messages to trigger virtualization */
+ for (let i = 0; i < 100; i++) {
+ console.log("item-"+i);
+ }
+ }
+ wrapper();
+ };
+ `);
+});
+
+const TEST_URI = `http://localhost:${httpServer.identity.primaryPort}/`;
+
+const { MockFilePicker } = SpecialPowers;
+MockFilePicker.init(window);
+MockFilePicker.returnValue = MockFilePicker.returnOK;
+
+var FileUtils = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+).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() {
+ // Clear clipboard content.
+ SpecialPowers.clipboardCopyString("");
+ // Display timestamp to make sure we export them (there's a container query that would
+ // hide them in the regular case, which we don't want).
+ await pushPref("devtools.webconsole.timestampMessages", true);
+
+ 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.
+ const lastMessage = await waitFor(() =>
+ findConsoleAPIMessage(hud, "item-99")
+ );
+
+ const clipboardText = await exportAllToClipboard(hud, lastMessage);
+ ok(true, "Clipboard text was found and saved");
+
+ checkExportedText(clipboardText);
+
+ info("Test export to file");
+ const fileText = await exportAllToFile(hud, lastMessage);
+ checkExportedText(fileText);
+});
+
+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:14
+ // test.js:6:17
+ // -----------------------------------------------------
+ // console.trace() myConsoleTrace test.js:7:9
+ // wrapper test.js:7
+ // logStuff test.js:14
+ // -----------------------------------------------------
+ // world ! test.js:8:17
+ // -----------------------------------------------------
+ // item-0 test.js:11:19
+ // -----------------------------------------------------
+ // item-1 test.js:11:19
+ // -----------------------------------------------------
+ // […]
+ // -----------------------------------------------------
+ // item-99 test.js:11:19
+ // -----------------------------------------------------
+ info("Check if all messages where exported as expected");
+ let lines = text.split("\n").map(line => line.replace(/\r$/, ""));
+
+ is(lines.length, 115, "There's 115 lines of text");
+ is(lines.at(-1), "", "Last line is empty");
+
+ info("Check that timestamp are displayed");
+ const timestampRegex = /^\d{2}:\d{2}:\d{2}\.\d{3} /;
+ // only check the first message
+ ok(timestampRegex.test(lines[0]), "timestamp are included in the messages");
+ lines = lines.map(l => l.replace(timestampRegex, ""));
+
+ 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:14`);
+ 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:14`);
+
+ info("Check console.info message");
+ is(lines[13], `world ! test.js:8:17`);
+
+ const numberMessagesStartIndex = 14;
+ for (let i = 0; i < 100; i++) {
+ is(
+ lines[numberMessagesStartIndex + i],
+ `item-${i} test.js:11:19`,
+ `Got expected text for line ${numberMessagesStartIndex + i}`
+ );
+ }
+}
+
+async function exportAllToFile(hud, message) {
+ const menuPopup = await openContextMenu(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");
+
+ menuPopup.hidePopup();
+
+ // The file may not be ready yet.
+ await waitFor(() => IOUtils.exists(nsiFile.path));
+ const buffer = await IOUtils.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 openContextMenu(hud, message);
+ const exportClipboard = menuPopup.querySelector(
+ "#console-menu-export-clipboard"
+ );
+ ok(exportClipboard, "copy menu item is enabled");
+
+ const clipboardText = await waitForClipboardPromise(
+ () => exportClipboard.click(),
+ data => data.includes("hello")
+ );
+
+ menuPopup.hidePopup();
+ return clipboardText;
+}
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..dbd053944e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_object_in_sidebar.js
@@ -0,0 +1,182 @@
+/* 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,<!DOCTYPE html>" +
+ `<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(() =>
+ findConsoleAPIMessage(hud, "Object { a: 1 }")
+ );
+ const [objectA, objectB] = message.querySelectorAll(
+ ".object-inspector .objectBox-object"
+ );
+ const number = findMessagePartByType(hud, {
+ text: "100",
+ typeSelector: ".console-api",
+ partSelector: ".objectBox",
+ });
+ const string = findMessagePartByType(hud, {
+ text: "foo",
+ typeSelector: ".console-api",
+ partSelector: ".objectBox",
+ });
+ const bool = findMessagePartByType(hud, {
+ text: "false",
+ typeSelector: ".console-api",
+ partSelector: ".objectBox",
+ });
+ const nullMessage = findMessagePartByType(hud, {
+ text: "null",
+ typeSelector: ".console-api",
+ partSelector: ".objectBox",
+ });
+ const undefinedMsg = findMessagePartByType(hud, {
+ text: "undefined",
+ typeSelector: ".console-api",
+ partSelector: ".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 = findConsoleAPIMessage(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: oh my",
+ "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..950a4408ae
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_open_url.js
@@ -0,0 +1,110 @@
+/* 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(() =>
+ findConsoleAPIMessage(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(() => findConsoleAPIMessage(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(() => findNetworkMessage(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..91c58b0203
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_reveal_in_inspector.js
@@ -0,0 +1,143 @@
+/* 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>
+ <!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(() => findConsoleAPIMessage(hud, `foo`));
+ const msgWithObj = await waitFor(() => findConsoleAPIMessage(hud, `Object`));
+ const nonDomEl = await waitFor(() =>
+ findMessagePartByType(hud, {
+ text: `<span>`,
+ typeSelector: ".console-api",
+ partSelector: ".objectBox-node",
+ })
+ );
+
+ const domEl = await waitFor(() =>
+ findMessagePartByType(hud, {
+ text: `<div>`,
+ typeSelector: ".console-api",
+ partSelector: ".objectBox-node",
+ })
+ );
+ const domTextEl = await waitFor(() =>
+ findMessagePartByType(hud, {
+ text: `test-text`,
+ typeSelector: ".console-api",
+ partSelector: ".objectBox-textNode",
+ })
+ );
+ const domElCollection = await waitFor(() =>
+ findMessagePartByType(hud, {
+ text: `html`,
+ typeSelector: ".console-api",
+ partSelector: ".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) {
+ if (
+ !accesskey &&
+ AppConstants.platform == "macosx" &&
+ Services.prefs.getBoolPref("widget.macos.native-context-menus", false)
+ ) {
+ info(
+ "Not testing accesskey behaviour since we can't use synthesized keypresses in macOS native menus."
+ );
+ return;
+ }
+ 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");
+ menuPopup.activateItem(revealInInspectorMenuItem);
+ } 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..c65d9fb3d0
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_store_as_global.js
@@ -0,0 +1,121 @@
+/* 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,<!DOCTYPE html><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(() => findConsoleAPIMessages(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 = waitForMessageByType(hud, "foo", ".console-api");
+ 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) {
+ // Refresh the reference to the message, as it may have been scrolled out of existence.
+ msg = await findMessageVirtualizedById({
+ hud,
+ messageId: msg.getAttribute("data-message-id"),
+ });
+ 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");
+ menuPopup.activateItem(storeMenuItem);
+
+ 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 executeAndWaitForResultMessage(
+ hud,
+ `temp${varIdx} === ${equalTo}`,
+ true
+ );
+ 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..af2f04cc90
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_cors_errors.js
@@ -0,0 +1,260 @@
+/* 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 = waitForMessageByType(hud, "Reason: CORS disabled", ".error");
+ makeFaultyCorsCall("CORSDisabled");
+ message = await onCorsMessage;
+ await checkCorsMessage(hud, message, "CORSDisabled");
+ await pushPref("content.cors.disable", false);
+
+ info("Test CORSPreflightDidNotSucceed");
+ onCorsMessage = waitForMessageByType(
+ hud,
+ `(Reason: CORS preflight response did not succeed). Status code: `,
+ ".error"
+ );
+ makeFaultyCorsCall("CORSPreflightDidNotSucceed");
+ message = await onCorsMessage;
+ await checkCorsMessage(hud, message, "CORSPreflightDidNotSucceed");
+
+ info("Test CORS did not succeed");
+ onCorsMessage = waitForMessageByType(
+ hud,
+ "(Reason: CORS request did not succeed). Status code: ",
+ ".error"
+ );
+ makeFaultyCorsCall("CORSDidNotSucceed");
+ message = await onCorsMessage;
+ await checkCorsMessage(hud, message, "CORSDidNotSucceed");
+
+ info("Test CORSExternalRedirectNotAllowed");
+ onCorsMessage = waitForMessageByType(
+ hud,
+ "Reason: CORS request external redirect not allowed",
+ ".error"
+ );
+ makeFaultyCorsCall("CORSExternalRedirectNotAllowed");
+ message = await onCorsMessage;
+ await checkCorsMessage(hud, message, "CORSExternalRedirectNotAllowed");
+
+ info("Test CORSMissingAllowOrigin");
+ onCorsMessage = waitForMessageByType(
+ hud,
+ `(Reason: CORS header ${quote(
+ "Access-Control-Allow-Origin"
+ )} missing). Status code: `,
+ ".error"
+ );
+ makeFaultyCorsCall("CORSMissingAllowOrigin");
+ message = await onCorsMessage;
+ await checkCorsMessage(hud, message, "CORSMissingAllowOrigin");
+
+ info("Test CORSMultipleAllowOriginNotAllowed");
+ onCorsMessage = waitForMessageByType(
+ hud,
+ `Reason: Multiple CORS header ${quote(
+ "Access-Control-Allow-Origin"
+ )} not allowed`,
+ ".error"
+ );
+ makeFaultyCorsCall("CORSMultipleAllowOriginNotAllowed");
+ message = await onCorsMessage;
+ await checkCorsMessage(hud, message, "CORSMultipleAllowOriginNotAllowed");
+
+ info("Test CORSAllowOriginNotMatchingOrigin");
+ onCorsMessage = waitForMessageByType(
+ hud,
+ `Reason: CORS header ` +
+ `${quote("Access-Control-Allow-Origin")} does not match ${quote(
+ "mochi.test"
+ )}`,
+ ".error"
+ );
+ makeFaultyCorsCall("CORSAllowOriginNotMatchingOrigin");
+ message = await onCorsMessage;
+ await checkCorsMessage(hud, message, "CORSAllowOriginNotMatchingOrigin");
+
+ info("Test CORSNotSupportingCredentials");
+ onCorsMessage = waitForMessageByType(
+ hud,
+ `Reason: Credential is not supported if the CORS ` +
+ `header ${quote("Access-Control-Allow-Origin")} is ${quote("*")}`,
+ ".error"
+ );
+ makeFaultyCorsCall("CORSNotSupportingCredentials");
+ message = await onCorsMessage;
+ await checkCorsMessage(hud, message, "CORSNotSupportingCredentials");
+
+ info("Test CORSMethodNotFound");
+ onCorsMessage = waitForMessageByType(
+ hud,
+ `Reason: Did not find method in CORS header ` +
+ `${quote("Access-Control-Allow-Methods")}`,
+ ".error"
+ );
+ makeFaultyCorsCall("CORSMethodNotFound");
+ message = await onCorsMessage;
+ await checkCorsMessage(hud, message, "CORSMethodNotFound");
+
+ info("Test CORSMissingAllowCredentials");
+ onCorsMessage = waitForMessageByType(
+ hud,
+ `Reason: expected ${quote("true")} in CORS ` +
+ `header ${quote("Access-Control-Allow-Credentials")}`,
+ ".error"
+ );
+ makeFaultyCorsCall("CORSMissingAllowCredentials");
+ message = await onCorsMessage;
+ await checkCorsMessage(hud, message, "CORSMissingAllowCredentials");
+
+ info("Test CORSInvalidAllowMethod");
+ onCorsMessage = waitForMessageByType(
+ hud,
+ `Reason: invalid token ${quote("xyz;")} in CORS ` +
+ `header ${quote("Access-Control-Allow-Methods")}`,
+ ".error"
+ );
+ makeFaultyCorsCall("CORSInvalidAllowMethod");
+ message = await onCorsMessage;
+ await checkCorsMessage(hud, message, "CORSInvalidAllowMethod");
+
+ info("Test CORSInvalidAllowHeader");
+ onCorsMessage = waitForMessageByType(
+ hud,
+ `Reason: invalid token ${quote("xyz;")} in CORS ` +
+ `header ${quote("Access-Control-Allow-Headers")}`,
+ ".error"
+ );
+ makeFaultyCorsCall("CORSInvalidAllowHeader");
+ message = await onCorsMessage;
+ await checkCorsMessage(hud, message, "CORSInvalidAllowHeader");
+
+ info("Test CORSMissingAllowHeaderFromPreflight");
+ onCorsMessage = waitForMessageByType(
+ hud,
+ `Reason: header ${quote("xyz")} is not allowed according to ` +
+ `header ${quote(
+ "Access-Control-Allow-Headers"
+ )} from CORS preflight response`,
+ ".error"
+ );
+ makeFaultyCorsCall("CORSMissingAllowHeaderFromPreflight");
+ message = await onCorsMessage;
+ await checkCorsMessage(hud, message, "CORSMissingAllowHeaderFromPreflight");
+
+ // See Bug 1480671.
+ // XXX: how to make Origin to not be included in the request ?
+ // onCorsMessage = waitForMessageByType(hud,
+ // `Reason: CORS header ${quote("Origin")} cannot be added`,
+ // ".error");
+ // makeFaultyCorsCall("CORSOriginHeaderNotAdded");
+ // message = await onCorsMessage;
+ // await checkCorsMessage(hud, 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 = waitForMessageByType(hud, "Reason: CORS request not http",
+ // ".error");
+ // const dir = getChromeDir(getResolvedURI(gTestPath));
+ // dir.append("sjs_cors-test-server.sjs");
+ // makeFaultyCorsCall("CORSRequestNotHttp", Services.io.newFileURI(dir).spec);
+ // message = await onCorsMessage;
+ // await checkCorsMessage(hud, message, "CORSRequestNotHttp");
+});
+
+async function checkCorsMessage(hud, message, category) {
+ // Get a new reference to the node, as it may have been scrolled out of existence.
+ const node = await findMessageVirtualizedById({
+ hud,
+ messageId: message.node.getAttribute("data-message-id"),
+ });
+ node.scrollIntoView();
+ 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..03da6d5d4f
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_csp_ignore_reflected_xss_message.js
@@ -0,0 +1,34 @@
+/* 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,<!DOCTYPE html>Web Console CSP ignoring reflected XSS (bug 1045902)";
+
+add_task(async function () {
+ const hud = await openNewTabAndConsole(TEST_URI);
+ await navigateTo(TEST_FILE);
+
+ await checkUniqueMessageExists(hud, EXPECTED_RESULT, ".warn");
+ ok(
+ true,
+ `CSP logs displayed in console when using "reflected-xss" directive`
+ );
+
+ info("Reload page and check that the CSP warning is not duplicated");
+ await reloadBrowser();
+ await checkUniqueMessageExists(hud, EXPECTED_RESULT, ".warn");
+
+ 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..5a8c7cbf2b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_csp_violation.js
@@ -0,0 +1,141 @@
+/* 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,<!DOCTYPE html>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 = waitForRepeatedMessageByType(
+ hud,
+ CSP_VIOLATION_MSG,
+ ".error",
+ 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(() => findErrorMessage(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 executeAndWaitForErrorMessage(
+ 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(() => findErrorMessage(hud, CSP_VIOLATION));
+ ok(msg, "Base-URI validation was Printed");
+ // Triggering the Violation via JS
+ await clearOutput(hud);
+ msg = await executeAndWaitForErrorMessage(
+ 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(() => findErrorMessage(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(() => findErrorMessage(hud, CSP_VIOLATION));
+ ok(msg, "Frame-Ancestors violation by html was printed");
+ }
+ await clearOutput(hud);
+ // Testing CSP inline event handler violations
+ {
+ const TEST_VIOLATION =
+ "https://example.com/browser/devtools/client/webconsole/" +
+ "test/browser/test-csp-violation-event-handler.html";
+ const CSP_VIOLATION = `Content-Security-Policy: The page’s settings blocked the loading of a resource at inline (“script-src”).
+Source: document.body.textContent = 'JavaScript …`;
+ // Future-Todo: Include line and column number.
+ const VIOLATION_LOCATION = "test-csp-violation-event-handler.html";
+ await navigateTo(TEST_VIOLATION);
+ const msg = await waitFor(() => findErrorMessage(hud, CSP_VIOLATION));
+ const locationNode = msg.querySelector(".message-location");
+ is(
+ locationNode.textContent,
+ VIOLATION_LOCATION,
+ "Inline event handler location doesn't yet include the line/column"
+ );
+ }
+});
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..328663ce28
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_cspro.js
@@ -0,0 +1,57 @@
+/* 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,<!DOCTYPE html>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 = waitForMessageByType(
+ hud,
+ CSP_VIOLATION_MSG,
+ ".error"
+ );
+ const onCspReportMessage = waitForMessageByType(
+ hud,
+ CSP_REPORT_MSG,
+ ".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..b11e84a74b
--- /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,<!DOCTYPE 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(() =>
+ findWarningMessage(hud, "Error in parsing value for ‘cursor’", ".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(() =>
+ findWarningMessage(hud, "Error in parsing value for ‘color’", ".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_custom_formatters.js b/devtools/client/webconsole/test/browser/browser_webconsole_custom_formatters.js
new file mode 100644
index 0000000000..bb4eb3cb79
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_custom_formatters.js
@@ -0,0 +1,193 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check display of custom formatters.
+const TEST_URI =
+ "https://example.com/browser/devtools/client/webconsole/" +
+ "test/browser/test-console-custom-formatters.html";
+
+add_task(async function () {
+ // ToDo: This preference can be removed once the custom formatters feature is stable enough
+ await pushPref("devtools.custom-formatters", true);
+ await pushPref("devtools.custom-formatters.enabled", true);
+
+ const hud = await openNewTabAndConsole(TEST_URI);
+
+ // Reload the browser to ensure the custom formatters are picked up
+ await reloadBrowser();
+
+ await testString(hud);
+ await testNumber(hud);
+ await testObjectWithoutFormatting(hud);
+ await testObjectWithFormattedHeader(hud);
+ await testObjectWithFormattedHeaderAndBody(hud);
+ await testCustomFormatterWithObjectTag(hud);
+});
+
+async function testString(hud) {
+ info("Test for string not being custom formatted");
+ await testCustomFormatting(hud, {
+ hasCustomFormatter: false,
+ messageText: "string",
+ });
+}
+
+async function testNumber(hud) {
+ info("Test for number not being custom formatted");
+ await testCustomFormatting(hud, {
+ hasCustomFormatter: false,
+ messageText: 1337,
+ });
+}
+
+async function testObjectWithoutFormatting(hud) {
+ info("Test for object not being custom formatted");
+ await testCustomFormatting(hud, {
+ hasCustomFormatter: false,
+ messageText: "Object { noFormat: true }",
+ });
+}
+
+async function testObjectWithFormattedHeader(hud) {
+ info("Simple test for custom formatted header");
+ await testCustomFormatting(hud, {
+ hasCustomFormatter: true,
+ messageText: "custom formatted header",
+ headerStyles: "font-size: 3rem;",
+ });
+}
+
+async function testObjectWithFormattedHeaderAndBody(hud) {
+ info("Simple test for custom formatted header with body");
+ await testCustomFormatting(hud, {
+ hasCustomFormatter: true,
+ messageText: "custom formatted body",
+ headerStyles: "font-style: italic;",
+ bodyText: "body",
+ bodyStyles: "font-size: 2rem; font-family: serif;",
+ });
+}
+
+async function testCustomFormatterWithObjectTag(hud) {
+ info(`Test for custom formatted object with "object" tag in the jsonMl`);
+ const node = await waitFor(() => {
+ return findConsoleAPIMessage(hud, "object tag");
+ });
+
+ const headerJsonMlNode = node.querySelector(".objectBox-jsonml");
+ is(
+ headerJsonMlNode.getAttribute("style"),
+ "color: purple;",
+ "The custom formatting of the header is correct"
+ );
+ const [buttonEl, child1, child2, child3, child4] =
+ headerJsonMlNode.childNodes;
+ is(child1.textContent, "object tag", "Got expected first item");
+ is(
+ child2.textContent,
+ `~[1,"a"]~`,
+ "Got expected second item, the replaced object, custom formatted"
+ );
+ ok(
+ child3.classList.contains("objectBox-null"),
+ "Got expected third item, an actual NullRep"
+ );
+ is(child3.textContent, `null`, "third item has expected content");
+
+ is(
+ child4.textContent,
+ ` | serialized: 42n undefined null Infinity [object Object]`,
+ "Got expected fourth item, serialized values"
+ );
+
+ buttonEl.click();
+ const bodyLevel1 = await waitFor(() =>
+ node.querySelector(".objectBox-jsonml-body-wrapper .objectBox-jsonml")
+ );
+ const [bodyChild1, bodyChild2] = bodyLevel1.childNodes;
+ is(bodyChild1.textContent, "body");
+
+ const bodyCustomFormattedChild = await waitFor(() =>
+ bodyChild2.querySelector(".objectBox-jsonml")
+ );
+ const [subButtonEl, subChild1, subChild2, subChild3] =
+ bodyCustomFormattedChild.childNodes;
+ ok(!!subButtonEl, "The body child can also be expanded");
+ is(subChild1.textContent, "object tag", "Got expected first item");
+ is(
+ subChild2.textContent,
+ `~[2,"b"]~`,
+ "Got expected body second item, the replaced object, custom formatted"
+ );
+ ok(
+ subChild3.classList.contains("object-inspector"),
+ "Got expected body third item, an actual ObjectInspector"
+ );
+ is(
+ subChild3.textContent,
+ `Array [ 2, "b" ]`,
+ "body third item has expected content"
+ );
+}
+
+async function testCustomFormatting(
+ hud,
+ { hasCustomFormatter, messageText, headerStyles, bodyText, bodyStyles }
+) {
+ const node = await waitFor(() => {
+ return findConsoleAPIMessage(hud, messageText);
+ });
+
+ const headerJsonMlNode = node.querySelector(".objectBox-jsonml");
+ if (hasCustomFormatter) {
+ ok(headerJsonMlNode, "The message is custom formatted");
+
+ if (!headerJsonMlNode) {
+ return;
+ }
+
+ is(
+ headerJsonMlNode.getAttribute("style"),
+ headerStyles,
+ "The custom formatting of the header is correct"
+ );
+
+ if (bodyText) {
+ const arrow = node.querySelector(".collapse-button");
+
+ ok(arrow, "There must be a toggle arrow for the header");
+
+ info("Expanding the Object");
+ const onBodyRendered = waitFor(
+ () =>
+ !!node.querySelector(
+ ".objectBox-jsonml-body-wrapper .objectBox-jsonml"
+ )
+ );
+
+ arrow.click();
+ await onBodyRendered;
+
+ ok(
+ node.querySelector(".collapse-button").classList.contains("expanded"),
+ "The arrow of the node has the expected class after clicking on it"
+ );
+
+ const bodyJsonMlNode = node.querySelector(
+ ".objectBox-jsonml-body-wrapper > .objectBox-jsonml"
+ );
+ ok(bodyJsonMlNode, "The body is custom formatted");
+
+ is(bodyJsonMlNode?.textContent, bodyText, "The body text is correct");
+ is(
+ bodyJsonMlNode.getAttribute("style"),
+ bodyStyles,
+ "The custom formatting of the body is correct"
+ );
+ }
+ } else {
+ ok(!headerJsonMlNode, "The message is not custom formatted");
+ }
+}
diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_custom_formatters_errors.js b/devtools/client/webconsole/test/browser/browser_webconsole_custom_formatters_errors.js
new file mode 100644
index 0000000000..71013ab58f
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_custom_formatters_errors.js
@@ -0,0 +1,199 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check display of custom formatters.
+const TEST_URI =
+ "https://example.com/browser/devtools/client/webconsole/" +
+ "test/browser/test-console-custom-formatters-errors.html";
+
+add_task(async function () {
+ // ToDo: This preference can be removed once the custom formatters feature is stable enough
+ await pushPref("devtools.custom-formatters", true);
+ await pushPref("devtools.custom-formatters.enabled", true);
+
+ // enable "can't access property "y", x is undefined" error message
+ await pushPref("javascript.options.property_error_message_fix", true);
+
+ const hud = await openNewTabAndConsole(TEST_URI);
+
+ // Reload the browser to ensure the custom formatters are picked up
+ await reloadBrowser();
+
+ await testHeaderNotAFunction(hud);
+ await testHeaderNotReturningJsonMl(hud);
+ await testHeaderNotReturningElementType(hud);
+ await testHeaderThrowing(hud);
+ await testHasBodyNotAFunction(hud);
+ await testHasBodyThrowing(hud);
+ await testBodyNotAFunction(hud);
+ await testBodyReturningNull(hud);
+ await testBodyNotReturningJsonMl(hud);
+ await testBodyNotReturningElementType(hud);
+ await testBodyThrowing(hud);
+ await testIncorrectObjectTag(hud);
+ await testInvalidTagname(hud);
+ await testNoPrivilegedAccess(hud);
+ await testErrorsLoggedOnce(hud);
+});
+
+async function testHeaderNotAFunction(hud) {
+ info(`Test for "header" not being a function`);
+ await testCustomFormatting(hud, {
+ messageText: `Custom formatter failed: devtoolsFormatters[0].header should be a function, got number`,
+ });
+}
+
+async function testHeaderNotReturningJsonMl(hud) {
+ info(`Test for "header" not returning JsonML`);
+ await testCustomFormatting(hud, {
+ messageText: `Custom formatter failed: devtoolsFormatters[1].header should return an array, got number`,
+ source: "test-console-custom-formatters-errors.html:19:18",
+ });
+}
+
+async function testHeaderNotReturningElementType(hud) {
+ info(`Test for "header" function returning array without element type`);
+ await testCustomFormatting(hud, {
+ messageText: `Custom formatter failed: devtoolsFormatters[2].header returned an empty array`,
+ });
+}
+
+async function testHeaderThrowing(hud) {
+ info(`Test for "header" function throwing`);
+ await testCustomFormatting(hud, {
+ messageText: `Custom formatter failed: devtoolsFormatters[3].header threw: ERROR`,
+ });
+}
+
+async function testHasBodyNotAFunction(hud) {
+ info(`Test for "hasBody" not being a function`);
+ await testCustomFormatting(hud, {
+ messageText: `Custom formatter failed: devtoolsFormatters[4].hasBody should be a function, got number`,
+ });
+}
+
+async function testHasBodyThrowing(hud) {
+ info(`Test for "hasBody" function throwing`);
+ await testCustomFormatting(hud, {
+ messageText: `Custom formatter failed: devtoolsFormatters[5].hasBody threw: ERROR`,
+ });
+}
+
+async function testBodyNotAFunction(hud) {
+ info(`Test for "body" not being a function`);
+ await testCustomFormatting(hud, {
+ messageText: "body not a function",
+ bodyText: `Custom formatter failed: devtoolsFormatters[6].body should be a function, got number`,
+ });
+}
+
+async function testBodyReturningNull(hud) {
+ info(`Test for "body" returning null`);
+ await testCustomFormatting(hud, {
+ messageText: "body returns null",
+ bodyText: `Custom formatter failed: devtoolsFormatters[7].body should return an array, got null`,
+ });
+}
+
+async function testBodyNotReturningJsonMl(hud) {
+ info(`Test for "body" not returning JsonML`);
+ await testCustomFormatting(hud, {
+ messageText: "body doesn't return JsonML",
+ bodyText: `Custom formatter failed: devtoolsFormatters[8].body should return an array, got number`,
+ });
+}
+
+async function testBodyNotReturningElementType(hud) {
+ info(`Test for "body" function returning array without element type`);
+ await testCustomFormatting(hud, {
+ messageText: "body array misses element type",
+ bodyText: `Custom formatter failed: devtoolsFormatters[9].body returned an empty array`,
+ });
+}
+
+async function testBodyThrowing(hud) {
+ info(`Test for "body" function throwing`);
+ await testCustomFormatting(hud, {
+ messageText: "body throws",
+ bodyText: `Custom formatter failed: devtoolsFormatters[10].body threw: ERROR`,
+ });
+}
+
+async function testErrorsLoggedOnce(hud) {
+ const messages = findMessagesByType(hud, "custom formatter failed", ".error");
+
+ messages.forEach(async message => {
+ await checkUniqueMessageExists(hud, message.textContent, ".error");
+ });
+}
+
+async function testIncorrectObjectTag(hud) {
+ info(`Test for "object" tag without attribute`);
+ await testCustomFormatting(hud, {
+ messageText: `Custom formatter failed: devtoolsFormatters[11] couldn't be run: "object" tag should have attributes`,
+ });
+
+ info(`Test for "object" tag without "object" attribute`);
+ await testCustomFormatting(hud, {
+ messageText: `Custom formatter failed: devtoolsFormatters[12] couldn't be run: attribute of "object" tag should have an "object" property`,
+ });
+
+ info(`Test for infinite "object" tag`);
+ await testCustomFormatting(hud, {
+ messageText: `Custom formatter failed: Too deep hierarchy of inlined custom previews`,
+ });
+}
+
+async function testInvalidTagname(hud) {
+ info(`Test invalid tagname in the returned JsonML`);
+ await testCustomFormatting(hud, {
+ messageText: `Custom formatter failed: devtoolsFormatters[14] couldn't be run: tagName should be a string, got number`,
+ });
+}
+
+async function testNoPrivilegedAccess(hud) {
+ info(`Test for denied access to windowUtils from hook`);
+ await testCustomFormatting(hud, {
+ messageText: `Custom formatter failed: devtoolsFormatters[17].header threw: can't access property "garbageCollect", window.windowUtils is undefined`,
+ });
+}
+
+async function testCustomFormatting(hud, { messageText, source, bodyText }) {
+ const headerNode = bodyText
+ ? await waitFor(() => {
+ return findConsoleAPIMessage(hud, messageText);
+ })
+ : await waitFor(() => {
+ return findErrorMessage(hud, messageText);
+ });
+
+ ok(true, `Got expected message: ${messageText}`);
+
+ if (source) {
+ const sourceLink = headerNode.querySelector(".message-location");
+ is(sourceLink?.textContent, source, "Source location is correct");
+ }
+
+ if (bodyText) {
+ const arrow = headerNode.querySelector(".collapse-button");
+
+ ok(arrow, "There must be a toggle arrow for the header");
+
+ info("Expanding the Object");
+ const bodyErrorNode = waitFor(() => {
+ return findErrorMessage(hud, bodyText);
+ });
+
+ arrow.click();
+ await bodyErrorNode;
+
+ ok(
+ headerNode
+ .querySelector(".collapse-button")
+ .classList.contains("expanded"),
+ "The arrow of the node has the expected class after clicking on it"
+ );
+ }
+}
diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_deprecation_warning.js b/devtools/client/webconsole/test/browser/browser_webconsole_deprecation_warning.js
new file mode 100644
index 0000000000..726ece98c9
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_deprecation_warning.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that calling deprecated getter displays a deprecation warning.
+const TEST_URI =
+ "data:text/html;charset=utf8,<!DOCTYPE html><h1>Deprecation warning";
+
+add_task(async function () {
+ const hud = await openNewTabAndConsole(TEST_URI);
+
+ const deprecatedWarningMessageText = "mozPressure is deprecated";
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ content.testMouseEvent = new content.MouseEvent("click");
+ content.wrappedJSObject.console.log("oi-test", content.testMouseEvent);
+ });
+ const node = await waitFor(() => findConsoleAPIMessage(hud, "oi-test"));
+
+ info("Expand the MouseEvent object");
+ const oi = node.querySelector(".tree");
+ expandObjectInspectorNode(oi);
+ await waitFor(() => getObjectInspectorNodes(oi).length > 1);
+
+ info("Wait for a bit so any warning message could be displayed");
+ await wait(1000);
+ ok(
+ !findWarningMessage(hud, deprecatedWarningMessageText, ".warn"),
+ "Expanding the MouseEvent object didn't triggered the deprecation warning"
+ );
+
+ info("Access the deprecated getter to trigger a warning message");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ content.testMouseEvent.mozPressure;
+ });
+
+ await waitFor(() =>
+ findWarningMessage(hud, deprecatedWarningMessageText, ".warn")
+ );
+ ok(
+ true,
+ "Calling the mozPressure getter did triggered the deprecation warning"
+ );
+
+ info("Clear the console and access the deprecated getter again");
+ await clearOutput(hud);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ content.testMouseEvent.mozPressure;
+ });
+ info("Wait for a bit so any warning message could be displayed");
+ await wait(1000);
+ ok(
+ !findWarningMessage(hud, deprecatedWarningMessageText, ".warn"),
+ "Calling the mozPressure getter a second time did not trigger the deprecation warning again"
+ );
+});
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..070c97623d
--- /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,<!DOCTYPE html>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,<!DOCTYPE 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..4c467784b8
--- /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(() => findErrorMessage(hud, "fooDuplicateError1"));
+
+ 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_enable_network_monitoring.js b/devtools/client/webconsole/test/browser/browser_webconsole_enable_network_monitoring.js
new file mode 100644
index 0000000000..13202b5c6f
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_enable_network_monitoring.js
@@ -0,0 +1,21 @@
+// Test that the "Enable Network Monitoring" checkbox item is not available
+// in the webconsole.
+
+"use strict";
+
+add_task(async function testEnableNetworkMonitoringInWebConsole() {
+ const hud = await openNewTabAndConsole(
+ `data:text/html,<!DOCTYPE html><script>foo;</script>`
+ );
+
+ const enableNetworkMonitoringItem = getConsoleSettingElement(
+ hud,
+ ".webconsole-console-settings-menu-item-enableNetworkMonitoring"
+ );
+ ok(
+ !enableNetworkMonitoringItem,
+ "The 'Enable Network Monitoring' setting item should not be avaliable in the webconsole"
+ );
+
+ await closeConsole();
+});
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..94b2322796
--- /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,<!DOCTYPE html><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(() => findConsoleAPIMessage(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..738db65376
--- /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,<!DOCTYPE html><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(() => findConsoleAPIMessage(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,
+ "Frames functions are displayed"
+ );
+ ok(
+ !!stackTraceElement.querySelectorAll(".frame .location").length,
+ "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..60d7f3cc99
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_error_with_unicode.js
@@ -0,0 +1,25 @@
+/* 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,<!DOCTYPE html><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(() => findErrorMessage(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..8c0c2a51f7
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_error_with_url.js
@@ -0,0 +1,62 @@
+/* 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,<!DOCTYPE html><script>
+ throw "Visit \u201c${url1}\u201d or \u201c${url2}\u201d to get more " +
+ "information on this error.";
+</script>`;
+const { ELLIPSIS } = require("resource://devtools/shared/l10n.js");
+
+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 getVisibleLinkText = linkEl => {
+ const [firstPart, , lastPart] = linkEl.children;
+ return `${firstPart.innerText}${ELLIPSIS}${lastPart.innerText}`;
+ };
+
+ const EXPECTED_MESSAGE = `get more information on this error`;
+
+ const msg = await waitFor(() => findErrorMessage(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(
+ getVisibleLinkText(comLink),
+ 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(
+ getVisibleLinkText(orgLink),
+ 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..456fd7c422
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_errors_after_page_reload.js
@@ -0,0 +1,46 @@
+/* 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 { onDomCompleteResource } =
+ await waitForNextTopLevelDomCompleteResource(hud.toolbox.commands);
+
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.wrappedJSObject.location.reload();
+ });
+ await onDomCompleteResource;
+ info("page reloaded");
+
+ // On e10s, the exception is triggered in child process
+ // and is ignored by test harness
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+
+ const onMessage = waitForMessageByType(
+ hud,
+ "fooBazBaz is not defined",
+ ".error"
+ );
+ 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..1083e31f00
--- /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 = findErrorMessage(hud, "eggplant");
+ 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(`Object { fav: "eggplant" }`));
+ ok(oiNodes[1].textContent.includes(`fav: "eggplant"`));
+ ok(oiNodes[2].textContent.includes(`<prototype>: Object { \u2026 }`));
+
+ execute(hud, `1 + @`);
+ const messageNode = await waitFor(() =>
+ findErrorMessage(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..5a894cb2de
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_eval_in_debugger_stackframe.js
@@ -0,0 +1,131 @@
+/* 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";
+
+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 executeAndWaitForResultMessage(hud, "foo", "globalFooBug783499");
+ ok(true, "|foo| value is correct");
+
+ info("Assign and check `foo2` value");
+ await executeAndWaitForResultMessage(
+ hud,
+ "foo2 = 'newFoo'; window.foo2",
+ "newFoo"
+ );
+ 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 executeAndWaitForResultMessage(
+ hud,
+ "foo + foo2",
+ "globalFooBug783499newFoo"
+ );
+
+ 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 executeAndWaitForResultMessage(
+ hud,
+ "foo + foo2",
+ "globalFooBug783499foo2SecondCall"
+ );
+ 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 executeAndWaitForResultMessage(
+ hud,
+ "foo + foo2 + foo3",
+ "fooFirstCallnewFoofoo3FirstCall"
+ );
+ ok(true, "`foo + foo2 + foo3` from `firstCall()`");
+
+ await executeAndWaitForResultMessage(
+ hud,
+ "foo = 'abba'; foo3 = 'bug783499'; foo + foo3",
+ "abbabug783499"
+ );
+ 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"
+ );
+ });
+ await resume(dbg);
+
+ info(
+ "Check executing expression with private properties access while paused in class method"
+ );
+ const onPaused = waitForPaused(dbg);
+ // breakFn has a debugger statement that will pause the debugger
+ execute(hud, `x = new Foo(); x.breakFn()`);
+ await onPaused;
+ // pausing opens the debugger, switch to the console again
+ await openConsole();
+
+ await executeAndWaitForResultMessage(
+ hud,
+ "this.#privateProp",
+ "privatePropValue"
+ );
+ ok(
+ true,
+ "evaluating a private properties while paused in a class method does work"
+ );
+
+ await executeAndWaitForResultMessage(
+ hud,
+ "Foo.#privateStatic",
+ `Object { first: "a", second: "b" }`
+ );
+ ok(
+ true,
+ "evaluating a static private properties while paused in a class method does work"
+ );
+
+ await resume(dbg);
+});
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..ac6e8450f0
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_eval_in_debugger_stackframe2.js
@@ -0,0 +1,76 @@
+/* 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 = waitForMessageByType(
+ hud,
+ "undefined",
+ ".result"
+ );
+
+ 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 executeAndWaitForResultMessage(hud, "1 + 2", "3");
+ ok(true, "`1 + 2` was evaluated whith debugger paused");
+
+ info("Executing command using scoped variables while paused");
+ await executeAndWaitForResultMessage(
+ hud,
+ "foo + foo2",
+ `"globalFooBug783499foo2SecondCall"`
+ );
+ 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..f417746ae3
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_eval_sources.js
@@ -0,0 +1,74 @@
+/* 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 toolbox = await gDevTools.getToolboxForTab(gBrowser.selectedTab);
+
+ let messageNode = await waitFor(() => findErrorMessage(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, {
+ text: "FOO",
+ typeSelector: ".console-api",
+ expectUrl: false,
+ });
+ await testOpenInDebugger(hud, {
+ text: "BAR",
+ typeSelector: ".error",
+ expectUrl: 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, {
+ text: "BAZ",
+ typeSelector: ".console-api",
+ expectUrl: false,
+ });
+
+ // Test that stacks in console.trace() calls work.
+ messageNode = await waitFor(() => findConsoleAPIMessage(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");
+ 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..8ccb6cd09f
--- /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 = waitForMessageByType(
+ hud,
+ "window.location.href;",
+ ".command"
+ );
+ const onEvaluationResultMessage = waitForMessageByType(
+ hud,
+ TEST_URI,
+ ".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..0d5e21d13c
--- /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 = waitForMessageByType(hud, "bogus is not defined", ".error");
+ 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..668cfe925b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_file_uri.js
@@ -0,0 +1,77 @@
+/* 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 waitForMessagesByType({
+ webconsole: hud,
+ messages: [
+ {
+ text: "running network console logging tests",
+ typeSelector: ".console-api",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "test-network.html",
+ typeSelector: ".network",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "test-image.png",
+ typeSelector: ".network",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "testscript.js",
+ typeSelector: ".network",
+ 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..74e5a2ef1a
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_filter_buttons_overflow.js
@@ -0,0 +1,83 @@
+/* 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 = waitForMessageByType(hud, "world", ".console-api");
+ 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,
+ interactiveRule: false,
+ focusableRule: false,
+ labelRule: false,
+ });
+ 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..e2a8e13f72
--- /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(
+ () =>
+ findConsoleAPIMessage(hud, lastSeason.english) &&
+ findConsoleAPIMessage(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..169e99c481
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_filter_by_regex_input.js
@@ -0,0 +1,106 @@
+/* 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(() => findConsoleAPIMessage(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);
+
+ info("Filter out messages starting with 'foo', case-sensitive default");
+ await setFilterInput(hud, "/^foo/", MESSAGES[1]);
+ filteredNodes = outputNode.querySelectorAll(".message");
+ checkFilteredMessages(filteredNodes, [MESSAGES[1]], 1);
+
+ info("Filter out messages starting with 'FOO', case-sensitive default");
+ await setFilterInput(hud, "/^FOO/", MESSAGES[4]);
+ filteredNodes = outputNode.querySelectorAll(".message");
+ checkFilteredMessages(filteredNodes, [MESSAGES[4]], 1);
+
+ info(
+ "Filter out messages starting with 'foo', case-insensitive flag specified"
+ );
+ await setFilterInput(hud, "/^foo/i", MESSAGES[4]);
+ filteredNodes = outputNode.querySelectorAll(".message");
+ checkFilteredMessages(filteredNodes, [MESSAGES[1], MESSAGES[4]], 2);
+
+ info("Plaintext search if a wrong flag is specified");
+ await setFilterInput(hud, "/abc.com/q", MESSAGES[2]);
+ filteredNodes = outputNode.querySelectorAll(".message");
+ checkFilteredMessages(filteredNodes, [MESSAGES[2]], 1);
+});
+
+async function setFilterInput(hud, value, lastMessage) {
+ await setFilterState(hud, { text: value });
+ await waitFor(() => findConsoleAPIMessage(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..a5d3849434
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_filter_groups.js
@@ -0,0 +1,272 @@
+/* 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(
+ () => findConsoleAPIMessage(hud, "[a]") && findConsoleAPIMessage(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 = await findMessageVirtualizedByType({
+ hud,
+ text: "[a]",
+ typeSelector: ".console-api",
+ });
+ const groupJ = await findMessageVirtualizedByType({
+ hud,
+ text: "[j]",
+ typeSelector: ".console-api",
+ });
+
+ 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 = findConsoleAPIMessage(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..91c1bd2dbb
--- /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,<!DOCTYPE html>
+ <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(
+ () => findConsoleAPIMessage(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(
+ () => findMessageByType(hud, "Navigated to", ".navigationMarker"),
+ "Wait for navigation message to be rendered"
+ );
+
+ // Wait for 2 hellow world messages to be displayed.
+ await waitFor(
+ () => findConsoleAPIMessages(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(
+ () => !findConsoleAPIMessage(hud, "hello world"),
+ "Wait for the log messages to be hidden"
+ );
+ ok(
+ findMessageByType(hud, "Navigated to", ".navigationMarker"),
+ "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(
+ () => findMessageByType(hud, "Navigated to " + newUrl, ".navigationMarker"),
+ "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(
+ () => findMessageByType(hud, "Navigated to " + newUrl, ".navigationMarker"),
+ "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..cafef1fee1
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_filter_scroll.js
@@ -0,0 +1,108 @@
+/* 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,<!DOCTYPE html>
+ <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(() => findConsoleAPIMessage(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(
+ () => !findConsoleAPIMessage(hud, "init-0"),
+ 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(
+ () => findConsoleAPIMessage(hud, "init-0"),
+ 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;
+
+ info("Wait for the layout to stabilize");
+ await new Promise(r =>
+ window.requestAnimationFrame(() => TestUtils.executeSoon(r))
+ );
+
+ await setFilterState(hud, { text: "init-9" });
+ onMessagesFiltered = waitFor(
+ async () => !findConsoleAPIMessage(hud, "init-0"),
+ 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(
+ () => findConsoleAPIMessage(hud, "init-0"),
+ 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..8e1d194323
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_filters.js
@@ -0,0 +1,85 @@
+/* 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 () {
+ await pushPref("dom.security.https_first", false);
+ 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(() => findErrorMessage(hud, "status=404", ".network"));
+ await waitFor(() => findErrorMessage(hud, "status=500", ".network"));
+
+ // 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(
+ findAllMessages(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(() => findAllMessages(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(
+ findAllMessages(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..a67f8f53ed
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_filters_persist.js
@@ -0,0 +1,72 @@
+/* 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(() =>
+ findWarningMessage(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(() =>
+ findWarningMessage(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..c54f9cdf1f
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_highlighter_console_helper.js
@@ -0,0 +1,64 @@
+/* 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,<!DOCTYPE html>
+<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");
+ await selectNodeWithPicker(toolbox, "h1");
+
+ info("Picker mode stopped, <h1> selected, now switching to the console");
+ const hud = await openConsole();
+
+ await clearOutput(hud);
+
+ await executeAndWaitForResultMessage(hud, "$0", "<h1>");
+ ok(true, "correct output for $0");
+
+ await clearOutput(hud);
+
+ const newH1Content = "newH1Content";
+ await executeAndWaitForResultMessage(
+ hud,
+ `$0.textContent = "${newH1Content}";$0`,
+ "<h1>"
+ );
+
+ ok(true, "correct output for $0 after setting $0.textContent");
+ const textContent = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () => content.document.querySelector("h1").textContent
+ );
+ 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..3d8b4b3331
--- /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,<!DOCTYPE html>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 = waitForMessageByType(hud, text, ".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..8d3563975f
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_iframe_wrong_hud.js
@@ -0,0 +1,40 @@
+/* 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 =
+ "https://example.com/browser/devtools/client/webconsole/" +
+ "test/browser/test-iframe-wrong-hud.html";
+
+const TEST_IFRAME_URI =
+ "https://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 reloadBrowser({ browser: tab1.linkedBrowser });
+
+ info("Waiting for messages");
+ await waitFor(() => findMessageByType(hud1, TEST_IFRAME_URI, ".network"));
+
+ const hud2 = await openConsole(tab2);
+ is(
+ findMessageByType(hud2, TEST_IFRAME_URI, ".network"),
+ undefined,
+ "iframe network request is not displayed in tab2"
+ );
+});
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..dd928e745f
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_in_line_layout.js
@@ -0,0 +1,135 @@
+/* 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,<!DOCTYPE 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 = waitForMessageByType(
+ hud,
+ "simple text message",
+ ".console-api"
+ );
+ 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 = waitForMessageByType(
+ hud,
+ "message-100",
+ ".console-api"
+ );
+ 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..eb7ae081ae
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_ineffective_iframe_sandbox_warning.js
@@ -0,0 +1,53 @@
+/* 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 =
+ "https://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 = waitForMessageByType(hud, sentinel, ".console-api");
+
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [sentinel], function (msg) {
+ content.console.log(msg);
+ });
+ await onSentinelMessage;
+
+ const warning = findWarningMessage(hud, INEFFECTIVE_IFRAME_SANDBOXING_MSG);
+ 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..a24f4594b1
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_init.js
@@ -0,0 +1,37 @@
+/* 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 = waitForMessageByType(hud, "19", ".console-api");
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ content.wrappedJSObject.doLogs(20);
+ });
+
+ await receievedMessages;
+
+ const outputContainer = ui.outputNode.querySelector(".webconsole-output");
+ is(
+ (await findMessagesVirtualizedByType({ hud, typeSelector: ".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..98688adedf
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_input_field_focus_on_panel_select.js
@@ -0,0 +1,34 @@
+/* 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,<!DOCTYPE html><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..d6e1ec4e58
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_input_focus.js
@@ -0,0 +1,92 @@
+/* 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,<!DOCTYPE html>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(() =>
+ findConsoleAPIMessage(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,
+ });
+ 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,
+ });
+ 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..7fcd221cf2
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_insecure_passwords_about_blank_web_console_warning.js
@@ -0,0 +1,22 @@
+/* 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 () {
+ await pushPref("dom.security.https_first", false);
+ const hud = await openNewTabAndConsole(TEST_URI);
+ await waitFor(() => findWarningMessage(hud, INSECURE_PASSWORD_MSG), "", 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..7a426f0415
--- /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 () {
+ // testing insecure password warnings, hence disabling https-first
+ await pushPref("dom.security.https_first", false);
+ 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(() => findWarningMessage(hud, warningMessage));
+ 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..c17d45ded1
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_inspect_cross_domain_object.js
@@ -0,0 +1,130 @@
+/* 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 =
+ "https://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(() => findConsoleAPIMessage(hud, "foobar"));
+ } else {
+ hud = await openNewTabAndConsole(
+ "data:text/html;charset=utf8,<!DOCTYPE html><p>hello"
+ );
+ info(
+ "Navigate and wait for the 'foobar' message to be logged by the frame"
+ );
+ const onMessage = waitForMessageByType(hud, "foobar", ".console-api");
+ 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..b74a75fa30
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_keyboard_accessibility.js
@@ -0,0 +1,119 @@
+/* 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,<!DOCTYPE html><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(
+ () => findConsoleAPIMessage(hud, "console message 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(() => !findAllMessages(hud).length);
+ 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 = waitForMessageByType(
+ hud,
+ "another simple text message",
+ ".console-api"
+ );
+ 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(() => !findAllMessages(hud).length);
+ 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"
+ );
+
+ info("Ctrl-U should open view:source when input is focused");
+ hud.jsterm.focus();
+ const onTabOpen = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ url => url.startsWith("view-source:"),
+ true
+ );
+ EventUtils.synthesizeKey("u", { accelKey: true });
+ await onTabOpen;
+ ok(
+ true,
+ "The view source tab was opened with the expected keyboard shortcut"
+ );
+});
diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_lenient_this_warning.js b/devtools/client/webconsole/test/browser/browser_webconsole_lenient_this_warning.js
new file mode 100644
index 0000000000..1d8914b845
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_lenient_this_warning.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that calling the LenientThis warning is only called when expected.
+const TEST_URI = `data:text/html;charset=utf8,<!DOCTYPE html>${encodeURI(`
+ <h1>LenientThis warning</h1>
+ <script>
+ const el = document.createElement('div');
+ globalThis.htmlDivElementProto = Object.getPrototypeOf(el);
+ function triggerLenientThisWarning(){
+ Object.getOwnPropertyDescriptor(
+ Object.getPrototypeOf(globalThis.htmlDivElementProto),
+ 'onmouseenter'
+ ).get.call()
+ }
+ </script>`)}`;
+
+add_task(async function () {
+ const hud = await openNewTabAndConsole(TEST_URI);
+
+ const expectedWarningMessageText =
+ "Ignoring get or set of property that has [LenientThis] ";
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ const global = content.wrappedJSObject;
+ global.console.log(global.htmlDivElementProto);
+ });
+
+ info("Wait for a bit so any warning message could be displayed");
+ await wait(1000);
+ await waitFor(() => findConsoleAPIMessage(hud, "HTMLDivElementPrototype"));
+
+ ok(
+ !findWarningMessage(hud, expectedWarningMessageText, ".warn"),
+ "Displaying the HTMLDivElementPrototype does not trigger the LenientThis warning"
+ );
+
+ info(
+ "Call a LenientThis getter with the wrong `this` to trigger a warning message"
+ );
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ content.wrappedJSObject.triggerLenientThisWarning();
+ });
+
+ await waitFor(() =>
+ findWarningMessage(hud, expectedWarningMessageText, ".warn")
+ );
+ ok(
+ true,
+ "Calling the LenientThis getter with an unexpected `this` did triggered the warning"
+ );
+
+ info(
+ "Clear the console and call the LenientThis getter with an unexpected `this` again"
+ );
+ await clearOutput(hud);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ content.wrappedJSObject.triggerLenientThisWarning();
+ });
+ info("Wait for a bit so any warning message could be displayed");
+ await wait(1000);
+ ok(
+ !findWarningMessage(hud, expectedWarningMessageText, ".warn"),
+ "Calling the LenientThis getter a second time did not trigger the warning again"
+ );
+});
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..f32e59a67c
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_limit_multiline.js
@@ -0,0 +1,77 @@
+/* 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("resource://devtools/shared/l10n.js");
+
+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,<!DOCTYPE html><meta charset=utf8>Test multi-line commands expandability"
+ );
+ info("Test that we don't slice messages with <= 5 lines");
+ const message = await executeAndWaitForMessageByType(
+ hud,
+ SMALL_EXPRESSION,
+ "function fib",
+ ".command"
+ );
+
+ is(
+ message.node.querySelector(".collapse-button"),
+ null,
+ "Collapse button does not exist"
+ );
+
+ info("Test messages with > 5 lines are sliced");
+
+ const messageExp = await executeAndWaitForMessageByType(
+ hud,
+ LONG_EXPRESSION,
+ "function fib",
+ ".command"
+ );
+
+ 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..d984ff50a7
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_location_debugger_link.js
@@ -0,0 +1,51 @@
+/* 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 toolbox = await gDevTools.getToolboxForTab(gBrowser.selectedTab);
+
+ await testOpenInDebugger(hud, {
+ text: "document.bar",
+ typeSelector: ".error",
+ });
+
+ info("Selecting the console again");
+ await toolbox.selectTool("webconsole");
+ await testOpenInDebugger(hud, {
+ text: "Blah Blah",
+ typeSelector: ".console-api",
+ });
+
+ // // check again the first node.
+ info("Selecting the console again");
+ await toolbox.selectTool("webconsole");
+ await testOpenInDebugger(hud, {
+ text: "document.bar",
+ typeSelector: ".error",
+ });
+});
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..23c3a76ec9
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_location_logpoint_debugger_link.js
@@ -0,0 +1,187 @@
+/* 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 =
+ "https://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(() => findAllMessages(hud).length === 2);
+
+ await testOpenInDebugger(hud, {
+ text: "undefinedVariable is not defined",
+ typeSelector: ".logPointError",
+ expectUrl: true,
+ expectLine: false,
+ expectColumn: false,
+ logPointExpr: "undefinedVariable",
+ });
+
+ info("Selecting the console again");
+ await toolbox.selectTool("webconsole");
+ await testOpenInDebugger(hud, {
+ text: "a is 1",
+ typeSelector: ".logPoint",
+ expectUrl: true,
+ expectLine: false,
+ expectColumn: false,
+ logPointExpr: "`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, {
+ text: "undefinedVariable is not defined",
+ typeSelector: ".logPointError",
+ expectUrl: true,
+ expectLine: true,
+ expectColumn: true,
+ });
+
+ info("Selecting the console again");
+ await toolbox.selectTool("webconsole");
+ await testOpenInDebugger(hud, {
+ text: "a is 1",
+ typeSelector: ".logPoint",
+ expectUrl: true,
+ expectLine: true,
+ expectColumn: true,
+ });
+});
+
+// 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(() => findAllMessages(hud).length === 1);
+ await testOpenInDebugger(hud, {
+ text: "a is 1",
+ typeSelector: ".logPoint",
+ expectUrl: true,
+ expectLine: false,
+ expectColumn: false,
+ logPointExpr: "`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(() => findAllMessages(hud).length === 2);
+ await testOpenInDebugger(hud, {
+ text: "c is 1",
+ typeSelector: ".logPoint",
+ expectUrl: true,
+ expectLine: false,
+ expectColumn: false,
+ logPointExpr: "`c is ${c}`",
+ });
+});
+
+async function setLogPoint(dbg, index, expression) {
+ rightClickElement(dbg, "gutter", index);
+ await waitForContextMenu(dbg);
+ selectContextMenuItem(
+ dbg,
+ `${selectors.addLogItem},${selectors.editLogItem}`
+ );
+ const onBreakpointSet = waitForDispatch(dbg.store, "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..b4bb5cfe43
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_location_styleeditor_link.js
@@ -0,0 +1,82 @@
+/* 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 toolbox = await gDevTools.getToolboxForTab(gBrowser.selectedTab);
+
+ 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(
+ () => findWarningMessage(hud, text),
+ `couldn't find message containing "${text}"`
+ );
+ const messageLocationNode = messageNode.querySelector(".message-location");
+ ok(messageLocationNode, "The message does have a location link");
+
+ const onStyleEditorSelected = toolbox.once("styleeditor-selected");
+
+ 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"
+ );
+
+ const win = panel.panelWindow;
+ ok(win, "Style Editor Window is defined");
+ is(
+ win.location.toString(),
+ "chrome://devtools/content/styleeditor/index.xhtml",
+ "This is the expected styleEditor document"
+ );
+
+ info("Waiting the style editor to be focused");
+ await new Promise(resolve => waitForFocus(resolve, win));
+
+ info("style editor window focused");
+ const href = messageLocationNode.getAttribute("data-url");
+ const line = messageLocationNode.getAttribute("data-line");
+ const column = messageLocationNode.getAttribute("data-column");
+ ok(line, "found source line");
+
+ const editor = panel.UI.editors.find(e => e.styleSheet.href == href);
+ ok(editor, "found style editor for " + href);
+ await waitFor(
+ () => panel.UI.selectedStyleSheetIndex == editor.styleSheet.styleSheetIndex
+ );
+ ok(true, "correct stylesheet is selected in the editor");
+
+ info("wait for source editor to load and to move the cursor");
+ await editor.getSourceEditor();
+ await waitFor(() => editor.sourceEditor.getCursor().line !== 0);
+
+ // Get the updated line and column position if the CSS source was prettified.
+ const position = editor.translateCursorPosition(line - 1, column - 1);
+ const cursor = editor.sourceEditor.getCursor();
+ is(cursor.line, position.line, "correct line is selected");
+ is(cursor.ch, position.column, "correct column is selected");
+}
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..d00c39471c
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_logErrorInPage.js
@@ -0,0 +1,20 @@
+/* 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,<!DOCTYPE html><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(() => findErrorMessage(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..65ddee6662
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_logWarningInPage.js
@@ -0,0 +1,20 @@
+/* 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,<!DOCTYPE html><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(() => findWarningMessage(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_logging_exceptions.js b/devtools/client/webconsole/test/browser/browser_webconsole_logging_exceptions.js
new file mode 100644
index 0000000000..f353233c47
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_logging_exceptions.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Check that logging exceptions works as expected
+
+"use strict";
+
+const TEST_URI =
+ `data:text/html;charset=utf8,` +
+ encodeURI(`<!DOCTYPE html><script>
+ const domExceptionOnLine2 = new DOMException("Bar");
+ /* console.error will be on line 4 */
+ console.error("Foo", domExceptionOnLine2);
+</script>`);
+
+add_task(async function () {
+ const hud = await openNewTabAndConsole(TEST_URI);
+
+ info("Wait for the error to be logged");
+ const msgNode = await waitFor(() => findConsoleAPIMessage(hud, "Foo"));
+ ok(!msgNode.classList.contains("open"), `Error logged not expanded`);
+
+ const framesNode = await waitFor(() => msgNode.querySelector(".pane.frames"));
+ ok(framesNode, "The DOMException stack is displayed right away");
+
+ const frameNodes = framesNode.querySelectorAll(".frame");
+ is(frameNodes.length, 1, "Expected frames are displayed");
+ is(
+ frameNodes[0].querySelector(".line").textContent,
+ "2",
+ "The stack displayed by default refers to second argument passed to console.error and refers to DOMException callsite"
+ );
+
+ info(
+ "Check that the console.error stack is refering to console.error() callsite"
+ );
+ await checkMessageStack(hud, "Foo", [4]);
+});
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..3ee5d4c70d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_loglimit.js
@@ -0,0 +1,86 @@
+/* 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,<!DOCTYPE html>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 = waitForMessageByType(
+ hud,
+ "test message [149]",
+ ".console-api"
+ );
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ for (let i = 0; i < 150; i++) {
+ content.console.log(`test message [${i}]`);
+ }
+ });
+ await onMessage;
+
+ ok(
+ !(await findMessageVirtualizedByType({
+ hud,
+ text: "test message [0]",
+ typeSelector: ".console-api",
+ })),
+ "Message 0 has been pruned"
+ );
+ ok(
+ !(await findMessageVirtualizedByType({
+ hud,
+ text: "test message [9]",
+ typeSelector: ".console-api",
+ })),
+ "Message 9 has been pruned"
+ );
+ ok(
+ await findMessageVirtualizedByType({
+ hud,
+ text: "test message [10]",
+ typeSelector: ".console-api",
+ }),
+ "Message 10 is still displayed"
+ );
+ is(
+ (await findAllMessagesVirtualized(hud)).length,
+ 140,
+ "Number of displayed messages is correct"
+ );
+
+ onMessage = waitForMessageByType(hud, "hello world", ".console-api");
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ content.console.log("hello world");
+ });
+ await onMessage;
+
+ ok(
+ !(await findMessageVirtualizedByType({
+ hud,
+ text: "test message [10]",
+ typeSelector: ".console-api",
+ })),
+ "Message 10 has been pruned"
+ );
+ ok(
+ await findMessageVirtualizedByType({
+ hud,
+ text: "test message [11]",
+ typeSelector: ".console-api",
+ }),
+ "Message 11 is still displayed"
+ );
+ is(
+ (await findAllMessagesVirtualized(hud)).length,
+ 140,
+ "Number of displayed messages is still correct"
+ );
+});
diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_logs_exceptions_order.js b/devtools/client/webconsole/test/browser/browser_webconsole_logs_exceptions_order.js
new file mode 100644
index 0000000000..13ff9a843a
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_logs_exceptions_order.js
@@ -0,0 +1,41 @@
+/* -*- 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 (cached and live) logs and errors are displayed in the expected order
+// in the console output. See Bug 1483662.
+
+"use strict";
+
+const TEST_URI =
+ "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-logs-exceptions-order.html";
+
+add_task(async function () {
+ const hud = await openNewTabAndConsole(TEST_URI);
+ await checkConsoleOutput(hud);
+
+ info("Reload the content window");
+ await reloadBrowser();
+ await checkConsoleOutput(hud);
+});
+
+async function checkConsoleOutput(hud) {
+ await waitFor(
+ () =>
+ findConsoleAPIMessage(hud, "First") &&
+ findErrorMessage(hud, "Second") &&
+ findConsoleAPIMessage(hud, "Third") &&
+ findErrorMessage(hud, "Fourth")
+ );
+
+ const messagesText = Array.from(
+ hud.ui.outputNode.querySelectorAll(".message .message-body")
+ ).map(n => n.textContent);
+
+ Assert.deepEqual(
+ messagesText,
+ ["First", "Uncaught Second", "Third", "Uncaught Fourth"],
+ "Errors are displayed in the expected order"
+ );
+}
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..9009e86641
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_longstring.js
@@ -0,0 +1,49 @@
+/* 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,<!DOCTYPE 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 = waitForMessageByType(
+ hud,
+ LONGSTRING.slice(0, 50),
+ ".console-api"
+ );
+ 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(() =>
+ findConsoleAPIMessage(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(
+ () => !findConsoleAPIMessage(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..a98674984b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_longstring_getter.js
@@ -0,0 +1,48 @@
+/* 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,<!DOCTYPE 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(() =>
+ findConsoleAPIMessage(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(() =>
+ findConsoleAPIMessage(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(
+ () => !findConsoleAPIMessage(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..9413b7eaea
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_message_categories.js
@@ -0,0 +1,167 @@
+/* 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("resource://devtools/shared/constants.js");
+
+const TEST_URI =
+ "data:text/html;charset=utf-8,<!DOCTYPE html>Web Console test for " +
+ "bug 595934 - message categories coverage.";
+const TESTS_PATH =
+ "https://example.com/browser/devtools/client/webconsole/test/browser/";
+const TESTS = [
+ {
+ // #0
+ file: "test-message-categories-css-loader.html",
+ category: "CSS Loader",
+ matchString: "text/css",
+ typeSelector: ".error",
+ },
+ {
+ // #1
+ file: "test-message-categories-imagemap.html",
+ category: "Layout: ImageMap",
+ matchString: 'shape="rect"',
+ typeSelector: ".warn",
+ },
+ {
+ // #2
+ file: "test-message-categories-html.html",
+ category: "HTML",
+ matchString: "multipart/form-data",
+ typeSelector: ".warn",
+ onload() {
+ 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",
+ typeSelector: ".error",
+ },
+ {
+ // #4
+ file: "test-message-categories-malformedxml.xhtml",
+ category: "malformed-xml",
+ matchString: "no root element found",
+ typeSelector: ".error",
+ },
+ {
+ // #5
+ file: "test-message-categories-svg.xhtml",
+ category: "SVG",
+ matchString: "fooBarSVG",
+ typeSelector: ".warn",
+ },
+ {
+ // #6
+ file: "test-message-categories-css-parser.html",
+ category: MESSAGE_CATEGORY.CSS_PARSER,
+ matchString: "foobarCssParser",
+ typeSelector: ".warn",
+ },
+ {
+ // #7
+ file: "test-message-categories-malformedxml-external.html",
+ category: "malformed-xml",
+ matchString: "</html>",
+ typeSelector: ".error",
+ },
+ {
+ // #8
+ file: "test-message-categories-empty-getelementbyid.html",
+ category: "DOM",
+ matchString: "getElementById",
+ typeSelector: ".warn",
+ },
+ {
+ // #9
+ file: "test-message-categories-canvas-css.html",
+ category: MESSAGE_CATEGORY.CSS_PARSER,
+ matchString: "foobarCanvasCssParser",
+ typeSelector: ".warn",
+ },
+ {
+ // #10
+ file: "test-message-categories-image.html",
+ category: "Image",
+ matchString: "corrupt",
+ typeSelector: ".warn",
+ // This message is not displayed in the main console in e10s. Bug 1431731
+ skipInE10s: true,
+ },
+];
+
+add_task(async function () {
+ // Disable bfcache for Fission for now.
+ // If Fission is disabled, the pref is no-op.
+ await SpecialPowers.pushPrefEnv({
+ set: [["fission.bfcacheInParent", false]],
+ });
+
+ 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, typeSelector, onload, skipInE10s } =
+ test;
+
+ if (skipInE10s && Services.appinfo.browserTabsRemoteAutostart) {
+ return;
+ }
+
+ const onMessageLogged = waitForMessageByType(hud, matchString, typeSelector);
+
+ 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..51fa43082c
--- /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(() => findErrorMessage(hud, MSG), "", 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..d2a9b3d32e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_multiple_windows_and_tabs.js
@@ -0,0 +1,92 @@
+/* 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,<!DOCTYPE html>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.commands.descriptorFront.localTab.linkedBrowser;
+ const message = "message for tab " + tabs.indexOf(tab);
+
+ // Log a message in the newly opened console.
+ const onMessage = waitForMessageByType(hud, message, ".console-api");
+ 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..61930ed439
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_navigate_to_parse_error.js
@@ -0,0 +1,30 @@
+/* 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,<!DOCTYPE html>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 = waitForMessageByType(
+ hud,
+ CSP_VIOLATION_MSG,
+ ".warn"
+ );
+ 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..93001057be
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_network_attach.js
@@ -0,0 +1,72 @@
+/* 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.net", false);
+ await pushPref("devtools.webconsole.filter.netxhr", true);
+ await openNewTabAndToolbox(TEST_URI, "netmonitor");
+
+ const currentTab = gBrowser.selectedTab;
+ const toolbox = await gDevTools.getToolboxForTab(currentTab);
+ 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(() =>
+ findMessageByType(hud, xhrUrl, ".network")
+ );
+ 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..2cbca0776b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_network_exceptions.js
@@ -0,0 +1,29 @@
+/* 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,<!DOCTYPE html>Web Console test for bug 618078";
+const TEST_URI2 =
+ "https://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 = waitForMessageByType(hud, "bug618078exception", ".error");
+ 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..4969885571
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_network_message_close_on_escape.js
@@ -0,0 +1,56 @@
+/* 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 toolbox = await gDevTools.getToolboxForTab(currentTab);
+
+ const xhrUrl = TEST_PATH + "test-data.json";
+ const onMessage = waitForMessageByType(hud, xhrUrl, ".network");
+
+ // 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..9941ac6a21
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_network_message_ctrl_click.js
@@ -0,0 +1,68 @@
+// Test that URL opens in a new tab when click while
+// pressing CTR (or CMD in MacOS) as expected.
+
+"use strict";
+
+const TEST_URI =
+ "https://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(() =>
+ findMessageByType(hud, "test-console.html", ".network")
+ );
+ 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");
+
+ 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_after_target_switching.js b/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_after_target_switching.js
new file mode 100644
index 0000000000..d5833e06f8
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_after_target_switching.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that after a target switch, the network details (headers, cookies etc) are available
+// when the request message is expanded.
+
+"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.net", true);
+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() {
+ info("Add an empty tab and open the console");
+ const hud = await openNewTabAndConsole("");
+
+ const onMessageAvailable = waitForMessageByType(hud, TEST_URI, ".network");
+ info(`Navigate to ${TEST_URI}`);
+ await navigateTo(TEST_URI);
+ const { node } = await onMessageAvailable;
+
+ info(`Click on ${TEST_FILE} request`);
+ node.querySelector(".url").click();
+
+ info("Wait for the network detail panel to be displayed");
+ await waitFor(
+ () => node.querySelector(".network-info"),
+ "Wait for .network-info to be rendered"
+ );
+
+ // Test that headers information is showing
+ await waitFor(
+ () => node.querySelector("#headers-panel .headers-overview"),
+ "Headers overview info is visible"
+ );
+});
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..96da695208
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_expand.js
@@ -0,0 +1,261 @@
+/* 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;
+const XHR_URL = TEST_PATH + "sjs_slow-response-test-server.sjs";
+
+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);
+
+/**
+ * Main test for checking HTTP logs in the Console panel.
+ */
+add_task(async function task() {
+ const hud = await openNewTabAndConsole(TEST_URI);
+
+ const messageNode = await doXhrAndExpand(hud);
+
+ await testNetworkMessage(hud.toolbox, messageNode);
+
+ await closeToolbox();
+});
+
+add_task(async function task() {
+ info(
+ "Verify that devtools.netmonitor.saveRequestAndResponseBodies=false disable response content collection"
+ );
+ await pushPref("devtools.netmonitor.saveRequestAndResponseBodies", false);
+ const hud = await openNewTabAndConsole(TEST_URI);
+
+ const messageNode = await doXhrAndExpand(hud);
+
+ const responseTab = messageNode.querySelector("#response-tab");
+ ok(responseTab, "Response tab is available");
+
+ const {
+ TEST_EVENTS,
+ } = require("resource://devtools/client/netmonitor/src/constants.js");
+ const onResponseContent = hud.ui.once(TEST_EVENTS.RECEIVED_RESPONSE_CONTENT);
+ // Select Response tab and check the content.
+ responseTab.click();
+
+ // Even if response content aren't collected by NetworkObserver,
+ // we do try to fetch the content via an RDP request, which
+ // we try to wait for here.
+ info("Wait for the async getResponseContent request");
+ await onResponseContent;
+ const responsePanel = messageNode.querySelector("#response-panel");
+
+ // This is updated only after we tried to fetch the response content
+ // and fired the getResponseContent request
+ info("Wait for the empty response content");
+ ok(
+ responsePanel.querySelector("div.empty-notice"),
+ "An empty notice is displayed instead of the response content"
+ );
+ const responseContent = messageNode.querySelector(
+ "#response-panel .editor-row-container .CodeMirror"
+ );
+ ok(!responseContent, "Response content is really not displayed");
+
+ await waitForLazyRequests(hud.toolbox);
+ await closeToolbox();
+});
+
+async function doXhrAndExpand(hud) {
+ // Execute XHR and expand it after all network
+ // update events are received. Consequently,
+ // check out content of all (HTTP details) tabs.
+ const onMessage = waitForMessageByType(hud, XHR_URL, ".network");
+ 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;
+
+ return messageNode;
+}
+
+// Panel testing helpers
+
+async function testNetworkMessage(toolbox, messageNode) {
+ await testStatusInfo(messageNode);
+ await testHeaders(messageNode);
+ await testCookies(messageNode);
+ await testRequest(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
+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"),
+ "Wait for .header-overview to be rendered"
+ );
+}
+
+// Cookies
+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"),
+ "Wait for .treeValueCell to be rendered"
+ );
+}
+
+// Request
+async function testRequest(messageNode) {
+ const requestTab = messageNode.querySelector("#request-tab");
+ ok(requestTab, "Request tab is available");
+
+ // Select Request tab and check the content. CodeMirror initialization
+ // is delayed to prevent UI freeze, so wait for a little while.
+ requestTab.click();
+ const requestPanel = messageNode.querySelector("#request-panel");
+ await waitForSourceEditor(requestPanel);
+ const requestContent = requestPanel.querySelector(
+ ".panel-container .CodeMirror"
+ );
+ ok(requestContent, "Request content is available");
+ ok(
+ requestContent.textContent.includes("Hello world!"),
+ "Request POST body is correct"
+ );
+}
+
+// Response
+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
+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();
+ const timingsContent = await waitFor(() =>
+ messageNode.querySelector(
+ "#timings-panel .timings-container .timings-label",
+ "Wait for .timings-label to be rendered"
+ )
+ );
+ ok(timingsContent, "Timings content is available");
+ ok(timingsContent.textContent, "Timings text is available");
+}
+
+// Stack Trace
+async function testStackTrace(messageNode) {
+ const stackTraceTab = messageNode.querySelector("#stack-trace-tab");
+ ok(stackTraceTab, "StackTrace tab is available");
+
+ // Select Stack Trace tab and check the content.
+ stackTraceTab.click();
+ await waitFor(
+ () => messageNode.querySelector("#stack-trace-panel .frame-link"),
+ "Wait for .frame-link to be rendered"
+ );
+}
+
+// Security
+async function testSecurity(messageNode) {
+ const securityTab = messageNode.querySelector("#security-tab");
+ ok(securityTab, "Security tab is available");
+
+ // Select Security tab and check the content.
+ securityTab.click();
+ await waitFor(
+ () => messageNode.querySelector("#security-panel .treeTable .treeRow"),
+ "Wait for #security-panel .treeTable .treeRow to be rendered"
+ );
+}
+
+// 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"),
+ "Wait for .network-info to be rendered"
+ );
+}
diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_expand_before_updates.js b/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_expand_before_updates.js
new file mode 100644
index 0000000000..1e25729b97
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_expand_before_updates.js
@@ -0,0 +1,316 @@
+/* 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(4);
+
+pushPref("devtools.webconsole.filter.net", false);
+pushPref("devtools.webconsole.filter.netxhr", true);
+
+// Update waitFor default interval (10ms) to avoid test timeouts.
+// The test often times out on waitFor statements use a 50ms interval instead.
+waitFor.overrideIntervalForTestFile = 50;
+
+const tabs = [
+ {
+ id: "headers",
+ testEmpty: testEmptyHeaders,
+ testContent: testHeaders,
+ },
+ {
+ id: "cookies",
+ testEmpty: testEmptyCookies,
+ testContent: testCookies,
+ },
+ {
+ id: "request",
+ testEmpty: testEmptyRequest,
+ testContent: testRequest,
+ },
+ {
+ 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);
+
+ // 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) {
+ info(`Test "${tab.id}" panel`);
+ await openRequestBeforeUpdates(hud, tab);
+ }
+});
+
+async function openRequestBeforeUpdates(hud, tab) {
+ const toolbox = hud.toolbox;
+
+ await clearOutput(hud);
+
+ const xhrUrl = TEST_PATH + "sjs_slow-response-test-server.sjs";
+ const onMessage = waitForMessageByType(hud, xhrUrl, ".network");
+
+ // Fire an XHR POST request.
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ content.wrappedJSObject.testXhrPostSlowResponse();
+ });
+ info(`Wait for ${xhrUrl} message`);
+ 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"
+ );
+
+ if (tab.testEmpty) {
+ info("Test that the tab is empty");
+ tab.testEmpty(messageNode);
+ }
+ }
+
+ info("Test content of the default tab");
+ await tab.testContent(messageNode);
+
+ info("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 testRequest(messageNode);
+ await testResponse(messageNode);
+ await testTimings(messageNode);
+ await testStackTrace(messageNode);
+ await testSecurity(messageNode);
+ await waitForLazyRequests(toolbox);
+}
+
+// Status Info
+async function testStatusInfo(messageNode) {
+ const statusInfo = await waitFor(() =>
+ 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"),
+ "Wait for .header-overview to be rendered"
+ );
+}
+
+// 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"),
+ "Wait for .treeValueCell to be rendered"
+ );
+}
+
+// Request
+function testEmptyRequest(messageNode) {
+ const emptyNotice = messageNode.querySelector("#request-panel .empty-notice");
+ ok(emptyNotice, "Request tab is empty");
+}
+
+async function testRequest(messageNode) {
+ const requestTab = messageNode.querySelector("#request-tab");
+ ok(requestTab, "Request tab is available");
+
+ // Select Request tab and check the content. CodeMirror initialization
+ // is delayed to prevent UI freeze, so wait for a little while.
+ requestTab.click();
+ const requestPanel = messageNode.querySelector("#request-panel");
+ await waitForSourceEditor(requestPanel);
+ const requestContent = requestPanel.querySelector(
+ ".panel-container .CodeMirror"
+ );
+ ok(requestContent, "Request content is available");
+ ok(
+ requestContent.textContent.includes("Hello world!"),
+ "Request POST body is correct"
+ );
+}
+
+// Response
+function testEmptyResponse(messageNode) {
+ const panel = messageNode.querySelector("#response-panel .tab-panel");
+ is(
+ panel.textContent,
+ "No response data available for this request",
+ "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");
+ const responsePayloadHeader = await waitFor(() =>
+ responsePanel.querySelector(".data-header")
+ );
+ // Expand the header if it wasn't yet.
+ if (responsePayloadHeader.getAttribute("aria-expanded") === "false") {
+ responsePayloadHeader.click();
+ }
+ 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, "No timings for this request", "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();
+ const timingsContent = await waitFor(() =>
+ messageNode.querySelector(
+ "#timings-panel .timings-container .timings-label",
+ "Wait for .timings-label to be rendered"
+ )
+ );
+ 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 .tab-panel");
+ 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 Stack Trace tab and check the content.
+ stackTraceTab.click();
+ await waitFor(
+ () => messageNode.querySelector("#stack-trace-panel .frame-link"),
+ "Wait for .frame-link to be rendered"
+ );
+}
+
+// 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 = await waitFor(() =>
+ messageNode.querySelector("#security-tab")
+ );
+ ok(securityTab, "Security tab is available");
+
+ // Select Security tab and check the content.
+ securityTab.click();
+ await waitFor(
+ () => messageNode.querySelector("#security-panel .treeTable .treeRow"),
+ "Wait for #security-panel .treeTable .treeRow to be rendered"
+ );
+}
+
+async function waitForSourceEditor(panel) {
+ return waitUntil(() => {
+ return !!panel.querySelector(".CodeMirror");
+ });
+}
+
+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"),
+ "Wait for .network-info to be rendered"
+ );
+}
diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_html_preview.js b/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_html_preview.js
new file mode 100644
index 0000000000..000e4f089d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_html_preview.js
@@ -0,0 +1,192 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if different response content types are handled correctly.
+ */
+
+const httpServer = createTestHTTPServer();
+httpServer.registerContentType("html", "text/html");
+
+const BASE_URL = `http://localhost:${httpServer.identity.primaryPort}/`;
+
+const REDIRECT_URL = BASE_URL + "redirect.html";
+
+// In all content previewed as HTML we ensure using proper html, head and body in order to
+// prevent having them added by the <browser> when loaded as a preview.
+function addBaseHtmlElements(body) {
+ return `<html><head><meta charset="utf8"></head><body>${body}</body></html>`;
+}
+
+// This first page asserts we can redirect to another URL, even if JS happen to be executed
+const FETCH_CONTENT_1 = addBaseHtmlElements(
+ `Fetch 1<script>window.parent.location.href = "${REDIRECT_URL}";</script>`
+);
+// This second page asserts that JS is disabled
+const FETCH_CONTENT_2 = addBaseHtmlElements(
+ `Fetch 2<script>document.write("JS activated")</script>`
+);
+// This third page asserts responses with line breaks
+const FETCH_CONTENT_3 = addBaseHtmlElements(`
+ <a href="#" id="link1">link1</a>
+ <a href="#" id="link2">link2</a>
+`);
+// This fourth page asserts that links and forms are disabled
+const FETCH_CONTENT_4 = addBaseHtmlElements(
+ `Fetch 3<a href="${REDIRECT_URL}">link</a> -- <form action="${REDIRECT_URL}"><input type="submit"></form>`
+);
+
+// Use fetch in order to prevent actually running this code in the test page
+const TEST_HTML = addBaseHtmlElements(`HTML<script>
+ fetch("${BASE_URL}fetch-1.html");
+ fetch("${BASE_URL}fetch-2.html");
+ fetch("${BASE_URL}fetch-3.html");
+ fetch("${BASE_URL}fetch-4.html");
+</script>`);
+const TEST_URL = BASE_URL + "doc-html-preview.html";
+
+httpServer.registerPathHandler(
+ "/doc-html-preview.html",
+ (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(TEST_HTML);
+ }
+);
+httpServer.registerPathHandler("/fetch-1.html", (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(FETCH_CONTENT_1);
+});
+httpServer.registerPathHandler("/fetch-2.html", (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(FETCH_CONTENT_2);
+});
+httpServer.registerPathHandler("/fetch-3.html", (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(FETCH_CONTENT_3);
+});
+httpServer.registerPathHandler("/fetch-4.html", (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(FETCH_CONTENT_4);
+});
+httpServer.registerPathHandler("/redirect.html", (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write("Redirected!");
+});
+
+/**
+ * Main test for checking HTTP logs in the Console panel.
+ */
+add_task(async function task() {
+ // Display network requests
+ pushPref("devtools.webconsole.filter.net", true);
+ pushPref("devtools.webconsole.filter.netxhr", true);
+ // Enable async events so that clicks on preview iframe's links are correctly
+ // going through the parent process which is meant to cancel any mousedown.
+ await pushPref("test.events.async.enabled", true);
+
+ const hud = await openNewTabAndConsole(TEST_URL);
+ await reloadBrowser();
+
+ await expandNetworkRequestAndWaitForHtmlView({
+ hud,
+ url: "doc-html-preview.html",
+ expectedHtml: TEST_HTML,
+ });
+ await expandNetworkRequestAndWaitForHtmlView({
+ hud,
+ url: "fetch-1.html",
+ expectedHtml: FETCH_CONTENT_1,
+ });
+ await expandNetworkRequestAndWaitForHtmlView({
+ hud,
+ url: "fetch-2.html",
+ expectedHtml: FETCH_CONTENT_2,
+ });
+ await expandNetworkRequestAndWaitForHtmlView({
+ hud,
+ url: "fetch-3.html",
+ expectedHtml: FETCH_CONTENT_3,
+ });
+
+ info("Try to click on the link and submit the form");
+ await expandNetworkRequestAndWaitForHtmlView({
+ hud,
+ url: "fetch-4.html",
+ expectedHtml: FETCH_CONTENT_4,
+ });
+});
+
+async function expandNetworkRequestAndWaitForHtmlView({
+ hud,
+ url,
+ expectedHtml,
+}) {
+ info(`Wait for ${url} message`);
+
+ const node = await waitFor(() => findMessageByType(hud, url, ".network"));
+ ok(node, `Network message found for ${url}`);
+
+ info("Expand the message and open the response tab");
+ const onPayloadReady = waitForPayloadReady(hud);
+ await expandXhrMessage(node);
+ await onPayloadReady;
+ node.querySelector("#response-tab").click();
+
+ info("Wait for the iframe to be rendered and loaded");
+ const iframe = await waitFor(() =>
+ node.querySelector("#response-panel .html-preview iframe")
+ );
+
+ // <xul:iframe type=content remote=true> don't emit "load" event.
+ // And SpecialPowsers.spawn throws if kept running during a page load.
+ // So poll for the end of the iframe load...
+ await waitFor(async () => {
+ // Note that if spawn executes early, the iframe may not yet be loading
+ // and would throw for the reason mentioned in previous comment.
+ try {
+ const rv = await SpecialPowers.spawn(iframe.browsingContext, [], () => {
+ return content.document.readyState == "complete";
+ });
+ return rv;
+ } catch (e) {
+ return false;
+ }
+ });
+
+ is(
+ iframe.browsingContext.currentWindowGlobal.isInProcess,
+ false,
+ "The preview is loaded in a content process"
+ );
+
+ await SpecialPowers.spawn(
+ iframe.browsingContext,
+ [expectedHtml],
+ async function (_expectedHtml) {
+ is(
+ content.document.documentElement.outerHTML,
+ _expectedHtml,
+ "iframe has the expected HTML"
+ );
+ }
+ );
+
+ return iframe;
+}
+
+async function waitForPayloadReady(hud) {
+ return hud.ui.once("network-request-payload-ready");
+}
+
+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"),
+ "Wait for .network-info to be rendered"
+ );
+}
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..0a4e21a3d6
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_openinnet.js
@@ -0,0 +1,111 @@
+/* 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,<!DOCTYPE html>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 =
+ "https://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 toolbox = await gDevTools.getToolboxForTab(currentTab);
+
+ 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("resource://devtools/client/netmonitor/src/selectors/index.js");
+
+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..900e3bfc34
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_resend_request.js
@@ -0,0 +1,48 @@
+/* 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,<!DOCTYPE html>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 =
+ "https://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(() =>
+ findMessageByType(hud, documentUrl, ".network")
+ );
+
+ 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
+ menuPopup.activateItem(openResendRequestMenuItem);
+ await waitFor(
+ () => findMessagesByType(hud, documentUrl, ".network").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..0326b24f98
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_stacktrace_console_initiated_request.js
@@ -0,0 +1,66 @@
+/* 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 executeAndWaitForMessageByType(
+ hud,
+ `
+ xhrConsole = () => testXhrPostSlowResponse();
+ xhrConsole();
+ `,
+ xhrUrl,
+ ".network"
+ );
+
+ 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..9d59a3fe63
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_network_messages_status_code.js
@@ -0,0 +1,72 @@
+/* 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;
+
+const NET_PREF = "devtools.webconsole.filter.net";
+const XHR_PREF = "devtools.webconsole.filter.netxhr";
+const {
+ l10n,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+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(() =>
+ findMessageByType(hud, xhrUrl, ".network")
+ );
+ ok(!!messageNode, "Network message found.");
+
+ const statusCodeNode = await waitFor(() =>
+ messageNode.querySelector(".status-code")
+ );
+ is(
+ statusCodeNode.title,
+ l10n.getStr("webConsoleMoreInfoLabel"),
+ "Status code has the expected tooltip"
+ );
+
+ info("Left click status code node and observe the link opens.");
+ const { link, where } = await simulateLinkClick(statusCodeNode);
+ is(link, LEARN_MORE_URI, `Clicking the provided link opens ${link}`);
+ is(where, "tab", "Link opened in correct tab.");
+
+ info("Right click status code node and observe the context menu opening.");
+ await openContextMenu(hud, statusCodeNode);
+ await hideContextMenu(hud);
+
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
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..8cc98024e9
--- /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(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..52daeec7a1
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_network_reset_filter.js
@@ -0,0 +1,81 @@
+/* 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 =
+ "https://example.com/browser/devtools/client/webconsole/" + "test/browser/";
+const TEST_URI = "data:text/html;charset=utf8,<!DOCTYPE html><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 = waitForMessagesByType({
+ hud,
+ messages: [
+ {
+ text: "running network console logging tests",
+ typeSelector: ".console-api",
+ },
+ {
+ text: "test-network.html",
+ typeSelector: ".network",
+ },
+ {
+ text: "testscript.js",
+ typeSelector: ".network",
+ },
+ ],
+ });
+
+ 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_network_unicode.js b/devtools/client/webconsole/test/browser/browser_webconsole_network_unicode.js
new file mode 100644
index 0000000000..338cede3d1
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_network_unicode.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that Unicode characters within the domain are displayed
+// encoded and not in Punycode or somehow garbled.
+
+"use strict";
+
+const TEST_URL =
+ "http://example.com/browser/devtools/client/webconsole/test/browser/test-network-request.html";
+
+add_task(async function () {
+ await pushPref("devtools.webconsole.filter.netxhr", true);
+
+ const toolbox = await openNewTabAndToolbox(TEST_URL, "webconsole");
+ const hud = toolbox.getCurrentPanel().hud;
+
+ const onMessage = waitForMessageByType(hud, "testxhr", ".network");
+
+ const XHR_TEST_URL_WITHOUT_PARAMS = "http://flüge.example.com/testxhr";
+ const XHR_TEST_URL = XHR_TEST_URL_WITHOUT_PARAMS + "?foo";
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [XHR_TEST_URL], url => {
+ content.fetch(url);
+ });
+
+ info("Wait for expected messages to appear");
+ const message = await onMessage;
+
+ const urlNode = message.node.querySelector(".url");
+ is(
+ urlNode.textContent,
+ XHR_TEST_URL,
+ "The network call is displayed with the expected URL"
+ );
+});
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..b95793c71c
--- /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 highlighterTestFront = await getHighlighterTestFront(toolbox);
+ const highlighter = toolbox.getHighlighter();
+ let onHighlighterShown;
+ let onHighlighterHidden;
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.wrappedJSObject.logNode("h1");
+ });
+
+ const msg = await waitFor(() => findConsoleAPIMessage(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 highlighterTestFront.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 highlighterTestFront.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..d601a987ff
--- /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(() => findConsoleAPIMessage(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..5374ee340d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_non_javascript_mime_warning.js
@@ -0,0 +1,20 @@
+/* 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 =
+ "https://example.com/browser/devtools/client/webconsole/" +
+ "test/browser/" +
+ "test-non-javascript-mime.html";
+const MIME_WARNING_MSG =
+ "The script from “https://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(() => findWarningMessage(hud, MIME_WARNING_MSG), "", 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..1e0e031b80
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_non_javascript_mime_worker_error.js
@@ -0,0 +1,26 @@
+/* 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 =
+ "https://example.com/browser/devtools/client/webconsole/" +
+ "test/browser/" +
+ "test-non-javascript-mime-worker.html";
+
+const JS_URI =
+ "https://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(() => findErrorMessage(hud, MIME_ERROR_MSG1), "", 100);
+ await waitFor(() => findErrorMessage(hud, MIME_ERROR_MSG2), "", 100);
+ ok(true, "MIME type error displayed");
+});
diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_non_standard_doctype_errors.js b/devtools/client/webconsole/test/browser/browser_webconsole_non_standard_doctype_errors.js
new file mode 100644
index 0000000000..c7be1b94ca
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_non_standard_doctype_errors.js
@@ -0,0 +1,126 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that warning messages are displayed for documents with non-standards doctype
+
+const QUIRKY_DOCTYPE = "<!DOCTYPE xhtml2>";
+const ALMOST_STANDARD_DOCTYPE = `<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN""http://www.w3.org/TR/html4/loose.dtd">`;
+const STANDARD_DOCTYPE = "<!DOCTYPE html>";
+
+const TEST_URI_QUIRKY_DOCTYPE = `data:text/html,${QUIRKY_DOCTYPE}<meta charset="utf8"><h1>Quirky</h1>`;
+const TEST_URI_ALMOST_STANDARD_DOCTYPE = `data:text/html,${ALMOST_STANDARD_DOCTYPE}<meta charset="utf8"><h1>Almost standard</h1>`;
+const TEST_URI_NO_DOCTYPE = `data:text/html,<meta charset="utf8"><h1>No DocType</h1>`;
+const TEST_URI_STANDARD_DOCTYPE = `data:text/html,${STANDARD_DOCTYPE}<meta charset="utf8"><h1>Standard</h1>`;
+
+const LEARN_MORE_URI =
+ "https://developer.mozilla.org/docs/Web/HTML/Quirks_Mode_and_Standards_Mode" +
+ DOCS_GA_PARAMS;
+
+add_task(async function () {
+ info("Navigate to page with quirky doctype");
+ const hud = await openNewTabAndConsole(TEST_URI_QUIRKY_DOCTYPE);
+
+ const quirkyDocTypeMessage = await waitFor(() =>
+ findWarningMessage(
+ hud,
+ `This page is in Quirks Mode. Page layout may be impacted. For Standards Mode use “<!DOCTYPE html>”`
+ )
+ );
+ ok(!!quirkyDocTypeMessage, "Quirky doctype warning message is visible");
+
+ info("Clicking on the Learn More link");
+ const quirkyDocTypeMessageLearnMoreLink =
+ quirkyDocTypeMessage.querySelector(".learn-more-link");
+ let linkSimulation = await simulateLinkClick(
+ quirkyDocTypeMessageLearnMoreLink
+ );
+
+ is(
+ linkSimulation.link,
+ LEARN_MORE_URI,
+ `Clicking the provided link opens expected URL`
+ );
+ is(linkSimulation.where, "tab", `Clicking the provided link opens in a tab`);
+
+ info("Navigate to page with almost standard doctype");
+ await navigateTo(TEST_URI_ALMOST_STANDARD_DOCTYPE);
+
+ const almostStandardDocTypeMessage = await waitFor(() =>
+ findWarningMessage(
+ hud,
+ `This page is in Almost Standards Mode. Page layout may be impacted. For Standards Mode use “<!DOCTYPE html>”`
+ )
+ );
+ ok(
+ !!almostStandardDocTypeMessage,
+ "Almost standards mode doctype warning message is visible"
+ );
+
+ info("Clicking on the Learn More link");
+ const almostStandardDocTypeMessageLearnMoreLink =
+ almostStandardDocTypeMessage.querySelector(".learn-more-link");
+ linkSimulation = await simulateLinkClick(
+ almostStandardDocTypeMessageLearnMoreLink
+ );
+
+ is(
+ linkSimulation.link,
+ LEARN_MORE_URI,
+ `Clicking the provided link opens expected URL`
+ );
+ is(linkSimulation.where, "tab", `Clicking the provided link opens in a tab`);
+
+ info("Navigate to page with no doctype");
+ await navigateTo(TEST_URI_NO_DOCTYPE);
+
+ const noDocTypeMessage = await waitFor(() =>
+ findWarningMessage(
+ hud,
+ `This page is in Quirks Mode. Page layout may be impacted. For Standards Mode use “<!DOCTYPE html>”`
+ )
+ );
+ ok(!!noDocTypeMessage, "No doctype warning message is visible");
+
+ info("Clicking on the Learn More link");
+ const noDocTypeMessageLearnMoreLink =
+ noDocTypeMessage.querySelector(".learn-more-link");
+ linkSimulation = await simulateLinkClick(noDocTypeMessageLearnMoreLink);
+
+ is(
+ linkSimulation.link,
+ LEARN_MORE_URI,
+ `Clicking the provided link opens expected URL`
+ );
+ is(linkSimulation.where, "tab", `Clicking the provided link opens in a tab`);
+
+ info("Navigate to a page with standard doctype");
+ await navigateTo(TEST_URI_STANDARD_DOCTYPE);
+ info("Wait for a bit to make sure there is no doctype messages");
+ await wait(1000);
+ ok(
+ !findWarningMessage(hud, `doctype`),
+ "There is no doctype warning message"
+ );
+
+ info("Navigate to a about:blank");
+ await navigateTo("about:blank");
+ info("Wait for a bit to make sure there is no doctype messages");
+ await wait(1000);
+ ok(
+ !findWarningMessage(hud, `doctype`),
+ "There is no doctype warning message for about:blank"
+ );
+
+ info("Navigate to a view-source uri");
+ await navigateTo(`view-source:${TEST_URI_NO_DOCTYPE}`);
+ info("Wait for a bit to make sure there is no doctype messages");
+ await wait(1000);
+ ok(
+ !findWarningMessage(hud, `doctype`),
+ "There is no doctype warning message for view-source"
+ );
+
+ await closeConsole();
+});
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..a7e7054245
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_ctrl_click.js
@@ -0,0 +1,125 @@
+/* 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,<!DOCTYPE html><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 = findConsoleAPIMessage(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,
+ interactiveRule: false,
+ focusableRule: false,
+ labelRule: false,
+ });
+ 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,
+ });
+ 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..8576886935
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_in_sidebar_keyboard_nav.js
@@ -0,0 +1,105 @@
+/* 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,<!DOCTYPE html>
+ <script>
+ console.log({
+ a:1,
+ b:2,
+ c: Array.from({length: 100}, (_, i) => i)
+ });
+ </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(() => findConsoleAPIMessage(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 = waitForMessageByType(hud, "Array", ".console-api");
+ 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("Array(3) [ 4, 5, 6 ]")
+ );
+ 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..2340c54e88
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector.js
@@ -0,0 +1,157 @@
+/* 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,<!DOCTYPE html><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(() => findConsoleAPIMessage(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..9ae55ce190
--- /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,<!DOCTYPE html><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(() => findConsoleAPIMessage(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_array_getters.js b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_array_getters.js
new file mode 100644
index 0000000000..0a801de5eb
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_array_getters.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check evaluating and expanding getters on an array in the console.
+const TEST_URI =
+ "data:text/html;charset=utf8,<!DOCTYPE html><h1>Object Inspector on Getters on an Array</h1>";
+
+add_task(async function () {
+ const hud = await openNewTabAndConsole(TEST_URI);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ const array = [];
+ Object.defineProperty(array, 0, {
+ get: () => "elem0",
+ });
+ Object.defineProperty(array, 1, {
+ set: x => {},
+ });
+ Object.defineProperty(array, 2, {
+ get: () => "elem2",
+ set: x => {},
+ });
+ content.wrappedJSObject.console.log("oi-array-test", array);
+ });
+
+ const node = await waitFor(() => findConsoleAPIMessage(hud, "oi-array-test"));
+ const oi = node.querySelector(".tree");
+
+ const arrayText = oi.querySelector(".objectBox-array");
+
+ is(
+ arrayText.textContent,
+ "Array(3) [ Getter, Setter, Getter & Setter ]",
+ "Elements with getter/setter should be shown correctly"
+ );
+
+ expandObjectInspectorNode(oi);
+ await waitFor(() => getObjectInspectorNodes(oi).length > 1);
+
+ await testGetter(oi, "0");
+ await testSetterOnly(oi, "1");
+ await testGetter(oi, "2");
+});
+
+async function testGetter(oi, index) {
+ let node = findObjectInspectorNode(oi, index);
+ is(
+ isObjectInspectorNodeExpandable(node),
+ false,
+ `The ${index} node can't be expanded`
+ );
+ const invokeButton = getObjectInspectorInvokeGetterButton(node);
+ ok(invokeButton, `There is an invoke button for ${index} as expected`);
+
+ invokeButton.click();
+ await waitFor(
+ () =>
+ !getObjectInspectorInvokeGetterButton(findObjectInspectorNode(oi, index))
+ );
+
+ node = findObjectInspectorNode(oi, index);
+ ok(
+ node.textContent.includes(`${index}: "elem${index}"`),
+ "Element ${index} now has the expected text content"
+ );
+ is(
+ isObjectInspectorNodeExpandable(node),
+ false,
+ `The ${index} node can't be expanded`
+ );
+}
+
+async function testSetterOnly(oi, index) {
+ const node = findObjectInspectorNode(oi, index);
+ is(
+ isObjectInspectorNodeExpandable(node),
+ false,
+ `The ${index} node can't be expanded`
+ );
+ const invokeButton = getObjectInspectorInvokeGetterButton(node);
+ ok(!invokeButton, `There is no invoke button for ${index}`);
+
+ ok(
+ node.textContent.includes(`${index}: Setter`),
+ "Element ${index} now has the expected text content"
+ );
+ is(
+ isObjectInspectorNodeExpandable(node),
+ false,
+ `The ${index} node can't be expanded`
+ );
+}
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..6e6b175e54
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_entries.js
@@ -0,0 +1,722 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check expanding/collapsing object with entries (Maps, Sets, URLSearchParams, …) in the console.
+const TEST_URI = `https://example.com/document-builder.sjs?html=${encodeURIComponent(
+ `<!DOCTYPE html><h1>Object Inspector on Object with entries</h1>`
+)}`;
+
+const { ELLIPSIS } = require("resource://devtools/shared/l10n.js");
+
+add_task(async function () {
+ // This will make it so we'll have stable MIDI devices reported
+ await pushPref("midi.testing", true);
+ await pushPref("dom.webmidi.enabled", true);
+ await pushPref("midi.prompt.testing", true);
+ await pushPref("media.navigator.permission.disabled", true);
+
+ const hud = await openNewTabAndConsole(TEST_URI);
+
+ logAllStoreChanges(hud);
+
+ const taskResult = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function () {
+ const formData = new content.FormData();
+ formData.append("a", 1);
+ formData.append("a", 2);
+ formData.append("b", 3);
+
+ const midiAccess = Cu.waiveXrays(
+ await content.wrappedJSObject.navigator.requestMIDIAccess()
+ );
+
+ 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)),
+ new content.URLSearchParams([
+ ["a", 1],
+ ["a", 2],
+ ["b", 3],
+ ["b", 3],
+ ["b", 5],
+ ["c", "this is 6"],
+ ["d", 7],
+ ["e", 8],
+ ["f", 9],
+ ["g", 10],
+ ["h", 11],
+ ]),
+ new content.Headers({ a: 1, b: 2, c: 3 }),
+ formData,
+ midiAccess.inputs,
+ midiAccess.outputs
+ );
+
+ return {
+ midi: {
+ inputs: [...midiAccess.inputs.values()].map(input => ({
+ id: input.id,
+ name: input.name,
+ type: input.type,
+ manufacturer: input.manufacturer,
+ })),
+ outputs: [...midiAccess.outputs.values()].map(output => ({
+ id: output.id,
+ name: output.name,
+ type: output.type,
+ manufacturer: output.manufacturer,
+ })),
+ },
+ };
+ }
+ );
+
+ const node = await waitFor(() =>
+ findConsoleAPIMessage(hud, "oi-entries-test")
+ );
+ const objectInspectors = [...node.querySelectorAll(".tree")];
+ is(
+ objectInspectors.length,
+ 11,
+ "There is the expected number of object inspectors"
+ );
+
+ const [
+ smallMapOi,
+ mapOi,
+ largeMapOi,
+ smallSetOi,
+ setOi,
+ largeSetOi,
+ urlSearchParamsOi,
+ headersOi,
+ formDataOi,
+ midiInputsOi,
+ midiOutputsOi,
+ ] = objectInspectors;
+
+ await testSmallMap(smallMapOi);
+ await testMap(mapOi);
+ await testLargeMap(largeMapOi);
+ await testSmallSet(smallSetOi);
+ await testSet(setOi);
+ await testLargeSet(largeSetOi);
+ await testUrlSearchParams(urlSearchParamsOi);
+ await testHeaders(headersOi);
+ await testFormData(formDataOi);
+ await testMidiInputs(midiInputsOi, taskResult.midi.inputs);
+ await testMidiOutputs(midiOutputsOi, taskResult.midi.outputs);
+});
+
+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]`);
+}
+
+async function testUrlSearchParams(oi) {
+ is(
+ oi.textContent,
+ `URLSearchParams(11) { a → "1", a → "2", b → "3", b → "3", b → "5", c → "this is 6", d → "7", e → "8", f → "9", g → "10", ${ELLIPSIS} }`,
+ "URLSearchParams has expected content"
+ );
+
+ info("Expanding the URLSearchParams");
+ let onOiMutation = waitForNodeMutation(oi, {
+ childList: true,
+ });
+
+ oi.querySelector(".arrow").click();
+ await onOiMutation;
+
+ 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");
+
+ const entriesNode = oiNodes[2];
+ is(
+ entriesNode.textContent,
+ "<entries>",
+ "There is the expected <entries> node"
+ );
+
+ info("Expanding the <entries> leaf of the URLSearchParams");
+ onOiMutation = waitForNodeMutation(oi, {
+ childList: true,
+ });
+
+ entriesNode.querySelector(".arrow").click();
+ await onOiMutation;
+
+ oiNodes = oi.querySelectorAll(".node");
+ // There are now 14 nodes, the 4 original ones, and the 11 entries.
+ is(oiNodes.length, 15, "There is the expected number of nodes in the tree");
+
+ is(
+ oiNodes[3].textContent,
+ `0: a → "1"`,
+ "First entry is displayed as expected"
+ );
+ is(
+ oiNodes[4].textContent,
+ `1: a → "2"`,
+ `Second "a" entry is also display although it has the same name as the first entry`
+ );
+ is(
+ oiNodes[5].textContent,
+ `2: b → "3"`,
+ `Third entry is the expected one...`
+ );
+ is(
+ oiNodes[6].textContent,
+ `3: b → "3"`,
+ `As well as fourth, even though both name and value are similar`
+ );
+ is(
+ oiNodes[7].textContent,
+ `4: b → "5"`,
+ `Fifth entry is displayed as expected`
+ );
+ is(
+ oiNodes[8].textContent,
+ `5: c → "this is 6"`,
+ `Sixth entry is displayed as expected`
+ );
+}
+
+async function testHeaders(oi) {
+ is(
+ oi.textContent,
+ `Headers(3) { a → "1", b → "2", c → "3" }`,
+ "Headers has expected content"
+ );
+
+ info("Expanding the Headers");
+ let onOiMutation = waitForNodeMutation(oi, {
+ childList: true,
+ });
+
+ oi.querySelector(".arrow").click();
+ await onOiMutation;
+
+ 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 3 nodes: the root, entries and the proto.
+ is(oiNodes.length, 3, "There is the expected number of nodes in the tree");
+
+ const entriesNode = oiNodes[1];
+ is(
+ entriesNode.textContent,
+ "<entries>",
+ "There is the expected <entries> node"
+ );
+
+ info("Expanding the <entries> leaf of the Headers");
+ onOiMutation = waitForNodeMutation(oi, {
+ childList: true,
+ });
+
+ entriesNode.querySelector(".arrow").click();
+ await onOiMutation;
+
+ oiNodes = oi.querySelectorAll(".node");
+ // There are now 6 nodes, the 3 original ones, and the 3 entries.
+ is(oiNodes.length, 6, "There is the expected number of nodes in the tree");
+
+ is(oiNodes[2].textContent, `a: "1"`, "First entry is displayed as expected");
+ is(
+ oiNodes[3].textContent,
+ `b: "2"`,
+ `Second "a" entry is also display although it has the same name as the first entry`
+ );
+ is(oiNodes[4].textContent, `c: "3"`, `Third entry is the expected one...`);
+}
+
+async function testFormData(oi) {
+ is(
+ oi.textContent,
+ `FormData(3) { a → "1", a → "2", b → "3" }`,
+ "FormData has expected content"
+ );
+
+ info("Expanding the FormData");
+ let onOiMutation = waitForNodeMutation(oi, {
+ childList: true,
+ });
+
+ oi.querySelector(".arrow").click();
+ await onOiMutation;
+
+ 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 3 nodes: the root, entries and the proto.
+ is(oiNodes.length, 3, "There is the expected number of nodes in the tree");
+
+ const entriesNode = oiNodes[1];
+ is(
+ entriesNode.textContent,
+ "<entries>",
+ "There is the expected <entries> node"
+ );
+
+ info("Expanding the <entries> leaf of the FormData");
+ onOiMutation = waitForNodeMutation(oi, {
+ childList: true,
+ });
+
+ entriesNode.querySelector(".arrow").click();
+ await onOiMutation;
+
+ oiNodes = oi.querySelectorAll(".node");
+ // There are now 6 nodes, the 3 original ones, and the 3 entries.
+ is(oiNodes.length, 6, "There is the expected number of nodes in the tree");
+
+ is(
+ oiNodes[2].textContent,
+ `0: a → "1"`,
+ "First entry is displayed as expected"
+ );
+ is(
+ oiNodes[3].textContent,
+ `1: a → "2"`,
+ `Second "a" entry is also display although it has the same name as the first entry`
+ );
+ is(
+ oiNodes[4].textContent,
+ `2: b → "3"`,
+ `Third entry entry is displayed as expected`
+ );
+}
+
+async function testMidiInputs(oi, midiInputs) {
+ const [input] = midiInputs;
+ is(
+ oi.textContent,
+ `MIDIInputMap { "${input.id}" → MIDIInput }`,
+ "MIDIInputMap has expected content"
+ );
+
+ info("Expanding the MIDIInputMap");
+ let onOiMutation = waitForNodeMutation(oi, {
+ childList: true,
+ });
+
+ oi.querySelector(".arrow").click();
+ await onOiMutation;
+
+ 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");
+
+ const entriesNode = oiNodes[2];
+ is(
+ entriesNode.textContent,
+ "<entries>",
+ "There is the expected <entries> node"
+ );
+
+ info("Expanding the <entries> leaf of the MIDIInputMap");
+ onOiMutation = waitForNodeMutation(oi, {
+ childList: true,
+ });
+
+ entriesNode.querySelector(".arrow").click();
+ await onOiMutation;
+
+ oiNodes = oi.querySelectorAll(".node");
+ // There are now 5 nodes, the 4 original ones, and the entry.
+ is(oiNodes.length, 5, "There is the expected number of nodes in the tree");
+
+ is(
+ oiNodes[3].textContent,
+ `"${input.id}": MIDIInput { id: "${input.id}", manufacturer: "${input.manufacturer}", name: "${input.name}", ${ELLIPSIS} }`,
+ "First entry is displayed as expected"
+ );
+}
+
+async function testMidiOutputs(oi, midiOutputs) {
+ is(
+ oi.textContent,
+ `MIDIOutputMap(3) { "${midiOutputs[0].id}" → MIDIOutput, "${midiOutputs[1].id}" → MIDIOutput, "${midiOutputs[2].id}" → MIDIOutput }`,
+ "MIDIOutputMap has expected content"
+ );
+
+ info("Expanding the MIDIOutputMap");
+ let onOiMutation = waitForNodeMutation(oi, {
+ childList: true,
+ });
+
+ oi.querySelector(".arrow").click();
+ await onOiMutation;
+
+ 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");
+
+ const entriesNode = oiNodes[2];
+ is(
+ entriesNode.textContent,
+ "<entries>",
+ "There is the expected <entries> node"
+ );
+
+ info("Expanding the <entries> leaf of the MIDIOutputMap");
+ onOiMutation = waitForNodeMutation(oi, {
+ childList: true,
+ });
+
+ entriesNode.querySelector(".arrow").click();
+ await onOiMutation;
+
+ oiNodes = oi.querySelectorAll(".node");
+ // There are now 7 nodes, the 4 original ones, and the 3 entries.
+ is(oiNodes.length, 7, "There is the expected number of nodes in the tree");
+
+ is(
+ oiNodes[3].textContent,
+ `"${midiOutputs[0].id}": MIDIOutput { id: "${midiOutputs[0].id}", manufacturer: "${midiOutputs[0].manufacturer}", name: "${midiOutputs[0].name}", ${ELLIPSIS} }`,
+ "First entry is displayed as expected"
+ );
+ is(
+ oiNodes[4].textContent,
+ `"${midiOutputs[1].id}": MIDIOutput { id: "${midiOutputs[1].id}", manufacturer: "${midiOutputs[1].manufacturer}", name: "${midiOutputs[1].name}", ${ELLIPSIS} }`,
+ "Second entry is displayed as expected"
+ );
+ is(
+ oiNodes[5].textContent,
+ `"${midiOutputs[2].id}": MIDIOutput { id: "${midiOutputs[2].id}", manufacturer: "${midiOutputs[2].manufacturer}", name: "${midiOutputs[2].name}", ${ELLIPSIS} }`,
+ "Third entry is displayed as expected"
+ );
+}
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..f16d07b2d4
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_getters.js
@@ -0,0 +1,664 @@
+/* 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,<!DOCTYPE html><h1>Object Inspector on Getters</h1>";
+const { ELLIPSIS } = require("resource://devtools/shared/l10n.js");
+
+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(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(() => findConsoleAPIMessage(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);
+ 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);
+ 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);
+ 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);
+ checkChildren(node, [`size`, `<entries>`, `<prototype>`]);
+
+ const entriesNode = findObjectInspectorNode(oi, "<entries>");
+ expandObjectInspectorNode(entriesNode);
+ await waitFor(() => !!getObjectInspectorChildrenNodes(entriesNode).length);
+ checkChildren(entriesNode, [`foo → Object { bar: "baz" }`]);
+
+ const entryNode = getObjectInspectorChildrenNodes(entriesNode)[0];
+ expandObjectInspectorNode(entryNode);
+ await waitFor(() => !!getObjectInspectorChildrenNodes(entryNode).length);
+ 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);
+ checkChildren(node, [`<target>`, `<handler>`]);
+
+ const targetNode = findObjectInspectorNode(oi, "<target>");
+ expandObjectInspectorNode(targetNode);
+ await waitFor(() => !!getObjectInspectorChildrenNodes(targetNode).length);
+ checkChildren(targetNode, [`a: 1`, `<prototype>`]);
+
+ const handlerNode = findObjectInspectorNode(oi, "<handler>");
+ expandObjectInspectorNode(handlerNode);
+ await waitFor(() => !!getObjectInspectorChildrenNodes(handlerNode).length);
+ 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);
+ 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..df409c834c
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_getters_prototype.js
@@ -0,0 +1,119 @@
+/* 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,<!DOCTYPE html><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(() => findConsoleAPIMessage(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);
+
+ 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..d443225f0f
--- /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,<!DOCTYPE html><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(() => findConsoleAPIMessage(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);
+}
+
+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..a8052c9b52
--- /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,<!DOCTYPE html>
+ <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 executeAndWaitForResultMessage(hud, command, "");
+ 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..0043048fce
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_local_session_storage.js
@@ -0,0 +1,117 @@
+/* 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(() =>
+ findConsoleAPIMessage(hud, "localStorage")
+ );
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.console.log("sessionStorage", content.sessionStorage);
+ });
+ const sessionStorageMsg = await waitFor(() =>
+ findConsoleAPIMessage(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..7eb5555764
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_nested_promise.js
@@ -0,0 +1,90 @@
+/* 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,<!DOCTYPE html>" +
+ "<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 = 5; i > 0; --i) {
+ nestedPromise[i] = i;
+ Object.setPrototypeOf(nestedPromise, null);
+ nestedPromise = Promise.resolve(nestedPromise);
+ }
+ nestedPromise[0] = 0;
+ content.wrappedJSObject.console.log("oi-test", nestedPromise);
+ });
+
+ const node = await waitFor(() => findConsoleAPIMessage(hud, "oi-test"));
+ const oi = node.querySelector(".tree");
+ const [promiseNode] = getObjectInspectorNodes(oi);
+
+ expandObjectInspectorNode(promiseNode);
+ await waitFor(() => getObjectInspectorNodes(oi).length > 1);
+ checkChildren(promiseNode, [`0`, `<state>`, `<value>`, `<prototype>`]);
+
+ const valueNode = findObjectInspectorNode(oi, "<value>");
+ expandObjectInspectorNode(valueNode);
+ await waitFor(() => !!getObjectInspectorChildrenNodes(valueNode).length);
+ checkChildren(valueNode, [`1`, `<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);
+ cyclicPromise.foo = "foo";
+ const otherPromise = Promise.reject(cyclicPromise);
+ otherPromise.catch(() => {});
+ Object.setPrototypeOf(otherPromise, null);
+ otherPromise.bar = "bar";
+ resolve(otherPromise);
+ content.wrappedJSObject.console.log("oi-test", cyclicPromise);
+ });
+
+ const node = await waitFor(() => findConsoleAPIMessage(hud, "oi-test"));
+ const oi = node.querySelector(".tree");
+ const [promiseNode] = getObjectInspectorNodes(oi);
+
+ expandObjectInspectorNode(promiseNode);
+ await waitFor(() => getObjectInspectorNodes(oi).length > 1);
+ checkChildren(promiseNode, [`foo`, `<state>`, `<value>`]);
+
+ const valueNode = findObjectInspectorNode(oi, "<value>");
+ expandObjectInspectorNode(valueNode);
+ await waitFor(() => !!getObjectInspectorChildrenNodes(valueNode).length);
+ checkChildren(valueNode, [`bar`, `<state>`, `<reason>`]);
+
+ const reasonNode = findObjectInspectorNode(oi, "<reason>");
+ expandObjectInspectorNode(reasonNode);
+ await waitFor(() => !!getObjectInspectorChildrenNodes(reasonNode).length);
+ checkChildren(reasonNode, [`foo`, `<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) => {
+ is(
+ child.querySelector(".object-label").textContent,
+ expectedChildren[index],
+ `Found correct child at index ${index}`
+ );
+ });
+}
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..ee3f1501e9
--- /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,<!DOCTYPE html>" +
+ "<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(() => findConsoleAPIMessage(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);
+ checkChildren(targetNode, [`<target>`, `<handler>`]);
+
+ const handlerNode = findObjectInspectorNode(oi, "<handler>");
+ expandObjectInspectorNode(handlerNode);
+ await waitFor(() => !!getObjectInspectorChildrenNodes(handlerNode).length);
+ 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_private_properties.js b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_private_properties.js
new file mode 100644
index 0000000000..d70483a0f3
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_private_properties.js
@@ -0,0 +1,306 @@
+/* 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,<!DOCTYPE html>";
+
+add_task(async function () {
+ const hud = await openNewTabAndConsole(TEST_URI);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ class MyClass {
+ constructor(isParent = true) {
+ this.publicProperty = "public property";
+ // A public property can start with a # character. Here we're
+ // adding a public property that looks like an existing private property
+ // to check we do render both.
+ this["#privateProperty"] = {
+ content: "actually this is a public property",
+ };
+ this[Symbol()] = "first Symbol";
+ this[Symbol()] = "second Symbol";
+
+ if (isParent) {
+ this.#privateProperty = new MyClass(false);
+ } else {
+ this.#privateProperty = null;
+ }
+ }
+
+ #privateProperty;
+ #privateMethod() {
+ return Math.random();
+ }
+ get #privateGetter() {
+ return 42;
+ }
+ getPrivateProperty() {
+ return this.#privateProperty;
+ }
+ }
+
+ content.wrappedJSObject.console.log(
+ "private-properties-test",
+ new MyClass(true)
+ );
+ });
+
+ const node = await waitFor(() =>
+ findConsoleAPIMessage(hud, "private-properties-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 = getObjectInspectorNodes(oi);
+ // The object inspector should look like this:
+ /*
+ * ▼ { … }
+ * | ▶︎ "#privateProperty": Object { content: "actually this is a public property" }
+ * | publicProperty: "public property"
+ * | Symbol(): "first Symbol",
+ * | Symbol(): "second Symbol",
+ * | ▶︎ #privateGetter: undefined
+ * | ▶︎ #privateProperty: Object { publicProperty: "public property", "#privateProperty": { … }, #privateProperty: null, Symbol(): "first Symbol", … }
+ * | ▶︎ <prototype>
+ */
+ is(oiNodes.length, 8, "There is the expected number of nodes in the tree");
+
+ const [
+ publicDisguisedAsPrivateNodeEl,
+ publicNodeEl,
+ firstSymbolNodeEl,
+ secondSymbolNodeEl,
+ // FIXME: This shouldn't appear here (See Bug 1759823)
+ privateGetterNodeEl,
+ privatePropertyNodeEl,
+ ] = Array.from(oiNodes).slice(1);
+
+ checkOiNodeText(
+ publicDisguisedAsPrivateNodeEl,
+ `"#privateProperty": Object { content: "actually this is a public property" }`,
+ `"fake" private property has expected text`
+ );
+ checkOiNodeText(
+ publicNodeEl,
+ `publicProperty: "public property"`,
+ "public property has expected text"
+ );
+ checkOiNodeText(
+ firstSymbolNodeEl,
+ `Symbol(): "first Symbol"`,
+ "first symbol has expected text"
+ );
+ checkOiNodeText(
+ secondSymbolNodeEl,
+ `Symbol(): "second Symbol"`,
+ "second symbol has expected text"
+ );
+ checkOiNodeText(
+ privateGetterNodeEl,
+ `#privateGetter: undefined`,
+ "private getter is rendered (at the wrong place with the wrong content, see Bug 1759823)"
+ );
+ checkOiNodeText(
+ privatePropertyNodeEl,
+ `#privateProperty: Object { publicProperty: "public property", "#privateProperty": {…}, #privateGetter: undefined, Symbol(): "first Symbol", … }`,
+ "private property is rendered as expected"
+ );
+
+ info("Expand public property disguised as private property");
+ expandObjectInspectorNode(publicDisguisedAsPrivateNodeEl);
+ const publicPropChildren = await waitFor(() => {
+ const children = getObjectInspectorChildrenNodes(
+ publicDisguisedAsPrivateNodeEl
+ );
+ if (children.length === 0) {
+ return null;
+ }
+ return children;
+ });
+ ok(true, "public property was expanded");
+
+ /*
+ * ObjectInspector now should look like:
+ *
+ * ▼ { … }
+ * | ▼ "#privateProperty": { … }
+ * | | content: "actually this is a public property"
+ * | | ▶︎ <prototype>
+ * | publicProperty: "public property"
+ * | Symbol(): "first Symbol",
+ * | Symbol(): "second Symbol",
+ * | ▶︎ #privateProperty: Object { publicProperty: "public property", "#privateProperty": { … }, #privateProperty: null, Symbol(): "first Symbol", … }
+ * | ▶︎ <prototype>
+ */
+ checkOiNodeText(
+ publicPropChildren[0],
+ `content: "actually this is a public property"`,
+ "public property child has expected text"
+ );
+
+ info("Expand private property");
+ expandObjectInspectorNode(privatePropertyNodeEl);
+ const privatePropChildren = await waitFor(() => {
+ const children = getObjectInspectorChildrenNodes(privatePropertyNodeEl);
+ if (children.length === 0) {
+ return null;
+ }
+ return children;
+ });
+ ok(true, "private property was expanded");
+
+ /*
+ * ObjectInspector now should look like:
+ *
+ * ▼ { … }
+ * | ▼ "#privateProperty": { … }
+ * | | content: "actually this is a public property"
+ * | | ▶︎ <prototype>
+ * | publicProperty: "public property"
+ * | Symbol(): "first Symbol",
+ * | Symbol(): "second Symbol",
+ * | ▶︎ #privateGetter: undefined
+ * | ▼ #privateProperty: { … }
+ * | | ▶︎ "#privateProperty": Object { content: "actually this is a public property" }
+ * | | publicProperty: "public property"
+ * | | Symbol(): "first Symbol"
+ * | | Symbol(): "second Symbol"
+ * | | ▶︎ #privateGetter: undefined
+ * | | #privateProperty: null
+ * | | ▶︎ <prototype>
+ * | ▶︎ <prototype>
+ */
+ checkOiNodeText(
+ privatePropChildren[0],
+ `"#privateProperty": Object { content: "actually this is a public property" }`,
+ "child private property has expected text "
+ );
+ checkOiNodeText(
+ privatePropChildren[1],
+ `publicProperty: "public property"`,
+ "public property of private property object has expected text"
+ );
+ checkOiNodeText(
+ privatePropChildren[2],
+ `Symbol(): "first Symbol"`,
+ "first symbol of private property object has expected text"
+ );
+ checkOiNodeText(
+ privatePropChildren[3],
+ `Symbol(): "second Symbol"`,
+ "second symbol of private property object has expected text"
+ );
+ checkOiNodeText(
+ privatePropChildren[4],
+ `#privateGetter: undefined`,
+ "private getter of private property object is displayed (even though it shouldn't, see Bug 1759823)"
+ );
+ checkOiNodeText(
+ privatePropChildren[5],
+ `#privateProperty: null`,
+ "private property of private property object has expected text"
+ );
+ const privatePropertyPrototypeEl = privatePropChildren[6];
+ checkOiNodeText(
+ privatePropertyPrototypeEl,
+ `<prototype>: Object { … }`,
+ "prototype of private property object has expected text"
+ );
+
+ info("Expand private property prototype");
+ expandObjectInspectorNode(privatePropertyPrototypeEl);
+ const privatePropertyPrototypeChildren = await waitFor(() => {
+ const children = getObjectInspectorChildrenNodes(
+ privatePropertyPrototypeEl
+ );
+ if (children.length === 0) {
+ return null;
+ }
+ return children;
+ });
+ ok(true, "private property prototype was expanded");
+
+ /*
+ * ObjectInspector now should look like:
+ *
+ * ▼ { … }
+ * | ▼ "#privateProperty": { … }
+ * | | content: "actually this is a public property"
+ * | | ▶︎ <prototype>
+ * | publicProperty: "public property"
+ * | Symbol(): "first Symbol",
+ * | Symbol(): "second Symbol",
+ * | ▶︎ #privateGetter: undefined
+ * | ▼ #privateProperty: { … }
+ * | | ▶︎ "#privateProperty": Object { content: "actually this is a public property" }
+ * | | publicProperty: "public property"
+ * | | Symbol(): "first Symbol"
+ * | | Symbol(): "second Symbol"
+ * | | ▶︎ #privateGetter: undefined
+ * | | #privateProperty: null
+ * | | ▼ <prototype>
+ * | | | constructor: function MyClass()
+ * | | | getPrivateProperty: function getPrivateProperty()
+ * | ▶︎ <prototype>
+ */
+ checkOiNodeText(
+ privatePropertyPrototypeChildren[0],
+ `constructor: function MyClass()`,
+ "constructor is displayed as expected"
+ );
+
+ checkOiNodeText(
+ privatePropertyPrototypeChildren[1],
+ `getPrivateProperty: function getPrivateProperty()`,
+ "private method is displayed as expected"
+ );
+
+ // TODO: #privateMethod should be displayed (See Bug 1759826)
+ // checkOiNodeText(
+ // privatePropertyPrototypeChildren[2],
+ // `#privateMethod: function #privateMethod()`,
+ // "private method is displayed as expected"
+ // );
+
+ // TODO: #privateGetter should be displayed here
+ // checkOiNodeText(
+ // privatePropertyPrototypeChildren[3],
+ // `#privateGetter: ">>"`,
+ // "private getter value is displayed as expected"
+ // );
+ // checkOiNodeText(
+ // privatePropertyPrototypeChildren[4],
+ // `<get #privateGetter>: "function"`,
+ // "private getter function is displayed as expected"
+ // );
+});
+
+function checkOiNodeText(oiNode, expectedText, assertionName) {
+ // strip out unwanted character before the label
+ is(
+ oiNode.querySelector(".node").textContent.trim(),
+ expectedText,
+ assertionName
+ );
+}
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..f4c52b2903
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_scroll.js
@@ -0,0 +1,59 @@
+/* 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,<!DOCTYPE html>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(() => findConsoleAPIMessage(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..bd616b5a5c
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_selected_text.js
@@ -0,0 +1,27 @@
+/* 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,<!DOCTYPE html><h1>test Object Inspector</h1>";
+
+add_task(async function () {
+ const hud = await openNewTabAndConsole(TEST_URI);
+
+ const label = "oi-test";
+ const onLoggedMessage = waitForMessageByType(hud, label, ".console-api");
+ 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..7f87fa1fea
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_symbols.js
@@ -0,0 +1,74 @@
+/* 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,<!DOCTYPE html>";
+
+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(() =>
+ findConsoleAPIMessage(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..4238cb2097
--- /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);
+ const tab = gBrowser.selectedTab;
+
+ info("Switch to the debugger");
+ await openDebugger();
+
+ info("Switch to the inspector");
+ const toolbox = await gDevTools.showToolboxForTab(tab, {
+ toolId: "inspector",
+ });
+
+ info("Call firstCall() and wait for the debugger statement to be reached.");
+ const dbg = createDebuggerContext(toolbox);
+ await pauseDebugger(dbg);
+
+ info("Switch back to the console");
+ await gDevTools.showToolboxForTab(tab, { toolId: "webconsole" });
+
+ info("Test logging and inspecting objects while on a breakpoint.");
+ const message = await executeAndWaitForResultMessage(
+ hud,
+ "fooObj",
+ '{ testProp2: "testValue2" }'
+ );
+
+ 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:
+ // Object { testProp2: "testValue2" }
+ // | 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(`Object { testProp2: "testValue2" }`));
+ 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..3ca44337a2
--- /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,<!DOCTYPE html><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..bfc7d09143
--- /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 executeAndWaitForResultMessage(hud, "upvar", "optimized out");
+ 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..74cbc98f8a
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_output_copy.js
@@ -0,0 +1,40 @@
+/* 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,<!DOCTYPE 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 = waitForMessageByType(hud, smokeMessage, ".console-api");
+ 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..c2304cbd69
--- /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,<!DOCTYPE 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 = waitForMessageByType(hud, lastMessage, ".console-api");
+ 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..e4041e68f8
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_output_order.js
@@ -0,0 +1,49 @@
+/* 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 executeAndWaitForResultMessage(
+ hud,
+ `for (let i = 0; i < 5; i++) { console.log("item-" + i); }`,
+ "undefined"
+ );
+
+ 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 = findConsoleAPIMessages(hud, "item-", ".log");
+ return messages.length === 5 ? messages : null;
+ });
+
+ const commandMessage = findMessageByType(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..177e7c69af
--- /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 executeAndWaitForResultMessage(hud, command, "");
+
+ 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..044a13be05
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_persist.js
@@ -0,0 +1,290 @@
+/* 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_FILE = "test-console.html";
+const TEST_COM_URI = URL_ROOT_COM_SSL + TEST_FILE;
+const TEST_ORG_URI = URL_ROOT_ORG_SSL + TEST_FILE;
+// TEST_MOCHI_URI uses a non standart port and hence
+// is not subject to https-first mode
+const TEST_MOCHI_URI = URL_ROOT_MOCHI_8888 + TEST_FILE;
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.webconsole.persistlog");
+});
+
+const INITIAL_LOGS_NUMBER = 5;
+
+const {
+ MESSAGE_TYPE,
+} = require("resource://devtools/client/webconsole/constants.js");
+const {
+ WILL_NAVIGATE_TIME_SHIFT,
+} = require("resource://devtools/server/actors/webconsole/listeners/document-events.js");
+
+async function logAndAssertInitialMessages(hud) {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [INITIAL_LOGS_NUMBER],
+ count => {
+ content.wrappedJSObject.doLogs(count);
+ }
+ );
+ await waitFor(() => findAllMessages(hud).length === INITIAL_LOGS_NUMBER);
+ ok(true, "Messages showed up initially");
+}
+
+add_task(async function () {
+ info("Testing that messages disappear on a refresh if logs aren't persisted");
+ const hud = await openNewTabAndConsole(TEST_COM_URI);
+
+ await logAndAssertInitialMessages(hud);
+
+ const onReloaded = hud.ui.once("reloaded");
+ await reloadBrowser();
+ await onReloaded;
+
+ info("Wait for messages to be cleared");
+ await waitFor(() => findAllMessages(hud).length === 0);
+ ok(true, "Messages disappeared");
+
+ await closeToolbox();
+});
+
+add_task(async function () {
+ info(
+ "Testing that messages disappear on a cross origin navigation if logs aren't persisted"
+ );
+ const hud = await openNewTabAndConsole(TEST_COM_URI);
+
+ await logAndAssertInitialMessages(hud);
+
+ await navigateTo(TEST_ORG_URI);
+ await waitFor(() => findAllMessages(hud).length === 0);
+ ok(true, "Messages disappeared");
+
+ await closeToolbox();
+});
+
+add_task(async function () {
+ info("Testing that messages disappear on bfcache navigations");
+ const firstLocation =
+ "data:text/html,<!DOCTYPE html><script>console.log('first document load');window.onpageshow=()=>console.log('first document show');</script>";
+ const secondLocation =
+ "data:text/html,<!DOCTYPE html><script>console.log('second document load');window.onpageshow=()=>console.log('second document show');</script>";
+ const hud = await openNewTabAndConsole(firstLocation);
+
+ info("Wait for first page messages");
+ // Look into .message-body as the default selector also include the frame,
+ // which is the document url, which also include the logged string...
+ await waitFor(
+ () =>
+ findMessagePartsByType(hud, {
+ text: "first document load",
+ typeSelector: ".console-api",
+ partSelector: ".message-body",
+ }).length === 1 &&
+ findMessagePartsByType(hud, {
+ text: "first document show",
+ typeSelector: ".console-api",
+ partSelector: ".message-body",
+ }).length === 1
+ );
+ const firstPageInnerWindowId =
+ gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.innerWindowId;
+
+ await navigateTo(secondLocation);
+
+ const secondPageInnerWindowId =
+ gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.innerWindowId;
+ isnot(
+ firstPageInnerWindowId,
+ secondPageInnerWindowId,
+ "The second page is having a distinct inner window id"
+ );
+ await waitFor(
+ () =>
+ findMessagePartsByType(hud, {
+ text: "second",
+ typeSelector: ".console-api",
+ partSelector: ".message-body",
+ }).length === 2
+ );
+ ok("Second page message appeared");
+ is(
+ findMessagePartsByType(hud, {
+ text: "first",
+ typeSelector: ".console-api",
+ partSelector: ".message-body",
+ }).length,
+ 0,
+ "First page message disappeared"
+ );
+
+ info("Go back to the first page");
+ gBrowser.selectedBrowser.goBack();
+ // When going pack, the page isn't reloaded, so that we only get the pageshow event
+ await waitFor(
+ () =>
+ findMessagePartsByType(hud, {
+ text: "first document show",
+ typeSelector: ".console-api",
+ partSelector: ".message-body",
+ }).length === 1
+ );
+ ok("First page message re-appeared");
+ is(
+ gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.innerWindowId,
+ firstPageInnerWindowId,
+ "The first page is really a bfcache navigation, keeping the same WindowGlobal"
+ );
+ is(
+ findMessagePartsByType(hud, {
+ text: "second",
+ typeSelector: ".console-api",
+ partSelector: ".message-body",
+ }).length,
+ 0,
+ "Second page message disappeared"
+ );
+
+ info("Go forward to the original second page");
+ gBrowser.selectedBrowser.goForward();
+ await waitFor(
+ () =>
+ findMessagePartsByType(hud, {
+ text: "second document show",
+ typeSelector: ".console-api",
+ partSelector: ".message-body",
+ }).length === 1
+ );
+ ok("Second page message appeared");
+ is(
+ gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.innerWindowId,
+ secondPageInnerWindowId,
+ "The second page is really a bfcache navigation, keeping the same WindowGlobal"
+ );
+ is(
+ findMessagePartsByType(hud, {
+ text: "first",
+ typeSelector: ".console-api",
+ partSelector: ".message-body",
+ }).length,
+ 0,
+ "First page message disappeared"
+ );
+
+ await closeToolbox();
+});
+
+add_task(async function () {
+ info("Testing that messages persist on a refresh if logs are persisted");
+
+ const hud = await openNewTabAndConsole(TEST_COM_URI);
+
+ await toggleConsoleSetting(
+ hud,
+ ".webconsole-console-settings-menu-item-persistentLogs"
+ );
+
+ await logAndAssertInitialMessages(hud);
+
+ const onNavigatedMessage = waitForMessageByType(
+ hud,
+ "Navigated to " + TEST_COM_URI,
+ ".navigationMarker"
+ );
+ const onReloaded = hud.ui.once("reloaded");
+ // Because will-navigate DOCUMENT_EVENT timestamp is shifted to workaround some other limitation,
+ // the reported time of navigation may actually be slightly off and be older than the real navigation start
+ let timeBeforeNavigation = Date.now() - WILL_NAVIGATE_TIME_SHIFT;
+ reloadBrowser();
+ await onNavigatedMessage;
+ await onReloaded;
+
+ ok(true, "Navigation message appeared as expected");
+ is(
+ findAllMessages(hud).length,
+ INITIAL_LOGS_NUMBER + 1,
+ "Messages logged before navigation are still visible"
+ );
+
+ assertLastMessageIsNavigationMessage(hud, timeBeforeNavigation, TEST_COM_URI);
+
+ info(
+ "Testing that messages also persist when doing a cross origin navigation if logs are persisted"
+ );
+ const onNavigatedMessage2 = waitForMessageByType(
+ hud,
+ "Navigated to " + TEST_ORG_URI,
+ ".navigationMarker"
+ );
+ timeBeforeNavigation = Date.now() - WILL_NAVIGATE_TIME_SHIFT;
+ await navigateTo(TEST_ORG_URI);
+ await onNavigatedMessage2;
+
+ ok(true, "Second navigation message appeared as expected");
+ is(
+ findAllMessages(hud).length,
+ INITIAL_LOGS_NUMBER + 2,
+ "Messages logged before the second navigation are still visible"
+ );
+
+ assertLastMessageIsNavigationMessage(hud, timeBeforeNavigation, TEST_ORG_URI);
+
+ info(
+ "Test doing a second cross origin navigation in order to triger a target switching with a target following the window global lifecycle"
+ );
+ const onNavigatedMessage3 = waitForMessageByType(
+ hud,
+ "Navigated to " + TEST_MOCHI_URI,
+ ".navigationMarker"
+ );
+ timeBeforeNavigation = Date.now() - WILL_NAVIGATE_TIME_SHIFT;
+ await navigateTo(TEST_MOCHI_URI);
+ await onNavigatedMessage3;
+
+ ok(true, "Third navigation message appeared as expected");
+ is(
+ findAllMessages(hud).length,
+ INITIAL_LOGS_NUMBER + 3,
+ "Messages logged before the third navigation are still visible"
+ );
+
+ assertLastMessageIsNavigationMessage(
+ hud,
+ timeBeforeNavigation,
+ TEST_MOCHI_URI
+ );
+
+ await closeToolbox();
+});
+
+function assertLastMessageIsNavigationMessage(hud, timeBeforeNavigation, url) {
+ const { visibleMessages, mutableMessagesById } = hud.ui.wrapper
+ .getStore()
+ .getState().messages;
+ const lastMessageId = visibleMessages.at(-1);
+ const lastMessage = mutableMessagesById.get(lastMessageId);
+
+ is(
+ lastMessage.type,
+ MESSAGE_TYPE.NAVIGATION_MARKER,
+ "The last message is a navigation marker"
+ );
+ is(
+ lastMessage.messageText,
+ "Navigated to " + url,
+ "The navigation message is correct"
+ );
+ // It is surprising, but the navigation may be timestamped at the same exact time
+ // as timeBeforeNavigation time record.
+ ok(
+ lastMessage.timeStamp >= timeBeforeNavigation,
+ "The navigation message has a timestamp newer (or equal) than the time before the navigation..."
+ );
+ ok(lastMessage.timeStamp < Date.now(), "...and older than current time");
+}
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..3d99d8f161
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_promise_rejected_object.js
@@ -0,0 +1,126 @@
+/* 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,<!DOCTYPE html>
+ <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(
+ () => findErrorMessage(hud, expectedError),
+ `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 ? 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 = findErrorMessage(
+ hud,
+ `Uncaught (in promise) Object { fav: "eggplant" }`
+ );
+ 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:
+ // Object { fav: "eggplant" }
+ // | 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(`Object { fav: "eggplant" }`));
+ 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_record_tuple.js b/devtools/client/webconsole/test/browser/browser_webconsole_record_tuple.js
new file mode 100644
index 0000000000..adcc9aa343
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_record_tuple.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check logging records and tuples in the console.
+const TEST_URI =
+ "data:text/html;charset=utf8,<!DOCTYPE html>" +
+ encodeURIComponent(
+ `<script>console.log("oi-test", #{hello: "world"}, #[42])</script>
+ <h1>Object Inspector on records and tuples</h1>`
+ );
+
+add_task(async function () {
+ const hud = await openNewTabAndConsole(TEST_URI);
+
+ const hasSupport = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ function () {
+ return typeof content.wrappedJSObject.Record == "function";
+ }
+ );
+
+ if (!hasSupport) {
+ ok(true, "Records and Tuples not supported yet");
+ return;
+ }
+
+ const node = await waitFor(() => findConsoleAPIMessage(hud, "oi-test"));
+ ok(node.textContent.includes("Record"), "Record is displayed as expected");
+ ok(node.textContent.includes("Tuple"), "Tuple is displayed as expected");
+});
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..8e6b6a4012
--- /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 reloadBrowser();
+ await waitForError(hud);
+
+ // Close and reopen
+ await closeConsole();
+
+ expectUncaughtExceptionNoE10s();
+ gBrowser.removeCurrentTab();
+ hud = await openNewTabAndConsole(TEST_URI);
+
+ expectUncaughtExceptionNoE10s();
+ await reloadBrowser();
+ await waitForError(hud);
+});
+
+async function waitForError(hud) {
+ info("Wait for error message");
+ await waitFor(() => findErrorMessage(hud, "fooBug597756_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..43fcc993f9
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_repeat_different_objects.js
@@ -0,0 +1,42 @@
+/* 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,<!DOCTYPE html>Test repeated objects";
+
+add_task(async function () {
+ const hud = await openNewTabAndConsole(TEST_URI);
+
+ const onMessages = waitForMessagesByType({
+ hud,
+ messages: [
+ {
+ text: "abba",
+ typeSelector: ".console-api",
+ },
+ {
+ text: "abba",
+ typeSelector: ".console-api",
+ },
+ {
+ text: "abba",
+ typeSelector: ".console-api",
+ },
+ ],
+ });
+
+ 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..53c8fc9b4d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_requestStorageAccess_errors.js
@@ -0,0 +1,132 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI_FIRST_PARTY = "https://example.com";
+const TEST_URI_THIRD_PARTY = "https://itisatracker.org";
+const LEARN_MORE_URI =
+ "https://developer.mozilla.org/docs/Web/API/Document/requestStorageAccess" +
+ DOCS_GA_PARAMS;
+
+const { UrlClassifierTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlClassifierTestUtils.sys.mjs"
+);
+
+UrlClassifierTestUtils.addTestTrackers();
+registerCleanupFunction(function () {
+ UrlClassifierTestUtils.cleanupTestTrackers();
+});
+
+/**
+ * Run document.requestStorageAccess in an iframe.
+ * @param {Object} options - Request / iframe options.
+ * @param {boolean} [options.withUserActivation] - Whether the requesting iframe
+ * should have user activation prior to calling rsA.
+ * @param {string} [options.sandboxAttr] - Iframe sandbox attributes.
+ * @param {boolean} [options.nested] - If the iframe calling rsA should be
+ * nested in another same-origin iframe.
+ */
+async function runRequestStorageAccess({
+ withUserActivation = false,
+ sandboxAttr = "",
+ nested = false,
+}) {
+ let parentBC = gBrowser.selectedBrowser.browsingContext;
+
+ // Spawn the rsA iframe in an iframe.
+ if (nested) {
+ parentBC = await SpecialPowers.spawn(
+ parentBC,
+ [TEST_URI_THIRD_PARTY],
+ async uri => {
+ const frame = content.document.createElement("iframe");
+ frame.setAttribute("src", uri);
+ const loadPromise = ContentTaskUtils.waitForEvent(frame, "load");
+ content.document.body.appendChild(frame);
+ await loadPromise;
+ return frame.browsingContext;
+ }
+ );
+ }
+
+ // Create an iframe which is a third party to the top level.
+ const frameBC = await SpecialPowers.spawn(
+ parentBC,
+ [TEST_URI_THIRD_PARTY, sandboxAttr],
+ async (uri, sandbox) => {
+ const frame = content.document.createElement("iframe");
+ frame.setAttribute("src", uri);
+ if (sandbox) {
+ frame.setAttribute("sandbox", sandbox);
+ }
+ const loadPromise = ContentTaskUtils.waitForEvent(frame, "load");
+ content.document.body.appendChild(frame);
+ await loadPromise;
+ return frame.browsingContext;
+ }
+ );
+
+ // Call requestStorageAccess in the iframe.
+ await SpecialPowers.spawn(frameBC, [withUserActivation], userActivation => {
+ if (userActivation) {
+ content.document.notifyUserGestureActivation();
+ }
+ content.document.requestStorageAccess();
+ });
+}
+
+add_task(async function () {
+ const hud = await openNewTabAndConsole(TEST_URI_FIRST_PARTY);
+
+ async function checkErrorMessage(text) {
+ const message = await waitFor(
+ () => findErrorMessage(hud, text),
+ 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 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 runRequestStorageAccess({ withUserActivation: false });
+ await checkErrorMessage(userGesture);
+
+ await runRequestStorageAccess({
+ withUserActivation: true,
+ sandboxAttr: "allow-scripts",
+ });
+ await checkErrorMessage(nullPrincipal);
+
+ await runRequestStorageAccess({
+ withUserActivation: true,
+ sandboxAttr: "allow-same-origin allow-scripts",
+ });
+ 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..0d40e3455e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_responsive_design_mode.js
@@ -0,0 +1,43 @@
+/* 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,<!DOCTYPE html><meta charset=utf8>Test logging in RDM";
+
+add_task(async function () {
+ const tab = await addTab(TEST_URI);
+
+ info("Open responsive design mode");
+ await openRDM(tab);
+
+ 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(
+ () => findConsoleAPIMessage(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(
+ () => findConsoleAPIMessage(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 closeRDM(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..67329335fb
--- /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,<!DOCTYPE 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 = Promise.all([
+ waitForMessageByType(hud, `"a" + "😎"`, ".command"),
+ waitForMessageByType(hud, `"a😎"`, ".result"),
+ ]);
+ 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 = Promise.all([
+ waitForMessageByType(hud, `"a" + "😎"`, ".command"),
+ waitForMessageByType(hud, `"a😎"`, ".result"),
+ ]);
+ 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..3bad1f6c7d
--- /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,<!DOCTYPE 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 = waitForMessageByType(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..ec8aab1924
--- /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,<!DOCTYPE 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 = waitForMessageByType(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..b6f14bd2f3
--- /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,<!DOCTYPE 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 = waitForMessageByType(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],
+ });
+});
+
+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..df574f06cf
--- /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,<!DOCTYPE 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..edbd287d80
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_same_origin_errors.js
@@ -0,0 +1,37 @@
+/* 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.
+
+// XPCNativeWrapper is not defined globally in ESLint as it may be going away.
+// See bug 1481337.
+/* global XPCNativeWrapper */
+
+"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 = waitForMessageByType(
+ hud,
+ "may not load data",
+ ".error"
+ );
+ 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..ccea9eb400
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_sandbox_update_after_navigation.js
@@ -0,0 +1,59 @@
+/* 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 = "https://example.com/" + BASE_URI;
+const TEST_URI2 = "https://example.org/" + BASE_URI;
+
+add_task(async function () {
+ await pushPref("devtools.webconsole.persistlog", false);
+
+ const hud = await openNewTabAndConsole(TEST_URI1);
+
+ await executeAndWaitForResultMessage(hud, "window.location.href", TEST_URI1);
+
+ // load second url
+ await navigateTo(TEST_URI2);
+
+ ok(
+ !findErrorMessage(hud, "Permission denied"),
+ "no permission denied errors"
+ );
+
+ info("wait for window.location.href after page navigation");
+ await clearOutput(hud);
+ await executeAndWaitForResultMessage(hud, "window.location.href", TEST_URI2);
+
+ ok(
+ !findErrorMessage(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"),
+ hud.commands.targetCommand.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 executeAndWaitForResultMessage(hud, "window.location.href", TEST_URI1);
+
+ ok(
+ !findErrorMessage(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..4cb348b364
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_script_errordoc_urls.js
@@ -0,0 +1,75 @@
+/* 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("resource://devtools/server/actors/errordocs.js");
+const TEST_URI = "data:text/html;charset=utf8,<!DOCTYPE html>errordoc tests";
+
+function makeURIData(script) {
+ return `data:text/html;charset=utf8,<!DOCTYPE html><script>${script}</script>`;
+}
+
+const TestData = [
+ {
+ jsmsg: "JSMSG_READ_ONLY",
+ script:
+ "'use strict'; (Object.freeze({name: 'Elsa', score: 157})).score = 0;",
+ selector: ".error",
+ isException: true,
+ expected: 'TypeError: "score" is read-only',
+ },
+ {
+ jsmsg: "JSMSG_STMT_AFTER_RETURN",
+ script: "function a() { return; 1 + 1; };",
+ selector: ".warn",
+ 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(() =>
+ findMessageByType(hud, testData.expected, testData.selector)
+ );
+ 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..53f9e45954
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_scroll.js
@@ -0,0 +1,407 @@
+/* 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,<!DOCTYPE html><p>Web Console test for scroll.</p>
+ <script>
+ var a = () => b();
+ var b = () => c();
+ var c = (i) => console.trace("trace in C " + i);
+
+ for (let i = 0; i <= 100; i++) {
+ console.log("init-" + i);
+ if (i % 10 === 0) {
+ c(i);
+ }
+ }
+ </script>
+`;
+
+const {
+ MESSAGE_SOURCE,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+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(() => findConsoleAPIMessage(hud, "init-100"));
+ 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(() => allTraceMessagesAreExpanded(hud));
+ ok(
+ isScrolledToBottom(outputContainer),
+ "The console is scrolled to the bottom"
+ );
+
+ await reloadBrowser();
+
+ info("Console should be scrolled to bottom after refresh from page logs");
+ await waitFor(() => findConsoleAPIMessage(hud, "init-100"));
+ 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(() => allTraceMessagesAreExpanded(hud));
+
+ // There's an annoying race here where the SmartTrace from above goes into
+ // the DOM, our waitFor passes, but the SmartTrace still hasn't called its
+ // onReady callback. If this happens, it will call ConsoleOutput's
+ // maybeScrollToBottomMessageCallback *after* we set scrollTop below,
+ // causing it to undo our work. Waiting a little bit here should resolve it.
+ await new Promise(r =>
+ window.requestAnimationFrame(() => TestUtils.executeSoon(r))
+ );
+ ok(
+ isScrolledToBottom(outputContainer),
+ "The console is scrolled to the bottom"
+ );
+
+ info("Scroll up and wait for the layout to stabilize");
+ outputContainer.scrollTop = 0;
+ await new Promise(r =>
+ window.requestAnimationFrame(() => TestUtils.executeSoon(r))
+ );
+
+ info("Add a console.trace message to check that the scroll isn't impacted");
+ let onMessage = waitForMessageByType(hud, "trace in C", ".console-api");
+ 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 executeAndWaitForResultMessage(hud, "21 + 21", "42");
+ ok(hasVerticalOverflow(outputContainer), "There is a vertical overflow");
+ ok(
+ isScrolledToBottom(outputContainer),
+ "The console is scrolled to the bottom"
+ );
+
+ info("Scroll up and wait for the layout to stabilize");
+ outputContainer.scrollTop = 0;
+ await new Promise(r =>
+ window.requestAnimationFrame(() => TestUtils.executeSoon(r))
+ );
+
+ info(
+ "Trigger a network request so the last message in the console store won't be visible"
+ );
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ await content.fetch(
+ "http://mochi.test:8888/browser/devtools/client/webconsole/test/browser/sjs_cors-test-server.sjs",
+ { mode: "cors" }
+ );
+ });
+
+ // Wait until the evalation result message isn't the last in the store anymore
+ await waitFor(() => {
+ const state = ui.wrapper.getStore().getState();
+ return (
+ state.messages.mutableMessagesById.get(state.messages.lastMessageId)
+ ?.source === MESSAGE_SOURCE.NETWORK
+ );
+ });
+
+ // Wait a bit so the pin to bottom would have the chance to be hit.
+ await wait(500);
+ ok(
+ !isScrolledToBottom(outputContainer),
+ "The console is not scrolled to the bottom"
+ );
+
+ info(
+ "Evaluate a new command to check that the console scrolls to the bottom"
+ );
+ await executeAndWaitForResultMessage(hud, "7 + 2", "9");
+ 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 = waitForMessageByType(hud, "scroll", ".console-api");
+ 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 executeAndWaitForResultMessage(
+ hud,
+ `
+ x = new Error("myErrorObject");
+ x.stack = "a@b/c.js:1:2\\nd@e/f.js:3:4";
+ x;`,
+ "myErrorObject"
+ );
+ 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 executeAndWaitForErrorMessage(
+ hud,
+ `
+ x = new Error("myEvaluatedThrownErrorObject");
+ x.stack = "a@b/c.js:1:2\\nd@e/f.js:3:4";
+ throw x;
+ `,
+ "Uncaught Error: myEvaluatedThrownErrorObject"
+ );
+ 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 executeAndWaitForErrorMessage(
+ 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"
+ );
+ 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 = waitForMessageByType(hud, "trace in C", ".console-api");
+ 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 = waitForMessageByType(hud, "repeat", ".console-api");
+ 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 = waitForMessageByType(hud, "after repeat", ".console-api");
+ 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 executeAndWaitForResultMessage(
+ hud,
+ `Array.from({length: 100}, (_, i) => i)
+ .reduce(
+ (acc, item) => {acc["item-" + item] = item; return acc;},
+ {}
+ )`,
+ "Object"
+ );
+ // 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"
+ );
+
+ await clearOutput(hud);
+ // Log a big object that will be much larger than the output container
+ onMessage = waitForMessageByType(hud, "WE ALL LIVE IN A", ".warn");
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ const win = content.wrappedJSObject;
+ for (let i = 1; i < 100; i++) {
+ win["a" + i] = function (j) {
+ win["a" + j]();
+ }.bind(null, i + 1);
+ }
+ win.a100 = function () {
+ win.console.warn(new Error("WE ALL LIVE IN A"));
+ };
+ win.a1();
+ });
+ message = await onMessage;
+ // Give the intersection observer a chance to break this if it's going to
+ await wait(500);
+ // Assert here and below for ease of debugging where we lost the scroll
+ is(
+ isScrolledToBottom(outputContainer),
+ true,
+ "The output was scrolled to the bottom"
+ );
+ // Then log something else to make sure we haven't lost our scroll pinning
+ onMessage = waitForMessageByType(hud, "YELLOW SUBMARINE", ".console-api");
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ content.wrappedJSObject.console.log("YELLOW SUBMARINE");
+ });
+ message = await onMessage;
+ // Again, give the scroll position a chance to be broken
+ await wait(500);
+ is(
+ isScrolledToBottom(outputContainer),
+ true,
+ "The output was 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
+ );
+}
+
+// This validates that 1) the last trace exists, and 2) that all *shown* traces
+// are expanded. Traces that have been scrolled out of existence due to
+// LazyMessageList are disregarded.
+function allTraceMessagesAreExpanded(hud) {
+ return (
+ findConsoleAPIMessage(hud, "trace in C 100") &&
+ findConsoleAPIMessages(hud, "trace in C").every(m =>
+ m.querySelector(".frames")
+ )
+ );
+}
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..2c320d9ff7
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_select_all.js
@@ -0,0 +1,61 @@
+/* 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.
+
+const TEST_URI = "http://example.com/";
+
+add_task(async function testSelectAll() {
+ const hud = await openNewTabAndConsole(TEST_URI);
+ await testSelectionWhenMovingBetweenBoxes(hud);
+ testBrowserMenuSelectAll(hud);
+});
+
+async function testSelectionWhenMovingBetweenBoxes(hud) {
+ // Fill the console with some output.
+ await clearOutput(hud);
+ await executeAndWaitForResultMessage(hud, "1 + 2", "3");
+ await executeAndWaitForResultMessage(hud, "3 + 4", "7");
+ await executeAndWaitForResultMessage(hud, "5 + 6", "11");
+}
+
+function testBrowserMenuSelectAll(hud) {
+ const { ui } = hud;
+ const outputContainer = ui.outputNode.querySelector(".webconsole-output");
+
+ is(
+ outputContainer.querySelectorAll(".message").length,
+ 6,
+ "the output node contains the expected number of messages"
+ );
+
+ // 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();
+}
+
+function checkMessagesSelected(outputContainer) {
+ const selection = outputContainer.ownerDocument.getSelection();
+ const messages = outputContainer.querySelectorAll(".message");
+
+ for (const message of messages) {
+ // Oddly, something about the top and bottom buffer having user-select be
+ // 'none' means that the messages themselves don't register as selected.
+ // However, all of their children will count as selected, which should be
+ // good enough for our purposes.
+ const selected = [...message.children].every(c =>
+ selection.containsNode(c)
+ );
+ 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..cbd94edb26
--- /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,<!DOCTYPE html>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(() => findWarningMessage(hud, SAMPLE_MSG));
+
+ 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..8d9a6f8ff8
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_shows_reqs_from_netmonitor.js
@@ -0,0 +1,83 @@
+/* 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,<!DOCTYPE html>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 =
+ "https://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 = waitForMessageByType(hud, TEST_PATH, ".network");
+
+ await navigateTo(TEST_PATH);
+ info("Document loaded.");
+
+ await onMessageAdded;
+ info("Network message found.");
+
+ // Test that the request appears in the network panel.
+ const toolbox = await gDevTools.showToolboxForTab(currentTab, {
+ toolId: "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));
+
+ // Lets also wait until all the event timings data requested
+ // has completed and the column is rendered.
+ await waitFor(() =>
+ document.querySelector(
+ ".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..09c61bc007
--- /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,<!DOCTYPE html>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 =
+ "https://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(() => findMessageByType(hud, TEST_PATH, ".network"));
+
+ 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);
+
+ 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..65aa428b5d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_sidebar_object_expand_when_message_pruned.js
@@ -0,0 +1,85 @@
+/* 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,<!DOCTYPE html>" +
+ "<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(() => findConsoleAPIMessage(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 = waitForMessageByType(hud, messageText, ".console-api");
+ SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [messageText],
+ async function (str) {
+ content.console.log(str);
+ }
+ );
+ await onMessage;
+
+ ok(!findConsoleAPIMessage(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..b4d5c55673
--- /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,<!DOCTYPE html>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 = waitForMessageByType(hud, "Document", ".console-api");
+ 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,
+ });
+ 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..4407322c67
--- /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(() => findWarningMessage(hud, "octopus"));
+ ok(!!node, "css warning seen");
+
+ info("Waiting for source map to be applied");
+ const found = await waitFor(() => {
+ const messageLocationNode = node.querySelector(".message-location");
+ const url = messageLocationNode.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..46428c7078
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_sourcemap_error.js
@@ -0,0 +1,26 @@
+/* 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(() => findConsoleAPIMessage(hud, "here"));
+ ok(node, "logged text is displayed in web console");
+
+ const node2 = await waitFor(() =>
+ findWarningMessage(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..daa0520f21
--- /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(() => findWarningMessage(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..d6e9f96755
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_sourcemap_nosource.js
@@ -0,0 +1,60 @@
+/* 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 = findConsoleAPIMessage(hud, "here");
+ if (!node) {
+ return false;
+ }
+ const messageLocationNode = node.querySelector(".message-location");
+ const url = messageLocationNode.getAttribute("data-url");
+ return url.includes("nosuchfile");
+ });
+
+ await testOpenInDebugger(hud, {
+ text: "here",
+ typeSelector: ".console-api",
+ expectUrl: true,
+ expectLine: false,
+ expectColumn: false,
+ });
+
+ info("Selecting the console again");
+ await toolbox.selectTool("webconsole");
+
+ const node = await waitFor(() => findWarningMessage(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..3d39cb74d0
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_split.js
@@ -0,0 +1,365 @@
+/* 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,<!DOCTYPE html>Web Console test for splitting";
+
+// Test is slow on Linux EC2 instances - Bug 962931
+requestLongerTimeout(4);
+
+add_task(async function () {
+ let toolbox;
+ const getFluentString = await getFluentStringHelper([
+ "devtools/client/toolbox.ftl",
+ ]);
+ const hideSplitConsoleLabel = getFluentString(
+ "toolbox-meatball-menu-hideconsole-label"
+ );
+
+ 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.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,
+ containerHeight,
+ webconsoleHeight,
+ splitterVisibility,
+ splitterHeight,
+ 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,
+ });
+ 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 === hideSplitConsoleLabel
+ ? "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
+ ),
+ Math.round(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 tab = gBrowser.selectedTab;
+ toolbox = await gDevTools.showToolboxForTab(tab, { 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..aae474cb7a
--- /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,<!DOCTYPE html><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..84c6935510
--- /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,<!DOCTYPE html><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..533b75461f
--- /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,<!DOCTYPE html><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..a30e35ce1a
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_split_persist.js
@@ -0,0 +1,143 @@
+/* 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 TEST_URI =
+ "data:text/html;charset=utf-8,<!DOCTYPE html><p>Web Console test for splitting</p>";
+
+add_task(async function () {
+ const getFluentString = await getFluentStringHelper([
+ "devtools/client/toolbox.ftl",
+ ]);
+ const hideLabel = getFluentString("toolbox-meatball-menu-hideconsole-label");
+ const showLabel = getFluentString("toolbox-meatball-menu-splitconsole-label");
+
+ 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");
+ is(
+ await getSplitConsoleMenuLabel(toolbox),
+ showLabel,
+ "Split console menu item says split by default"
+ );
+
+ await toggleSplitConsoleWithEscape(toolbox);
+ ok(toolbox.splitConsole, "Split console is now visible.");
+ is(
+ await getSplitConsoleMenuLabel(toolbox),
+ hideLabel,
+ "Split console menu item now says hide"
+ );
+ ok(getVisiblePrefValue(), "Visibility pref is true");
+
+ is(
+ parseInt(getHeightPrefValue(), 10),
+ parseInt(toolbox.webconsolePanel.style.height, 10),
+ "Panel height matches the pref"
+ );
+ toolbox.webconsolePanel.style.height = "200px";
+
+ 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"
+ );
+ is(
+ await getSplitConsoleMenuLabel(toolbox),
+ hideLabel,
+ "Split console menu item initially says hide"
+ );
+ is(
+ getHeightPrefValue(),
+ 200,
+ "Height is set based on panel height after closing"
+ );
+
+ toolbox.webconsolePanel.style.height = "1px";
+ ok(
+ toolbox.webconsolePanel.clientHeight > 1,
+ "The actual height of the console is bound with a min height"
+ );
+
+ await toggleSplitConsoleWithEscape(toolbox);
+ ok(!toolbox.splitConsole, "Split console is now hidden.");
+ is(
+ await getSplitConsoleMenuLabel(toolbox),
+ showLabel,
+ "Split console menu item now says split"
+ );
+ ok(!getVisiblePrefValue(), "Visibility pref is false");
+
+ await toolbox.destroy();
+
+ is(
+ getHeightPrefValue(),
+ 1,
+ "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 getSplitConsoleMenuLabel(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"
+ );
+
+ toolbox.doc.addEventListener(
+ "popuphidden",
+ () => {
+ resolve(menuItem?.querySelector(".label")?.textContent);
+ },
+ { 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..0d762c0d14
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_stacktrace_location_debugger_link.js
@@ -0,0 +1,63 @@
+/* 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 toolbox = await gDevTools.getToolboxForTab(gBrowser.selectedTab);
+
+ await testOpenFrameInDebugger(hud, toolbox, "console.trace()");
+ await testOpenFrameInDebugger(hud, toolbox, "myErrorObject");
+});
+
+async function testOpenFrameInDebugger(hud, toolbox, text) {
+ info(`Testing message with text "${text}"`);
+ const messageNode = await waitFor(() => findConsoleAPIMessage(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 checkMousedownOnNode(hud, toolbox, frameNode);
+
+ info("Selecting the console again");
+ await toolbox.selectTool("webconsole");
+ }
+}
+
+async function checkMousedownOnNode(hud, toolbox, frameNode) {
+ info("checking click on node location");
+ const onSourceInDebuggerOpened = once(hud, "source-in-debugger-opened");
+ 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..ac12f0baec
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_stacktrace_mapped_location_debugger_link.js
@@ -0,0 +1,65 @@
+/* -*- 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 =
+ "https://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 =
+ "https://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 = waitForMessageByType(
+ hud,
+ "console.trace",
+ ".console-api"
+ );
+ 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.");
+ 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..0894f4f6df
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_strict_mode_errors.js
@@ -0,0 +1,48 @@
+/* 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,<!DOCTYPE html>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(() => findErrorMessage(hud, text));
+ 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,<!DOCTYPE html><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..341f2bac10
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_string.js
@@ -0,0 +1,70 @@
+/* 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 = waitForMessageByType(hud, "stringLog", ".console-api");
+
+ 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 = waitForMessageByType(
+ hud,
+ "hello <empty string>",
+ ".console-api"
+ );
+
+ 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 = waitForMessageByType(
+ hud,
+ `Object { a: "" }`,
+ ".console-api"
+ );
+
+ 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 executeAndWaitForResultMessage(
+ hud,
+ '"string\\nconstant"',
+ "constant"
+ );
+ 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 executeAndWaitForResultMessage(hud, '""', '""');
+ 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..9718a8efd1
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_stubs_console_api.js
@@ -0,0 +1,343 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ STUBS_UPDATE_ENV,
+ createCommandsForTab,
+ getStubFile,
+ getCleanedPacket,
+ getSerializedPacket,
+ writeStubsToFile,
+} = require(`${CHROME_URL_ROOT}stub-generator-helpers`);
+
+const TEST_URI =
+ "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html";
+const STUB_FILE = "consoleApi.js";
+
+add_task(async function () {
+ const isStubsUpdate = Services.env.get(STUBS_UPDATE_ENV) == "true";
+ info(`${isStubsUpdate ? "Update" : "Check"} ${STUB_FILE}`);
+
+ const generatedStubs = await generateConsoleApiStubs();
+
+ if (isStubsUpdate) {
+ await writeStubsToFile(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,
+ replaceActorIds: true,
+ });
+ const existingPacketStr = getSerializedPacket(
+ existingStubs.rawPackets.get(key),
+ { sortKeys: true, replaceActorIds: 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 generateConsoleApiStubs() {
+ const stubs = new Map();
+
+ const tab = await addTab(TEST_URI);
+ const commands = await createCommandsForTab(tab);
+ await commands.targetCommand.startListening();
+ const resourceCommand = commands.resourceCommand;
+
+ // 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 resourceCommand.watchResources(
+ [resourceCommand.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;
+ }
+
+ resourceCommand.unwatchResources([resourceCommand.TYPES.CONSOLE_MESSAGE], {
+ onAvailable: onConsoleMessage,
+ });
+
+ await commands.destroy();
+
+ 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('data:image/png,base64,iVBORw0KGgoAAAAN'), url('https://example.com/test'); position:absolute; top:10px; ",
+ "color:red; line-height: 1.5; background:\\165rl('https://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('https://example.com/test');position:absolute;top:10px",
+ "color:red;background:\\165rl('https://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('https://example.com/test');position:absolute;top:10px",
+ "color:red;background:\\165rl('https://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..356fb15b74
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_stubs_css_message.js
@@ -0,0 +1,130 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ STUBS_UPDATE_ENV,
+ createCommandsForTab,
+ getCleanedPacket,
+ getSerializedPacket,
+ getStubFile,
+ writeStubsToFile,
+} = require(`${CHROME_URL_ROOT}stub-generator-helpers`);
+
+const TEST_URI =
+ "https://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 = Services.env.get(STUBS_UPDATE_ENV) == "true";
+ info(`${isStubsUpdate ? "Update" : "Check"} ${STUB_FILE}`);
+
+ const generatedStubs = await generateCssMessageStubs();
+
+ if (isStubsUpdate) {
+ await writeStubsToFile(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 = getSerializedPacket(packet, {
+ sortKeys: true,
+ replaceActorIds: true,
+ });
+ const existingPacketStr = getSerializedPacket(
+ existingStubs.rawPackets.get(key),
+ { sortKeys: true, replaceActorIds: 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 generateCssMessageStubs() {
+ const stubs = new Map();
+
+ const tab = await addTab(TEST_URI);
+ const commands = await createCommandsForTab(tab);
+ await commands.targetCommand.startListening();
+ const resourceCommand = commands.resourceCommand;
+
+ // The resource command 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 resourceCommand.watchResources([resourceCommand.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;
+ }
+
+ resourceCommand.unwatchResources([resourceCommand.TYPES.CSS_MESSAGE], {
+ onAvailable: onCSSMessageAvailable,
+ });
+
+ await commands.destroy();
+ 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..db78426617
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_stubs_evaluation_result.js
@@ -0,0 +1,185 @@
+/* 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,<!DOCTYPE html>stub generation";
+const STUB_FILE = "evaluationResult.js";
+
+add_task(async function () {
+ const isStubsUpdate = Services.env.get(STUBS_UPDATE_ENV) == "true";
+ info(`${isStubsUpdate ? "Update" : "Check"} ${STUB_FILE}`);
+
+ const generatedStubs = await generateEvaluationResultStubs();
+
+ if (isStubsUpdate) {
+ await writeStubsToFile(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,
+ replaceActorIds: true,
+ });
+ const existingPacketStr = getSerializedPacket(
+ existingStubs.rawPackets.get(key),
+ { sortKeys: true, replaceActorIds: 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");
+ for (const [key, code] of getCommands()) {
+ const packet = await toolbox.commands.scriptCommand.execute(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 throw Error Object with error cause`,
+ `
+ var originalError = new SyntaxError("original error")
+ var err = new Error("something went wrong", {
+ cause: originalError
+ });
+ throw err;
+ `
+ );
+ evaluationResult.set(
+ `eval throw Error Object with cause chain`,
+ `
+ var errA = new Error("err-a")
+ var errB = new Error("err-b", { cause: errA })
+ var errC = new Error("err-c", { cause: errB })
+ var errD = new Error("err-d", { cause: errC })
+ throw errD;
+ `
+ );
+ evaluationResult.set(
+ `eval throw Error Object with cyclical cause chain`,
+ `
+ var errX = new Error("err-x", { cause: errY})
+ var errY = new Error("err-y", { cause: errX })
+ throw errY;
+ `
+ );
+ evaluationResult.set(
+ `eval throw Error Object with falsy cause`,
+ `throw new Error("false cause", { cause: false });`
+ );
+ evaluationResult.set(
+ `eval throw Error Object with null cause`,
+ `throw new Error("null cause", { cause: null });`
+ );
+ evaluationResult.set(
+ `eval throw Error Object with undefined cause`,
+ `throw new Error("undefined cause", { cause: undefined });`
+ );
+ evaluationResult.set(
+ `eval throw Error Object with number cause`,
+ `throw new Error("number cause", { cause: 0 });`
+ );
+ evaluationResult.set(
+ `eval throw Error Object with string cause`,
+ `throw new Error("string cause", { cause: "cause message" });`
+ );
+ evaluationResult.set(
+ `eval throw Error Object with object cause`,
+ `throw new Error("object cause", { cause: { code: 234, message: "ERR_234"} });`
+ );
+
+ 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)`
+ );
+ evaluationResult.set(
+ `eval rejected promise with Error`,
+ `Promise.resolve().then(() => {
+ try {
+ a.b.c
+ } catch(e) {
+ throw new Error("something went wrong", { cause: e })
+ }
+ })`
+ );
+
+ 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..135166cef7
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_stubs_network_event.js
@@ -0,0 +1,233 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ createCommandsForTab,
+ STUBS_UPDATE_ENV,
+ getCleanedPacket,
+ getSerializedPacket,
+ getStubFile,
+ writeStubsToFile,
+} = require(`${CHROME_URL_ROOT}stub-generator-helpers`);
+
+const TEST_URI =
+ "https://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 = Services.env.get(STUBS_UPDATE_ENV) == "true";
+ info(`${isStubsUpdate ? "Update" : "Check"} ${STUB_FILE}`);
+
+ const generatedStubs = await generateNetworkEventStubs();
+
+ if (isStubsUpdate) {
+ await writeStubsToFile(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 = getSerializedPacket(packet, {
+ sortKeys: true,
+ replaceActorIds: true,
+ });
+ const existingPacketStr = getSerializedPacket(
+ existingStubs.stubPackets.get(key),
+ { sortKeys: true, replaceActorIds: 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 generateNetworkEventStubs() {
+ const stubs = new Map();
+ const tab = await addTab(TEST_URI);
+ const commands = await createCommandsForTab(tab);
+ await commands.targetCommand.startListening();
+ const resourceCommand = commands.resourceCommand;
+
+ const stacktraces = new Map();
+ let addNetworkStub = function () {};
+ let addNetworkUpdateStub = function () {};
+
+ const onAvailable = resources => {
+ for (const resource of resources) {
+ if (resource.resourceType == resourceCommand.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 == resourceCommand.TYPES.NETWORK_EVENT_STACKTRACE
+ ) {
+ stacktraces.set(resource.channelId, resource);
+ }
+ }
+ };
+ const onUpdated = updates => {
+ for (const { resource } of updates) {
+ addNetworkUpdateStub(resource);
+ }
+ };
+
+ await resourceCommand.watchResources(
+ [
+ resourceCommand.TYPES.NETWORK_EVENT_STACKTRACE,
+ resourceCommand.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]);
+ }
+ resourceCommand.unwatchResources(
+ [
+ resourceCommand.TYPES.NETWORK_EVENT_STACKTRACE,
+ resourceCommand.TYPES.NETWORK_EVENT,
+ ],
+ {
+ onAvailable,
+ onUpdated,
+ }
+ );
+
+ await commands.destroy();
+
+ 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..d6610b7309
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_stubs_page_error.js
@@ -0,0 +1,254 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ STUBS_UPDATE_ENV,
+ createCommandsForTab,
+ getCleanedPacket,
+ getSerializedPacket,
+ getStubFile,
+ writeStubsToFile,
+} = require(`${CHROME_URL_ROOT}stub-generator-helpers`);
+
+const TEST_URI =
+ "https://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 = Services.env.get(STUBS_UPDATE_ENV) == "true";
+ info(`${isStubsUpdate ? "Update" : "Check"} ${STUB_FILE}`);
+
+ const generatedStubs = await generatePageErrorStubs();
+
+ if (isStubsUpdate) {
+ await writeStubsToFile(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,
+ replaceActorIds: true,
+ });
+ const existingPacketStr = getSerializedPacket(
+ existingStubs.rawPackets.get(key),
+ { sortKeys: true, replaceActorIds: 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 generatePageErrorStubs() {
+ const stubs = new Map();
+
+ const tab = await addTab(TEST_URI);
+ const commands = await createCommandsForTab(tab);
+ await commands.targetCommand.startListening();
+ const resourceCommand = commands.resourceCommand;
+
+ // 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 resourceCommand.watchResources([resourceCommand.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(
+ `throw Error Object with error cause`,
+ `
+ var originalError = new SyntaxError("original error")
+ var err = new Error("something went wrong", {
+ cause: originalError
+ });
+ throw err;
+ `
+ );
+ pageError.set(
+ `throw Error Object with cause chain`,
+ `
+ var a = new Error("err-a")
+ var b = new Error("err-b", { cause: a })
+ var c = new Error("err-c", { cause: b })
+ var d = new Error("err-d", { cause: c })
+ throw d;
+ `
+ );
+ pageError.set(
+ `throw Error Object with cyclical cause chain`,
+ `
+ var a = new Error("err-a", { cause: b})
+ var b = new Error("err-b", { cause: a })
+ throw b;
+ `
+ );
+ pageError.set(
+ `throw Error Object with falsy cause`,
+ `throw new Error("null cause", { cause: null });`
+ );
+ pageError.set(
+ `throw Error Object with number cause`,
+ `throw new Error("number cause", { cause: 0 });`
+ );
+ pageError.set(
+ `throw Error Object with string cause`,
+ `throw new Error("string cause", { cause: "cause message" });`
+ );
+ pageError.set(
+ `throw Error Object with object cause`,
+ `throw new Error("object cause", { cause: { code: 234, message: "ERR_234"} });`
+ );
+ 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);
+ `
+ );
+ pageError.set(
+ `Promise reject Error Object with error cause`,
+ `Promise.resolve().then(() => {
+ try {
+ unknownFunc();
+ } catch(e) {
+ throw new Error("something went wrong", { cause: e })
+ }
+ })`
+ );
+ 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..aeadddc011
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_stubs_platform_messages.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ STUBS_UPDATE_ENV,
+ createCommandsForMainProcess,
+ getCleanedPacket,
+ getSerializedPacket,
+ getStubFile,
+ writeStubsToFile,
+} = require(`${CHROME_URL_ROOT}stub-generator-helpers`);
+
+const STUB_FILE = "platformMessage.js";
+
+add_task(async function () {
+ const isStubsUpdate = Services.env.get(STUBS_UPDATE_ENV) == "true";
+ info(`${isStubsUpdate ? "Update" : "Check"} ${STUB_FILE}`);
+
+ const generatedStubs = await generatePlatformMessagesStubs();
+
+ if (isStubsUpdate) {
+ await writeStubsToFile(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,
+ replaceActorIds: true,
+ });
+ const existingPacketStr = getSerializedPacket(
+ existingStubs.rawPackets.get(key),
+ { sortKeys: true, replaceActorIds: 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();
+
+ const commands = await createCommandsForMainProcess();
+ await commands.targetCommand.startListening();
+ const resourceCommand = commands.resourceCommand;
+
+ // 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 resourceCommand.watchResources(
+ [resourceCommand.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));
+ }
+
+ await commands.destroy();
+
+ 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..a28cd6bee5
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_telemetry_execute_js.js
@@ -0,0 +1,94 @@
+/* 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,<!DOCTYPE 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 keyboardExecuteAndWaitForResultMessage(hud, `"single line"`, "");
+
+ info("Evaluate another single line");
+ await keyboardExecuteAndWaitForResultMessage(hud, `"single line 2"`, "");
+
+ info("Evaluate multiple lines");
+ await keyboardExecuteAndWaitForResultMessage(hud, `"n"\n.trim()`, "");
+
+ info("Switch to editor mode");
+ await toggleLayout(hud);
+
+ info("Evaluate a single line in editor mode");
+ await keyboardExecuteAndWaitForResultMessage(hud, `"single line 3"`, "");
+
+ info("Evaluate multiple lines in editor mode");
+ await keyboardExecuteAndWaitForResultMessage(
+ hud,
+ `"y"\n.trim()\n.trim()`,
+ ""
+ );
+
+ info("Evaluate multiple lines again in editor mode");
+ await keyboardExecuteAndWaitForResultMessage(hud, `"x"\n.trim()`, "");
+
+ 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..95ff2e8e51
--- /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,<!DOCTYPE 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..4f4fbe7939
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_telemetry_js_errors.js
@@ -0,0 +1,62 @@
+/* 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,<!DOCTYPE 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(() => findErrorMessage(hud, "is not a function"));
+ checkErrorDisplayedTelemetry("JSMSG_NOT_FUNCTION", 1);
+
+ await reloadBrowser();
+
+ info("Reloading the page (and having the same error) increments the sum");
+ await waitFor(() => findErrorMessage(hud, "is not a function"));
+ checkErrorDisplayedTelemetry("JSMSG_NOT_FUNCTION", 2);
+
+ info(
+ "Evaluating an expression resulting in the same error increments the sum"
+ );
+ await executeAndWaitForErrorMessage(
+ 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 executeAndWaitForErrorMessage(
+ hud,
+ `"a".repeat(-1)`,
+ "repeat count must be non-negative"
+ );
+ checkErrorDisplayedTelemetry("JSMSG_NEGATIVE_REPETITION_COUNT", 1);
+
+ await executeAndWaitForErrorMessage(
+ 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..07a1d575f5
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_telemetry_jump_to_definition.js
@@ -0,0 +1,52 @@
+/* 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,<!DOCTYPE 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(() =>
+ findConsoleAPIMessage(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..6dc6149295
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_telemetry_object_expanded.js
@@ -0,0 +1,72 @@
+/* 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,<!DOCTYPE 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(() =>
+ findConsoleAPIMessage(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..e61dbbe7ec
--- /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.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+const TEST_URI = `data:text/html,<!DOCTYPE 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..f021eba61d
--- /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.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+const TEST_URI = `data:text/html,<!DOCTYPE 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 keyboardExecuteAndWaitForResultMessage(hud, `"single line 1"`, "");
+ await keyboardExecuteAndWaitForResultMessage(hud, `"single line 2"`, "");
+ await keyboardExecuteAndWaitForResultMessage(hud, `"single line 3"`, "");
+
+ 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 = waitForMessageByType(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..ee8b9b30e2
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_time_methods.js
@@ -0,0 +1,87 @@
+/* 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,<!DOCTYPE html><script>" +
+ "console.timeEnd('bTimer');</script>";
+
+const TEST_URI3 =
+ "data:text/html;charset=utf-8,<!DOCTYPE html><script>" +
+ "console.time('bTimer');console.log('smoke signal');</script>";
+
+const TEST_URI4 =
+ "data:text/html;charset=utf-8,<!DOCTYPE html>" +
+ "<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(() =>
+ findConsoleAPIMessage(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(() =>
+ findWarningMessage(hud2, "bTimer", ".timeEnd")
+ );
+ 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(() => findConsoleAPIMessage(hud2, "smoke signal"));
+
+ is(
+ findConsoleAPIMessage(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(() =>
+ findWarningMessage(hud2, "bTimer", ".timeEnd")
+ );
+ 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..85f78f0639
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_timestamps.js
@@ -0,0 +1,55 @@
+/* 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("resource://devtools/client/shared/prefs.js");
+
+const TEST_URI = `data:text/html;charset=utf-8,<!DOCTYPE html>
+ 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 = waitForMessageByType(
+ hud,
+ "simple text message",
+ ".console-api"
+ );
+ 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..c2d91fcb3b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_trackingprotection_errors.js
@@ -0,0 +1,268 @@
+/* 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.importESModule(
+ "resource://testing-common/UrlClassifierTestUtils.sys.mjs"
+);
+
+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(() =>
+ findWarningMessage(
+ 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(() =>
+ findWarningMessage(
+ 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(
+ () =>
+ findWarningMessage(
+ 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(() =>
+ findWarningMessage(
+ 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(() =>
+ findWarningMessage(
+ 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(() =>
+ findWarningMessage(
+ hud,
+ `Partitioned cookie or storage access was provided to ${PARTITIONED_URL} because it is ` +
+ `loaded in the third-party context and dynamic state 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(() =>
+ findWarningMessage(
+ 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..48970dd2f6
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_uncaught_exception.js
@@ -0,0 +1,130 @@
+/* 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,<!DOCTYPE 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 custom error with name and message getters");
+ // register the class
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ const script = content.document.createElement("script");
+ script.append(
+ content.document.createTextNode(
+ `
+ class CustomError extends Error {
+ get name() {
+ return "CustomErrorName";
+ }
+
+ get message() {
+ return "custom-error-message";
+ }
+ }`.trim()
+ )
+ );
+ content.document.body.append(script);
+ });
+
+ await checkThrowingWithStack(
+ hud,
+ `new CustomError()`,
+ "Uncaught CustomErrorName: custom-error-message",
+ // Additional frames: the stacktrace contains the CustomError call
+ [1]
+ );
+ info("Check that object in errors can be expanded");
+ const rejectedObjectMessage = findErrorMessage(hud, "eggplant");
+ 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:
+ // Object { fav: "eggplant" }
+ // | 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(`Object { fav: "eggplant" }`));
+ ok(oiNodes[1].textContent.includes(`fav: "eggplant"`));
+ ok(oiNodes[2].textContent.includes(`<prototype>: Object { \u2026 }`));
+});
+
+async function checkThrowingWithStack(
+ hud,
+ expression,
+ expectedMessage,
+ additionalFrameLines = []
+) {
+ 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, [
+ ...additionalFrameLines,
+ 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..6a5a92d535
--- /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(() =>
+ findErrorMessage(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..ac949e4079
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_visibility_messages.js
@@ -0,0 +1,137 @@
+/* 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(() => findConsoleAPIMessage(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.mutableMessagesById.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 findConsoleAPIMessages(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(() => findConsoleAPIMessage(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(() => findConsoleAPIMessage(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");
+ await waitFor(
+ async () =>
+ (
+ await findMessagesVirtualizedByType({
+ hud,
+ text: "in-inspector log ",
+ typeSelector: ".console-api",
+ })
+ ).length === MESSAGES_COUNT
+ );
+ 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..b455ac61be
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_warn_about_replaced_api.js
@@ -0,0 +1,59 @@
+/* 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,<!DOCTYPE html><script>console = {log: () => ''}</script>";
+const TEST_URI_NOT_REPLACED =
+ "data:text/html;charset=utf8,<!DOCTYPE html><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.loadURIString(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(!findWarningMessage(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 reloadBrowser();
+ await waitFor(() => {
+ return (
+ findConsoleAPIMessages(hud, "foo").length === 2 &&
+ findMessagesByType(hud, "foo", ".navigationMarker").length === 1
+ );
+ });
+
+ ok(!findWarningMessage(hud, "logging API"), "no warning displayed");
+}
+
+async function testWarningPresent(hud) {
+ info("wait for the warning to show");
+ await waitFor(() => findWarningMessage(hud, "logging API"));
+
+ info("reload the test page and wait for the warning to show");
+ await reloadBrowser();
+ await waitFor(() => {
+ return findWarningMessages(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..dbe5b508d1
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_warning_group_content_blocking.js
@@ -0,0 +1,256 @@
+/* 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 = "https://example.com/" + TEST_FILE;
+
+const TRACKER_URL = "https://tracking.example.com/";
+const IMG_FILE =
+ "browser/devtools/client/webconsole/test/browser/test-image.png";
+const TRACKER_IMG = "https://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.importESModule(
+ "resource://testing-common/UrlClassifierTestUtils.sys.mjs"
+);
+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(
+ "https://tracking.example.org/" + TEST_FILE
+ );
+ const now = Date.now();
+
+ info("Test content blocking message");
+ const message =
+ `The resource at \u201chttps://tracking.example.com/?1&${now}\u201d ` +
+ `was blocked because content blocking is enabled`;
+ const onContentBlockingWarningMessage = waitForMessageByType(
+ 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 = waitForMessageByType(
+ 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"
+ );
+
+ await checkConsoleOutputForWarningGroup(hud, [
+ `▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL} 2`,
+ ]);
+
+ info("Open the group");
+ node.querySelector(".arrow").click();
+ await waitFor(() =>
+ findWarningMessage(hud, "https://tracking.example.com/?1")
+ );
+
+ await checkConsoleOutputForWarningGroup(hud, [
+ `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL} 2`,
+ `| The resource at \u201chttps://tracking.example.com/?1&${now}\u201d was blocked`,
+ `| The resource at \u201chttps://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(
+ "https://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);
+
+ // Bug 1763367 - Filter out message like:
+ // Cookie “name=value” has been rejected as third-party.
+ // that appear in a random order.
+ await setFilterState(hud, { text: "-has been rejected" });
+
+ const getWarningMessage = url => groupLabel.replace("<URL>", url);
+
+ const onStorageAccessBlockedMessage = waitForMessageByType(
+ 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 = waitForMessageByType(
+ 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"
+ );
+
+ await checkConsoleOutputForWarningGroup(hud, [`▶︎⚠ ${groupLabel} 2`]);
+
+ info("Open the group");
+ node.querySelector(".arrow").click();
+ await waitFor(() => findWarningMessage(hud, TRACKER_IMG));
+
+ await 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..bc611efde1
--- /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” does not have a proper “SameSite” attribute value. Soon, cookies without the “SameSite” attribute or with an invalid value will be treated as “Lax”. This means that the cookie will no longer be sent in third-party contexts. If your application depends on this cookie being available in such contexts, please add the “SameSite=None“ attribute to it. To know more about the “SameSite“ attribute, read https://developer.mozilla.org/docs/Web/HTTP/Headers/Set-Cookie/SameSite",
+ typeMessage1: ".warn",
+ message2:
+ "Cookie “b” does not have a proper “SameSite” attribute value. Soon, cookies without the “SameSite” attribute or with an invalid value will be treated as “Lax”. This means that the cookie will no longer be sent in third-party contexts. If your application depends on this cookie being available in such contexts, please add the “SameSite=None“ attribute to it. To know more about the “SameSite“ attribute, read https://developer.mozilla.org/docs/Web/HTTP/Headers/Set-Cookie/SameSite",
+ },
+ ];
+
+ 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 = waitForMessageByType(
+ 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 = waitForMessageByType(
+ 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"
+ );
+
+ await checkConsoleOutputForWarningGroup(hud, [`▶︎⚠ ${test.groupLabel} 2`]);
+
+ info("Open the group");
+ node.querySelector(".arrow").click();
+ await waitFor(() => findWarningMessage(hud, "SameSite"));
+
+ await 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 waitForMessageByType(hud, groupLabel, ".warn");
+ is(
+ node.querySelector(".warning-group-badge").textContent,
+ "2",
+ "The badge has the expected text"
+ );
+
+ await checkConsoleOutputForWarningGroup(hud, [`▶︎⚠ ${groupLabel} 2`]);
+
+ info("Open the group");
+ node.querySelector(".arrow").click();
+ await waitFor(() => findWarningMessage(hud, "SameSite"));
+
+ await 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_csp.js b/devtools/client/webconsole/test/browser/browser_webconsole_warning_group_csp.js
new file mode 100644
index 0000000000..bbd7ee4dd9
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_warning_group_csp.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Load a page that generates multiple CSP parser warnings.
+
+"use strict";
+
+const TEST_FILE =
+ "browser/devtools/client/webconsole/test/browser/test-warning-group-csp.html";
+
+add_task(async function testCSPGroup() {
+ const GROUP_LABEL = "Content-Security-Policy warnings";
+
+ const hud = await openNewTabAndConsole("https://example.org/" + TEST_FILE);
+
+ info("Checking for warning group");
+ await checkConsoleOutputForWarningGroup(hud, [`▶︎⚠ ${GROUP_LABEL} 4`]);
+
+ info("Expand the warning group");
+ const node = findWarningMessage(hud, GROUP_LABEL);
+ node.querySelector(".arrow").click();
+ await checkConsoleOutputForWarningGroup(hud, [
+ `▼︎⚠ ${GROUP_LABEL} 4`,
+ `| Ignoring “http:” within script-src: ‘strict-dynamic’ specified`,
+ `| Ignoring “https:” within script-src: ‘strict-dynamic’ specified`,
+ `| Ignoring “'unsafe-inline'” within script-src: ‘strict-dynamic’ specified`,
+ `| Keyword ‘strict-dynamic’ within “script-src” with no valid nonce or hash might block all scripts from loading`,
+ ]);
+});
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..c73ddc9483
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_warning_group_multiples.js
@@ -0,0 +1,326 @@
+/* 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 = "https://example.org/" + TEST_FILE;
+
+const TRACKER_URL = "https://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 = "https://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.importESModule(
+ "resource://testing-common/UrlClassifierTestUtils.sys.mjs"
+);
+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);
+
+ // Bug 1763367 - Filter out message like:
+ // Cookie “name=value” has been rejected as third-party.
+ // that appear in a random order.
+ await setFilterState(hud, { text: "-has been rejected" });
+
+ info(
+ "Log a tracking protection message to check a single message isn't grouped"
+ );
+ let onContentBlockedMessage = waitForMessageByType(
+ 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.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 = waitForMessageByType(
+ 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"
+ );
+
+ await checkConsoleOutputForWarningGroup(hud, [
+ `▶︎⚠ ${CONTENT_BLOCKED_GROUP_LABEL}`,
+ `simple message 1`,
+ ]);
+
+ let onStorageBlockedWarningGroupMessage = waitForMessageByType(
+ hud,
+ STORAGE_BLOCKED_URL,
+ ".warn"
+ );
+
+ emitStorageAccessBlockedMessage(hud);
+ ({ node } = await onStorageBlockedWarningGroupMessage);
+ is(
+ node.querySelector(".warning-indent"),
+ null,
+ "The message has the expected style"
+ );
+ is(
+ node.getAttribute("data-indent"),
+ "0",
+ "The message has the expected indent"
+ );
+
+ info("Log a second simple message");
+ await logString(hud, "simple message 2");
+
+ await 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 = waitForMessageByType(
+ 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(() => findWarningMessage(hud, STORAGE_BLOCKED_URL));
+
+ await 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 = waitForMessageByType(
+ hud,
+ STORAGE_BLOCKED_URL,
+ ".warn"
+ );
+ emitStorageAccessBlockedMessage(hud);
+ await onStorageBlockedMessage;
+
+ await 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(() => findWarningMessage(hud, CONTENT_BLOCKED_URL));
+
+ await 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(() =>
+ findMessageByType(hud, "Navigated to", ".navigationMarker")
+ );
+
+ info("Add a storage blocked message and a content blocked one");
+ onStorageBlockedMessage = waitForMessageByType(
+ hud,
+ STORAGE_BLOCKED_URL,
+ ".warn"
+ );
+ emitStorageAccessBlockedMessage(hud);
+ await onStorageBlockedMessage;
+
+ onContentBlockedMessage = waitForMessageByType(
+ hud,
+ CONTENT_BLOCKED_URL,
+ ".warn"
+ );
+ emitContentBlockingMessage(hud);
+ await onContentBlockedMessage;
+
+ await 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 = waitForMessageByType(
+ hud,
+ STORAGE_BLOCKED_GROUP_LABEL,
+ ".warn"
+ );
+ emitStorageAccessBlockedMessage();
+ await onStorageBlockedWarningGroupMessage;
+
+ onContentBlockedWarningGroupMessage = waitForMessageByType(
+ hud,
+ CONTENT_BLOCKED_GROUP_LABEL,
+ ".warn"
+ );
+ emitContentBlockingMessage();
+ await onContentBlockedWarningGroupMessage;
+
+ await 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 = waitForMessageByType(hud, str, ".console-api");
+ 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..86276b5567
--- /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 dynamic state 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 = waitForMessageByType(
+ 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 = waitForMessageByType(
+ 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"
+ );
+
+ await checkConsoleOutputForWarningGroup(hud, [
+ `▶︎⚠ ${STORAGE_ISOLATION_GROUP_LABEL} 2`,
+ ]);
+
+ info("Open the group");
+ node.querySelector(".arrow").click();
+ await waitFor(() => findWarningMessage(hud, url1));
+
+ await 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..a78926fe10
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_warning_groups.js
@@ -0,0 +1,286 @@
+/* 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 = "https://example.org/" + TEST_FILE;
+
+const TRACKER_URL = "https://tracking.example.com/";
+const BLOCKED_URL =
+ TRACKER_URL +
+ "browser/devtools/client/webconsole/test/browser/test-image.png";
+
+const { UrlClassifierTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlClassifierTestUtils.sys.mjs"
+);
+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 = waitForMessageByType(
+ hud,
+ BLOCKED_URL,
+ ".warn"
+ );
+ emitContentBlockedMessage(hud);
+ let { node } = await onContentBlockingWarningMessage;
+ is(
+ node.querySelector(".warning-indent"),
+ null,
+ "The message has the expected style"
+ );
+ is(
+ node.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 = waitForMessageByType(
+ 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"
+ );
+
+ await checkConsoleOutputForWarningGroup(hud, [
+ `▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
+ `simple message 1`,
+ ]);
+
+ info("Log another simple message");
+ await logString(hud, "simple message 2");
+
+ await 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"
+ );
+
+ await checkConsoleOutputForWarningGroup(hud, [
+ `▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
+ `simple message 1`,
+ `simple message 2`,
+ ]);
+
+ info("Open the group");
+ node.querySelector(".arrow").click();
+ await waitFor(() => findWarningMessage(hud, BLOCKED_URL));
+
+ await 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 = waitForMessageByType(
+ hud,
+ BLOCKED_URL,
+ ".warn"
+ );
+ emitContentBlockedMessage(hud);
+ await onContentBlockingWarningMessage;
+ ok(true, "The new tracking protection message is displayed");
+
+ await 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(() =>
+ findMessageByType(hud, "Navigated to", ".navigationMarker")
+ );
+
+ info("Log a tracking protection message to check it is not grouped");
+ onContentBlockingWarningMessage = waitForMessageByType(
+ hud,
+ BLOCKED_URL,
+ ".warn"
+ );
+ emitContentBlockedMessage(hud);
+ await onContentBlockingWarningMessage;
+
+ await logString(hud, "simple message 3");
+
+ await 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 = waitForMessageByType(
+ 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"
+ );
+
+ await 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(() => findWarningMessages(hud, BLOCKED_URL).length === 6);
+
+ await 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(() => findWarningMessages(hud, BLOCKED_URL).length === 4);
+
+ await 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"
+ );
+
+ await 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 = waitForMessageByType(hud, str, ".console-api");
+ 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..b85f35e809
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_warning_groups_filtering.js
@@ -0,0 +1,337 @@
+/* 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 = "https://example.org/" + TEST_FILE;
+
+const TRACKER_URL = "https://tracking.example.com/";
+const BLOCKED_URL =
+ TRACKER_URL +
+ "browser/devtools/client/webconsole/test/browser/test-image.png";
+
+const { UrlClassifierTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlClassifierTestUtils.sys.mjs"
+);
+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 = waitForMessageByType(
+ hud,
+ BLOCKED_URL,
+ ".warn"
+ );
+ emitContentBlockedMessage(hud);
+ await onContentBlockingWarningMessage;
+ await logStrings(hud, "simple message A");
+ let onContentBlockingWarningGroupMessage = waitForMessageByType(
+ 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(() =>
+ findMessageByType(hud, "Navigated to", ".navigationMarker")
+ );
+
+ onContentBlockingWarningMessage = waitForMessageByType(
+ hud,
+ BLOCKED_URL,
+ ".warn"
+ );
+ emitContentBlockedMessage(hud);
+ await onContentBlockingWarningMessage;
+ await logStrings(hud, "simple message C");
+ onContentBlockingWarningGroupMessage = waitForMessageByType(
+ hud,
+ CONTENT_BLOCKING_GROUP_LABEL,
+ ".warn"
+ );
+ emitContentBlockedMessage(hud);
+ const warningGroupMessage2 = (await onContentBlockingWarningGroupMessage)
+ .node;
+ emitContentBlockedMessage(hud);
+ await waitForBadgeNumber(warningGroupMessage2, "3");
+
+ await 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(() => !findWarningMessage(hud, CONTENT_BLOCKING_GROUP_LABEL));
+
+ await 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(() => findWarningMessage(hud, CONTENT_BLOCKING_GROUP_LABEL));
+
+ await 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");
+ findWarningMessages(hud, CONTENT_BLOCKING_GROUP_LABEL)[0]
+ .querySelector(".arrow")
+ .click();
+ await waitFor(() => findWarningMessage(hud, BLOCKED_URL));
+
+ await 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(() => !findWarningMessage(hud, CONTENT_BLOCKING_GROUP_LABEL));
+
+ await 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(() => findWarningMessage(hud, CONTENT_BLOCKING_GROUP_LABEL));
+ await 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(() => !findConsoleAPIMessage(hud, "simple message"));
+ await 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");
+ findWarningMessages(hud, CONTENT_BLOCKING_GROUP_LABEL)[1]
+ .querySelector(".arrow")
+ .click();
+ await waitFor(() => findWarningMessage(hud, "?6"));
+
+ await 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(() => !findWarningMessage(hud, "?1"));
+ await 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(() => findWarningMessage(hud, "?7"));
+ await 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: "" });
+ await 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(() => !findWarningMessage(hud, CONTENT_BLOCKING_GROUP_LABEL));
+ await 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(() => findWarningMessage(hud, CONTENT_BLOCKING_GROUP_LABEL));
+ await 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 = waitForMessageByType(hud, `${str} #1`, ".console-api");
+ const onSecondMessage = waitForMessageByType(
+ hud,
+ `${str} #2`,
+ ".console-api"
+ );
+ 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..3b5db30c86
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_warning_groups_outside_console_group.js
@@ -0,0 +1,220 @@
+/* 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 = "https://example.org/" + TEST_FILE;
+
+const TRACKER_URL = "https://tracking.example.com/";
+const BLOCKED_URL =
+ TRACKER_URL +
+ "browser/devtools/client/webconsole/test/browser/test-image.png";
+
+const { UrlClassifierTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlClassifierTestUtils.sys.mjs"
+);
+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 = waitForMessageByType(hud, "myGroup", ".console-api");
+ let onInGroupMessage = waitForMessageByType(
+ hud,
+ "log in group",
+ ".console-api"
+ );
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ content.wrappedJSObject.console.group("myGroup");
+ content.wrappedJSObject.console.log("log in group");
+ });
+ const { node: consoleGroupMessageNode } = await onGroupMessage;
+ await onInGroupMessage;
+
+ await 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 = waitForMessageByType(
+ hud,
+ BLOCKED_URL,
+ ".warn"
+ );
+ emitContentBlockedMessage(now);
+ await onContentBlockingWarningMessage;
+
+ await checkConsoleOutputForWarningGroup(hud, [
+ `▼ myGroup`,
+ `| log in group`,
+ `| ${BLOCKED_URL}?${now}-1`,
+ ]);
+
+ info("Collapse the console.group");
+ consoleGroupMessageNode.querySelector(".arrow").click();
+ await waitFor(() => !findConsoleAPIMessage(hud, "log in group"));
+
+ await checkConsoleOutputForWarningGroup(hud, [`▶︎ myGroup`]);
+
+ info("Expand the console.group");
+ consoleGroupMessageNode.querySelector(".arrow").click();
+ await waitFor(() => findConsoleAPIMessage(hud, "log in group"));
+
+ await 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 = waitForMessageByType(
+ hud,
+ CONTENT_BLOCKING_GROUP_LABEL,
+ ".warn"
+ );
+ emitContentBlockedMessage(now);
+ const { node: warningGroupNode } = await onContentBlockingWarningGroupMessage;
+
+ await checkConsoleOutputForWarningGroup(hud, [
+ `▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
+ `▼ myGroup`,
+ `| log in group`,
+ ]);
+
+ info("Open the warning group");
+ warningGroupNode.querySelector(".arrow").click();
+ await waitFor(() => findWarningMessage(hud, BLOCKED_URL));
+
+ await 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 = waitForMessageByType(
+ hud,
+ BLOCKED_URL,
+ ".warn"
+ );
+ emitContentBlockedMessage(now);
+ await onContentBlockingWarningMessage;
+ ok(true, "The new tracking protection message is displayed");
+
+ await 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 = waitForMessageByType(hud, "log in group", ".console-api");
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ content.wrappedJSObject.console.log("second log in group");
+ });
+ await onInGroupMessage;
+
+ await 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(() => !findConsoleAPIMessage(hud, "log in group"));
+
+ await 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(() => !findWarningMessage(hud, BLOCKED_URL));
+
+ await checkConsoleOutputForWarningGroup(hud, [
+ `▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
+ `▶︎ myGroup`,
+ ]);
+
+ info("Open the console group");
+ consoleGroupMessageNode.querySelector(".arrow").click();
+ await waitFor(() => findConsoleAPIMessage(hud, "log in group"));
+
+ await 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(() => !findConsoleAPIMessage(hud, "log in group"));
+
+ await checkConsoleOutputForWarningGroup(hud, [
+ `▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
+ `▶︎ myGroup`,
+ ]);
+
+ info("Open the warning group");
+ warningGroupNode.querySelector(".arrow").click();
+ await waitFor(() => findWarningMessage(hud, BLOCKED_URL));
+
+ await 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..54e3d884e3
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_warning_groups_toggle.js
@@ -0,0 +1,284 @@
+/* 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("resource://devtools/client/shared/prefs.js");
+
+const TEST_FILE =
+ "browser/devtools/client/webconsole/test/browser/test-warning-groups.html";
+const TEST_URI = "https://example.org/" + TEST_FILE;
+
+const TRACKER_URL = "https://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.importESModule(
+ "resource://testing-common/UrlClassifierTestUtils.sys.mjs"
+);
+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 = waitForMessageByType(
+ hud,
+ `${BLOCKED_URL}?1`,
+ ".warn"
+ );
+ emitContentBlockedMessage(hud);
+ await onContentBlockingWarningMessage;
+ await logString(hud, "simple message 1");
+
+ onContentBlockingWarningMessage = waitForMessageByType(
+ hud,
+ `${BLOCKED_URL}?2`,
+ ".warn"
+ );
+ emitContentBlockedMessage(hud);
+ await onContentBlockingWarningMessage;
+
+ onContentBlockingWarningMessage = waitForMessageByType(
+ hud,
+ `${BLOCKED_URL}?3`,
+ ".warn"
+ );
+ emitContentBlockedMessage(hud);
+ await onContentBlockingWarningMessage;
+
+ await 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(() =>
+ findWarningMessage(hud, CONTENT_BLOCKING_GROUP_LABEL)
+ );
+ is(
+ warningGroupMessage1.querySelector(".warning-group-badge").textContent,
+ "3",
+ "The badge has the expected text"
+ );
+
+ await 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(() => findWarningMessage(hud, `${BLOCKED_URL}?4`));
+
+ // Warning messages are displayed at the expected positions.
+ await 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(() =>
+ findWarningMessage(hud, CONTENT_BLOCKING_GROUP_LABEL)
+ );
+
+ await checkConsoleOutputForWarningGroup(hud, [
+ `▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
+ `simple message 1`,
+ ]);
+
+ info("Expand the warning group");
+ warningGroupMessage1.querySelector(".arrow").click();
+ await waitFor(() => findWarningMessage(hud, BLOCKED_URL));
+
+ await 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(() =>
+ findMessageByType(hud, "Navigated to", ".navigationMarker")
+ );
+
+ info("Disable the warningGroup feature pref again");
+ await toggleWarningGroupPreference(hud);
+
+ info("Add one warning message and one simple message");
+ await waitFor(() => findWarningMessage(hud, `${BLOCKED_URL}?4`));
+ onContentBlockingWarningMessage = waitForMessageByType(
+ hud,
+ BLOCKED_URL,
+ ".warn"
+ );
+ emitContentBlockedMessage(hud);
+ await onContentBlockingWarningMessage;
+ await logString(hud, "simple message 2");
+
+ // nothing is grouped.
+ await 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(() => findWarningMessage(hud, CONTENT_BLOCKING_GROUP_LABEL));
+
+ await 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 = waitForMessageByType(
+ hud,
+ CONTENT_BLOCKING_GROUP_LABEL,
+ ".warn"
+ );
+ emitContentBlockedMessage(hud);
+ const warningGroupMessage2 = (await onContentBlockingWarningGroupMessage)
+ .node;
+ await waitForBadgeNumber(warningGroupMessage2, "2");
+
+ await 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(() => findWarningMessage(hud, `${BLOCKED_URL}?6`));
+
+ await 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 = waitForMessageByType(hud, str, ".console-api");
+ 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..a064215d42
--- /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,<!DOCTYPE html>Wasm errors`;
+
+add_task(async function () {
+ const hud = await openNewTabAndConsole(TEST_URI);
+
+ const onCompileError = waitForMessageByType(
+ 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 = waitForMessageByType(
+ 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 = waitForMessageByType(
+ 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_webextension_promise_rejection.js b/devtools/client/webconsole/test/browser/browser_webconsole_webextension_promise_rejection.js
new file mode 100644
index 0000000000..a575e3e13f
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_webextension_promise_rejection.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that an uncaught promise rejection from a content script
+// is reported to the tabs' webconsole.
+
+"use strict";
+
+const TEST_URI =
+ "https://example.com/browser/devtools/client/webconsole/" +
+ "test/browser/test-blank.html";
+
+add_task(async function () {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [
+ {
+ matches: [TEST_URI],
+ js: ["content-script.js"],
+ },
+ ],
+ },
+
+ files: {
+ "content-script.js": function () {
+ Promise.reject("abc");
+ },
+ },
+ });
+
+ await extension.startup();
+
+ const hud = await openNewTabAndConsole(TEST_URI);
+ await waitFor(() => findErrorMessage(hud, "uncaught exception: abc"));
+
+ await extension.unload();
+});
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..dfa15c7c1c
--- /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 =
+ "https://example.com/browser/devtools/client/webconsole/test/browser/test-websocket.html";
+
+add_task(async function () {
+ const hud = await openNewTabAndConsole(TEST_URI);
+ await waitFor(
+ () => findErrorMessage(hud, "wss://0.0.0.0:81"),
+ "Did not find error message for wss://0.0.0.0:81 connection",
+ 500
+ );
+ await waitFor(
+ () => findErrorMessage(hud, "wss://0.0.0.0:82"),
+ "Did not find error message for wss://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..6f7ad3f88b
--- /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..98aca6298b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_worker_evaluate.js
@@ -0,0 +1,30 @@
+/* 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.commands.targetCommand.store.getState().targets.length == 2
+ );
+ const dbg = createDebuggerContext(toolbox);
+
+ execute(hud, "pauseInWorker(42)");
+
+ await waitForPaused(dbg);
+ await openConsole();
+
+ await executeAndWaitForResultMessage(hud, "data", "42");
+ 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..36ad8b43db
--- /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(() =>
+ findErrorMessage(hud, "uncaught exception: worker-error")
+ );
+
+ await waitFor(() =>
+ findErrorMessage(hud, "uncaught exception: worklet-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..63b64c74b6
--- /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(() =>
+ findErrorMessage(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..e5d10b54d4
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/head.js
@@ -0,0 +1,1912 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+"use strict";
+
+/* globals Task, openToolboxForTab, gBrowser */
+
+// shared-head.js handles imports, constants, and utility functions
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
+
+// Import helpers for the new debugger
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/debugger/test/mochitest/shared-head.js",
+ this
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/webconsole/test/browser/shared-head.js",
+ this
+);
+
+var {
+ BrowserConsoleManager,
+} = require("resource://devtools/client/webconsole/browser-console-manager.js");
+
+var WCUL10n = require("resource://devtools/client/webconsole/utils/l10n.js");
+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("resource://devtools/client/webconsole/actions/index.js");
+
+registerCleanupFunction(async function () {
+ // Reset all cookies, tests loading sjs_slow-response-test-server.sjs will
+ // set a foo cookie which might have side effects on other tests.
+ Services.cookies.removeAll();
+
+ 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.mutableMessagesById.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 with given message type 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: {String} Partial text match in .message-body
+ * - typeSelector: {String} A part of selector for the message, to
+ * specify the message type.
+ * @return promise
+ * A promise that is resolved to an array of the message nodes
+ */
+function waitForMessagesByType({ hud, messages }) {
+ return new Promise(resolve => {
+ const matchedMessages = [];
+ hud.ui.on("new-messages", function messagesReceived(newMessages) {
+ for (const message of messages) {
+ if (message.matched) {
+ continue;
+ }
+
+ const typeSelector = message.typeSelector;
+ if (!typeSelector) {
+ throw new Error("typeSelector property is required");
+ }
+ if (!typeSelector.startsWith(".")) {
+ throw new Error(
+ "typeSelector property start with a dot e.g. `.result`"
+ );
+ }
+ const selector = ".message" + typeSelector;
+
+ 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 {String} typeSelector : A part of selector for the message, to
+ * specify the message type.
+ * @param {Number} repeat : expected repeat count in .message-repeats
+ */
+function waitForRepeatedMessageByType(hud, text, typeSelector, repeat) {
+ return waitFor(() => {
+ // Wait for a message matching the provided text.
+ const node = findMessageByType(hud, text, typeSelector);
+ 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 with given message type in the web console output,
+ * resolving with the first message that matches the query once it is received.
+ *
+ * @param {Object} hud : the webconsole
+ * @param {String} text : text included in .message-body
+ * @param {String} typeSelector : A part of selector for the message, to
+ * specify the message type.
+ * @return promise
+ * A promise that is resolved to the message node
+ */
+async function waitForMessageByType(hud, text, typeSelector) {
+ const messages = await waitForMessagesByType({
+ hud,
+ messages: [{ text, typeSelector }],
+ });
+ 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
+ * with given message type 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} typeSelector : A part of selector for the message, to
+ * specify the message type.
+ */
+function executeAndWaitForMessageByType(
+ hud,
+ input,
+ matchingText,
+ typeSelector
+) {
+ const onMessage = waitForMessageByType(hud, matchingText, typeSelector);
+ execute(hud, input);
+ return onMessage;
+}
+
+/**
+ * Type-specific wrappers for executeAndWaitForMessageByType
+ *
+ * @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.
+ */
+function executeAndWaitForResultMessage(hud, input, matchingText) {
+ return executeAndWaitForMessageByType(hud, input, matchingText, ".result");
+}
+
+function executeAndWaitForErrorMessage(hud, input, matchingText) {
+ return executeAndWaitForMessageByType(hud, input, matchingText, ".error");
+}
+
+/**
+ * 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 with given message type 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} typeSelector : A part of selector for the message, to
+ * specify the message type.
+ */
+function keyboardExecuteAndWaitForMessageByType(
+ hud,
+ input,
+ matchingText,
+ typeSelector
+) {
+ hud.jsterm.focus();
+ setInputValue(hud, input);
+ const onMessage = waitForMessageByType(hud, matchingText, typeSelector);
+ if (isEditorModeEnabled(hud)) {
+ EventUtils.synthesizeKey("KEY_Enter", {
+ [Services.appinfo.OS === "Darwin" ? "metaKey" : "ctrlKey"]: true,
+ });
+ } else {
+ EventUtils.synthesizeKey("VK_RETURN");
+ }
+ return onMessage;
+}
+
+/**
+ * Type-specific wrappers for keyboardExecuteAndWaitForMessageByType
+ *
+ * @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.
+ */
+function keyboardExecuteAndWaitForResultMessage(hud, input, matchingText) {
+ return keyboardExecuteAndWaitForMessageByType(
+ hud,
+ input,
+ matchingText,
+ ".result"
+ );
+}
+
+/**
+ * Wait for a message to be logged and ensure it is logged only once.
+ *
+ * @param object hud
+ * The web console.
+ * @param string text
+ * A substring that can be found in the message.
+ * @param string typeSelector
+ * A part of selector for the message, to specify the message type.
+ * @return {Node} the node corresponding the found message
+ */
+async function checkUniqueMessageExists(hud, msg, typeSelector) {
+ info(`Checking "${msg}" was logged`);
+ let messages;
+ try {
+ messages = await waitFor(async () => {
+ const msgs = await findMessagesVirtualizedByType({
+ hud,
+ text: msg,
+ typeSelector,
+ });
+ return msgs.length ? msgs : null;
+ });
+ } catch (e) {
+ ok(false, `Message "${msg}" wasn't logged\n`);
+ return null;
+ }
+
+ is(messages.length, 1, `"${msg}" was logged once`);
+ const [messageEl] = messages;
+ const repeatNode = messageEl.querySelector(".message-repeats");
+ is(repeatNode, null, `"${msg}" wasn't repeated`);
+ return messageEl;
+}
+
+/**
+ * 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 || popup.state == "hidden") {
+ 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");
+}
+
+/**
+ * Toggle Enable network monitoring setting
+ *
+ * @param object hud
+ * The web console.
+ * @param boolean shouldBeSwitchedOn
+ * The expected state the setting should be in after the toggle.
+ */
+async function toggleNetworkMonitoringConsoleSetting(hud, shouldBeSwitchedOn) {
+ const selector =
+ ".webconsole-console-settings-menu-item-enableNetworkMonitoring";
+ const settingChanged = waitFor(() => {
+ const el = getConsoleSettingElement(hud, selector);
+ return shouldBeSwitchedOn
+ ? el.getAttribute("aria-checked") === "true"
+ : el.getAttribute("aria-checked") !== "true";
+ });
+ await toggleConsoleSetting(hud, selector);
+ await settingChanged;
+}
+
+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} options
+ * - text: {String} The text to search for. This should be contained in
+ * the message. The searching is done with
+ * @see findMessageByType.
+ * - typeSelector: {string} A part of selector for the message, to
+ * specify the message type.
+ * - expectUrl: {boolean} Whether the URL in the opened source should
+ * match the link, or whether it is expected to
+ * be null.
+ * - expectLine: {boolean} It indicates if there is the need to check
+ * the line.
+ * - expectColumn: {boolean} It indicates if there is the need to check
+ * the column.
+ * - logPointExpr: {String} The logpoint expression
+ */
+async function testOpenInDebugger(
+ hud,
+ {
+ text,
+ typeSelector,
+ expectUrl = true,
+ expectLine = true,
+ expectColumn = true,
+ logPointExpr = undefined,
+ }
+) {
+ info(`Finding message for open-in-debugger test; text is "${text}"`);
+ const messageNode = await waitFor(() =>
+ findMessageByType(hud, text, typeSelector)
+ );
+ const locationNode = messageNode.querySelector(".message-location");
+ ok(locationNode, "The message does have a location link");
+ await checkClickOnNode(
+ hud,
+ hud.toolbox,
+ locationNode,
+ 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");
+
+ 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;
+ }
+
+ let toolbox = await gDevTools.getToolboxForTab(options.tab);
+ const dbgPanelAlreadyOpen = toolbox && toolbox.getPanel("jsdebugger");
+ if (dbgPanelAlreadyOpen) {
+ await toolbox.selectTool("jsdebugger");
+
+ return {
+ target: toolbox.target,
+ toolbox,
+ panel: toolbox.getCurrentPanel(),
+ };
+ }
+
+ toolbox = await gDevTools.showToolboxForTab(options.tab, {
+ toolId: "jsdebugger",
+ });
+ const panel = toolbox.getCurrentPanel();
+
+ await toolbox.threadFront.getSources();
+
+ return { target: toolbox.target, toolbox, panel };
+}
+
+async function openInspector(options = {}) {
+ if (!options.tab) {
+ options.tab = gBrowser.selectedTab;
+ }
+
+ const toolbox = await gDevTools.showToolboxForTab(options.tab, {
+ toolId: "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) {
+ tab = tab || gBrowser.selectedTab;
+ let toolbox = await gDevTools.getToolboxForTab(tab);
+ if (!toolbox) {
+ toolbox = await gDevTools.showToolboxForTab(tab);
+ }
+ 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) {
+ tab = tab || gBrowser.selectedTab;
+ const toolbox = await gDevTools.showToolboxForTab(tab, {
+ toolId: "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 toolbox = await gDevTools.getToolboxForTab(tab);
+ if (toolbox) {
+ await toolbox.destroy();
+ }
+}
+
+/**
+ * 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(() =>
+ findMessageByType(hud, urlInConsole, ".network")
+ );
+
+ 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");
+ menuPopup.activateItem(openInNetMenuItem);
+
+ 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 {String} selector: The selector for the node we want to select.
+ */
+async function selectNodeWithPicker(toolbox, 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");
+
+ await safeSynthesizeMouseEventAtCenterInContentPage(selector);
+
+ 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.store, "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.
+ */
+async function checkConsoleOutputForWarningGroup(hud, expectedMessages) {
+ const messages = await findAllMessagesVirtualized(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("▼︎⚠");
+ };
+
+ for (let [i, expectedMessage] of expectedMessages.entries()) {
+ // Refresh the reference to the message, as it may have been scrolled out of existence.
+ const message = await findMessageVirtualizedById({
+ hud,
+ messageId: messages[i].getAttribute("data-message-id"),
+ });
+ info(`Checking "${expectedMessage}"`);
+
+ // Collapsed Warning group
+ if (expectedMessage.startsWith("▶︎⚠")) {
+ is(
+ message.querySelector(".arrow").getAttribute("aria-expanded"),
+ "false",
+ "There's a collapsed arrow"
+ );
+ is(
+ message.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.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)) {
+ ok(
+ message.querySelector(".warning-indent"),
+ "The message has the expected indent"
+ );
+ }
+
+ expectedMessage = expectedMessage.replace("| ", "");
+ } else {
+ is(
+ message.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(
+ () => findErrorMessage(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 ui = toolbox.getCurrentPanel().hud.ui;
+ return waitUntil(() => {
+ return (
+ !ui.networkDataProvider.lazyRequestData.size &&
+ // Make sure that batched request updates are all complete
+ // as they trigger late lazy data requests.
+ !ui.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 (see checkContextSelectorMenuItemAt
+ * for expected properties)
+ */
+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((expectedItem, i) => {
+ checkContextSelectorMenuItemAt(hud, i, expectedItem);
+ });
+}
+
+/**
+ * Check that the evaluation context selector menu has the expected item at the specified index.
+ *
+ * @param {WebConsole} hud
+ * @param {Number} index
+ * @param {Object} expected
+ * @param {String} expected.label: The label of the target
+ * @param {String} expected.tooltip: The tooltip of the target element in the menu
+ * @param {Boolean} expected.checked: if the target should be selected or not
+ * @param {Boolean} expected.separator: if the element is a simple separator
+ */
+function checkContextSelectorMenuItemAt(hud, index, expected) {
+ const el = getContextSelectorItems(hud).at(index);
+
+ if (expected.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, expected.label, `The item has the expected label`);
+ is(elTooltip, expected.tooltip, `Item "${elLabel}" has the expected tooltip`);
+ is(
+ elChecked,
+ expected.checked,
+ `Item "${elLabel}" is ${expected.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/shared-head.js b/devtools/client/webconsole/test/browser/shared-head.js
new file mode 100644
index 0000000000..868a6fccc4
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/shared-head.js
@@ -0,0 +1,514 @@
+/* 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/>. */
+
+/**
+ * Helper methods for finding messages in the virtualized output of the
+ * webconsole. This file can be safely required from other panel test
+ * files.
+ */
+
+"use strict";
+
+/* eslint-disable no-unused-vars */
+
+// Assume that shared-head is always imported before this file
+/* import-globals-from ../../../shared/test/shared-head.js */
+
+/**
+ * Find a message with given messageId in the output, scrolling through the
+ * output from top to bottom in order to make sure the messages are actually
+ * rendered.
+ *
+ * @param object hud
+ * The web console.
+ * @param messageId
+ * A message ID to look for. This could be baked into the selector, but
+ * is provided as a convenience.
+ * @return {Node} the node corresponding the found message
+ */
+async function findMessageVirtualizedById({ hud, messageId }) {
+ if (!messageId) {
+ throw new Error("messageId parameter is required");
+ }
+
+ const elements = await findMessagesVirtualized({
+ hud,
+ expectedCount: 1,
+ messageId,
+ });
+ return elements.at(-1);
+}
+
+/**
+ * Find the last message with given message type in the output, scrolling
+ * through the output from top to bottom in order to make sure the messages are
+ * actually rendered.
+ *
+ * @param object hud
+ * The web console.
+ * @param string text
+ * A substring that can be found in the message.
+ * @param string typeSelector
+ * A part of selector for the message, to specify the message type.
+ * @return {Node} the node corresponding the found message
+ */
+async function findMessageVirtualizedByType({ hud, text, typeSelector }) {
+ const elements = await findMessagesVirtualizedByType({
+ hud,
+ text,
+ typeSelector,
+ expectedCount: 1,
+ });
+ return elements.at(-1);
+}
+
+/**
+ * Find all messages in the output, scrolling through the output from top
+ * to bottom in order to make sure the messages are actually rendered.
+ *
+ * @param object hud
+ * The web console.
+ * @return {Array} all of the message nodes in the console output. Some of
+ * these may be stale from having been scrolled out of view.
+ */
+async function findAllMessagesVirtualized(hud) {
+ return findMessagesVirtualized({ hud });
+}
+
+// This is just a reentrancy guard. Because findMessagesVirtualized mucks
+// around with the scroll position, if we do something like
+// let promise1 = findMessagesVirtualized(...);
+// let promise2 = findMessagesVirtualized(...);
+// await promise1;
+// await promise2;
+// then the two calls will end up messing up each other's expected scroll
+// position, at which point they could get stuck. This lets us throw an
+// error when that happens.
+let gInFindMessagesVirtualized = false;
+// And this lets us get a little more information in the error - it just holds
+// the stack of the prior call.
+let gFindMessagesVirtualizedStack = null;
+
+/**
+ * Find multiple messages in the output, scrolling through the output from top
+ * to bottom in order to make sure the messages are actually rendered.
+ *
+ * @param object options
+ * @param object options.hud
+ * The web console.
+ * @param options.text [optional]
+ * A substring that can be found in the message.
+ * @param options.typeSelector
+ * A part of selector for the message, to specify the message type.
+ * @param options.expectedCount [optional]
+ * The number of messages to get. This lets us stop scrolling early if
+ * we find that number of messages.
+ * @return {Array} all of the message nodes in the console output matching the
+ * provided filters. If expectedCount is greater than 1, or equal to -1,
+ * some of these may be stale from having been scrolled out of view.
+ */
+async function findMessagesVirtualizedByType({
+ hud,
+ text,
+ typeSelector,
+ expectedCount,
+}) {
+ if (!typeSelector) {
+ throw new Error("typeSelector parameter is required");
+ }
+ if (!typeSelector.startsWith(".")) {
+ throw new Error("typeSelector should start with a dot e.g. `.result`");
+ }
+
+ return findMessagesVirtualized({
+ hud,
+ text,
+ selector: ".message" + typeSelector,
+ expectedCount,
+ });
+}
+
+/**
+ * Find multiple messages in the output, scrolling through the output from top
+ * to bottom in order to make sure the messages are actually rendered.
+ *
+ * @param object options
+ * @param object options.hud
+ * The web console.
+ * @param options.text [optional]
+ * A substring that can be found in the message.
+ * @param options.selector [optional]
+ * The selector to use in finding the message.
+ * @param options.expectedCount [optional]
+ * The number of messages to get. This lets us stop scrolling early if
+ * we find that number of messages.
+ * @param options.messageId [optional]
+ * A message ID to look for. This could be baked into the selector, but
+ * is provided as a convenience.
+ * @return {Array} all of the message nodes in the console output matching the
+ * provided filters. If expectedCount is greater than 1, or equal to -1,
+ * some of these may be stale from having been scrolled out of view.
+ */
+async function findMessagesVirtualized({
+ hud,
+ text,
+ selector,
+ expectedCount,
+ messageId,
+}) {
+ if (text === undefined) {
+ text = "";
+ }
+ if (selector === undefined) {
+ selector = ".message";
+ }
+ if (expectedCount === undefined) {
+ expectedCount = -1;
+ }
+
+ const outputNode = hud.ui.outputNode;
+ const scrollport = outputNode.querySelector(".webconsole-output");
+
+ function getVisibleMessageIds() {
+ return JSON.parse(scrollport.getAttribute("data-visible-messages"));
+ }
+
+ function getVisibleMessageMap() {
+ return new Map(
+ JSON.parse(scrollport.getAttribute("data-visible-messages")).map(
+ (id, i) => [id, i]
+ )
+ );
+ }
+
+ function getMessageIndex(message) {
+ return getVisibleMessageIds().indexOf(
+ message.getAttribute("data-message-id")
+ );
+ }
+
+ function getNextMessageId(prevMessage) {
+ const visible = getVisibleMessageIds();
+ let index = 0;
+ if (prevMessage) {
+ const lastId = prevMessage.getAttribute("data-message-id");
+ index = visible.indexOf(lastId);
+ if (index === -1) {
+ throw new Error(
+ `Tried to get next message ID for message that doesn't exist. Last seen ID: ${lastId}, all visible ids: [${visible.join(
+ ", "
+ )}]`
+ );
+ }
+ }
+ if (index + 1 >= visible.length) {
+ return null;
+ }
+ return visible[index + 1];
+ }
+
+ if (gInFindMessagesVirtualized) {
+ throw new Error(
+ `findMessagesVirtualized was re-entered somehow. This is not allowed. Other stack: [${gFindMessagesVirtualizedStack}]`
+ );
+ }
+ try {
+ gInFindMessagesVirtualized = true;
+ gFindMessagesVirtualizedStack = new Error().stack;
+ // The console output will automatically scroll to the bottom of the
+ // scrollport in certain circumstances. Because we need to scroll the
+ // output to find all messages, we need to disable this. This attribute
+ // controls that.
+ scrollport.setAttribute("disable-autoscroll", "");
+
+ // This array is here purely for debugging purposes. We collect the indices
+ // of every element we see in order to validate that we don't have any gaps
+ // in the list.
+ const allIndices = [];
+
+ const allElements = [];
+ const seenIds = new Set();
+ let lastItem = null;
+ while (true) {
+ if (scrollport.scrollHeight > scrollport.clientHeight) {
+ if (!lastItem && scrollport.scrollTop != 0) {
+ // For simplicity's sake, we always start from the top of the output.
+ scrollport.scrollTop = 0;
+ } else if (!lastItem && scrollport.scrollTop == 0) {
+ // We want to make sure that we actually change the scroll position
+ // here, because we're going to wait for an update below regardless,
+ // just to flush out any changes that may have just happened. If we
+ // don't do this, and there were no changes before this function was
+ // called, then we'll just hang on the promise below.
+ scrollport.scrollTop = 1;
+ } else {
+ // This is the core of the loop. Scroll down to the bottom of the
+ // current scrollport, wait until we see the element after the last
+ // one we've seen, and then harvest the messages that are displayed.
+ scrollport.scrollTop = scrollport.scrollTop + scrollport.clientHeight;
+ }
+
+ // Wait for something to happen in the output before checking for our
+ // expected next message.
+ await new Promise(resolve =>
+ hud.ui.once("lazy-message-list-updated-or-noop", resolve)
+ );
+
+ try {
+ await waitFor(async () => {
+ const nextMessageId = getNextMessageId(lastItem);
+ if (
+ nextMessageId === undefined ||
+ scrollport.querySelector(`[data-message-id="${nextMessageId}"]`)
+ ) {
+ return true;
+ }
+
+ // After a scroll, we typically expect to get an updated list of
+ // elements. However, we have some slack at the top of the list,
+ // because we draw elements above and below the actual scrollport to
+ // avoid white flashes when async scrolling.
+ const scrollTarget = scrollport.scrollTop + scrollport.clientHeight;
+ scrollport.scrollTop = scrollTarget;
+ await new Promise(resolve =>
+ hud.ui.once("lazy-message-list-updated-or-noop", resolve)
+ );
+ return false;
+ });
+ } catch (e) {
+ throw new Error(
+ `Failed waiting for next message ID (${getNextMessageId(
+ lastItem
+ )}) Visible messages: [${[
+ ...scrollport.querySelectorAll(".message"),
+ ].map(el => el.getAttribute("data-message-id"))}]`
+ );
+ }
+ }
+
+ const bottomPlaceholder = scrollport.querySelector(
+ ".lazy-message-list-bottom"
+ );
+ if (!bottomPlaceholder) {
+ // When there are no messages in the output, there is also no
+ // top/bottom placeholder. There's nothing more to do at this point,
+ // so break and return.
+ break;
+ }
+
+ lastItem = bottomPlaceholder.previousSibling;
+
+ // This chunk is just validating that we have no gaps in our output so
+ // far.
+ const indices = [...scrollport.querySelectorAll("[data-message-id]")]
+ .filter(
+ el => el !== scrollport.firstChild && el !== scrollport.lastChild
+ )
+ .map(el => getMessageIndex(el));
+ allIndices.push(...indices);
+ allIndices.sort((lhs, rhs) => lhs - rhs);
+ for (let i = 1; i < allIndices.length; i++) {
+ if (
+ allIndices[i] != allIndices[i - 1] &&
+ allIndices[i] != allIndices[i - 1] + 1
+ ) {
+ throw new Error(
+ `Gap detected in virtualized webconsole output between ${
+ allIndices[i - 1]
+ } and ${allIndices[i]}. Indices: ${allIndices.join(",")}`
+ );
+ }
+ }
+
+ const messages = scrollport.querySelectorAll(selector);
+ const filtered = [...messages].filter(
+ el =>
+ // Core user filters:
+ el.textContent.includes(text) &&
+ (!messageId || el.getAttribute("data-message-id") === messageId) &&
+ // Make sure we don't collect duplicate messages:
+ !seenIds.has(el.getAttribute("data-message-id"))
+ );
+ allElements.push(...filtered);
+ for (const message of filtered) {
+ seenIds.add(message.getAttribute("data-message-id"));
+ }
+
+ if (expectedCount >= 0 && allElements.length >= expectedCount) {
+ break;
+ }
+
+ // If the bottom placeholder has 0 height, it means we've scrolled to the
+ // bottom and output all the items.
+ if (bottomPlaceholder.getBoundingClientRect().height == 0) {
+ break;
+ }
+
+ await waitForTime(0);
+ }
+
+ // Finally, we get the map of message IDs to indices within the output, and
+ // sort the message nodes according to that index. They can come in out of
+ // order for a number of reasons (we continue rendering any messages that
+ // have been expanded, and we always render the topmost and bottommost
+ // messages for a11y reasons.)
+ const idsToIndices = getVisibleMessageMap();
+ allElements.sort(
+ (lhs, rhs) =>
+ idsToIndices.get(lhs.getAttribute("data-message-id")) -
+ idsToIndices.get(rhs.getAttribute("data-message-id"))
+ );
+ return allElements;
+ } finally {
+ scrollport.removeAttribute("disable-autoscroll");
+ gInFindMessagesVirtualized = false;
+ gFindMessagesVirtualizedStack = null;
+ }
+}
+
+/**
+ * Find the last message with given message type in the output.
+ *
+ * @param object hud
+ * The web console.
+ * @param string text
+ * A substring that can be found in the message.
+ * @param string typeSelector
+ * A part of selector for the message, to specify the message type.
+ * @return {Node} the node corresponding the found message, otherwise undefined
+ */
+function findMessageByType(hud, text, typeSelector) {
+ const elements = findMessagesByType(hud, text, typeSelector);
+ return elements.at(-1);
+}
+
+/**
+ * Find multiple messages with given message type in the output.
+ *
+ * @param object hud
+ * The web console.
+ * @param string text
+ * A substring that can be found in the message.
+ * @param string typeSelector
+ * A part of selector for the message, to specify the message type.
+ * @return {Array} The nodes corresponding the found messages
+ */
+function findMessagesByType(hud, text, typeSelector) {
+ if (!typeSelector) {
+ throw new Error("typeSelector parameter is required");
+ }
+ if (!typeSelector.startsWith(".")) {
+ throw new Error("typeSelector should start with a dot e.g. `.result`");
+ }
+
+ const selector = ".message" + typeSelector;
+ const messages = hud.ui.outputNode.querySelectorAll(selector);
+ const elements = Array.from(messages).filter(el =>
+ el.textContent.includes(text)
+ );
+ return elements;
+}
+
+/**
+ * Find all messages in the output.
+ *
+ * @param object hud
+ * The web console.
+ * @return {Array} The nodes corresponding the found messages
+ */
+function findAllMessages(hud) {
+ const messages = hud.ui.outputNode.querySelectorAll(".message");
+ return Array.from(messages);
+}
+
+/**
+ * Find a part of the last message with given message type in the output.
+ *
+ * @param object hud
+ * The web console.
+ * @param object options
+ * - text : {String} A substring that can be found in the message.
+ * - typeSelector: {String} A part of selector for the message,
+ * to specify the message type.
+ * - partSelector: {String} A selector for the part of the message.
+ * @return {Node} the node corresponding the found part, otherwise undefined
+ */
+function findMessagePartByType(hud, options) {
+ const elements = findMessagePartsByType(hud, options);
+ return elements.at(-1);
+}
+
+/**
+ * Find parts of multiple messages with given message type in the output.
+ *
+ * @param object hud
+ * The web console.
+ * @param object options
+ * - text : {String} A substring that can be found in the message.
+ * - typeSelector: {String} A part of selector for the message,
+ * to specify the message type.
+ * - partSelector: {String} A selector for the part of the message.
+ * @return {Array} The nodes corresponding the found parts
+ */
+function findMessagePartsByType(hud, { text, typeSelector, partSelector }) {
+ if (!typeSelector) {
+ throw new Error("typeSelector parameter is required");
+ }
+ if (!typeSelector.startsWith(".")) {
+ throw new Error("typeSelector should start with a dot e.g. `.result`");
+ }
+ if (!partSelector) {
+ throw new Error("partSelector parameter is required");
+ }
+
+ const selector = ".message" + typeSelector + " " + partSelector;
+ const parts = hud.ui.outputNode.querySelectorAll(selector);
+ const elements = Array.from(parts).filter(el =>
+ el.textContent.includes(text)
+ );
+ return elements;
+}
+
+/**
+ * Type-specific wrappers for findMessageByType and findMessagesByType.
+ *
+ * @param object hud
+ * The web console.
+ * @param string text
+ * A substring that can be found in the message.
+ * @param string extraSelector [optional]
+ * An extra part of selector for the message, in addition to
+ * type-specific selector.
+ * @return {Node|Array} See findMessageByType or findMessagesByType.
+ */
+function findEvaluationResultMessage(hud, text, extraSelector = "") {
+ return findMessageByType(hud, text, ".result" + extraSelector);
+}
+function findEvaluationResultMessages(hud, text, extraSelector = "") {
+ return findMessagesByType(hud, text, ".result" + extraSelector);
+}
+function findErrorMessage(hud, text, extraSelector = "") {
+ return findMessageByType(hud, text, ".error" + extraSelector);
+}
+function findErrorMessages(hud, text, extraSelector = "") {
+ return findMessagesByType(hud, text, ".error" + extraSelector);
+}
+function findWarningMessage(hud, text, extraSelector = "") {
+ return findMessageByType(hud, text, ".warn" + extraSelector);
+}
+function findWarningMessages(hud, text, extraSelector = "") {
+ return findMessagesByType(hud, text, ".warn" + extraSelector);
+}
+function findConsoleAPIMessage(hud, text, extraSelector = "") {
+ return findMessageByType(hud, text, ".console-api" + extraSelector);
+}
+function findConsoleAPIMessages(hud, text, extraSelector = "") {
+ return findMessagesByType(hud, text, ".console-api" + extraSelector);
+}
+function findNetworkMessage(hud, text, extraSelector = "") {
+ return findMessageByType(hud, text, ".network" + extraSelector);
+}
+function findNetworkMessages(hud, text, extraSelector = "") {
+ return findMessagesByType(hud, text, ".network" + extraSelector);
+}
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..9c2eb76b5e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/sjs_cors-test-server.sjs
@@ -0,0 +1,165 @@
+/* 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..d7b85efad4
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/sjs_slow-response-test-server.sjs
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+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..1159053f49
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/stub-generator-helpers.js
@@ -0,0 +1,437 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+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 createCommandsForTab(tab) {
+ const {
+ CommandsFactory,
+ } = require("devtools/shared/commands/commands-factory");
+ const commands = await CommandsFactory.forTab(tab);
+ return commands;
+}
+
+async function createCommandsForMainProcess() {
+ const {
+ CommandsFactory,
+ } = require("devtools/shared/commands/commands-factory");
+ const commands = await CommandsFactory.forMainProcess();
+ return commands;
+}
+
+// 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 (generated id, 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.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) {
+ // `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.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.eventActor) {
+ // Clean startedDateTime on network messages.
+ res.eventActor.startedDateTime = existingPacket.startedDateTime;
+ }
+
+ if (res.pageError) {
+ // Clean innerWindowID on pageError messages.
+ res.pageError.innerWindowID = existingPacket.pageError.innerWindowID;
+
+ 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];
+ // We're replacing sourceId here even if the property in frame is null to avoid
+ // a frequent intermittent. The sourceId is retrieved from the Debugger#findSources
+ // API, which is not deterministic (See https://searchfox.org/mozilla-central/rev/b172dd415c475e8b2899560e6005b3a953bead2a/js/src/doc/Debugger/Debugger.md#367-375)
+ // This should be fixed in Bug 1717037.
+ if (frame && existingFrame && "sourceId" in frame) {
+ 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;
+ }
+
+ // waitingTime can be very small and rounded to 0. However this is still a
+ // valid waiting time, so check isNaN instead of a simple truthy check.
+ if (!isNaN(res.waitingTime) && existingPacket.waitingTime) {
+ res.waitingTime = existingPacket.waitingTime;
+ }
+
+ 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;
+ }
+}
+
+/**
+ * Write stubs to a given file
+ *
+ * @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(fileName, packets, isNetworkMessage) {
+ const mozRepo = Services.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. SEE devtools/client/webconsole/test/README.md.
+ */
+
+const {
+ parsePacketsWithFronts,
+} = require("chrome://mochitests/content/browser/devtools/client/webconsole/test/browser/stub-generator-helpers.js");
+const { prepareMessage } = require("resource://devtools/client/webconsole/utils/messages.js");
+const {
+ ConsoleMessage,
+ NetworkEventMessage,
+} = require("resource://devtools/client/webconsole/types.js");
+
+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 IOUtils.write(filePath, new TextEncoder().encode(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} options
+ * @param {Boolean} options.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.
+ * @param {Boolean} options.replaceActorIds
+ * Pass true to replace actorIDs with a fake one so it's easier to compare stubs
+ * that includes grips.
+ */
+function getSerializedPacket(
+ packet,
+ { sortKeys = false, replaceActorIds = false } = {}
+) {
+ if (sortKeys) {
+ packet = sortObjectKeys(packet);
+ }
+
+ const actorIdPlaceholder = "XXX";
+
+ return JSON.stringify(
+ packet,
+ function (key, value) {
+ // The message can have fronts that we need to serialize
+ if (value && value._grip) {
+ return {
+ _grip: value._grip,
+ actorID: replaceActorIds ? actorIdPlaceholder : value.actorID,
+ };
+ }
+
+ if (
+ replaceActorIds &&
+ (key === "actor" || key === "actorID" || key === "sourceId") &&
+ typeof value === "string"
+ ) {
+ return actorIdPlaceholder;
+ }
+
+ if (key === "resourceId") {
+ return undefined;
+ }
+
+ 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,
+ createCommandsForTab,
+ createCommandsForMainProcess,
+ 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..5db64afd84
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-autocomplete-in-stackframe.html
@@ -0,0 +1,59 @@
+<!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 shadowed = Object.assign(Object.create(null), {
+ foo: true
+ });
+
+ var foo1Obj = Object.assign(Object.create(null), {
+ prop1: "111",
+ prop2: {
+ prop21: "212121"
+ },
+ method() {
+ debugger;
+ }
+ });
+
+ 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 shadowed = Object.assign(Object.create(null), {
+ bar: true
+ });
+
+
+ var foo3Obj = Object.assign(Object.create(null), {
+ prop1: Object.assign(Object.create(null), {
+ prop11: "313131"
+ })
+ });
+
+ foo1Obj.method();
+ }
+ </script>
+ </head>
+ <body>
+ <p>Hello world!</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/browser/test-autocomplete-mapped.html b/devtools/client/webconsole/test/browser/test-autocomplete-mapped.html
new file mode 100644
index 0000000000..cae1784969
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-autocomplete-mapped.html
@@ -0,0 +1,15 @@
+<!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 autocomplete displaying mapped variable names</title>
+ <script src="test-autocomplete-mapped.js"></script>
+ </head>
+ <body>
+ <p>Hello world!</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/browser/test-autocomplete-mapped.js b/devtools/client/webconsole/test/browser/test-autocomplete-mapped.js
new file mode 100644
index 0000000000..33762c03a4
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-autocomplete-mapped.js
@@ -0,0 +1,18 @@
+"use strict";
+const i = { x: { y: { importResult: true } } };
+const j = { get x() { return blackbox({ get y() { return blackbox({ getterResult: 1 }); } }); } };
+
+const blackbox = x=>[x].pop();
+
+function firstCall() {
+ const t = 42;
+ const u = i.x.y;
+ const v = j.x.y.getterResult;
+ const o = {
+ get value() {
+ return blackbox(Promise.resolve());
+ }
+ };
+ debugger;
+}
+//# sourceMappingURL=test-autocomplete-mapped.js.map
diff --git a/devtools/client/webconsole/test/browser/test-autocomplete-mapped.js.map b/devtools/client/webconsole/test/browser/test-autocomplete-mapped.js.map
new file mode 100644
index 0000000000..9f71c8f530
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-autocomplete-mapped.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["test-autocomplete-mapped.src.js"],"names":["blackbox","x","pop","firstCall","value","imported","getter","localWithGetter","Promise","resolve"],"mappings":"AAAA;AACA,MAAS,CAAQ;AACjB,MAAS,CAAM;;AAEf,MAAMA,WAAWC,GAAK,CAACA,GAAGC;;AAE1B,SAASC;EACP,MAAMC,IAAQ;EACd,MAAMC,IAAO,KAAQ;EACrB,MAAMC,IAAQ,kBAAM;EACpB,MAAMC,IAAkB;IACtBH;MAAc,OAAOJ,SAASQ,QAAQC;;;EAGxC","file":"test-autocomplete-mapped.js"} \ No newline at end of file
diff --git a/devtools/client/webconsole/test/browser/test-autocomplete-mapped.src.js b/devtools/client/webconsole/test/browser/test-autocomplete-mapped.src.js
new file mode 100644
index 0000000000..24085c63c5
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-autocomplete-mapped.src.js
@@ -0,0 +1,16 @@
+"use strict";
+import { imported } from "somewhere";
+import { getter } from "somewhere-else";
+
+const blackbox = x => [x].pop();
+
+function firstCall() {
+ const value = 42;
+ const temp = imported;
+ const temp2 = getter;
+ const localWithGetter = {
+ get value() { return blackbox(Promise.resolve()); }
+ };
+ const unmapped = 100;
+ debugger;
+}
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-blank.html b/devtools/client/webconsole/test/browser/test-blank.html
new file mode 100644
index 0000000000..367ce6c804
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-blank.html
@@ -0,0 +1,10 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Blank</title>
+</head>
+<body>
+</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..c73390bae3
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-click-function-to-source.js
@@ -0,0 +1,12 @@
+// prettier-ignore
+
+/**
+ * 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..7516de7e8f
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-click-function-to-source.min.js
@@ -0,0 +1,3 @@
+// prettier-ignore
+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..e06d1a4dba
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-click-function-to-source.unmapped.min.js
@@ -0,0 +1,2 @@
+// prettier-ignore
+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..dae3fcae89
--- /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,<!DOCTYPE html>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-custom-formatters-errors.html b/devtools/client/webconsole/test/browser/test-console-custom-formatters-errors.html
new file mode 100644
index 0000000000..a186bb338a
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-console-custom-formatters-errors.html
@@ -0,0 +1,185 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8"/>
+ <title>Webconsole erroneous custom formatters test page</title>
+ </head>
+ <body>
+ <p>Erroneous custom formatters test page</p>
+ <script>
+ "use strict";
+
+ window.devtoolsFormatters = [
+ {
+ // this header is invalid because it is not a function
+ header: 1,
+ },
+ {
+ // this header is invalid because it doesn't return JsonML
+ header: () => 1,
+ },
+ {
+ // this header is invalid because the returned array misses an element type
+ header: () => [],
+ },
+ {
+ // this header is invalid because it throws an exception
+ header: () => { throw new Error("ERROR"); },
+ },
+ {
+ header: (obj) => {
+ return obj.hasOwnProperty("hasBodyNotAFunction") ?
+ ["div", "hasBody not a function"] :
+ null;
+ },
+ // this hasBody is invalid because it is not a function
+ hasBody: 1,
+ },
+ {
+ header: (obj) => {
+ return obj.hasOwnProperty("hasBodyThrows") ?
+ ["div", "hasBody throws"] :
+ null;
+ },
+ // this hasBody throws an exception
+ hasBody: () => { throw new Error("ERROR"); },
+ },
+ {
+ header: (obj) => {
+ return obj.hasOwnProperty("bodyNotAFunction") ?
+ ["div", "body not a function"] :
+ null;
+ },
+ hasBody: () => true,
+ // this body is invalid because it is not a function
+ body: 1,
+ },
+ {
+ header: (obj) => {
+ return obj.hasOwnProperty("bodyReturnsNull") ?
+ ["div", "body returns null"] :
+ null;
+ },
+ hasBody: () => true,
+ // this body is invalid because it doesn't return JsonML
+ body: () => null,
+ },
+ {
+ header: (obj) => {
+ return obj.hasOwnProperty("bodyNoJsonMl") ?
+ ["div", "body doesn't return JsonML"] :
+ null;
+ },
+ hasBody: () => true,
+ // this body is invalid because it doesn't return JsonML
+ body: () => 1,
+ },
+ {
+ header: (obj) => {
+ return obj.hasOwnProperty("bodyNoElementType") ?
+ ["div", "body array misses element type"] :
+ null;
+ },
+ hasBody: () => true,
+ // this body is invalid because the returned array misses an element type
+ body: () => [],
+ },
+ {
+ header: (obj) => {
+ return obj.hasOwnProperty("bodyThrows") ?
+ ["div", "body throws"] :
+ null;
+ },
+ hasBody: () => true,
+ // this body is invalid because it throws an exception
+ body: () => { throw new Error("ERROR"); },
+ },
+ {
+ header: (obj) => {
+ if (obj?.hasOwnProperty("objectTagWithNoAttribute")) {
+ // This is invalid because "object" tag should have attributes
+ return ["object"];
+ }
+ return null;
+ },
+ },
+ {
+ header: (obj) => {
+ if (obj?.hasOwnProperty("objectTagWithoutObjectAttribute")) {
+ // This is invalid because "object" tag should have an "object" attribute
+ return [
+ "object",
+ { config: "something" }
+ ];
+ }
+ return null;
+ },
+ },
+ {
+ header: (obj) => {
+ if (obj?.hasOwnProperty("infiniteObjectTag")) {
+ // This is invalid because this triggers an infinite loop (object is being
+ // replaced by itself, which will keep triggering the custom formatter hook)
+ return [ "object", { object: obj }];
+ }
+ return null;
+ },
+ },
+ {
+ // the returned value is invalid because the tagName isn't a string
+ header: (obj) => {
+ if (obj?.hasOwnProperty("invalidTag")) {
+ return [ 42 ];
+ }
+ return null;
+ },
+ },
+ {
+ header: (obj) => {
+ if (obj.hasOwnProperty("customFormatHeader")) {
+ return ["span", {"style": "font-size: 3rem;"}, "custom formatted header"];
+ }
+ return null;
+ },
+ hasBody: (obj) => false
+ },
+ {
+ header: (obj) => {
+ if (obj.hasOwnProperty("customFormatHeaderAndBody")) {
+ return ["span", {"style": "font-style: italic;"}, "custom formatted body"];
+ }
+ return null;
+ },
+ hasBody: (obj) => true,
+ body: (obj) => ["span", {"style": "font-family: serif; font-size: 2rem;"}, obj.customFormatHeaderAndBody]
+ },
+ {
+ header: (obj) => {
+ if (obj.hasOwnProperty("privileged")) {
+ // This should throw as the hooks should not have privileged access
+ window.windowUtils.garbageCollect();
+ return ["span", {}, "privileged"];
+ }
+ return null;
+ },
+ },
+ ];
+
+ [
+ {},
+ {hasBodyNotAFunction: true},
+ {hasBodyThrows: true},
+ {bodyNotAFunction: true},
+ {bodyReturnsNull: true},
+ {bodyNoJsonMl: true},
+ {bodyNoElementType: true},
+ {bodyThrows: true},
+ {objectTagWithNoAttribute: true},
+ {objectTagWithoutObjectAttribute: true},
+ {infiniteObjectTag: true},
+ {invalidTag: true},
+ {privileged: true},
+ ].forEach(variable => console.log(variable));
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/browser/test-console-custom-formatters.html b/devtools/client/webconsole/test/browser/test-console-custom-formatters.html
new file mode 100644
index 0000000000..edf3f50887
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-console-custom-formatters.html
@@ -0,0 +1,112 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8"/>
+ <title>Webconsole custom formatters test page</title>
+ </head>
+ <body>
+ <p>Custom formatters test page</p>
+ <script>
+ "use strict";
+
+ const variables = [
+ "string",
+ 1337,
+ { noFormat: true },
+ { customFormatHeader: "header" },
+ { customFormatHeaderAndBody: "body" },
+ { customFormatObjectAndConfig: true }
+ ];
+
+ window.devtoolsFormatters = [
+ {
+ header: (obj, config) => {
+ if (obj.hasOwnProperty("customFormatHeader")) {
+ return [
+ "span",
+ {"style": "font-size: 3rem;"},
+ config ? `~${JSON.stringify(config)}~` : "custom formatted header",
+ ];
+ }
+ return null;
+ },
+ hasBody: obj => false
+ },
+ {
+ header: obj => {
+ if (obj.hasOwnProperty("customFormatHeaderAndBody")) {
+ return ["span", {"style": "font-style: italic;"}, "custom formatted body"];
+ }
+ return null;
+ },
+ hasBody: obj => true,
+ body: obj => ["span", {"style": "font-family: serif; font-size: 2rem;"}, obj.customFormatHeaderAndBody]
+ },
+ {
+ header: (obj, config) => {
+ if (obj.hasOwnProperty("customFormatObjectAndConfig")) {
+ return [
+ "span",
+ {"style": "color: purple;"},
+ `object tag`,
+ [
+ "object",
+ {
+ // This will trigger the "customFormatHeader" custom formatter
+ object: {customFormatHeader: true},
+ config: config || [1, "a"]
+ }
+ ],
+ // This should print the `config` object, not formatted
+ [
+ "object",
+ {
+ object: config || null,
+ }
+ ],
+ [
+ "span",
+ " | serialized: ",
+ 42n,
+ " ",
+ undefined,
+ " ",
+ null,
+ " ",
+ Infinity,
+ " ",
+ {foo: "bar"}
+ ]
+ ];
+ }
+ return null;
+ },
+ hasBody: (obj, config) => obj.hasOwnProperty("customFormatObjectAndConfig") || !!config,
+ body: (obj, config) => {
+ if (!config) {
+ config = [1, "a"];
+ }
+ return [
+ "span",
+ {"style": "font-family: serif; font-size: 2rem;"},
+ "body",
+ [
+ "object",
+ {
+ object: {
+ customFormatObjectAndConfig: true,
+ },
+ config: [
+ config[0] + 1,
+ String.fromCharCode(config[1].charCodeAt(0) + 1)
+ ]
+ }
+ ]
+ ]}
+ }
+ ];
+
+ variables.forEach(variable => console.log(variable));
+ </script>
+ </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..c5e3138100
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-console-evaluation-context-selector.html
@@ -0,0 +1,16 @@
+<!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);
+ globalThis.foobar = "hello";
+ globalThis.foobaz = "world";
+ </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..3ddccc0f61
--- /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-logs-exceptions-order.html b/devtools/client/webconsole/test/browser/test-console-logs-exceptions-order.html
new file mode 100644
index 0000000000..e51df0b38d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-console-logs-exceptions-order.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Webconsole order test test page</title>
+ </head>
+ <body>
+ <button>dispatched event target</button>
+ <script>
+ "use strict";
+ const btn = document.querySelector("button");
+ btn.addEventListener("click", () => {
+ console.log("First");
+ // Don't throw an error as its stacktrace (whose rendering is delayed)
+ // might show up in the console message body and mess with the test.
+ // eslint-disable-next-line no-throw-literal
+ throw "Second";
+ });
+
+ // Use dispatchEvent as the event listener callback will be called directly,
+ // before the next lines are executed, which gives us a higher chance of
+ // having all the messages being emitted within the same millisecond.
+ btn.dispatchEvent(new MouseEvent("click"));
+
+ console.log("Third");
+ // Don't throw an error as its stacktrace (whose rendering is delayed)
+ // might show up in the console message body and mess with the test.
+ // eslint-disable-next-line no-throw-literal
+ throw "Fourth";
+ </script>
+ </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..273512e992
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-console.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Simple webconsole test page</title>
+ <style>
+ p {
+ color: bled;
+ }
+ </style>
+ </head>
+ <body>
+ <p>Simple webconsole test page</p>
+ <script>
+ /* exported doLogs, stringLog, throwError */
+ "use strict";
+
+ function doLogs(num) {
+ num = num || 1;
+ for (let i = 0; i < num; i++) {
+ console.log(i);
+ }
+ }
+
+ function stringLog() {
+ console.log("stringLog");
+ }
+
+ function throwError(errorMessage) {
+ setTimeout(() => {
+ throw errorMessage;
+ }, 0);
+ }
+ </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-event-handler.html b/devtools/client/webconsole/test/browser/test-csp-violation-event-handler.html
new file mode 100644
index 0000000000..49ab77a0c8
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-csp-violation-event-handler.html
@@ -0,0 +1,8 @@
+ <html>
+ <head>
+ <title>CSP Inline Event Handlers Violations Test</title>
+ </head>
+ <body onload="document.body.textContent = 'JavaScript executed!';">
+ JavaScript should not execute.
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/browser/test-csp-violation-event-handler.html^headers^ b/devtools/client/webconsole/test/browser/test-csp-violation-event-handler.html^headers^
new file mode 100644
index 0000000000..add0ff8e89
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-csp-violation-event-handler.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: script-src '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-data.json b/devtools/client/webconsole/test/browser/test-data.json
new file mode 100644
index 0000000000..797ab7a9dc
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-data.json
@@ -0,0 +1 @@
+{ "id": "test JSON data", "myArray": ["foo", "bar", "baz", "biff"] }
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.mjs b/devtools/client/webconsole/test/browser/test-dynamic-import.mjs
new file mode 100644
index 0000000000..bb3504a8e8
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-dynamic-import.mjs
@@ -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..4c66036304
--- /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.mjs")
+).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.mjs b/devtools/client/webconsole/test/browser/test-error-worklet.mjs
new file mode 100644
index 0000000000..cca6667d19
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-error-worklet.mjs
@@ -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..fbc6fae346
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-eval-in-stackframe.html
@@ -0,0 +1,53 @@
+<!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;
+ }
+
+ class Foo {
+ x = 1;
+ #privateProp = "privatePropValue";
+ static #privateStatic = { first: "a", second: "b" };
+ #privateMethod() {
+ return this.#privateProp;
+ }
+ breakFn() {
+ let i = this.x * this.#privateProp + Foo.#privateStatic;
+ 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..e386d91ce9
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-external-script-errors.js
@@ -0,0 +1,7 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+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..d0dd66def5
--- /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="https://example.org/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..021b581849
--- /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="https://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..202199eeb8
--- /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", Date.now());
+ </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..c3977525ab
--- /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="https://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..7abae90cc9
--- /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..94e445d11c
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-message-categories-canvas-css.js
@@ -0,0 +1,8 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+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..353ab4b2a3
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-message-categories-empty-getelementbyid.js
@@ -0,0 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+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..eb727c7321
--- /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-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..a929c5c34e
--- /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', 'https://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..77132f3a61
--- /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("https://example.com/browser/devtools/client/webconsole/test/browser/test-non-javascript-mime.js");
+
+ // Test importScripts
+ const source = `importScripts("https://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..398f354c42
--- /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..a506631cfb
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-sourcemap-original.js
@@ -0,0 +1,18 @@
+// prettier-ignore
+
+/**
+ * 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..9d4808ec6b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-sourcemap.min.js
@@ -0,0 +1,3 @@
+// prettier-ignore
+function logString(str){console.log(str)}function logTrace(){var logTraceInner=function(){console.trace()};logTraceInner()}
+//# sourceMappingURL=test-sourcemap.min.js.map
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-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-group-csp.html b/devtools/client/webconsole/test/browser/test-warning-group-csp.html
new file mode 100644
index 0000000000..f7a446fb3b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-warning-group-csp.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>CSP warning group</title>
+</head>
+<body>
+<h1>Look at the Content-Security-Policy header</h1>
+<pre>Content-Security-Policy: script-src 'strict-dynamic' http: https: 'unsafe-inline';</pre>
+</body>
+</html>
diff --git a/devtools/client/webconsole/test/browser/test-warning-group-csp.html^headers^ b/devtools/client/webconsole/test/browser/test-warning-group-csp.html^headers^
new file mode 100644
index 0000000000..719213a279
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-warning-group-csp.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: script-src 'strict-dynamic' http: https: 'unsafe-inline'; \ No newline at end of file
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..d84202dadf
--- /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("wss://0.0.0.0:81");
+ ws1.onopen = function () {
+ ws1.send("test 1");
+ ws1.close();
+ };
+
+ const ws2 = new window.frames[0].WebSocket("wss://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-worker.js b/devtools/client/webconsole/test/browser/test-worker.js
new file mode 100644
index 0000000000..76e7934230
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-worker.js
@@ -0,0 +1,13 @@
+"use strict";
+
+self.addEventListener("message", function onMessage(event) {
+ const { type, message } = event.data;
+
+ switch (type) {
+ case "log":
+ console.log(message);
+ break;
+ case "error":
+ throw new Error(message);
+ }
+});
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..e6e3231921
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test_hsts-invalid-headers.sjs
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+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..6af9104fb8
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test_jsterm_screenshot_command.html
@@ -0,0 +1,87 @@
+<html>
+ <head>
+ <meta charset="utf8">
+ <style>
+ html, body {
+ margin: 0;
+ padding: 0;
+ }
+
+ body {
+ --fixed-header-height: 50px;
+ margin-top: var(--fixed-header-height);
+ }
+
+ header {
+ height: var(--fixed-header-height);
+ background: rgb(255, 0, 0);
+ position: fixed;
+ left: 0;
+ top: 0;
+ right: 0;
+ /* Since we may check the background-color, put the text in the center so we don't pick a pixel from the text */
+ text-align: center;
+ }
+
+ img {
+ height: 100px;
+ width: 100px;
+ }
+
+ iframe {
+ display: block;
+ height: 50px;
+ border: none;
+ }
+
+ .overflow {
+ overflow: scroll;
+ height: 200vh;
+ width: 200vw;
+ }
+ </style>
+ </head>
+ <body>
+ <header>Fixed header</header>
+ <iframe id="same-origin-iframe" data-bg-color="rgb(255, 255, 0)"></iframe>
+ <iframe id="remote-iframe" data-bg-color="rgb(0, 255, 255)"></iframe>
+ <img id="testImage"></img>
+ <script>
+ "use strict";
+
+ async function loadIframe(iframeEl, origin) {
+ const onIframeLoaded = new Promise(resolve => {
+ iframeEl.addEventListener("load", resolve, {once: true})
+ });
+ const bgColor = iframeEl.getAttribute("data-bg-color");
+ iframeEl.src =
+ `${origin}/document-builder.sjs?html=
+ <style>
+ html {
+ background:${bgColor};
+ text-align: center;
+ }
+
+ span {
+ background-color: rgb(0, 100, 0);
+ /* move the text to right so we can check the span background color from test */
+ padding-left: 10px;
+ }
+ </style>
+ <span>${origin}</span>`;
+ await onIframeLoaded;
+ iframeEl.classList.add("loaded-iframe");
+ }
+
+ const origin = document.location.origin;
+ // Since we can't know on which origin the document is loaded, we check it so we
+ // can pick another one for the remote iframe.
+ const remoteOrigin = origin.endsWith(".com")
+ ? origin.replace(".com", ".org")
+ : origin.replace(".org", ".com");
+
+ loadIframe(document.getElementById("same-origin-iframe"), origin);
+ loadIframe(document.getElementById("remote-iframe"), remoteOrigin);
+ </script>
+ </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");